aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2024-03-06 09:29:56 -0800
committerXin Li <delphij@google.com>2024-03-06 09:29:56 -0800
commit646563934a3e2ee26f50171f94d95173a1662e2c (patch)
treec1643be8ab17fc607cea748a8bb1d621a5964873
parentb1463ed3bc39c55c2a9496b8332940473fc3c023 (diff)
parent28d03a2a1cabbe01d7bcb6cf5166c10e50d3c2c6 (diff)
downloadpigweed-master.tar.gz
Merge Android 14 QPR2 to AOSP mainHEADmastermain
Bug: 319669529 Merged-In: Icdf552029fb97a34e83c6dd7799433fc473a2506 Change-Id: I193fcbb5c284df7b04c8b6f860a61ffb6f521d37
-rw-r--r--.bazelrc78
-rw-r--r--.eslintrc.cjs39
-rw-r--r--.eslintrc.json48
-rw-r--r--.gitignore15
-rw-r--r--.gn38
-rw-r--r--.prettierignore1
-rw-r--r--.prettierrc.cjs5
-rw-r--r--.vscode/pw_project_extensions.json8
-rw-r--r--BUILD.bazel8
-rw-r--r--BUILD.gn362
-rw-r--r--CMakeLists.txt42
-rw-r--r--Kconfig.zephyr5
-rw-r--r--PIGWEED_MODULES20
-rw-r--r--PW_PLUGINS23
-rw-r--r--README.md2
-rw-r--r--WORKSPACE347
-rw-r--r--bootstrap.bat9
-rw-r--r--bootstrap.sh14
-rw-r--r--docker/docs.rst28
-rw-r--r--docs/BUILD.gn124
-rw-r--r--docs/Doxyfile7
-rw-r--r--docs/_static/css/pigweed.css114
-rw-r--r--docs/_static/js/pigweed.js75
-rw-r--r--docs/blog/01-kudzu.rst256
-rw-r--r--docs/blog/index.rst12
-rw-r--r--docs/build_system.rst695
-rw-r--r--docs/changelog.rst2741
-rw-r--r--docs/code_reviews.rst132
-rw-r--r--docs/concepts/index.rst146
-rw-r--r--docs/conf.py99
-rw-r--r--docs/contributing/index.rst (renamed from docs/contributing.rst)210
-rw-r--r--docs/contributing/module_docs.rst304
-rw-r--r--docs/editors.rst232
-rw-r--r--docs/embedded_cpp_guide.rst88
-rw-r--r--docs/facades.rst153
-rw-r--r--docs/faq.rst11
-rw-r--r--docs/get_started/bazel.rst217
-rw-r--r--docs/get_started/index.rst57
-rw-r--r--docs/get_started/upstream.rst (renamed from docs/getting_started.rst)110
-rw-r--r--docs/glossary.rst52
-rw-r--r--docs/images/pw_env_setup_demo.gifbin477189 -> 0 bytes
-rw-r--r--docs/images/pw_status_test.pngbin80866 -> 0 bytes
-rw-r--r--docs/images/pw_watch_build_demo.gifbin294644 -> 0 bytes
-rw-r--r--docs/images/pw_watch_on_device_demo.gifbin344863 -> 0 bytes
-rw-r--r--docs/images/pw_watch_test_demo.gifbin262146 -> 0 bytes
-rw-r--r--docs/images/pw_watch_test_demo2.gifbin864841 -> 0 bytes
-rw-r--r--docs/images/stm32f429i-disc1_connected.jpgbin118263 -> 0 bytes
-rw-r--r--docs/index.rst141
-rw-r--r--docs/infra/ci_cq_intro.rst376
-rw-r--r--docs/infra/index.rst9
-rw-r--r--docs/infra/rollers.rst74
-rw-r--r--docs/layout/page.html25
-rw-r--r--docs/mission.rst55
-rw-r--r--docs/module_guides.rst6
-rw-r--r--docs/module_structure.rst382
-rw-r--r--docs/os/index.rst (renamed from docs/os_abstraction_layers.rst)18
-rw-r--r--docs/os/zephyr/index.rst47
-rw-r--r--docs/os/zephyr/kconfig.rst12
-rw-r--r--docs/overview.rst138
-rw-r--r--docs/python_build.rst466
-rw-r--r--docs/release_notes/2022_jan.rst192
-rw-r--r--docs/release_notes/index.rst10
-rw-r--r--docs/style/commit_message.rst296
-rw-r--r--docs/style/cpp.rst826
-rw-r--r--docs/style/doxygen.rst253
-rw-r--r--docs/style/sphinx.rst632
-rw-r--r--docs/style_guide.rst1573
-rw-r--r--docs/targets.rst53
-rw-r--r--docs/templates/docs/api.rst13
-rw-r--r--docs/templates/docs/cli.rst11
-rw-r--r--docs/templates/docs/concepts.rst9
-rw-r--r--docs/templates/docs/design.rst14
-rw-r--r--docs/templates/docs/docs.rst117
-rw-r--r--docs/templates/docs/gui.rst9
-rw-r--r--docs/templates/docs/guides.rst15
-rw-r--r--docs/templates/docs/tutorials/index.rst13
-rw-r--r--jest.config.ts22
-rw-r--r--kudzu/BUILD.gn (renamed from pw_arduino_build/py/setup.py)10
-rw-r--r--kudzu/docs.rst17
-rw-r--r--package-lock.json5435
-rw-r--r--package.json18
-rw-r--r--pigweed.json96
-rw-r--r--pw_alignment/docs.rst15
-rw-r--r--pw_alignment/public/pw_alignment/alignment.h17
-rw-r--r--pw_allocator/Android.bp53
-rw-r--r--pw_allocator/BUILD.bazel230
-rw-r--r--pw_allocator/BUILD.gn193
-rw-r--r--pw_allocator/CMakeLists.txt205
-rw-r--r--pw_allocator/allocator.cc43
-rw-r--r--pw_allocator/allocator_metric_proxy.cc78
-rw-r--r--pw_allocator/allocator_metric_proxy_test.cc175
-rw-r--r--pw_allocator/allocator_test.cc166
-rw-r--r--pw_allocator/allocator_testing.cc81
-rw-r--r--pw_allocator/block.cc259
-rw-r--r--pw_allocator/block_test.cc1195
-rw-r--r--pw_allocator/docs.rst81
-rw-r--r--pw_allocator/fallback_allocator.cc55
-rw-r--r--pw_allocator/fallback_allocator_test.cc221
-rw-r--r--pw_allocator/freelist_heap.cc47
-rw-r--r--pw_allocator/freelist_heap_test.cc37
-rw-r--r--pw_allocator/libc_allocator.cc51
-rw-r--r--pw_allocator/libc_allocator_test.cc77
-rw-r--r--pw_allocator/null_allocator_test.cc38
-rw-r--r--pw_allocator/public/pw_allocator/allocator.h380
-rw-r--r--pw_allocator/public/pw_allocator/allocator_metric_proxy.h71
-rw-r--r--pw_allocator/public/pw_allocator/allocator_testing.h131
-rw-r--r--pw_allocator/public/pw_allocator/block.h1025
-rw-r--r--pw_allocator/public/pw_allocator/fallback_allocator.h51
-rw-r--r--pw_allocator/public/pw_allocator/freelist.h92
-rw-r--r--pw_allocator/public/pw_allocator/freelist_heap.h4
-rw-r--r--pw_allocator/public/pw_allocator/libc_allocator.h42
-rw-r--r--pw_allocator/public/pw_allocator/null_allocator.h41
-rw-r--r--pw_allocator/public/pw_allocator/simple_allocator.h87
-rw-r--r--pw_allocator/public/pw_allocator/split_free_list_allocator.h288
-rw-r--r--pw_allocator/py/BUILD.gn1
-rw-r--r--pw_allocator/py/pw_allocator/heap_viewer.py2
-rw-r--r--pw_allocator/simple_allocator_test.cc66
-rw-r--r--pw_allocator/size_report/BUILD.bazel54
-rw-r--r--pw_allocator/size_report/BUILD.gn46
-rw-r--r--pw_allocator/size_report/split_free_list_allocator.cc131
-rw-r--r--pw_allocator/split_free_list_allocator.cc29
-rw-r--r--pw_allocator/split_free_list_allocator_test.cc420
-rw-r--r--pw_allocator/unique_ptr_test.cc149
-rw-r--r--pw_analog/BUILD.bazel2
-rw-r--r--pw_analog/BUILD.gn7
-rw-r--r--pw_analog/docs.rst41
-rw-r--r--pw_analog/public/pw_analog/analog_input.h70
-rw-r--r--pw_analog/public/pw_analog/microvolt_input.h68
-rw-r--r--pw_android_toolchain/docs.rst4
-rw-r--r--pw_android_toolchain/toolchains.gni3
-rw-r--r--pw_arduino_build/arduino.gni21
-rw-r--r--pw_arduino_build/py/BUILD.bazel2
-rw-r--r--pw_arduino_build/py/BUILD.gn3
-rw-r--r--pw_arduino_build/py/pw_arduino_build/__main__.py14
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/builder.py28
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_installer.py329
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_patches/__init__.py0
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.53-cpp17.diff70
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.58.1-cpp17.diff38
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_patches/teensy/02-teensy4_nonstatic_flash_functions.diff42
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_patches/teensy/__init__.py0
-rw-r--r--pw_arduino_build/py/pw_arduino_build/teensy_detector.py2
-rw-r--r--pw_arduino_build/py/setup.cfg4
-rw-r--r--pw_assert/BUILD.bazel24
-rw-r--r--pw_assert/assert_backend_compile_test.cc2
-rw-r--r--pw_assert/assert_backend_compile_test_c.c1
-rw-r--r--pw_assert/docs.rst29
-rw-r--r--pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h12
-rw-r--r--pw_assert/public/pw_assert/internal/libc_assert.h2
-rw-r--r--pw_assert/public/pw_assert/internal/print_and_abort.h27
-rw-r--r--pw_assert_basic/BUILD.bazel25
-rw-r--r--pw_assert_basic/BUILD.gn2
-rw-r--r--pw_assert_basic/CMakeLists.txt2
-rw-r--r--pw_assert_basic/basic_handler.cc2
-rw-r--r--pw_assert_log/Android.bp25
-rw-r--r--pw_assert_log/BUILD.bazel3
-rw-r--r--pw_assert_log/BUILD.gn4
-rw-r--r--pw_assert_log/public/pw_assert_log/check_log.h51
-rw-r--r--pw_assert_tokenized/BUILD.bazel43
-rw-r--r--pw_assert_tokenized/docs.rst58
-rw-r--r--pw_assert_zephyr/CMakeLists.txt11
-rw-r--r--pw_assert_zephyr/Kconfig8
-rw-r--r--pw_assert_zephyr/docs.rst4
-rw-r--r--pw_async/BUILD.bazel118
-rw-r--r--pw_async/BUILD.gn38
-rw-r--r--pw_async/CMakeLists.txt55
-rw-r--r--pw_async/OWNERS2
-rw-r--r--pw_async/backend.cmake21
-rw-r--r--pw_async/docs.rst37
-rw-r--r--pw_async/fake_dispatcher_fixture.gni1
-rw-r--r--pw_async/fake_dispatcher_test.cc374
-rw-r--r--pw_async/fake_dispatcher_test.gni2
-rw-r--r--pw_async/heap_dispatcher.cc66
-rw-r--r--pw_async/heap_dispatcher.gni38
-rw-r--r--pw_async/public/pw_async/context.h32
-rw-r--r--pw_async/public/pw_async/dispatcher.h71
-rw-r--r--pw_async/public/pw_async/fake_dispatcher.h40
-rw-r--r--pw_async/public/pw_async/fake_dispatcher_fixture.h17
-rw-r--r--pw_async/public/pw_async/function_dispatcher.h52
-rw-r--r--pw_async/public/pw_async/heap_dispatcher.h48
-rw-r--r--pw_async/public/pw_async/internal/types.h45
-rw-r--r--pw_async/public/pw_async/task.h21
-rw-r--r--pw_async/public/pw_async/task_function.h39
-rw-r--r--pw_async_basic/BUILD.bazel93
-rw-r--r--pw_async_basic/BUILD.gn25
-rw-r--r--pw_async_basic/CMakeLists.txt43
-rw-r--r--pw_async_basic/dispatcher.cc61
-rw-r--r--pw_async_basic/dispatcher_test.cc1
-rw-r--r--pw_async_basic/docs.rst16
-rw-r--r--pw_async_basic/fake_dispatcher.cc61
-rw-r--r--pw_async_basic/fake_dispatcher_fixture_test.cc23
-rw-r--r--pw_async_basic/heap_dispatcher_test.cc70
-rw-r--r--pw_async_basic/public/pw_async_basic/dispatcher.h9
-rw-r--r--pw_async_basic/public/pw_async_basic/fake_dispatcher.h17
-rw-r--r--pw_async_basic/public/pw_async_basic/task.h19
-rw-r--r--pw_async_basic/size_report/BUILD.bazel38
-rw-r--r--pw_async_basic/size_report/post_1_task.cc5
-rw-r--r--pw_async_basic/size_report/task.cc3
-rw-r--r--pw_base64/Android.bp47
-rw-r--r--pw_base64/BUILD.gn3
-rw-r--r--pw_base64/Kconfig25
-rw-r--r--pw_base64/base64.cc17
-rw-r--r--pw_base64/docs.rst11
-rw-r--r--pw_base64/public/pw_base64/base64.h127
-rw-r--r--pw_bloat/bloat.gni20
-rw-r--r--pw_bloat/docs.rst419
-rw-r--r--pw_bloat/py/BUILD.gn1
-rw-r--r--pw_bloat/py/label_test.py64
-rwxr-xr-xpw_bloat/py/pw_bloat/bloat.py75
-rw-r--r--pw_bloat/py/pw_bloat/label.py91
-rw-r--r--pw_bloat/py/pw_bloat/label_output.py8
-rw-r--r--pw_blob_store/blob_store.cc6
-rw-r--r--pw_blob_store/docs.rst41
-rw-r--r--pw_blob_store/flat_file_system_entry.cc2
-rw-r--r--pw_blob_store/public/pw_blob_store/blob_store.h8
-rw-r--r--pw_bluetooth/BUILD.gn62
-rw-r--r--pw_bluetooth/api_test.cc2
-rw-r--r--pw_bluetooth/emboss_test.cc6
-rw-r--r--pw_bluetooth/public/pw_bluetooth/gatt/types.h2
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci.emb2570
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci_commands.emb2934
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci_common.emb503
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci_events.emb1751
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci_test.emb37
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci_vendor.emb332
-rw-r--r--pw_bluetooth/public/pw_bluetooth/vendor.emb112
-rw-r--r--pw_bluetooth/size_report/BUILD.gn6
-rw-r--r--pw_bluetooth/size_report/make_2_views_and_write.cc2
-rw-r--r--pw_bluetooth/size_report/make_view_and_write.cc2
-rw-r--r--pw_bluetooth_hci/BUILD.bazel1
-rw-r--r--pw_bluetooth_hci/BUILD.gn6
-rw-r--r--pw_bluetooth_hci/docs.rst27
-rw-r--r--pw_boot/BUILD.bazel14
-rw-r--r--pw_boot/docs.rst2
-rw-r--r--pw_boot_cortex_m/BUILD.gn18
-rw-r--r--pw_boot_cortex_m/basic_cortex_m.ld10
-rw-r--r--pw_boot_cortex_m/core_init.c9
-rw-r--r--pw_boot_cortex_m/docs.rst10
-rw-r--r--pw_boot_cortex_m/toolchain.gni (renamed from pw_tokenizer/backend.gni)13
-rw-r--r--pw_build/BUILD.bazel69
-rw-r--r--pw_build/BUILD.gn131
-rw-r--r--pw_build/CMakeLists.txt1
-rw-r--r--pw_build/bazel.rst355
-rw-r--r--pw_build/bazel_internal/BUILD.bazel45
-rw-r--r--pw_build/bazel_internal/linker_script.ld6
-rw-r--r--pw_build/bazel_internal/pigweed_internal.bzl180
-rw-r--r--pw_build/bazel_internal/test.cc9
-rw-r--r--pw_build/cc_library.gni12
-rw-r--r--pw_build/cmake.rst184
-rw-r--r--pw_build/constraints/chipset/BUILD.bazel5
-rw-r--r--pw_build/constraints/rtos/BUILD.bazel8
-rw-r--r--pw_build/copy_from_cipd.gni2
-rw-r--r--pw_build/coverage_report.gni270
-rw-r--r--pw_build/defaults.gni99
-rw-r--r--pw_build/docs.rst1189
-rw-r--r--pw_build/exec.gni3
-rw-r--r--pw_build/facade.gni9
-rw-r--r--pw_build/file_prefix_map_test.cc30
-rw-r--r--pw_build/generated_pigweed_modules_lists.gni83
-rw-r--r--pw_build/gn.rst1341
-rw-r--r--pw_build/gn_internal/build_target.gni62
-rw-r--r--pw_build/gn_internal/defaults.gni38
-rw-r--r--pw_build/pigweed.bzl320
-rw-r--r--pw_build/pigweed.cmake23
-rw-r--r--pw_build/platforms/BUILD.bazel96
-rw-r--r--pw_build/pw_build_private/file_prefix_map_test.h36
-rw-r--r--pw_build/py/BUILD.bazel7
-rw-r--r--pw_build/py/BUILD.gn21
-rw-r--r--pw_build/py/bazel_query_test.py409
-rw-r--r--pw_build/py/build_recipe_test.py41
-rw-r--r--pw_build/py/create_python_tree_test.py15
-rw-r--r--pw_build/py/file_prefix_map_test.py3
-rw-r--r--pw_build/py/generate_3p_gn_test.py744
-rw-r--r--pw_build/py/generate_cc_blob_library_test.py4
-rw-r--r--pw_build/py/gn_config_test.py210
-rw-r--r--pw_build/py/gn_target_test.py227
-rw-r--r--pw_build/py/gn_tests/BUILD.gn71
-rw-r--r--pw_build/py/gn_tests/package_data/empty.csv1
-rw-r--r--pw_build/py/gn_utils_test.py351
-rw-r--r--pw_build/py/gn_writer_test.py324
-rw-r--r--pw_build/py/pw_build/bazel_query.py302
-rw-r--r--pw_build/py/pw_build/build_recipe.py81
-rw-r--r--pw_build/py/pw_build/collect_wheels.py36
-rw-r--r--pw_build/py/pw_build/create_gn_venv.py57
-rw-r--r--pw_build/py/pw_build/create_python_tree.py86
-rw-r--r--pw_build/py/pw_build/file_prefix_map.py1
-rw-r--r--pw_build/py/pw_build/generate_3p_gn.py475
-rw-r--r--pw_build/py/pw_build/generate_cc_blob_library.py5
-rw-r--r--pw_build/py/pw_build/generate_python_package.py2
-rw-r--r--pw_build/py/pw_build/generate_python_requirements.py38
-rw-r--r--pw_build/py/pw_build/generate_python_wheel.py106
-rw-r--r--pw_build/py/pw_build/generate_python_wheel_cache.py181
-rw-r--r--pw_build/py/pw_build/generate_report.py249
-rw-r--r--pw_build/py/pw_build/gn_config.py386
-rw-r--r--pw_build/py/pw_build/gn_resolver.py2
-rw-r--r--pw_build/py/pw_build/gn_target.py324
-rw-r--r--pw_build/py/pw_build/gn_utils.py361
-rw-r--r--pw_build/py/pw_build/gn_writer.py379
-rwxr-xr-xpw_build/py/pw_build/merge_profraws.py133
-rw-r--r--pw_build/py/pw_build/pigweed_upstream_build.py118
-rw-r--r--pw_build/py/pw_build/pip_install_python_deps.py47
-rw-r--r--pw_build/py/pw_build/project_builder.py117
-rw-r--r--pw_build/py/pw_build/project_builder_argparse.py10
-rw-r--r--pw_build/py/pw_build/project_builder_context.py31
-rw-r--r--pw_build/py/pw_build/project_builder_presubmit_runner.py860
-rw-r--r--pw_build/py/pw_build/python_package.py23
-rwxr-xr-xpw_build/py/pw_build/python_runner.py10
-rw-r--r--pw_build/py/pw_build/python_wheels.py67
-rw-r--r--pw_build/py/pw_build/wrap_ninja.py2
-rw-r--r--pw_build/py/setup.cfg2
-rw-r--r--pw_build/python.gni53
-rw-r--r--pw_build/python.rst31
-rw-r--r--pw_build/python_action.gni5
-rw-r--r--pw_build/python_action_test.gni187
-rw-r--r--pw_build/python_dist.gni136
-rw-r--r--pw_build/python_dist/setup.bat30
-rwxr-xr-xpw_build/python_dist/setup.sh35
-rw-r--r--pw_build/python_gn_args.gni30
-rw-r--r--pw_build/python_venv.gni274
-rw-r--r--pw_build/rust_executable.gni6
-rw-r--r--pw_build/rust_library.gni5
-rw-r--r--pw_build/rust_proc_macro.gni32
-rw-r--r--pw_build/rust_test.gni80
-rw-r--r--pw_build/target_config.bzl60
-rw-r--r--pw_build/target_types.gni2
-rw-r--r--pw_build/test_info.gni101
-rw-r--r--pw_build_info/BUILD.bazel43
-rw-r--r--pw_build_info/BUILD.gn17
-rw-r--r--pw_build_info/build_id_noop.cc22
-rw-r--r--pw_build_info/docs.rst4
-rw-r--r--pw_build_info/py/BUILD.bazel2
-rw-r--r--pw_build_info/py/BUILD.gn1
-rw-r--r--pw_build_info/py/build_id_test.py6
-rw-r--r--pw_build_info/py/setup.py18
-rw-r--r--pw_build_mcuxpresso/docs.rst126
-rw-r--r--pw_build_mcuxpresso/py/BUILD.gn3
-rw-r--r--pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py42
-rw-r--r--pw_build_mcuxpresso/py/pw_build_mcuxpresso/bazel.py89
-rw-r--r--pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py126
-rw-r--r--pw_build_mcuxpresso/py/pw_build_mcuxpresso/gn.py59
-rw-r--r--pw_build_mcuxpresso/py/setup.py18
-rw-r--r--pw_build_mcuxpresso/py/tests/components_test.py28
-rw-r--r--pw_bytes/Android.bp6
-rw-r--r--pw_bytes/BUILD.bazel20
-rw-r--r--pw_bytes/BUILD.gn16
-rw-r--r--pw_bytes/CMakeLists.txt27
-rw-r--r--pw_bytes/Kconfig8
-rw-r--r--pw_bytes/alignment_test.cc116
-rw-r--r--pw_bytes/docs.rst11
-rw-r--r--pw_bytes/public/pw_bytes/alignment.h35
-rw-r--r--pw_bytes/public/pw_bytes/bit.h24
-rw-r--r--pw_bytes/public/pw_bytes/endian.h1
-rw-r--r--pw_bytes/public/pw_bytes/suffix.h33
-rw-r--r--pw_bytes/suffix_test.cc28
-rw-r--r--pw_checksum/Android.bp35
-rw-r--r--pw_checksum/BUILD.gn6
-rw-r--r--pw_checksum/CMakeLists.txt17
-rw-r--r--pw_checksum/Kconfig8
-rw-r--r--pw_checksum/docs.rst2
-rw-r--r--pw_checksum/size_report/BUILD.gn12
-rw-r--r--pw_checksum/size_report/run_checksum.cc6
-rw-r--r--pw_chre/BUILD.bazel60
-rw-r--r--pw_chre/BUILD.gn153
-rw-r--r--pw_chre/chre.cc62
-rw-r--r--pw_chre/chre_api_re.cc48
-rw-r--r--pw_chre/chre_empty_host_link.cc25
-rw-r--r--pw_chre/context.cc21
-rw-r--r--pw_chre/docs.rst138
-rw-r--r--pw_chre/example_init.cc39
-rw-r--r--pw_chre/host_link.cc45
-rw-r--r--pw_chre/include/chre/target_platform/atomic_base.h30
-rw-r--r--pw_chre/include/chre/target_platform/atomic_base_impl.h63
-rw-r--r--pw_chre/include/chre/target_platform/condition_variable_base.h26
-rw-r--r--pw_chre/include/chre/target_platform/condition_variable_impl.h42
-rw-r--r--pw_chre/include/chre/target_platform/fatal_error.h21
-rw-r--r--pw_chre/include/chre/target_platform/host_link_base.h25
-rw-r--r--pw_chre/include/chre/target_platform/log.h24
-rw-r--r--pw_chre/include/chre/target_platform/mutex_base.h20
-rw-r--r--pw_chre/include/chre/target_platform/mutex_base_impl.h31
-rw-r--r--pw_chre/include/chre/target_platform/platform_nanoapp_base.h (renamed from pw_polyfill/cstddef_public_overrides/cstddef)19
-rw-r--r--pw_chre/include/chre/target_platform/platform_sensor_base.h43
-rw-r--r--pw_chre/include/chre/target_platform/platform_sensor_manager_base.h38
-rw-r--r--pw_chre/include/chre/target_platform/platform_sensor_type_helpers_base.h20
-rw-r--r--pw_chre/include/chre/target_platform/power_control_manager_base.h21
-rw-r--r--pw_chre/include/chre/target_platform/static_nanoapp_init.h50
-rw-r--r--pw_chre/include/chre/target_platform/system_timer_base.h38
-rw-r--r--pw_chre/memory.cc31
-rw-r--r--pw_chre/memory_manager.cc27
-rw-r--r--pw_chre/platform_debug_dump_manager.cc26
-rw-r--r--pw_chre/platform_nanoapp.cc77
-rw-r--r--pw_chre/platform_pal.cc23
-rw-r--r--pw_chre/power_control_manager.cc24
-rw-r--r--pw_chre/public/pw_chre/chre.h71
-rw-r--r--pw_chre/public/pw_chre/host_link.h67
-rw-r--r--pw_chre/static_nanoapps.cc34
-rw-r--r--pw_chre/system_time.cc45
-rw-r--r--pw_chre/system_timer.cc67
-rw-r--r--pw_chrono/Android.bp39
-rw-r--r--pw_chrono/BUILD.bazel24
-rw-r--r--pw_chrono/public/pw_chrono/system_clock.h5
-rw-r--r--pw_chrono/py/BUILD.bazel9
-rw-r--r--pw_chrono_embos/BUILD.bazel46
-rw-r--r--pw_chrono_freertos/BUILD.bazel45
-rw-r--r--pw_chrono_freertos/system_timer.cc21
-rw-r--r--pw_chrono_rp2040/BUILD.bazel61
-rw-r--r--pw_chrono_rp2040/BUILD.gn68
-rw-r--r--pw_chrono_rp2040/CMakeLists.txt42
-rw-r--r--pw_chrono_rp2040/clock_properties_test.cc53
-rw-r--r--pw_chrono_rp2040/docs.rst17
-rw-r--r--pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_config.h43
-rw-r--r--pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_inline.h27
-rw-r--r--pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_config.h19
-rw-r--r--pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_inline.h16
-rw-r--r--pw_chrono_stl/Android.bp40
-rw-r--r--pw_chrono_stl/BUILD.bazel29
-rw-r--r--pw_chrono_threadx/BUILD.bazel21
-rw-r--r--pw_chrono_zephyr/CMakeLists.txt5
-rw-r--r--pw_chrono_zephyr/Kconfig8
-rw-r--r--pw_chrono_zephyr/docs.rst2
-rw-r--r--pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_inline.h2
-rw-r--r--pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_native.h4
-rw-r--r--pw_cli/docs.rst131
-rw-r--r--pw_cli/py/BUILD.gn26
-rw-r--r--pw_cli/py/plugins_test.py23
-rw-r--r--pw_cli/py/process_integration_test.py11
-rw-r--r--pw_cli/py/pw_cli/__main__.py25
-rw-r--r--pw_cli/py/pw_cli/arguments.py105
-rw-r--r--pw_cli/py/pw_cli/branding.py2
-rw-r--r--pw_cli/py/pw_cli/color.py14
-rw-r--r--pw_cli/py/pw_cli/env.py3
-rw-r--r--pw_cli/py/pw_cli/envparse.py10
-rw-r--r--pw_cli/py/pw_cli/log.py6
-rw-r--r--pw_cli/py/pw_cli/plugins.py27
-rw-r--r--pw_cli/py/pw_cli/process.py3
-rw-r--r--pw_cli/py/pw_cli/pw_command_plugins.py34
-rwxr-xr-xpw_cli/py/pw_cli/requires.py119
-rw-r--r--pw_cli/py/pw_cli/shell_completion/__init__.py0
-rw-r--r--pw_cli/py/pw_cli/shell_completion/common.bash65
-rw-r--r--pw_cli/py/pw_cli/shell_completion/pw.bash75
-rw-r--r--pw_cli/py/pw_cli/shell_completion/pw.zsh (renamed from pw_hdlc/py/setup.py)8
-rw-r--r--pw_cli/py/pw_cli/shell_completion/pw_build.bash62
-rw-r--r--pw_cli/py/pw_cli/shell_completion/pw_build.zsh (renamed from pw_allocator/py/setup.py)8
-rw-r--r--pw_cli/py/pw_cli/shell_completion/zsh/__init__.py0
-rw-r--r--pw_cli/py/pw_cli/shell_completion/zsh/pw/__init__.py0
-rwxr-xr-xpw_cli/py/pw_cli/shell_completion/zsh/pw/_pw49
-rw-r--r--pw_cli/py/pw_cli/shell_completion/zsh/pw_build/__init__.py0
-rw-r--r--pw_cli/py/pw_cli/shell_completion/zsh/pw_build/_pw_build40
-rw-r--r--pw_cli/py/setup.cfg10
-rw-r--r--pw_compilation_testing/negative_compilation_test.gni2
-rw-r--r--pw_compilation_testing/py/BUILD.gn1
-rw-r--r--pw_compilation_testing/py/setup.py18
-rw-r--r--pw_console/docs.rst26
-rw-r--r--pw_console/embedding.rst37
-rw-r--r--pw_console/internals.rst3
-rw-r--r--pw_console/plugins.rst11
-rw-r--r--pw_console/py/BUILD.bazel22
-rw-r--r--pw_console/py/BUILD.gn3
-rw-r--r--pw_console/py/command_runner_test.py25
-rw-r--r--pw_console/py/pw_console/__main__.py6
-rw-r--r--pw_console/py/pw_console/command_runner.py98
-rw-r--r--pw_console/py/pw_console/console_app.py120
-rw-r--r--pw_console/py/pw_console/console_prefs.py87
-rw-r--r--pw_console/py/pw_console/docs/user_guide.rst142
-rw-r--r--pw_console/py/pw_console/help_window.py4
-rw-r--r--pw_console/py/pw_console/html/index.html21
-rw-r--r--pw_console/py/pw_console/html/main.js316
-rw-r--r--pw_console/py/pw_console/html/style.css225
-rw-r--r--pw_console/py/pw_console/key_bindings.py2
-rw-r--r--pw_console/py/pw_console/log_pane.py15
-rw-r--r--pw_console/py/pw_console/log_store.py5
-rw-r--r--pw_console/py/pw_console/log_view.py7
-rw-r--r--pw_console/py/pw_console/plugins/bandwidth_toolbar.py152
-rw-r--r--pw_console/py/pw_console/plugins/calc_pane.py6
-rw-r--r--pw_console/py/pw_console/pyserial_wrapper.py137
-rw-r--r--pw_console/py/pw_console/python_logging.py17
-rw-r--r--pw_console/py/pw_console/repl_pane.py10
-rw-r--r--pw_console/py/pw_console/socket_client.py218
-rw-r--r--pw_console/py/pw_console/test_mode.py20
-rw-r--r--pw_console/py/pw_console/widgets/border.py4
-rw-r--r--pw_console/py/pw_console/widgets/event_count_history.py6
-rw-r--r--pw_console/py/setup.py18
-rw-r--r--pw_console/py/socket_client_test.py181
-rw-r--r--pw_console/testing.rst3
-rw-r--r--pw_containers/BUILD.bazel116
-rw-r--r--pw_containers/BUILD.gn135
-rw-r--r--pw_containers/CMakeLists.txt108
-rw-r--r--pw_containers/Kconfig8
-rw-r--r--pw_containers/algorithm_test.cc10
-rw-r--r--pw_containers/docs.rst139
-rw-r--r--pw_containers/filtered_view_test.cc37
-rw-r--r--pw_containers/flat_map_test.cc174
-rw-r--r--pw_containers/inline_deque_test.cc960
-rw-r--r--pw_containers/inline_queue_test.cc777
-rw-r--r--pw_containers/intrusive_list.cc22
-rw-r--r--pw_containers/intrusive_list_test.cc24
-rw-r--r--pw_containers/public/pw_containers/filtered_view.h35
-rw-r--r--pw_containers/public/pw_containers/flat_map.h140
-rw-r--r--pw_containers/public/pw_containers/inline_deque.h661
-rw-r--r--pw_containers/public/pw_containers/inline_queue.h241
-rw-r--r--pw_containers/public/pw_containers/internal/intrusive_list_impl.h82
-rw-r--r--pw_containers/public/pw_containers/internal/raw_storage.h110
-rw-r--r--pw_containers/public/pw_containers/intrusive_list.h4
-rw-r--r--pw_containers/public/pw_containers/variable_length_entry_queue.h330
-rw-r--r--pw_containers/public/pw_containers/vector.h84
-rw-r--r--pw_containers/pw_containers_private/test_helpers.h84
-rw-r--r--pw_containers/pw_containers_private/variable_length_entry_queue_test_oracle.h93
-rw-r--r--pw_containers/py/BUILD.gn33
-rw-r--r--pw_containers/py/pw_containers/__init__.py0
-rw-r--r--pw_containers/py/pw_containers/variable_length_entry_queue.py79
-rw-r--r--pw_containers/py/variable_length_entry_queue_test.py61
-rw-r--r--pw_containers/raw_storage_test.cc72
-rw-r--r--pw_containers/test_helpers.cc22
-rw-r--r--pw_containers/variable_length_entry_queue.c249
-rw-r--r--pw_containers/variable_length_entry_queue_test.cc271
-rw-r--r--pw_containers/vector_test.cc124
-rw-r--r--pw_cpu_exception/BUILD.bazel162
-rw-r--r--pw_cpu_exception/docs.rst81
-rw-r--r--pw_cpu_exception/start_exception_handler.cc2
-rw-r--r--pw_cpu_exception_cortex_m/BUILD.bazel44
-rw-r--r--pw_cpu_exception_cortex_m/BUILD.gn2
-rw-r--r--pw_cpu_exception_cortex_m/CMakeLists.txt2
-rw-r--r--pw_cpu_exception_cortex_m/docs.rst254
-rw-r--r--pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h2
-rw-r--r--pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h2
-rw-r--r--pw_cpu_exception_cortex_m/py/BUILD.bazel7
-rw-r--r--pw_cpu_exception_cortex_m/py/BUILD.gn1
-rw-r--r--pw_cpu_exception_cortex_m/py/setup.py18
-rw-r--r--pw_crypto/BUILD.bazel93
-rw-r--r--pw_crypto/BUILD.gn61
-rw-r--r--pw_crypto/docs.rst81
-rw-r--r--pw_crypto/ecdsa_boringssl.cc106
-rw-r--r--pw_crypto/public/pw_crypto/ecdsa.h41
-rw-r--r--pw_crypto/public/pw_crypto/sha256.h84
-rw-r--r--pw_crypto/public/pw_crypto/sha256_boringssl.h33
-rw-r--r--pw_crypto/public/pw_crypto/sha256_mbedtls.h7
-rw-r--r--pw_crypto/public_overrides/mbedtls/pw_crypto/sha256_backend.h2
-rw-r--r--pw_crypto/sha256_boringssl.cc48
-rw-r--r--pw_crypto/sha256_mbedtls.cc11
-rw-r--r--pw_digital_io/Android.bp53
-rw-r--r--pw_digital_io/BUILD.bazel35
-rw-r--r--pw_digital_io/BUILD.gn16
-rw-r--r--pw_digital_io/CMakeLists.txt3
-rw-r--r--pw_digital_io/digital_io.proto55
-rw-r--r--pw_digital_io/digital_io_service.cc104
-rw-r--r--pw_digital_io/docs.rst44
-rw-r--r--pw_digital_io/public/pw_digital_io/digital_io.h328
-rw-r--r--pw_digital_io/public/pw_digital_io/digital_io_service.h54
-rw-r--r--pw_digital_io_mcuxpresso/BUILD.bazel50
-rw-r--r--pw_digital_io_mcuxpresso/BUILD.gn70
-rw-r--r--pw_digital_io_mcuxpresso/OWNERS2
-rw-r--r--pw_digital_io_mcuxpresso/digital_io.cc125
-rw-r--r--pw_digital_io_mcuxpresso/docs.rst62
-rw-r--r--pw_digital_io_mcuxpresso/interrupt_controller.cc108
-rw-r--r--pw_digital_io_mcuxpresso/interrupt_line.cc50
-rw-r--r--pw_digital_io_mcuxpresso/mimxrt595_test.cc39
-rw-r--r--pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/digital_io.h57
-rw-r--r--pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_controller.h42
-rw-r--r--pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_line.h47
-rw-r--r--pw_digital_io_rp2040/BUILD.bazel27
-rw-r--r--pw_digital_io_rp2040/BUILD.gn56
-rw-r--r--pw_digital_io_rp2040/OWNERS1
-rw-r--r--pw_digital_io_rp2040/digital_io.cc64
-rw-r--r--pw_digital_io_rp2040/digital_io_test.cc42
-rw-r--r--pw_digital_io_rp2040/docs.rst34
-rw-r--r--pw_digital_io_rp2040/public/pw_digital_io_rp2040/digital_io.h46
-rw-r--r--pw_docgen/docs.gni166
-rw-r--r--pw_docgen/docs.rst127
-rw-r--r--pw_docgen/py/BUILD.gn4
-rw-r--r--pw_docgen/py/pw_docgen/docgen.py32
-rw-r--r--pw_docgen/py/pw_docgen/sphinx/kconfig.py137
-rw-r--r--pw_docgen/py/pw_docgen/sphinx/module_metadata.py329
-rw-r--r--pw_docgen/py/pw_docgen/sphinx/pigweed_live.py130
-rw-r--r--pw_docgen/py/pw_docgen/sphinx/seed_metadata.py133
-rw-r--r--pw_docgen/py/setup.cfg1
-rw-r--r--pw_docgen/py/setup.py18
-rw-r--r--pw_doctor/docs.rst22
-rw-r--r--pw_doctor/py/BUILD.gn1
-rwxr-xr-xpw_doctor/py/pw_doctor/doctor.py27
-rw-r--r--pw_doctor/py/setup.py18
-rw-r--r--pw_emu/BUILD.gn36
-rw-r--r--pw_emu/OWNERS2
-rw-r--r--pw_emu/README.md2
-rw-r--r--pw_emu/api.rst19
-rw-r--r--pw_emu/cli.rst16
-rw-r--r--pw_emu/config.rst155
-rw-r--r--pw_emu/design.rst103
-rw-r--r--pw_emu/docs.rst80
-rw-r--r--pw_emu/guide.rst285
-rw-r--r--pw_emu/py/BUILD.gn68
-rw-r--r--pw_emu/py/mock_emu.py111
-rw-r--r--pw_emu/py/mock_emu_frontend.py154
-rw-r--r--pw_emu/py/pw_emu/__init__.py14
-rw-r--r--pw_emu/py/pw_emu/__main__.py448
-rw-r--r--pw_emu/py/pw_emu/core.py956
-rw-r--r--pw_emu/py/pw_emu/frontend.py328
-rw-r--r--pw_emu/py/pw_emu/pigweed_emulators.py27
-rw-r--r--pw_emu/py/pw_emu/py.typed0
-rw-r--r--pw_emu/py/pw_emu/qemu.py343
-rw-r--r--pw_emu/py/pw_emu/renode.py209
-rw-r--r--pw_emu/py/pyproject.toml16
-rw-r--r--pw_emu/py/setup.cfg28
-rw-r--r--pw_emu/py/setup.py (renamed from pw_cli/py/setup.py)4
-rw-r--r--pw_emu/py/tests/__init__.py14
-rw-r--r--pw_emu/py/tests/cli_test.py276
-rw-r--r--pw_emu/py/tests/common.py67
-rw-r--r--pw_emu/py/tests/core_test.py480
-rw-r--r--pw_emu/py/tests/frontend_test.py150
-rw-r--r--pw_emu/py/tests/py.typed0
-rw-r--r--pw_emu/py/tests/qemu_test.py280
-rw-r--r--pw_emu/py/tests/renode_test.py238
-rw-r--r--pw_emu/qemu-lm3s6965evb.json20
-rw-r--r--pw_emu/qemu-netduinoplus2.json20
-rw-r--r--pw_emu/qemu-stm32vldiscovery.json20
-rw-r--r--pw_emu/renode-stm32f4_discovery.json20
-rw-r--r--pw_env_setup/BUILD.gn44
-rw-r--r--pw_env_setup/bazel/cipd_setup/cipd_rules.bzl20
-rw-r--r--pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl44
-rw-r--r--pw_env_setup/config.json21
-rw-r--r--pw_env_setup/doc_resources/pw_env_setup_output.pngbin51614 -> 0 bytes
-rw-r--r--pw_env_setup/docs.rst358
-rw-r--r--pw_env_setup/py/BUILD.gn33
-rw-r--r--pw_env_setup/py/cipd_setup_update_test.py186
-rw-r--r--pw_env_setup/py/pw_env_setup/apply_visitor.py7
-rw-r--r--pw_env_setup/py/pw_env_setup/batch_visitor.py6
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests45
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/OWNERS1
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/arm.json16
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json17
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/clang.json3
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/clang_next.json18
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/coverage.json22
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/default.json1
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/go.json2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/luci.json26
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/msrv_python.json5
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json24
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/python.json17
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/python310.json29
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/python311.json30
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/python38.json29
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/python39.json29
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/testing.json26
-rwxr-xr-xpw_env_setup/py/pw_env_setup/cipd_setup/update.py81
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json2
-rw-r--r--pw_env_setup/py/pw_env_setup/config_file.py37
-rw-r--r--pw_env_setup/py/pw_env_setup/entry_points/__init__.py0
-rw-r--r--pw_env_setup/py/pw_env_setup/entry_points/arm_gdb.py66
-rwxr-xr-xpw_env_setup/py/pw_env_setup/env_setup.py163
-rw-r--r--pw_env_setup/py/pw_env_setup/gni_visitor.py62
-rw-r--r--pw_env_setup/py/pw_env_setup/json_visitor.py6
-rw-r--r--pw_env_setup/py/pw_env_setup/python_packages.py6
-rw-r--r--pw_env_setup/py/pw_env_setup/shell_visitor.py6
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/BUILD.bazel15
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list126
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_darwin.list1177
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_linux.list1171
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_windows.list1175
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py115
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt13
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/python_base_requirements.txt9
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_darwin_lock.txt1230
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_linux_lock.txt1224
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_windows_lock.txt1228
-rw-r--r--pw_env_setup/py/pw_env_setup/windows_env_start.py23
-rw-r--r--pw_env_setup/py/setup.cfg30
-rw-r--r--pw_env_setup/py/setup.py18
-rw-r--r--pw_env_setup/pypi_common_setup.cfg2
-rw-r--r--pw_env_setup/sample_project_action/__init__.py26
-rw-r--r--pw_env_setup/util.sh4
-rw-r--r--pw_file/BUILD.bazel4
-rw-r--r--pw_file/BUILD.gn1
-rw-r--r--pw_file/CMakeLists.txt2
-rw-r--r--pw_file/flat_file_system.cc15
-rw-r--r--pw_file/py/BUILD.gn31
-rw-r--r--pw_file/py/pw_file/__init__.py13
-rw-r--r--pw_format/BUILD.gn34
-rw-r--r--pw_format/docs.rst22
-rw-r--r--pw_format/rust/BUILD.bazel112
-rw-r--r--pw_format/rust/pw_format/lib.rs411
-rw-r--r--pw_format/rust/pw_format/macros.rs487
-rw-r--r--pw_format/rust/pw_format/tests.rs1472
-rw-r--r--pw_format/rust/pw_format_example_macro.rs142
-rw-r--r--pw_format/rust/pw_format_example_macro_test.rs24
-rw-r--r--pw_format/rust/pw_format_test_macros.rs227
-rw-r--r--pw_format/rust/pw_format_test_macros_test.rs168
-rw-r--r--pw_function/Android.bp6
-rw-r--r--pw_function/BUILD.gn23
-rw-r--r--pw_function/CMakeLists.txt3
-rw-r--r--pw_function/Kconfig8
-rw-r--r--pw_function/docs.rst179
-rw-r--r--pw_function/function.gni23
-rw-r--r--pw_function/public/pw_function/config.h35
-rw-r--r--pw_function/public/pw_function/function.h55
-rw-r--r--pw_function/size_report/callable_size.cc11
-rw-r--r--pw_fuzzer/BUILD.bazel56
-rw-r--r--pw_fuzzer/BUILD.gn276
-rw-r--r--pw_fuzzer/CMakeLists.txt57
-rw-r--r--pw_fuzzer/OWNERS1
-rw-r--r--pw_fuzzer/concepts.rst53
-rw-r--r--pw_fuzzer/doc_resources/pw_fuzzer_coverage_guided.pngbin44027 -> 72646 bytes
-rw-r--r--pw_fuzzer/docs.rst440
-rw-r--r--pw_fuzzer/domain_test.cc508
-rw-r--r--pw_fuzzer/examples/fuzztest/BUILD.bazel54
-rw-r--r--pw_fuzzer/examples/fuzztest/BUILD.gn62
-rw-r--r--pw_fuzzer/examples/fuzztest/CMakeLists.txt52
-rw-r--r--pw_fuzzer/examples/fuzztest/metrics.cc131
-rw-r--r--pw_fuzzer/examples/fuzztest/metrics.h90
-rw-r--r--pw_fuzzer/examples/fuzztest/metrics_fuzztest.cc87
-rw-r--r--pw_fuzzer/examples/fuzztest/metrics_unittest.cc57
-rw-r--r--pw_fuzzer/examples/libfuzzer/BUILD.bazel (renamed from pw_bloat/py/setup.py)12
-rw-r--r--pw_fuzzer/examples/libfuzzer/BUILD.gn32
-rw-r--r--pw_fuzzer/examples/libfuzzer/toy_fuzzer.cc (renamed from pw_fuzzer/examples/toy_fuzzer.cc)0
-rw-r--r--pw_fuzzer/fuzz_test.gni42
-rw-r--r--pw_fuzzer/fuzzer.bzl19
-rw-r--r--pw_fuzzer/fuzzer.gni88
-rw-r--r--pw_fuzzer/guides/fuzztest.rst394
-rw-r--r--pw_fuzzer/guides/index.rst18
-rw-r--r--pw_fuzzer/guides/libfuzzer.rst359
-rw-r--r--pw_fuzzer/guides/reproducing_oss_fuzz_bugs.rst116
-rw-r--r--pw_fuzzer/private/pw_fuzzer/internal/fuzztest.h (renamed from pw_polyfill/iterator_public_overrides/iterator)17
-rw-r--r--pw_fuzzer/private_overrides/pw_fuzzer/internal/fuzztest.h399
-rw-r--r--pw_fuzzer/public/pw_fuzzer/fuzztest.h1017
-rw-r--r--pw_fuzzer/public_overrides/fuzztest/fuzztest.h150
-rw-r--r--pw_fuzzer/pw_fuzzer_disabled.cc2
-rw-r--r--pw_hdlc/Android.bp58
-rw-r--r--pw_hdlc/BUILD.bazel3
-rw-r--r--pw_hdlc/BUILD.gn29
-rw-r--r--pw_hdlc/CMakeLists.txt7
-rw-r--r--pw_hdlc/Kconfig10
-rw-r--r--pw_hdlc/api.rst266
-rw-r--r--pw_hdlc/decoder.cc3
-rw-r--r--pw_hdlc/decoder_test.cc14
-rw-r--r--pw_hdlc/design.rst65
-rw-r--r--pw_hdlc/docs.rst384
-rw-r--r--pw_hdlc/guide.rst156
-rw-r--r--pw_hdlc/public/pw_hdlc/decoder.h32
-rw-r--r--pw_hdlc/public/pw_hdlc/encoder.h34
-rw-r--r--pw_hdlc/py/BUILD.bazel12
-rw-r--r--pw_hdlc/py/BUILD.gn2
-rw-r--r--pw_hdlc/py/pw_hdlc/rpc.py467
-rwxr-xr-xpw_hdlc/py/rpc_test.py294
-rw-r--r--pw_hdlc/rpc_example/BUILD.bazel1
-rw-r--r--pw_hdlc/rpc_example/docs.rst116
-rwxr-xr-xpw_hdlc/rpc_example/example_script.py36
-rw-r--r--pw_hdlc/rpc_packets.cc2
-rw-r--r--pw_hdlc/ts/crc32.ts79
-rw-r--r--pw_hdlc/ts/decoder.ts24
-rw-r--r--pw_hdlc/ts/decoder_test.ts4
-rw-r--r--pw_hdlc/ts/encoder.ts4
-rw-r--r--pw_hdlc/ts/encoder_test.ts33
-rw-r--r--pw_hdlc/ts/index.ts4
-rw-r--r--pw_hdlc/ts/protocol.ts13
-rw-r--r--pw_hdlc/ts/util.ts8
-rw-r--r--pw_hex_dump/public/pw_hex_dump/hex_dump.h6
-rw-r--r--pw_i2c/Android.bp59
-rw-r--r--pw_i2c/BUILD.bazel50
-rw-r--r--pw_i2c/BUILD.gn35
-rw-r--r--pw_i2c/CMakeLists.txt185
-rw-r--r--pw_i2c/address_test.cc4
-rw-r--r--pw_i2c/docs.rst62
-rw-r--r--pw_i2c/i2c.options19
-rw-r--r--pw_i2c/i2c.proto52
-rw-r--r--pw_i2c/i2c_service.cc84
-rw-r--r--pw_i2c/i2c_service_test.cc244
-rw-r--r--pw_i2c/public/pw_i2c/device.h1
-rw-r--r--pw_i2c/public/pw_i2c/i2c_service.h50
-rw-r--r--pw_i2c/public/pw_i2c/initiator.h32
-rw-r--r--pw_i2c/public/pw_i2c/register_device.h2
-rw-r--r--pw_i2c/register_device.cc4
-rw-r--r--pw_i2c_linux/BUILD.bazel57
-rw-r--r--pw_i2c_linux/BUILD.gn61
-rw-r--r--pw_i2c_linux/OWNERS1
-rw-r--r--pw_i2c_linux/docs.rst84
-rw-r--r--pw_i2c_linux/initiator.cc235
-rw-r--r--pw_i2c_linux/initiator_test.cc44
-rw-r--r--pw_i2c_linux/public/pw_i2c_linux/initiator.h96
-rw-r--r--pw_i2c_mcuxpresso/BUILD.bazel11
-rw-r--r--pw_i2c_mcuxpresso/BUILD.gn14
-rw-r--r--pw_i2c_mcuxpresso/docs.rst32
-rw-r--r--pw_i2c_mcuxpresso/initiator.cc29
-rw-r--r--pw_i2c_mcuxpresso/initiator_test.cc39
-rw-r--r--pw_i2c_mcuxpresso/public/pw_i2c_mcuxpresso/initiator.h27
-rw-r--r--pw_ide/docs.rst104
-rw-r--r--pw_ide/py/BUILD.gn2
-rw-r--r--pw_ide/py/commands_test.py3
-rw-r--r--pw_ide/py/cpp_test.py1226
-rw-r--r--pw_ide/py/editors_test.py433
-rw-r--r--pw_ide/py/pw_ide/activate.py10
-rw-r--r--pw_ide/py/pw_ide/cli.py108
-rw-r--r--pw_ide/py/pw_ide/commands.py1005
-rw-r--r--pw_ide/py/pw_ide/cpp.py827
-rw-r--r--pw_ide/py/pw_ide/editors.py213
-rw-r--r--pw_ide/py/pw_ide/settings.py188
-rw-r--r--pw_ide/py/pw_ide/status_reporter.py143
-rw-r--r--pw_ide/py/pw_ide/vscode.py305
-rw-r--r--pw_ide/py/test_cases.py2
-rw-r--r--pw_ide/py/vscode_test.py8
-rw-r--r--pw_ide/ts/pigweed-vscode/.vscode/launch.json30
-rw-r--r--pw_ide/ts/pigweed-vscode/.vscode/settings.json12
-rw-r--r--pw_ide/ts/pigweed-vscode/.vscode/tasks.json18
-rw-r--r--pw_ide/ts/pigweed-vscode/CHANGELOG.md13
-rw-r--r--pw_ide/ts/pigweed-vscode/README.md25
-rw-r--r--pw_ide/ts/pigweed-vscode/package-lock.json4452
-rw-r--r--pw_ide/ts/pigweed-vscode/package.json67
-rw-r--r--pw_ide/ts/pigweed-vscode/src/config.ts94
-rw-r--r--pw_ide/ts/pigweed-vscode/src/extension.ts242
-rw-r--r--pw_ide/ts/pigweed-vscode/tsconfig.json15
-rw-r--r--pw_ide/ts/pigweed-vscode/webpack.config.js72
-rw-r--r--pw_interrupt/BUILD.bazel37
-rw-r--r--pw_interrupt/docs.rst11
-rw-r--r--pw_interrupt/public/pw_interrupt/context.h7
-rw-r--r--pw_interrupt_cortex_m/BUILD.bazel46
-rw-r--r--pw_interrupt_xtensa/BUILD.bazel36
-rw-r--r--pw_interrupt_xtensa/BUILD.gn31
-rw-r--r--pw_interrupt_xtensa/context.cc34
-rw-r--r--pw_interrupt_xtensa/docs.rst7
-rw-r--r--pw_interrupt_zephyr/CMakeLists.txt7
-rw-r--r--pw_interrupt_zephyr/Kconfig8
-rw-r--r--pw_intrusive_ptr/BUILD.gn4
-rw-r--r--pw_kvs/BUILD.bazel79
-rw-r--r--pw_kvs/BUILD.gn65
-rw-r--r--pw_kvs/CMakeLists.txt60
-rw-r--r--pw_kvs/alignment_test.cc93
-rw-r--r--pw_kvs/docs.rst12
-rw-r--r--pw_kvs/fake_flash_test_key_value_store.cc3
-rw-r--r--pw_kvs/fake_flash_test_logical_sector_partition.cc58
-rw-r--r--pw_kvs/flash_memory.cc5
-rw-r--r--pw_kvs/flash_partition_stream_test.cc113
-rw-r--r--pw_kvs/key_value_store.cc330
-rw-r--r--pw_kvs/key_value_store_test.cc26
-rw-r--r--pw_kvs/public/pw_kvs/alignment.h18
-rw-r--r--pw_kvs/public/pw_kvs/checksum.h5
-rw-r--r--pw_kvs/public/pw_kvs/flash_memory.h27
-rw-r--r--pw_kvs/public/pw_kvs/flash_partition_with_logical_sectors.h71
-rw-r--r--pw_kvs/sectors.cc24
-rw-r--r--pw_kvs/size_report/base_with_only_flash.cc8
-rw-r--r--pw_kvs/size_report/with_kvs.cc6
-rw-r--r--pw_libc/BUILD.gn142
-rw-r--r--pw_libcxx/BUILD.bazel27
-rw-r--r--pw_libcxx/BUILD.gn33
-rw-r--r--pw_libcxx/__cxa_deleted_virtual.cc15
-rw-r--r--pw_libcxx/__cxa_pure_virtual.cc15
-rw-r--r--pw_libcxx/docs.rst9
-rw-r--r--pw_libcxx/operator_delete.cc37
-rw-r--r--pw_log/Android.bp154
-rw-r--r--pw_log/BUILD.bazel25
-rw-r--r--pw_log/BUILD.gn8
-rw-r--r--pw_log/CMakeLists.txt1
-rw-r--r--pw_log/basic_log_test.cc4
-rw-r--r--pw_log/docs.rst310
-rw-r--r--pw_log/glog_adapter_test.cc2
-rw-r--r--pw_log/public/pw_log/options.h2
-rw-r--r--pw_log/public/pw_log/tokenized_args.h52
-rw-r--r--pw_log/py/BUILD.bazel44
-rw-r--r--pw_log/py/BUILD.gn52
-rw-r--r--pw_log/py/log_decoder_test.py869
-rw-r--r--pw_log/py/pw_log/__init__.py13
-rw-r--r--pw_log/py/pw_log/log_decoder.py413
-rw-r--r--pw_log/py/pyproject.toml16
-rw-r--r--pw_log/py/setup.cfg25
-rw-r--r--pw_log/tokenized_args.rst76
-rw-r--r--pw_log_basic/Android.bp40
-rw-r--r--pw_log_basic/BUILD.bazel6
-rw-r--r--pw_log_basic/log_basic.cc2
-rw-r--r--pw_log_basic/public/pw_log_basic/log_basic.h2
-rw-r--r--pw_log_rpc/BUILD.gn12
-rw-r--r--pw_log_rpc/docs.rst60
-rw-r--r--pw_log_rpc/log_service_test.cc18
-rw-r--r--pw_log_rpc/py/BUILD.bazel43
-rw-r--r--pw_log_rpc/py/BUILD.gn52
-rw-r--r--pw_log_rpc/py/pw_log_rpc/__init__.py13
-rw-r--r--pw_log_rpc/py/pw_log_rpc/rpc_log_stream.py87
-rw-r--r--pw_log_rpc/py/pyproject.toml16
-rw-r--r--pw_log_rpc/py/rpc_log_stream_test.py356
-rw-r--r--pw_log_rpc/py/setup.cfg25
-rw-r--r--pw_log_rpc/rpc_log_drain.cc11
-rw-r--r--pw_log_string/BUILD.bazel26
-rw-r--r--pw_log_string/BUILD.gn34
-rw-r--r--pw_log_string/CMakeLists.txt16
-rw-r--r--pw_log_string/docs.rst160
-rw-r--r--pw_log_string/public/pw_log_string/config.h (renamed from pw_log_string/public/pw_log_string/log_string.h)33
-rw-r--r--pw_log_string/public/pw_log_string/handler.h16
-rw-r--r--pw_log_string/public_overrides/pw_log_backend/log_backend.h13
-rw-r--r--pw_log_tokenized/Android.bp25
-rw-r--r--pw_log_tokenized/BUILD.bazel60
-rw-r--r--pw_log_tokenized/BUILD.gn97
-rw-r--r--pw_log_tokenized/CMakeLists.txt74
-rw-r--r--pw_log_tokenized/base64_over_hdlc.cc17
-rw-r--r--pw_log_tokenized/compatibility.cc60
-rw-r--r--pw_log_tokenized/docs.rst20
-rw-r--r--pw_log_tokenized/gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h24
-rw-r--r--pw_log_tokenized/log_tokenized.cc4
-rw-r--r--pw_log_tokenized/log_tokenized_test.cc17
-rw-r--r--pw_log_tokenized/log_tokenized_test_c.c4
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/base64.h51
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/config.h30
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/gcc_partially_tokenized.h48
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h6
-rw-r--r--pw_log_tokenized/public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h21
-rw-r--r--pw_log_tokenized/py/BUILD.gn1
-rw-r--r--pw_log_tokenized/py/setup.py18
-rw-r--r--pw_log_zephyr/CMakeLists.txt99
-rw-r--r--pw_log_zephyr/Kconfig115
-rw-r--r--pw_log_zephyr/Kconfig.tokenized53
-rw-r--r--pw_log_zephyr/docs.rst14
-rw-r--r--pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc45
-rw-r--r--pw_log_zephyr/py/pw_log_zephyr/pw_zephyr_detokenizer.py238
-rw-r--r--pw_log_zephyr/py/pyproject.toml (renamed from pw_trace_tokenized/py/pyproject.toml)0
-rw-r--r--pw_log_zephyr/py/setup.cfg (renamed from pw_trace_tokenized/py/setup.cfg)9
-rw-r--r--pw_log_zephyr/zephyr_public_overrides/zephyr_custom_log.h (renamed from pw_log_zephyr/public_overrides/zephyr_custom_log.h)21
-rw-r--r--pw_malloc/BUILD.bazel9
-rw-r--r--pw_malloc/docs.rst7
-rw-r--r--pw_malloc_freelist/BUILD.bazel2
-rw-r--r--pw_metric/Android.bp56
-rw-r--r--pw_metric/BUILD.bazel8
-rw-r--r--pw_metric/BUILD.gn3
-rw-r--r--pw_metric/CMakeLists.txt35
-rw-r--r--pw_metric/docs.rst651
-rw-r--r--pw_metric/metric.cc4
-rw-r--r--pw_metric/metric_service_nanopb.cc2
-rw-r--r--pw_metric/public/pw_metric/metric.h10
-rw-r--r--pw_metric/py/BUILD.bazel5
-rw-r--r--pw_minimal_cpp_stdlib/BUILD.bazel44
-rw-r--r--pw_minimal_cpp_stdlib/BUILD.gn52
-rw-r--r--pw_minimal_cpp_stdlib/CMakeLists.txt4
-rw-r--r--pw_minimal_cpp_stdlib/Kconfig23
-rw-r--r--pw_minimal_cpp_stdlib/docs.rst42
-rw-r--r--pw_minimal_cpp_stdlib/isolated_test.cc54
l---------pw_minimal_cpp_stdlib/public/algorithm2
l---------pw_minimal_cpp_stdlib/public/array2
l---------pw_minimal_cpp_stdlib/public/cinttypes2
l---------pw_minimal_cpp_stdlib/public/climits2
l---------pw_minimal_cpp_stdlib/public/cmath2
l---------pw_minimal_cpp_stdlib/public/cstdarg2
l---------pw_minimal_cpp_stdlib/public/cstddef2
l---------pw_minimal_cpp_stdlib/public/cstdint2
l---------pw_minimal_cpp_stdlib/public/cstdio2
l---------pw_minimal_cpp_stdlib/public/cstring2
l---------pw_minimal_cpp_stdlib/public/functional1
l---------pw_minimal_cpp_stdlib/public/initializer_list2
l---------pw_minimal_cpp_stdlib/public/iterator2
l---------pw_minimal_cpp_stdlib/public/limits2
l---------pw_minimal_cpp_stdlib/public/memory1
l---------pw_minimal_cpp_stdlib/public/new2
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/algorithm.h (renamed from pw_minimal_cpp_stdlib/public/internal/algorithm.h)12
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/array.h (renamed from pw_minimal_cpp_stdlib/public/internal/array.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cinttypes.h (renamed from pw_minimal_cpp_stdlib/public/internal/cinttypes.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/climits.h (renamed from pw_minimal_cpp_stdlib/public/internal/climits.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cmath.h (renamed from pw_minimal_cpp_stdlib/public/internal/cmath.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdarg.h (renamed from pw_minimal_cpp_stdlib/public/internal/cstdarg.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstddef.h (renamed from pw_minimal_cpp_stdlib/public/internal/cstddef.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdint.h (renamed from pw_minimal_cpp_stdlib/public/internal/cstdint.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdio.h (renamed from pw_minimal_cpp_stdlib/public/internal/cstdio.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstring.h (renamed from pw_minimal_cpp_stdlib/public/internal/cstring.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/functional.h39
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/initializer_list.h (renamed from pw_minimal_cpp_stdlib/public/internal/initializer_list.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/iterator.h (renamed from pw_minimal_cpp_stdlib/public/internal/iterator.h)6
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/limits.h (renamed from pw_minimal_cpp_stdlib/public/internal/limits.h)47
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/memory.h39
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/new.h (renamed from pw_minimal_cpp_stdlib/public/internal/new.h)0
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string.h73
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string_view.h (renamed from pw_minimal_cpp_stdlib/public/internal/string_view.h)4
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/type_traits.h (renamed from pw_minimal_cpp_stdlib/public/internal/type_traits.h)149
-rw-r--r--pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/utility.h (renamed from pw_minimal_cpp_stdlib/public/internal/utility.h)20
l---------pw_minimal_cpp_stdlib/public/string1
l---------pw_minimal_cpp_stdlib/public/string_view2
l---------pw_minimal_cpp_stdlib/public/type_traits2
l---------pw_minimal_cpp_stdlib/public/utility2
-rw-r--r--pw_module/py/BUILD.gn1
-rw-r--r--pw_module/py/setup.py18
-rw-r--r--pw_multibuf/BUILD.bazel76
-rw-r--r--pw_multibuf/BUILD.gn82
-rw-r--r--pw_multibuf/CMakeLists.txt75
-rw-r--r--pw_multibuf/chunk.cc247
-rw-r--r--pw_multibuf/chunk_test.cc430
-rw-r--r--pw_multibuf/docs.rst87
-rw-r--r--pw_multibuf/multibuf.cc129
-rw-r--r--pw_multibuf/multibuf_test.cc214
-rw-r--r--pw_multibuf/public/pw_multibuf/chunk.h372
-rw-r--r--pw_multibuf/public/pw_multibuf/internal/test_utils.h162
-rw-r--r--pw_multibuf/public/pw_multibuf/multibuf.h373
-rw-r--r--pw_multisink/BUILD.gn3
-rw-r--r--pw_multisink/Kconfig38
-rw-r--r--pw_multisink/docs.rst9
-rw-r--r--pw_multisink/public/pw_multisink/multisink.h10
-rw-r--r--pw_multisink/public/pw_multisink/util.h2
-rw-r--r--pw_multisink/util.cc10
-rw-r--r--pw_package/docs.rst40
-rw-r--r--pw_package/py/BUILD.gn8
-rw-r--r--pw_package/py/pw_package/git_repo.py36
-rw-r--r--pw_package/py/pw_package/package_manager.py18
-rw-r--r--pw_package/py/pw_package/packages/abseil_cpp.py46
-rw-r--r--pw_package/py/pw_package/packages/arduino_core.py54
-rw-r--r--pw_package/py/pw_package/packages/boringssl.py2
-rw-r--r--pw_package/py/pw_package/packages/chre.py43
-rw-r--r--pw_package/py/pw_package/packages/emboss.py10
-rw-r--r--pw_package/py/pw_package/packages/fuzztest.py46
-rw-r--r--pw_package/py/pw_package/packages/googletest.py11
-rw-r--r--pw_package/py/pw_package/packages/mbedtls.py3
-rw-r--r--pw_package/py/pw_package/packages/mcuxpresso.py48
-rw-r--r--pw_package/py/pw_package/packages/micro_ecc.py7
-rw-r--r--pw_package/py/pw_package/packages/pico_sdk.py7
-rw-r--r--pw_package/py/pw_package/packages/picotool.py94
-rw-r--r--pw_package/py/pw_package/packages/protobuf.py7
-rw-r--r--pw_package/py/pw_package/packages/re2.py46
-rw-r--r--pw_package/py/pw_package/packages/stm32cube.py5
-rw-r--r--pw_package/py/pw_package/packages/zephyr.py142
-rw-r--r--pw_package/py/pw_package/pigweed_packages.py7
-rw-r--r--pw_package/py/setup.py18
-rw-r--r--pw_perf_test/BUILD.bazel173
-rw-r--r--pw_perf_test/BUILD.gn160
-rw-r--r--pw_perf_test/CMakeLists.txt116
-rw-r--r--pw_perf_test/docs.rst68
-rw-r--r--pw_perf_test/examples/example_perf_test.cc (renamed from pw_perf_test/performance_test_generic.cc)6
-rw-r--r--pw_perf_test/framework.cc53
-rw-r--r--pw_perf_test/perf_test.cc80
-rw-r--r--pw_perf_test/perf_test.gni33
-rw-r--r--pw_perf_test/perf_test_test.cc44
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h2
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/framework.h57
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/test_info.h50
-rw-r--r--pw_perf_test/public/pw_perf_test/perf_test.h141
-rw-r--r--pw_perf_test/public/pw_perf_test/state.h87
-rw-r--r--pw_perf_test/state.cc66
-rw-r--r--pw_perf_test/state_test.cc3
-rw-r--r--pw_perf_test/test_info.cc28
-rw-r--r--pw_perf_test/timer_test.cc4
-rw-r--r--pw_persistent_ram/BUILD.bazel22
-rw-r--r--pw_persistent_ram/BUILD.gn18
-rw-r--r--pw_persistent_ram/CMakeLists.txt20
-rw-r--r--pw_persistent_ram/flat_file_system_entry_test.cc127
-rw-r--r--pw_persistent_ram/public/pw_persistent_ram/flat_file_system_entry.h73
-rw-r--r--pw_persistent_ram/public/pw_persistent_ram/persistent.h2
-rw-r--r--pw_polyfill/BUILD.bazel39
-rw-r--r--pw_polyfill/BUILD.gn57
-rw-r--r--pw_polyfill/CMakeLists.txt5
-rw-r--r--pw_polyfill/Kconfig10
-rw-r--r--pw_polyfill/README.md2
-rw-r--r--pw_polyfill/docs.rst35
-rw-r--r--pw_polyfill/public/pw_polyfill/language_feature_macros.h4
-rw-r--r--pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h75
-rw-r--r--pw_polyfill/standard_library_public/pw_polyfill/standard_library/iterator.h55
-rw-r--r--pw_polyfill/test.cc36
-rw-r--r--pw_preprocessor/CMakeLists.txt4
-rw-r--r--pw_preprocessor/Kconfig8
-rw-r--r--pw_preprocessor/arguments_test.cc70
-rw-r--r--pw_preprocessor/public/pw_preprocessor/arch.h2
-rw-r--r--pw_preprocessor/public/pw_preprocessor/compiler.h2
-rw-r--r--pw_presubmit/docs.rst622
-rw-r--r--pw_presubmit/py/BUILD.gn7
-rw-r--r--pw_presubmit/py/bazel_parser_test.py425
-rw-r--r--pw_presubmit/py/context_test.py84
-rw-r--r--pw_presubmit/py/cpp_checks_test.py125
-rw-r--r--pw_presubmit/py/gitmodules_test.py11
-rw-r--r--pw_presubmit/py/ninja_parser_test.py96
-rwxr-xr-xpw_presubmit/py/presubmit_test.py11
-rw-r--r--pw_presubmit/py/pw_presubmit/bazel_parser.py20
-rw-r--r--pw_presubmit/py/pw_presubmit/build.py506
-rw-r--r--pw_presubmit/py/pw_presubmit/cli.py63
-rw-r--r--pw_presubmit/py/pw_presubmit/cpp_checks.py126
-rwxr-xr-xpw_presubmit/py/pw_presubmit/format_code.py317
-rw-r--r--pw_presubmit/py/pw_presubmit/git_repo.py15
-rw-r--r--pw_presubmit/py/pw_presubmit/gitmodules.py18
-rw-r--r--pw_presubmit/py/pw_presubmit/inclusive_language.py15
-rw-r--r--pw_presubmit/py/pw_presubmit/javascript_checks.py44
-rw-r--r--pw_presubmit/py/pw_presubmit/json_check.py38
-rw-r--r--pw_presubmit/py/pw_presubmit/keep_sorted.py10
-rw-r--r--pw_presubmit/py/pw_presubmit/module_owners.py4
-rw-r--r--pw_presubmit/py/pw_presubmit/ninja_parser.py115
-rw-r--r--pw_presubmit/py/pw_presubmit/npm_presubmit.py3
-rw-r--r--pw_presubmit/py/pw_presubmit/owners_checks.py31
-rwxr-xr-xpw_presubmit/py/pw_presubmit/pigweed_presubmit.py441
-rw-r--r--pw_presubmit/py/pw_presubmit/presubmit.py474
-rw-r--r--pw_presubmit/py/pw_presubmit/presubmit_context.py624
-rw-r--r--pw_presubmit/py/pw_presubmit/python_checks.py230
-rw-r--r--pw_presubmit/py/pw_presubmit/rst_format.py246
-rw-r--r--pw_presubmit/py/pw_presubmit/shell_checks.py23
-rw-r--r--pw_presubmit/py/pw_presubmit/source_in_build.py7
-rw-r--r--pw_presubmit/py/pw_presubmit/todo_check.py37
-rw-r--r--pw_presubmit/py/pw_presubmit/tools.py66
-rw-r--r--pw_presubmit/py/setup.py18
-rw-r--r--pw_presubmit/py/todo_check_test.py174
-rwxr-xr-xpw_presubmit/py/tools_test.py52
-rw-r--r--pw_protobuf/Android.bp120
-rw-r--r--pw_protobuf/BUILD.bazel51
-rw-r--r--pw_protobuf/BUILD.gn63
-rw-r--r--pw_protobuf/CMakeLists.txt3
-rw-r--r--pw_protobuf/codegen_decoder_test.cc286
-rw-r--r--pw_protobuf/codegen_encoder_test.cc8
-rw-r--r--pw_protobuf/codegen_message_test.cc80
-rw-r--r--pw_protobuf/decoder.cc14
-rw-r--r--pw_protobuf/docs.rst2073
-rw-r--r--pw_protobuf/encoder.cc8
-rw-r--r--pw_protobuf/encoder_fuzzer.cc2
-rw-r--r--pw_protobuf/encoder_perf_test.cc2
-rw-r--r--pw_protobuf/encoder_test.cc4
-rw-r--r--pw_protobuf/find.cc49
-rw-r--r--pw_protobuf/find_test.cc129
-rw-r--r--pw_protobuf/public/pw_protobuf/encoder.h16
-rw-r--r--pw_protobuf/public/pw_protobuf/find.h742
-rw-r--r--pw_protobuf/public/pw_protobuf/internal/codegen.h8
-rw-r--r--pw_protobuf/public/pw_protobuf/serialized_size.h7
-rw-r--r--pw_protobuf/public/pw_protobuf/stream_decoder.h4
-rw-r--r--pw_protobuf/py/BUILD.bazel7
-rw-r--r--pw_protobuf/py/BUILD.gn1
-rw-r--r--pw_protobuf/py/pw_protobuf/codegen_pwpb.py742
-rw-r--r--pw_protobuf/py/pw_protobuf/proto_tree.py7
-rw-r--r--pw_protobuf/py/setup.cfg6
-rw-r--r--pw_protobuf/py/setup.py18
-rw-r--r--pw_protobuf/stream_decoder.cc15
-rw-r--r--pw_protobuf_compiler/BUILD.bazel2
-rw-r--r--pw_protobuf_compiler/deps.bzl7
-rw-r--r--pw_protobuf_compiler/docs.rst384
-rw-r--r--pw_protobuf_compiler/proto.bzl32
-rw-r--r--pw_protobuf_compiler/proto.gni39
-rw-r--r--pw_protobuf_compiler/pw_nanopb_cc_library.bzl37
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/BUILD.bazel2
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel2
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel2
-rw-r--r--pw_protobuf_compiler/pw_proto_library.bzl441
-rw-r--r--pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel7
-rw-r--r--pw_protobuf_compiler/py/BUILD.gn1
-rw-r--r--pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py11
-rw-r--r--pw_protobuf_compiler/py/setup.cfg12
-rw-r--r--pw_protobuf_compiler/py/setup.py18
-rw-r--r--pw_protobuf_compiler/toolchain.gni2
-rw-r--r--pw_protobuf_compiler/ts/build.ts50
-rw-r--r--pw_protobuf_compiler/ts/codegen/template_replacement.ts19
-rw-r--r--pw_protobuf_compiler/ts/proto_collection.ts39
-rw-r--r--pw_protobuf_compiler/ts/ts_proto_collection.template.ts13
-rw-r--r--pw_protobuf_compiler/ts/ts_proto_collection_test.ts6
-rw-r--r--pw_random/BUILD.gn12
-rw-r--r--pw_random/docs.rst37
-rw-r--r--pw_random/public/pw_random/random.h44
-rw-r--r--pw_random/public/pw_random/xor_shift.h44
-rw-r--r--pw_result/BUILD.bazel18
-rw-r--r--pw_result/BUILD.gn16
-rw-r--r--pw_result/CMakeLists.txt20
-rw-r--r--pw_result/Kconfig8
-rw-r--r--pw_result/docs.rst11
-rw-r--r--pw_result/expected_test.cc249
-rw-r--r--pw_result/public/pw_result/expected.h48
-rw-r--r--pw_result/public/pw_result/internal/expected_impl.h1011
-rw-r--r--pw_result/public/pw_result/internal/result_internal.h8
-rw-r--r--pw_ring_buffer/BUILD.bazel4
-rw-r--r--pw_ring_buffer/BUILD.gn7
-rw-r--r--pw_ring_buffer/docs.rst24
-rw-r--r--pw_ring_buffer/prefixed_entry_ring_buffer.cc19
-rw-r--r--pw_ring_buffer/prefixed_entry_ring_buffer_test.cc6
-rw-r--r--pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h9
-rw-r--r--pw_ring_buffer/size_report/ring_buffer_multi.cc4
-rw-r--r--pw_router/Android.bp77
-rw-r--r--pw_router/CMakeLists.txt12
-rw-r--r--pw_router/Kconfig20
-rw-r--r--pw_router/docs.rst6
-rw-r--r--pw_rpc/Android.bp360
-rw-r--r--pw_rpc/BUILD.bazel86
-rw-r--r--pw_rpc/BUILD.gn83
-rw-r--r--pw_rpc/CMakeLists.txt18
-rw-r--r--pw_rpc/Kconfig20
-rw-r--r--pw_rpc/benchmark.cc37
-rw-r--r--pw_rpc/bidirectional_streaming_rpc.svg86
-rw-r--r--pw_rpc/bidirectional_streaming_rpc_cancelled.svg94
-rw-r--r--pw_rpc/call.cc77
-rw-r--r--pw_rpc/call_test.cc41
-rw-r--r--pw_rpc/callback_test.cc23
-rw-r--r--pw_rpc/client.cc5
-rw-r--r--pw_rpc/client_call.cc7
-rw-r--r--pw_rpc/client_integration_test.cc61
-rw-r--r--pw_rpc/client_streaming_rpc.svg69
-rw-r--r--pw_rpc/client_streaming_rpc_cancelled.svg76
-rw-r--r--pw_rpc/docs.rst1475
-rw-r--r--pw_rpc/fake_channel_output.cc2
-rw-r--r--pw_rpc/fuzz/BUILD.gn12
-rw-r--r--pw_rpc/fuzz/alarm_timer_test.cc6
-rw-r--r--pw_rpc/fuzz/argparse.cc4
-rw-r--r--pw_rpc/fuzz/argparse_test.cc7
-rw-r--r--pw_rpc/fuzz/engine.cc2
-rw-r--r--pw_rpc/fuzz/engine_test.cc32
-rw-r--r--pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h1
-rw-r--r--pw_rpc/internal/packet.proto8
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java2
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java40
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java6
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java2
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java145
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel14
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java12
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java12
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java6
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java70
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java8
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java21
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java7
-rw-r--r--pw_rpc/method_test.cc2
-rw-r--r--pw_rpc/nanopb/Android.bp6
-rw-r--r--pw_rpc/nanopb/BUILD.bazel25
-rw-r--r--pw_rpc/nanopb/BUILD.gn27
-rw-r--r--pw_rpc/nanopb/CMakeLists.txt45
-rw-r--r--pw_rpc/nanopb/Kconfig32
-rw-r--r--pw_rpc/nanopb/callback_test.cc269
-rw-r--r--pw_rpc/nanopb/client_reader_writer_test.cc78
-rw-r--r--pw_rpc/nanopb/client_server_context_test.cc146
-rw-r--r--pw_rpc/nanopb/client_server_context_threaded_test.cc163
-rw-r--r--pw_rpc/nanopb/docs.rst38
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h27
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h36
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h40
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h21
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h3
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h14
-rw-r--r--pw_rpc/packet.cc22
-rw-r--r--pw_rpc/packet_meta.cc2
-rw-r--r--pw_rpc/packet_meta_test.cc29
-rw-r--r--pw_rpc/packet_test.cc36
-rw-r--r--pw_rpc/public/pw_rpc/benchmark.h11
-rw-r--r--pw_rpc/public/pw_rpc/channel.h2
-rw-r--r--pw_rpc/public/pw_rpc/internal/call.h85
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_call.h13
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_server_testing.h37
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h16
-rw-r--r--pw_rpc/public/pw_rpc/internal/config.h50
-rw-r--r--pw_rpc/public/pw_rpc/internal/fake_channel_output.h4
-rw-r--r--pw_rpc/public/pw_rpc/internal/hash.h6
-rw-r--r--pw_rpc/public/pw_rpc/internal/method_lookup.h6
-rw-r--r--pw_rpc/public/pw_rpc/internal/packet.h5
-rw-r--r--pw_rpc/public/pw_rpc/internal/server_call.h65
-rw-r--r--pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h200
-rw-r--r--pw_rpc/public/pw_rpc/internal/test_method_context.h2
-rw-r--r--pw_rpc/public/pw_rpc/packet_meta.h9
-rw-r--r--pw_rpc/public/pw_rpc/server.h5
-rw-r--r--pw_rpc/public/pw_rpc/synchronous_call.h460
-rw-r--r--pw_rpc/public/pw_rpc/writer.h2
-rw-r--r--pw_rpc/pw_rpc_private/fake_server_reader_writer.h6
-rw-r--r--pw_rpc/pw_rpc_private/test_method.h (renamed from pw_rpc/public/pw_rpc/internal/test_method.h)20
-rw-r--r--pw_rpc/pwpb/Android.bp48
-rw-r--r--pw_rpc/pwpb/BUILD.bazel21
-rw-r--r--pw_rpc/pwpb/BUILD.gn11
-rw-r--r--pw_rpc/pwpb/CMakeLists.txt31
-rw-r--r--pw_rpc/pwpb/client_reader_writer_test.cc77
-rw-r--r--pw_rpc/pwpb/client_server_context_test.cc119
-rw-r--r--pw_rpc/pwpb/client_server_context_threaded_test.cc139
-rw-r--r--pw_rpc/pwpb/codegen_test.cc46
-rw-r--r--pw_rpc/pwpb/docs.rst54
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h150
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h35
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h37
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h2
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h3
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h6
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h6
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h14
-rw-r--r--pw_rpc/pwpb/synchronous_call_test.cc75
-rw-r--r--pw_rpc/py/Android.bp15
-rw-r--r--pw_rpc/py/BUILD.bazel18
-rw-r--r--pw_rpc/py/BUILD.gn28
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/call.py9
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/impl.py40
-rw-r--r--pw_rpc/py/pw_rpc/client.py61
-rw-r--r--pw_rpc/py/pw_rpc/codegen.py45
-rw-r--r--pw_rpc/py/pw_rpc/codegen_nanopb.py99
-rw-r--r--pw_rpc/py/pw_rpc/codegen_pwpb.py103
-rw-r--r--pw_rpc/py/pw_rpc/codegen_raw.py8
-rw-r--r--pw_rpc/py/pw_rpc/console_tools/console.py7
-rw-r--r--pw_rpc/py/pw_rpc/descriptors.py26
-rw-r--r--pw_rpc/py/pw_rpc/packets.py67
-rw-r--r--pw_rpc/py/pw_rpc/testing.py2
-rwxr-xr-xpw_rpc/py/tests/callback_client_test.py343
-rwxr-xr-xpw_rpc/py/tests/client_test.py117
-rwxr-xr-xpw_rpc/py/tests/packets_test.py56
-rwxr-xr-xpw_rpc/py/tests/python_client_cpp_server_test.py49
-rw-r--r--pw_rpc/raw/Android.bp4
-rw-r--r--pw_rpc/raw/BUILD.bazel13
-rw-r--r--pw_rpc/raw/BUILD.gn17
-rw-r--r--pw_rpc/raw/client_reader_writer_test.cc81
-rw-r--r--pw_rpc/raw/client_test.cc182
-rw-r--r--pw_rpc/raw/codegen_test.cc2
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h23
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/client_testing.h3
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h8
-rw-r--r--pw_rpc/raw/synchronous_call_test.cc241
-rw-r--r--pw_rpc/request_packets.svg42
-rw-r--r--pw_rpc/response_packets.svg42
-rw-r--r--pw_rpc/server.cc41
-rw-r--r--pw_rpc/server_call.cc7
-rw-r--r--pw_rpc/server_streaming_rpc.svg58
-rw-r--r--pw_rpc/server_streaming_rpc_cancelled.svg77
-rw-r--r--pw_rpc/server_test.cc89
-rw-r--r--pw_rpc/size_report/base.cc4
-rw-r--r--pw_rpc/size_report/server_only.cc6
-rw-r--r--pw_rpc/system_server/BUILD.bazel4
-rw-r--r--pw_rpc/ts/call.ts26
-rw-r--r--pw_rpc/ts/call_test.ts12
-rw-r--r--pw_rpc/ts/client.ts36
-rw-r--r--pw_rpc/ts/client_test.ts99
-rw-r--r--pw_rpc/ts/descriptors.ts19
-rw-r--r--pw_rpc/ts/descriptors_test.ts14
-rw-r--r--pw_rpc/ts/method.ts122
-rw-r--r--pw_rpc/ts/packets.ts59
-rw-r--r--pw_rpc/ts/packets_test.ts8
-rw-r--r--pw_rpc/ts/queue.ts2
-rw-r--r--pw_rpc/ts/rpc_classes.ts10
-rw-r--r--pw_rpc/unary_rpc.svg47
-rw-r--r--pw_rpc/unary_rpc_cancelled.svg59
-rw-r--r--pw_rpc_transport/Android.bp153
-rw-r--r--pw_rpc_transport/BUILD.bazel338
-rw-r--r--pw_rpc_transport/BUILD.gn314
-rw-r--r--pw_rpc_transport/CMakeLists.txt221
-rw-r--r--pw_rpc_transport/OWNERS2
-rw-r--r--pw_rpc_transport/docs.rst249
-rw-r--r--pw_rpc_transport/egress_ingress.cc49
-rw-r--r--pw_rpc_transport/egress_ingress_test.cc406
-rw-r--r--pw_rpc_transport/hdlc_framing_test.cc193
-rw-r--r--pw_rpc_transport/internal/packet_buffer_queue_test.cc83
-rw-r--r--pw_rpc_transport/internal/test.options (renamed from .prettierrc.js)4
-rw-r--r--pw_rpc_transport/internal/test.proto25
-rw-r--r--pw_rpc_transport/local_rpc_egress.cc47
-rw-r--r--pw_rpc_transport/local_rpc_egress_test.cc224
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/egress_ingress.h170
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/hdlc_framing.h111
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/internal/packet_buffer_queue.h92
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/local_rpc_egress.h144
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/rpc_transport.h108
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/service_registry.h57
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/simple_framing.h252
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/socket_rpc_transport.h232
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/stream_rpc_dispatcher.h77
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/stream_rpc_frame_sender.h40
-rw-r--r--pw_rpc_transport/public/pw_rpc_transport/test_loopback_service_registry.h120
-rw-r--r--pw_rpc_transport/rpc_integration_test.cc136
-rw-r--r--pw_rpc_transport/simple_framing.cc37
-rw-r--r--pw_rpc_transport/simple_framing_test.cc381
-rw-r--r--pw_rpc_transport/socket_rpc_transport.cc54
-rw-r--r--pw_rpc_transport/socket_rpc_transport_test.cc364
-rw-r--r--pw_rpc_transport/stream_rpc_dispatcher_test.cc141
-rw-r--r--pw_rust/BUILD.bazel20
-rw-r--r--pw_rust/bazel_patches/0001-rustdoc_test-Apply-prefix-stripping-to-proc_macro-de.patch28
-rw-r--r--pw_rust/bazel_patches/0002-PROTOTYPE-Add-ability-to-document-multiple-crates-at.patch370
-rw-r--r--pw_rust/bazel_patches/BUILD.bazel0
-rw-r--r--pw_rust/docs.rst6
-rw-r--r--pw_rust/examples/embedded_hello/BUILD.bazel8
-rw-r--r--pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld2
-rw-r--r--pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld2
-rw-r--r--pw_rust/examples/host_executable/BUILD.gn29
-rw-r--r--pw_rust/examples/host_executable/proc_macro/lib.rs21
-rw-r--r--pw_rust/examples/host_executable/proc_macro/test.rs21
-rw-r--r--pw_snapshot/BUILD.bazel6
-rw-r--r--pw_snapshot/proto_format.rst86
-rw-r--r--pw_snapshot/py/BUILD.bazel6
-rw-r--r--pw_snapshot/py/metadata_test.py16
-rw-r--r--pw_snapshot/py/pw_snapshot_metadata/metadata.py2
-rw-r--r--pw_software_update/BUILD.bazel18
-rw-r--r--pw_software_update/BUILD.gn8
-rw-r--r--pw_software_update/bundled_update.proto2
-rw-r--r--pw_software_update/bundled_update_service.cc3
-rw-r--r--pw_software_update/cli.rst229
-rw-r--r--pw_software_update/design.rst235
-rw-r--r--pw_software_update/docs.rst487
-rw-r--r--pw_software_update/get_started.rst153
-rw-r--r--pw_software_update/guides.rst134
-rw-r--r--pw_software_update/public/pw_software_update/bundled_update_backend.h6
-rw-r--r--pw_software_update/public/pw_software_update/bundled_update_service.h3
-rw-r--r--pw_software_update/py/pw_software_update/generate_test_bundle.py4
-rw-r--r--pw_software_update/py/remote_sign_test.py2
-rw-r--r--pw_software_update/update_bundle_accessor.cc9
-rw-r--r--pw_software_update/update_bundle_test.cc4
-rw-r--r--pw_span/Android.bp2
-rw-r--r--pw_span/BUILD.bazel2
-rw-r--r--pw_span/CMakeLists.txt6
-rw-r--r--pw_span/Kconfig8
-rw-r--r--pw_span/docs.rst3
-rw-r--r--pw_span/public/pw_span/span.h2
-rw-r--r--pw_spi/BUILD.bazel17
-rw-r--r--pw_spi/BUILD.gn11
-rw-r--r--pw_spi/CMakeLists.txt59
-rw-r--r--pw_spi/docs.rst62
-rw-r--r--pw_spi/public/pw_spi/chip_selector.h2
-rw-r--r--pw_spi/public/pw_spi/device.h18
-rw-r--r--pw_spi/public/pw_spi/initiator.h7
-rw-r--r--pw_spi/public/pw_spi/responder.h56
-rw-r--r--pw_spi/spi_test.cc15
-rw-r--r--pw_spi_mcuxpresso/BUILD.bazel61
-rw-r--r--pw_spi_mcuxpresso/BUILD.gn114
-rw-r--r--pw_spi_mcuxpresso/OWNERS2
-rw-r--r--pw_spi_mcuxpresso/docs.rst61
-rw-r--r--pw_spi_mcuxpresso/flexspi.cc221
-rw-r--r--pw_spi_mcuxpresso/flexspi_test.cc97
-rw-r--r--pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/flexspi.h86
-rw-r--r--pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/spi.h99
-rw-r--r--pw_spi_mcuxpresso/spi.cc184
-rw-r--r--pw_spi_mcuxpresso/spi_test.cc102
-rw-r--r--pw_status/CMakeLists.txt3
-rw-r--r--pw_status/Kconfig8
-rw-r--r--pw_status/docs.rst670
-rw-r--r--pw_status/public/pw_status/status.h197
-rw-r--r--pw_status/public/pw_status/status_with_size.h3
-rw-r--r--pw_status/py/BUILD.gn1
-rw-r--r--pw_status/py/pw_status/__init__.py7
-rw-r--r--pw_status/py/setup.py18
-rw-r--r--pw_status/rust/BUILD.bazel44
-rw-r--r--pw_status/rust/pw_status.rs116
-rw-r--r--pw_status/ts/status.ts8
-rw-r--r--pw_stm32cube_build/docs.rst194
-rw-r--r--pw_stm32cube_build/py/BUILD.gn1
-rw-r--r--pw_stm32cube_build/py/pw_stm32cube_build/find_files.py11
-rw-r--r--pw_stm32cube_build/py/setup.py18
-rw-r--r--pw_stm32cube_build/py/tests/find_files_test.py14
-rw-r--r--pw_stream/Android.bp28
-rw-r--r--pw_stream/BUILD.bazel32
-rw-r--r--pw_stream/BUILD.gn40
-rw-r--r--pw_stream/CMakeLists.txt38
-rw-r--r--pw_stream/Kconfig8
-rw-r--r--pw_stream/docs.rst522
-rw-r--r--pw_stream/mpsc_stream.cc523
-rw-r--r--pw_stream/mpsc_stream_test.cc601
-rw-r--r--pw_stream/public/pw_stream/mpsc_stream.h394
-rw-r--r--pw_stream/public/pw_stream/socket_stream.h23
-rw-r--r--pw_stream/public/pw_stream/stream.h539
-rw-r--r--pw_stream/rust/BUILD.bazel52
-rw-r--r--pw_stream/rust/pw_stream/cursor.rs534
-rw-r--r--pw_stream/rust/pw_stream/integer.rs366
-rw-r--r--pw_stream/rust/pw_stream/lib.rs360
-rw-r--r--pw_stream/socket_stream.cc130
-rw-r--r--pw_stream_shmem_mcuxpresso/BUILD.bazel48
-rw-r--r--pw_stream_shmem_mcuxpresso/BUILD.gn59
-rw-r--r--pw_stream_shmem_mcuxpresso/docs.rst128
-rw-r--r--pw_stream_shmem_mcuxpresso/public/pw_stream_shmem_mcuxpresso/stream.h64
-rw-r--r--pw_stream_shmem_mcuxpresso/stream.cc109
-rw-r--r--pw_stream_shmem_mcuxpresso/stream_test.cc31
-rw-r--r--pw_stream_uart_linux/Android.bp50
-rw-r--r--pw_stream_uart_linux/BUILD.bazel49
-rw-r--r--pw_stream_uart_linux/BUILD.gn45
-rw-r--r--pw_stream_uart_linux/OWNERS1
-rw-r--r--pw_stream_uart_linux/docs.rst37
-rw-r--r--pw_stream_uart_linux/public/pw_stream_uart_linux/stream.h65
-rw-r--r--pw_stream_uart_linux/stream.cc149
-rw-r--r--pw_stream_uart_linux/stream_test.cc39
-rw-r--r--pw_stream_uart_mcuxpresso/BUILD.bazel47
-rw-r--r--pw_stream_uart_mcuxpresso/BUILD.gn60
-rw-r--r--pw_stream_uart_mcuxpresso/OWNERS2
-rw-r--r--pw_stream_uart_mcuxpresso/docs.rst44
-rw-r--r--pw_stream_uart_mcuxpresso/public/pw_stream_uart_mcuxpresso/stream.h53
-rw-r--r--pw_stream_uart_mcuxpresso/stream.cc56
-rw-r--r--pw_stream_uart_mcuxpresso/stream_test.cc33
-rw-r--r--pw_string/Android.bp10
-rw-r--r--pw_string/BUILD.bazel6
-rw-r--r--pw_string/BUILD.gn33
-rw-r--r--pw_string/CMakeLists.txt7
-rw-r--r--pw_string/Kconfig8
-rw-r--r--pw_string/api.rst16
-rw-r--r--pw_string/code_size.rst40
-rw-r--r--pw_string/design.rst29
-rw-r--r--pw_string/docs.rst168
-rw-r--r--pw_string/format.cc34
-rw-r--r--pw_string/format_test.cc61
-rw-r--r--pw_string/guide.rst125
-rw-r--r--pw_string/public/pw_string/format.h60
-rw-r--r--pw_string/public/pw_string/internal/string_common_functions.inc230
-rw-r--r--pw_string/public/pw_string/internal/string_impl.h92
-rw-r--r--pw_string/public/pw_string/string.h270
-rw-r--r--pw_string/public/pw_string/to_string.h2
-rw-r--r--pw_string/size_report/format_many_without_error_handling.cc2
-rw-r--r--pw_string/string_builder.cc8
-rw-r--r--pw_string/string_builder_test.cc23
-rw-r--r--pw_string/string_test.cc367
-rw-r--r--pw_string/to_string_test.cc11
-rw-r--r--pw_string/type_to_string.cc22
-rw-r--r--pw_string/type_to_string_test.cc8
-rw-r--r--pw_symbolizer/docs.rst15
-rw-r--r--pw_symbolizer/py/BUILD.bazel2
-rw-r--r--pw_symbolizer/py/llvm_symbolizer_test.py49
-rw-r--r--pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py42
-rw-r--r--pw_sync/Android.bp11
-rw-r--r--pw_sync/BUILD.bazel151
-rw-r--r--pw_sync/BUILD.gn41
-rw-r--r--pw_sync/CMakeLists.txt46
-rw-r--r--pw_sync/binary_semaphore_facade_test.cc2
-rw-r--r--pw_sync/borrow_test.cc301
-rw-r--r--pw_sync/condition_variable_test.cc3
-rw-r--r--pw_sync/counting_semaphore_facade_test.cc2
-rw-r--r--pw_sync/docs.rst31
-rw-r--r--pw_sync/interrupt_spin_lock_facade_test.cc19
-rw-r--r--pw_sync/lock_testing.cc44
-rw-r--r--pw_sync/lock_traits_test.cc62
-rw-r--r--pw_sync/mutex_facade_test.cc38
-rw-r--r--pw_sync/public/pw_sync/borrow.h51
-rw-r--r--pw_sync/public/pw_sync/interrupt_spin_lock.h28
-rw-r--r--pw_sync/public/pw_sync/lock_testing.h72
-rw-r--r--pw_sync/public/pw_sync/lock_traits.h123
-rw-r--r--pw_sync/public/pw_sync/mutex.h26
-rw-r--r--pw_sync/public/pw_sync/timed_mutex.h26
-rw-r--r--pw_sync/public/pw_sync/virtual_basic_lockable.h30
-rw-r--r--pw_sync/pw_sync_private/borrow_lockable_tests.h324
-rw-r--r--pw_sync/recursive_mutex_facade_test.cc2
-rw-r--r--pw_sync/thread_notification_facade_test.cc2
-rw-r--r--pw_sync/timed_mutex_facade_test.cc74
-rw-r--r--pw_sync/timed_thread_notification_facade_test.cc2
-rw-r--r--pw_sync_baremetal/BUILD.bazel16
-rw-r--r--pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h2
-rw-r--r--pw_sync_embos/BUILD.bazel101
-rw-r--r--pw_sync_freertos/BUILD.bazel157
-rw-r--r--pw_sync_freertos/BUILD.gn8
-rw-r--r--pw_sync_freertos/thread_notification_test.cc4
-rw-r--r--pw_sync_freertos/timed_thread_notification.cc1
-rw-r--r--pw_sync_freertos/timed_thread_notification_test.cc10
-rw-r--r--pw_sync_stl/Android.bp52
-rw-r--r--pw_sync_stl/BUILD.bazel88
-rw-r--r--pw_sync_stl/BUILD.gn2
-rw-r--r--pw_sync_threadx/BUILD.bazel99
-rw-r--r--pw_sync_zephyr/CMakeLists.txt124
-rw-r--r--pw_sync_zephyr/Kconfig29
-rw-r--r--pw_sync_zephyr/binary_semaphore.cc2
-rw-r--r--pw_sync_zephyr/docs.rst1
-rw-r--r--pw_sync_zephyr/interrupt_spin_lock.cc35
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_inline.h2
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_native.h2
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_inline.h36
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_native.h31
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_inline.h41
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_native.h23
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/timed_thread_notification_inline.h33
-rw-r--r--pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_inline.h16
-rw-r--r--pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_native.h16
-rw-r--r--pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_inline.h16
-rw-r--r--pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_native.h16
-rw-r--r--pw_sync_zephyr/public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h16
-rw-r--r--pw_sys_io/BUILD.bazel35
-rw-r--r--pw_sys_io/docs.rst22
-rw-r--r--pw_sys_io/public/pw_sys_io/sys_io.h123
-rw-r--r--pw_sys_io_ambiq_sdk/BUILD.bazel40
-rw-r--r--pw_sys_io_ambiq_sdk/BUILD.gn44
-rw-r--r--pw_sys_io_ambiq_sdk/docs.rst23
-rw-r--r--pw_sys_io_ambiq_sdk/public/pw_sys_io_ambiq_sdk/init.h23
-rw-r--r--pw_sys_io_ambiq_sdk/sys_io.cc138
-rw-r--r--pw_sys_io_arduino/BUILD.bazel10
-rw-r--r--pw_sys_io_arduino/docs.rst7
-rw-r--r--pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel6
-rw-r--r--pw_sys_io_baremetal_lm3s6965evb/docs.rst2
-rw-r--r--pw_sys_io_baremetal_stm32f429/BUILD.bazel6
-rw-r--r--pw_sys_io_baremetal_stm32f429/docs.rst35
-rw-r--r--pw_sys_io_emcraft_sf2/BUILD.bazel8
-rw-r--r--pw_sys_io_emcraft_sf2/docs.rst22
-rw-r--r--pw_sys_io_mcuxpresso/BUILD.bazel15
-rw-r--r--pw_sys_io_mcuxpresso/docs.rst20
-rw-r--r--pw_sys_io_pico/docs.rst8
-rw-r--r--pw_sys_io_rp2040/BUILD.bazel (renamed from pw_sys_io_pico/BUILD.bazel)12
-rw-r--r--pw_sys_io_rp2040/BUILD.gn (renamed from pw_sys_io_pico/BUILD.gn)2
-rw-r--r--pw_sys_io_rp2040/OWNERS (renamed from pw_sys_io_pico/OWNERS)0
-rw-r--r--pw_sys_io_rp2040/docs.rst8
-rw-r--r--pw_sys_io_rp2040/sys_io.cc (renamed from pw_sys_io_pico/sys_io.cc)0
-rw-r--r--pw_sys_io_stdio/BUILD.bazel5
-rw-r--r--pw_sys_io_stdio/docs.rst7
-rw-r--r--pw_sys_io_stm32cube/BUILD.bazel11
-rw-r--r--pw_sys_io_stm32cube/docs.rst24
-rw-r--r--pw_sys_io_zephyr/CMakeLists.txt10
-rw-r--r--pw_sys_io_zephyr/Kconfig8
-rw-r--r--pw_sys_io_zephyr/sys_io.cc3
-rw-r--r--pw_system/BUILD.bazel155
-rw-r--r--pw_system/BUILD.gn107
-rw-r--r--pw_system/CMakeLists.txt125
-rw-r--r--pw_system/Kconfig52
-rw-r--r--pw_system/backend.cmake3
-rw-r--r--pw_system/cli.rst12
-rw-r--r--pw_system/docs.rst186
-rw-r--r--pw_system/example_user_app_init.cc4
-rw-r--r--pw_system/file_manager.cc47
-rw-r--r--pw_system/file_service.cc32
-rw-r--r--pw_system/freertos_backends.gni1
-rw-r--r--pw_system/freertos_target_hooks.cc18
-rw-r--r--pw_system/hdlc_rpc_server.cc53
-rw-r--r--pw_system/init.cc35
-rw-r--r--pw_system/log.cc2
-rw-r--r--pw_system/public/pw_system/config.h32
-rw-r--r--pw_system/public/pw_system/file_manager.h61
-rw-r--r--pw_system/public/pw_system/file_service.h22
-rw-r--r--pw_system/public/pw_system/rpc_server.h7
-rw-r--r--pw_system/public/pw_system/target_hooks.h2
-rw-r--r--pw_system/public/pw_system/trace_service.h25
-rw-r--r--pw_system/public/pw_system/transfer_handlers.h37
-rw-r--r--pw_system/public/pw_system/transfer_service.h27
-rw-r--r--pw_system/py/BUILD.bazel6
-rw-r--r--pw_system/py/BUILD.gn8
-rw-r--r--pw_system/py/pw_system/console.py193
-rw-r--r--pw_system/py/pw_system/device.py218
-rw-r--r--pw_system/py/pw_system/device_tracing.py162
-rw-r--r--pw_system/py/pw_system/py.typed0
-rw-r--r--pw_system/py/pw_system/trace_client.py211
-rw-r--r--pw_system/py/setup.py18
-rw-r--r--pw_system/stl_backends.gni1
-rw-r--r--pw_system/stl_target_hooks.cc8
-rw-r--r--pw_system/system_target.gni64
-rw-r--r--pw_system/trace_service.cc40
-rw-r--r--pw_system/transfer_handlers.cc39
-rw-r--r--pw_system/transfer_service.cc68
-rw-r--r--pw_system/work_queue.cc2
-rw-r--r--pw_system/zephyr_target_hooks.cc67
-rw-r--r--pw_target_runner/docs.rst6
-rw-r--r--pw_thread/Android.bp55
-rw-r--r--pw_thread/BUILD.bazel109
-rw-r--r--pw_thread/BUILD.gn26
-rw-r--r--pw_thread/CMakeLists.txt76
-rw-r--r--pw_thread/backend.cmake6
-rw-r--r--pw_thread/backend.gni3
-rw-r--r--pw_thread/docs.rst20
-rw-r--r--pw_thread/public/pw_thread/config.h2
-rw-r--r--pw_thread/public/pw_thread/non_portable_test_thread_options.h53
-rw-r--r--pw_thread/public/pw_thread/snapshot.h4
-rw-r--r--pw_thread/public/pw_thread/test_thread_context.h68
-rw-r--r--pw_thread/public/pw_thread/test_threads.h36
-rw-r--r--pw_thread/py/BUILD.bazel4
-rw-r--r--pw_thread/py/thread_analyzer_test.py87
-rw-r--r--pw_thread/snapshot.cc4
-rw-r--r--pw_thread/test_thread_context_facade_test.cc62
-rw-r--r--pw_thread/thread_facade_test.cc2
-rw-r--r--pw_thread/thread_snapshot_service_test.cc13
-rw-r--r--pw_thread_embos/BUILD.bazel76
-rw-r--r--pw_thread_embos/BUILD.gn6
-rw-r--r--pw_thread_embos/docs.rst2
-rw-r--r--pw_thread_embos/test_threads.cc3
-rw-r--r--pw_thread_freertos/BUILD.bazel88
-rw-r--r--pw_thread_freertos/BUILD.gn9
-rw-r--r--pw_thread_freertos/docs.rst2
-rw-r--r--pw_thread_freertos/dynamic_test_threads.cc2
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/snapshot.h6
-rw-r--r--pw_thread_freertos/py/BUILD.gn1
-rw-r--r--pw_thread_freertos/py/setup.py18
-rw-r--r--pw_thread_freertos/snapshot.cc56
-rw-r--r--pw_thread_freertos/static_test_threads.cc2
-rw-r--r--pw_thread_freertos/thread.cc20
-rw-r--r--pw_thread_freertos/thread_iteration_test.cc2
-rw-r--r--pw_thread_stl/Android.bp43
-rw-r--r--pw_thread_stl/BUILD.bazel52
-rw-r--r--pw_thread_stl/BUILD.gn24
-rw-r--r--pw_thread_stl/CMakeLists.txt15
-rw-r--r--pw_thread_stl/public/pw_thread_stl/test_thread_context_native.h36
-rw-r--r--pw_thread_stl/test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h16
-rw-r--r--pw_thread_stl/test_threads.cc3
-rw-r--r--pw_thread_threadx/BUILD.bazel74
-rw-r--r--pw_thread_threadx/BUILD.gn6
-rw-r--r--pw_thread_threadx/docs.rst224
-rw-r--r--pw_thread_threadx/test_threads.cc3
-rw-r--r--pw_thread_zephyr/CMakeLists.txt64
-rw-r--r--pw_thread_zephyr/Kconfig39
-rw-r--r--pw_thread_zephyr/docs.rst49
-rw-r--r--pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_inline.h (renamed from pw_crypto/public_overrides/boringssl/pw_crypto/sha256_backend.h)5
-rw-r--r--pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_native.h16
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/config.h30
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/context.h120
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/id_inline.h30
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/id_native.h50
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/options.h70
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/thread_inline.h49
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/thread_native.h30
-rw-r--r--pw_thread_zephyr/pw_thread_zephyr_private/thread_iteration.h25
-rw-r--r--pw_thread_zephyr/thread.cc118
-rw-r--r--pw_thread_zephyr/thread_iteration.cc50
-rw-r--r--pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_inline.h16
-rw-r--r--pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_native.h16
-rw-r--r--pw_tls_client/BUILD.bazel28
-rw-r--r--pw_tls_client/BUILD.gn5
-rw-r--r--pw_tls_client/docs.rst238
-rw-r--r--pw_tls_client/py/BUILD.gn1
-rw-r--r--pw_tls_client/py/setup.py18
-rw-r--r--pw_tls_client/test_server_test.cc3
-rw-r--r--pw_tls_client_boringssl/BUILD.bazel2
-rw-r--r--pw_tls_client_boringssl/tls_client_boringssl.cc14
-rw-r--r--pw_tls_client_boringssl/tls_client_boringssl_test.cc2
-rw-r--r--pw_tls_client_mbedtls/BUILD.bazel4
-rw-r--r--pw_tls_client_mbedtls/BUILD.gn5
-rw-r--r--pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h1
-rw-r--r--pw_tls_client_mbedtls/tls_client_mbedtls.cc12
-rw-r--r--pw_tls_client_mbedtls/tls_client_mbedtls_test.cc2
-rw-r--r--pw_tokenizer/Android.bp159
-rw-r--r--pw_tokenizer/BUILD.bazel41
-rw-r--r--pw_tokenizer/BUILD.gn61
-rw-r--r--pw_tokenizer/CMakeLists.txt9
-rw-r--r--pw_tokenizer/Kconfig38
-rw-r--r--pw_tokenizer/OWNERS1
-rw-r--r--pw_tokenizer/api.rst94
-rw-r--r--pw_tokenizer/base64.cc6
-rw-r--r--pw_tokenizer/base64_test.cc2
-rw-r--r--pw_tokenizer/database.cmake124
-rw-r--r--pw_tokenizer/detokenization.rst583
-rw-r--r--pw_tokenizer/detokenize.cc78
-rw-r--r--pw_tokenizer/detokenize_fuzzer.cc2
-rw-r--r--pw_tokenizer/detokenize_test.cc56
-rw-r--r--pw_tokenizer/docs.rst1591
-rw-r--r--pw_tokenizer/encode_args.cc9
-rw-r--r--pw_tokenizer/encode_args_test.cc12
-rw-r--r--pw_tokenizer/get_started.rst95
-rw-r--r--pw_tokenizer/options.proto2
-rw-r--r--pw_tokenizer/proto.rst138
-rw-r--r--pw_tokenizer/public/pw_tokenizer/base64.h29
-rw-r--r--pw_tokenizer/public/pw_tokenizer/config.h82
-rw-r--r--pw_tokenizer/public/pw_tokenizer/detokenize.h11
-rw-r--r--pw_tokenizer/public/pw_tokenizer/encode_args.h28
-rw-r--r--pw_tokenizer/public/pw_tokenizer/hash.h12
-rw-r--r--pw_tokenizer/public/pw_tokenizer/internal/decode.h4
-rw-r--r--pw_tokenizer/public/pw_tokenizer/nested_tokenization.h25
-rw-r--r--pw_tokenizer/public/pw_tokenizer/token_database.h272
-rw-r--r--pw_tokenizer/public/pw_tokenizer/tokenize.h95
-rw-r--r--pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h34
-rw-r--r--pw_tokenizer/pw_tokenizer_linker_rules.ld55
-rw-r--r--pw_tokenizer/pw_tokenizer_linker_sections.ld34
-rw-r--r--pw_tokenizer/pw_tokenizer_private/generated_hash_test_cases.h290
-rw-r--r--pw_tokenizer/pw_tokenizer_private/tokenize_test.h2
-rw-r--r--pw_tokenizer/pw_tokenizer_zephyr.ld40
-rw-r--r--pw_tokenizer/py/BUILD.bazel7
-rwxr-xr-xpw_tokenizer/py/detokenize_test.py345
-rwxr-xr-xpw_tokenizer/py/generate_hash_test_data.py112
-rwxr-xr-xpw_tokenizer/py/pw_tokenizer/database.py3
-rwxr-xr-xpw_tokenizer/py/pw_tokenizer/detokenize.py488
-rw-r--r--pw_tokenizer/py/pw_tokenizer/encode.py3
-rw-r--r--pw_tokenizer/py/pw_tokenizer/proto/__init__.py4
-rw-r--r--pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py2
-rw-r--r--pw_tokenizer/py/pw_tokenizer/tokens.py12
-rw-r--r--pw_tokenizer/rust/BUILD.bazel90
-rw-r--r--pw_tokenizer/rust/pw_tokenizer/internal.rs79
-rw-r--r--pw_tokenizer/rust/pw_tokenizer/lib.rs277
-rw-r--r--pw_tokenizer/rust/pw_tokenizer_core.rs107
-rw-r--r--pw_tokenizer/rust/pw_tokenizer_core_test_cases.rs744
-rw-r--r--pw_tokenizer/rust/pw_tokenizer_macro.rs214
-rw-r--r--pw_tokenizer/token_database.cc8
-rw-r--r--pw_tokenizer/token_database_fuzzer.cc2
-rw-r--r--pw_tokenizer/token_database_test.cc58
-rw-r--r--pw_tokenizer/token_databases.rst310
-rw-r--r--pw_tokenizer/tokenization.rst741
-rw-r--r--pw_tokenizer/tokenize.cc3
-rw-r--r--pw_tokenizer/tokenize_c99_test.c134
-rw-r--r--pw_tokenizer/tokenize_c99_test_entry_point.cc23
-rw-r--r--pw_tokenizer/ts/detokenizer.ts39
-rw-r--r--pw_tokenizer/ts/detokenizer_test.ts22
-rw-r--r--pw_tokenizer/ts/index.ts4
-rw-r--r--pw_tokenizer/ts/int_testdata.ts94
-rw-r--r--pw_tokenizer/ts/printf_decoder.ts78
-rw-r--r--pw_tokenizer/ts/printf_decoder_test.ts50
-rw-r--r--pw_tokenizer/ts/token_database.ts6
-rw-r--r--pw_toolchain/BUILD.bazel21
-rw-r--r--pw_toolchain/BUILD.gn4
-rw-r--r--pw_toolchain/CMakeLists.txt1
-rw-r--r--pw_toolchain/arm_clang/BUILD.gn3
-rw-r--r--pw_toolchain/arm_clang/clang_config.gni7
-rw-r--r--pw_toolchain/arm_clang/clang_flags.cmake66
-rw-r--r--pw_toolchain/arm_clang/toolchains.gni75
-rw-r--r--pw_toolchain/arm_gcc/BUILD.bazel245
-rw-r--r--pw_toolchain/arm_gcc/BUILD.gn31
-rw-r--r--pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc1
-rw-r--r--pw_toolchain/arm_gcc/toolchains.gni196
-rw-r--r--pw_toolchain/clang_tools.gni88
-rw-r--r--pw_toolchain/docs.rst78
-rw-r--r--pw_toolchain/generate_toolchain.gni67
-rw-r--r--pw_toolchain/host_clang/BUILD.bazel265
-rw-r--r--pw_toolchain/host_clang/BUILD.gn42
-rw-r--r--pw_toolchain/host_clang/toolchain.cmake3
-rw-r--r--pw_toolchain/host_clang/toolchains.gni102
-rw-r--r--pw_toolchain/host_gcc/toolchain.cmake2
-rw-r--r--pw_toolchain/host_gcc/toolchains.gni11
-rw-r--r--pw_toolchain/no_destructor_test.cc15
-rw-r--r--pw_toolchain/public/pw_toolchain/no_destructor.h67
-rw-r--r--pw_toolchain/py/BUILD.gn2
-rw-r--r--pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py5
-rw-r--r--pw_toolchain/py/pw_toolchain/clang_tidy.py7
-rw-r--r--pw_toolchain/py/setup.py18
-rw-r--r--pw_toolchain/rust/BUILD.bazel22
-rw-r--r--pw_toolchain/rust/defs.bzl216
-rw-r--r--pw_toolchain/rust/rust_stdlib.BUILD31
-rw-r--r--pw_toolchain/rust/rust_toolchain.BUILD33
-rw-r--r--pw_toolchain/rust_toolchain.bzl48
-rw-r--r--pw_toolchain/static_analysis_toolchain.gni35
-rw-r--r--pw_toolchain/traits.gni4
-rw-r--r--pw_toolchain_bazel/BUILD.gn31
-rw-r--r--pw_toolchain_bazel/WORKSPACE.bazel (renamed from third_party/rules_proto_grpc/BUILD.bazel)4
-rw-r--r--pw_toolchain_bazel/api.rst298
-rw-r--r--pw_toolchain_bazel/cc_toolchain/BUILD.bazel15
-rw-r--r--pw_toolchain_bazel/cc_toolchain/defs.bzl57
-rw-r--r--pw_toolchain_bazel/cc_toolchain/private/BUILD.bazel (renamed from pw_ide/py/setup.py)7
-rw-r--r--pw_toolchain_bazel/cc_toolchain/private/cc_toolchain.bzl417
-rw-r--r--pw_toolchain_bazel/cc_toolchain/private/flag_set.bzl225
-rw-r--r--pw_toolchain_bazel/cc_toolchain/private/providers.bzl23
-rw-r--r--pw_toolchain_bazel/cc_toolchain/private/toolchain_feature.bzl215
-rw-r--r--pw_toolchain_bazel/cc_toolchain/private/utils.bzl73
-rw-r--r--pw_toolchain_bazel/constraints/arm_mcpu/BUILD.bazel80
-rw-r--r--pw_toolchain_bazel/docs.rst59
-rw-r--r--pw_toolchain_bazel/features/BUILD.bazel87
-rw-r--r--pw_toolchain_bazel/features/macos/BUILD.bazel28
-rw-r--r--pw_toolchain_bazel/features/macos/generate_xcode_repository.bzl25
-rw-r--r--pw_toolchain_bazel/features/macos/private/BUILD.bazel15
-rw-r--r--pw_toolchain_bazel/features/macos/private/xcode_command_line_tools.bzl116
-rw-r--r--pw_trace/BUILD.bazel15
-rw-r--r--pw_trace/example/basic.cc2
-rw-r--r--pw_trace/example/public/pw_trace/example/sample_app.h2
-rw-r--r--pw_trace/example/sample_app.cc8
-rw-r--r--pw_trace/public/pw_trace/internal/null.h18
-rw-r--r--pw_trace/py/BUILD.gn1
-rwxr-xr-xpw_trace/py/pw_trace/trace.py22
-rw-r--r--pw_trace/py/setup.py18
-rwxr-xr-xpw_trace/py/trace_test.py12
-rw-r--r--pw_trace_tokenized/BUILD.bazel144
-rw-r--r--pw_trace_tokenized/BUILD.gn66
-rw-r--r--pw_trace_tokenized/CMakeLists.txt26
-rw-r--r--pw_trace_tokenized/base_trace_service.cc76
-rw-r--r--pw_trace_tokenized/docs.rst7
-rw-r--r--pw_trace_tokenized/example/basic.cc3
-rw-r--r--pw_trace_tokenized/example/filter.cc8
-rw-r--r--pw_trace_tokenized/example/linux_group_by_tid.cc7
-rw-r--r--pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h15
-rw-r--r--pw_trace_tokenized/example/rpc.cc33
-rw-r--r--pw_trace_tokenized/example/trigger.cc9
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/base_trace_service.h43
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/config.h20
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h48
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h7
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/trace_service_pwpb.h38
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h19
-rw-r--r--pw_trace_tokenized/pw_trace_protos/trace_rpc.proto4
-rw-r--r--pw_trace_tokenized/pw_trace_protos/trace_service.options13
-rw-r--r--pw_trace_tokenized/pw_trace_protos/trace_service.proto53
-rw-r--r--pw_trace_tokenized/py/BUILD.gn12
-rwxr-xr-xpw_trace_tokenized/py/pw_trace_tokenized/get_trace.py17
-rw-r--r--pw_trace_tokenized/py/setup.py18
-rw-r--r--pw_trace_tokenized/trace.cc89
-rw-r--r--pw_trace_tokenized/trace_buffer.cc15
-rw-r--r--pw_trace_tokenized/trace_buffer_log.cc2
-rw-r--r--pw_trace_tokenized/trace_buffer_log_test.cc4
-rw-r--r--pw_trace_tokenized/trace_rpc_service_nanopb.cc14
-rw-r--r--pw_trace_tokenized/trace_service_pwpb.cc75
-rw-r--r--pw_trace_tokenized/trace_service_pwpb_test.cc130
-rw-r--r--pw_trace_tokenized/trace_test.cc27
-rw-r--r--pw_transfer/BUILD.bazel40
-rw-r--r--pw_transfer/BUILD.gn93
-rw-r--r--pw_transfer/OWNERS1
-rw-r--r--pw_transfer/api.rst12
-rw-r--r--pw_transfer/chunk.cc55
-rw-r--r--pw_transfer/client.cc6
-rw-r--r--pw_transfer/client_test.cc205
-rw-r--r--pw_transfer/context.cc48
-rw-r--r--pw_transfer/docs.rst41
-rw-r--r--pw_transfer/integration_test/BUILD.bazel35
-rw-r--r--pw_transfer/integration_test/client.cc18
-rw-r--r--pw_transfer/integration_test/config.proto2
-rw-r--r--pw_transfer/integration_test/cross_language_large_read_test.py2
-rw-r--r--pw_transfer/integration_test/cross_language_large_write_test.py2
-rw-r--r--pw_transfer/integration_test/cross_language_medium_read_test.py2
-rw-r--r--pw_transfer/integration_test/cross_language_medium_write_test.py2
-rw-r--r--pw_transfer/integration_test/cross_language_small_test.py2
-rw-r--r--pw_transfer/integration_test/expected_errors_test.py2
-rw-r--r--pw_transfer/integration_test/legacy_binaries_test.py11
-rw-r--r--pw_transfer/integration_test/multi_transfer_test.py2
-rw-r--r--pw_transfer/integration_test/proxy.py4
-rw-r--r--pw_transfer/integration_test/python_client.py183
-rw-r--r--pw_transfer/integration_test/test_fixture.py2
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel1
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java8
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java34
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java19
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java99
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java4
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java41
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java4
-rw-r--r--pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java870
-rw-r--r--pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h44
-rw-r--r--pw_transfer/public/pw_transfer/client.h8
-rw-r--r--pw_transfer/public/pw_transfer/internal/chunk.h33
-rw-r--r--pw_transfer/public/pw_transfer/internal/client_context.h7
-rw-r--r--pw_transfer/public/pw_transfer/internal/config.h21
-rw-r--r--pw_transfer/public/pw_transfer/internal/context.h12
-rw-r--r--pw_transfer/public/pw_transfer/internal/event.h2
-rw-r--r--pw_transfer/public/pw_transfer/internal/server_context.h3
-rw-r--r--pw_transfer/public/pw_transfer/transfer.h8
-rw-r--r--pw_transfer/public/pw_transfer/transfer_thread.h39
-rw-r--r--pw_transfer/pw_transfer_private/chunk_testing.h3
-rw-r--r--pw_transfer/py/pw_transfer/chunk.py57
-rw-r--r--pw_transfer/py/pw_transfer/client.py39
-rw-r--r--pw_transfer/py/pw_transfer/transfer.py24
-rw-r--r--pw_transfer/py/tests/transfer_test.py91
-rw-r--r--pw_transfer/test_rpc_server.cc123
-rw-r--r--pw_transfer/transfer.cc16
-rw-r--r--pw_transfer/transfer.proto22
-rw-r--r--pw_transfer/transfer_test.cc270
-rw-r--r--pw_transfer/transfer_thread.cc78
-rw-r--r--pw_transfer/transfer_thread_test.cc28
-rw-r--r--pw_transfer/ts/client.ts38
-rw-r--r--pw_transfer/ts/index.ts3
-rw-r--r--pw_transfer/ts/transfer.ts57
-rw-r--r--pw_transfer/ts/transfer_test.ts36
-rw-r--r--pw_unit_test/BUILD.bazel110
-rw-r--r--pw_unit_test/BUILD.gn61
-rw-r--r--pw_unit_test/CMakeLists.txt21
-rw-r--r--pw_unit_test/docs.rst262
-rw-r--r--pw_unit_test/framework.cc30
-rw-r--r--pw_unit_test/framework_test.cc176
-rw-r--r--pw_unit_test/googletest_handler_adapter.cc16
-rw-r--r--pw_unit_test/googletest_test_matchers_test.cc193
-rw-r--r--pw_unit_test/logging_main.cc3
-rw-r--r--pw_unit_test/public/pw_unit_test/event_handler.h5
-rw-r--r--pw_unit_test/public/pw_unit_test/googletest_test_matchers.h256
-rw-r--r--pw_unit_test/public/pw_unit_test/internal/framework.h344
-rw-r--r--pw_unit_test/public/pw_unit_test/printf_event_handler.h3
-rw-r--r--pw_unit_test/public/pw_unit_test/unit_test_service.h2
-rw-r--r--pw_unit_test/py/BUILD.gn26
-rw-r--r--pw_unit_test/py/pw_unit_test/rpc.py87
-rw-r--r--pw_unit_test/py/pw_unit_test/serial_test_runner.py196
-rw-r--r--pw_unit_test/py/pw_unit_test/test_runner.py54
-rw-r--r--pw_unit_test/py/setup.cfg1
-rw-r--r--pw_unit_test/py/setup.py18
-rw-r--r--pw_unit_test/py/test_group_metadata_test.py7
-rw-r--r--pw_unit_test/test.cmake2
-rw-r--r--pw_unit_test/test.gni218
-rw-r--r--pw_unit_test/unit_test_service.cc69
-rw-r--r--pw_unit_test_zephyr/BUILD.gn26
-rw-r--r--pw_unit_test_zephyr/CMakeLists.txt28
-rw-r--r--pw_unit_test_zephyr/docs.rst10
-rw-r--r--pw_unit_test_zephyr/main.cc24
-rw-r--r--pw_unit_test_zephyr/prj.conf32
-rw-r--r--pw_unit_test_zephyr/testcase.yaml (renamed from pw_build/py/setup.py)15
-rw-r--r--pw_varint/Android.bp25
-rw-r--r--pw_varint/BUILD.bazel4
-rw-r--r--pw_varint/BUILD.gn18
-rw-r--r--pw_varint/CMakeLists.txt7
-rw-r--r--pw_varint/Kconfig8
-rw-r--r--pw_varint/docs.rst99
-rw-r--r--pw_varint/public/pw_varint/stream.h24
-rw-r--r--pw_varint/public/pw_varint/varint.h359
-rw-r--r--pw_varint/rust/BUILD.bazel45
-rw-r--r--pw_varint/rust/pw_varint.rs367
-rw-r--r--pw_varint/stream_test.cc6
-rw-r--r--pw_varint/varint.cc35
-rw-r--r--pw_varint/varint_c.c77
-rw-r--r--pw_varint/varint_test.cc412
-rw-r--r--pw_varint/varint_test_c.c46
-rw-r--r--pw_watch/BUILD.gn8
-rw-r--r--pw_watch/cli.rst16
l---------pw_watch/doc_resources/pw_watch_on_device_demo.gif1
l---------pw_watch/doc_resources/pw_watch_test_demo2.gif1
-rw-r--r--pw_watch/docs.rst176
-rw-r--r--pw_watch/guide.rst140
-rw-r--r--pw_watch/py/BUILD.gn1
-rw-r--r--pw_watch/py/pw_watch/argparser.py11
-rwxr-xr-xpw_watch/py/pw_watch/watch.py45
-rw-r--r--pw_watch/py/pw_watch/watch_app.py6
-rw-r--r--pw_watch/py/setup.py18
-rw-r--r--pw_web/BUILD.gn5
-rw-r--r--pw_web/docs.rst156
-rw-r--r--pw_web/log-viewer/.gitignore28
-rw-r--r--pw_web/log-viewer/README.md52
-rw-r--r--pw_web/log-viewer/index.html36
-rw-r--r--pw_web/log-viewer/package-lock.json4892
-rw-r--r--pw_web/log-viewer/package.json32
-rw-r--r--pw_web/log-viewer/src/assets/favicon.svg97
-rw-r--r--pw_web/log-viewer/src/components/log-list/log-list.styles.ts327
-rw-r--r--pw_web/log-viewer/src/components/log-list/log-list.ts588
-rw-r--r--pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts114
-rw-r--r--pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts354
-rw-r--r--pw_web/log-viewer/src/components/log-view/log-view.styles.ts27
-rw-r--r--pw_web/log-viewer/src/components/log-view/log-view.ts352
-rw-r--r--pw_web/log-viewer/src/components/log-viewer.styles.ts68
-rw-r--r--pw_web/log-viewer/src/components/log-viewer.ts193
-rw-r--r--pw_web/log-viewer/src/createLogViewer.ts63
-rw-r--r--pw_web/log-viewer/src/custom/json-log-source.ts131
-rw-r--r--pw_web/log-viewer/src/custom/log_data.json37
-rw-r--r--pw_web/log-viewer/src/custom/mock-log-source.ts132
-rw-r--r--pw_web/log-viewer/src/events/add-view.ts23
-rw-r--r--pw_web/log-viewer/src/events/clear-logs.ts27
-rw-r--r--pw_web/log-viewer/src/events/close-view.ts27
-rw-r--r--pw_web/log-viewer/src/events/column-toggle.ts (renamed from pw_transfer/test_server.proto)22
-rw-r--r--pw_web/log-viewer/src/events/download-logs.ts28
-rw-r--r--pw_web/log-viewer/src/events/input-change.ts27
-rw-r--r--pw_web/log-viewer/src/events/wrap-toggle.ts27
-rw-r--r--pw_web/log-viewer/src/index.css71
-rw-r--r--pw_web/log-viewer/src/index.ts28
-rw-r--r--pw_web/log-viewer/src/log-source.ts52
-rw-r--r--pw_web/log-viewer/src/shared/interfaces.ts50
-rw-r--r--pw_web/log-viewer/src/shared/state.ts42
-rw-r--r--pw_web/log-viewer/src/themes/dark.ts211
-rw-r--r--pw_web/log-viewer/src/themes/light.ts213
-rw-r--r--pw_web/log-viewer/src/utils/log-filter/log-filter.models.ts61
-rw-r--r--pw_web/log-viewer/src/utils/log-filter/log-filter.ts254
-rw-r--r--pw_web/log-viewer/src/utils/log-filter/log-filter_test.ts173
-rw-r--r--pw_web/log-viewer/src/utils/log-filter/test-data.ts361
-rw-r--r--pw_web/log-viewer/src/utils/strings.ts22
-rw-r--r--pw_web/log-viewer/tsconfig.json28
-rw-r--r--pw_web/log-viewer/vite.config.js19
-rw-r--r--pw_web/testing.rst57
-rw-r--r--pw_web/webconsole/.eslintrc.json3
-rw-r--r--pw_web/webconsole/common/device.ts4
-rw-r--r--pw_web/webconsole/common/logService.ts7
-rw-r--r--pw_web/webconsole/common/protos.ts3
-rw-r--r--pw_web/webconsole/components/repl/autocomplete.ts81
-rw-r--r--pw_web/webconsole/components/repl/basicSetup.ts58
-rw-r--r--pw_web/webconsole/components/repl/localStorageArray.ts6
-rw-r--r--pw_web/webconsole/components/uploadDb.tsx11
-rw-r--r--pw_web/webconsole/next.config.js4
-rw-r--r--pw_web/webconsole/package-lock.json6437
-rw-r--r--pw_web/webconsole/package.json2
-rw-r--r--pw_web/webconsole/pages/index.tsx16
-rw-r--r--pw_web/webconsole/styles/Home.module.scss4
-rw-r--r--pw_web/webconsole/styles/globals.css1
-rw-r--r--pw_web/webconsole/tsconfig.json16
-rw-r--r--pw_work_queue/BUILD.bazel2
-rw-r--r--pw_work_queue/BUILD.gn7
-rw-r--r--pw_work_queue/CMakeLists.txt2
-rw-r--r--pw_work_queue/docs.rst106
-rw-r--r--pw_work_queue/public/pw_work_queue/internal/circular_buffer.h80
-rw-r--r--pw_work_queue/public/pw_work_queue/work_queue.h98
-rw-r--r--pw_work_queue/work_queue.cc52
-rw-r--r--rollup.config.js123
-rw-r--r--seed/0000-index.rst14
-rw-r--r--seed/0001-the-seed-process.rst104
-rw-r--r--seed/0002-template.rst37
-rw-r--r--seed/0101-pigweed.json.rst146
-rw-r--r--seed/0102-module-docs.rst36
-rw-r--r--seed/0104-display-support.rst785
-rw-r--r--seed/0105-pw_tokenizer-pw_log-nested-tokens.rst470
-rw-r--r--seed/0107-communications.rst640
-rw-r--r--seed/0108-pw_emu-emulators-frontend.rst685
-rw-r--r--seed/0109-comms-buffers.rst490
-rw-r--r--seed/0110-memory-allocation-interfaces.rst464
-rw-r--r--seed/0111-build-systems.rst269
-rw-r--r--seed/0112-async-poll.rst615
-rw-r--r--seed/0113-bazel-cc-toolchain-api.rst724
-rw-r--r--seed/BUILD.gn45
-rw-r--r--targets/BUILD.bazel207
-rw-r--r--targets/android/target_docs.rst55
-rw-r--r--targets/android/target_toolchains.gni19
-rw-r--r--targets/apollo4/BUILD.bazel27
-rw-r--r--targets/apollo4/BUILD.gn107
-rw-r--r--targets/apollo4/OWNERS1
-rw-r--r--targets/apollo4/apollo4_executable.gni33
-rw-r--r--targets/apollo4/boot.cc37
-rw-r--r--targets/apollo4/target_docs.rst165
-rw-r--r--targets/apollo4/target_toolchains.gni149
-rw-r--r--targets/apollo4/vector_table.c222
-rw-r--r--targets/apollo4_pw_system/BUILD.bazel27
-rw-r--r--targets/apollo4_pw_system/BUILD.gn87
-rw-r--r--targets/apollo4_pw_system/OWNERS1
-rw-r--r--targets/apollo4_pw_system/config/FreeRTOSConfig.h81
-rw-r--r--targets/apollo4_pw_system/main.cc74
-rw-r--r--targets/apollo4_pw_system/target_docs.rst111
-rw-r--r--targets/arduino/BUILD.bazel2
-rw-r--r--targets/arduino/BUILD.gn14
-rw-r--r--targets/arduino/target_docs.rst149
-rw-r--r--targets/arduino/target_toolchains.gni9
-rw-r--r--targets/default_config.BUILD177
-rw-r--r--targets/docs/BUILD.gn4
-rw-r--r--targets/docs/target_docs.rst2
-rw-r--r--targets/emcraft_sf2_som/BUILD.bazel2
-rw-r--r--targets/emcraft_sf2_som/BUILD.gn5
-rw-r--r--targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld2
-rw-r--r--targets/emcraft_sf2_som/target_docs.rst15
-rw-r--r--targets/host/pw_add_test_executable.cmake2
-rw-r--r--targets/host/system_rpc_server.cc2
-rw-r--r--targets/host/target_docs.rst8
-rw-r--r--targets/host/target_toolchains.gni72
-rw-r--r--targets/host_device_simulator/target_docs.rst26
-rw-r--r--targets/lm3s6965evb_qemu/BUILD.gn2
-rw-r--r--targets/lm3s6965evb_qemu/py/BUILD.gn1
-rw-r--r--targets/lm3s6965evb_qemu/py/setup.py18
-rw-r--r--targets/lm3s6965evb_qemu/target_docs.rst8
-rw-r--r--targets/lm3s6965evb_qemu/target_toolchains.gni2
-rw-r--r--targets/mimxrt595_evk/target_docs.rst50
-rw-r--r--targets/mimxrt595_evk_freertos/BUILD.bazel80
-rw-r--r--targets/mimxrt595_evk_freertos/BUILD.gn219
-rw-r--r--targets/mimxrt595_evk_freertos/FreeRTOSConfig.h155
-rw-r--r--targets/mimxrt595_evk_freertos/OWNERS2
-rw-r--r--targets/mimxrt595_evk_freertos/boot.cc53
-rw-r--r--targets/mimxrt595_evk_freertos/mimxrt595_executable.gni64
-rw-r--r--targets/mimxrt595_evk_freertos/mimxrt595_flash.ld308
-rw-r--r--targets/mimxrt595_evk_freertos/target_docs.rst162
-rw-r--r--targets/mimxrt595_evk_freertos/target_toolchains.gni152
-rw-r--r--targets/mimxrt595_evk_freertos/tasks.c40
-rw-r--r--targets/mimxrt595_evk_freertos/vector_table.c414
-rw-r--r--targets/rp2040/BUILD.bazel2
-rw-r--r--targets/rp2040/BUILD.gn22
-rw-r--r--targets/rp2040/pico_executable.gni8
-rw-r--r--targets/rp2040/py/BUILD.gn37
-rw-r--r--targets/rp2040/py/pyproject.toml16
-rw-r--r--targets/rp2040/py/rp2040_utils/__init__.py0
-rw-r--r--targets/rp2040/py/rp2040_utils/device_detector.py183
-rw-r--r--targets/rp2040/py/rp2040_utils/py.typed0
-rw-r--r--targets/rp2040/py/rp2040_utils/unit_test_client.py58
-rw-r--r--targets/rp2040/py/rp2040_utils/unit_test_runner.py195
-rw-r--r--targets/rp2040/py/rp2040_utils/unit_test_server.py139
-rw-r--r--targets/rp2040/py/setup.cfg34
-rw-r--r--targets/rp2040/target_docs.rst155
-rw-r--r--targets/rp2040_pw_system/BUILD.gn1
-rw-r--r--targets/rp2040_pw_system/config/FreeRTOSConfig.h2
-rw-r--r--targets/rp2040_pw_system/target_docs.rst36
-rw-r--r--targets/stm32f429i_disc1/BUILD.bazel5
-rw-r--r--targets/stm32f429i_disc1/boot.cc2
-rw-r--r--targets/stm32f429i_disc1/py/BUILD.gn1
-rw-r--r--targets/stm32f429i_disc1/py/setup.py18
-rw-r--r--targets/stm32f429i_disc1/target_docs.rst32
-rw-r--r--targets/stm32f429i_disc1/target_toolchains.gni7
-rw-r--r--targets/stm32f429i_disc1_stm32cube/BUILD.bazel46
-rw-r--r--targets/stm32f429i_disc1_stm32cube/BUILD.gn68
-rw-r--r--targets/stm32f429i_disc1_stm32cube/boot.cc1
-rw-r--r--targets/stm32f429i_disc1_stm32cube/config/stm32f4xx_hal_conf.h2
-rw-r--r--targets/stm32f429i_disc1_stm32cube/target_docs.rst24
-rw-r--r--third_party/Android.bp6
-rw-r--r--third_party/abseil-cpp/BUILD.gn56
-rw-r--r--third_party/abseil-cpp/OWNERS1
-rw-r--r--third_party/abseil-cpp/abseil-cpp.gni23
-rw-r--r--third_party/abseil-cpp/absl/algorithm/BUILD.gn50
-rw-r--r--third_party/abseil-cpp/absl/base/BUILD.gn374
-rw-r--r--third_party/abseil-cpp/absl/cleanup/BUILD.gn54
-rw-r--r--third_party/abseil-cpp/absl/container/BUILD.gn426
-rw-r--r--third_party/abseil-cpp/absl/crc/BUILD.gn160
-rw-r--r--third_party/abseil-cpp/absl/debugging/BUILD.gn201
-rw-r--r--third_party/abseil-cpp/absl/flags/BUILD.gn331
-rw-r--r--third_party/abseil-cpp/absl/functional/BUILD.gn83
-rw-r--r--third_party/abseil-cpp/absl/hash/BUILD.gn90
-rw-r--r--third_party/abseil-cpp/absl/log/BUILD.gn245
-rw-r--r--third_party/abseil-cpp/absl/log/internal/BUILD.gn413
-rw-r--r--third_party/abseil-cpp/absl/memory/BUILD.gn37
-rw-r--r--third_party/abseil-cpp/absl/meta/BUILD.gn34
-rw-r--r--third_party/abseil-cpp/absl/numeric/BUILD.gn75
-rw-r--r--third_party/abseil-cpp/absl/profiling/BUILD.gn75
-rw-r--r--third_party/abseil-cpp/absl/random/BUILD.gn137
-rw-r--r--third_party/abseil-cpp/absl/random/internal/BUILD.gn497
-rw-r--r--third_party/abseil-cpp/absl/status/BUILD.gn80
-rw-r--r--third_party/abseil-cpp/absl/strings/BUILD.gn408
-rw-r--r--third_party/abseil-cpp/absl/synchronization/BUILD.gn119
-rw-r--r--third_party/abseil-cpp/absl/time/BUILD.gn57
-rw-r--r--third_party/abseil-cpp/absl/time/internal/cctz/BUILD.gn65
-rw-r--r--third_party/abseil-cpp/absl/types/BUILD.gn183
-rw-r--r--third_party/abseil-cpp/absl/utility/BUILD.gn51
-rw-r--r--third_party/abseil-cpp/configs/BUILD.gn39
-rw-r--r--third_party/abseil-cpp/docs.rst56
-rw-r--r--third_party/abseil-cpp/repo.json7
-rw-r--r--third_party/ambiq/BUILD.gn216
-rw-r--r--third_party/ambiq/ambiq.gni24
-rw-r--r--third_party/boringssl/BUILD.bazel6
-rw-r--r--third_party/boringssl/BUILD.gn16
-rw-r--r--third_party/boringssl/crypto_sysrand.cc51
-rw-r--r--third_party/boringssl/docs.rst2
-rw-r--r--third_party/chre/BUILD.bazel25
-rw-r--r--third_party/chre/BUILD.gn203
-rw-r--r--third_party/chre/chre.gni29
-rw-r--r--third_party/chre/integration_test.cc118
-rw-r--r--third_party/chre/version.cc (renamed from pw_log_tokenized/public/pw_log_tokenized/base64_over_hdlc.h)18
-rw-r--r--third_party/emboss/BUILD.gn40
-rw-r--r--third_party/emboss/build_defs.gni19
-rw-r--r--third_party/emboss/docs.rst16
-rw-r--r--third_party/emboss/emboss.gni4
-rw-r--r--third_party/emboss/embossc_runner.py1
-rw-r--r--third_party/freertos/BUILD.bazel170
-rw-r--r--third_party/freertos/BUILD.gn33
-rw-r--r--third_party/freertos/docs.rst18
-rw-r--r--third_party/freertos/public/pw_third_party/freertos/config_assert.h2
-rw-r--r--third_party/fuchsia/BUILD.bazel7
-rw-r--r--third_party/fuchsia/BUILD.gn57
-rw-r--r--third_party/fuchsia/copy.bara.sky14
-rw-r--r--third_party/fuchsia/fuchsia.gni21
-rwxr-xr-xthird_party/fuchsia/generate_fuchsia_patch.py26
-rw-r--r--third_party/fuchsia/pigweed_adaptations.patch86
-rw-r--r--third_party/fuchsia/repo/LICENSE24
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/BUILD.gn45
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/defer.h145
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h138
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h19
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h100
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h17
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h20
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h18
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc202
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/BUILD.gn46
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h8
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h14
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/functional.h146
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h7
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h21
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h8
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h6
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h6
-rw-r--r--third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/internal/storage.h66
-rw-r--r--third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/lazy_init.h287
-rw-r--r--third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/options.h50
-rw-r--r--third_party/fuzztest/BUILD.gn32
-rw-r--r--third_party/fuzztest/CMakeLists.txt52
-rw-r--r--third_party/fuzztest/OWNERS1
-rw-r--r--third_party/fuzztest/centipede/BUILD.gn1386
-rw-r--r--third_party/fuzztest/codelab/BUILD.gn43
-rw-r--r--third_party/fuzztest/configs/BUILD.gn37
-rw-r--r--third_party/fuzztest/docs.rst105
-rw-r--r--third_party/fuzztest/domain_tests/BUILD.gn61
-rw-r--r--third_party/fuzztest/fuzztest.bazelrc47
-rw-r--r--third_party/fuzztest/fuzztest.gni23
-rw-r--r--third_party/fuzztest/fuzztest/BUILD.gn663
-rw-r--r--third_party/fuzztest/repo.json30
-rw-r--r--third_party/googletest/BUILD.gn1
-rw-r--r--third_party/googletest/docs.rst73
-rw-r--r--third_party/llvm_builtins/BUILD.gn260
-rw-r--r--third_party/llvm_libc/BUILD.gn19
-rw-r--r--third_party/llvm_libc/docs.rst44
-rw-r--r--third_party/llvm_libc/llvm_libc.gni150
-rw-r--r--third_party/mbedtls/BUILD.bazel30
-rw-r--r--third_party/mbedtls/BUILD.gn130
-rw-r--r--third_party/mbedtls/BUILD.mbedtls114
-rw-r--r--third_party/mbedtls/configs/config_default.h7
-rw-r--r--third_party/mbedtls/configs/config_pigweed_common.h2
-rw-r--r--third_party/mcuxpresso/BUILD.bazel29
-rw-r--r--third_party/mcuxpresso/mcuxpresso.gni5
-rw-r--r--third_party/nanopb/BUILD.gn50
-rw-r--r--third_party/nanopb/CMakeLists.txt27
-rw-r--r--third_party/nanopb/docs.rst2
-rw-r--r--third_party/nanopb/generate_nanopb_proto.py41
-rw-r--r--third_party/nanopb/nanopb.gni9
-rw-r--r--third_party/pico_sdk/gn/BUILD.gn11
-rw-r--r--third_party/pico_sdk/src/BUILD.gn1
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn1
-rw-r--r--third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn7
-rw-r--r--third_party/re2/BUILD.gn89
-rw-r--r--third_party/re2/OWNERS1
-rw-r--r--third_party/re2/configs/BUILD.gn49
-rw-r--r--third_party/re2/docs.rst56
-rw-r--r--third_party/re2/re2.gni (renamed from pw_tokenizer/py/setup.py)17
-rw-r--r--third_party/re2/repo.json5
-rw-r--r--third_party/rules_proto_grpc/OWNERS1
-rw-r--r--third_party/rules_proto_grpc/internal_proto.bzl348
-rw-r--r--third_party/stm32cube/BUILD.bazel14
-rw-r--r--third_party/stm32cube/BUILD.gn15
-rw-r--r--third_party/stm32cube/cmsis_core.BUILD.bazel30
-rw-r--r--third_party/stm32cube/cmsis_device.BUILD.bazel46
-rw-r--r--third_party/stm32cube/public/stm32cube/init.h2
-rw-r--r--third_party/stm32cube/stm32_hal_driver.BUILD.bazel117
-rw-r--r--ts/buildprotos.ts60
-rw-r--r--ts/device/index.ts144
-rw-r--r--ts/device/index_test.ts75
-rw-r--r--ts/device/object_set.ts85
-rw-r--r--ts/index.ts16
-rw-r--r--ts/index_test.ts36
-rw-r--r--ts/logging.ts26
-rw-r--r--ts/logging_source_rpc.ts83
-rw-r--r--ts/transport/device_transport.ts2
-rw-r--r--ts/transport/serial_mock.ts40
-rw-r--r--ts/transport/web_serial_transport.ts68
-rw-r--r--ts/transport/web_serial_transport_test.ts12
-rw-r--r--ts/types/serial.d.ts45
-rw-r--r--zephyr/CMakeLists.txt59
2253 files changed, 179979 insertions, 36661 deletions
diff --git a/.bazelrc b/.bazelrc
index 832402187..b8d5c6278 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -12,9 +12,29 @@
# License for the specific language governing permissions and limitations under
# the License.
+# Silence all C/C++ warnings in external code.
+#
+# Note that this will not silence warnings from external headers #include'd in
+# first-party code.
+common --per_file_copt=external/.*@-w
+common --host_per_file_copt=external/.*@-w
+
+# Don't automatically create __init__.py files.
+#
+# This prevents spurious package name collisions at import time, and should be
+# the default (https://github.com/bazelbuild/bazel/issues/7386). It's
+# particularly helpful for Pigweed, because we have many potential package name
+# collisions due to a profusion of stuttering paths like
+# pw_transfer/py/pw_transfer.
+common --incompatible_default_to_explicit_init_py
+
# Required for new toolchain resolution API.
build --incompatible_enable_cc_toolchain_resolution
+# Do not attempt to configure an autodetected (local) toolchain. We vendor all
+# our toolchains, and CI VMs may not have any local toolchain to detect.
+common --repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1
+
# Required for combined code coverage reports.
coverage --experimental_generate_llvm_lcov
coverage --combined_report=lcov
@@ -28,6 +48,10 @@ build --ui_event_filters=-debug
# variables.
build --incompatible_strict_action_env
+# Workaround for https://github.com/bazelbuild/rules_jvm_external/issues/445.
+build --java_runtime_version=remotejdk_11
+build --repo_env=JAVA_HOME=../bazel_tools/jdk
+
# Silence compiler warnings from external repositories.
#
# This is supported by Bazel's default C++ toolchain, but not yet by
@@ -35,22 +59,11 @@ build --incompatible_strict_action_env
# (https://github.com/bazelembedded/rules_cc_toolchain/issues/46).
build --features=external_include_paths
-# TODO(b/269204725): Move the following flags to the toolchain configuration.
-# By default build with C++17.
-build --cxxopt='-std=c++17'
-build --cxxopt="-fno-rtti"
-build --cxxopt="-Wnon-virtual-dtor"
-# Allow uses of the register keyword, which may appear in C headers.
-build --cxxopt="-Wno-register"
-
-# This leaks the PATH variable into the Bazel build environment, which
-# enables the Python pw_protobuf plugins to find the Python version
-# via CIPD and pw_env_setup. This is a partial fix for pwbug/437,
-# however this does not provide a fix for downstream projects that
-# use system Python < 3.6. This approach is problematic because of the
-# Protobuf rebuild sensitivity to environment variable changes.
-# TODO(pwbug/437): Remove this once pwbug/437 is completely resolved.
-build --action_env=PATH
+# This feature can't be enabled until __unordtf2 and __letf2 are implemented by
+# compiler-rt. See https://reviews.llvm.org/D53608.
+# build --features=fully_static_link
+
+build --@mbedtls//:mbedtls_config=//third_party/mbedtls:default_config
# Define the --config=asan-libfuzzer configuration.
build:asan-libfuzzer \
@@ -59,6 +72,31 @@ build:asan-libfuzzer \
--@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
build:asan-libfuzzer --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
+# Include FuzzTest build configurations.
+try-import %workspace%/third_party/fuzztest/fuzztest.bazelrc
+build:fuzztest \
+ --@pigweed//targets:pw_fuzzer_fuzztest_backend=@com_google_fuzztest//fuzztest
+build:fuzztest \
+ --@pigweed//targets:pw_unit_test_main=@com_google_fuzztest//fuzztest:fuzztest_gtest_main
+
+# We use non-default labels for the STM32Cube repositories upstream (to reserve
+# the option of building for more than one MCU family down the road), so need to
+# override the three labels below.
+common --//third_party/stm32cube:hal_driver=@stm32f4xx_hal_driver//:hal_driver
+common --@stm32f4xx_hal_driver//:cmsis_device=@cmsis_device_f4//:cmsis_device
+common --@stm32f4xx_hal_driver//:cmsis_init=@cmsis_device_f4//:default_cmsis_init
+
+# Config for the stm32f429i_disc1_stm32cube platform.
+#
+# TODO: b/301334234 - Make the platform set the flags below.
+build:stm32f429i --platforms=//targets/stm32f429i_disc1_stm32cube:platform
+build:stm32f429i --copt="-DSTM32CUBE_HEADER=\"stm32f4xx.h\""
+build:stm32f429i --copt="-DSTM32F429xx"
+build:stm32f429i --@stm32f4xx_hal_driver//:hal_config=//targets/stm32f429i_disc1_stm32cube:hal_config
+build:stm32f429i --//pw_log:backend_impl=@pigweed//pw_log_tokenized:impl
+build:stm32f429i --//targets:pw_log_backend=@pigweed//pw_log_tokenized
+build:stm32f429i --//targets:pw_log_tokenized_handler_backend=@pigweed//pw_system:log_backend
+
# Specifies desired output mode for running tests.
# Valid values are
# 'summary' to output only test status summary
@@ -68,3 +106,11 @@ build:asan-libfuzzer --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
# (this will force tests to be executed locally one at a time regardless
# of --test_strategy value).
test --test_output=errors
+
+# User bazelrc file; see
+# https://bazel.build/configure/best-practices#bazelrc-file
+#
+# Note: this should be at the bottom of the file, so that user-specified
+# options override anything in this file
+# (https://bazel.build/run/bazelrc#imports)
+try-import %workspace%/user.bazelrc
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 000000000..9ef87ba66
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,39 @@
+// ESLint configuration
+module.exports = {
+ env: {
+ browser: true,
+ es2021: true,
+ },
+ root: true,
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:lit-a11y/recommended",
+ ],
+ overrides: [],
+ parserOptions: {
+ ecmaVersion: "latest",
+ sourceType: "module",
+ },
+ plugins: [
+ "@typescript-eslint",
+ "lit-a11y",
+ ],
+ rules: {
+ "@typescript-eslint/ban-ts-comment": "warn",
+ "@typescript-eslint/no-explicit-any": "warn",
+ "@typescript-eslint/no-unused-vars": "warn",
+ },
+ ignorePatterns: [
+ "**/next.config.js",
+ "bazel-bin",
+ "bazel-out",
+ "bazel-pigweed",
+ "bazel-testlogs",
+ "node-modules",
+ "pw_ide/ts/pigweed-vscode/webpack.config.js",
+ "pw_web/log-viewer/src/assets/**",
+ "pw_web/log-viewer/src/legacy/**/*",
+ "pw_web/log-viewer/src/models/**",
+ ],
+};
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 5204bc260..000000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-{
- "ignorePatterns": [
- "bazel-bin",
- "bazel-pigweed",
- "bazel-out",
- "bazel-testlogs",
- "node-modules",
- "*.js",
- "*.d.ts"
- ],
- "extends": [
- "./node_modules/gts/",
- "plugin:react/recommended"
- ],
- "plugins": [
- "react"
- ],
- "parserOptions": {
- "ecmaFeatures": {
- "jsx": true
- },
- "ecmaVersion": 2018,
- "sourceType": "module"
- },
- "rules": {
- "node/no-unsupported-features/es-syntax": "off",
- "node/no-unpublished-import": "off"
- },
- "settings": {
- "react": {
- "version": "detect"
- }
- }
-}
diff --git a/.gitignore b/.gitignore
index ed3ae360b..e8b70a59f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,13 @@
# Build
compile_commands.json
+rust-project.json
+user.bazelrc
out/
bazel-*
outbazel/
.presubmit/
docs/_build
+twister-out*/
# Editors
.pw_ide/
@@ -25,6 +28,8 @@ docs/_build
*flycheck_*
*_flymake.*
.#*
+*~
+\#*#
# Python
*.pyc
@@ -65,3 +70,13 @@ build_overrides/pigweed_environment.gni
# Web Tools
node_modules
yarn-error.log
+
+# pw_emu
+.pw_emu
+
+# Emboss
+*.emb.h
+
+# Console Logs
+pw_console-logs.txt
+pw_console-device-logs.txt
diff --git a/.gn b/.gn
index 3fe130bfc..f18bc7ee9 100644
--- a/.gn
+++ b/.gn
@@ -13,6 +13,10 @@
# the License.
buildconfig = "//BUILDCONFIG.gn"
+import("//build_overrides/pigweed_environment.gni")
+
+# Generate a compilation database for all targets.
+export_compile_commands = [ ":*" ]
default_args = {
# Default all upstream Pigweed toolchains to enable pw::span asserts.
@@ -23,20 +27,40 @@ default_args = {
pw_build_PIP_REQUIREMENTS = [ "//pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt" ]
- # Exclude third-party headers from static analysis.
+ # TODO(b/292098416): This should stay false until a cross-platform constraint
+ # file workflow is implemented.
+ pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES = false
+
+ pw_build_PYTHON_PIP_INSTALL_FIND_LINKS =
+ [ "${pw_env_setup_CIPD_MSRV_PYTHON}/pip_cache" ]
+
pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES = [
+ # Exclude third party CHRE headers that were downloaded from static analysis.
+ "environment/packages/chre/.*",
+
+ # Exclude third-party headers from static analysis.
"third_party/.*",
- ".*packages/mbedtls.*",
- ".*packages/boringssl.*",
+
+ # Exclude GoogleTest, which fails some clang-tidy checks.
+ ".*/googletest/src/.*",
+ ".*/googlemock/src/.*",
+
+ # Exclude Boringssl, which fails some clang-tidy checks.
+ ".*/boringssl/src/.*",
]
pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = [
- "mbedtls/include",
- "mbedtls",
- "boringssl/src/include",
- "boringssl",
+ # Exclude third party CHRE code from static analysis.
+ "../environment/packages/chre/.*",
# Code generated by third-party tool.
"pw_tls_client/generate_test_data",
+
+ # Exclude GoogleTest, which fails some clang-tidy checks.
+ ".*/googletest/include.*",
+ ".*/googlemock/include.*",
+
+ # Exclude Boringssl, which fails some clang-tidy checks.
+ ".*/boringssl/src/include.*",
]
}
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..a30a151a9
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+pw_web/log-viewer/vite.config.js \ No newline at end of file
diff --git a/.prettierrc.cjs b/.prettierrc.cjs
new file mode 100644
index 000000000..86aeaaf05
--- /dev/null
+++ b/.prettierrc.cjs
@@ -0,0 +1,5 @@
+// Prettier configuration
+module.exports = {
+ tabWidth: 2,
+ singleQuote: true,
+};
diff --git a/.vscode/pw_project_extensions.json b/.vscode/pw_project_extensions.json
new file mode 100644
index 000000000..278d8c519
--- /dev/null
+++ b/.vscode/pw_project_extensions.json
@@ -0,0 +1,8 @@
+{
+ "recommendations": [
+ "swyddfa.esbonio"
+ ],
+ "unwantedRecommendations": [
+ "trond-snekvik.simple-rst"
+ ]
+}
diff --git a/BUILD.bazel b/BUILD.bazel
index a7ef535e1..38273ae0d 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -12,8 +12,16 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@python3_10//:defs.bzl", "interpreter")
+
licenses(["notice"])
+alias(
+ name = "python3_interpreter",
+ actual = interpreter,
+ visibility = ["//visibility:public"],
+)
+
exports_files(
["tsconfig.json"],
visibility = [":__subpackages__"],
diff --git a/BUILD.gn b/BUILD.gn
index aa97568b9..6a9a23145 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -15,17 +15,22 @@
import("//build_overrides/pi_pico.gni")
import("//build_overrides/pigweed.gni")
+import("$dir_pw_android_toolchain/android.gni")
import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_build/coverage_report.gni")
import("$dir_pw_build/host_tool.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_rpc/config.gni")
import("$dir_pw_rust/rust.gni")
+import("$dir_pw_third_party/ambiq/ambiq.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
import("$dir_pw_toolchain/c_optimization.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_toolchain/non_c_toolchain.gni")
+import("$dir_pw_trace/backend.gni")
import("$dir_pw_unit_test/test.gni")
# Main build file for upstream Pigweed.
@@ -46,6 +51,10 @@ declare_args() {
# List of application image GN targets specific to the Pigweed target.
pw_TARGET_APPLICATIONS = []
+
+ # Gates known broken configurations from building. This is what allows the
+ # implicit `all` target to work even though there are known broken targets.
+ pw_BUILD_BROKEN_GROUPS = false
}
# This toolchain is used to force some dependencies to not be parsed by the
@@ -97,6 +106,55 @@ group("default") {
]
}
+# This group tries to collect together most build steps in Pigweed's build.
+# Some targets in this group are not included in the main build to keep the
+# default build time down, while others are excluded from the main build because
+# they require additional configuration or don't work on every OS.
+#
+# To increase coverage, use `pw package` to install these packages:
+# boringssl
+# freertos
+# mbedtls
+# micro-ecc
+# nanopb
+# pico_sdk
+# protobuf
+# stm32cube_f4
+# teensy
+group("extended_default") {
+ deps = [
+ ":cpp20_compatibility",
+ ":default",
+ ":host_clang_debug_dynamic_allocation",
+ ":pw_system_demo",
+ ":stm32f429i",
+ ]
+
+ if (host_os != "win") {
+ deps += [
+ ":qemu_clang",
+ ":qemu_gcc",
+ ]
+ }
+
+ if (host_os == "linux") {
+ deps += [
+ ":integration_tests",
+ ":runtime_sanitizers",
+ ]
+ }
+
+ # Requires setting pw_arduino_build_CORE_PATH.
+ if (pw_arduino_build_CORE_PATH != "") {
+ deps += [ ":arduino" ]
+ }
+
+ # Requires setting pw_third_party_mcuxpresso_SDK.
+ if (pw_third_party_mcuxpresso_SDK != "") {
+ deps += [ ":mimxrt595" ]
+ }
+}
+
# Verify that this BUILD.gn file is only used by Pigweed itself.
assert(get_path_info("//", "abspath") == get_path_info(".", "abspath"),
"Pigweed's top-level BUILD.gn may only be used when building upstream " +
@@ -160,7 +218,7 @@ group("pw_system_demo") {
group("pi_pico") {
if (PICO_SRC_DIR != "") {
- deps = [ ":pw_module_tests(targets/rp2040)" ]
+ deps = [ ":pigweed_default(targets/rp2040)" ]
}
}
@@ -218,6 +276,17 @@ if (pw_third_party_mcuxpresso_SDK != "") {
_build_pigweed_default_at_all_optimization_levels("mimxrt595") {
toolchain_prefix = "$dir_pigweed/targets/mimxrt595_evk:mimxrt595_evk_"
}
+
+ _build_pigweed_default_at_all_optimization_levels("mimxrt595_freertos") {
+ toolchain_prefix =
+ "$dir_pigweed/targets/mimxrt595_evk_freertos:mimxrt595_evk_freertos_"
+ }
+}
+
+if (dir_pw_third_party_ambiq_SDK != "") {
+ _build_pigweed_default_at_all_optimization_levels("apollo4") {
+ toolchain_prefix = "$dir_pigweed/targets/apollo4:apollo4_"
+ }
}
_build_pigweed_default_at_all_optimization_levels("stm32f429i") {
@@ -230,23 +299,30 @@ if (pw_arduino_build_CORE_PATH != "") {
}
}
-_build_pigweed_default_at_all_optimization_levels("qemu_gcc") {
- toolchain_prefix =
- "$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_gcc_"
-}
-
-# TODO(b/244604080): Inline string tests are too big to fit in the QEMU firmware
+# TODO: b/244604080 - Inline string tests are too big to fit in the QEMU firmware
# except under a size-optimized build. For now, only build size-optimized.
-#
-# _build_pigweed_default_at_all_optimization_levels("qemu_clang") {
-# toolchain_prefix =
-# "$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_"
-# }
-group("qemu_clang_size_optimized") {
- deps = [ ":pigweed_default($dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_size_optimized)" ]
-}
-group("qemu_clang") {
- deps = [ ":qemu_clang_size_optimized" ]
+if (pw_BUILD_BROKEN_GROUPS) {
+ _build_pigweed_default_at_all_optimization_levels("qemu_gcc") {
+ toolchain_prefix =
+ "$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_gcc_"
+ }
+ _build_pigweed_default_at_all_optimization_levels("qemu_clang") {
+ toolchain_prefix =
+ "$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_"
+ }
+} else {
+ group("qemu_gcc_size_optimized") {
+ deps = [ ":pigweed_default($dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_gcc_size_optimized)" ]
+ }
+ group("qemu_gcc") {
+ deps = [ ":qemu_gcc_size_optimized" ]
+ }
+ group("qemu_clang_size_optimized") {
+ deps = [ ":pigweed_default($dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_size_optimized)" ]
+ }
+ group("qemu_clang") {
+ deps = [ ":qemu_clang_size_optimized" ]
+ }
}
# Run clang-tidy on pigweed_default with pw_strict_host_clang_debug toolchain options.
@@ -260,6 +336,24 @@ group("static_analysis") {
}
}
+if (pw_android_toolchain_NDK_PATH != "") {
+ group("android") {
+ deps = []
+ foreach(_cpu, pw_android_toolchain_cpu_targets) {
+ _toolchain_prefix = "$dir_pigweed/targets/android:${_cpu}_android_"
+ deps += [
+ ":pigweed_default($_toolchain_prefix$pw_DEFAULT_C_OPTIMIZATION_LEVEL)",
+ ]
+ }
+ }
+
+ foreach(_cpu, pw_android_toolchain_cpu_targets) {
+ _build_pigweed_default_at_all_optimization_levels("${_cpu}_android") {
+ toolchain_prefix = "$dir_pigweed/targets/android:${_cpu}_android_"
+ }
+ }
+}
+
group("docs") {
deps = [ "$dir_pigweed/docs($dir_pigweed/targets/docs)" ]
}
@@ -271,98 +365,131 @@ group("docs") {
# build.
group("action_tests") {
_default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
- deps = [ "$dir_pw_unit_test:test_group_metadata_test.action($_default_tc)" ]
+ deps = [ ":pw_action_tests.run($_default_tc)" ]
}
# Tests larger than unit tests, typically run in a specific configuration.
group("integration_tests") {
_default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
- deps = [
- "$dir_pw_cli/py:process_integration_test.action($_default_tc)",
- "$dir_pw_rpc:cpp_client_server_integration_test($_default_tc)",
- "$dir_pw_rpc/py:python_client_cpp_server_test.action($_default_tc)",
- "$dir_pw_unit_test/py:rpc_service_test.action($_default_tc)",
- ]
+ deps = [ ":pw_integration_tests.run($_default_tc)" ]
+}
+
+group("pip_constraint_update") {
+ public_deps = []
+
+ if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
+ public_deps += [ "$dir_pw_env_setup:pigweed_build_venv._compile_requirements($pw_build_PYTHON_TOOLCHAIN)" ]
+ } else {
+ public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+}
+
+group("pip_vendor_wheels") {
+ public_deps = []
+
+ if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
+ public_deps += [ "$dir_pw_env_setup:pigweed_build_venv.vendor_wheels($pw_build_PYTHON_TOOLCHAIN)" ]
+ } else {
+ public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
}
-# Build-only target for fuzzers.
group("fuzzers") {
deps = []
- # TODO(b/274437709): The client_fuzzer encounters build errors on macos. Limit
- # it to Linux hosts for now.
- if (host_os == "linux") {
+ if (host_os != "win" && dir_pw_third_party_fuzztest != "") {
+ # Coverage-guided fuzzing is only supported on Linux and MacOS using clang.
+ # Fuzztest-based fuzzers will run in unit test mode. libFuzzer-based fuzzers
+ # will only build.
+ _clang_fuzz_tc = _default_toolchain_prefix + "fuzz"
+ deps += [ ":pw_module_tests.run($_clang_fuzz_tc)" ]
+ }
+
+ # Also build (but do not run) bespoke fuzzers.
+ if (!pw_toolchain_OSS_FUZZ_ENABLED) {
_default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
- deps += [ "$dir_pw_rpc/fuzz:client_fuzzer($_default_tc)" ]
+ deps += [ ":pw_custom_fuzzers($_default_tc)" ]
}
+}
- if (host_os != "win") {
- # Coverage-guided fuzzing is only supported on Linux and MacOS using clang.
- deps += [
- "$dir_pw_bluetooth_hci:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
- "$dir_pw_fuzzer:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
- "$dir_pw_protobuf:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
- "$dir_pw_random:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
- "$dir_pw_tokenizer:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
- ]
+# Build-only target for OSS-Fuzz. No-op unless OSS-Fuzz is enabled.
+group("oss_fuzz") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ _clang_fuzz_tc = _default_toolchain_prefix + "fuzz"
+ deps = [ ":pw_module_tests($_clang_fuzz_tc)" ]
}
}
group("asan") {
+ # TODO: b/302181521 - asan doesn't work on Windows yet.
if (host_os != "win") {
deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_asan)" ]
}
}
-# TODO(b/234876100): msan will not work until the C++ standard library included
-# in the sysroot has a variant built with msan.
+# TODO: b/234876100 - msan will not work without false positives until the C++
+# standard library included in the sysroot has a variant built with msan.
group("msan") {
- # TODO(b/259695498): msan doesn't work on macOS yet.
- if (host_os != "win" && host_os != "mac" && host_os != "linux") {
+ # TODO: b/259695498 - msan doesn't work on macOS yet.
+ # TODO: b/302181521 - msan doesn't work on Windows yet.
+ if (pw_BUILD_BROKEN_GROUPS) {
deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_msan)" ]
}
}
group("tsan") {
+ # TODO: b/302181521 - tsan doesn't work on Windows yet.
if (host_os != "win") {
deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_tsan)" ]
}
}
group("ubsan") {
- # TODO(b/259695498): ubsan doesn't work on macOS yet.
- if (host_os != "win" && host_os != "mac") {
+ # TODO: b/259695498 - ubsan doesn't work on macOS yet.
+ # TODO: b/302181521 - ubsan doesn't work on Windows yet.
+ if ((host_os != "win" && host_os != "mac") || pw_BUILD_BROKEN_GROUPS) {
deps =
[ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_ubsan)" ]
}
}
group("ubsan_heuristic") {
- # TODO(b/259695498): ubsan_heuristic doesn't work on macOS yet.
- if (host_os != "win" && host_os != "mac") {
+ # TODO: b/259695498 - ubsan_heuristic doesn't work on macOS yet.
+ # TODO: b/302181521 - ubsan doesn't work on Windows yet.
+ if ((host_os != "win" && host_os != "mac") || pw_BUILD_BROKEN_GROUPS) {
deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_ubsan_heuristic)" ]
}
}
group("runtime_sanitizers") {
- if (host_os != "win") {
+ if (host_os != "win" || pw_BUILD_BROKEN_GROUPS) {
deps = [
":asan",
":tsan",
":ubsan",
+ ]
- # TODO(b/234876100): msan will not work until the C++ standard library
+ if (pw_BUILD_BROKEN_GROUPS) {
+ # TODO: b/234876100 - msan will not work until the C++ standard library
# included in the sysroot has a variant built with msan.
- # ":msan",
+ deps += [ ":msan" ]
# No ubsan_heuristic, which may have false positives.
- ]
+ deps += [ ":ubsan_heuristic" ]
+ }
+ }
+}
+
+group("coverage") {
+ if (host_os == "linux") {
+ deps = [ ":coverage_report($dir_pigweed/targets/host:host_clang_coverage)" ]
}
}
pw_python_group("python") {
python_deps = [
"$dir_pw_env_setup:python($pw_build_PYTHON_TOOLCHAIN)",
+ "$dir_pw_env_setup:sample_project_action($pw_build_PYTHON_TOOLCHAIN)",
"$dir_pw_env_setup:target_support_packages($pw_build_PYTHON_TOOLCHAIN)",
]
}
@@ -379,7 +506,7 @@ group("host_tools") {
]
if (pw_rust_ENABLE_EXPERIMENTAL_BUILD) {
- deps += [ "$dir_pw_rust/examples/host_executable:hello($dir_pigweed/targets/host:host_clang_debug)" ]
+ deps += [ "$dir_pw_rust/examples/host_executable:host_executable($dir_pigweed/targets/host:host_clang_debug)" ]
}
}
@@ -389,44 +516,51 @@ group("pigweed_default") {
# Prevent the default toolchain from parsing any other BUILD.gn files.
if (current_toolchain != default_toolchain) {
+ _is_host_toolchain = defined(pw_toolchain_SCOPE.is_host_toolchain) &&
+ pw_toolchain_SCOPE.is_host_toolchain
+
deps = [ ":apps" ]
- if (pw_unit_test_AUTOMATIC_RUNNER == "") {
- # Without a test runner defined, build the tests but don't run them.
- deps += [ ":pw_module_tests" ]
- } else {
- # With a test runner, depend on the run targets so they run with the
- # build.
- deps += [ ":pw_module_tests.run" ]
- }
- # Add performance tests to the automatic build
- deps += [ ":pw_perf_tests" ]
+ # Upstream GoogleTest assumes the presence of a POSIX filesystem. If an
+ # external gtest backend has been provided, limit the tests to host-only.
+ if (pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_unit_test:light" ||
+ _is_host_toolchain) {
+ if (pw_unit_test_AUTOMATIC_RUNNER == "") {
+ # Without a test runner defined, build the tests but don't run them.
+ deps += [ ":pw_module_tests" ]
+ } else {
+ # With a test runner, depend on the run targets so they run with the
+ # build.
+ deps += [ ":pw_module_tests.run" ]
+ }
+
+ # Add performance tests to the automatic build
+ deps += [ ":pw_perf_tests" ]
+ }
# Add action tests to the automatic build
- deps += [ ":action_tests" ]
+ if (_is_host_toolchain) {
+ deps += [ ":pw_action_tests.run" ]
+ }
# Trace examples currently only support running on non-windows host
- if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
- pw_toolchain_SCOPE.is_host_toolchain && host_os != "win") {
- deps += [
- "$dir_pw_trace:trace_example_basic",
- "$dir_pw_trace_tokenized:trace_tokenized_example_basic",
- "$dir_pw_trace_tokenized:trace_tokenized_example_filter",
- "$dir_pw_trace_tokenized:trace_tokenized_example_rpc",
- "$dir_pw_trace_tokenized:trace_tokenized_example_trigger",
- ]
+ if (_is_host_toolchain && host_os != "win") {
+ deps += [ "$dir_pw_trace:trace_example_basic" ]
+
+ if (get_label_info(pw_trace_BACKEND, "label_no_toolchain") ==
+ get_label_info(":pw_trace_tokenized", "label_no_toolchain")) {
+ deps += [
+ "$dir_pw_trace_tokenized:trace_tokenized_example_basic",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_filter",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_linux_group_by_tid",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_rpc",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_trigger",
+ ]
+ }
}
}
}
-group("cpp14_compatibility") {
- _cpp14_toolchain = "$_internal_toolchains:pw_strict_host_clang_debug_cpp14"
- deps = [
- ":cpp14_modules($_cpp14_toolchain)",
- ":cpp14_tests.run($_cpp14_toolchain)",
- ]
-}
-
# Build Pigweed with -std=c++20 to ensure compatibility. Compile with
# optimizations since the compiler tends to catch more errors with optimizations
# enabled than without.
@@ -440,11 +574,21 @@ group("build_with_pw_minimal_cpp_stdlib") {
# This list of supported modules is incomplete.
deps = [
+ "$dir_pw_base64($_toolchain)",
+ "$dir_pw_containers:variable_length_entry_queue($_toolchain)",
"$dir_pw_status($_toolchain)",
+ "$dir_pw_tokenizer:base64($_toolchain)",
"$dir_pw_tokenizer($_toolchain)",
+ "$dir_pw_varint($_toolchain)",
]
}
+group("host_clang_debug_dynamic_allocation") {
+ _toolchain =
+ "$_internal_toolchains:pw_strict_host_clang_debug_dynamic_allocation"
+ deps = [ ":pigweed_default($_toolchain)" ]
+}
+
# The default toolchain is not used for compiling C/C++ code.
if (current_toolchain != default_toolchain) {
group("apps") {
@@ -459,7 +603,7 @@ if (current_toolchain != default_toolchain) {
pw_toolchain_SCOPE.is_host_toolchain) {
deps += [ "$dir_pw_tool" ]
- # TODO(b/240982565): Build integration tests on Windows and macOS when
+ # TODO: b/240982565 - Build integration tests on Windows and macOS when
# SocketStream supports those platforms.
if (host_os == "linux") {
# Build the integration test binaries, but don't run them by default.
@@ -480,37 +624,51 @@ if (current_toolchain != default_toolchain) {
# Targets for all module unit test groups.
pw_test_group("pw_module_tests") {
group_deps = pw_module_tests
+ output_metadata = true
}
- group("pw_perf_tests") {
- deps = [
+ pw_test_group("pw_action_tests") {
+ tests = [ "$dir_pw_unit_test:test_group_metadata_test" ]
+ }
+
+ pw_test_group("pw_integration_tests") {
+ tests = [
+ "$dir_pw_build/py/gn_tests:python_package_integration_tests",
+ "$dir_pw_cli/py:process_integration_test",
+ "$dir_pw_rpc:cpp_client_server_integration_test",
+ "$dir_pw_rpc/py:python_client_cpp_server_test",
+ "$dir_pw_unit_test/py:rpc_service_test",
+ ]
+
+ output_metadata = true
+ }
+
+ pw_test_group("pw_perf_tests") {
+ tests = [
"$dir_pw_checksum:perf_tests",
- "$dir_pw_perf_test:perf_test_tests_test",
+ "$dir_pw_perf_test:examples",
"$dir_pw_protobuf:perf_tests",
]
+ output_metadata = true
}
- # Modules that support C++14.
- # TODO(hepler): pw_kvs is supposed to compile as C++14, but does not.
- group("cpp14_modules") {
- public_deps = [
- dir_pw_polyfill,
- dir_pw_preprocessor,
- dir_pw_tokenizer,
- dir_pw_varint,
- ]
+ # Fuzzers not based on a fuzzing engine. Engine-based fuzzers should be
+ # included in `pw_module_tests`.
+ pw_test_group("pw_custom_fuzzers") {
+ # TODO: b/274437709 - The RPC client_fuzzer encounters build errors on macos.
+ # Limit it to Linux hosts for now.
+ if (host_os == "linux") {
+ tests = [ "$dir_pw_rpc/fuzz:client_fuzzer" ]
+ }
+ output_metadata = true
}
- # Tests that support C++14.
- pw_test_group("cpp14_tests") {
- group_deps = [
- "$dir_pw_polyfill:tests",
- "$dir_pw_span:tests",
- ]
- tests = [
- "$dir_pw_tokenizer:simple_tokenize_test",
- "$dir_pw_containers:to_array_test",
- "$dir_pw_string:string_test",
+ pw_coverage_report("coverage_report") {
+ filter_paths = []
+ ignore_filename_patterns = [
+ "third_party",
+ "protocol_buffer/gen",
]
+ group_deps = [ ":pw_module_tests" ]
}
}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4e463d29a..97ab990bc 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,9 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
-project(Pigweed)
-
cmake_minimum_required(VERSION 3.16)
+project(Pigweed)
# Do not rely on the PW_ROOT environment variable being set through bootstrap.
# Regardless of whether it's set or not the following include will ensure it is.
@@ -35,12 +34,32 @@ if(CONFIG_ZEPHYR_PIGWEED_MODULE)
pw_log_tokenized.handler pw_log_zephyr.tokenized_handler pw_log_tokenized/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED
pw_log pw_log_tokenized pw_log/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYSTEM_HDLC_RPC_SERVER
+ pw_system.rpc_server pw_system.hdlc_rpc_server pw_system/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYSTEM_LOG_BACKEND
+ pw_log pw_system.log_backend pw_log/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYSTEM_LOG_BACKEND
+ pw_log_tokenized.handler pw_system.log_backend pw_log_tokenized/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYSTEM_TARGET_HOOKS
+ pw_system.target_hooks pw_system.zephyr_target_hooks pw_system/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_THREAD_ITERATION
+ pw_thread.thread_iteration pw_thread_zephyr.thread_iteration pw_thread/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_THREAD_SLEEP
pw_thread.sleep pw_thread_zephyr.sleep pw_thread/backend.cmake)
- pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_MUTEX
- pw_sync.mutex pw_sync_zephyr.mutex_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_THREAD
+ pw_thread.id pw_thread_zephyr.id pw_thread/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_THREAD
+ pw_thread.thread pw_thread_zephyr.thread pw_thread/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE
pw_sync.mutex pw_sync_zephyr.binary_semaphore_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_INTERRUPT_SPIN_LOCK
+ pw_sync.interrupt_spin_lock pw_sync_zephyr.interrupt_spin_lock_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_THREAD_NOTIFICATION
+ pw_sync.thread_notification pw_sync_zephyr.thread_notification_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_TIMED_THREAD_NOTIFICATION
+ pw_sync.timed_thread_notification pw_sync_zephyr.timed_thread_notification_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_MUTEX
+ pw_sync.mutex pw_sync_zephyr.mutex_backend pw_sync/backend.cmake)
pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYS_IO
pw_sys_io pw_sys_io_zephyr pw_sys_io/backend.cmake)
@@ -54,6 +73,8 @@ add_subdirectory(pw_assert EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert_basic EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert_log EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert_zephyr EXCLUDE_FROM_ALL)
+add_subdirectory(pw_async EXCLUDE_FROM_ALL)
+add_subdirectory(pw_async_basic EXCLUDE_FROM_ALL)
add_subdirectory(pw_base64 EXCLUDE_FROM_ALL)
add_subdirectory(pw_blob_store EXCLUDE_FROM_ALL)
add_subdirectory(pw_bluetooth EXCLUDE_FROM_ALL)
@@ -72,7 +93,9 @@ add_subdirectory(pw_cpu_exception_cortex_m EXCLUDE_FROM_ALL)
add_subdirectory(pw_digital_io EXCLUDE_FROM_ALL)
add_subdirectory(pw_file EXCLUDE_FROM_ALL)
add_subdirectory(pw_function EXCLUDE_FROM_ALL)
+add_subdirectory(pw_fuzzer EXCLUDE_FROM_ALL)
add_subdirectory(pw_hdlc EXCLUDE_FROM_ALL)
+add_subdirectory(pw_i2c EXCLUDE_FROM_ALL)
add_subdirectory(pw_interrupt EXCLUDE_FROM_ALL)
add_subdirectory(pw_interrupt_cortex_m EXCLUDE_FROM_ALL)
add_subdirectory(pw_interrupt_zephyr EXCLUDE_FROM_ALL)
@@ -99,8 +122,10 @@ add_subdirectory(pw_result EXCLUDE_FROM_ALL)
add_subdirectory(pw_ring_buffer EXCLUDE_FROM_ALL)
add_subdirectory(pw_router EXCLUDE_FROM_ALL)
add_subdirectory(pw_rpc EXCLUDE_FROM_ALL)
+add_subdirectory(pw_rpc_transport EXCLUDE_FROM_ALL)
add_subdirectory(pw_snapshot EXCLUDE_FROM_ALL)
add_subdirectory(pw_span EXCLUDE_FROM_ALL)
+add_subdirectory(pw_spi EXCLUDE_FROM_ALL)
add_subdirectory(pw_status EXCLUDE_FROM_ALL)
add_subdirectory(pw_stream EXCLUDE_FROM_ALL)
add_subdirectory(pw_string EXCLUDE_FROM_ALL)
@@ -129,9 +154,16 @@ add_subdirectory(pw_work_queue EXCLUDE_FROM_ALL)
add_subdirectory(third_party/nanopb EXCLUDE_FROM_ALL)
add_subdirectory(third_party/freertos EXCLUDE_FROM_ALL)
add_subdirectory(third_party/fuchsia EXCLUDE_FROM_ALL)
+add_subdirectory(third_party/fuzztest EXCLUDE_FROM_ALL)
add_subdirectory(third_party/googletest EXCLUDE_FROM_ALL)
-if(NOT ZEPHYR_PIGWEED_MODULE_DIR)
+if(CONFIG_ZEPHYR_PIGWEED_MODULE)
+ add_subdirectory(zephyr)
+ if(CONFIG_PIGWEED_MINIMAL_CPP_STDLIB)
+ # Add include path to pw_minimal_cpp_stdlib to compiler
+ zephyr_compile_options(-isystem ${ZEPHYR_PIGWEED_MODULE_DIR}/pw_minimal_cpp_stdlib/public)
+ endif()
+else()
add_subdirectory(targets/host EXCLUDE_FROM_ALL)
add_custom_target(pw_apps)
diff --git a/Kconfig.zephyr b/Kconfig.zephyr
index 9e07bf0ae..d85771f68 100644
--- a/Kconfig.zephyr
+++ b/Kconfig.zephyr
@@ -13,12 +13,12 @@
# the License.
config ZEPHYR_PIGWEED_MODULE
- select REQUIRES_FULL_LIBCPP
depends on STD_CPP17 || STD_CPP2A || STD_CPP20 || STD_CPP2B
if ZEPHYR_PIGWEED_MODULE
rsource "pw_assert_zephyr/Kconfig"
+rsource "pw_base64/Kconfig"
rsource "pw_bytes/Kconfig"
rsource "pw_checksum/Kconfig"
rsource "pw_chrono_zephyr/Kconfig"
@@ -27,6 +27,8 @@ rsource "pw_function/Kconfig"
rsource "pw_hdlc/Kconfig"
rsource "pw_interrupt_zephyr/Kconfig"
rsource "pw_log_zephyr/Kconfig"
+rsource "pw_minimal_cpp_stdlib/Kconfig"
+rsource "pw_multisink/Kconfig"
rsource "pw_polyfill/Kconfig"
rsource "pw_preprocessor/Kconfig"
rsource "pw_result/Kconfig"
@@ -38,6 +40,7 @@ rsource "pw_stream/Kconfig"
rsource "pw_string/Kconfig"
rsource "pw_sync_zephyr/Kconfig"
rsource "pw_sys_io_zephyr/Kconfig"
+rsource "pw_system/Kconfig"
rsource "pw_thread_zephyr/Kconfig"
rsource "pw_tokenizer/Kconfig"
rsource "pw_varint/Kconfig"
diff --git a/PIGWEED_MODULES b/PIGWEED_MODULES
index ef740fcf3..6064f4d57 100644
--- a/PIGWEED_MODULES
+++ b/PIGWEED_MODULES
@@ -24,9 +24,11 @@ pw_build_info
pw_build_mcuxpresso
pw_bytes
pw_checksum
+pw_chre
pw_chrono
pw_chrono_embos
pw_chrono_freertos
+pw_chrono_rp2040
pw_chrono_stl
pw_chrono_threadx
pw_chrono_zephyr
@@ -38,23 +40,30 @@ pw_cpu_exception
pw_cpu_exception_cortex_m
pw_crypto
pw_digital_io
+pw_digital_io_mcuxpresso
+pw_digital_io_rp2040
pw_docgen
pw_doctor
+pw_emu
pw_env_setup
pw_file
+pw_format
pw_function
pw_fuzzer
pw_hdlc
pw_hex_dump
pw_i2c
+pw_i2c_linux
pw_i2c_mcuxpresso
pw_ide
pw_interrupt
pw_interrupt_cortex_m
+pw_interrupt_xtensa
pw_interrupt_zephyr
pw_intrusive_ptr
pw_kvs
pw_libc
+pw_libcxx
pw_log
pw_log_android
pw_log_basic
@@ -68,6 +77,7 @@ pw_malloc_freelist
pw_metric
pw_minimal_cpp_stdlib
pw_module
+pw_multibuf
pw_multisink
pw_package
pw_perf_test
@@ -82,14 +92,19 @@ pw_result
pw_ring_buffer
pw_router
pw_rpc
+pw_rpc_transport
pw_rust
pw_snapshot
pw_software_update
pw_span
pw_spi
+pw_spi_mcuxpresso
pw_status
pw_stm32cube_build
pw_stream
+pw_stream_shmem_mcuxpresso
+pw_stream_uart_linux
+pw_stream_uart_mcuxpresso
pw_string
pw_symbolizer
pw_sync
@@ -100,12 +115,13 @@ pw_sync_stl
pw_sync_threadx
pw_sync_zephyr
pw_sys_io
+pw_sys_io_ambiq_sdk
pw_sys_io_arduino
pw_sys_io_baremetal_lm3s6965evb
pw_sys_io_baremetal_stm32f429
pw_sys_io_emcraft_sf2
pw_sys_io_mcuxpresso
-pw_sys_io_pico
+pw_sys_io_rp2040
pw_sys_io_stdio
pw_sys_io_stm32cube
pw_sys_io_zephyr
@@ -123,10 +139,12 @@ pw_tls_client_mbedtls
pw_tokenizer
pw_tool
pw_toolchain
+pw_toolchain_bazel
pw_trace
pw_trace_tokenized
pw_transfer
pw_unit_test
+pw_unit_test_zephyr
pw_varint
pw_watch
pw_web
diff --git a/PW_PLUGINS b/PW_PLUGINS
deleted file mode 100644
index b78b0b3af..000000000
--- a/PW_PLUGINS
+++ /dev/null
@@ -1,23 +0,0 @@
-# The PW_PLUGINS file lists commands that should be included with the pw command
-# when it is invoked in this directory or its subdirectories. Commands in this
-# file override those registered in parent directories.
-#
-# Entries in this file have three columns:
-#
-# <name> <Python module> <function>
-#
-# The Python package containing that module must be installed in the Pigweed
-# virtual environment. The function must have no required arguments and should
-# return an int to use as the exit code.
-
-# keep-sorted: start
-console pw_console.__main__ main
-format pw_presubmit.format_code _pigweed_upstream_main
-heap-viewer pw_allocator.heap_viewer main
-ide pw_ide.__main__ main
-package pw_package.pigweed_packages main
-presubmit pw_presubmit.pigweed_presubmit main
-requires pw_cli.requires main
-rpc pw_hdlc.rpc_console main
-update pw_software_update.cli main
-# keep-sorted: end
diff --git a/README.md b/README.md
index af69b6a3e..e9e865352 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ For more information please see our website: https://pigweed.dev/.
## Links
-<!-- TODO(b/256680603) Remove query string from issue tracker link. -->
+<!-- TODO: b/256680603 - Remove query string from issue tracker link. -->
- [Documentation](https://pigweed.dev/)
- [Source Code](https://cs.pigweed.dev/pigweed)
diff --git a/WORKSPACE b/WORKSPACE
index 8dcbd4781..06f1c89d2 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -16,8 +16,8 @@ workspace(
name = "pigweed",
)
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository")
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load(
"//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl",
"cipd_client_repository",
@@ -30,13 +30,23 @@ load(
# Used in modules: //pw_build, (Assorted modules via select statements).
http_archive(
name = "platforms",
- sha256 = "5308fc1d8865406a49427ba24a9ab53087f17f5266a7aabbfc28823f3916e1ca",
+ sha256 = "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74",
urls = [
- "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.6/platforms-0.0.6.tar.gz",
- "https://github.com/bazelbuild/platforms/releases/download/0.0.6/platforms-0.0.6.tar.gz",
+ "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz",
+ "https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz",
],
)
+local_repository(
+ name = "pw_toolchain",
+ path = "pw_toolchain_bazel",
+)
+
+# Setup xcode on mac.
+load("@pw_toolchain//features/macos:generate_xcode_repository.bzl", "pw_xcode_command_line_tools_repository")
+
+pw_xcode_command_line_tools_repository()
+
# Setup CIPD client and packages.
# Required by: pigweed.
# Used by modules: all.
@@ -48,24 +58,60 @@ cipd_init()
cipd_client_repository()
+# Set up legacy pw_transfer test binaries.
+# Required by: pigweed.
+# Used in modules: //pw_transfer.
cipd_repository(
name = "pw_transfer_test_binaries",
path = "pigweed/pw_transfer_test_binaries/${os=linux}-${arch=amd64}",
tag = "version:pw_transfer_test_binaries_528098d588f307881af83f769207b8e6e1b57520-linux-amd64-cipd.cipd",
)
-# Set up Python support.
-# Required by: rules_fuzzing, com_github_nanopb_nanopb.
-# Used in modules: None.
-http_archive(
- name = "rules_python",
- sha256 = "a30abdfc7126d497a7698c29c46ea9901c6392d6ed315171a6df5ce433aa4502",
- strip_prefix = "rules_python-0.6.0",
- url = "https://github.com/bazelbuild/rules_python/archive/0.6.0.tar.gz",
+# Fetch llvm toolchain.
+# Required by: pigweed.
+# Used in modules: //pw_toolchain.
+cipd_repository(
+ name = "llvm_toolchain",
+ path = "fuchsia/third_party/clang/${os}-${arch}",
+ tag = "git_revision:8475d0a2b853f6184948b428ec679edf84ed2688",
+)
+
+# Fetch linux sysroot for host builds.
+# Required by: pigweed.
+# Used in modules: //pw_toolchain.
+cipd_repository(
+ name = "linux_sysroot",
+ path = "fuchsia/third_party/sysroot/linux",
+ tag = "git_revision:d342388843734b6c5c50fb7e18cd3a76476b93aa",
+)
+
+# Note that the order of registration matters: Bazel will use the first
+# toolchain compatible with the target platform. So, they should be listed from
+# most-restrive to least-restrictive.
+register_toolchains(
+ "//pw_toolchain/host_clang:host_cc_toolchain_linux_kythe",
+ "//pw_toolchain/host_clang:host_cc_toolchain_linux",
+ "//pw_toolchain/host_clang:host_cc_toolchain_macos",
+)
+
+# Fetch gcc-arm-none-eabi toolchain.
+# Required by: pigweed.
+# Used in modules: //pw_toolchain.
+cipd_repository(
+ name = "gcc_arm_none_eabi_toolchain",
+ path = "fuchsia/third_party/armgcc/${os}-${arch}",
+ tag = "version:2@12.2.mpacbti-rel1.1",
+)
+
+register_toolchains(
+ "//pw_toolchain/arm_gcc:arm_gcc_cc_toolchain_cortex-m0",
+ "//pw_toolchain/arm_gcc:arm_gcc_cc_toolchain_cortex-m3",
+ "//pw_toolchain/arm_gcc:arm_gcc_cc_toolchain_cortex-m4",
+ "//pw_toolchain/arm_gcc:arm_gcc_cc_toolchain_cortex-m4+nofp",
)
# Set up Starlark library.
-# Required by: io_bazel_rules_go, com_google_protobuf.
+# Required by: io_bazel_rules_go, com_google_protobuf, rules_python
# Used in modules: None.
# This must be instantiated before com_google_protobuf as protobuf_deps() pulls
# in an older version of bazel_skylib. However io_bazel_rules_go requires a
@@ -81,9 +127,48 @@ load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
bazel_skylib_workspace()
+# Set up Python support.
+# Required by: rules_fuzzing, com_github_nanopb_nanopb.
+# Used in modules: None.
+http_archive(
+ name = "rules_python",
+ sha256 = "0a8003b044294d7840ac7d9d73eef05d6ceb682d7516781a4ec62eeb34702578",
+ strip_prefix = "rules_python-0.24.0",
+ url = "https://github.com/bazelbuild/rules_python/releases/download/0.24.0/rules_python-0.24.0.tar.gz",
+)
+
+load("@rules_python//python:repositories.bzl", "python_register_toolchains")
+
+# Use Python 3.10 for bazel Python rules.
+python_register_toolchains(
+ name = "python3_10",
+ # Allows building as root in a docker container. Required by oss-fuzz.
+ ignore_root_user_error = True,
+ python_version = "3.10",
+)
+
+load("@python3_10//:defs.bzl", "interpreter")
+load("@rules_python//python:pip.bzl", "pip_parse")
+
+# Specify third party Python package versions with pip_parse.
+# pip_parse will generate and expose a repository for each package in the
+# requirements_lock file named @python_packages_{PACKAGE}.
+pip_parse(
+ name = "python_packages",
+ python_interpreter_target = interpreter,
+ requirements_darwin = "//pw_env_setup/py/pw_env_setup/virtualenv_setup:upstream_requirements_darwin_lock.txt",
+ requirements_linux = "//pw_env_setup/py/pw_env_setup/virtualenv_setup:upstream_requirements_linux_lock.txt",
+ requirements_windows = "//pw_env_setup/py/pw_env_setup/virtualenv_setup:upstream_requirements_windows_lock.txt",
+)
+
+load("@python_packages//:requirements.bzl", "install_deps")
+
+# Run pip install for all @python_packages_*//:pkg deps.
+install_deps()
+
# Set up upstream googletest and googlemock.
# Required by: Pigweed.
-# Used in modules: //pw_analog, //pw_i2c.
+# Used in modules: //pw_analog, //pw_fuzzer, //pw_i2c.
http_archive(
name = "com_google_googletest",
sha256 = "ad7fdba11ea011c1d925b3289cf4af2c66a352e18d4c7264392fead75e919363",
@@ -93,26 +178,7 @@ http_archive(
],
)
-# Set up host hermetic host toolchain.
-# Required by: All cc targets.
-# Used in modules: All cc targets.
-git_repository(
- name = "rules_cc_toolchain",
- commit = "9f209fda87414285bc66accd3612575b29760fba",
- remote = "https://github.com/bazelembedded/rules_cc_toolchain",
- shallow_since = "1675385535 -0800",
-)
-
-load("@rules_cc_toolchain//:rules_cc_toolchain_deps.bzl", "rules_cc_toolchain_deps")
-
-rules_cc_toolchain_deps()
-
-load("@rules_cc_toolchain//cc_toolchain:cc_toolchain.bzl", "register_cc_toolchains")
-
-register_cc_toolchains()
-
# Sets up Bazels documentation generator.
-# Required by: rules_cc_toolchain.
# Required by modules: All
git_repository(
name = "io_bazel_stardoc",
@@ -121,29 +187,6 @@ git_repository(
shallow_since = "1651081130 -0400",
)
-# Set up tools to build custom GRPC rules.
-#
-# We use a fork that silences some zlib compilation warnings.
-#
-# Required by: pigweed.
-# Used in modules: //pw_protobuf.
-git_repository(
- name = "rules_proto_grpc",
- commit = "2fbf774a5553b773372f7b91f9b1dc06ee0da2d3",
- remote = "https://github.com/tpudlik/rules_proto_grpc.git",
- shallow_since = "1675375991 -0800",
-)
-
-load(
- "@rules_proto_grpc//:repositories.bzl",
- "rules_proto_grpc_repos",
- "rules_proto_grpc_toolchains",
-)
-
-rules_proto_grpc_toolchains()
-
-rules_proto_grpc_repos()
-
# Set up Protobuf rules.
# Required by: pigweed.
# Used in modules: //pw_protobuf.
@@ -161,144 +204,81 @@ protobuf_deps()
# Setup Nanopb protoc plugin.
# Required by: Pigweed.
# Used in modules: pw_protobuf.
-git_repository(
+http_archive(
name = "com_github_nanopb_nanopb",
- commit = "e601fca6d9ed7fb5c09e2732452753b2989f128b",
- remote = "https://github.com/nanopb/nanopb.git",
- shallow_since = "1641373017 +0800",
+ sha256 = "3f78bf63722a810edb6da5ab5f0e76c7db13a961c2aad4ab49296e3095d0d830",
+ strip_prefix = "nanopb-0.4.8",
+ url = "https://github.com/nanopb/nanopb/archive/refs/tags/0.4.8.tar.gz",
)
-load("@com_github_nanopb_nanopb//:nanopb_deps.bzl", "nanopb_deps")
+load("@com_github_nanopb_nanopb//extra/bazel:nanopb_deps.bzl", "nanopb_deps")
nanopb_deps()
-load("@com_github_nanopb_nanopb//:python_deps.bzl", "nanopb_python_deps")
+load("@com_github_nanopb_nanopb//extra/bazel:python_deps.bzl", "nanopb_python_deps")
-nanopb_python_deps()
+nanopb_python_deps(interpreter)
-load("@com_github_nanopb_nanopb//:nanopb_workspace.bzl", "nanopb_workspace")
+load("@com_github_nanopb_nanopb//extra/bazel:nanopb_workspace.bzl", "nanopb_workspace")
nanopb_workspace()
-# Set up embedded C/C++ toolchains.
-# Required by: pigweed.
-# Used in modules: //pw_polyfill, //pw_build (all pw_cc* targets).
-git_repository(
- name = "bazel_embedded",
- commit = "91dcc13ebe5df755ca2fe896ff6f7884a971d05b",
- remote = "https://github.com/bazelembedded/bazel-embedded.git",
- shallow_since = "1631751909 +0800",
-)
-
-# Configure bazel_embedded toolchains and platforms.
-load(
- "@bazel_embedded//:bazel_embedded_deps.bzl",
- "bazel_embedded_deps",
-)
-
-bazel_embedded_deps()
-
-load(
- "@bazel_embedded//platforms:execution_platforms.bzl",
- "register_platforms",
-)
-
-register_platforms()
-
-# Fetch gcc-arm-none-eabi compiler and register for toolchain
-# resolution.
-load(
- "@bazel_embedded//toolchains/compilers/gcc_arm_none_eabi:gcc_arm_none_repository.bzl",
- "gcc_arm_none_compiler",
-)
-
-gcc_arm_none_compiler()
-
-load(
- "@bazel_embedded//toolchains/gcc_arm_none_eabi:gcc_arm_none_toolchain.bzl",
- "register_gcc_arm_none_toolchain",
-)
-
-register_gcc_arm_none_toolchain()
-
# Rust Support
#
-git_repository(
+http_archive(
name = "rules_rust",
- # Pulls in the main branch with https://github.com/bazelbuild/rules_rust/pull/1803
- # merged. Once a release is cut with that commit, we should switch to
- # using a release tarbal.
- commit = "a5853fd37053b65ee30ba4f8064b9db67c90d53f",
- remote = "https://github.com/bazelbuild/rules_rust",
- shallow_since = "1675302817 -0800",
+ patch_args = ["-p1"],
+ patches = [
+ # Fix rustdoc test w/ proc macros
+ # https://github.com/bazelbuild/rules_rust/pull/1952
+ "//pw_rust/bazel_patches:0001-rustdoc_test-Apply-prefix-stripping-to-proc_macro-de.patch",
+ # Adds prototype functionality for documenting multiple crates in one
+ # HTML output directory. While the approach in this patch may have
+ # issues scaling to giant mono-repos, it is apporpriate for embedded
+ # projects and minimally invasive and should be easy to maintain. Once
+ # the `rules_rust` community decides on a way to propperly support this,
+ # we will migrate to that solution.
+ # https://github.com/konkers/rules_rust/tree/wip/rustdoc
+ "//pw_rust/bazel_patches:0002-PROTOTYPE-Add-ability-to-document-multiple-crates-at.patch",
+ ],
+ sha256 = "9d04e658878d23f4b00163a72da3db03ddb451273eb347df7d7c50838d698f49",
+ urls = ["https://github.com/bazelbuild/rules_rust/releases/download/0.26.0/rules_rust-v0.26.0.tar.gz"],
)
-load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_analyzer_toolchain_repository", "rust_repository_set")
+load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies")
rules_rust_dependencies()
-# Here we pull in a specific toolchain. Unfortunately `rust_repository_set`
-# does not provide a way to add `target_compatible_with` options which are
-# needed to be compatible with `@bazel_embedded` (specifically
-# `@bazel_embedded//constraints/fpu:none` which is specified in
-# `//platforms`)
-#
-# See `//toolchain:rust_linux_x86_64` for how this is used.
-#
-# Note: This statement creates name mangled remotes of the form:
-# `@{name}__{triplet}_tools`
-# (example: `@rust_linux_x86_64__thumbv7m-none-eabi_tools/`)
-rust_repository_set(
- name = "rust_linux_x86_64",
- edition = "2021",
- exec_triple = "x86_64-unknown-linux-gnu",
- extra_target_triples = [
- "thumbv7m-none-eabi",
- "thumbv6m-none-eabi",
- ],
- versions = ["1.67.0"],
+load(
+ "//pw_toolchain/rust:defs.bzl",
+ "pw_rust_register_toolchain_and_target_repos",
+ "pw_rust_register_toolchains",
)
-# Registers our Rust toolchains that are compatable with `@bazel_embedded`.
-register_toolchains(
- "//pw_toolchain:thumbv7m_rust_linux_x86_64",
- "//pw_toolchain:thumbv6m_rust_linux_x86_64",
+pw_rust_register_toolchain_and_target_repos(
+ cipd_tag = "rust_revision:faee636ebfff793ea9dcff17960a611b580e3cd5",
)
# Allows creation of a `rust-project.json` file to allow rust analyzer to work.
load("@rules_rust//tools/rust_analyzer:deps.bzl", "rust_analyzer_dependencies")
-# Since we do not use rust_register_toolchains, we need to define a
-# rust_analyzer_toolchain.
-register_toolchains(rust_analyzer_toolchain_repository(
- name = "rust_analyzer_toolchain",
- # This should match the currently registered toolchain.
- version = "1.67.0",
-))
-
rust_analyzer_dependencies()
+pw_rust_register_toolchains()
+
# Vendored third party rust crates.
git_repository(
name = "rust_crates",
- commit = "c39c1d1d4e4bdf2d8145beb8882af6f6e4e6dbbc",
+ commit = "6d975531f7672cc6aa54bdd7517e1beeffa578da",
remote = "https://pigweed.googlesource.com/third_party/rust_crates",
shallow_since = "1675359057 +0000",
)
# Registers platforms for use with toolchain resolution
-register_execution_platforms("//pw_build/platforms:all")
-
-load("//pw_build:target_config.bzl", "pigweed_config")
+register_execution_platforms("@local_config_platform//:host", "//pw_build/platforms:all")
-# Configure Pigweeds backend.
-pigweed_config(
- name = "pigweed_config",
- build_file = "//targets:default_config.BUILD",
-)
-
-# Required by: rules_fuzzing.
+# Required by: rules_fuzzing, fuzztest
#
# Provided here explicitly to override an old version of absl that
# rules_fuzzing_dependencies attempts to pull in. That version has
@@ -328,6 +308,28 @@ load("@rules_fuzzing//fuzzing:init.bzl", "rules_fuzzing_init")
rules_fuzzing_init()
+load("@fuzzing_py_deps//:requirements.bzl", fuzzing_install_deps = "install_deps")
+
+fuzzing_install_deps()
+
+# Required by: fuzztest
+http_archive(
+ name = "com_googlesource_code_re2",
+ sha256 = "f89c61410a072e5cbcf8c27e3a778da7d6fd2f2b5b1445cd4f4508bee946ab0f",
+ strip_prefix = "re2-2022-06-01",
+ url = "https://github.com/google/re2/archive/refs/tags/2022-06-01.tar.gz",
+)
+
+# Required by: pigweed.
+# Used in modules: //pw_fuzzer.
+FUZZTEST_COMMIT = "f2e9e2a19a7b16101d1e6f01a87e639687517a1c"
+
+http_archive(
+ name = "com_google_fuzztest",
+ strip_prefix = "fuzztest-" + FUZZTEST_COMMIT,
+ url = "https://github.com/google/fuzztest/archive/" + FUZZTEST_COMMIT + ".zip",
+)
+
RULES_JVM_EXTERNAL_TAG = "2.8"
RULES_JVM_EXTERNAL_SHA = "79c9850690d7614ecdb72d68394f994fef7534b292c4867ce5e7dec0aa7bdfad"
@@ -375,6 +377,15 @@ git_repository(
shallow_since = "1637714942 +0000",
)
+git_repository(
+ name = "mbedtls",
+ build_file = "//:third_party/mbedtls/BUILD.mbedtls",
+ # mbedtls-3.2.1 released 2022-07-12
+ commit = "869298bffeea13b205343361b7a7daf2b210e33d",
+ remote = "https://pigweed.googlesource.com/third_party/github/ARMmbed/mbedtls",
+ shallow_since = "1648504566 -0700",
+)
+
http_archive(
name = "freertos",
build_file = "//:third_party/freertos/BUILD.bazel",
@@ -382,3 +393,27 @@ http_archive(
strip_prefix = "FreeRTOS-Kernel-10.5.1",
urls = ["https://github.com/FreeRTOS/FreeRTOS-Kernel/archive/refs/tags/V10.5.1.tar.gz"],
)
+
+http_archive(
+ name = "stm32f4xx_hal_driver",
+ build_file = "//third_party/stm32cube:stm32_hal_driver.BUILD.bazel",
+ sha256 = "c8741e184555abcd153f7bdddc65e4b0103b51470d39ee0056ce2f8296b4e835",
+ strip_prefix = "stm32f4xx_hal_driver-1.8.0",
+ urls = ["https://github.com/STMicroelectronics/stm32f4xx_hal_driver/archive/refs/tags/v1.8.0.tar.gz"],
+)
+
+http_archive(
+ name = "cmsis_device_f4",
+ build_file = "//third_party/stm32cube:cmsis_device.BUILD.bazel",
+ sha256 = "6390baf3ea44aff09d0327a3c112c6ca44418806bfdfe1c5c2803941c391fdce",
+ strip_prefix = "cmsis_device_f4-2.6.8",
+ urls = ["https://github.com/STMicroelectronics/cmsis_device_f4/archive/refs/tags/v2.6.8.tar.gz"],
+)
+
+http_archive(
+ name = "cmsis_core",
+ build_file = "//third_party/stm32cube:cmsis_core.BUILD.bazel",
+ sha256 = "f711074a546bce04426c35e681446d69bc177435cd8f2f1395a52db64f52d100",
+ strip_prefix = "cmsis_core-5.4.0_cm4",
+ urls = ["https://github.com/STMicroelectronics/cmsis_core/archive/refs/tags/v5.4.0_cm4.tar.gz"],
+)
diff --git a/bootstrap.bat b/bootstrap.bat
index a61d2233e..2f6065909 100644
--- a/bootstrap.bat
+++ b/bootstrap.bat
@@ -110,7 +110,6 @@ call "%python%" "%PW_ROOT%\pw_env_setup\py\pw_env_setup\env_setup.py" ^
--pw-root "%PW_ROOT%" ^
--shell-file "%shell_file%" ^
--install-dir "%_PW_ACTUAL_ENVIRONMENT_ROOT%" ^
- --config-file "%PW_ROOT%/pw_env_setup/config.json" ^
--project-root "%PW_PROJECT_ROOT%"
goto activate_shell
@@ -123,7 +122,13 @@ if exist "%shell_file%" (
)
:activate_shell
-call "%shell_file%"
+if exist "%shell_file%" (
+ call "%shell_file%"
+) else (
+ echo.
+ echo Bootstrap failed! See output above for the culprit.
+ echo.
+)
:finish
::WINDOWS_ONLY
diff --git a/bootstrap.sh b/bootstrap.sh
index a615fc501..dedcc5bcd 100644
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -81,7 +81,7 @@ SETUP_SH="$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.sh"
if [ "$(basename "$_PW_BOOTSTRAP_PATH")" = "bootstrap.sh" ] || \
[ ! -f "$SETUP_SH" ] || \
[ ! -s "$SETUP_SH" ]; then
- pw_bootstrap --shell-file "$SETUP_SH" --install-dir "$_PW_ACTUAL_ENVIRONMENT_ROOT" --config-file "$PW_ROOT/pw_env_setup/config.json"
+ pw_bootstrap --shell-file "$SETUP_SH" --install-dir "$_PW_ACTUAL_ENVIRONMENT_ROOT"
pw_finalize bootstrap "$SETUP_SH"
else
pw_activate
@@ -93,6 +93,18 @@ unset _PW_BOOTSTRAP_PATH
unset SETUP_SH
unset _bootstrap_abspath
+if [[ "$TERM" != dumb ]]; then
+ # Shell completion: bash.
+ if test -n "$BASH"; then
+ . "$PW_ROOT/pw_cli/py/pw_cli/shell_completion/pw.bash"
+ . "$PW_ROOT/pw_cli/py/pw_cli/shell_completion/pw_build.bash"
+ # Shell completion: zsh.
+ elif test -n "$ZSH_NAME"; then
+ . "$PW_ROOT/pw_cli/py/pw_cli/shell_completion/pw.zsh"
+ . "$PW_ROOT/pw_cli/py/pw_cli/shell_completion/pw_build.zsh"
+ fi
+fi
+
pw_cleanup
git -C "$PW_ROOT" config blame.ignoreRevsFile .git-blame-ignore-revs
diff --git a/docker/docs.rst b/docker/docs.rst
index ed6a29050..6bdc60651 100644
--- a/docker/docs.rst
+++ b/docker/docs.rst
@@ -1,25 +1,33 @@
------
-docker
+Docker
------
``pw_env_setup`` reliably initializes a working environment for Pigweed, but
can take awhile to run. It intelligently caches where it can, but that caching
is designed around a particular execution environment. That environment
-assumption is poor when running tests with docker. To help out teams using
-docker for automated testing, the Pigweed team has a publicly-available docker
+assumption is poor when running tests with Docker. To help out teams using
+Docker for automated testing, the Pigweed team has a publicly-available Docker
image with a cache of some of the Pigweed environment. The current tag of this
image is in ``docker/tag`` under the Pigweed checkout.
-To build your own docker image, start with ``docker/Dockerfile.cache`` and
+We're still trying to improve this process, so if you have any ideas for
+improvements please `create an issue`_.
+
+.. _create an issue: https://issues.pigweed.dev/issues?q=status:open
+
+Build your own Docker image
+===========================
+
+To build your own Docker image, start with ``docker/Dockerfile.cache`` and
run ``docker build --file docker/Dockerfile.cache .`` from the root of your
Pigweed checkout.
-To use the publicly-available docker image, run
+Use the publicly available Docker image
+=======================================
+
+To use the publicly-available Docker image, run
``docker build --file docker/Dockerfile.run --build-arg from=$(cat docker/tag) .``
from the root of your Pigweed checkout. You still need to run
-``. ./bootstrap.sh`` within the docker image, but it should complete much
-faster than on a vanilla docker image.
+``. ./bootstrap.sh`` within the Docker image, but it should complete much
+faster than on a vanilla Docker image.
-We're still trying to improve this process, so if you have any ideas for
-improvements please `send us a note`_.
-.. _send us a note: pigweed@googlegroups.com
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index bbfc362db..dc0968b87 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -21,6 +21,7 @@ import("$dir_pw_docgen/docs.gni")
pw_doc_group("static_assets") {
inputs = [
"_static/css/pigweed.css",
+ "_static/js/pigweed.js",
"_static/pw_logo.ico",
"_static/pw_logo.svg",
]
@@ -30,54 +31,53 @@ pw_doc_group("static_assets") {
# split out from the overall docgen target below.
pw_doc_group("core_docs") {
inputs = [
+ "$dir_pw_build/py/gn_tests/BUILD.gn",
+ "$dir_pw_build/py/pw_build/generate_python_wheel_cache.py",
"$dir_pw_build/python.gni",
- "images/pw_env_setup_demo.gif",
- "images/pw_status_test.png",
- "images/pw_watch_build_demo.gif",
- "images/pw_watch_on_device_demo.gif",
- "images/pw_watch_test_demo.gif",
- "images/pw_watch_test_demo2.gif",
- "images/stm32f429i-disc1_connected.jpg",
+ "$dir_pw_build/python_gn_args.gni",
+ "layout/page.html",
"run_doxygen.py",
]
sources = [
+ "blog/01-kudzu.rst",
+ "blog/index.rst",
"code_of_conduct.rst",
+ "code_reviews.rst",
"concepts/index.rst",
- "contributing.rst",
+ "contributing/index.rst",
+ "contributing/module_docs.rst",
+ "editors.rst",
"embedded_cpp_guide.rst",
+ "facades.rst",
"faq.rst",
- "getting_started.rst",
+ "get_started/bazel.rst",
+ "get_started/index.rst",
+ "get_started/upstream.rst",
+ "glossary.rst",
+ "infra/ci_cq_intro.rst",
+ "infra/index.rst",
+ "infra/rollers.rst",
+ "mission.rst",
"module_structure.rst",
- "os_abstraction_layers.rst",
+ "os/index.rst",
+ "os/zephyr/index.rst",
+ "os/zephyr/kconfig.rst",
+ "overview.rst",
"size_optimizations.rst",
+ "style/commit_message.rst",
+ "style/cpp.rst",
+ "style/doxygen.rst",
+ "style/sphinx.rst",
"style_guide.rst",
]
}
-pw_doc_group("release_notes") {
- sources = [
- "release_notes/2022_jan.rst",
- "release_notes/index.rst",
- ]
-}
-
-pw_doc_group("templates") {
- sources = [
- "templates/docs/api.rst",
- "templates/docs/cli.rst",
- "templates/docs/concepts.rst",
- "templates/docs/design.rst",
- "templates/docs/docs.rst",
- "templates/docs/gui.rst",
- "templates/docs/guides.rst",
- "templates/docs/tutorials/index.rst",
- ]
-}
-
# Documentation for upstream Pigweed targets.
group("target_docs") {
deps = [
"$dir_pigweed/targets/android:target_docs",
+ "$dir_pigweed/targets/apollo4:target_docs",
+ "$dir_pigweed/targets/apollo4_pw_system:target_docs",
"$dir_pigweed/targets/arduino:target_docs",
"$dir_pigweed/targets/docs:target_docs",
"$dir_pigweed/targets/emcraft_sf2_som:docs",
@@ -98,38 +98,77 @@ group("module_docs") {
group("third_party_docs") {
deps = [
+ "$dir_pigweed/third_party/abseil-cpp:docs",
"$dir_pigweed/third_party/boringssl:docs",
"$dir_pigweed/third_party/emboss:docs",
"$dir_pigweed/third_party/freertos:docs",
"$dir_pigweed/third_party/fuchsia:docs",
+ "$dir_pigweed/third_party/fuzztest:docs",
"$dir_pigweed/third_party/googletest:docs",
"$dir_pigweed/third_party/nanopb:docs",
+ "$dir_pigweed/third_party/re2:docs",
"$dir_pigweed/third_party/tinyusb:docs",
]
}
-_doxygen_input_files = [
- # All sources with doxygen comment blocks.
+# All sources with doxygen comment blocks.
+_doxygen_input_files = [ # keep-sorted: start
+ "$dir_pw_allocator/public/pw_allocator/allocator.h",
+ "$dir_pw_allocator/public/pw_allocator/block.h",
+ "$dir_pw_allocator/public/pw_allocator/freelist.h",
+ "$dir_pw_analog/public/pw_analog/analog_input.h",
+ "$dir_pw_analog/public/pw_analog/microvolt_input.h",
+ "$dir_pw_async/public/pw_async/context.h",
"$dir_pw_async/public/pw_async/dispatcher.h",
"$dir_pw_async/public/pw_async/fake_dispatcher_fixture.h",
+ "$dir_pw_async/public/pw_async/function_dispatcher.h",
+ "$dir_pw_async/public/pw_async/heap_dispatcher.h",
"$dir_pw_async/public/pw_async/task.h",
+ "$dir_pw_async/public/pw_async/task_function.h",
"$dir_pw_async_basic/public/pw_async_basic/dispatcher.h",
+ "$dir_pw_base64/public/pw_base64/base64.h",
"$dir_pw_bluetooth/public/pw_bluetooth/gatt/client.h",
"$dir_pw_bluetooth/public/pw_bluetooth/gatt/server.h",
"$dir_pw_bluetooth/public/pw_bluetooth/host.h",
"$dir_pw_bluetooth/public/pw_bluetooth/low_energy/central.h",
"$dir_pw_bluetooth/public/pw_bluetooth/low_energy/connection.h",
"$dir_pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h",
+ "$dir_pw_chre/public/pw_chre/chre.h",
+ "$dir_pw_chre/public/pw_chre/host_link.h",
"$dir_pw_chrono/public/pw_chrono/system_clock.h",
"$dir_pw_chrono/public/pw_chrono/system_timer.h",
+ "$dir_pw_containers/public/pw_containers/filtered_view.h",
+ "$dir_pw_containers/public/pw_containers/inline_deque.h",
+ "$dir_pw_containers/public/pw_containers/inline_queue.h",
+ "$dir_pw_containers/public/pw_containers/variable_length_entry_queue.h",
+ "$dir_pw_crypto/public/pw_crypto/ecdsa.h",
+ "$dir_pw_crypto/public/pw_crypto/sha256.h",
+ "$dir_pw_digital_io/public/pw_digital_io/digital_io.h",
+ "$dir_pw_function/public/pw_function/function.h",
+ "$dir_pw_function/public/pw_function/pointer.h",
"$dir_pw_function/public/pw_function/scope_guard.h",
+ "$dir_pw_hdlc/public/pw_hdlc/decoder.h",
+ "$dir_pw_hdlc/public/pw_hdlc/encoder.h",
+ "$dir_pw_i2c/public/pw_i2c/initiator.h",
+ "$dir_pw_i2c_linux/public/pw_i2c_linux/initiator.h",
+ "$dir_pw_interrupt/public/pw_interrupt/context.h",
+ "$dir_pw_log/public/pw_log/tokenized_args.h",
+ "$dir_pw_log_string/public/pw_log_string/handler.h",
+ "$dir_pw_log_tokenized/public/pw_log_tokenized/base64.h",
"$dir_pw_log_tokenized/public/pw_log_tokenized/handler.h",
"$dir_pw_log_tokenized/public/pw_log_tokenized/metadata.h",
+ "$dir_pw_multibuf/public/pw_multibuf/chunk.h",
+ "$dir_pw_multibuf/public/pw_multibuf/multibuf.h",
+ "$dir_pw_protobuf/public/pw_protobuf/find.h",
+ "$dir_pw_random/public/pw_random/random.h",
+ "$dir_pw_random/public/pw_random/xor_shift.h",
"$dir_pw_rpc/public/pw_rpc/internal/config.h",
+ "$dir_pw_rpc/public/pw_rpc/synchronous_call.h",
+ "$dir_pw_status/public/pw_status/status.h",
+ "$dir_pw_stream/public/pw_stream/stream.h",
+ "$dir_pw_stream_uart_linux/public/pw_stream_uart_linux/stream.h",
"$dir_pw_string/public/pw_string/format.h",
"$dir_pw_string/public/pw_string/string.h",
- "$dir_pw_function/public/pw_function/function.h",
- "$dir_pw_function/public/pw_function/pointer.h",
"$dir_pw_string/public/pw_string/string_builder.h",
"$dir_pw_string/public/pw_string/util.h",
"$dir_pw_sync/public/pw_sync/binary_semaphore.h",
@@ -143,9 +182,20 @@ _doxygen_input_files = [
"$dir_pw_sync/public/pw_sync/timed_mutex.h",
"$dir_pw_sync/public/pw_sync/timed_thread_notification.h",
"$dir_pw_sync/public/pw_sync/virtual_basic_lockable.h",
+ "$dir_pw_sys_io/public/pw_sys_io/sys_io.h",
+ "$dir_pw_thread/public/pw_thread/test_thread_context.h",
+ "$dir_pw_tokenizer/public/pw_tokenizer/config.h",
"$dir_pw_tokenizer/public/pw_tokenizer/encode_args.h",
+ "$dir_pw_tokenizer/public/pw_tokenizer/nested_tokenization.h",
+ "$dir_pw_tokenizer/public/pw_tokenizer/token_database.h",
"$dir_pw_tokenizer/public/pw_tokenizer/tokenize.h",
-]
+ "$dir_pw_toolchain/public/pw_toolchain/no_destructor.h",
+ "$dir_pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h",
+ "$dir_pw_unit_test/public/pw_unit_test/internal/framework.h",
+ "$dir_pw_varint/public/pw_varint/stream.h",
+ "$dir_pw_varint/public/pw_varint/varint.h",
+ "$dir_pw_work_queue/public/pw_work_queue/work_queue.h",
+] # keep-sorted: end
pw_python_action("generate_doxygen") {
_output_dir = "docs/doxygen"
@@ -177,6 +227,7 @@ pw_doc_gen("docs") {
# contrast, the pw_doc_group above should not use the docs prefix.
"automated_analysis.rst",
"build_system.rst",
+ "changelog.rst",
"index.rst",
"module_guides.rst",
"python_build.rst",
@@ -188,11 +239,10 @@ pw_doc_gen("docs") {
":core_docs",
":generate_doxygen",
":module_docs",
- ":release_notes",
":static_assets",
":target_docs",
- ":templates",
":third_party_docs",
+ "$dir_pigweed/kudzu:docs",
"$dir_pigweed/seed:docs",
"$dir_pw_env_setup:python.install",
]
diff --git a/docs/Doxyfile b/docs/Doxyfile
index 28f2b8f22..ef05ea348 100644
--- a/docs/Doxyfile
+++ b/docs/Doxyfile
@@ -290,7 +290,9 @@ ALIASES = "rst=^^\verbatim embed:rst:leading-asterisk^^" \
"c_macro{1}=@crossref{c,macro,\1}" \
"cpp_class{1}=@crossref{cpp,class,\1}" \
"cpp_func{1}=@crossref{cpp,func,\1}" \
- "cpp_type{1}=@crossref{cpp,type,\1}"
+ "cpp_type{1}=@crossref{cpp,type,\1}" \
+ "cpp_enum{1}=@crossref{cpp,type,\1}" \
+ "pw_status{1}=@crossref{c,enumerator,\1}"
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
# only. Doxygen will then generate output that is more tailored for C. For
@@ -2382,6 +2384,7 @@ PREDEFINED = __cplusplus \
PW_LOCKABLE= \
PW_PRINTF_FORMAT(...)= \
PW_CONSTEXPR_CPP20= \
+ PW_INLINE_VARIABLE=inline \
PW_EXCLUSIVE_LOCK_FUNCTION(...)= \
PW_EXCLUSIVE_TRYLOCK_FUNCTION(...)= \
PW_UNLOCK_FUNCTION(...)= \
@@ -2391,6 +2394,8 @@ PREDEFINED = __cplusplus \
PW_LOCKS_EXCLUDED(...)= \
PW_EXCLUSIVE_LOCKS_REQUIRED(...)= \
PW_GUARDED_BY(...)= \
+ PW_NO_SANITIZE(...)= \
+ PW_EXCLUDE_FROM_DOXYGEN=1
# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
# tag can be used to specify a list of macro names that should be expanded. The
diff --git a/docs/_static/css/pigweed.css b/docs/_static/css/pigweed.css
index bea0fee45..a90cf4f4e 100644
--- a/docs/_static/css/pigweed.css
+++ b/docs/_static/css/pigweed.css
@@ -24,8 +24,21 @@
letter-spacing: 0.1em;
text-transform: uppercase;
}
+
.sidebar-brand-text {
font-size: 2.5rem;
+ text-align: center;
+}
+
+/********** Scrolling behavior ***********/
+
+/*
+ * All nodes across the site should scroll instantly. This rule is intentionally
+ * broad and agressive. Context:
+ * https://pigweed.dev/docs/style_guide.html#site-nav-scrolling
+ */
+* {
+ scroll-behavior: auto !important;
}
/********** General document coloring ***********/
@@ -77,9 +90,10 @@ dl.describe>dt {
text-indent: 0em;
}
-/* Diagram backgrounds should always be light */
-div>svg {
- background: #ffffff;
+/* Remove blank space before/after function signature open/close parens. */
+.sig > dl {
+ margin-block-start: 0;
+ margin-block-end: 0;
}
/* Make inline code standout more */
@@ -103,6 +117,11 @@ body[data-theme="light"] .highlight .hll {
background-color: var(--color-code-hll-background);
}
+/* Override pygments.css code block background color */
+body[data-theme="dark"] .highlight {
+ background-color: var(--color-code-background) !important;
+}
+
/* Use normal mixed-case for h4, h5, and h6 */
h4, h5, h6 {
text-transform: none;
@@ -133,46 +152,93 @@ h4, h5, h6 {
font-size: var(--font-size--normal);
}
-/* Support taglines inline with page titles */
-section.with-subtitle > h1 {
- display: inline;
+/* Indicate in-progress SEEDs */
+a[href="seed/0000-index.html"] ~ ul > li > a.reference.external[href*="pigweed-review.googlesource.com"]::before {
+ content: "🚧 ";
}
-/* Restore the padding to the top of the page that was removed by making the
- h1 element inline */
-section.with-subtitle {
- padding-top: 1.5em;
+div.pw-main {
+ display: flex;
+ flex-direction: column;
}
-.section-subtitle {
- display: inline;
+div.pw-topnav {
+ background-color: var(--color-background-primary);
+ padding: 0 3em;
+ width: 100%;
+}
+
+div.pw-topnav-inline {
+ display: flex;
+ flex-direction: row;
+ padding: 1em 0 0 0;
+}
+
+div.pw-topnav-inline h1 {
+ padding: 0;
+ margin: 0;
+}
+
+.pw-topnav-title {
+ font-size: 2.5em;
+ font-weight: 700;
+ line-height: 1.25;
+ padding: 0;
+ margin: 0;
+}
+
+.pw-topnav-subtitle {
+ display: block;
font-size: larger;
font-weight: bold;
+ margin: 0.75em 0 0.5em 0;
+ padding: 0;
+}
+
+.pw-text-center-align {
+ text-align: center
}
-/* Styling for module doc section buttons */
-ul.pw-module-section-buttons {
+ul.pw-module-section-nav-group {
display: flex;
- justify-content: center;
+ margin: 0 1em;
padding: 0;
}
-li.pw-module-section-button {
+li.pw-module-section-nav-link {
display: inline;
list-style-type: none;
- padding: 0 4px;
+ margin: auto;
+ padding: 0 1em;
}
-li.pw-module-section-button p {
+li.pw-module-section-nav-link p {
display: inline;
+ font-size: large;
}
-li.pw-module-section-button p a {
- background-color: var(--color-section-button) !important;
- border-color: var(--color-section-button) !important;
+li.pw-module-section-nav-link p a {
+ color: var(--color-section-button) !important;
+}
+
+li.pw-module-section-nav-link p a:hover {
+ color: var(--color-section-button-hover) !important;
+}
+
+div.font-monospace {
+ font-family: var(--font-stack--monospace);
+}
+
+/* Sales pitch pages like //pw_tokenizer/docs.rst use the feature grid as a
+ * call-to-action to check out other docs in the set. The following CSS makes
+ * the feature grid items stand out a bit more. */
+
+.sales-pitch-cta-primary .sd-card-title {
+ border-bottom: 3px solid var(--color-sidebar-brand-text);
+ padding-bottom: 0.5rem;
}
-li.pw-module-section-button p a:hover {
- background-color: var(--color-section-button-hover) !important;
- border-color: var(--color-section-button-hover) !important;
+.sales-pitch-cta-secondary .sd-card-title {
+ border-bottom: 3px solid var(--color-sidebar-link-text);
+ padding-bottom: 0.5rem;
}
diff --git a/docs/_static/js/pigweed.js b/docs/_static/js/pigweed.js
new file mode 100644
index 000000000..6553ed336
--- /dev/null
+++ b/docs/_static/js/pigweed.js
@@ -0,0 +1,75 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+window.pigweed = {};
+
+// Scroll the site nav so that the current page is visible.
+// Context: https://pigweed.dev/docs/style_guide.html#site-nav-scrolling
+window.pigweed.scrollSiteNav = () => {
+ const siteNav = document.querySelector('.sidebar-scroll');
+ // The node within the site nav that represents the page that the user is
+ // currently looking at.
+ const currentPage = document.querySelector('.current-page');
+ // Determine which site nav node to scroll to. Scroll directly to top-level
+ // (L1) and second-level (L2) nodes. For L3 nodes and deeper, scroll to the
+ // node's L2 ancestor so that the user sees all the docs in the set.
+ let targetNode;
+ if (
+ currentPage.classList.contains('toctree-l1') ||
+ currentPage.classList.contains('toctree-l2')
+ ) {
+ targetNode = currentPage;
+ } else {
+ targetNode = document.querySelector('li.toctree-l2.current');
+ }
+ // Scroll to the node. Note that we also tried scrollIntoView() but
+ // it wasn't reliable enough.
+ const scrollDistance =
+ targetNode.getBoundingClientRect().top -
+ siteNav.getBoundingClientRect().top;
+ siteNav.scrollTop = scrollDistance;
+};
+
+// If the URL has a deep link, make sure the page scrolls to that section.
+// Context: https://pigweed.dev/docs/style_guide.html#site-nav-scrolling
+window.pigweed.scrollMainContent = () => {
+ // Only run this logic if there's a deep link in the URL.
+ if (!window.location.hash) {
+ return;
+ }
+ // Make sure that there's actually a node that matches the deep link before
+ // attempting to jump to it.
+ const targetNode = document.querySelector(window.location.hash);
+ if (!targetNode) {
+ return;
+ }
+ // Scroll to the node. Note that we also tried scrollIntoView() but
+ // it wasn't reliable enough.
+ const mainContent = document.querySelector('div.main');
+ const scrollDistance =
+ targetNode.getBoundingClientRect().top -
+ mainContent.getBoundingClientRect().top;
+ mainContent.scrollTop = scrollDistance;
+};
+
+window.addEventListener('DOMContentLoaded', () => {
+ // Manually control when Mermaid diagrams render to prevent scrolling issues.
+ // Context: https://pigweed.dev/docs/style_guide.html#site-nav-scrolling
+ if (window.mermaid) {
+ // https://mermaid.js.org/config/usage.html#using-mermaid-run
+ window.mermaid.run();
+ }
+ window.pigweed.scrollSiteNav();
+ window.pigweed.scrollMainContent();
+});
diff --git a/docs/blog/01-kudzu.rst b/docs/blog/01-kudzu.rst
new file mode 100644
index 000000000..5eb13391b
--- /dev/null
+++ b/docs/blog/01-kudzu.rst
@@ -0,0 +1,256 @@
+.. _docs-blog-01-kudzu:
+
+==========================
+Pigweed Eng Blog #1: Kudzu
+==========================
+.. admonition:: A note from the Pigweed Eng Blog editors
+
+ Welcome to the Pigweed Eng Blog! This is an informal blog where Pigweed
+ teammates, contributors, and users can share ideas and projects related to
+ Pigweed.
+
+ Our first post comes from Erik Gilling, a software engineer on the
+ Pigweed team. Today, Erik is going to tell you about Kudzu,
+ "Pigweed's whimsical take on a development board"…
+
+ Please note that **while Kudzu is open source, its hardware isn't publicly
+ available**. Pigweed users may find the `Kudzu source
+ code <https://pigweed.googlesource.com/pigweed/kudzu/+/refs/heads/main>`_
+ to be a helpful example of a complex Pigweed integration.
+
+.. card::
+ :img-background: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-diagonal.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-diagonal.jpg
+ :img-alt: A single Kudzu badge face-up on a table viewed at a diagonal.
+
+----------------------------
+It all started so innocently
+----------------------------
+The Pigweed team is taking a field trip to the
+`Bay Area Maker Faire <https://makerfaire.com/bay-area/>`_ because
+unsurprisingly, that's the kind of good time we're up for! While discussing
+the plans at a team meeting I suggested: "We should make PCB badges that run
+Pigweed and wear that to the Faire!" I've always wanted to make a PCB badge
+and this seemed like the perfect opportunity to make a simple PCB that we could
+do a little bit of hacking on.
+
+--------
+"Simple"
+--------
+The idea resonated with the team… perhaps too well. What started as
+something simple in my head quickly started creeping features. Hence
+the name `Kudzu <https://en.wikipedia.org/wiki/Kudzu>`_: a vine
+considered invasive in many parts of the world. Pigweed's a weed.
+Our RFCs are called "seeds". We're all about the plant puns…
+
+Anyways, the conversation went something like this:
+
+ "We should have some sort of sensor so it does something…"
+
+ "How should we power it? Let's do LiPo charging…"
+
+ "Let's add a display to highlight our recent
+ :ref:`Display Support SEED <seed-0104>`!"
+
+ "Touch screen?"
+
+ "D-Pad and buttons?"
+
+ "Speaker?"
+
+ "Wireless?"
+
+ "No!… but also yes…"
+
+We quickly realized that what we wanted was more than a badge. We wanted a
+showcase for Pigweed. We wanted a project we can point people at to show them
+Pigweed running at it's best. And thus Kudzu was born. Part badge, part
+development board, part handheld gaming system, and all Pigweed.
+
+----------------
+The laundry list
+----------------
+We settled on the following laundry list of features and components:
+
+`RP2040 Microcontroller <https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html>`_
+ There's a lot to love about the RP2040: a reasonable amount of SRAM,
+ support for large/cheap/execute-in-place external flash, a wicked cool
+ programmable I/O block, and most importantly: easy and cheap to source!
+
+`16 MB of Flash <https://www.winbond.com/resource-files/w25q128jv%20revf%2003272018%20plus.pdf>`_
+ We're adding the maximum amount of flash that the RP2040 can support. This
+ way we can pack as much awesome into the firmware as possible. Realistically
+ 16 MB is an embarrassingly large amount of space for an embedded project and I
+ can't wait to see what cool stuff we fill it with!
+
+USB-C Connector
+ While we're not adding USB Power Delivery to the board, the USB C connector
+ is robust and common. Many of us on the team are tired of digging micro
+ (or even mini) USB cables out of our desk drawers to hook up brand new dev
+ boards or JTAG programmers!
+
+LiPo Battery with `charger <https://www.microchip.com/en-us/product/mcp73831>`_ and `fuel gauge <https://www.analog.com/en/products/max17048.html>`_
+ Once we decided on a portable gaming form factor, we wanted to have a
+ built-in, rechargeable battery. The battery is 900mA and is set to charge at 500mA
+ from USB when the system is off and 250mA when the system is running. As an
+ added bonus we threw in a fuel gauge chip. Partly because it's really nice to
+ have an accurate view of the battery charge state and partly because it's
+ a neat chip to write software for.
+
+`3.2" IPS display with capacitive touch <https://www.buydisplay.com/3-2-inch-240x320-ips-tft-lcd-display-optl-capacitive-touchscreen-st7789>`_
+ This display is 240x320 which presents two challenges. First, it's naturally
+ portrait instead of landscape. We solve this by rotating the buffers once
+ they're rendered. The second is that a single 16-bit x 320 x 240 frame buffer
+ is ~150K which is over half of the 264K of SRAM in the RP2040. Instead, we're
+ rendering at 160x120 and using the PIO module to `pixel double
+ <https://github.com/32blit/32blit-sdk>`_ the buffer as we're sending it to the
+ display. As an added bonus, the chunkier pixels gives Kudzu a nice retro feel.
+
+Directional Pad and Buttons
+ Here we're leaning on off-the-shelf buttons and silicone pads. Game
+ controller design is a whole rabbit hole and we're going to rely on the
+ collective wisdom of the retro modding community to give us nice-feeling
+ controls.
+
+`Six Axis IMU <https://invensense.tdk.com/products/motion-tracking/6-axis/icm-42670-p/>`_
+ An IMU is a great general purpose peripheral to demonstrate Pigweed's HAL
+ layer. Plus, there's all sorts of cool demos you can write with an IMU and
+ a display.
+
+`I2S Audio DAC/Amplifier <https://www.analog.com/media/en/technical-documentation/data-sheets/max98357a-max98357b.pdf>`_ and Speaker
+ Chip tunes are best tunes. A couple of us on the team would love to
+ port/write a tracker or FM synthesis engine.
+
+Gameboy Advance Link Port
+ As a simple way of hooking two devices together, we added link port. Again
+ we're using an existing link port and cable to avoid reinventing to wheel.
+ Plus, there's something awful nostalgic about that port!
+
+... and `One More Thing <https://www.espressif.com/en/products/socs/esp32-c3>`_
+ I kinda snuck an ESP32-C3 module onto the board at the last minute. Having
+ wireless is something we wanted but didn't want to burden the initial design
+ and bring up with it. My thinking is that we'll leave the module un-populated
+ for now. My hope is that adding it to the board now may keep these boards from
+ becoming landfill when we decide to tackle wireless.
+
+--------------------------
+Design, build, and rollout
+--------------------------
+I used `KiCad <https://www.kicad.org/>`_ to design the board. It's an open
+source PCB design package that has been making incredible strides in
+functionality and usability in the past few years. It comes with a high-quality
+library of symbols and footprint which is supplemented by community-maintained
+open source libraries.
+
+.. card::
+ :img-top: https://storage.googleapis.com/pigweed-media/kudzu-schematic.png
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-schematic.png
+ :img-alt: A screenshot of Kudzu's schematic.
+ :text-align: center
+
+ Kudzu schematic
+
+After some careful design review from the team and a few friends of Pigweed we
+sent off the board to get fabbed and "patiently" waited for it to be delivered.
+
+An EE at a previous company I worked at had a saying: "If you haven't found
+three problems with your board, you're not done looking". The three problems
+we found in order from least to most crushing are:
+
+.. card::
+ :img-top: https://storage.googleapis.com/pigweed-media/kudzu-display-connector.jpeg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-display-connector.jpeg
+ :img-alt: The reworked display connector with many bodge wires.
+ :text-align: center
+
+ The reworked display connector and the unpopulated footprint for the "one more thing"
+ that "we'll get to eventually"
+
+#. **The BOOT and RESET labels were reversed.** This led to some initial
+ confusion on why the boards would not come up in bootloader mode.
+
+#. **One of the FETs (Q3) had the wrong pinout.** This caused the power
+ switch to be stuck on and the charge rate switching to not work.
+
+#. **The pins on the display FPC connector were swapped.** This one was really
+ crushing. The connector was fairly fine-pitched and 40 pins!
+
+We were able to bring up the whole board including the display by rotating the
+connector. Sadly the display would not fit in the 3D printed parts
+we'd designed when plugged into the rotated connection. To validate our 3D
+printed parts, I painstakingly reworked on-board to get the connector oriented
+correctly. However, that was too much work and too fragile for all the boards.
+We had to do a re-spin and Maker Faire was approaching quickly! Time to lather,
+rinse, and repeat.
+
+Fast forward to Monday night before Maker Faire. The boards come in and I spent
+the evening preparing for a build party. On Tuesday, with some
+`robotic help <https://www.opulo.io/>`_, we managed to build and test 8 boards
+and get them in team members' hands on Wednesday.
+
+.. card::
+ :img-top: https://storage.googleapis.com/pigweed-media/kudzu-pnp.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-pnp.jpg
+ :img-alt: A photo of the Opulo LumenPnP
+ :text-align: center
+
+ Our robotic help (Opulo LumenPnP)
+
+Thankfully, because Pigweed is modular and portable, we were able to get our
+software working on it quickly, freeing us to spend the next couple days hacking
+together some simple fun demos for Maker Faire!
+
+------
+Photos
+------
+.. grid:: 1 1 2 2
+
+ .. grid-item-card::
+ :img-background: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-diagonal.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-diagonal.jpg
+ :img-alt: A single Kudzu badge face-up on a table viewed at a diagonal.
+
+ .. grid-item-card::
+ :img-background: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-back.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-back.jpg
+ :img-alt: A single Kudzu badge face-down on a table viewed at a diagonal.
+
+.. grid:: 1 1 2 2
+
+ .. grid-item-card::
+ :img-background: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-top-down.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-top-down.jpg
+ :img-alt: A single Kudzu badge face-up on a table viewed from above.
+
+ .. grid-item-card::
+ :img-background: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-front-standing.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-front-standing.jpg
+ :img-alt: A single Kudzu badge standing up on a table viewed from the front.
+
+.. card::
+ :img-top: https://storage.googleapis.com/pigweed-media/kudzu-badges.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-badges.jpg
+ :img-alt: A photo of 6 of the Kudzu badges
+ :text-align: center
+
+ Six Kudzu badges for Maker Faire 2023
+
+----------
+Learn more
+----------
+We don't have any plans to distribute hardware outside of our team but are
+excited to publish the living project to serve as an example of how to build
+firmware integrated with Pigweed. Over the coming months we'll be publishing
+more functionality to the repository.
+
+Head over to the `Kudzu repo <https://pigweed.googlesource.com/pigweed/kudzu>`_
+where you'll find:
+
+* KiCad PCB Design
+* Example firmware demonstrating:
+ * :ref:`module-pw_system` integration
+ * :ref:`module-pw_rpc` and :ref:`module-pw_log` support
+ * Use of Pigweed's :ref:`module-pw_digital_io`, :ref:`module-pw_i2c`,
+ and :ref:`module-pw_spi` hardware abstraction layers
+
+.. pigweed-live::
diff --git a/docs/blog/index.rst b/docs/blog/index.rst
new file mode 100644
index 000000000..ce62af092
--- /dev/null
+++ b/docs/blog/index.rst
@@ -0,0 +1,12 @@
+.. _docs-blog:
+
+================
+Pigweed Eng Blog
+================
+The Pigweed Eng Blog is an informal blog where Pigweed teammates, contributors,
+and users can share ideas and projects related to Pigweed.
+
+.. toctree::
+ :maxdepth: 1
+
+ #1: Kudzu <01-kudzu>
diff --git a/docs/build_system.rst b/docs/build_system.rst
index a7a5c8e8e..981952832 100644
--- a/docs/build_system.rst
+++ b/docs/build_system.rst
@@ -163,6 +163,8 @@ and finally Bazel.
To avoid confusing the two, we refer to the former as "GN/Bazel targets" and the
latter as "Pigweed targets".
+.. _docs-build-system-gn:
+
GN
--
A perhaps unfamiliar name, `GN (Generate Ninja)`_ is a meta-build system that
@@ -200,15 +202,15 @@ Pigweed's build arguments, which apply across all Pigweed targets. For example,
a project could configure the protobuf libraries that it uses. This is done by
defining a ``default_args`` scope containing the overrides.
-.. code::
+.. code-block::
- # The location of the BUILDCONFIG file.
- buildconfig = "//BUILDCONFIG.gn"
+ # The location of the BUILDCONFIG file.
+ buildconfig = "//BUILDCONFIG.gn"
- # Build arguments set across all Pigweed targets.
- default_args = {
- dir_pw_third_party_nanopb = "//third_party/nanopb-0.4.2"
- }
+ # Build arguments set across all Pigweed targets.
+ default_args = {
+ dir_pw_third_party_nanopb = "//third_party/nanopb-0.4.2"
+ }
Configuration: BUILDCONFIG.gn
-----------------------------
@@ -271,22 +273,22 @@ therefore should not evaluate any other GN files. The pattern that Pigweed uses
to achieve this is to wrap all dependencies within a condition checking the
toolchain.
-.. code::
+.. code-block::
- group("my_application_images") {
- deps = [] # Empty in the default toolchain.
+ group("my_application_images") {
+ deps = [] # Empty in the default toolchain.
- if (current_toolchain != default_toolchain) {
- # This is only evaluated by Pigweed target toolchains, which configure
- # all of the required options to build Pigweed code.
- deps += [ "//images:evt" ]
- }
- }
+ if (current_toolchain != default_toolchain) {
+ # This is only evaluated by Pigweed target toolchains, which configure
+ # all of the required options to build Pigweed code.
+ deps += [ "//images:evt" ]
+ }
+ }
- # The images group is instantiated for each of the project's Pigweed targets.
- group("my_pigweed_target") {
- deps = [ ":my_application_images(//toolchains:my_pigweed_target)" ]
- }
+ # The images group is instantiated for each of the project's Pigweed targets.
+ group("my_pigweed_target") {
+ deps = [ ":my_application_images(//toolchains:my_pigweed_target)" ]
+ }
.. warning::
Pigweed's default toolchain is never used, so it is set to an empty toolchain
@@ -359,10 +361,10 @@ Next runtime sanitizers supported:
* ``ubsan_heuristic`` -- `UndefinedBehaviorSanitizer`_ with the following
additional checks enabled:
- * ``integer``: Checks for undefined or suspicious integer behavior.
- * ``float-divide-by-zero``: Checks for floating point division by zero.
- * ``implicit-conversion``: Checks for suspicious behavior of implicit conversions.
- * ``nullability``: Checks for null as function arg, lvalue and return type.
+ * ``integer``: Checks for undefined or suspicious integer behavior.
+ * ``float-divide-by-zero``: Checks for floating point division by zero.
+ * ``implicit-conversion``: Checks for suspicious behavior of implicit conversions.
+ * ``nullability``: Checks for null as function arg, lvalue and return type.
These additional checks are heuristic and may not correspond to undefined
behavior.
@@ -376,6 +378,10 @@ with ``pw_module_tests`` per supported sanitizer.
.. _UndefinedBehaviorSanitizer: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
.. _ThreadSanitizer: https://clang.llvm.org/docs/ThreadSanitizer.html
+coverage
+~~~~~~~~~~
+This group defines host-side build target for Clang source-based code coverage.
+
pw_modules
~~~~~~~~~~
This group lists the main libraries for all of Pigweed's modules.
@@ -427,13 +433,13 @@ Pigweed's modules, set relative to a project-specific ``dir_pigweed``.
To depend on Pigweed modules from GN code, import Pigweed's overrides file and
reference these module variables.
-.. code::
+.. code-block::
- # This must be imported before .gni files from any other Pigweed modules. To
- # prevent gn format from reordering this import, it must be separated by a
- # blank line from other imports.
+ # This must be imported before .gni files from any other Pigweed modules. To
+ # prevent gn format from reordering this import, it must be separated by a
+ # blank line from other imports.
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
GN target type wrappers
-----------------------
@@ -488,34 +494,34 @@ Building a custom executable/app image
1. Define your executable GN target using the ``pw_executable`` template.
- .. code::
+ .. code-block::
- # //foo/BUILD.gn
- pw_executable("foo") {
- sources = [ "main.cc" ]
- deps = [ ":libfoo" ]
- }
+ # //foo/BUILD.gn
+ pw_executable("foo") {
+ sources = [ "main.cc" ]
+ deps = [ ":libfoo" ]
+ }
2. In the root ``BUILD.gn`` file, add the executable's GN target to the ``apps``
group.
- .. code::
+ .. code-block::
- # //BUILD.gn
- group("apps") {
- deps = [
- # ...
- "//foo", # Shorthand for //foo:foo
- ]
- }
+ # //BUILD.gn
+ group("apps") {
+ deps = [
+ # ...
+ "//foo", # Shorthand for //foo:foo
+ ]
+ }
3. Run the ninja build to compile your executable. The apps group is built by
default, so there's no need to provide a target. The executable will be
compiled for every supported Pigweed target.
- .. code::
+ .. code-block::
- ninja -C out
+ ninja -C out
Alternatively, build your executable by itself by specifying its path to
Ninja. When building a GN target manually, the Pigweed target for which it
@@ -523,9 +529,9 @@ Building a custom executable/app image
For example, to build for the Pigweed target ``host_gcc_debug``:
- .. code::
+ .. code-block::
- ninja -C out host_gcc_debug/obj/foo/bin/foo
+ ninja -C out host_gcc_debug/obj/foo/bin/foo
.. note::
@@ -535,9 +541,9 @@ Building a custom executable/app image
4. Retrieve your compiled binary from the out directory. It is located at the
path
- .. code::
+ .. code-block::
- out/<pw_target>/obj/<gn_path>/{bin,test}/<executable>
+ out/<pw_target>/obj/<gn_path>/{bin,test}/<executable>
where ``pw_target`` is the Pigweed target for which the binary was built,
``gn_path`` is the GN path to the BUILD.gn file defining the executable,
@@ -548,27 +554,18 @@ Building a custom executable/app image
For example, the ``foo`` executable defined above and compiled for the
Pigweed target stm32f429i_disc1_debug is found at:
- .. code::
+ .. code-block::
- out/stm32f429i_disc1_debug/obj/foo/bin/foo
+ out/stm32f429i_disc1_debug/obj/foo/bin/foo
-CMake
------
+The CMake build
+===============
A well-known name in C/C++ development, `CMake`_ is widely used by all kinds of
projects, including embedded devices. Pigweed's CMake support is provided
primarily for projects that have an existing CMake build and wish to integrate
Pigweed modules.
-Bazel
------
-The open source version of Google's internal build system. `Bazel`_ has been
-growing in popularity within the open source world, as well as being adopted by
-various proprietary projects. Its modular structure makes it a great fit for
-à la carte usage.
-
-.. note::
- Bazel support is experimental and only for the brave for now. If you are
- looking for stable set of build API's please use GN.
+.. _docs-build-system-bazel:
The Bazel build
===============
@@ -586,7 +583,7 @@ While described in more detail in the Bazel docs there a few Bazel features that
are of particular importance when targeting embedded platforms. The most
commonly used commands used in bazel are;
-.. code:: sh
+.. code-block:: sh
bazel build //your:target
bazel test //your:target
@@ -596,17 +593,17 @@ commonly used commands used in bazel are;
Building
^^^^^^^^
-When it comes to building/testing your Bazel target for a specific Pigweed
-target (e.g. stm32f429i-discovery) a slight variation is required.
+When it comes to building/testing your build target for a specific target
+platform (e.g. stm32f429i-discovery) a slight variation is required.
-.. code:: sh
+.. code-block:: sh
bazel build //your:target \
- --platforms=@pigweed//pw_build/platforms:stm32f429i-disc1
+ --platforms=@pigweed//pw_build/platforms:lm3s6965evb
For more information on how to create your own platforms refer to the official
`Bazel platforms reference`_. You may also find helpful examples of constraints
-and platforms in the '//pw_build/platforms' and '//pw_build/constraints'
+and platforms in the ``//pw_build/platforms`` and ``//pw_build/constraints``
directories.
.. _Bazel platforms reference: https://docs.bazel.build/versions/main/platforms.html
@@ -615,8 +612,8 @@ Testing
^^^^^^^
Running tests on an embedded target with Bazel is possible although support for
this is experimental. The easiest way of achieving this at the moment is to use
-Bazel's '--run_under' flag. To make this work create a Bazel target
-('//your_handler') that;
+Bazel's ``--run_under`` flag. To make this work create a Bazel target
+(``//your_handler``) that:
1. Takes a single argument (the path to the elf) and uploads the elf to your
Pigweed target.
@@ -625,35 +622,53 @@ Bazel's '--run_under' flag. To make this work create a Bazel target
and returns (0, 1) respectively if one of the keywords is intercepted. (This
step assumes you are using the pw_unit_test package and it is configured for
your target).
-4. Run;
- .. code:: sh
+Then, run:
+
+ .. code-block:: sh
bazel test //your:test --platforms=//your/platform --run_under=//your_handler
+Test tag conventions
+~~~~~~~~~~~~~~~~~~~~
+Pigweed observes the standard Bazel test `tag conventions
+<https://bazel.build/reference/test-encyclopedia#tag-conventions>`_. We also
+use the following additional tags:
+
+* ``integration``: large, slow integration tests in upstream Pigweed are given
+ the ``integration`` tag. You can skip running these tests using
+ `--test_tag_filters
+ <https://bazel.build/docs/user-manual#test-tag-filters>`_. For example,
+
+ .. code-block:: sh
+
+ bazel test --test_tag_filters=-integration //...
+
+ will run all tests *except* for these integration tests.
+
Code Coverage
^^^^^^^^^^^^^
Making use of the code coverage functionality in Bazel is straightforward.
1. Add the following lines to your '.bazelrc'.
- .. code:: sh
+ .. code-block:: sh
- coverage --experimental_generate_llvm_lcov
- coverage --combined_report=lcov
+ coverage --experimental_generate_llvm_lcov
+ coverage --combined_report=lcov
2. Generate a combined lcov coverage report. This will produce a combined lcov
coverage report at the path 'bazel-out/_coverage/_coverage_report.dat'. e.g.
- .. code:: sh
+ .. code-block:: sh
- bazel coverage //pw_log/...
+ bazel coverage //pw_log/...
3. View the results using the command line utility 'lcov'.
- .. code:: sh
+ .. code-block:: sh
- lcov --list bazel-out/_coverage/_coverage_report.dat
+ lcov --list bazel-out/_coverage/_coverage_report.dat
Configuration
-------------
@@ -670,7 +685,7 @@ Selects are useful for specifying different dependencies/source depending on the
platform that is currently being targeted. For more information on this please
see the `Bazel selects reference`_. e.g.
-.. code:: py
+.. code-block:: py
pw_cc_library(
name = "some_platform_dependant_library",
@@ -686,7 +701,7 @@ Compatibility lists allow you to specify which platforms your targets are
compatible with. Consider an example where you want to specify that a target is
compatible with only a host os;
-.. code:: py
+.. code-block:: py
pw_cc_library(
name = "some_host_only_lib",
@@ -705,15 +720,17 @@ other OS's will fail if this target is explicitly depended on. However if
building with a wild card for a non-host platform this target will be skipped
and the build will continue. e.g.
-.. code:: sh
+.. code-block:: sh
- bazel build //... --platforms=@pigweed//pw_build/platforms:cortex_m0
+ bazel build //... --platforms=@pigweed//pw_build/platforms:lm3s6965evb
This allows for you to easily create compatibility matricies without adversely
affecting your ability build your entire repo for a given Pigweed target.
For more detailed information on how to use the target_compatible_with attribute
please see `Bazel target_compatible_with reference`_.
+.. _docs-build_system-bazel_flags:
+
Flags/build settings
^^^^^^^^^^^^^^^^^^^^
Flags/build settings are particularly useful in scenarios where you may want to
@@ -729,7 +746,7 @@ select statement.
A simple example of when it is useful to use a label_flag is when you want to
swap out a single dependency from the command line. e.g.
-.. code:: py
+.. code-block:: py
pw_cc_library(
name = "some_default_io",
@@ -754,7 +771,7 @@ swap out a single dependency from the command line. e.g.
From here the label_flag by default redirects to the target ":some_default_io",
however it is possible to override this from the command line. e.g.
-.. code:: sh
+.. code-block:: sh
bazel build //:some_target_that_needs_io --//:io=//:some_other_io
@@ -768,312 +785,286 @@ however it is possible to override this from the command line. e.g.
.. _docs-build_system-bazel_configuration:
-Pigweed's Bazel configuration
+Facades and backends tutorial
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Pigweeds Bazel configuration API is designed to be distributed across the
-Pigweed repository and/or your downstream repository. If you are coming from
-GN's centralized configuration API it might be useful to think about
-Pigweed+Bazel's configuration as the transpose of GN's configuration. The
-configuration interface that is supported by Pigweed is designed to start simple
-and then grow with your project.
+This section walks you through an example of configuring :ref:`facade
+<module-pw_build-bazel-pw_cc_facade>` backends in a Pigweed Bazel project.
-.. note::
- There are plans to extend the configuration API for Bazel. However,
- currently the only configurations that are available under the Bazel+Pigweed
- configuration API is the ability to switch facade backends. For more
- information on what this is please see the
- :ref:`docs-module-structure-facades` section of :ref:`docs-module-structure`.
+Consider a scenario that you are building a flight controller for a spacecraft.
+But you have very little experience with Pigweed and have just landed here.
+First things first, you would set up your WORKSPACE to fetch Pigweed
+repository. Then, add the dependencies that you need from Pigweed's WORKSPACE.
-Consider a scenario that you are building a flight controller for a
-spacecraft. But have very little experience with Pigweed and you have just
-landed here. First things first you would;
+Maybe you want to try using the :ref:`pw_chrono <module-pw_chrono>` module. So
+you create a target in your repository like so:
-1. Set up your WORKSPACE to fetch the Pigweeds repository. Then add the
- dependencies that you need from Pigweeds WORKSPACE.
+.. code-block:: python
-2. Add a pigweed_config rule to your WORKSPACE, using Pigweed's default
- configuration.
+ # BUILD.bazel
+ pw_cc_library(
+ name = "time_is_relative",
+ srcs = ["relative_time_on_earth.cc"],
+ deps = ["@pigweed//pw_chrono:system_clock"],
+ )
- .. code:: py
+This should work out of the box for any host operating system. E.g., running,
- # WORKSPACE ...
- load("//pw_build:target_config.bzl", "pigweed_config")
+.. code-block:: console
- # Configure Pigweeds backend.
- pigweed_config(
- name = "pigweed_config",
- build_file = "@pigweed//targets:default_config.BUILD",
- )
+ bazel build //:time_is_relative
-.. note::
- We are aware, that the experience of setting up your WORKSPACE file to work
- with pigweed is less than ideal. This API is under construction, but we are
- working on this!
+will produce a working library for your host OS.
-..
- TODO: Add in a better way to pull down WORKSPACE dependencies in Bazel then
- add docs in here.
+Using Pigweed-provided backends
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+But you're probably here because Pigweed offers a set of embedded libraries,
+and might be interested in running your code on some micro-controller/FPGA
+combined with an RTOS. For now let's assume you are using FreeRTOS and are
+happy to make use of our default ``//pw_chrono`` backend for FreeRTOS. You
+could build your library with:
-Now to explain what is going on here. Housed under the "pigweed_config" remote
-repository is a set of configuration flags. These can be used to inject
-dependencies into facades to override the default backend.
+.. code-block:: console
-Continuing on with our scenario, consider that you maybe want to try using the
-'//pw_chrono' module. So you create a target in your repository like so;
+ bazel build \
+ --@pigweed//targets:pw_chrono_system_clock_backend=@pigweed//pw_chrono_freertos:system_clock_backend \
+ //:time_is_relative
-.. code::
+Then, ``//pw_chrono:system_clock`` will use the FreeRTOS backend
+``//pw_chrono_freertos:system_clock_backend``.
- # BUILD
- pw_cc_library(
- name = "time_is_relative",
- srcs = ["relative_time_on_earth.cc"],
- deps = ["@pigweed//pw_chrono"],
- )
+How does this work? Here's the relevant part of the dependency tree for your
+target:
-Now this should work out of the box for any host operating system. e.g. Running;
+.. code-block::
-.. code::
+ //:time_is_relative
+ |
+ v
+ @pigweed//pw_chrono:system_clock -------> @pigweed//targets:pw_chrono_system_clock_backend
+ | (Injectable)
+ | |
+ | v
+ | @pigweed//pw_chrono_freertos:system_clock
+ | (Actual backend)
+ v |
+ @pigweed//pw_chrono:system_clock_facade <------------------.
- bazel build //:time_is_relative
+When building ``//:time_is_relative``, Bazel checks the dependencies of
+``@pigweed//pw_chrono:system_clock`` and finds that it depends on
+``@pigweed//targets:pw_chrono_system_clock_backend``, which looks like this:
-will produce a working library. But as your probably here because Pigweed offers
-a set of embedded libraries you might be interested in running your code on some
-random micro-controller/FPGA combined with an RTOS. For now let's assume that by
-some coincidence you are using FreeRTOS and are happy to make use
-of our default '//pw_chrono' backend for FreeRTOS. You could build the following
-with;
+.. code-block:: python
-.. code:: sh
+ # @pigweed//targets/BUILD.bazel
- bazel build //:time_is_relative \
- --platforms=@pigweed//pw_build/platforms:freertos
+ label_flag(
+ name = "pw_chrono_system_clock_backend",
+ build_setting_default = "@pigweed//pw_chrono:system_clock_backend_multiplexer",
+ )
-There is a fair bit to unpack here in terms of how our configuration system
-is determining which dependencies to choose for your build. The dependency
-tree (that is important for configuration) in a project such as this would
-look like.
+This is a `label_flag
+<https://bazel.build/extending/config#label-typed-build-settings>`_: a
+dependency edge in the build graph that can be overridden by command line
+flags. By setting
-.. code::
+.. code-block:: console
- @pigweed//pw_chrono:pw_chrono_facade <-----------.
- ^ |
- | @pigweed//pw_chrono_freertos:system_clock
- | (Actual backend)
- | ^
- | |
- | @pigweed//pw_chrono:system_clock_backend_multiplexer
- | Select backend based on OS:
- | [FreeRTOS (X), Embos ( ), STL ( ), Threadx ( )]
- | ^
- | |
- @pigweed//pw_chrono -------> @pigweed_config//:pw_chrono_system_clock_backend
- ^ (Injectable)
- |
- //:time_is_relative
+ --@pigweed//targets:pw_chrono_system_clock_backend=\
+ @pigweed//pw_chrono_freertos:system_clock_backend
-So when evaluating this setup Bazel checks the dependencies for '//pw_chrono'
-and finds that it depends on "@pigweed_config//:pw_chrono_system_clock_backend" which looks
-like this;
+on the command line, you told Bazel to override the default and use the
+FreeRTOS backend.
-.. code:: py
+Defining a custom backend
+~~~~~~~~~~~~~~~~~~~~~~~~~
+Continuing with our scenario, let's say that you have read
+:ref:`docs-module-structure` and now want to implement your own backend for
+``//pw_chrono:system_clock`` using a hardware RTC. In this case you would
+create a new directory ``pw_chrono_my_hardware_rtc``, containing some header
+files and a BUILD file like,
+
+.. code-block:: python
+
+ # //pw_chrono_my_hardware_rtc/BUILD.bazel
+
+ pw_cc_library(
+ name = "system_clock",
+ hdrs = [
+ "public/pw_chrono_stl/system_clock_config.h",
+ "public/pw_chrono_stl/system_clock_inline.h",
+ "public_overrides/pw_chrono_backend/system_clock_config.h",
+ "public_overrides/pw_chrono_backend/system_clock_inline.h",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ deps = [
+ "//pw_chrono:system_clock_facade",
+ ],
+ )
- # pw_chrono config.
- label_flag(
- name = "pw_chrono_system_clock_backend",
- build_setting_default = "@pigweed//pw_chrono:system_clock_backend_multiplexer",
- )
+To build your ``//:time_is_relative`` target using this backend, you'd run,
-Looking at the 'build_setting_default' we can see that by default it depends
-back on the target "@pigweed//pw_chrono:system_clock_backend_multiplexer". If
-you only had one backend you could actually just change the
-'build_setting_default' to point directly to your backend. However because we
-have four different backends we have to use the select semantics to choose the
-right one. In this case it looks like;
+.. code-block:: console
-.. code:: py
+ bazel build //:time_is_relative \
+ --@pigweed//targets:pw_chrono_system_clock_backend=//pw_chrono_my_hardware_rtc:system_clock
- pw_cc_library(
- name = "system_clock_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
- deps = select({
- "@pigweed//pw_build/constraints/rtos:freertos":
- ["//pw_chrono_freertos:system_clock"],
- "@pigweed//pw_build/constraints/rtos:embos":
- ["//pw_chrono_embos:system_clock"],
- "@pigweed//pw_build/constraints/rtos:threadx":
- ["//pw_chrono_threadx:system_clock"],
- "//conditions:default": ["//pw_chrono_stl:system_clock"],
- }),
- )
+This modifies the build graph to look something like this:
-Intuitively you can see that the first option was selected, which terminates
-the configuration chain.
+.. code-block::
-Continuing on with our scenario let's say that you have read
-:ref:`docs-module-structure` and now want to implement your own backend for
-'//pw_chrono' using a hardware RTC. In this case you would create a new
-directory 'pw_chrono_my_hardware_rtc'. To ensure that your new backend compiles
-with the facade an easy and temporary way to override the dependency tree is
-to override the label flag in '@pigweed_config'. For example;
-
-.. code:: sh
-
- bazel build //:time_is_relative \
- --@pigweed_config//pw_chrono_system_clock_backend=//pw_chrono_my_hardware_rtc:system_clock
-
-This temporarily modifies the build graph to look something like this;
-
-.. code::
-
- @pigweed//pw_chrono:pw_chrono_facade <-----.
- ^ |
- | @your_workspace//pw_chrono_my_hardware_rtc:system_clock
- | (Actual backend)
- | ^
- | |
- @pigweed//pw_chrono -> @pigweed_config//:pw_chrono_system_clock_backend
- ^ (Injectable)
- |
- //:time_is_relative
-
-Now while this is a nice temporary change, but you might find yourself in need
-of a more permanent configuration. Particularly if you want to override multiple
-different backends. In other words if you had several backends to override, that
-would translate to several different command line flags (one for each override).
-This problem further compounds as you have multiple Pigweed targets all
-requiring different combinations of different backends as you can't even reuse
-your command line entries. Instead you would have to memorize the correct
-combination of backends for each of your targets.
-
-So continuing on with our scenario, let's say we add a backup micro-controller,
+ //:time_is_relative
+ |
+ v
+ @pigweed//pw_chrono:system_clock -------> @pigweed//targets:pw_chrono_system_clock_backend
+ | (Injectable)
+ | |
+ | v
+ | //pw_chrono_my_hardware_rtc:system_clock
+ | (Actual backend)
+ v |
+ @pigweed//pw_chrono:system_clock_facade <------------------.
+
+Associating backends with platforms through bazelrc
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+As your project grows, you will want to select backends for an increasing
+number of facades. The right backend to choose will depend on the target
+platform (host vs embedded, with potentially multiple target embedded
+platforms). Managing this through command-line flags would be pretty arduous!
+
+What we recommend you do instead is group these flags into configs in your
+`bazelrc <https://bazel.build/run/bazelrc>`_. Eventually, your bazelrc may look
+something like this:
+
+.. code-block:: sh
+
+ # The Cortex M7 config
+ build:m7 --@pigweed//tagrets:pw_chrono_system_clock_backend=//pw_chrono_my_hardware_rtc:system_clock
+ build:m7 --@pigweed//targets:pw_sys_io_backend=//cortex-m7:sys_io
+
+ # The Cortex M4 config
+ build:m4 --@pigweed//tagrets:pw_chrono_system_clock_backend=//pw_chrono_my_hardware_rtc:system_clock
+ build:m4 --@pigweed//targets:pw_sys_io_backend=//cortex-m4:sys_io
+ build:m4 --@pigweed//targets:pw_log_backend=@pigweed//pw_log_string
+ build:m4 --@pigweed//targets:pw_log_string_handler_backend=@pigweed//pw_system:log_backend
+
+Then, to build your library for a particular configuration, on the command line
+you just specify the ``--config`` on the command line:
+
+.. code-block:: console
+
+ bazel build --config=m4 //:time_is_relative
+
+Multiplexer-based backend selection
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+TODO(`b/272090220 <https://issues.pigweed.dev/issues/272090220>`_): Not all
+facades and backends expose this interface yet.
+
+As an alternative to directly switching backends using label flags, Pigweed
+supports backend selection based on the target `platform
+<https://bazel.build/extending/platforms>`_. That is, on the command line you
+build with,
+
+.. code-block:: console
+
+ bazel build --platforms-//platforms:primary_computer //:time_is_relative
+
+and backend selection is done by Bazel based on the platform definition. Let's
+discuss how to set this up.
+
+Continuing with our scenario, let's say we add a backup microcontroller
to our spacecraft. But this backup computer doesn't have a hardware RTC. We
still want to share the bulk of the code between the two computers but now we
need two separate implementations for our pw_chrono facade. Let's say we choose
-to keep the primary flight computer using the hardware RTC and switch the backup
-computer over to use Pigweeds default FreeRTOS backend. In this case we might,
-want to do something similar to
-'@pigweed//pw_chrono:system_clock_backend_multiplexer' and create selectable
-dependencies for the two different computers. Now because there are no default
-constraint_setting's that meet our requirements we are going to have to;
-
-1. Create a constraint_setting and a set of constraint_value's for the flight
- computer. For example;
-
- .. code:: py
-
- # //platforms/flight_computer/BUILD
- constraint_setting(
- name = "flight_computer",
- )
-
- constraint_value(
- name = "primary",
- constraint_setting = ":flight_computer",
- )
-
- constraint_value(
- name = "backup",
- constraint_setting = ":flight_computer",
- )
-
-2. Create a set of platforms that can be used to switch constraint_value's.
- For example;
-
- .. code:: py
-
- # //platforms/BUILD
- platform(
- name = "primary_computer",
- constraint_values = ["//platforms/flight_computer:primary"],
- )
-
- platform(
- name = "backup_computer",
- constraint_values = ["//platforms/flight_computer:backup"],
- )
-
-3. Create a target multiplexer that will select the right backend depending on
- which computer you are using. For example;
-
- .. code:: py
-
- # //pw_chrono/BUILD
- load("//pw_build:pigweed.bzl", "pw_cc_library")
-
- pw_cc_library(
- name = "system_clock_backend_multiplexer",
- deps = select({
- "//platforms/flight_computer:primary": [
- "//pw_chrono_my_hardware_rtc:system_clock",
- ],
- "//platforms/flight_computer:backup": [
- "@pigweed//pw_chrono_freertos:system_clock",
- ],
- "//conditions:default": [
- "@pigweed//pw_chrono_stl:system_clock",
- ],
- }),
- )
-
-4. Copy and paste across the target/default_config.BUILD across from the
- Pigweed repository and modifying the build_setting_default for the target
- 'pw_chrono_system_clock_backend' to point to your new system_clock_backend_multiplexer
- target. For example;
-
- This;
-
- .. code:: py
-
- # @pigweed//target:default_config.BUILD
- label_flag(
- name = "pw_chrono_system_clock_backend",
- build_setting_default = "@pigweed//pw_chrono:system_clock_backend_multiplexer",
- )
-
- Becomes this;
-
- .. code:: py
-
- # @your_workspace//target:your_config.BUILD
- label_flag(
- name = "pw_chrono_system_clock_backend",
- build_setting_default =
- "@your_workspace//pw_chrono:system_clock_backend_multiplexer",
- )
-
-5. Switch your workspace 'pigweed_config' rule over to use your custom config.
-
- .. code:: py
-
- # WORKSPACE
- pigweed_config(
- name = "pigweed_config",
- build_file = "//target/your_config.BUILD",
- )
+to keep the primary flight computer using the hardware RTC and switch the
+backup computer over to use Pigweed's default FreeRTOS backend:
+
+#. Create a constraint value corresponding to your custom backend:
+
+ .. code-block:: python
+
+ # //pw_chrono_my_hardware_rtc/BUILD.bazel
+ constraint_value(
+ name = "system_clock_backend",
+ constraint_setting = "//pw_chrono:system_clock_constraint_setting",
+ )
+
+#. Create a set of platforms that can be used to switch constraint values.
+ For example:
+
+ .. code-block:: python
+
+ # //platforms/BUILD.bazel
+ platform(
+ name = "primary_computer",
+ constraint_values = ["//pw_chrono_my_hardware_rtc:system_clock_backend"],
+ )
+
+ platform(
+ name = "backup_computer",
+ constraint_values = ["@pigweed//pw_chrono_freertos:system_clock_backend"],
+ )
+
+ If you already have platform definitions for the primary and backup
+ computers, just add these constraint values to them.
+
+#. Create a target multiplexer that will select the right backend depending on
+ which computer you are using. For example:
+
+ .. code-block:: python
+
+ # //targets/BUILD.bazel
+ load("//pw_build:pigweed.bzl", "pw_cc_library")
+
+ pw_cc_library(
+ name = "system_clock_backend_multiplexer",
+ deps = select({
+ "//pw_chrono_my_hardware_rtc:system_clock_backend": [
+ "//pw_chrono_my_hardware_rtc:system_clock",
+ ],
+ "@pigweed//pw_chrono_freertos:system_clock_backend": [
+ "@pigweed//pw_chrono_freertos:system_clock",
+ ],
+ "//conditions:default": [
+ "@pigweed//pw_chrono_stl:system_clock",
+ ],
+ }),
+ )
+
+#. Add a build setting override for the ``pw_chrono_system_clock_backend`` label
+ flag to your ``.bazelrc`` file that points to your new target multiplexer.
+
+ .. code-block:: bash
+
+ # //.bazelrc
+ build --@pigweed//targets:pw_chrono_system_clock_backend=//targets:system_clock_backend_multiplexer
Building your target now will result in slightly different build graph. For
example, running;
-.. code:: sh
+.. code-block:: sh
bazel build //:time_is_relative --platforms=//platforms:primary_computer
Will result in a build graph that looks like;
-.. code::
-
- @pigweed//pw_chrono:pw_chrono_facade <---.
- ^ |
- | @your_workspace//pw_chrono_my_hardware_rtc:system_clock
- | (Actual backend)
- | ^
- | |
- | @your_workspace//pw_chrono:system_clock_backend_multiplexer
- | Select backend based on OS:
- | [Primary (X), Backup ( ), Host only default ( )]
- | ^
- | |
- @pigweed//pw_chrono -> @pigweed_config//:pw_chrono_system_clock_backend
- ^ (Injectable)
- |
- //:time_is_relative
+.. code-block::
+
+ //:time_is_relative
+ |
+ @pigweed//pw_chrono -> @pigweed//targets:pw_chrono_system_clock_backend
+ | (Injectable)
+ | |
+ | v
+ | //targets:system_clock_backend_multiplexer
+ | Select backend based on OS:
+ | [Primary (X), Backup ( ), Host only default ( )]
+ | |
+ | v
+ | //pw_chrono_my_hardware_rtc:system_clock
+ | (Actual backend)
+ v |
+ @pigweed//pw_chrono:pw_chrono_facade <---.
+
diff --git a/docs/changelog.rst b/docs/changelog.rst
new file mode 100644
index 000000000..26465d7b7
--- /dev/null
+++ b/docs/changelog.rst
@@ -0,0 +1,2741 @@
+:tocdepth: 2
+
+.. _docs-changelog:
+
+=====================
+What's New In Pigweed
+=====================
+
+----------------------------------------
+Discuss what's new with the Pigweed team
+----------------------------------------
+.. pigweed-live::
+
+.. _docs-changelog-latest:
+
+-----------
+Nov 3, 2023
+-----------
+.. changelog_highlights_start
+
+Highlights (Oct 19, 2023 to Nov 3, 2023):
+
+* A lot more of the :cpp:class:`pw::multibuf::Chunk` API was implemented.
+* :ref:`module-pw_format` is a new module dedicated to Rust format string parsing.
+* The tokenizer prefix is now configurable via
+ ``PW_TOKENIZER_NESTED_PREFIX_STR``.
+* References to C++14 have been removed throughout the codebase. Pigweed no
+ longer supports C++14; C++17 or newer is required.
+* The upstream Pigweed GN build is now
+ :ref:`more isolated <docs-changelog-20231103-pw_build>` so that downstream
+ projects have less conflicts when importing Pigweed into their existing GN
+ build.
+* Build configuration is moving away from Bazel macros like ``pw_cc_library``
+ and towards the toolchain configuration so that downstream projects can have
+ :ref:`full control <docs-changelog-20231103-bazel>` over how Pigweed libraries
+ are built.
+* New guidelines for authoring module docs have been published at
+ :ref:`docs-contrib-moduledocs`. :ref:`module-pw_string` is now an example
+ of a "golden" module docs set that follows the new guidelines. Please leave
+ feedback on the new guidelines (and module docs updated to follow the
+ guidelines) in `issue #309123039 <https://issues.pigweed.dev/issues/309123039>`__.
+
+.. changelog_highlights_end
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0110: Memory Allocation Interfaces <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168772>`__
+* `SEED-0113: Modular Bazel C/C++ Toolchain API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173453>`__
+* `SEED-0114: Channels <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175471>`__
+* `SEED-0115: Sensors <http://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176760>`__
+* `SEED-0116: Sockets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177696>`__
+
+Modules
+=======
+
+pw_allocator
+------------
+The docs now have an auto-generated :ref:`module-pw_allocator-size`.
+``pw::allocator::SplitFreeListAllocator`` has a new ``blocks()`` method for getting the
+range of blocks being tracked. The class was also refactored to
+use the existing ``Block`` API. The ``Block`` API itself was refactored to
+encode offsets and flags into fields.
+
+* `Add size reporting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178370>`__
+* `Return Range from SplitFreeListAllocator <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177807>`__
+* `Refactor SplitFreeListAllocator to use Block <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176579>`__
+* `Refactor Block to use encoded offsets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176578>`__
+
+pw_arduino_build
+----------------
+* `STM32 Core fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177750>`__
+
+pw_assert
+---------
+* `Update print_and_abort backend formatting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177845>`__
+
+pw_bluetooth
+------------
+More :ref:`Emboss <module-pw_third_party_emboss>` definitions were added.
+
+.. todo-check: disable
+
+* `Add TODO for issue 308794058 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/151070>`__
+ (issue `#308794058 <https://issues.pigweed.dev/issues/308794058>`__)
+* `Remove anonymous entry in LEPeerAddressTypeNoAnon <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177881>`__
+* `Separate LEAddressType and LEExtendedAddressType <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178010>`__
+* `Define LEExtendedCreateConnectionV1 Emboss structure <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176574>`__
+ (issue `#305976440 <https://issues.pigweed.dev/issues/305976440>`__)
+* `Define LEEnhancedConnectionCompleteSubeventV1 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176576>`__
+ (issue `#305976440 <https://issues.pigweed.dev/issues/305976440>`__)
+* `Remove padding from Emboss command definitions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176772>`__
+
+.. todo-check: enable
+
+.. _docs-changelog-20231103-pw_build:
+
+pw_build
+--------
+Pigweed used to inject a selection of recommended configs into every ``pw_*``
+C/C++ target in the GN build. These were previously only possible to remove
+with the ``remove_configs`` argument. These configs are now bundled with
+toolchains instead, and if you don't use a Pigweed-style toolchain you'll
+no longer need to find ways to strip the default configs from Pigweed build rules.
+More importantly, this changes makes Pigweed's recommended configs behave
+identically to other toolchain configs, and they're now more clearly part of
+GN toolchain definitions. This change is transparent to most projects, but some
+Pigweed customers have been asking for this for a while.
+
+The :ref:`module-pw_build-bazel-empty_cc_library` Bazel utility was added.
+
+* `Add empty_cc_library <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178555>`__
+* `Remove pw_build_default_configs_in_toolchain <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177894>`__
+* `Apply pigweed_default_configs in toolchain <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/120610>`__
+ (issue `#260111641 <https://issues.pigweed.dev/issues/260111641>`__)
+* `Fix blob attribute ordering <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177458>`__
+* `Only use -Wextra-semi on C++ files with GCC <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177171>`__
+ (issue `#301262374 <https://issues.pigweed.dev/issues/306734552, b/301262374>`__)
+* `Silence Windows-specific warnings <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177172>`__
+
+pw_bytes
+--------
+A new ``_b`` literal was added to make it easier to create bytes for tests
+and constants.
+
+* `Add _b suffix for byte literals <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178134>`__
+
+pw_containers
+-------------
+The reference docs for the variable length entry queue API in C and Python
+were updated.
+
+* `Update VariableLengthEntryQueue size functions; cleanup <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173454>`__
+
+pw_digital_io_mcuxpresso
+------------------------
+* `Remove RT595 size def <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178353>`__
+
+pw_doctor
+---------
+* `Trivial linter fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176939>`__
+
+pw_emu
+------
+* `renode: Show more details when failing to connect <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178563>`__
+ (issue `#307736513 <https://issues.pigweed.dev/issues/307736513>`__)
+
+pw_env_setup
+------------
+``pip`` has been pinned to ``23.2.1`` and ``pip-tools`` to ``7.3.0`` to
+prevent dependency resolution problems.
+
+* `Pin pip and pip-tools <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177834>`__
+* `Update protoc to 2@24.4 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177050>`__
+ (issue `#306461552 <https://issues.pigweed.dev/issues/306461552>`__)
+
+pw_format
+---------
+:ref:`module-pw_format` is a new module dedicated to Rust format string parsing.
+
+* `Correct crate name in docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178078>`__
+* `Move Rust format string parsing into its own module <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168362>`__
+
+pw_fuzzer
+---------
+* `Inline NonOkStatus() <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178212>`__
+* `Fix instrumentation config <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178214>`__
+
+.. _docs-changelog-20231103-pw_hdlc:
+
+pw_hdlc
+-------
+Using read callbacks in ``RpcClient`` is no longer accepted and the use of
+``CancellableReader`` is now enforced because it provides a safe and clean
+shutdown process.
+
+* `Enforce use of CancellableReader <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173618>`__
+ (issue `#301496598 <https://issues.pigweed.dev/issues/301496598>`__)
+
+pw_libcxx
+---------
+:ref:`module-pw_libcxx` is a new module that provides ``libcxx`` symbols and
+will eventually facilitate pulling in headers as well.
+
+* `Add pw_libcxx library <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/144970>`__
+
+pw_log
+------
+A :ref:`module-pw_log-bazel-backend_impl` label flag was added to Bazel to
+avoid circular dependencies.
+
+* `Enable sandboxing for pigweed genrules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178550>`__
+ (issue `#307824623 <https://issues.pigweed.dev/issues/307824623>`__)
+* `Introduce backend_impl label flag <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177842>`__
+ (issue `#234877642 <https://issues.pigweed.dev/issues/234877642>`__)
+
+pw_multibuf
+-----------
+A lot more of the :cpp:class:`pw::multibuf::Chunk` API was implemented.
+
+* `Add basic MultiBuf operations <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178036>`__
+* `Add Chunk::Merge <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177636>`__
+* `Fix TrackingAllocatorWithMemory UAF <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177694>`__
+* `Add module and Chunk implementation <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173951>`__
+
+pw_package
+----------
+* `Use mirror for stm32cube <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/142510>`__
+ (issue `#278914999 <https://issues.pigweed.dev/issues/278914999>`__)
+* `Fix Zephyr URL <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177456>`__
+
+pw_presubmit
+------------
+A CSS formatter was added.
+
+* `Add basic CSS formatter <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178810>`__
+ (issue `#308948504 <https://issues.pigweed.dev/issues/308948504>`__)
+* `Kalypsi-based coverage upload <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175070>`__
+ (issue `#279161371 <https://issues.pigweed.dev/issues/279161371>`__)
+* `Handle missing upstream better <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177038>`__
+ (issue `#282808936 <https://issues.pigweed.dev/issues/282808936>`__)
+* `Trivial linter fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176939>`__
+
+pw_protobuf
+-----------
+* `Enable sandboxing for pigweed genrules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178550>`__
+ (issue `#307824623 <https://issues.pigweed.dev/issues/307824623>`__)
+
+pw_rpc
+------
+:ref:`pw::rpc::SynchronousCallFor() <module-pw_rpc-client-sync-call-wrappers>`
+now supports :ref:`DynamicClient <module-pw_rpc_pw_protobuf-client>`.
+
+* `Update Java service error with tip <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178812>`__
+ (issue `#293361955 <https://issues.pigweed.dev/issues/293361955>`__)
+* `Support DynamicClient with SynchronousCallFor API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177637>`__
+
+pw_string
+---------
+The docs were updated to match the new :ref:`docs-contrib-moduledocs`.
+
+* `Docs tweaks <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177883>`__
+
+pw_sys_io
+---------
+Backends that depend on ``default_putget_bytes`` were updated to express the
+dependency.
+
+* `Fix Bazel backends <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177656>`__
+
+pw_system
+---------
+See :ref:`docs-changelog-20231103-pw_hdlc` for an explanation of the
+``CancellableReader`` change.
+
+* `Enforce use of CancellableReader <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173618>`__
+ (issue `#301496598 <https://issues.pigweed.dev/issues/301496598>`__)
+
+pw_tls_client
+-------------
+* `Update to new boringssl API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178150>`__
+
+pw_tokenizer
+------------
+The tokenizer prefix is now configurable via ``PW_TOKENIZER_NESTED_PREFIX_STR``.
+
+* `Enable sandboxing for pigweed genrules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178550>`__
+ (issue `#307824623 <https://issues.pigweed.dev/issues/307824623>`__)
+* `Let tokenizer prefix be configurable <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177575>`__
+
+pw_toolchain
+------------
+You can now set the ``dir_pw_third_party_builtins`` GN var to your
+``compiler-rt/builtins`` checkout to enable buildings LLVM ``builtins`` from
+source instead of relying on a shipped ``libgcc``.
+
+* `Apply pigweed_default_configs in toolchain <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/120610>`__
+ (issue `#260111641 <https://issues.pigweed.dev/issues/260111641>`__)
+* `Build compiler-rt builtins to replace libgcc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/144050>`__
+
+pw_unit_test
+------------
+* `Pass verbose flag to TestRunner <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177470>`__
+
+pw_web
+------
+* `Limit component rerendering <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177810>`__
+ (issue `#307559191 <https://issues.pigweed.dev/issues/307559191>`__)
+
+Build
+=====
+References to C++14 have been removed throughout the codebase. Pigweed no
+longer supports C++14; C++17 or newer is required.
+
+* `Drop C++14 compatibility from the build and docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177610>`__
+
+.. _docs-changelog-20231103-bazel:
+
+Bazel
+-----
+Build configuration is moving away from Bazel macros like ``pw_cc_library``
+and towards the toolchain configuration so that downstream projects can have
+full control over how Pigweed libraries are built.
+
+* `Move Kythe copts to toolchain configuration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178592>`__
+ (issue `#267498492 <https://issues.pigweed.dev/issues/267498492>`__)
+* `Move warnings to toolchain configuration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178557>`__
+ (issue `#240466562 <https://issues.pigweed.dev/issues/240466562>`__)
+* `Silence warnings from external code <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178436>`__
+ (issue `#300330623 <https://issues.pigweed.dev/issues/300330623>`__)
+* `stm32cube support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177134>`__
+* `Remove most copts from pw_cc_library macro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170824>`__
+ (issue `#267498492 <https://issues.pigweed.dev/issues/267498492>`__)
+
+Targets
+=======
+``pw_assert_BACKEND`` for :ref:`target-host` was set to
+``print_and_abort_check_backend`` to enable compatibility with GoogleTest death
+tests.
+
+* (``host``) `Change pw_assert_BACKEND <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177835>`__
+
+OS support
+==========
+* (``zephyr``) `Update checkout to v3.5 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177669>`__
+
+Docs
+====
+New guidelines for authoring module docs have been published at
+:ref:`docs-contrib-moduledocs`. :ref:`module-pw_string` is now an example
+of a "golden" module docs set that follows the new guidelines. Please leave
+feedback on the new guidelines (and module docs updated to follow the
+guidelines) in `issue #309123039 <https://issues.pigweed.dev/issues/309123039>`__.
+
+There's now a definition for :ref:`docs-glossary-facade` in the glossary.
+
+* `Update module docs authoring guidelines <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177465>`__
+* `Fix nav and main content scrolling <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178591>`__
+ (issue `#303261476 <https://issues.pigweed.dev/issues/303261476>`__)
+* `Add udev instructions to Bazel Get Started <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178435>`__
+* `Add information on the experimental repo to contributing.rst <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178272>`__
+* `Mention command for updating Py dep hashes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177799>`__
+* `Define facade in glossary <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177632>`__
+* `Remove symlinks to files that were removed <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177530>`__
+* `Mention upstream development guide in contributor guidelines <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177459>`__
+* `Move all images out of the repo <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176751>`__
+* `Update changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177085>`__
+ (issue `#292247409 <https://issues.pigweed.dev/issues/292247409>`__)
+* `Move CoC to Contributors section of sitenav <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177071>`__
+
+SEEDs
+=====
+* (SEED-0107) `Update SEED references; fix typo <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177698>`__
+* (SEED-0112) `Async Poll Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168337>`__
+* (SEED-0115) `Fix link <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177093>`__
+* (SEED-0116) `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177697>`__
+
+Third party
+===========
+* (nanopb) `Detect protoc updates <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177650>`__
+
+------------
+Oct 20, 2023
+------------
+Highlights (Oct 5, 2023 to Oct 20, 2023):
+
+* ``pw_emu`` has launched! Check out :ref:`module-pw_emu` to get started.
+ See :ref:`seed-0108` for background.
+* :ref:`module-pw_log-tokenized-args` are now supported. See :ref:`seed-0105`
+ for background.
+* The new :cpp:class:`pw::allocator::UniquePtr` class offers a safer, simpler
+ RAII API for allocating individual values within an allocator.
+* A few SEEDs were accepted: :ref:`seed-0105`, :ref:`seed-0109`, and
+ :ref:`seed-0111`.
+* Lots of new docs, including a guide for
+ :ref:`getting started with Bazel <docs-get-started-bazel>`, a
+ conceptual explanation of :ref:`facades and backends <docs-facades>`,
+ and an eng blog post detailing :ref:`Kudzu <docs-blog-01-kudzu>`, an
+ electronic badge that the Pigweed team made for Maker Faire 2023.
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0110: Memory Allocation Interfaces <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168772>`__
+* `SEED-0113: Modular Bazel C/C++ Toolchain API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173453>`__
+* `SEED-0114: Channels <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175471>`__
+* `SEED-0115: Sensors <http://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176760>`__
+
+Modules
+=======
+
+pw_allocator
+------------
+The new :cpp:class:`pw::allocator::UniquePtr` class offers a safer, simpler
+RAII API for allocating individual values within an allocator.
+
+* `Fix SplitFreeListAllocator region alignment <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175232>`__
+* `Add UniquePtr\<T\> <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176781>`__
+
+pw_async
+--------
+* `Add CMake support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175475>`__
+
+pw_async_basic
+--------------
+* `Add missing include <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175476>`__
+* `Fix build error when using pw_async:heap_dispatcher <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173480>`__
+
+pw_bluetooth
+------------
+* `Define LEChannelSelectionAlgorithmSubevent <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176577>`__
+* `Define LEScanTimeoutSubevent <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176575>`__
+ (issue `#265052417 <https://issues.pigweed.dev/issues/265052417>`__)
+* `Use $size_in_bits instead of hardcoding size <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176573>`__
+* `Switch from parameterized value to determining at run time <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176572>`__
+ (issue `#305975969 <https://issues.pigweed.dev/issues/305975969>`__)
+* `Fix size reports <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173620>`__
+
+pw_build
+--------
+:ref:`module-pw_build-bazel-pw_linker_script` now describes how to work
+with linker scripts.
+
+* `Update pw_linker_script docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174848>`__
+* `Move pw_linker_script rule definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174872>`__
+
+pw_chre
+-------
+* `Remove TODOs for CHRE MacOS support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175490>`__
+
+pw_cli
+------
+* `Honor NO_COLOR and CLICOLOR_FORCE <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176860>`__
+* `Use typing.Literal <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176778>`__
+
+pw_digital_io
+-------------
+* `Add Android.bp for proto/rpc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176270>`__
+
+pw_emu
+------
+The module has launched! Check out :ref:`module-pw_emu` to get started.
+
+* `renode: Increase start timeout to 120s <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176865>`__
+* `Fix pid file race condition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176782>`__
+* `mock_emu: start listening before making the port available <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176856>`__
+ (issue `#306155313 <https://issues.pigweed.dev/issues/306155313>`__)
+* `qemu: Force using IPv4 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176430>`__
+ (issue `#305810466 <https://issues.pigweed.dev/issues/305810466>`__)
+* `Add renode support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173613>`__
+* `Add QEMU support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173612>`__
+* `core: Let the OS terminate foreground emulator processes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175638>`__
+* `Add user APIs and the command line interface <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173611>`__
+* `Add core components <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173610>`__
+* `Add Emulators Frontend module boilerplate <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162096>`__
+
+pw_env_setup
+------------
+* `Allow disabling CIPD cache <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176650>`__
+* `Add prpc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175236>`__
+
+pw_function
+-----------
+* `Move pw_function_CONFIG to .gni <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173652>`__
+
+pw_hdlc
+-------
+:ref:`module-pw_hdlc-api-rpc` now has much more information on how to use
+``pw_hdlc`` for RPC in Python.
+
+* `Update Python RPC documents <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174825>`__
+
+pw_i2c
+------
+* `Fix accidental c++2a <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176511>`__
+* `Add Android.bp for i2c proto/rpc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176070>`__
+
+pw_kvs
+------
+The new ``FlashPartitionWithLogicalSectors`` variant of ``FlashPartition``
+supports combining multiple physical ``FlashMemory`` sectors into a single
+logical ``FlashPartition`` sector.
+
+* `Add FlashPartitionWithLogicalSectors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/106917>`__
+
+pw_log_tokenized
+----------------
+:ref:`module-pw_log-tokenized-args` are now supported. See :ref:`seed-0105` for background.
+
+* `Add tokenized string args support to log backend <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164514>`__
+
+pw_log_zephyr
+-------------
+* `Clean-up unused dependencies from TOKENIZED_LIB <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174813>`__
+
+pw_minimal_cpp_stdlib
+---------------------
+* `Support additional libraries <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173814>`__
+* `Add Zephyr Kconfig to enable include path <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173653>`__
+
+pw_package
+----------
+* `Update boringssl commit & skip clang-tidy <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175016>`__
+* `Update Emboss commit <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173619>`__
+
+pw_presubmit
+------------
+:ref:`module-pw_presubmit-presubmit-checks` has more guidance on when to use
+``--base`` and ``--full``.
+
+* `Add note about --full and --base <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175633>`__
+
+pw_snapshot
+-----------
+* `More detokenization tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176759>`__
+
+pw_spi
+------
+* `Fix cmake integration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175190>`__
+
+pw_sync_zephyr
+--------------
+* `Add TimedThreadNotification::try_acquire_until <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175478>`__
+
+pw_system
+---------
+The ``Device`` class's constructor now accepts a ``logger`` argument
+that enables you to specify which logger should be used.
+
+* `Add option to pass logger to Device <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175075>`__
+
+pw_third_party_freertos
+-----------------------
+* `Add arm_cm7_not_r0p1 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172382>`__
+
+pw_thread
+---------
+* `More detokenization tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176759>`__
+
+pw_thread_freertos
+------------------
+* `Fix extra wakeups when detaching threads <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175310>`__
+ (issue `#303885539 <https://issues.pigweed.dev/issues/303885539>`__)
+
+pw_tokenizer
+------------
+:ref:`module-pw_tokenizer-get-started-integration` has new guidance around
+configuring linker scripts in Bazel.
+
+* `Expose linker_script in BUILD.bazel <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175590>`__
+
+pw_toolchain
+------------
+* `Exclude googletest from static analysis <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173482>`__
+
+pw_transfer
+-----------
+* `Start the API reference <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170011>`__
+ (issue `#299147635 <https://issues.pigweed.dev/issues/299147635>`__)
+
+pw_web
+------
+* `Reduce table cell padding <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176690>`__
+ (issue `#305022558 <https://issues.pigweed.dev/issues/305022558>`__)
+* `Fix invisible jump button <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175330>`__
+* `Enable manual color scheme setting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173630>`__
+ (issue `#301498553 <https://issues.pigweed.dev/issues/301498553>`__)
+
+Build
+=====
+* `Fix pw_BUILD_BROKEN_GROUPS <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176114>`__
+* `Update Android.bp <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175631>`__
+ (issue `#277108894 <https://issues.pigweed.dev/issues/277108894>`__)
+
+Bazel
+-----
+* `Don't autodetect C++ toolchain <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175613>`__
+ (issue `#304880653 <https://issues.pigweed.dev/issues/304880653>`__)
+* `Add O2 to arm_gcc toolchain <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175536>`__
+ (issue `#299994234 <https://issues.pigweed.dev/issues/299994234>`__)
+
+Targets
+=======
+* (rp2040_pw_system) `Enable time slicing <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175074>`__
+
+OS support
+==========
+* (zephyr) `Allow direct CMake inclusions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175477>`__
+
+Docs
+====
+* `Move CoC to Contributors section of sitenav <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177071>`__
+* `Create concepts section in sitenav <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177037>`__
+* `Add facades and backends page <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170602>`__
+* `Add Bazel getting started tutorial <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176319>`__
+* `Remove css class on Kudzu image captions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176770>`__
+* `Kudzu photos <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176710>`__
+* `Refactor the getting started section <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176331>`__
+* `Add sitemap <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176492>`__
+* `Add hat tip for pixel doubling technique <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175639>`__
+* `Start eng blog and add Kudzu page <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175619>`__
+* `Add Pigweed Live directive <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174892>`__
+* `Add builder viz to CI/CQ intro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175414>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Fix link <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175415>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Add changelog highlight <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175231>`__
+* `Update changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174818>`__
+
+SEEDs
+=====
+A few SEEDs were accepted and a few more started.
+
+* (SEED-0105) `Add nested tokens to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* (SEED-0109) `Communication Buffers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168357>`__
+* (SEED-0111) `Update status, add link to SEED-0113 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176254>`__
+* (SEED-0111) `Make Bazel Pigweed's Primary Build System <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171695>`__
+* (SEED-0113) `Claim SEED number (Modular Bazel C/C++ Toolchain API) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175510>`__
+* (SEED-0114) `Claim SEED number (Channels) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175412>`__
+* (SEED-0115) `Clain SEED number (Sensors) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176763>`__
+
+Third party
+===========
+* (boringssl) `Remove crypto_sysrand.cc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175017>`__
+* (fuchsia) `Copybara import <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173651>`__
+* (fuchsia) `Update copybara with fit/defer.h <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173481>`__
+
+Miscellaneous
+=============
+* `Update formatting for new clang version <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175311>`__
+* `Use C++20 everywhere <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174630>`__
+ (issue `#303371098 <https://issues.pigweed.dev/issues/303371098>`__)
+* (revert) `Use .test convention" <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171793>`__
+* `Add generated Emboss code <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176571>`__
+
+-----------
+Oct 6, 2023
+-----------
+Highlights (Sep 21, 2023 to Oct 6, 2023):
+
+* We expanded our RP2040 support. See the new :ref:`module-pw_chrono_rp2040`
+ and :ref:`module-pw_digital_io_rp2040` modules.
+* The :ref:`new CancellableReader class in pw_hdlc <docs-changelog-20231009-pw_hdlc>`
+ is an interface for receiving RPC packets that guarantees its read process can be
+ stopped.
+* ``pw_rpc`` now :ref:`automatically generates a new DynamicClient interface
+ <docs-changelog-20231009-pw_rpc>` when dynamic allocation is enabled.
+* The Python backend for ``pw_tokenizer`` now supports :ref:`tokenizing strings as
+ arguments <docs-changelog-20231009-pw_tokenizer>`.
+* The ``pigweed_config`` mechanism in Bazel is now officially retired.
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0105: Add nested tokens and tokenized args to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0109: Communication Buffers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168357>`__
+* `SEED-0110: Memory Allocation Interfaces <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168772>`__
+* `SEED-0111: Make Bazel Pigweed's Primary Build System <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171695>`__
+* `SEED-0112: Async Poll Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168337>`__
+
+Modules
+=======
+
+.. _docs-changelog-20231009-pw_allocator:
+
+pw_allocator
+------------
+We added a bunch of new allocator APIs! ``AllocatorMetricProxy`` is a wrapper for
+``Allocator`` that tracks the number and total of current memory allocations as well
+as peak memory usage. ``LibCAllocator`` is an allocator that uses ``malloc()`` and
+``free()``. ``NullAllocator`` is an allocator that always fails which is useful for
+disallowing memory allocations under certain circumstances. ``SplitFreeListAllocator``
+uses a free list to reduce fragmentation. ``FallbackAllocator`` enables you to
+specify primary and secondary allocators.
+
+* `Add Android.bp <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173851>`__
+* `Add pool accessors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173615>`__
+* `Move Resize assertion <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173614>`__
+* `Add AllocatorMetricProxy <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172380>`__
+* `Add LibCAllocator <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172232>`__
+* `Add NullAllocator <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172233>`__
+* `Add SplitFreeListAllocator <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172231>`__
+* `Add FallbackAllocator <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171837>`__
+* `Generic interface for allocators <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171709>`__
+
+pw_analog
+---------
+* `Migrate MicrovoltInput to Doxygen <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170593>`__
+ (issue `#299147635 <https://issues.pigweed.dev/issues/299147635>`__)
+
+pw_async
+--------
+* `Add OWNERS file <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173690>`__
+
+pw_bloat
+--------
+``pw_size_report()`` has a new ``json_key_prefix`` argument which is an
+optional prefix for key names in JSON size reports and a new
+``full_json_summary`` argument which provides more control over how
+much detail is provided in a JSON size report.
+
+* `Update API to allow verbose json content <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168718>`__
+ (issue `#282057969 <https://issues.pigweed.dev/issues/282057969>`__)
+
+pw_bluetooth
+------------
+* `Format Emboss files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174832>`__
+* `Update comments in HCI event defs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174070>`__
+ (issue `#265052417 <https://issues.pigweed.dev/issues/265052417>`__)
+
+pw_build
+--------
+
+
+* `Fix path in Bazel pw_linker_script <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174591>`__
+* `Expose pw_linker_script in Bazel <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174590>`__
+ (issue `#303482154 <https://issues.pigweed.dev/issues/303482154>`__)
+* `Define empty configs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174490>`__
+* `Add bazel implementation of pw_cc_blob_library <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173452>`__
+ (issue `#238339027 <https://issues.pigweed.dev/issues/238339027>`__)
+* `Clean up build_target.gni <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/120215>`__
+ (issue `#260111641 <https://issues.pigweed.dev/issues/260111641>`__)
+* `Allow add_global_link_deps to be overriden <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150050>`__
+* `Expose pigweed_default_configs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173372>`__
+ (issue `#260111641 <https://issues.pigweed.dev/issues/260111641>`__)
+* `Apply -Wextra-semi to C code as well as C++ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172372>`__
+
+pw_chre
+-------
+* `Update bug numbers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172330>`__
+
+pw_chrono
+---------
+* `Add clarification to is_nmi_safe <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174573>`__
+
+pw_chrono_rp2040
+----------------
+This module is a new ``pw::chrono::SystemClock`` backend for RP2040.
+
+* `System clock backend <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174651>`__
+
+pw_cli
+------
+* `Update requires script <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126101>`__
+* `Narrow logic around colors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173232>`__
+
+pw_containers
+-------------
+There's a new C implementation for ``VariableLengthEntryDeque`` which is a
+double-ended queue buffer that stores variable-length entries inline in a
+circular (ring) buffer. The old ``VariableLengthEntryDeque`` was renamed
+to ``VariableLengthEntryQueue``.
+
+* `Add missing <utility> include for std::move <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173879>`__
+* `Rename to VariableLengthEntryQueue <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173451>`__
+* `Rename files to variable_length_entry_queue <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173450>`__
+* `VariableLengthEntryDeque Entry struct <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173130>`__
+* `VariableLengthEntryDeque C implementation <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169731>`__
+
+pw_digital_io_rp2040
+--------------------
+This module is a new RP2040 backend for ``pw_digital_io``.
+
+* `Implementation <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173550>`__
+ (issue `#303255049 <https://issues.pigweed.dev/issues/303255049>`__)
+
+pw_env_setup
+------------
+We made the Pigweed bootstrap process on Windows more robust.
+
+* `Fix double bootstrap.bat failures on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172416>`__
+ (issue `#300992566 <https://issues.pigweed.dev/issues/300992566>`__)
+* `Better highlight bootstrap failure <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172415>`__
+* `Fix double bootstrap.bat failures on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172410>`__
+ (issue `#300992566 <https://issues.pigweed.dev/issues/300992566>`__)
+
+.. _docs-changelog-20231009-pw_hdlc:
+
+pw_hdlc
+-------
+The new ``CancellableReader`` class is a new interface for receiving RPC
+packets that guarantees its read process can be stopped.
+
+* `Add CancellableReader <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172051>`__
+ (issue `#294858483 <https://issues.pigweed.dev/issues/294858483>`__)
+
+pw_i2c
+------
+* `Fix docs to use MakeExpectedTransactionArray <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173570>`__
+* `Add cmake integration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172210>`__
+
+pw_kvs
+------
+The new ``FlashPartitionWithLogicalSectors`` C++ class supports combining
+multiple physical ``FlashMemory`` sectors into a single logical
+``FlashPartition`` sector.
+
+* `Add FlashPartitionWithLogicalSectors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/106917>`__
+
+pw_libc
+-------
+* `Don't implicitly link against global link_deps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150051>`__
+
+pw_metric
+---------
+* `Make constructors constexpr <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172379>`__
+
+pw_minimal_cpp_stdlib
+---------------------
+* `Update to compile with stdcompat <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173350>`__
+* `Namespace public/internal to module <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173692>`__
+
+pw_perf_test
+------------
+* `Gate on pw_chrono_SYSTEM_TIMER_BACKEND <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174650>`__
+
+pw_presubmit
+------------
+* `Allow dots in module part of commit message <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174232>`__
+* `Use autodoc for context classes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169119>`__
+* `Allow passing kwargs to build.bazel <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173853>`__
+ (issue `#302045722 <https://issues.pigweed.dev/issues/302045722>`__)
+* `No env_with_clang_vars with bazel <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173656>`__
+
+pw_ring_buffer
+--------------
+* `Minor build and docs updates <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173030>`__
+
+.. _docs-changelog-20231009-pw_rpc:
+
+pw_rpc
+------
+If dynamic allocation is enabled via ``PW_RPC_DYNAMIC_ALLOCATION`` a new
+``DynamicClient`` is now generated which dynamically allocates the call
+object with ``PW_RPC_MAKE_UNIQUE_PTR``.
+
+* `Generate DynamicClient that dynamically allocates call objects <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168534>`__
+* `Add CancellableReader <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172051>`__
+ (issue `#294858483 <https://issues.pigweed.dev/issues/294858483>`__)
+
+pw_rpc_transport
+----------------
+* `Add a test loopback service registry <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171114>`__
+ (issue `#300663813 <https://issues.pigweed.dev/issues/300663813>`__)
+
+pw_stream
+---------
+``pw_stream`` now has initial support for ``winsock2``.
+
+* `Add Windows socket support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172413>`__
+
+pw_sys_io_rp2040
+----------------
+* `Renamed from pw_sys_io_pico <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174910>`__
+
+.. _docs-changelog-20231009-pw_tokenizer:
+
+pw_tokenizer
+------------
+The Python backend now supports nested hashing tokenization. See
+:ref:`module-pw_tokenizer-nested-arguments`.
+
+* `Support nested hashing tokenization (python backend) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/145339>`__
+ (issue `#278890205 <https://issues.pigweed.dev/issues/278890205>`__)
+* `Test for C99 support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170150>`__
+
+pw_toolchain
+------------
+* `Add libc stub for gettimeofday, update visibility rules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173850>`__
+* `Link against pw_libc for host clang toolchains <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/151439>`__
+
+pw_transfer
+-----------
+* `Start the API reference <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170011>`__
+ (issue `#299147635 <https://issues.pigweed.dev/issues/299147635>`__)
+* `Remove old test server <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172951>`__
+ (issue `#234875234 <https://issues.pigweed.dev/issues/234875234>`__)
+
+pw_unit_test
+------------
+* `Do not print contents of unknown objects <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174911>`__
+* `Add more pw_unit_test_TESTONLY args <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173670>`__
+ (issue `#234873207 <https://issues.pigweed.dev/issues/234873207>`__)
+* `Add pw_unit_test_TESTONLY build arg <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171970>`__
+ (issue `#234873207 <https://issues.pigweed.dev/issues/234873207>`__)
+
+pw_watch
+--------
+* `Add link to served docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173456>`__
+
+pw_web
+------
+* `Make ongoing transfers accessible downstream <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174231>`__
+* `TypeScript workarounds for disambiguation errors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173590>`__
+* `Throw error as an Error type <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173051>`__
+* `Remove need for Buffer package in pw_hdlc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172377>`__
+* `Remove date-fns <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172371>`__
+
+Build
+=====
+* `Fix extended default group <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174574>`__
+ (issue `#279161371 <https://issues.pigweed.dev/issues/279161371>`__)
+* `Fix \`all\` target in GN build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173050>`__
+* `Add an extended default group <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/110391>`__
+
+Bazel
+-----
+* `Retire pigweed_config (part 3) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172411>`__
+* `Retire pigweed_config (part 2) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170058>`__
+ (issue `#291106264 <https://issues.pigweed.dev/issues/291106264>`__)
+
+Docs
+====
+We started a :ref:`glossary <docs-glossary>` and added new docs about
+:ref:`rollers <docs-rollers>` and :ref:`CI/CQ <docs-ci-cq-intro>`.
+
+* `Add docs on rollers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174770>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Remove redundant auto-submit section <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174890>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Reformat CI/CQ Intro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174870>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Move CI/CQ Intro to infra/ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174776>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Address comments on CI/CQ intro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173932>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Tidy up build system docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173658>`__
+* `Fix typo <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173872>`__
+* `Add CI/CQ Intro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173455>`__
+ (issue `#302680656 <https://issues.pigweed.dev/issues/302680656>`__)
+* `Add policy on incomplete docs changes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173617>`__
+* `Start the glossary <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172952>`__
+* `Update changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172810>`__
+ (issue `#292247409 <https://issues.pigweed.dev/issues/292247409>`__)
+* `Add Doxygen @endcode guidance <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172470>`__
+
+SEEDs
+=====
+* (SEED-0112) `Fix link <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174771>`__
+
+Miscellaneous
+=============
+
+pigweed.json
+------------
+* `Exclude patches.json from formatting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/174230>`__
+ (issue `#232234662 <https://issues.pigweed.dev/issues/232234662>`__)
+
+------------
+Sep 22, 2023
+------------
+Highlights (Sep 07, 2023 to Sep 22, 2023):
+
+* ``pw_tokenizer`` has :ref:`new C++ methods for detokenizing
+ Base64-encoded strings and new C functions for manually encoding tokenized
+ messages that contain integers <docs-changelog-pw_tokenizer-20230922>`.
+* ``pw::rpc::SynchronousCall`` now supports the use of :ref:`custom response message
+ classes <docs-changelog-pw_rpc-20230922>`.
+* The C API for ``pw_varint`` got :ref:`lots of ergonomic improvements
+ <docs-changelog-pw_varint-20230922>`.
+* The new :ref:`docs-code_reviews` document outlines the upstream Pigweed code
+ review process.
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0105: Add nested tokens and tokenized args to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0109: Communication Buffers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168357>`__
+* `SEED-0110: Memory Allocation Interfaces <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168772>`__
+* `SEED-0111: Future of Pigweed build systems <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171695>`__
+* `SEED-0112: Async Poll Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168337>`__
+
+Modules
+=======
+
+pw function
+-----------
+* `Sign conversion fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171832>`__
+ (issue `#301079199 <https://issues.pigweed.dev/issues/301079199>`__)
+
+pw perf_test
+------------
+* `Sign conversion fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171832>`__
+ (issue `#301079199 <https://issues.pigweed.dev/issues/301079199>`__)
+
+pw_analog
+---------
+* `Migrate AnalogInput to Doxygen <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170511>`__
+ (issue `#299147635 <https://issues.pigweed.dev/issues/299147635>`__)
+
+pw_async
+--------
+The ``Run*()`` methods of ``FakeDispatcher`` now return a boolean that indicates
+whether any tasks were invoked.
+
+* `Return bool from FakeDispatcher Run*() methods <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170599>`__
+
+pw_async_basic
+--------------
+``release()`` is now only called outside of locked contexts to prevent an
+issue where the thread wakes up and then immediately goes back to sleep.
+An unnecessary 5-second wakeup has been removed from ``BasicDispatcher``.
+
+* `release outside of lock context <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171103>`__
+* `Remove unnecessary 5-second wakeup <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171102>`__
+
+pw_base64
+---------
+The new ``pw::base64::IsValidChar()`` method can help you determine if a
+character is valid Base64.
+
+* `Add base64 detokenizer handler <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165010>`__
+
+pw_bluetooth
+------------
+More :ref:`Emboss <module-pw_third_party_emboss>` definitions were added.
+
+* `Add ReadLocalSupportedCommandsCommandCompleteEvent Emboss <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169877>`__
+* `Add LEReadLocalSupportedFeaturesCommandCompleteEvent <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169931>`__
+* `Add ReadBufferSizeCommandComplete Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169869>`__
+* `Add ReadBdAddrCommandCompleteEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170052>`__
+* `Add ReadLocalVersionInfoCommandCompleteEvent Emboss def <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169951>`__
+* `Add LELongTermKeyRequestSubevent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169950>`__
+* `Add UserPasskeyNotificationEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169917>`__
+
+pw_build
+--------
+* `Apply -Wextra-semi to C code as well as C++ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172372>`__
+
+pw_bytes
+--------
+The ``AlignDown()``, ``AlignUp()``, and ``Padding()`` methods of ``pw_kvs``
+have moved to ``pw_bytes`` to enable ``pw_allocator`` to use them without
+taking a dependency on ``pw_kvs``.
+
+* `Move Align functions from pw_kvs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171831>`__
+
+pw_checksum
+-----------
+* `Sign conversion fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171832>`__
+ (issue `#301079199 <https://issues.pigweed.dev/issues/301079199>`__)
+
+pw_chre
+-------
+The implementation of a module that will enable to work more seamlessly with
+Android's `Context Hub Runtime Environment <https://source.android.com/docs/core/interaction/contexthub>`__
+has begun.
+
+* `Update bug numbers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172330>`__
+* `Minor fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171851>`__
+ (issue `#301079509 <https://issues.pigweed.dev/issues/301079509>`__)
+* `Fix build rules to use paramertized paths <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171850>`__
+ (issue `#298474212 <https://issues.pigweed.dev/issues/298474212>`__)
+* `Split out shared_platform <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170791>`__
+* `Write our own version.cc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170790>`__
+ (issue `#300633363 <https://issues.pigweed.dev/issues/300633363>`__)
+* `Add barebones CHRE <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162510>`__
+ (issue `#294106526 <https://issues.pigweed.dev/issues/294106526>`__)
+
+pw_console
+----------
+When invoking ``pw_console`` directly from Python, you can now provide arguments
+through an ``argparse.Namespace`` instead of messing with ``sys.argv`` or forking
+another process.
+
+* `Allow injecting args via Python call <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172414>`__
+
+pw_containers
+-------------
+`MemorySanitizer <https://github.com/google/sanitizers/wiki/MemorySanitizer>`__ has
+been disabled in some of the ``InlineDeque`` implementation to prevent some false
+positive detections of uninitialized memory reads.
+
+* `Silence MSAN false positives <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171990>`__
+
+pw_env_setup
+------------
+Work continues on making the Windows bootstrap process more robust.
+
+* `Better highlight bootstrap failure <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172415>`__
+* `Fix double bootstrap.bat failures on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172410>`__
+ (issue `#300992566 <https://issues.pigweed.dev/issues/300992566>`__)
+* `Enable overriding Clang CIPD version <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171838>`__
+* `PyPI version bump to 0.0.15 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171836>`__
+* `Add relative_pigweed_root to pigweed.json <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171218>`__
+ (issue `#300632028 <https://issues.pigweed.dev/issues/300632028>`__)
+* `Roll cipd to 0f08b927516 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170452>`__
+
+pw_function
+-----------
+The documentation has been updated for accuracy.
+
+* `Update config.h comments <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171250>`__
+* `Add configurable Allocator default <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171130>`__
+* `Update example to match guidelines for parameters <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170651>`__
+* `Add Allocator injection <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170190>`__
+
+pw_fuzzer
+---------
+Conditional logic around fuzzing support has been refactored to allow for
+dedicated targets based on specific conditions and to make it clearer
+exactly what configurations and dependencies are being used.
+
+* `Refactor conditional GN targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169712>`__
+
+pw_ide
+------
+* `Reformat json files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172310>`__
+* `Fix clangd path on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171099>`__
+* `Move VSC extension into npm package dir <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170995>`__
+
+pw_libc
+-------
+The initial implementation work continues.
+
+* `Pull in 'abort' <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/138518>`__
+* `Use .test convention <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171793>`__
+* `Use underscore prefixed variables <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171792>`__
+* `Add documentation for pw_libc_source_set <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171693>`__
+* `Pull in 'gmtime' <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/137699>`__
+* `Fix printf for newer llvm-libc commits <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170831>`__
+* `Fix llvm-libc after internal assert changes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168830>`__
+
+pw_log
+------
+The implementation work continues to enable an Android component to read logs
+from a component running the ``pw_log_rpc`` service.
+
+* `Update Android.bp to generate RPC header files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169530>`__
+ (issue `#298693458 <https://issues.pigweed.dev/issues/298693458>`__)
+
+pw_log_string
+-------------
+* `Fix the default impl to handle zero length va args <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169975>`__
+
+pw_package
+----------
+Mirrors are now being used for various third-party dependencies.
+
+* `Use mirror for zephyrproject-rtos/zephyr <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170355>`__
+ (issue `#278914999 <https://issues.pigweed.dev/issues/278914999>`__)
+* `Use Pigweed mirror for google/emboss <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170356>`__
+ (issue `#278914999 <https://issues.pigweed.dev/issues/278914999>`__)
+* `Use mirror for raspberrypi/picotool <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170040>`__
+ (issue `#278914999 <https://issues.pigweed.dev/issues/278914999>`__)
+
+pw_polyfill
+-----------
+* `Increase __GNUC__ for __constinit <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171150>`__
+ (issue `#300478321 <https://issues.pigweed.dev/issues/300478321>`__)
+
+pw_presubmit
+------------
+A new JSON formatting check has been added. The missing newline check has been
+made more robust.
+
+* `Add JSON formatter <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171991>`__
+* `Better handling of missing newlines <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172230>`__
+ (issue `#301315329 <https://issues.pigweed.dev/issues/301315329>`__)
+* `Expand Bazel parser to tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171890>`__
+* `Remove now-unnecessary flag <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171670>`__
+ (issue `#271299438 <https://issues.pigweed.dev/issues/271299438>`__)
+* `Additional functions for handling gn args <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170594>`__
+* `Include bazel_build in full program <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170056>`__
+
+pw_protobuf
+-----------
+* `Fix "Casting..." heading level <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171030>`__
+
+.. _docs-changelog-pw_rpc-20230922:
+
+pw_rpc
+------
+``pw::rpc::SynchronousCall`` now supports the use of custom response message
+classes that set field callbacks in their constructor. See
+:ref:`module-pw_rpc-client-sync-call-wrappers`.
+
+.. todo-check: disable
+
+* `Refer to bug in TODO and fix format <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172453>`__
+* `Support custom response messages in SynchronousCall <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170041>`__
+ (issue `#299920227 <https://issues.pigweed.dev/issues/299920227>`__)
+* `Add fuzz tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/143474>`__
+
+.. todo-check: enable
+
+pw_stream
+---------
+* `Add Windows socket support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172413>`__
+
+pw_string
+---------
+* `Fix signed integer overflow <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171839>`__
+
+pw_system
+---------
+* `Add arm_none_eabi_gcc_support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158730>`__
+
+pw_thread
+---------
+* `Fix small typo in docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171750>`__
+
+.. _docs-changelog-pw_tokenizer-20230922:
+
+pw_tokenizer
+------------
+``pw::tokenizer::Detokenizer`` has new ``DetokenizeBase64Message()`` and
+``DetokenizeBase64()`` methods for detokenizing Base64-encoded strings.
+The new ``pw_tokenizer_EncodeInt()`` and ``pw_tokenizer_EncodeInt64()``
+functions in the C API make it easier to manually encode tokenized messages
+with integers from C.
+
+* `C++ Base64 detokenization improvements <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171675>`__
+* `Add base64 detokenizer handler <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165010>`__
+* `C functions for encoding arguments <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169976>`__
+
+pw_toolchain
+------------
+``arm_gcc`` now supports Cortex-M33.
+
+* `Add missing objcopy tool to bazel toolchains <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171697>`__
+ (issue `#301004620 <https://issues.pigweed.dev/issues/301004620>`__)
+* `Add cpu flags to asmopts as well <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171671>`__
+* `Add cortex-m33 support to arm_gcc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171237>`__
+
+pw_toolchain_bazel
+------------------
+* `Support ar opts in pw_toolchain_features <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171673>`__
+* `Add cortex-m7 constraint_value <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171071>`__
+ (issue `#300467616 <https://issues.pigweed.dev/issues/300467616>`__)
+
+.. _docs-changelog-pw_varint-20230922:
+
+pw_varint
+---------
+The C encoding functions now have an output size argument, making them much
+easier to use. There's a new macro for calculating the encoded size of an
+integer in a C constant expression. Incremental versions of the encode and
+decode functions have been exposed to support in-place encoding and decoding
+with non-contiguous buffers.
+
+* `C API updates <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170050>`__
+
+pw_web
+------
+The ``ProgressStats`` and ``ProgressCallback`` types are now exported.
+Styling and scrolling behavior in the log viewer has been improved.
+
+* `Remove need for Buffer package in pw_hdlc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172377>`__
+* `Remove date-fns <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172371>`__
+* `Export ProgressStats, ProgressCallback types <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171707>`__
+* `Add back 'buffer' dependency <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171891>`__
+* `NPM version bump to 0.0.13 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171110>`__
+* `Improve scrolling behavior <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171290>`__
+ (issue `#298097109 <https://issues.pigweed.dev/issues/298097109>`__)
+* `Fix leading white spaces, scrollbar size, and filters in quotes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170811>`__
+* `NPM version bump to 0.0.12 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170597>`__
+* `Fix column sizing & toggling, update UI <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169591>`__
+* `Replace Map() with object in proto collection <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170493>`__
+
+pw_work_queue
+-------------
+* `Don't lock around work_notification_ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170450>`__
+* `Migrate API reference to Doxygen <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169830>`__
+ (issue `#299147635 <https://issues.pigweed.dev/issues/299147635>`__)
+
+Build
+=====
+* `Update Android.bp <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171510>`__
+
+Bazel
+-----
+* `Add platform-printing aspect <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/122974>`__
+* `Retire pigweed_config (part 2) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170058>`__
+ (issue `#291106264 <https://issues.pigweed.dev/issues/291106264>`__)
+* `Retire pigweed_config (part 1) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168721>`__
+ (issue `#291106264 <https://issues.pigweed.dev/issues/291106264>`__)
+* `Remove -Wno-private-header from copts <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170951>`__
+ (issue `#240466562 <https://issues.pigweed.dev/issues/240466562>`__)
+* `Remove bazelembedded dependency <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170650>`__
+ (issue `#297239780 <https://issues.pigweed.dev/issues/297239780>`__)
+* `Move cxxopts out of bazelrc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170639>`__
+ (issue `#269195628 <https://issues.pigweed.dev/issues/269195628>`__)
+* `Use the same clang version as in GN <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170638>`__
+* `Arm gcc configuration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168192>`__
+ (issue `#297239780 <https://issues.pigweed.dev/issues/297239780>`__)
+
+Targets
+=======
+* `Fix pico_sdk elf2uf2 on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170770>`__
+* `Add pw_strict_host_clang_debug_dynamic_allocation tc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171674>`__
+
+Docs
+====
+The new :ref:`docs-code_reviews` document outlines the upstream Pigweed code
+review process.
+
+* `Add Doxygen @endcode guidance <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172470>`__
+* `Clean up remaining instances of code:: <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172050>`__
+ (issue `#300317685 <https://issues.pigweed.dev/issues/300317685>`__)
+* `Document code review process <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171774>`__
+* `Add link to in-progress hardware targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171239>`__
+* `Fix link title for pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170670>`__
+* `Update changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170055>`__
+ (issue `#292247409 <https://issues.pigweed.dev/issues/292247409>`__)
+
+SEEDs
+=====
+* `Update process document <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170390>`__
+* (SEED-0104) `Display Support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150793>`__
+* (SEED-0109) `Make link externally accessible <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170043>`__
+* (SEED-0110) `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170038>`__
+* (SEED-0111) `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171672>`__
+* (SEED-0112) `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168359>`__
+
+Third party
+===========
+* `Add public configs for FuzzTest deps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169711>`__
+
+third_party/fuchsia
+-------------------
+* `Copybara import <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171010>`__
+* `Update patch script and patch <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170890>`__
+* `Update patch <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170794>`__
+* `Support specifying the Fuchsia repo to use <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170170>`__
+
+third_party/pico_sdk
+--------------------
+* `Selectively disable elf2uf2 warnings <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171072>`__
+ (issue `#300474559 <https://issues.pigweed.dev/issues/300474559>`__)
+* `Fix multicore source filename <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170330>`__
+
+Miscellaneous
+=============
+.. todo-check: disable
+
+* `Use new TODO style <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/170730>`__
+* `Add toolchain team members <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172170>`__
+* `Fix double bootstrap.bat failures on Windows" <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/172410>`__
+ (issue `#300992566 <https://issues.pigweed.dev/issues/300992566>`__)
+
+.. todo-check: enable
+
+-----------
+Sep 8, 2023
+-----------
+Highlights (Aug 25, 2023 to Sep 8, 2023):
+
+* SEED :ref:`seed-0107` has been approved! Pigweed will adopt a new sockets API as
+ its primary networking abstraction. The sockets API will be backed by a new,
+ lightweight embedded-focused network protocol stack inspired by TCP/IP.
+* SEED :ref:`seed-0108` has also been approved! Coming soon, the new ``pw_emu``
+ module will make it easier to work with emulators.
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0104: display support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150793>`__
+* `SEED-0105: Add nested tokens and tokenized args to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0109: Communication Buffers <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168357>`__
+
+Modules
+=======
+
+pw_assert
+---------
+We fixed circular dependencies in Bazel.
+
+* `Remove placeholder target <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168844>`__
+* `Fix Bazel circular deps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160794>`__
+ (issue `#234877642 <https://issues.pigweed.dev/issues/234877642>`__)
+* `Introduce pw_assert_backend_impl <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168774>`__
+ (issue `#234877642 <https://issues.pigweed.dev/issues/234877642>`__)
+
+pw_bluetooth
+------------
+We added :ref:`Emboss <module-pw_third_party_emboss>` definitions.
+
+* `Add SimplePairingCompleteEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169916>`__
+* `Add UserPasskeyRequestEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169912>`__
+* `Add UserConfirmationRequestEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169871>`__
+* `Use hci.LinkKey in LinkKeyNotificationEvent.link_key <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168858>`__
+* `Add IoCapabilityResponseEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168354>`__
+* `Add IoCapabilityRequestEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168353>`__
+* `Add EncryptionKeyRefreshCompleteEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168331>`__
+* `Add ExtendedInquiryResultEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168330>`__
+
+pw_build
+--------
+* `Force watch and default recipe names <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169911>`__
+
+pw_build_mcuxpresso
+-------------------
+* `Output formatted bazel target <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169740>`__
+
+pw_cpu_exception
+----------------
+We added Bazel support.
+
+* `bazel build support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169733>`__
+ (issue `#242183021 <https://issues.pigweed.dev/issues/242183021>`__)
+
+pw_crypto
+---------
+The complete ``pw_crypto`` API reference is now documented on :ref:`module-pw_crypto`.
+
+* `Add API reference <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169572>`__
+ (issue `#299147635 <https://issues.pigweed.dev/issues/299147635>`__)
+
+pw_env_setup
+------------
+Banners should not print correctly on Windows.
+
+* `Add i2c protos to python deps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169231>`__
+* `Fix banner printing on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169172>`__
+ (issue `#289008307 <https://issues.pigweed.dev/issues/289008307>`__)
+
+pw_file
+-------
+* `Add pw_file python package <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168831>`__
+
+pw_function
+-----------
+The :cpp:func:`pw::bind_member()` template is now exposed in the public API.
+``bind_member()`` is useful for binding the ``this`` argument of a callable.
+We added a section to the docs explaining :ref:`why pw::Function is not a
+literal <module-pw_function-non-literal>`.
+
+* `Explain non-literal design rationale <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168777>`__
+* `Expose \`bind_member\` <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169123>`__
+
+pw_fuzzer
+---------
+We refactored ``pw_fuzzer`` logic to be more robust and expanded the
+:ref:`module-pw_fuzzer-guides-reproducing_oss_fuzz_bugs` doc.
+
+* `Refactor OSS-Fuzz support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167348>`__
+ (issue `#56955 <https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=56955>`__)
+
+pw_i2c
+------
+* `Use new k{FieldName}MaxSize constants to get buffer size <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168913>`__
+
+pw_kvs
+------
+We are discouraging the use of the shorter macros because they collide with
+Abseil's logging API.
+
+* `Remove usage of pw_log/shorter.h API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169920>`__
+ (issue `#299520256 <https://issues.pigweed.dev/issues/299520256>`__)
+
+pw_libc
+-------
+``snprintf()`` support was added.
+
+* `Import LLVM libc's snprintf <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/137735>`__
+
+pw_log_string
+-------------
+We added more detail to :ref:`module-pw_log_string`.
+
+* `Fix the default impl to handle zero length va args <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169975>`__
+* `Provide more detail in the getting started docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168934>`__
+ (issue `#298124226 <https://issues.pigweed.dev/issues/298124226>`__)
+
+pw_log_zephyr
+-------------
+It's now possible to define ``pw_log_tokenized_HandleLog()`` outside of Pigweed
+so that Zephyr projects have more flexibility around how they capture tokenized
+logs.
+
+* `Split tokenize handler into its own config <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168612>`__
+
+pw_package
+----------
+* `Handle failed cipd acl checks <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168530>`__
+
+pw_persistent_ram
+-----------------
+* `Add persistent_buffer flat_file_system_entry <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168832>`__
+
+pw_presubmit
+------------
+We added a reStructuredText formatter.
+
+* `Make builds_from_previous_iteration ints <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169721>`__
+ (issue `#299336222 <https://issues.pigweed.dev/issues/299336222>`__)
+* `Move colorize_diff to tools <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168839>`__
+* `RST formatting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168541>`__
+
+pw_protobuf
+-----------
+``max_size`` and ``max_count`` are now exposed in generated headers.
+The new ``proto_message_field_props()`` helper function makes it easier to
+iterate through a messages fields and properties.
+
+* `Expose max_size, max_count in generated header file <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168973>`__
+ (issue `#297364973 <https://issues.pigweed.dev/issues/297364973>`__)
+* `Introduce proto_message_field_props() <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168972>`__
+* `Change PROTO_FIELD_PROPERTIES to a dict of classes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168971>`__
+* `Rename 'node' to 'message' in forward_declare() <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168970>`__
+* `Simplify unnecessary Tuple return type <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168910>`__
+
+pw_random
+---------
+We're now auto-generating the ``XorShiftStarRng64`` API reference via Doxygen.
+
+* `Doxygenify xor_shift.h <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164510>`__
+
+pw_rpc
+------
+The new ``request_completion()`` method in Python enables you to send a
+completion packet for server streaming calls.
+
+* `Add request_completion to ServerStreamingCall python API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168439>`__
+
+pw_spi
+------
+* `Fix Responder.SetCompletionHandler() signature <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169130>`__
+
+pw_symbolizer
+-------------
+The ``LlvmSymbolizer`` Python class has a new ``close()`` method to
+deterministically close the background process.
+
+* `LlvmSymbolizer tool improvement <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168863>`__
+
+pw_sync
+-------
+We added :ref:`module-pw_sync-genericbasiclockable`.
+
+* `Add GenericBasicLockable <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165930>`__
+
+pw_system
+---------
+``pw_system`` now supports different channels for primary and logging RPC.
+
+* `Multi-channel configuration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167158>`__
+ (issue `#297076185 <https://issues.pigweed.dev/issues/297076185>`__)
+
+pw_thread_freertos
+------------------
+* `Add missing dep to library <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169239>`__
+
+pw_tokenizer
+------------
+We added :c:macro:`PW_TOKENIZE_FORMAT_STRING_ANY_ARG_COUNT` and
+:c:macro:`PW_TOKENIZER_REPLACE_FORMAT_STRING`. We refactored the docs
+so that you don't have to jump around the docs as much when learning about
+key topics like tokenization and token databases. Database loads now happen
+in a separate thread to avoid blocking the main thread. Change detection for
+directory databases now works more as expected. The config API is now exposed
+in the API reference.
+
+* `Remove some unused deps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169573>`__
+* `Simplify implementing a custom tokenization macro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169121>`__
+* `Refactor the docs to be task-focused <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169124>`__
+* `Reload database in dedicated thread <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168866>`__
+* `Combine duplicated docs sections <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168865>`__
+* `Support change detection for directory dbs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168630>`__
+* `Move config value check to .cc file <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168615>`__
+
+pw_unit_test
+------------
+We added ``testing::Test::HasFailure()``, ``FRIEND_TEST``, and ``<<`` messages
+to improve gTest compatibility.
+
+* `Add testing::Test::HasFailure() <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168810>`__
+* `Add FRIEND_TEST <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169270>`__
+* `Allow <<-style messages in test expectations <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168860>`__
+
+pw_varint
+---------
+``pw_varint`` now has a :ref:`C-only API <module-pw_varint-api-c>`.
+
+* `Add C-only implementation; cleanup <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169122>`__
+
+pw_web
+------
+Logs can now be downloaded as plaintext.
+
+* `Fix TypeScript errors in Device files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169930>`__
+* `Json Log Source example <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169176>`__
+* `Enable downloading logs as plain text <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168130>`__
+* `Fix UI/state bugs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167911>`__
+* `NPM version bump to 0.0.11 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168591>`__
+* `Add basic bundling tests for log viewer bundle <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168539>`__
+
+Build
+=====
+
+Bazel
+-----
+* `Fix alwayslink support in MacOS host_clang <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168614>`__
+ (issue `#297413805 <https://issues.pigweed.dev/issues/297413805>`__)
+* `Fix lint issues after roll <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169611>`__
+
+Docs
+====
+* `Fix broken links <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169579>`__
+ (issue `#299181944 <https://issues.pigweed.dev/issues/299181944>`__)
+* `Recommend enabling long file paths on Windows <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169578>`__
+* `Update Windows command for git hook <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168592>`__
+* `Fix main content scrolling <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168555>`__
+ (issue `#297384789 <https://issues.pigweed.dev/issues/297384789>`__)
+* `Update changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168540>`__
+ (issue `#292247409 <https://issues.pigweed.dev/issues/292247409>`__)
+* `Use code-block:: instead of code:: everywhere <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168617>`__
+* `Add function signature line breaks <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168554>`__
+* `Cleanup indentation <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168537>`__
+
+SEEDs
+=====
+* `SEED-0108: Emulators Frontend <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158190>`__
+
+Third party
+===========
+* `Add public configs for FuzzTest deps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169711>`__
+* `Reconfigure deps & add cflags to config <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/152691>`__
+
+Miscellaneous
+=============
+* `Fix formatting with new clang version <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/169078>`__
+
+mimxrt595_evk_freertos
+----------------------
+* `Use config_assert helper <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160378>`__
+
+------------
+Aug 25, 2023
+------------
+Highlights (Aug 11, 2023 to Aug 25, 2023):
+
+* ``pw_tokenizer`` now has Rust support.
+* The ``pw_web`` log viewer now has advanced filtering and a jump-to-bottom
+ button.
+* The ``run_tests()`` method of ``pw_unit_test`` now returns a new
+ ``TestRecord`` dataclass which provides more detailed information
+ about the test run.
+* A new Ambiq Apollo4 target that uses the Ambiq Suite SDK and FreeRTOS
+ has been added.
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0104: display support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150793>`__
+* `SEED-0105: Add nested tokens and tokenized args to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0108: Emulators Frontend <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158190>`__
+
+Modules
+=======
+
+pw_bloat
+--------
+* `Fix typo in method name <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166832>`__
+
+pw_bluetooth
+------------
+The :ref:`module-pw_third_party_emboss` files were refactored.
+
+* `Add SynchronousConnectionCompleteEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167862>`__
+* `Add all Emboss headers/deps to emboss_test & fix errors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168355>`__
+* `Add InquiryResultWithRssiEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167859>`__
+* `Add DataBufferOverflowEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167858>`__
+* `Add LinkKeyNotificationEvent Emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167855>`__
+* `Add LinkKeyRequestEvent emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167349>`__
+* `Remove unused hci emboss files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167090>`__
+* `Add RoleChangeEvent emboss definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167230>`__
+* `Add missing test dependency <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167130>`__
+* `Add new hci subset files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166730>`__
+
+pw_build
+--------
+The ``pw_build`` docs were split up so that each build system has its own page
+now. The new ``output_logs`` flag enables you to not output logs for ``pw_python_venv``.
+
+* `Handle read-only files when deleting venvs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167863>`__
+* `Split build system docs into separate pages <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165071>`__
+* `Use pw_toolchain_clang_tools <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167671>`__
+* `Add missing pw_linker_script flag <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167632>`__
+ (issue `#296928739 <https://issues.pigweed.dev/issues/296928739>`__)
+* `Fix output_logs_ unused warning <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166991>`__
+ (issue `#295524695 <https://issues.pigweed.dev/issues/295524695>`__)
+* `Don't include compile cmds when preprocessing ldscripts <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166490>`__
+* `Add pw_python_venv.output_logs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165330>`__
+ (issue `#295524695 <https://issues.pigweed.dev/issues/295524695>`__)
+* `Increase size of test linker script memory region <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164823>`__
+* `Add integration test metadata <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154553>`__
+
+pw_cli
+------
+* `Default change pw_protobuf default <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/126806>`__
+ (issue `#266298474 <https://issues.pigweed.dev/issues/266298474>`__)
+
+pw_console
+----------
+* `Update web viewer to use pigweedjs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162995>`__
+
+pw_containers
+-------------
+* `Silence MSAN false positive in pw::Vector <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167111>`__
+
+pw_docgen
+---------
+Docs builds should be faster now because Sphinx has been configured to use
+all available cores.
+
+* `Remove top nav bar <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168446>`__
+* `Parallelize Sphinx <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164738>`__
+
+pw_env_setup
+------------
+Sphinx was updated from v5.3.0 to v7.1.2. We switched back to the upstream Furo
+theme and updated to v2023.8.19. The content of ``pigweed_environment.gni`` now
+gets logged. There was an update to ensure that ``arm-none-eabi-gdb`` errors
+propagate correctly. There is now a way to override Bazel build files for CIPD
+repos.
+
+* `Upgrade sphinx and dependencies for docs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168431>`__
+* `Upgrade sphinx-design <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168339>`__
+* `Copy pigweed_environment.gni to logs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167850>`__
+* `arm-gdb: propagate errors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165411>`__
+* `arm-gdb: exclude %VIRTUAL_ENV%\Scripts from search paths <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164370>`__
+* `Add ability to override bazel BUILD file for CIPD repos <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165530>`__
+
+pw_function
+-----------
+* `Rename template parameter <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168334>`__
+
+pw_fuzzer
+---------
+* `Add test metadata <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154555>`__
+
+pw_hdlc
+-------
+A new ``close()`` method was added to ``HdlcRpcClient`` to signal to the thread
+to stop.
+
+* `Use explicit logger name <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166591>`__
+* `Mitigate errors on Python background thread <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162712>`__
+ (issue `#293595266 <https://issues.pigweed.dev/issues/293595266>`__)
+
+pw_ide
+------
+A new ``--install-editable`` flag was added to install Pigweed Python modules
+in editable mode so that code changes are instantly realized.
+
+* `Add cmd to install Py packages as editable <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163572>`__
+* `Make VSC extension run on older versions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167054>`__
+
+pw_perf_test
+------------
+* `Add test metadata <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154554>`__
+
+pw_presubmit
+------------
+``pw_presubmit`` now has an ESLint check for linting and a Prettier check for
+formatting JavaScript and TypeScript files.
+
+* `Add msan to OTHER_CHECKS <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168325>`__
+ (issue `#234876100 <https://issues.pigweed.dev/issues/234876100>`__)
+* `Upstream constraint file output fix <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166270>`__
+* `JavaScript and TypeScript lint check <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165410>`__
+* `Apply TypeScript formatting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164825>`__
+* `Use prettier for JS and TS files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165390>`__
+
+pw_rpc
+------
+A ``request_completion()`` method was added to the ``ServerStreamingCall``
+Python API. A bug was fixed related to encoding failures when dynamic buffers
+are enabled.
+
+* `Add request_completion to ServerStreamingCall python API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168439>`__
+* `Various small enhancements <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167162>`__
+* `Remove deprecated method from Service <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165510>`__
+* `Prevent encoding failure when dynamic buffer enabled <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166833>`__
+ (issue `#269633514 <https://issues.pigweed.dev/issues/269633514>`__)
+
+pw_rpc_transport
+----------------
+* `Add simple_framing Soong rule <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165350>`__
+
+pw_rust
+-------
+* `Update rules_rust to 0.26.0 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166831>`__
+
+pw_stm32cube_build
+------------------
+* `Windows path fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167865>`__
+
+pw_stream
+---------
+Error codes were updated to be more accurate and descriptive.
+
+* `Use more appropriate error codes for Cursor <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164592>`__
+
+pw_stream_uart_linux
+--------------------
+Common baud rates such as ``9600``, ``19200``, and so on are now supported.
+
+* `Add support for baud rates other than 115200 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165070>`__
+
+pw_sync
+-------
+Tests were added to make sure that ``pw::sync::Borrowable`` works with lock
+annotations.
+
+* `Test Borrowable with Mutex, TimedMutex, and InterruptSpinLock <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/153575>`__
+ (issue `#261078330 <https://issues.pigweed.dev/issues/261078330>`__)
+
+pw_system
+---------
+The ``pw_system.device.Device`` Python class can now be used as a
+`context manager <https://realpython.com/python-with-statement/>`_.
+
+* `Make pw_system.device.Device a context manager <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163410>`__
+
+pw_tokenizer
+------------
+``pw_tokenizer`` now has Rust support. The ``pw_tokenizer`` C++ config API
+is now documented at :ref:`module-pw_tokenizer-api-configuration` and
+the C++ token database API is now documented at
+:ref:`module-pw_tokenizer-api-token-databases`. When creating a token
+database, parent directories are now automatically created if they don't
+already exist. ``PrefixedMessageDecoder`` has been renamed to
+``NestedMessageDecoder``.
+
+* `Move config value check to .cc file <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168615>`__
+* `Create parent directory as needed <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168510>`__
+* `Rework pw_tokenizer.detokenize.PrefixedMessageDecoder <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167150>`__
+* `Minor binary database improvements <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167053>`__
+* `Update binary DB docs and convert to Doxygen <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163570>`__
+* `Deprecate tokenizer buffer size config <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163257>`__
+* `Fix instance of -Wconstant-logical-operand <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166731>`__
+* `Add Rust support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/145389>`__
+
+pw_toolchain
+------------
+A new Linux host toolchain built using ``pw_toolchain_bazel`` has been
+started. CIPD-provided Rust toolchains are now being used.
+
+* `Link against system libraries using libs not ldflags <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/151050>`__
+* `Use %package% for cxx_builtin_include_directories <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168340>`__
+* `Extend documentation for tool prefixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167633>`__
+* `Add Linux host toolchain <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164824>`__
+ (issue `#269204725 <https://issues.pigweed.dev/issues/269204725>`__)
+* `Use CIPD provided Rust toolchains <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166852>`__
+* `Switch macOS to use builtin_sysroot <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165414>`__
+* `Add cmake helpers for getting clang compile+link flags <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163811>`__
+
+pw_unit_test
+------------
+``run_tests()`` now returns the new ``TestRecord`` dataclass which provides
+more detailed information about the test run. ``SetUpTestSuit()`` and
+``TearDownTestSuite()`` were added to improve GoogleTest compatibility.
+
+* `Add TestRecord of Test Results <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166273>`__
+* `Reset static value before running tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166590>`__
+ (issue `#296157327 <https://issues.pigweed.dev/issues/296157327>`__)
+* `Add per-fixture setup/teardown <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165210>`__
+
+pw_web
+------
+Log viewers are now drawn every 100 milliseconds at most to prevent crashes
+when many logs arrive simultaneously. The log viewer now has a jump-to-bottom
+button. Advanced filtering has been added.
+
+* `NPM version bump to 0.0.11 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168591>`__
+* `Add basic bundling tests for log viewer bundle <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168539>`__
+* `Limit LogViewer redraws to 100ms <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167852>`__
+* `Add jump to bottom button, fix UI bugs and fix state bugs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164272>`__
+* `Implement advanced filtering <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162070>`__
+* `Remove object-path dependency from Device API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165013>`__
+* `Log viewer toolbar button toggle style <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165412>`__
+* `Log-viewer line wrap toggle <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164010>`__
+
+Targets
+=======
+
+targets
+-------
+A new Ambiq Apollo4 target that uses the Ambiq Suite SDK and FreeRTOS
+has been added.
+
+* `Ambiq Apollo4 support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/129490>`__
+
+Language support
+================
+
+Python
+------
+* `Upgrade mypy to 1.5.0 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166272>`__
+* `Upgrade pylint to 2.17.5 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166271>`__
+
+Docs
+====
+Doxygen-generated function signatures now present each argument on a separate
+line. Tabbed content looks visually different than before.
+
+* `Use code-block:: instead of code:: everywhere <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168617>`__
+* `Add function signature line breaks <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168554>`__
+* `Cleanup indentation <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168537>`__
+* `Remove unused myst-parser <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168392>`__
+* `Use sphinx-design for tabbed content <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168341>`__
+* `Update changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164810>`__
+
+SEEDs
+=====
+:ref:`SEED-0107 (Pigweed Communications) <seed-0107>` was accepted and
+SEED-0109 (Communication Buffers) was started.
+
+* `Update protobuf SEED title in index <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/166470>`__
+* `Update status to Accepted <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167770>`__
+* `Pigweed communications <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157090>`__
+* `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168358>`__
+
+Miscellaneous
+=============
+
+Build
+-----
+* `Make it possible to run MSAN in GN <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/167112>`__
+
+soong
+-----
+* `Remove host/vendor properties from defaults <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/165270>`__
+
+------------
+Aug 11, 2023
+------------
+Highlights (Jul 27, 2023 to Aug 11, 2023):
+
+* We're prototyping a Pigweed extension for VS Code. Learn more at
+ :ref:`docs-editors`.
+* We added ``pw_toolchain_bazel``, a new LLVM toolchain for building with
+ Bazel on macOS.
+* We are working on many docs improvements in parallel: auto-generating ``rustdocs``
+ for modules that support Rust
+ (`example <https://pigweed.dev/rustdoc/pw_varint/>`_), refactoring the
+ :ref:`module-pw_tokenizer` docs, migrating API references to Doxygen,
+ fixing `longstanding docs site UI issues <https://issues.pigweed.dev/issues/292273650>`_,
+ and more.
+
+Active SEEDs
+============
+Help shape the future of Pigweed! Please leave feedback on the following active RFCs (SEEDs):
+
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0104: display support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150793>`__
+* `SEED-0105: Add nested tokens and tokenized args to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0107: Pigweed communications <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157090>`__
+* `SEED-0108: Emulators Frontend <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158190>`__
+
+Modules
+=======
+
+pw_alignment
+------------
+* `Fix typos <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163250>`__
+
+pw_analog
+---------
+Long-term, all of our API references will be generated from header comments via
+Doxygen. Short-term, we are starting to show header files directly within the
+docs as a stopgap solution for helping Pigweed users get a sense of each
+module's API. See :ref:`module-pw_analog` for an example.
+
+* `Include header files as stopgap API reference <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161491>`__
+ (issue `#293895312 <https://issues.pigweed.dev/issues/293895312>`__)
+
+pw_base64
+---------
+We finished migrating the ``pw_random`` API reference to Doxygen.
+
+* `Finish Doxygenifying the API reference <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162911>`__
+* `Doxygenify the Encode() functions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156532>`__
+
+pw_boot_cortex_m
+----------------
+* `Allow explict target name <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159790>`__
+
+pw_build
+--------
+We added a ``log_build_steps`` option to ``ProjectBuilder`` that enables you
+to log all build step lines to your screen and logfiles.
+
+* `Handle ProcessLookupError exceptions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163710>`__
+* `ProjectBuilder log build steps option <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162931>`__
+* `Fix progress bar clear <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160791>`__
+
+pw_cli
+------
+We polished tab completion support.
+
+* `Zsh shell completion autoload <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160796>`__
+* `Make pw_cli tab completion reusable <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160379>`__
+
+pw_console
+----------
+We made copy-to-clipboard functionality more robust when running ``pw_console``
+over SSH.
+
+* `Set clipboard fallback methods <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150238>`__
+
+pw_containers
+-------------
+We updated :cpp:class:`filteredview` constructors and migrated the
+``FilteredView`` API reference to Doxygen.
+
+* `Doxygenify pw::containers::FilteredView <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160373>`__
+* `Support copying the FilteredView predicate <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160372>`__
+
+pw_docgen
+---------
+At the top of pages like :ref:`module-pw_tokenizer` there is a UI widget that
+provides information about the module. Previously, this UI widget had links
+to all the module's docs. This is no longer needed now that the site nav
+automatically scrolls to the page you're on, which allows you to see the
+module's other docs.
+
+* `Remove the navbar from the module docs header widget <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162991>`__
+ (issue `#292273650 <https://issues.pigweed.dev/issues/292273650>`__)
+
+pw_env_setup
+------------
+We made Python setup more flexible.
+
+* `Add clang_next.json <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163810>`__
+ (issue `#295020927 <https://issues.pigweed.dev/issues/295020927>`__)
+* `Pip installs from CIPD <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162093>`__
+* `Include Python packages from CIPD <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162073>`__
+* `Remove unused pep517 package <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162072>`__
+* `Use more available Python 3.9 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161492>`__
+ (issue `#292278707 <https://issues.pigweed.dev/issues/292278707>`__)
+* `Update Bazel to 2@6.3.0.6 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161010>`__
+
+pw_ide
+------
+We are prototyping a ``pw_ide`` extension for VS Code.
+
+* `Restore stable clangd settings link <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164011>`__
+* `Add command to install prototype extension <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162412>`__
+* `Prototype VS Code extension <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/151653>`__
+
+pw_interrupt
+------------
+We added a backend for Xtensa processors.
+
+* `Add backend for xtensa processors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160031>`__
+* `Tidy up target compatibility <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160650>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+
+pw_log_zephyr
+-------------
+We encoded tokenized messages to ``pw::InlineString`` so that the output is
+always null-terminated.
+
+* `Fix null termination of Base64 messages <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163650>`__
+
+pw_presubmit
+------------
+We increased
+`LUCI <https://chromium.googlesource.com/infra/infra/+/main/doc/users/services/about_luci.md>`_
+support and updated the ``#pragma once`` check to look for matching ``#ifndef``
+and ``#define`` lines.
+
+* `Fix overeager format_code matches <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162611>`__
+* `Exclude vsix files from copyright <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163011>`__
+* `Clarify unicode errors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162993>`__
+* `Upload coverage json to zoss <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162090>`__
+ (issue `#279161371 <https://issues.pigweed.dev/issues/279161371>`__)
+* `Add to context tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162311>`__
+* `Add patchset to LuciTrigger <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162310>`__
+* `Add helpers to LuciContext <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162091>`__
+* `Update Python vendor wheel dir <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161514>`__
+* `Add summaries to guard checks <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161391>`__
+ (issue `#287529705 <https://issues.pigweed.dev/issues/287529705>`__)
+* `Copy Python packages <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161490>`__
+* `Add ifndef/define check <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/152173>`__
+ (issue `#287529705 <https://issues.pigweed.dev/issues/287529705>`__)
+
+pw_protobuf_compiler
+--------------------
+We continued work to ensure that the Python environment in Bazel is hermetic.
+
+* `Use hermetic protoc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162913>`__
+ (issue `#294284927 <https://issues.pigweed.dev/issues/294284927>`__)
+* `Move reference to python interpreter <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162932>`__
+ (issue `#294414535 <https://issues.pigweed.dev/issues/294414535>`__)
+* `Make nanopb hermetic <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162313>`__
+ (issue `#293792686 <https://issues.pigweed.dev/issues/293792686>`__)
+
+pw_python
+---------
+We fixed bugs related to ``requirements.txt`` files not getting found.
+
+* `setup.sh requirements arg fix path <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164430>`__
+* `setup.sh arg spaces bug <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163510>`__
+
+pw_random
+---------
+We continued migrating the ``pw_random`` API reference to Doxygen.
+
+* `Doxygenify random.h <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163730>`__
+
+pw_rpc
+------
+We made the Java client more robust.
+
+* `Java client backwards compatibility <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164515>`__
+* `Avoid reflection in Java client <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162930>`__
+ (issue `#293361955 <https://issues.pigweed.dev/issues/293361955>`__)
+* `Use hermetic protoc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162913>`__
+ (issue `#294284927 <https://issues.pigweed.dev/issues/294284927>`__)
+* `Improve Java client error message for missing parser() method <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159471>`__
+
+pw_spi
+------
+We continued work on implementing a SPI responder interface.
+
+* `Responder interface definition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159230>`__
+
+pw_status
+---------
+We fixed the nesting on a documentation section.
+
+* `Promote Zephyr heading to h2 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160730>`__
+
+pw_stream
+---------
+We added ``remaining()``, ``len()``, and ``position()`` methods to the
+``Cursor`` wrapping in Rust.
+
+* `Add infalible methods to Rust Cursor <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164271>`__
+
+pw_stream_shmem_mcuxpresso
+--------------------------
+We added the :ref:`module-pw_stream_shmem_mcuxpresso` backend for ``pw_stream``.
+
+* `Add shared memory stream for NXP MCU cores <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160831>`__
+ (issue `#294406620 <https://issues.pigweed.dev/issues/294406620>`__)
+
+pw_sync_freertos
+----------------
+* `Fix ODR violation in tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160795>`__
+
+pw_thread
+---------
+* `Fix test_thread_context typo and presubmit <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162770>`__
+
+pw_tokenizer
+------------
+We added support for unaligned token databases and continued the
+:ref:`seed-0102` update of the ``pw_tokenizer`` docs.
+
+* `Separate API reference and how-to guide content <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163256>`__
+* `Polish the sales pitch <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163571>`__
+* `Support unaligned databases <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163333>`__
+* `Move the basic overview into getting started <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163253>`__
+* `Move the case study to guides.rst <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163255>`__
+* `Restore info that get lost during the SEED-0102 migration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163330>`__
+* `Use the same tagline on every doc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163332>`__
+* `Replace savings table with flowchart <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158893>`__
+* `Ignore string nonliteral warnings <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162092>`__
+
+pw_toolchain
+------------
+We fixed a regression that made it harder to use Pigweed in an environment where
+``pw_env_setup`` has not been run and fixed a bug related to incorrect Clang linking.
+
+* `Optionally depend on pw_env_setup_CIPD_PIGWEED <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163790>`__
+ (issue `#294886611 <https://issues.pigweed.dev/issues/294886611>`__)
+* `Prefer start-group over whole-archive <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150610>`__
+ (issue `#285357895 <https://issues.pigweed.dev/issues/285357895>`__)
+
+pw_toolchain_bazel
+------------------
+We added a an LLVM toolchain for building with Bazel on macOS.
+
+* `LLVM toolchain for macOS Bazel build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157634>`__
+ (issue `#291795899 <https://issues.pigweed.dev/issues/291795899>`__)
+
+pw_trace_tokenized
+------------------
+We made tracing more robust.
+
+* `Replace trace callback singletons with dep injection <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156912>`__
+
+pw_transfer
+-----------
+We made integration tests more robust.
+
+* `Fix use-after-destroy in integration test client <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163252>`__
+ (issue `#294101325 <https://issues.pigweed.dev/issues/294101325>`__)
+* `Fix legacy binary path <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162914>`__
+ (issue `#294284927 <https://issues.pigweed.dev/issues/294284927>`__)
+* `Mark linux-only Bazel tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162094>`__
+ (issue `#294101325 <https://issues.pigweed.dev/issues/294101325>`__)
+
+pw_web
+------
+We added support for storing user preferences in ``localStorage``.
+
+* `Fix TypeScript warnings in web_serial_transport.ts <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164591>`__
+* `Add state for view number, search string, and columns visible <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161390>`__
+* `Fix TypeScript warnings in transfer.ts <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162411>`__
+* `Fix TypeScript warnings <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162095>`__
+* `Remove dependency on 'crc' and 'buffer' NPM packages <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160830>`__
+
+Build
+=====
+We made the Bazel system more hermetic and fixed an error related to not
+finding the Java runtime.
+
+* `Do not allow PATH leakage into Bazel build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162610>`__
+ (issue `#294284927 <https://issues.pigweed.dev/issues/294284927>`__)
+* `Use remote Java runtime for Bazel build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160793>`__
+ (issue `#291791485 <https://issues.pigweed.dev/issues/291791485>`__)
+
+Docs
+====
+We created a new doc (:ref:`docs-editors`) that explains how to improve Pigweed
+support in various IDEs. We standardized how we present call-to-action buttons
+on module homepages. See :ref:`module-pw_tokenizer` for an example. We fixed a
+longstanding UI issue around the site nav not scrolling to the page that you're
+currently on.
+
+* `Add call-to-action buttons <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163331>`__
+* `Update module items in site nav <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163251>`__
+* `Add editor support doc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/110261>`__
+* `Delay nav scrolling to fix main content scrolling <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162990>`__
+ (issue `#292273650 <https://issues.pigweed.dev/issues/292273650>`__)
+* `Suggest editor configuration <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162710>`__
+* `Scroll to the current page in the site nav <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162410>`__
+ (issue `#292273650 <https://issues.pigweed.dev/issues/292273650>`__)
+* `Add changelog <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160170>`__
+ (issue `#292247409 <https://issues.pigweed.dev/issues/292247409>`__)
+
+SEEDs
+=====
+We created a UI widget to standardize how we present SEED status information.
+See the start of :ref:`seed-0102` for an example.
+
+* `Create Sphinx directive for metadata <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/161517>`__
+
+Third party
+===========
+
+third_party/mbedtls
+-------------------
+* `3.3.0 compatibility <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160790>`__
+ (issue `#293612945 <https://issues.pigweed.dev/issues/293612945>`__)
+
+Miscellaneous
+=============
+
+OWNERS
+------
+* `Add kayce@ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/163254>`__
+
+------------
+Jul 28, 2023
+------------
+Highlights (Jul 13, 2023 to Jul 28, 2023):
+
+* `SEED-0107: Pigweed Communications <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157090>`__,
+ a proposal for an embedded-focused network protocol stack, is under
+ discussion. Please review and provide your input!
+* ``pw_cli`` now supports tab completion!
+* A new UART Linux backend for ``pw_stream`` was added (``pw_stream_uart_linux``).
+
+Active SEEDs
+============
+* `SEED-0103: pw_protobuf Object Model <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>`__
+* `SEED-0104: display support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150793>`__
+* `SEED-0105: Add nested tokens and tokenized args to pw_tokenizer and pw_log <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154190>`__
+* `SEED-0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>`__
+* `SEED-0107: Pigweed communications <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157090>`__
+* `SEED-0108: Emulators Frontend <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158190>`__
+
+Modules
+=======
+
+pw_allocator
+------------
+We started migrating the ``pw_allocator`` API reference to Doxygen.
+
+* `Doxygenify the freelist chunk methods <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155395>`__
+
+pw_async
+--------
+We increased Bazel support.
+
+* `Fill in bazel build rules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156911>`__
+
+pw_async_basic
+--------------
+We reduced logging noisiness.
+
+* `Remove debug logging <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158193>`__
+
+pw_base64
+---------
+We continued migrating the ``pw_base64`` API reference to Doxygen.
+
+* `Doxygenify MaxDecodedSize() <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157091>`__
+
+pw_bloat
+--------
+We improved the performance of label creation. One benchmark moved from 120
+seconds to 0.02 seconds!
+
+* `Cache and optimize label production <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159474>`__
+
+pw_bluetooth
+------------
+Support for 3 events was added.
+
+* `Add 3 Event packets & format hci.emb <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157663>`__
+
+pw_build
+--------
+* `Fix progress bar clear <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160791>`__
+* `Upstream build script fixes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159473>`__
+* `Add pw_test_info <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154551>`__
+* `Upstream build script & presubmit runner <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/137130>`__
+* `pw_watch: Redraw interval and bazel steps <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159490>`__
+* `Avoid extra newlines for docs in generate_3p_gn <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/150233>`__
+* `pip install GN args <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155270>`__
+ (issue `#240701682 <https://issues.pigweed.dev/issues/240701682>`__)
+* `pw_python_venv generate_hashes option <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157630>`__
+ (issue `#292098416 <https://issues.pigweed.dev/issues/292098416>`__)
+
+pw_build_mcuxpresso
+-------------------
+Some H3 elements in the ``pw_build_mcuxpresso`` docs were being incorrectly
+rendered as H2.
+
+* `Fix doc headings <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155570>`__
+
+pw_chrono_freertos
+------------------
+We investigated what appeared to be a race condition but turned out to be an
+unreliable FreeRTOS variable.
+
+* `Update SystemTimer comments <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159231>`__
+ (issue `#291346908 <https://issues.pigweed.dev/issues/291346908>`__)
+* `Remove false callback precondition <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156091>`__
+ (issue `#291346908 <https://issues.pigweed.dev/issues/291346908>`__)
+
+pw_cli
+------
+``pw_cli`` now supports tab completion!
+
+* `Zsh shell completion autoload <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160796>`__
+* `Make pw_cli tab completion reusable <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160379>`__
+* `Print tab completions for pw commands <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160032>`__
+* `Fix logging msec timestamp format <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159930>`__
+
+pw_console
+----------
+Communication errors are now handled more gracefully.
+
+* `Detect comms errors in Python <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155814>`__
+
+pw_containers
+-------------
+The flat map implementation and docs have been improved.
+
+* `Doxygenify pw::containers::FilteredView <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160373>`__
+* `Support copying the FilteredView predicate <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160372>`__
+* `Improve FlatMap algorithm and filtered_view support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156652>`__
+* `Improve FlatMap doc example <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156651>`__
+* `Update FlatMap doc example so it compiles <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156650>`__
+
+pw_digital_io
+-------------
+We continued migrating the API reference to Doxygen. An RPC service was added.
+
+* `Doxygenify the interrupt handler methods <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154193>`__
+* `Doxygenify Enable() and Disable() <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155817>`__
+* `Add digital_io rpc service <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154974>`__
+
+pw_digital_io_mcuxpresso
+------------------------
+We continued migrating the API reference to Doxygen. Support for a new RPC
+service was added.
+
+* `Remove unneeded constraints <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155394>`__
+
+pw_docgen
+---------
+Support for auto-linking to Rust docs (when available) was added. We also
+explained how to debug Pigweed's Sphinx extensions.
+
+* `Add rustdoc linking support to pigweed-module tag <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159292>`__
+* `Add extension debugging instructions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156090>`__
+
+pw_env_setup
+------------
+There were lots of updates around how the Pigweed environment uses Python.
+
+* `pw_build: Disable pip version check <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160551>`__
+* `Add docstrings to visitors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159131>`__
+* `Sort pigweed_environment.gni lines <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158892>`__
+* `Mac and Windows Python requirements <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158912>`__
+ (issue `#292098416 <https://issues.pigweed.dev/issues/292098416>`__)
+* `Add more Python versions <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158891>`__
+ (issue `#292278707 <https://issues.pigweed.dev/issues/292278707>`__)
+* `Remove python.json from Bazel CIPD <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158911>`__
+ (issue `#292585791 <https://issues.pigweed.dev/issues/292585791>`__)
+* `Redirect variables from empty dirs <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158890>`__
+ (issue `#292278707 <https://issues.pigweed.dev/issues/292278707>`__)
+* `Split Python constraints per OS <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157657>`__
+ (issue `#292278707 <https://issues.pigweed.dev/issues/292278707>`__)
+* `Add --additional-cipd-file argument <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158170>`__
+ (issue `#292280529 <https://issues.pigweed.dev/issues/292280529>`__)
+* `Upgrade Python cryptography to 41.0.2 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157654>`__
+* `Upgrade ipython to 8.12.2 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157653>`__
+* `Upgrade PyYAML to 6.0.1 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157652>`__
+* `Add Python constraints with hashes <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/153470>`__
+ (issue `#287302102 <https://issues.pigweed.dev/issues/287302102>`__)
+* `Bump pip and pip-tools <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156470>`__
+* `Add coverage utilities <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155810>`__
+ (issue `#279161371 <https://issues.pigweed.dev/issues/279161371>`__)
+
+pw_fuzzer
+---------
+A fuzzer example was updated to more closely follow Pigweed code conventions.
+
+* `Update fuzzers to use Pigweed domains <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/148337>`__
+
+pw_hdlc
+-------
+Communication errors are now handled more gracefully.
+
+* `Detect comms errors in Python <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155814>`__
+* `Add target to Bazel build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157651>`__
+
+pw_i2c
+------
+An RPC service was added. Docs and code comments were updated to use ``responder``
+and ``initiator`` terminology consistently.
+
+* `Standardize naming on initiator/responder <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159132>`__
+* `Add i2c rpc service <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155250>`__
+
+pw_i2c_mcuxpresso
+-----------------
+* `Allow for static initialization of initiator <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155790>`__
+* `Add test to ensure compilation of module <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155390>`__
+
+pw_ide
+------
+* `Support multiple comp DB search paths <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/144210>`__
+ (issue `#280363633 <https://issues.pigweed.dev/issues/280363633>`__)
+
+pw_interrupt
+------------
+Work continued on how facade backend selection works in Bazel.
+
+* `Add backend for xtensa processors <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160031>`__
+* `Tidy up target compatibility <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160650>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+* `Remove cpu-based backend selection <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160380>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+* `Add backend constraint setting <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160371>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+* `Remove redundant Bazel targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154500>`__
+ (issue `#290359233 <https://issues.pigweed.dev/issues/290359233>`__)
+
+pw_log_rpc
+----------
+A docs section was added that explains how ``pw_log``, ``pw_log_tokenized``,
+and ``pw_log_rpc`` interact with each other.
+
+* `Explain relation to pw_log and pw_log_tokenized <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157231>`__
+
+pw_package
+----------
+Raspberry Pi Pico and Zephyr support improved.
+
+* `Add picotool package installer <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155791>`__
+* `Handle windows Zephyr SDK setup <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157030>`__
+* `Run Zephyr SDK setup.sh after syncing from CIPD <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156530>`__
+
+pw_perf_test
+------------
+* `Remove redundant Bazel targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154498>`__
+ (issue `#290359233 <https://issues.pigweed.dev/issues/290359233>`__)
+
+pw_presubmit
+------------
+* `Add ifndef/define check <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/152173>`__
+ (issue `#287529705 <https://issues.pigweed.dev/issues/287529705>`__)
+* `Remove deprecated gn_docs_build step <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159291>`__
+* `Fix issues with running docs_build twice <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159290>`__
+* `Add Rust docs to docs site <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157656>`__
+
+pw_protobuf_compiler
+--------------------
+* `Disable legacy namespace <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157232>`__
+* `Transition to our own proto compiler rules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157033>`__
+ (issue `#234874064 <https://issues.pigweed.dev/issues/234874064>`__)
+* `Allow external usage of macros <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155432>`__
+
+pw_ring_buffer
+--------------
+``pw_ring_buffer`` now builds with ``-Wconversion`` enabled.
+
+* `Conversion warning cleanups <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157430>`__
+ (issue `#259746255 <https://issues.pigweed.dev/issues/259746255>`__)
+
+pw_rpc
+------
+* `Create client call hook in Python client <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157870>`__
+* `Provide way to populate response callbacks during tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156670>`__
+* `Add Soong rule for pwpb echo service <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156270>`__
+
+pw_rpc_transport
+----------------
+* `Add more Soong rules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155035>`__
+
+pw_rust
+-------
+We are preparing pigweed.dev to automatically link to auto-generated
+Rust module documentation when available.
+
+* `Add combined Rust doc support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157632>`__
+* `Update @rust_crates sha <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155051>`__
+
+pw_spi
+------
+We updated docs and code comments to use ``initiator`` and ``responder``
+terminology consistently.
+
+* `Standardize naming on initiator/responder <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159132>`__
+
+pw_status
+---------
+* `Add Clone and Copy to Rust Error enum <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157093>`__
+
+pw_stream
+---------
+We continued work on Rust support.
+
+* `Fix Doxygen typo <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154732>`__
+* `Add read_exact() an write_all() to Rust Read and Write traits <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157094>`__
+* `Clean up rustdoc warnings <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157092>`__
+* `Add Rust varint reading and writing support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156451>`__
+* `Refactor Rust cursor to reduce monomorphization <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155391>`__
+* `Add Rust integer reading support <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155053>`__
+* `Move Rust Cursor to it's own sub-module <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155052>`__
+
+pw_stream_uart_linux
+--------------------
+A new UART Linux backend for ``pw_stream`` was added.
+
+* `Add stream for UART on Linux <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156910>`__
+
+pw_sync
+-------
+C++ lock traits were added and used.
+
+* `Improve Borrowable lock traits and annotations <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/153573>`__
+ (issue `#261078330 <https://issues.pigweed.dev/issues/261078330>`__)
+* `Add lock traits <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/153572>`__
+
+pw_sync_freertos
+----------------
+* `Fix ODR violation in tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160795>`__
+
+pw_sys_io
+---------
+* `Add android to alias as host system <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157871>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+* `Add chromiumos to alias as host system <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155811>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+
+pw_system
+---------
+* `Update IPython init API <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157872>`__
+* `Remove redundant Bazel targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154497>`__
+ (issue `#290359233 <https://issues.pigweed.dev/issues/290359233>`__)
+
+pw_tokenizer
+------------
+We refactored the ``pw_tokenizer`` docs to adhere to :ref:`seed-0102`.
+
+* `Update tagline, restore missing info, move sections <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158192>`__
+* `Migrate the proto docs (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157655>`__
+* `Remove stub sections and add guides link (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157631>`__
+* `Migrate the custom macro example (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157032>`__
+* `Migrate the Base64 docs (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156531>`__
+* `Migrate token collision docs (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155818>`__
+* `Migrate detokenization docs (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155815>`__
+* `Migrate masking docs (SEED-0102) <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155812>`__
+
+pw_toolchain
+------------
+The build system was modified to use relative paths to avoid unintentionally
+relying on the path environment variable. Map file generation is now optional
+to avoid generating potentially large map files when they're not needed.
+
+* `Test trivially destructible class <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159232>`__
+* `Make tools use relative paths <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159130>`__
+ (issue `#290848929 <https://issues.pigweed.dev/issues/290848929>`__)
+* `Support conditionally creating mapfiles <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157431>`__
+
+pw_trace_tokenized
+------------------
+* `Replace singletons with dependency injection <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155813>`__
+* `Remove redundant Bazel targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154499>`__
+ (issue `#290359233 <https://issues.pigweed.dev/issues/290359233>`__)
+
+pw_unit_test
+------------
+* `Update metadata test type for unit tests <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/154550>`__
+
+pw_varint
+---------
+* `Update Rust API to return number of bytes written <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156450>`__
+
+pw_watch
+--------
+We fixed an issue where builds were getting triggered when files were opened
+or closed without modication.
+
+* `Trigger build only on file modifications <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157633>`__
+
+pw_web
+------
+* `Remove dependency on 'crc' and 'buffer' NPM packages <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160830>`__
+* `Update theme token values and usage <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155970>`__
+* `Add disconnect() method to WebSerialTransport <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156471>`__
+* `Add docs section for log viewer component <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155050>`__
+
+Build
+=====
+
+bazel
+-----
+* `Add host_backend_alias macro <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160550>`__
+ (issue `#272090220 <https://issues.pigweed.dev/issues/272090220>`__)
+* `Fix missing deps in some modules <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160376>`__
+* `Support user bazelrc files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160030>`__
+* `Update rules_python to 0.24.0 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158913>`__
+ (issue `#266950138 <https://issues.pigweed.dev/issues/266950138>`__)
+
+build
+-----
+* `Use remote Java runtime for Bazel build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160793>`__
+ (issue `#291791485 <https://issues.pigweed.dev/issues/291791485>`__)
+* `Add Rust toolchain to Bazel macOS build <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159491>`__
+ (issue `#291749888 <https://issues.pigweed.dev/issues/291749888>`__)
+* `Mark linux-only Bazel build targets <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158191>`__
+
+Targets
+=======
+
+targets/rp2040_pw_system
+------------------------
+Some of the Pico docs incorrectly referred to another hardware platform.
+
+* `Fix references to STM32 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157233>`__
+ (issue `#286652309 <https://issues.pigweed.dev/issues/286652309>`__)
+
+Language support
+================
+
+python
+------
+* `Remove setup.py files <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159472>`__
+
+rust
+----
+* `Add rustdoc links for existing crates <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/159470>`__
+
+OS support
+==========
+
+zephyr
+------
+* `Add project name to unit test root <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156850>`__
+* `Add pigweed root as module <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156596>`__
+* `Fix setup.sh call <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156591>`__
+
+Docs
+====
+We added a feature grid to the homepage and fixed outdated info in various
+docs.
+
+* `pigweed.dev feature grid <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157658>`__
+* `Mention SEED-0102 in module_structure.rst <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157234>`__
+ (issue `#286477675 <https://issues.pigweed.dev/issues/286477675>`__)
+* `Remove outdated Homebrew info in getting_started.rst <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157291>`__
+ (issue `#287528787 <https://issues.pigweed.dev/issues/287528787>`__)
+* `Fix "gn args" examples which reference pw_env_setup_PACKAGE_ROOT <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/156452>`__
+* `Consolidate contributing docs in site nav <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155816>`__
+
+SEEDs
+=====
+
+SEED-0107
+---------
+* `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157031>`__
+
+SEED-0108
+---------
+* `Claim SEED number <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/158171>`__
+
+Third party
+===========
+
+third_party
+-----------
+* `Remove now unused rules_proto_grpc <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157290>`__
+
+third_party/mbedtls
+-------------------
+* `3.3.0 compatibility <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/160790>`__
+ (issue `#293612945 <https://issues.pigweed.dev/issues/293612945>`__)
diff --git a/docs/code_reviews.rst b/docs/code_reviews.rst
new file mode 100644
index 000000000..fcd42082c
--- /dev/null
+++ b/docs/code_reviews.rst
@@ -0,0 +1,132 @@
+.. _docs-code_reviews:
+
+============
+Code Reviews
+============
+All Pigweed development happens on Gerrit, following the `typical Gerrit
+development workflow <http://ceres-solver.org/contributing.html>`_. Consult the
+`Gerrit User Guide
+<https://gerrit-documentation.storage.googleapis.com/Documentation/2.12.3/intro-user.html>`_
+for more information on using Gerrit.
+
+You may add the special address
+``gwsq-pigweed@pigweed.google.com.iam.gserviceaccount.com`` as a reviewer to
+have Gerrit automatically choose an appropriate person to review your change.
+
+-----------
+For authors
+-----------
+
+.. _docs-code_reviews-small-changes:
+
+Small changes
+=============
+Please follow the guidance in `Google's Eng-Practices Small CLs
+<https://google.github.io/eng-practices/review/developer/small-cls.html>`_.
+
+Complete changes
+================
+Please follow the guidance in :ref:`docs-contributing-contribution-standards`.
+In summary, CLs must be complete, tested, and include documentation and unit
+tests for new code, bug fixes, and any code changes that merit it. However, to
+enable iterative work and small changes, ``TODO`` comments are acceptable. They
+must include an explanation of the problem and an action to take.
+
+We will not take over incomplete changes to avoid shifting our focus. We may
+reject changes that do not meet the criteria above.
+
+-------------
+For reviewers
+-------------
+
+Review speed
+============
+Follow the advice at `Speed of Code Reviews
+<https://google.github.io/eng-practices/review/reviewer/speed.html>`_. Most
+importantly,
+
+* If you are not in the middle of a focused task, **you should do a code review
+ shortly after it comes in**.
+* **One business day is the maximum time it should take to respond** to a code
+ review request (i.e., first thing the next morning).
+* If you will not be able to review the change within a business day, comment
+ on the change stating so, and reassign to another reviewer if possible.
+
+Attention set management
+========================
+Remove yourself from the `attention set
+<https://gerrit-review.googlesource.com/Documentation/user-attention-set.html>`_
+for changes where another person (author or reviewer) must take action before
+you can continue to review. You are encouraged, but not required, to leave a
+comment when doing so, especially for changes by external contributors who may
+not be familiar with our process.
+
+----------------------
+Common advice playbook
+----------------------
+What follows are bite-sized copy-paste-able advice when doing code reviews.
+Feel free to link to them from code review comments, too.
+
+.. _docs-code_reviews-playbook-platform-design:
+
+Shared platforms require careful design
+=======================================
+Pigweed is a platform shared by many embedded projects. This makes contributing
+to Pigweed rewarding: your change will help teams around the world! But it also
+makes contributing *hard*:
+
+* Edge cases that may not matter for one project can, and eventually will, come
+ up in another one.
+* Pigweed has many modules that can be used in isolation, but should also work
+ together, exhibiting a unified design philosophy and guiding users towards
+ safe, scalable patterns.
+
+As a result, Pigweed can't be as nimble as individual embedded projects, and
+often needs to engage in more careful design review, either in meetings with
+the core team or through :ref:`SEED-0001`. But we're committed to working
+through this with you!
+
+
+.. _docs-code_reviews-playbook-stale-changes:
+
+Stale changes
+=============
+Sometimes, a change doesn't make it out of the review process: after some
+rounds of review, there are unresolved comments from the Pigweed team, but the
+author is no longer actively working on the change.
+
+For any change that's not seen activity for 3 months, the Pigweed team will,
+
+#. `File a bug <https://issues.pigweed.dev/issues?q=status:open>`_ for the
+ feature or bug that the change was addressing, referencing the change.
+#. Mark the change Abandoned in Gerrit.
+
+This does *not* mean the change is rejected! It just indicates no further
+action on it is expected. As its author, you should feel free to reopen it when
+you have time to work on it again.
+
+Before making or sending major changes or SEEDs, please reach out in our
+`chat room <https://discord.gg/M9NSeTA>`_ or on the `mailing list
+<https://groups.google.com/forum/#!forum/pigweed>`_ first to ensure the changes
+make sense for upstream. We generally go through a design phase before making
+large changes. See :ref:`SEED-0001` for a description of this process; but
+please discuss with us before writing a full SEED. Let us know of any
+priorities, timelines, requirements, and limitations ahead of time.
+
+Gerrit for PRs
+==============
+We don't currently support GitHub pull requests. All Pigweed development takes
+place on `our Gerrit instance <https://pigweed-review.googlesource.com/>`_.
+Please resubmit your change there!
+
+See :ref:`docs-contributing` for instructions, and consult the `Gerrit User
+Guide
+<https://gerrit-documentation.storage.googleapis.com/Documentation/2.12.3/intro-user.html>`_
+for more information on using Gerrit.
+
+.. _docs-code_reviews-incomplete-docs-changes:
+
+Docs-Only Changes Do Not Need To Be Complete
+============================================
+Documentation-only changes should generally be accepted if they make the docs
+better or more complete, even if the documentation change itself is incomplete.
diff --git a/docs/concepts/index.rst b/docs/concepts/index.rst
index a08bbf81d..1f9c5c2a2 100644
--- a/docs/concepts/index.rst
+++ b/docs/concepts/index.rst
@@ -1,129 +1,39 @@
.. _docs-concepts:
-=============
-About Pigweed
-=============
+========
+Concepts
+========
+.. grid:: 1
-Why Build Pigweed?
-==================
-Our goal is to make embedded software development efficient, robust, and
-heck, even delightful, for projects ranging from weekend Arduino experiements
-to commercial products selling in the millions.
+ .. grid-item-card:: :octicon:`info` Facades & Backends
+ :link: docs-facades
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-Embedded software development is notoriously arcane. Developers often have to
-track down vendor toolchains specific to the hardware they're targeting, write
-their code against hardware-specfic SDKs/HALs, and limit themselves to a small
-subset of C. Project teams are on their own to figure out how to set up a build
-system, automated testing, serial communication, and many other embedded
-project fundamentals. This is error prone and takes effort away from developing
-the actual product!
+ A key concept in Pigweed's architecture. If you're using Pigweed
+ modules extensively in complex ways or contributing to upstream Pigweed,
+ you'll probably need to understand these ideas.
-There are solutions on the market that promise to solve all of these problems
-with a monolithic framework—just write your code against the framework and use
-hardware the framework supports, and you get an efficient embedded development
-environment. But this approach doesn't work well for existing projects that
-weren't built on the framework from the beginning or for projects that have
-specific needs the framework wasn't designed for. We know from experience that
-this approach alone doesn't meet our goal.
+.. grid:: 2
-So we have set out to build a platform that supports successful embedded
-developers at every scale by allowing them to adopt as much or as little of
-what Pigweed provides as they need, in the way that works best for their
-project.
+ .. grid-item-card:: :octicon:`info` FAQs
+ :link: docs-faq
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-How Pigweed Works
-=================
-Pigweed provides four foundational pillars to support your embedded development:
+ Answers to general, frequently asked questions about Pigweed.
-* :ref:`A comprehensive set of libraries for embedded development<docs-concepts-embedded-development-libraries>`
-* :ref:`A hermetic and replicable development environment<docs-concepts-development-environment>`
-* :ref:`A system for building, testing, and linting your project<docs-concepts-build-system>`
-* :ref:`A full framework for new projects that want a turn-key solution<docs-concepts-full-framework>`
+ .. grid-item-card:: :octicon:`info` Glossary
+ :link: docs-glossary
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-.. _docs-concepts-embedded-development-libraries:
+ Concise definitions of key Pigweed terms.
-Embedded Development Libraries
-------------------------------
-Pigweed enables you to use modern C++ and software development best practices in
-your embedded project without compromising performance or increasing memory use
-compared to conventional embedded C.
+.. toctree::
+ :maxdepth: 1
+ :hidden:
-We provide libraries (modules) for :ref:`strings<module-pw_string>`,
-:ref:`time<module-pw_chrono>`, :ref:`assertions<module-pw_assert>`,
-:ref:`logging<module-pw_log>`, :ref:`serial communication<module-pw_spi>`,
-:ref:`remote procedure calls (RPC)<module-pw_rpc>`, and
-:ref:`much more<docs-module-guides>`.
-
-These modules are designed to work both on your host machine and on a wide
-variety of target devices. We achieve this by writing them in an inherently
-portable way, or through the facade/backend pattern. As a result, you can write
-most or all of your code to run transparently on your host machine and targets.
-
-.. _docs-concepts-development-environment:
-
-Development Environment
------------------------
-Managing toolchains, build systems, and other software needed for a project is
-complex. Pigweed provides all of this out of the box for Linux, Mac, and
-Windows systems in a sealed environment that leaves the rest of your system
-untouched. Getting new developers started is as simple as cloning your project
-repository and activating the Pigweed environment.
-
-.. _docs-concepts-build-system:
-
-Build System
-------------
-Pigweed modules are built to integrate seamlessly into projects using GN. We
-are rapidly expanding our good support for CMake and nascent support for Bazel
-so you can use your build system of choice. For new projects, Pigweed provides a
-build system you can integrate your own code into that works out of the box.
-
-.. _docs-concepts-full-framework:
-
-Full Framework (coming in 2022)
--------------------------------
-For those who want a fully-integrated solution that provides everything Pigweed
-has to offer with an opinionated project structure, we are working diligently
-on a :ref:`Pigweed framework<module-pw_system>`. Stay tuned for more news to
-come! In the meantime, we invite you to discuss this and collaborate with us
-on `Discord <https://discord.gg/M9NSeTA>`_.
-
-.. _docs-concepts-right-for-my-project:
-
-Is Pigweed Right for My Project?
-================================
-Pigweed is still in its early stages, and while we have ambitious plans for it,
-Pigweed might not be the right fit for your project today. Here are some things
-to keep in mind:
-
-* Many individual modules are stable and are running on shipped devices today.
- If any of those modules meet your needs, you should feel safe bringing them
- into your project.
-
-* Some modules are in very early and active stages of development. They likely
- have unstable APIs and may not work on all supported targets. If this is the
- case, it will be indicated in the module's documentation. If you're interested
- in contributing to the development of one of these modules, we encourage you
- to experiment with them. Otherwise they aren't ready for use in most projects.
-
-* Setting up new projects to use Pigweed is currently not very easy, but we are
- working to address that. In the meantime, join the Pigweed community on
- `Discord <https://discord.gg/M9NSeTA>`_ to get help.
-
-Supported language versions
-===========================
-
-C++
----
-Most Pigweed code requires C++17, but a few modules, such as
-:ref:`module-pw_kvs` and :ref:`module-pw_tokenizer`, work with C++14. All
-Pigweed code is compatible with C++20. Pigweed defines GN toolchains for
-building with C++14 and C++20; see :ref:`target-host` target documentation for
-more information. For Bazel, the C++ standard version can be configured using
-the `--cxxopt flag <https://bazel.build/docs/user-manual#cxxopt>`_.
-
-.. _docs-concepts-python-version:
-
-Python
-------
-Pigweed officially supports Python 3.8, 3.9, and 3.10.
+ ../facades
+ ../glossary
+ FAQs <../faq>
diff --git a/docs/conf.py b/docs/conf.py
index f99f920e5..244794f0e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -17,7 +17,7 @@ from datetime import date
import sphinx
# The suffix of source filenames.
-source_suffix = ['.rst', '.md']
+source_suffix = ['.rst']
# The master toctree document. # inclusive-language: ignore
master_doc = 'index'
@@ -41,29 +41,18 @@ pygments_dark_style = 'pw_console.pigweed_code_style.PigweedCodeStyle'
extensions = [
'pw_docgen.sphinx.google_analytics', # Enables optional Google Analytics
+ 'pw_docgen.sphinx.kconfig',
'pw_docgen.sphinx.module_metadata',
+ 'pw_docgen.sphinx.pigweed_live',
+ 'pw_docgen.sphinx.seed_metadata',
'sphinx.ext.autodoc', # Automatic documentation for Python code
'sphinx.ext.napoleon', # Parses Google-style docstrings
'sphinxarg.ext', # Automatic documentation of Python argparse
'sphinxcontrib.mermaid',
'sphinx_design',
- 'myst_parser',
'breathe',
'sphinx_copybutton', # Copy-to-clipboard button on code blocks
-]
-
-myst_enable_extensions = [
- # "amsmath",
- "colon_fence",
- # "deflist",
- "dollarmath",
- # "html_admonition",
- # "html_image",
- # "linkify",
- # "replacements",
- # "smartquotes",
- # "substitution",
- # "tasklist",
+ 'sphinx_sitemap',
]
# When a user clicks the copy-to-clipboard button the `$ ` prompt should not be
@@ -124,6 +113,11 @@ html_css_files = [
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css",
]
+html_js_files = ['js/pigweed.js']
+
+# Furo color theme variables based on:
+# https://github.com/pradyunsg/furo/blob/main/src/furo/assets/styles/variables/_colors.scss
+# Colors with unchanged defaults are left commented out for easy updating.
html_theme_options = {
'light_css_variables': {
# Make the logo text more amaranth-like
@@ -160,8 +154,8 @@ html_theme_options = {
'color-sidebar-search-border': '#e815a5',
'color-sidebar-link-text--top-level': '#ff79c6',
'color-sidebar-link-text': '#8be9fd',
- 'color-sidebar-item-background--current': '#575757',
- 'color-sidebar-item-background--hover': '#4c333f',
+ 'color-sidebar-item-background--current': '#2a3037',
+ 'color-sidebar-item-background--hover': '#30353d',
'color-sidebar-item-expander-background--hover': '#4c333f',
# Function signature colors
'color-api-function-border': '#575757',
@@ -173,7 +167,8 @@ html_theme_options = {
'color-api-pre-name': '#87c1e5',
# Function name
'color-api-name': '#87c1e5',
- 'color-inline-code-background': '#2b2b2b',
+ 'color-code-background': '#2d333b',
+ 'color-inline-code-background': '#2d333b',
'color-inline-code-border': '#575757',
'color-text-selection-background': '#2674bf',
'color-text-selection-foreground': '#ffffff',
@@ -183,16 +178,62 @@ html_theme_options = {
'color-code-hll-background': '#ffc55140',
'color-section-button': '#fb71fe',
'color-section-button-hover': '#b529aa',
+ # The following color changes modify Furo's default dark mode colors for
+ # slightly less high-contrast.
+ # Base Colors
+ # 'color-foreground-primary': '#ffffffcc', # Main text and headings
+ # 'color-foreground-secondary': '#9ca0a5', # Secondary text
+ # 'color-foreground-muted': '#81868d', # Muted text
+ # 'color-foreground-border': '#666666', # Content borders
+ 'color-background-primary': '#1c2128', # Content
+ 'color-background-secondary': '#22272e', # Navigation and TOC
+ 'color-background-hover': '#30353dff', # Navigation-item hover
+ 'color-background-hover--transparent': '#30353d00',
+ 'color-background-border': '#444c56', # UI borders
+ 'color-background-item': '#373e47', # "background" items (eg: copybutton)
+ # Announcements
+ # 'color-announcement-background': '#000000dd',
+ # 'color-announcement-text': '#eeebee',
+ # Brand colors
+ # 'color-brand-primary': '#2b8cee',
+ # 'color-brand-content': '#368ce2',
+ # Highlighted text (search)
+ # 'color-highlighted-background': '#083563',
+ # GUI Labels
+ # 'color-guilabel-background': '#08356380',
+ # 'color-guilabel-border': '#13395f80',
+ # API documentation
+ # 'color-api-keyword': 'var(--color-foreground-secondary)',
+ # 'color-highlight-on-target': '#333300',
+ # Admonitions
+ 'color-admonition-background': 'var(--color-background-secondary)',
+ # Cards
+ 'color-card-border': 'var(--color-background-border)',
+ 'color-card-background': 'var(--color-background-secondary)',
+ # 'color-card-marginals-background': 'var(--color-background-hover)',
+ # Sphinx Design cards
+ 'sd-color-card-background': 'var(--color-background-secondary)',
+ 'sd-color-card-border': 'var(--color-background-border)',
},
}
-mermaid_version = '9.4.0'
-# TODO(tonymd): Investigate if ESM only v10 Mermaid can be used.
-# This does not work:
-# mermaid_init_js = '''
-# import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
-# mermaid.initialize({ startOnLoad: true });
-# '''
+# sphinx-sitemap needs this:
+# https://sphinx-sitemap.readthedocs.io/en/latest/getting-started.html#usage
+html_baseurl = 'https://pigweed.dev/'
+
+# https://sphinx-sitemap.readthedocs.io/en/latest/advanced-configuration.html
+sitemap_url_scheme = '{link}'
+
+mermaid_init_js = '''
+mermaid.initialize({
+ // Mermaid is manually started in //docs/_static/js/pigweed.js.
+ startOnLoad: false,
+ // sequenceDiagram Note text alignment
+ noteAlign: "left",
+ // Set mermaid theme to the current furo theme
+ theme: localStorage.getItem("theme") == "dark" ? "dark" : "default"
+});
+'''
# Output file base name for HTML help builder.
htmlhelp_basename = 'Pigweeddoc'
@@ -216,6 +257,7 @@ texinfo_documents = [
),
]
+templates_path = ['docs/layout']
exclude_patterns = ['docs/templates/**']
breathe_projects = {
@@ -250,6 +292,11 @@ cpp_paren_attributes = [
# Disable Python type hints
# autodoc_typehints = 'none'
+# Break class and function signature arguments into one arg per line if the
+# total length exceeds 130 characters. 130 seems about right for keeping one or
+# two parameters on a single line.
+maximum_signature_line_length = 130
+
def do_not_skip_init(app, what, name, obj, would_skip, options):
if name == "__init__":
diff --git a/docs/contributing.rst b/docs/contributing/index.rst
index 13ab4a9dc..006b76c69 100644
--- a/docs/contributing.rst
+++ b/docs/contributing/index.rst
@@ -4,16 +4,16 @@
Contributing
============
We'd love to accept your patches and contributions to Pigweed. There are just a
-few small guidelines you need to follow. Before making or sending major
-changes, please reach out on the `mailing list
-<https://groups.google.com/forum/#!forum/pigweed>`_ first to ensure the changes
-make sense for upstream. We generally go through a design phase before making
-large changes.
+few small guidelines you need to follow.
Before participating in our community, please take a moment to review our
:ref:`docs-code-of-conduct`. We expect everyone who interacts with the project
to respect these guidelines.
+Get started
+-----------
+See :ref:`docs-get-started-upstream`.
+
Pigweed contribution overview
-----------------------------
.. note::
@@ -36,8 +36,35 @@ One-time contributor setup
#. Install the Pigweed presubmit check hook with ``pw presubmit --install``.
Remember to :ref:`activate-pigweed-environment` first!
+Presubmission process
+^^^^^^^^^^^^^^^^^^^^^
+Before making or sending major changes or SEEDs, please reach out in our
+`chat room <https://discord.gg/M9NSeTA>`_ or on the `mailing list
+<https://groups.google.com/forum/#!forum/pigweed>`_ first to ensure the changes
+make sense for upstream. We generally go through a design phase before making
+large changes. See :ref:`SEED-0001` for a description of this process; but
+please discuss with us before writing a full SEED. Let us know of any
+priorities, timelines, requirements, and limitations ahead of time.
+
+For minor changes that don't fit the SEED process, follow the
+:ref:`docs-code_reviews-small-changes` guidance and the `Change submission
+process`_.
+
+.. warning::
+ Skipping communicating with us before doing large amounts of work risks
+ accepting your contribution. Communication is key!
+
Change submission process
^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. note::
+ A change is a single git commit, and is also known as Change List or CL for
+ short.
+
+.. tip::
+ Follow :ref:`docs-code_reviews-small-changes` for a smooth submission process
+
+#. Go through the `Presubmission process`_ and review this document's guidance.
#. Ensure all files include the correct copyright and license headers.
#. Include any necessary changes to the documentation.
#. Run :ref:`module-pw_presubmit` to detect style or compilation issues before
@@ -64,6 +91,25 @@ You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
+.. _docs-contributing-contribution-standards:
+
+Contribution Standards
+----------------------
+Contributions, i.e. CLs, are expected to be complete solutions, accompanied by
+documentation and unit tests when merited, e.g. new code, bug fixes, or code
+that changes some behavior. This also means that code changes must be tested.
+Tests will avoid or minimize any negative impact to Pigweed users, while
+documentation will help others learn about the new changes.
+
+We understand that you may have different priorities or not know the best way to
+complete your contribution. In that case, reach out via our `chat room
+<https://discord.gg/M9NSeTA>`_ or on the `mailing list
+<https://groups.google.com/forum/#!forum/pigweed>`_ for help or `File a bug
+<https://issues.pigweed.dev/issues?q=status:open>`_
+requesting fixes describing how this may be blocking you. Otherwise you risk
+working on a CL that does not match our vision and could be rejected. To keep
+our focus, we cannot adopt incomplete CLs.
+
.. _gerrit-commit-hook:
Gerrit Commit Hook
@@ -72,33 +118,49 @@ Gerrit requires all changes to have a ``Change-Id`` tag at the bottom of each
commit message. You should set this up to be done automatically using the
instructions below.
-**Linux/macOS**
-
-The command below assumes that your current working directory is the root
+The commands below assume that your current working directory is the root
of your Pigweed repository.
-.. code:: bash
+**Linux/macOS**
+
+.. code-block:: bash
- $ f=`git rev-parse --git-dir`/hooks/commit-msg ; mkdir -p $(dirname $f) ; curl -Lo $f https://gerrit-review.googlesource.com/tools/hooks/commit-msg ; chmod +x $f
+ f=`git rev-parse --git-dir`/hooks/commit-msg ; mkdir -p $(dirname $f) ; curl -Lo $f https://gerrit-review.googlesource.com/tools/hooks/commit-msg ; chmod +x $f
**Windows**
-Download `the Gerrit commit hook
-<https://gerrit-review.googlesource.com/tools/hooks/commit-msg>`_ and then copy
-it to the ``.git\hooks`` directory in the Pigweed repository.
-
-.. code::
+.. code-block:: batch
- copy %HOMEPATH%\Downloads\commit-msg %HOMEPATH%\pigweed\.git\hooks\commit-msg
+ git rev-parse --git-dir > gitrepopath.txt & set /p "g="< gitrepopath.txt & del gitrepopath.txt & call set "f=%g%/hooks" & call mkdir "%f%" & call curl -Lo "%f%/commit-msg" https://gerrit-review.googlesource.com/tools/hooks/commit-msg
Commit Message
--------------
-See the :ref:`commit message section of the style guide<commit-style>` for how
-commit messages should look.
+See the :ref:`commit message section of the style
+guide<docs-pw-style-commit-message>` for how commit messages should look.
Documentation
-------------
-All Pigweed changes must either
+Most changes to Pigweed should have an associated documentation change.
+
+Building
+^^^^^^^^
+To build the documentation, follow the :ref:`getting
+started<docs-get-started-upstream>` guide so you can build Pigweed. Then:
+
+#. Change to your checkout directory and ``. activate.sh`` if necessary
+#. Run ``pw watch out`` to build the code, run tests, and build docs
+#. Wait for the build to finish (see a ``PASS``)
+#. Navigate to ``<CHECKOUT>/out/docs/gen/docs/html/index.html``
+#. Edit the relevant ``.rst`` file. Save when ready
+#. Refresh your browser after the build completes
+
+Alternately, you can use the local webserver in watch; this works better for
+some pages that have external resources: ``pw watch --serve-docs`` then
+navigate to `http://localhost:8000 <http://localhost:8000>`_ in your browser.
+
+Submission checklist
+^^^^^^^^^^^^^^^^^^^^
+All Pigweed changes must either:
#. Include updates to documentation, or
#. Include ``No-Docs-Update-Reason: <reason>`` in a Gerrit comment on the CL.
@@ -114,43 +176,40 @@ doesn't have any other documentation.
Code Reviews
------------
-All Pigweed development happens on Gerrit, following the `typical Gerrit
-development workflow <http://ceres-solver.org/contributing.html>`_. Consult the
-`Gerrit User Guide
-<https://gerrit-documentation.storage.googleapis.com/Documentation/2.12.3/intro-user.html>`_
-for more information on using Gerrit.
-
-You may add the special address
-``gwsq-pigweed@pigweed.google.com.iam.gserviceaccount.com`` as a reviewer to
-have Gerrit automatically choose an appropriate person to review your change.
-
-In the future we may support GitHub pull requests, but until that time we will
-close GitHub pull requests and ask that the changes be uploaded to Gerrit
-instead.
+See :ref:`docs-code_reviews` for information about the code review process.
+
+Experimental Repository and Where to Land Code
+----------------------------------------------
+Pigweed's has an `Experimental Repository
+<https://pigweed.googlesource.com/pigweed/experimental>`_ which differs from
+our main repository in a couple key ways:
+
+* Code is not expected to become production grade.
+* Code review standards are relaxed to allow experimentation.
+* In general the value of the code in the repository is the knowledge gained
+ from the experiment, not the code itself.
+
+Good uses of the repo include:
+
+* Experimenting with using an API (ex. C++20 coroutines) with no plans to
+ turn it into production code.
+* One-off test programs to gather data.
+
+We would like to avoid large pieces of code being developed in the experimental
+repository then imported into the Pigweed repository. If large amounts of code
+end up needing to migrate from experimental to main, then it must be landed
+incrementally as a series of reviewable patches, typically no
+`larger than 500 lines each
+<https://google.github.io/eng-practices/review/developer/small-cls.html>`_.
+This creates a large code review burden that often results in poorer reviews.
+Therefore, if the eventual location of the code will be the main Pigweed
+repository, it is **strongly encouraged** that the code be developed in the
+**main repository under an experimental flag**.
-Instructions for reviewers
-^^^^^^^^^^^^^^^^^^^^^^^^^^
-#. Get the `Gerrit Monitor extension
- <https://chrome.google.com/webstore/detail/gerrit-monitor/leakcdjcdifiihdgalplgkghidmfafoh?hl=en>`_.
-#. When added to the attention set for a change, respond within 1 business day:
-
- * Review the change if possible, OR
- * If you will not be able to review the change within 1 business day (e.g.
- due to handling P0s), comment on the change stating so, and reassign to
- another reviewer if possible.
- * The response time expectation only applies to Googlers working full-time
- on Pigweed.
-#. Remove yourself from the `attention set
- <https://gerrit-review.googlesource.com/Documentation/user-attention-set.html>`_
- for changes where another person (author or reviewer) must take action
- before you can continue to review. You are encouraged, but not required, to
- leave a comment when doing so, especially for changes by external
- contributors who may not be familiar with our process.
-
-SLO
-^^^
-90% of changes on which a Googler working on Pigweed full-time is added to the
-attention set as a reviewer get triaged within 1 business day.
+.. note::
+ The current organization of the experimental repository does not reflect
+ this policy. This will be re-organized once we have concrete recommendations
+ on its organization.
Community Guidelines
--------------------
@@ -165,7 +224,7 @@ browsers.
Apache header for C and C++ files:
-.. code:: none
+.. code-block:: none
// Copyright 2021 The Pigweed Authors
//
@@ -183,7 +242,7 @@ Apache header for C and C++ files:
Apache header for Python and GN files:
-.. code:: none
+.. code-block:: none
# Copyright 2020 The Pigweed Authors
#
@@ -257,26 +316,26 @@ git push hook using the following command:
Linux/macOS
^^^^^^^^^^^
-.. code:: bash
+.. code-block:: bash
$ pw presubmit --install
This will be effectively the same as running the following command before every
``git push``:
-.. code:: bash
+.. code-block:: bash
$ pw presubmit
-.. image:: ../pw_presubmit/docs/pw_presubmit_demo.gif
+.. image:: ../../pw_presubmit/docs/pw_presubmit_demo.gif
:width: 800
:alt: pw presubmit demo
If you ever need to bypass the presubmit hook (due to it being broken, for
example) you may push using this command:
-.. code:: bash
+.. code-block:: bash
$ git push origin HEAD:refs/for/main --no-verify
@@ -285,7 +344,7 @@ Presubmit and branch management
When creating new feature branches, make sure to specify the upstream branch to
track, e.g.
-.. code:: bash
+.. code-block:: bash
$ git checkout -b myfeature origin/main
@@ -338,3 +397,30 @@ Presubmit flags
.. inclusive-language: enable
+.. _docs-contributing-presubmit-virtualenv-hashes:
+
+Updating Python dependencies in the virtualenv_setup directory
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+If you update any of the requirements or constraints files in
+``//pw_env_setup/py/pw_env_setup/virtualenv_setup``, you must run this command
+to ensure that all of the hashes are updated:
+
+.. code-block:: console
+
+ pw presubmit --step update_upstream_python_constraints --full
+
+For Python packages that have native extensions, the command needs to be run 3
+times: once on Linux, once on macOS, and once on Windows. Please run it on the
+OSes that are available to you; a core Pigweed teammate will run it on the rest.
+See the warning about caching Python packages for multiple platforms in
+:ref:`docs-python-build-downloading-packages`.
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ ../embedded_cpp_guide
+ ../style_guide
+ ../code_reviews
+ ../code_of_conduct
+ module_docs
diff --git a/docs/contributing/module_docs.rst b/docs/contributing/module_docs.rst
new file mode 100644
index 000000000..63476696f
--- /dev/null
+++ b/docs/contributing/module_docs.rst
@@ -0,0 +1,304 @@
+.. _docs-contrib-moduledocs:
+
+======================
+Module Docs Guidelines
+======================
+This page provides guidelines on how to write documentation for a single
+Pigweed module.
+
+These guidelines are a work-in-progress. Most of the guidelines are
+recommendations, not requirements, because we're still figuring out which
+guidelines are truly universal to all modules and which ones only work for
+certain types of modules.
+
+---------------------
+Choose your adventure
+---------------------
+Our guidance for you changes depending on the nature of the module you're
+documenting. This flowchart summarizes the decision path:
+
+.. mermaid::
+
+ flowchart TD
+ A[Start] --> B{Simple module or complex?}
+ B -->|Complex| C[Talk to the Pigweed team]
+ B -->|Simple| D{A little content or a lot?}
+ D -->|A little| E[Use the single-page approach]
+ D -->|A lot| F[Use the multi-page approach]
+
+.. _docs-contrib-moduledocs-moduletypes:
+
+-------------------------------------
+Simple modules versus complex modules
+-------------------------------------
+We're currently focused on updating the docs for "simple" modules. Here's our
+general definition for a simple module:
+
+* The module's API is end-user-facing or HAL-facing, not
+ infrastructure-facing.
+* The module does not use :ref:`facades <docs-glossary-facade>`.
+* The module's API surface is small or medium in size.
+* The module's API is in a single programming language.
+
+If your module doesn't meet these criteria, it's probably a "complex" module.
+`Create an issue <https://issues.pigweed.dev/issues/new>`_ before attempting
+to refactor a complex module's docs.
+
+.. note::
+
+ Why the focus on simple modules? We tried refactoring complex modules to
+ adhere to :ref:`SEED-0102 <seed-0102>`. The migrations were difficult and
+ the resulting docs weren't an obvious improvement. We learned that it's more
+ effective to focus on simple modules for now and take more time to figure out
+ what works for complex modules.
+
+Examples of simple modules:
+
+* :ref:`module-pw_string`
+* :ref:`module-pw_i2c`
+
+Examples of complex modules:
+
+* :ref:`module-pw_tokenizer`
+
+.. _docs-contrib-moduledocs-simple:
+
+------------------------
+Simple module guidelines
+------------------------
+Follow these guidelines if you're writing docs for a
+:ref:`simple module <docs-contrib-moduledocs-moduletypes>`.
+
+Single-page approach versus multi-page approach
+===============================================
+If your module meets the following criteria then you should *probably* use
+the :ref:`docs-contrib-moduledocs-singlepage`:
+
+* There is less than 1000 words of content in total.
+* The API has 2 classes or less.
+* The API has 10 methods or less.
+
+If your module doesn't meet all these criteria, then you should *probably*
+use the :ref:`docs-contrib-moduledocs-multipage`. As you can tell by our use of
+*probably*, this is just a soft guideline. E.g. if you have 2000 words of
+content but you feel strongly that the single-page approach is better for your
+module, then go for it!
+
+The content that you write mostly stays the same whether you use the single-page
+or multi-page approach. All modules must have
+:ref:`docs-contrib-moduledocs-sales` content for example. The only
+difference is that in the single-page approach this is the first *section* of
+content whereas in the multi-page approach it's the first *page* of content.
+
+.. _docs-contrib-moduledocs-singlepage:
+
+Single-page approach
+====================
+When using the single-page approach, this is the default ordering of
+sections in ``docs.rst``:
+
+* :ref:`docs-contrib-moduledocs-sales`
+* :ref:`docs-contrib-moduledocs-getstarted`
+* :ref:`docs-contrib-moduledocs-guides`
+* :ref:`docs-contrib-moduledocs-reference`
+* :ref:`docs-contrib-moduledocs-design`
+* :ref:`docs-contrib-moduledocs-roadmap`
+* :ref:`docs-contrib-moduledocs-size`
+
+The sales pitch must come first, followed by the getting started instructions.
+Everything else beyond that is optional. The sections can be re-arranged if
+you feel strongly about it, but we've found this is an intuitive ordering.
+
+The file must be located at ``//pw_<name>/docs.rst``, where ``<name>`` is
+replaced with the actual name of your module.
+
+.. _docs-contrib-moduledocs-multipage:
+
+Multi-page approach
+===================
+When using the multi-page approach, this is the default ordering of
+pages:
+
+.. list-table::
+ :header-rows: 1
+
+ * - Page Title
+ - Filename
+ - Description
+ * - ``pw_<name>``
+ - ``docs.rst``
+ - The :ref:`docs-contrib-moduledocs-sales` content.
+ * - ``Get Started & Guides``
+ - ``guides.rst``
+ - The :ref:`docs-contrib-moduledocs-getstarted` content followed by the
+ :ref:`docs-contrib-moduledocs-guides` content. See the note below.
+ * - ``API Reference``
+ - ``api.rst``
+ - The :ref:`docs-contrib-moduledocs-reference` content.
+ * - ``Design & Roadmap``
+ - ``design.rst``
+ - The :ref:`docs-contrib-moduledocs-design` content. See the note below.
+ * - ``Code Size Analysis``
+ - ``size.rst``
+ - The :ref:`docs-contrib-moduledocs-size` content.
+
+The sales pitch and getting started instructions are required. Everything else
+is optional. The sections can be re-arranged if you feel strongly about it,
+but we've found that this is an intuitive ordering.
+
+You can split ``Get Started & Guides`` into 2 docs if that works better for
+your module. The filenames should be ``get_started.rst`` and ``guides.rst``.
+
+``Design & Roadmap`` can also be split into 2 docs. The filenames should be
+``design.rst`` and ``roadmap.rst``.
+
+Nav cards
+---------
+When using the multi-page approach, every page in the set must link to every
+other page in the set. We recommend using the ``grid`` directive to create
+call-to-action buttons on the bottom of every page. See ``//pw_string/docs.rst``
+for an example.
+
+.. note::
+
+ We will eventually automate this. We know it's error-prone and tedious to
+ do this manually.
+
+------------------
+Content guidelines
+------------------
+The following sections provide instructions on how to write each content type.
+
+.. note::
+
+ We call them "content types" because in the
+ :ref:`docs-contrib-moduledocs-singlepage` each of these things represent a
+ section of content on ``docs.rst`` whereas in the
+ :ref:`docs-contrib-moduledocs-multipage` they might be an entire page of
+ content or a section within a page.
+
+.. _docs-contrib-moduledocs-sales:
+
+Sales pitch
+===========
+The sales pitch should:
+
+* Assume that the reader is an embedded developer.
+* Clearly explain how the reader's work as an embedded developer
+ will improve if they adopt the module.
+* Provide a code sample demonstrating one of the most important
+ problems the module solves. (Only required for modules that expose
+ an API.)
+
+Examples:
+
+* :ref:`module-pw_string`
+* :ref:`module-pw_tokenizer`
+
+.. _docs-contrib-moduledocs-getstarted:
+
+Get started
+===========
+The get started instructions should:
+
+* Show how to get set up in Bazel, GN, and CMake.
+* Present Bazel instructions first.
+* Clearly state when a build system isn't supported.
+* Format the instructions with the ``.. tab-set::`` directive. See
+ ``//pw_string/guide.rst`` for an example. The Bazel instructions are
+ presented in the first tab, the GN instructions in the next, and so on.
+* Demonstrate how to complete a common use case. See the next paragraph.
+
+If your get started content is on the same page as your guides, then the get
+started section doesn't need to demonstrate a common use case. The reader can
+just scroll down and see how to complete common tasks. If your get started
+content is a standalone page, it should demonstrate how to complete a common
+task. The reader shouldn't have to dig around multiple docs just to figure out
+how to do something useful with the module.
+
+Examples:
+
+* :ref:`module-pw_string-get-started` (pw_string)
+
+.. _docs-contrib-moduledocs-guides:
+
+Guides
+======
+The guides should:
+
+* Focus on how to solve real-world problems with the module. See
+ `About how-to guides <https://diataxis.fr/how-to-guides/>`_.
+
+Examples:
+
+* :ref:`module-pw_string-guide-stringbuilder`
+
+.. _docs-contrib-moduledocs-reference:
+
+API reference
+=============
+The API reference should:
+
+* Be auto-generated from :ref:`docs-pw-style-doxygen` (for C++ / C APIs) or
+ autodoc (for Python APIs).
+* Provide a code example demonstrating how to use class, at minimum. Consider
+ whether it's also helpful to provide more granular examples demonstrating
+ how to use each method, variable, etc.
+
+The typical approach is to order everything alphabetically. Some module docs
+group classes logically according to the tasks they're related to. We don't
+have a hard guideline here because we're not sure one of these approaches is
+universally better than the other.
+
+Examples:
+
+* :ref:`module-pw_string-api` (pw_string)
+* :ref:`module-pw_tokenizer-api` (pw_tokenizer)
+
+.. _docs-contrib-moduledocs-design:
+
+Design
+======
+The design content should:
+
+* Focus on `theory of operation <https://en.wikipedia.org/wiki/Theory_of_operation>`_
+ or `explanation <https://diataxis.fr/explanation/>`_.
+
+Examples:
+
+* :ref:`module-pw_string-design-inlinestring` (pw_string)
+
+.. _docs-contrib-moduledocs-roadmap:
+
+Roadmap
+=======
+The roadmap should:
+
+* Focus on things known to be missing today that could make sense in the
+ future. The reader should be encouraged to talk to the Pigweed team.
+
+The roadmap should not:
+
+* Make very definite guarantees that a particular feature will ship by a
+ certain date. You can get an exception if you really need to do this, but
+ it should be avoided in most cases.
+
+Examples:
+
+* :ref:`module-pw_string-roadmap` (pw_string)
+
+.. _docs-contrib-moduledocs-size:
+
+Size analysis
+=============
+The size analysis should:
+
+* Be auto-generated. See the ``pw_size_diff`` targets in ``//pw_string/BUILD.gn``
+ for examples.
+
+We elevate the size analysis to its own section or page because it's a very
+important consideration for many embedded developers.
+
+Examples:
+
+* :ref:`module-pw_string-size-reports` (pw_string)
diff --git a/docs/editors.rst b/docs/editors.rst
new file mode 100644
index 000000000..94ba0209f
--- /dev/null
+++ b/docs/editors.rst
@@ -0,0 +1,232 @@
+.. _docs-editors:
+
+-------------------
+Code Editor Support
+-------------------
+Pigweed projects can be (and have been!) successfully developed in simple text
+editors. However, if you want to take advantage of the latest code intelligence
+tools and IDE features, those can be configured to work great with Pigweed.
+
+We are building the ideal development experience for Pigweed projects using
+`Visual Studio Code <https://code.visualstudio.com/>`_, a powerful and highly
+extensible code editor. However, most of the features described in this doc are
+available to other popular editors; you may just need to do a little more
+configuration to make it work.
+
+Quick Start for Contributing to Pigweed
+=======================================
+If you're developing on upstream Pigweed, you can use Visual Studio Code, which
+is already configured to work best with Pigweed, or you can use another editor
+and configure it yourself.
+
+With Visual Studio Code
+------------------------
+There are three quick steps to get started with Pigweed in Visual Studio Code.
+
+1. Bootstrap your Pigweed environment (if you haven't already).
+
+2. Run ``gn gen out`` (if you haven't already).
+
+3. Run ``pw ide sync``
+
+4. Open the Pigweed repository in Visual Studio Code and install the recommended
+ plugins.
+
+You're done! Refer to the ``pw_ide`` documentation for
+:ref:`Visual Studio Code<module-pw_ide-vscode>` for more guidance.
+
+With Other Editors
+-------------------
+There are three short steps to get code intelligence in most editors for C++ and
+Python, the two most prevalent languages in Pigweed.
+
+1. Bootstrap your Pigweed environment (if you haven't already).
+
+2. Ensure that your Python plugin/language server is configured to use Pigweed's
+ Python virtual environment, located at
+ ``$PW_PROJECT_ROOT/environment/pigweed-venv``.
+
+3. Set up C++ code intelligence by running ``pw ide cpp --clangd-command`` to
+ generate the ``clangd`` invocation for your system, and configure your
+ language server to use it.
+
+Quick Start for Setting Up Projects Using Pigweed
+=================================================
+If you're developing on a downstream project using Pigweed, the instructions
+for upstream Pigweed may just work out of the box on your project. Give it a
+try!
+
+If it doesn't work, here are some common reasons why, and ways you can fix it
+for your project.
+
+It can't find any compilation databases
+---------------------------------------
+You need to generate at least one compilation database before running
+``pw ide sync``. Usually you will do this with your
+:ref:`build system<docs-editors-compdb>`.
+
+It isn't working for my toolchain
+---------------------------------
+You may need to specify additional `query drivers <https://releases.llvm.org/10.0.0/tools/clang/tools/extra/docs/clangd/Configuration.html#query-driver>`_
+to allow ``clangd`` to use your toolchains. Refer to the
+:ref:`pw_ide documentation<module-pw_ide-configuration>` for information on
+configuring this.
+
+Using Visual Studio Code
+========================
+
+Code Intelligence
+-----------------
+If you followed the quick start steps above, you're already set! Here's a
+non-exhaustive list of cool features you can now enjoy:
+
+* C/C++
+
+ * Code navigation, including routing through facades to the correct backend
+
+ * Code completion, including correct class members and function signatures
+
+ * Tool tips with docs, inferred types for ``auto``, inferred values for
+ ``constexpr``, data type sizes, etc.
+
+ * Compiler errors and warnings
+
+* Python
+
+ * Code navigation and code completion
+
+ * Tool tips with docs
+
+ * Errors, warnings, and linting feedback
+
+Integrated Terminal
+-------------------
+When launching the integrated terminal, it will automatically activate the
+Pigweed environment within that shell session.
+
+Configuration
+-------------
+See the :ref:`pw_ide docs<module-pw_ide-vscode>`.
+
+Code Intelligence Features for Specific Languages
+=================================================
+
+C/C++
+-----
+Pigweed projects have a few characteristics that make it challenging for some
+IDEs and language servers to support C/C++ code intelligence out of the box:
+
+* Pigweed projects generally don't use the default host toolchain.
+
+* Pigweed projects usually use multiple toolchains for separate targets.
+
+* Pigweed projects rely on the build system to define the relationship between
+ :ref:`facades and backends<docs-module-structure-facades>` for each target.
+
+We've found that the best solution is to use the
+`clangd <https://clangd.llvm.org/>`_ language server or alternative language
+servers that use the same
+`compilation database format <https://clang.llvm.org/docs/JSONCompilationDatabase.html>`_
+and comply with the language server protocol. We supplement that with Pigweed
+tools to produce target-specific compilation databases that work well with
+the language servers.
+
+.. _docs-editors-compdb:
+
+Producing a Compilation Database
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you're using GN, then ``gn gen out --export-compile-commands`` will output
+a compilation database (``compile_commands.json``) in the ``out`` directory
+along with all of the other GN build outputs (in other words, it produces the
+same output as ``gn gen out`` but *additionally* produces the compilation
+database).
+
+If you're using CMake, you can enable the ``CMAKE_EXPORT_COMPILE_COMMANDS``
+option to ensure a compilation database (``compile_commands.json``) is produced.
+This can be done either in your project's ``CMakeLists.txt`` (i.e.
+``set(CMAKE_EXPORT_COMPILE_COMMANDS ON)``), or by setting the flag when
+invoking CMake (i.e. ``-DCMAKE_EXPORT_COMPILE_COMMANDS=ON``).
+
+Bazel does not natively support generating compilation databases right now,
+though you may find third party extensions that provide this functionality.
+
+Processing the Compilation Database
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+If you examine a ``compile_commands.json`` file produced GN, you'll observe
+two things:
+
+* It contains multiple commands for each file, i.e. ``foo.cc`` will have
+ compile commands for multiple ``clang`` host builds, multiple ``gcc`` host
+ builds, multiple ``gcc-arm-none-eabi`` device builds, etc.
+
+* It contains many commands that are not actually valid compile commands,
+ because they involve targets that use Pigweed Python wrappers to do various
+ static analyses.
+
+Both of these confound ``clangd`` and will produce broken, unexpected, or
+inconsistent results when used for code intelligence. So the
+:ref:`pw_ide<module-pw_ide>` module provides CLI tools that process the "raw"
+compilation database produced by the build system into one or more "clean"
+compilation databases that will work smoothly with ``clangd``.
+
+Once you have a compilation database, run this command to process it:
+
+.. code-block:: bash
+
+ pw ide cpp --process <path to compilation database>
+
+Or better yet, just let ``pw_ide`` find any compilation databases you have
+in your build and process them:
+
+.. code-block:: bash
+
+ pw ide cpp --process
+
+If your ``compile_commands.json`` file *did not* come from GN, it may not
+exhibit any of these problems and therefore not require any processing.
+Nonetheless, you can still run ``pw ide cpp --process``; ``pw_ide`` will
+determine if the file needs processing or not.
+
+.. admonition:: Note
+ :class: warning
+
+ **How often do we need to process the compilation database?** With GN, if
+ you're just editing existing files, there's no need to reprocess the
+ compilation database. But any time the build changes (e.g. adding new source
+ files or new targets), you will need to reprocess the compilation database.
+
+ With CMake, it is usually not necessary to reprocess compilation databases,
+ since ``pw_ide`` will automatically pick up any changes that CMake makes.
+
+Setting the Target to Use for Analysis
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Discover which targets are available for code analysis:
+
+.. code-block::
+
+ $ pw ide cpp --list
+
+ C/C++ targets available for language server analysis:
+ pw_strict_host_gcc_debug
+ pw_strict_host_clang_debug
+ stm32f429i_disc1_debug
+
+Select the target you want to use for code analysis:
+
+.. code-block::
+
+ $ pw ide cpp --set pw_strict_host_gcc_debug
+
+ Set C/C++ langauge server analysis target to: pw_strict_host_gcc_debug
+
+Check which target is currently used for code analysis:
+
+.. code-block::
+
+ $ pw ide cpp
+
+ Current C/C++ language server analysis target: pw_strict_host_gcc_debug
+
+Your target selection will remain stable even after reprocessing the compilation
+database. Your editor configuration also remains stable; you don't need to
+change the editor configuration to change the target you're analyzing.
diff --git a/docs/embedded_cpp_guide.rst b/docs/embedded_cpp_guide.rst
index d5925b044..afe901a15 100644
--- a/docs/embedded_cpp_guide.rst
+++ b/docs/embedded_cpp_guide.rst
@@ -117,14 +117,14 @@ one tool to help identify and fix bugs early in development.
Pigweed compiles with a strict set of warnings. The warnings include the
following:
- * ``-Wall`` and ``-Wextra`` -- Standard sets of compilation warnings, which
- are recommended for all projects.
- * ``-Wimplicit-fallthrough`` -- Requires explicit ``[[fallthrough]]``
- annotations for fallthrough between switch cases. Prevents unintentional
- fallthroughs if a ``break`` or ``return`` is forgotten.
- * ``-Wundef`` -- Requires macros to be defined before using them. This
- disables the standard, problematic behavior that replaces undefined (or
- misspelled) macros with ``0``.
+* ``-Wall`` and ``-Wextra`` -- Standard sets of compilation warnings, which
+ are recommended for all projects.
+* ``-Wimplicit-fallthrough`` -- Requires explicit ``[[fallthrough]]``
+ annotations for fallthrough between switch cases. Prevents unintentional
+ fallthroughs if a ``break`` or ``return`` is forgotten.
+* ``-Wundef`` -- Requires macros to be defined before using them. This
+ disables the standard, problematic behavior that replaces undefined (or
+ misspelled) macros with ``0``.
Unused variable and function warnings
-------------------------------------
@@ -133,46 +133,46 @@ functions. Usually, the best way to address these warnings is to remove the
unused items. In some circumstances, these cannot be removed, so the warning
must be silenced. This is done in one of the following ways:
- 1. When possible, delete unused variables, functions, or class definitions.
- 2. If an unused entity must remain in the code, avoid giving it a name. A
- common situation that triggers unused parameter warnings is implementing a
- virtual function or callback. In C++, function parameters may be unnamed.
- If desired, the variable name can remain in the code as a comment.
+1. When possible, delete unused variables, functions, or class definitions.
+2. If an unused entity must remain in the code, avoid giving it a name. A
+ common situation that triggers unused parameter warnings is implementing a
+ virtual function or callback. In C++, function parameters may be unnamed.
+ If desired, the variable name can remain in the code as a comment.
- .. code-block:: cpp
+ .. code-block:: cpp
- class BaseCalculator {
- public:
- virtual int DoMath(int number_1, int number_2, int number_3) = 0;
- };
+ class BaseCalculator {
+ public:
+ virtual int DoMath(int number_1, int number_2, int number_3) = 0;
+ };
- class Calculator : public BaseCalculator {
- int DoMath(int number_1, int /* number_2 */, int) override {
- return number_1 * 100;
- }
- };
+ class Calculator : public BaseCalculator {
+ int DoMath(int number_1, int /* number_2 */, int) override {
+ return number_1 * 100;
+ }
+ };
- 3. In C++, annotate unused entities with `[[maybe_unused]]
- <https://en.cppreference.com/w/cpp/language/attributes/maybe_unused>`_ to
- silence warnings.
+3. In C++, annotate unused entities with `[[maybe_unused]]
+ <https://en.cppreference.com/w/cpp/language/attributes/maybe_unused>`_ to
+ silence warnings.
- .. code-block:: cpp
+ .. code-block:: cpp
- // This variable is unused in certain circumstances.
- [[maybe_unused]] int expected_size = size * 4;
- #if OPTION_1
- DoThing1(expected_size);
- #elif OPTION_2
- DoThing2(expected_size);
- #endif
+ // This variable is unused in certain circumstances.
+ [[maybe_unused]] int expected_size = size * 4;
+ #if OPTION_1
+ DoThing1(expected_size);
+ #elif OPTION_2
+ DoThing2(expected_size);
+ #endif
- 4. As a final option, cast unused variables to ``void`` to silence these
- warnings. Use ``static_cast<void>(unused_var)`` in C++ or
- ``(void)unused_var`` in C.
+4. As a final option, cast unused variables to ``void`` to silence these
+ warnings. Use ``static_cast<void>(unused_var)`` in C++ or
+ ``(void)unused_var`` in C.
- In C, silencing warnings on unused functions may require compiler-specific
- attributes (``__attribute__((unused))``). Avoid this by removing the
- functions or compiling with C++ and using ``[[maybe_unused]]``.
+ In C, silencing warnings on unused functions may require compiler-specific
+ attributes (``__attribute__((unused))``). Avoid this by removing the
+ functions or compiling with C++ and using ``[[maybe_unused]]``.
Dealing with ``nodiscard`` return values
----------------------------------------
@@ -183,8 +183,8 @@ used.
.. code-block:: cpp
- // <tuple> defines std::ignore.
- #include <tuple>
+ // <tuple> defines std::ignore.
+ #include <tuple>
- DoThingWithStatus().IgnoreError();
- std::ignore = DoThingWithReturnValue(); \ No newline at end of file
+ DoThingWithStatus().IgnoreError();
+ std::ignore = DoThingWithReturnValue();
diff --git a/docs/facades.rst b/docs/facades.rst
new file mode 100644
index 000000000..c067e3052
--- /dev/null
+++ b/docs/facades.rst
@@ -0,0 +1,153 @@
+.. _docs-facades:
+
+====================
+Facades and backends
+====================
+This page explains what "facades" and "backends" mean in the context of Pigweed
+and provides guidelines on when to use them.
+
+------------------------------
+What are facades and backends?
+------------------------------
+.. _top-down approach: https://en.wikipedia.org/wiki/Bottom%E2%80%93up_and_top%E2%80%93down_design
+
+Let's take a `top-down approach`_, starting with high-level definitions.
+In the context of a Pigweed module:
+
+* A facade is an API contract of a module that must be satisfied at compile-time,
+ i.e. a swappable dependency that changes the implementation of an API at
+ compile-time.
+* A backend is an implementation of a facade's contract.
+
+Usually, the facade and the backend are in different modules. One module
+exposes the facade contract, and another module is the backend that implements
+the facade contract. The naming pattern that Pigweed follows is
+``{facade_name}_{backend_name}``.
+
+Facades, by design, don't need to be configured until they're actually used.
+This makes it significantly easier to bring up features piece-by-piece.
+
+Example: pw_log and pw_log_string
+=================================
+Here's a step-by-step walkthrough of how a real backend module implements
+another module's facade.
+
+.. _//pw_log/public/pw_log/log.h: https://cs.opensource.google/pigweed/pigweed/+/main:pw_log/public/pw_log/log.h
+.. _//pw_log_string/public_overrides/pw_log_backend/log_backend.h: https://cs.opensource.google/pigweed/pigweed/+/main:pw_log_string/public_overrides/pw_log_backend/log_backend.h
+
+* :ref:`module-pw_log` is a module that exposes a facade. The macros listed in
+ :ref:`module-pw_log-macros` represent the API of the module.
+* The ``#include "pw_log/log_backend.h"`` line in `//pw_log/public/pw_log/log.h`_
+ represents the facade contract of ``pw_log``.
+* :ref:`module-pw_log_string` is a backend module, It implements the
+ ``pw_log/log_backend.h`` facade contract in
+ `//pw_log_string/public_overrides/pw_log_backend/log_backend.h`_.
+* In the build system there is a variable of some sort that specifies the
+ backend. In Bazel there's a `label
+ flag <https://bazel.build/extending/config#label-typed-build-settings>`_
+ at ``//targets:pw_log_backend``. In CMake there's a ``pw_log_BACKEND``
+ variable set to ``pw_log_string``. In GN there's a ``pw_log_BACKEND``
+ variable set to ``dir_pw_log_string``.
+
+.. note::
+
+ There are a few more steps needed to get ``pw_log_string`` hooked up as the
+ backend for ``pw_log`` but they aren't essential for the current discussion.
+ See :ref:`module-pw_log_string-get-started-gn` for the details.
+
+Example: Swappable OS libraries
+===============================
+The facade and backend system is similar to swappable OS libraries: you can
+write code against ``libc`` (for example) and it will work so long as you have
+an object to link against that has the symbols you're depending on. You can
+swap in different versions of ``libc`` and your code doesn't need to know about
+it.
+
+A similar example from Windows is ``d3d9.dll``. Developers often swap this DLL
+with a different library like ReShade to customize shading behavior.
+
+Can a module have multiple facades?
+===================================
+Yes. The module-to-facade relationship is one-to-many. A module can expose
+0, 1, or many facades. There can be (and usually are) multiple backend modules
+implementing the same facade.
+
+Is the module facade the same thing as its API?
+===============================================
+No. It's best to think of them as different concepts. The API is the interface
+that downstream projects interact with. There's always a one-to-one relationship
+between a module and its API. A facade represents the build system "glue" that
+binds a backend to an API.
+
+This is a common point of confusion because there are a few modules that
+blur this distinction. Historically, the facade comprised the API as well as
+the build system concept. We're moving away from this perspective.
+
+Are Pigweed facades the same as the GoF facade design pattern?
+==============================================================
+.. _facade pattern: https://en.wikipedia.org/wiki/Facade_pattern
+
+There are some loose similarities but it's best to think of them as different
+things. The goal of the Gang of Four `facade pattern`_ is to compress some
+ugly, complex API behind a much smaller API that is more aligned with your
+narrow business needs. The motivation behind some Pigweed facades is loosely
+the same: we don't want a device HAL to leak out into your include paths when
+a facade is implemented.
+
+------------------------------
+Why does Pigweed have facades?
+------------------------------
+Pigweed's facades are basically a pattern that builds off the ideas of
+`link-time subsitution <https://bramtertoolen.medium.com/91ffd4ef8687>`_.
+Link-time subsitution only allows you to replace one source file with another,
+whereas facades enable substituting program elements defined in *header files*.
+
+Pigweed facades enable implementation flexibility with zero performance cost.
+There are two ways to look at this. Continuing with the ``pw_log`` example:
+
+* Organizations can reuse more code across projects by adopting the ``pw_log``
+ API as their logging layer. Facades enable each project to customize how
+ its logs are handled.
+* For projects that want to integrate with ``pw_log`` but cannot adopt its
+ API, facades can wrap the existing API which enables Pigweed to be compatible
+ with the existing system.
+
+Two of the major aspects of "implementation flexibility" enabled by facades are:
+
+* Portability. For example, ``pw_sync`` needs platform-specific
+ implementations to work correctly.
+* Customized behavior. For example, you can make a fully portable ``pw_log``
+ implementation, but what makes it special is the ability to tune it to your
+ needs.
+
+Why compile-time?
+=================
+Resolving facades and backends at compile-time enables:
+
+* Call-site control from backends.
+* Static allocation of backend-provided types.
+* Explicit backend includes so it’s visually obvious you’re poking through
+ abstraction.
+
+--------------------------------
+When to use facades and backends
+--------------------------------
+If you're trying to use a Pigweed module, and that module exposes a facade,
+then you've got no choice: you've got to hook up a backend to fulfill that
+facade contract or else the module won't work.
+
+When to roll your own facades and backends
+==========================================
+* You need a global function or macro.
+* You absolutely must avoid the overhead of virtual functions.
+
+When to NOT roll your own facades and backends
+==============================================
+* If you can afford the runtime cost of dependency injection, use that.
+ In all other cases where link-time subsitution will work, use that.
+ Only if the API contract requires a backend to provide a header (which
+ link-time substitution doesn't let you do) should you reach for a facde.
+* You're trying to use globals to avoid dependency injection. Use
+ the dependency injection! It makes testing much easier.
+* Your needs can be served by a standard mechanism like virtual interfaces.
+ Use the standard mechanism.
diff --git a/docs/faq.rst b/docs/faq.rst
index ab73f90ed..c3e0d11b8 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -51,15 +51,18 @@ the Pigweed integrated environment and build, or just use individual modules?
A la carte: Individual modules only
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-This is the best option if you have an existing project, with pre-existing build
-in place.
+This option can work for small projects with a pre-existing build system in place.
+Large projects will probably need a more maintainable and scalable solution.
To use the libraries, submodule or copy the relevant Pigweed modules into your
-project, and use them like any other C++ library. You can reference the
-existing GN files or CMake files when doing this. In the case of CMake, you can
+project, and use them like any other C++ library. You'll need to also copy over the
+`transitive dependencies`_ of any module you use. You can find a module's transitive
+dependencies by inspecting its build files. If your project uses CMake, you can
directly import Pigweed's build from your project with CMake's external project
system, or just use a CMake include statement.
+.. _transitive dependencies: https://en.wikipedia.org/wiki/Transitive_dependency#Computer_programs
+
Monolith: Using the integrated system
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This may be a good option if you are starting a new project. However,
diff --git a/docs/get_started/bazel.rst b/docs/get_started/bazel.rst
new file mode 100644
index 000000000..68ba18821
--- /dev/null
+++ b/docs/get_started/bazel.rst
@@ -0,0 +1,217 @@
+.. _docs-get-started-bazel:
+
+==================================
+Get Started With Pigweed And Bazel
+==================================
+This guide provides a starting point for using Pigweed in a Bazel-based project.
+Bazel is :ref:`the recommended build system <seed-0111>` for new projects using
+Pigweed.
+
+-----------
+Limitations
+-----------
+.. TODO: b/306393519 - Update the MacOS description once that path is verified.
+
+* **Linux**. Your development host must be running Linux. Bazel-based Pigweed
+ projects are not yet supported on Windows. MacOS probably works but is
+ unverified.
+
+-----
+Setup
+-----
+#. `Install Bazel <https://bazel.build/install>`_.
+
+ .. tip::
+
+ If you want to minimize system-wide installations, first install
+ `Node Version Manager <https://github.com/nvm-sh/nvm>`_ and then
+ install Bazelisk through NPM using ``npm install -g @bazel/bazelisk``.
+ If you use this workflow, remember that Bazel will only be available
+ in the version of Node that's currently activated through NVM.
+
+#. Clone `the project <https://pigweed.googlesource.com/example/echo/+/refs/heads/main>`_:
+
+ .. code-block:: console
+
+ $ git clone --recursive https://pigweed.googlesource.com/example/echo
+
+ .. tip::
+
+ If you forgot the ``--recursive`` flag when cloning the code, run
+ ``git submodule update --init``.
+
+All subsequent commands that you see in this guide should be run from the
+root directory of your new ``echo`` repo.
+
+-----------------
+Build the project
+-----------------
+#. Build the project for :ref:`target-host` and
+ :ref:`target-stm32f429i-disc1`:
+
+ .. code-block:: console
+
+ $ bazel build //...
+
+ You should see output like this:
+
+ .. code-block:: none
+
+ Starting local Bazel server and connecting to it...
+ INFO: Analyzed 7 targets (101 packages loaded, 17371 targets configured).
+ INFO: Found 7 targets...
+ INFO: From Linking src/echo:
+ /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/bin/ld: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-closer.o): in function `_close_r':
+ closer.c:(.text._close_r+0xc): warning: _close is not implemented and will always fail
+ /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/bin/ld: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-signalr.o): in function `_getpid_r':
+ signalr.c:(.text._getpid_r+0x0): warning: _getpid is not implemented and will always fail
+ /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/bin/ld: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-signalr.o): in function `_kill_r':
+ signalr.c:(.text._kill_r+0xe): warning: _kill is not implemented and will always fail
+ /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/bin/ld: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-lseekr.o): in function `_lseek_r':
+ lseekr.c:(.text._lseek_r+0x10): warning: _lseek is not implemented and will always fail
+ /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/bin/ld: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-readr.o): in function `_read_r':
+ readr.c:(.text._read_r+0x10): warning: _read is not implemented and will always fail
+ /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/bin/ld: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/gcc_arm_none_eabi_toolchain/bin/../lib/gcc/arm-none-eabi/12.2.1/../../../../arm-none-eabi/lib/thumb/v7e-m/nofp/libc_nano.a(libc_a-writer.o): in function `_write_r':
+ writer.c:(.text._write_r+0x10): warning: _write is not implemented and will always fail
+ INFO: Elapsed time: 6.716s, Critical Path: 1.98s
+ INFO: 40 processes: 3 internal, 37 linux-sandbox.
+ INFO: Build completed successfully, 40 total actions
+
+-----------------------
+Run the project locally
+-----------------------
+#. Run the project locally on your Linux development host:
+
+ .. code-block:: console
+
+ bazel run //src:echo
+
+ You should see output like this:
+
+ .. code-block:: none
+
+ INFO: Analyzed target //src:echo (36 packages loaded, 202 targets configured).
+ INFO: Found 1 target...
+ Target //src:echo up-to-date:
+ bazel-bin/src/echo
+ INFO: Elapsed time: 0.899s, Critical Path: 0.03s
+ INFO: 1 process: 1 internal.
+ INFO: Build completed successfully, 1 total action
+ INFO: Running command line: bazel-bin/src/echo
+
+#. Press ``Ctrl`` + ``C`` to stop running the project.
+
+----------------------------------------
+Flash the project onto a Discovery board
+----------------------------------------
+If you have an `STM32F429 Discovery <https://www.st.com/stm32f4-discover>`_
+board, you can run the project on that hardware.
+
+.. note::
+
+ You don't need this hardware to run the project. Because this project
+ supports the :ref:`target-host` target, you can run everything
+ on your Linux development host.
+
+#. Ensure your udev rules are set up to allow the user running the commands
+ below to access the Discovery Board. For example, you may want to add the
+ following rule as ``/etc/udev/rules.d/99-stm32f329i-disc1.rules``:
+
+ .. code-block:: console
+
+ ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", MODE="664", GROUP="plugdev"
+
+ The user running the commands needs to be in the group ``plugdev``.
+
+#. Connect the Discovery board to your development host with a USB
+ cable. **Use the Mini-B USB port on the Discovery board, not the
+ Micro-B port**.
+
+#. Flash the project to the Discovery board:
+
+ .. code-block:: console
+
+ $ bazel run //tools:flash
+
+ You should see output like this:
+
+ .. code-block:: none
+
+ INFO: Analyzed target //tools:flash (52 packages loaded, 2760 targets configured).
+ INFO: Found 1 target...
+ Target //tools:flash up-to-date:
+ bazel-bin/tools/flash
+ INFO: Elapsed time: 0.559s, Critical Path: 0.04s
+ INFO: 1 process: 1 internal.
+ INFO: Build completed successfully, 1 total action
+ INFO: Running command line: bazel-bin/tools/flash
+ binary Rlocation is: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/execroot/__main__/bazel-out/k8-fastbuild/bin/src/echo.elf
+ openocd Rlocation is: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/openocd/bin/openocd
+ openocd config Rlocation is: /home/xyz/.cache/bazel/_bazel_xyz/8c700b5cf88b83b789ceaf0e4e271fac/external/pigweed/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg
+ xPack OpenOCD x86_64 Open On-Chip Debugger 0.11.0+dev (2021-12-07-17:30)
+ Licensed under GNU GPL v2
+ For bug reports, read
+ http://openocd.org/doc/doxygen/bugs.html
+ DEPRECATED! use 'adapter driver' not 'interface'
+ DEPRECATED! use 'adapter serial' not 'hla_serial'
+ Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
+ srst_only separate srst_nogate srst_open_drain connect_deassert_srst
+
+ Info : clock speed 2000 kHz
+ Info : STLINK V2J25M14 (API v2) VID:PID 0483:374B
+ Info : Target voltage: 2.837377
+ Info : stm32f4x.cpu: Cortex-M4 r0p1 processor detected
+ Info : stm32f4x.cpu: target has 6 breakpoints, 4 watchpoints
+ Info : gdb port disabled
+ Info : Unable to match requested speed 2000 kHz, using 1800 kHz
+ Info : Unable to match requested speed 2000 kHz, using 1800 kHz
+ target halted due to debug-request, current mode: Thread
+ xPSR: 0x01000000 pc: 0x08000708 msp: 0x20030000
+ Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+ Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+ ** Programming Started **
+ Info : device id = 0x20016419
+ Info : flash size = 2048 kbytes
+ Info : Dual Bank 2048 kiB STM32F42x/43x/469/479 found
+ Info : Padding image section 0 at 0x08000010 with 496 bytes
+ ** Programming Finished **
+ ** Resetting Target **
+ Info : Unable to match requested speed 2000 kHz, using 1800 kHz
+ Info : Unable to match requested speed 2000 kHz, using 1800 kHz
+ shutdown command invoked
+
+
+Communicate with the project over serial
+========================================
+After you've flashed the project onto your Discovery board, your Linux development
+host can communicate with the project over a serial terminal like ``miniterm``.
+
+#. Transmit and receive characters:
+
+ .. code-block:: console
+
+ $ bazel run //tools:miniterm -- /dev/ttyACM0 --filter=debug
+
+ After typing ``hello`` and pressing ``Ctrl`` + ``]`` to exit you should see output
+ like this:
+
+ .. code-block:: none
+
+ INFO: Analyzed target //tools:miniterm (41 packages loaded, 2612 targets configured).
+ INFO: Found 1 target...
+ Target //tools:miniterm up-to-date:
+ bazel-bin/tools/miniterm
+ INFO: Elapsed time: 0.373s, Critical Path: 0.02s
+ INFO: 1 process: 1 internal.
+ INFO: Build completed successfully, 1 total action
+ INFO: Running command line: bazel-bin/tools/miniterm /dev/ttyACM0 '--filter=debug'
+ --- Miniterm on /dev/ttyACM0 115200,8,N,1 ---
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
+ [TX:'h'] [RX:'h'] h [TX:'e'] [RX:'e'] e [TX:'l'] [RX:'l'] l [TX:'l'] [RX:'l'] l [TX:'o'] [RX:'o'] o
+ --- exit ---
+
+------------------------------
+Questions? Comments? Feedback?
+------------------------------
+Please join `our Discord <https://discord.com/invite/M9NSeTA>`_ and talk to us
+in the ``#bazel-build`` channel or `file a bug <https://issues.pigweed.dev>`_.
diff --git a/docs/get_started/index.rst b/docs/get_started/index.rst
new file mode 100644
index 000000000..5dc484aef
--- /dev/null
+++ b/docs/get_started/index.rst
@@ -0,0 +1,57 @@
+.. _docs-get-started:
+
+===========
+Get Started
+===========
+.. grid:: 1
+
+ .. grid-item-card:: :octicon:`rocket` Bazel
+ :link: docs-get-started-bazel
+ :link-type: ref
+ :class-item: sales-pitch-cta-primary
+
+ Fork our minimal, Bazel-based starter code. Bazel is the recommended
+ build system for new projects using Pigweed.
+
+.. grid:: 2
+
+ .. grid-item-card:: :octicon:`code` Sample Project
+ :link: https://pigweed.googlesource.com/pigweed/sample_project/+/main/README.md
+ :link-type: url
+ :class-item: sales-pitch-cta-secondary
+
+ Fork the Sample Project, a repository that outlines the recommended way to use
+ Pigweed in a broader GN-based project. Note that Bazel is the recommended
+ build system for new projects using Pigweed, whereas this sample project uses
+ GN.
+
+ .. grid-item-card:: :octicon:`code` Kudzu
+ :link: docs-kudzu
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ Study the code of Kudzu, a just-for-fun Maker Faire 2023 project that
+ demonstrates complex Pigweed usage. This project also uses GN.
+
+.. grid:: 2
+
+ .. grid-item-card:: :octicon:`list-ordered` Zephyr
+ :link: docs-os-zephyr-get-started
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ Instructions on how to use Pigweed in a Zephyr-based project.
+
+ .. grid-item-card:: :octicon:`list-ordered` Upstream Pigweed
+ :link: docs-get-started-upstream
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ Get set up to contribute to upstream Pigweed.
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ Bazel <bazel>
+ Upstream Pigweed <upstream>
diff --git a/docs/getting_started.rst b/docs/get_started/upstream.rst
index 54d503637..136443048 100644
--- a/docs/getting_started.rst
+++ b/docs/get_started/upstream.rst
@@ -1,8 +1,8 @@
-.. _docs-getting-started:
+.. _docs-get-started-upstream:
-===============
-Getting Started
-===============
+=============================================
+Get Started With Upstream Pigweed Development
+=============================================
This guide will walk you through the typical upstream development workflow.
.. note::
@@ -36,7 +36,7 @@ To get setup:
#. Clone Pigweed and bootstrap the environment (compiler setup & more). **Be
patient, this step downloads ~1GB of LLVM, GCC, and other tooling**.
- .. code:: bash
+ .. code-block:: bash
$ cd ~
$ git clone https://pigweed.googlesource.com/pigweed/pigweed
@@ -48,14 +48,14 @@ To get setup:
#. Configure the GN build.
- .. code:: bash
+ .. code-block:: bash
$ gn gen out
Done. Made 1047 targets from 91 files in 114ms
#. Start the watcher. The watcher will invoke Ninja to build all the targets
- .. code:: bash
+ .. code-block:: bash
$ pw watch
@@ -118,25 +118,13 @@ that includes many popular hardware debuggers.
To start using Pigweed on MacOS, you'll need to install XCode. Download it
via the App Store, then install the relevant tools from the command line.
-.. code:: none
+.. code-block:: none
$ xcode-select --install
-On macOS you may get SSL certificate errors with the system Python
-installation. Run ``/Applications/Python <default_py_version>/Install Certificates.command``
-to fix this. If you get SSL
-errors with the Python from `Homebrew <https://brew.sh>`_ try running the
-following commands to ensure Python knows how to use OpenSSL.
-
-.. code:: none
-
- $ brew install openssl
- $ brew uninstall python
- $ brew install python
-
-To flash firmware to a STM32 Discovery development board (and run ``pw test``)
-from macOS, you will need to install OpenOCD. Install
-`Homebrew <https://brew.sh>`_, then install OpenOCD with ``brew install openocd``.
+If you get SSL certificate errors, try installing
+`Python 3.11.3 <https://www.python.org/downloads/release/python-3113/>`
+and then running ``/Applications/Python\ 3.11/Install\ Certificates.command``.
**Windows**
@@ -149,6 +137,13 @@ To start using Pigweed on Windows, you'll need to do the following:
* Ensure that `Developer Mode
<https://docs.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development>`_
is enabled.
+* Enable long file paths. This can be done using ``regedit`` or by running the
+ following command as an administrator:
+
+ .. code-block:: bat
+
+ REG ADD HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem /v LongPathsEnabled /t REG_DWORD /d 1 /f
+
If you plan to flash devices with firmware, you'll need to install OpenOCD and
ensure it's on your system path.
@@ -162,7 +157,7 @@ bootstrap may take several minutes to complete, so please be patient.
**Linux & macOS**
-.. code:: bash
+.. code-block:: bash
$ git clone https://pigweed.googlesource.com/pigweed/pigweed ~/pigweed
$ cd ~/pigweed
@@ -170,7 +165,7 @@ bootstrap may take several minutes to complete, so please be patient.
**Windows**
-.. code:: batch
+.. code-block:: batch
:: Run git commands from the shell you set up to use with Git during install.
> git clone https://pigweed.googlesource.com/pigweed/pigweed %HOMEPATH%\pigweed
@@ -179,7 +174,7 @@ bootstrap may take several minutes to complete, so please be patient.
Below is a real-time demo with roughly what you should expect to see as output:
-.. image:: images/pw_env_setup_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_env_setup_demo.gif
:width: 800
:alt: build example using pw watch
@@ -196,13 +191,13 @@ environment in a new session with the following command:
**Linux & macOS**
-.. code:: bash
+.. code-block:: bash
$ source ./activate.sh
**Windows**
-.. code:: batch
+.. code-block:: batch
> activate.bat
@@ -224,13 +219,24 @@ invocation.
Run GN as seen below:
-.. code:: bash
+.. code-block:: bash
$ gn gen out
-Note that ``out`` is simply the directory the build files are saved to. Unless
-this directory is deleted or you desire to do a clean build, there's no need to
-run GN again; just rebuild using Ninja directly.
+.. note::
+ ``out`` is simply the directory the build files are saved to. Unless
+ this directory is deleted or you desire to do a clean build, there's no need
+ to run GN again; just rebuild using Ninja directly.
+
+.. warning::
+ Unless your build directory (the ``out`` in ``gn gen out``) is exactly one
+ directory away from the project root directory (the Pigweed repo root in this
+ case), there will be issues finding source files while debugging and while
+ generating coverage reports. This is due an issue in upstream LLVM reordering
+ debug and coverage path mappings (tracked by
+ `b/278898014 <https://issuetracker.google.com/278898014>`_ and
+ `b/278906020 <https://issuetracker.google.com/278906020>`_). **We recommend
+ sticking to simple, single directory build directories for the time being.**
Now that we have build files, it's time to build Pigweed!
@@ -239,7 +245,7 @@ time you make a change, but that's tedious. Instead, let's use ``pw_watch``.
Go ahead and start ``pw_watch``:
-.. code:: bash
+.. code-block:: bash
$ pw watch
@@ -254,7 +260,7 @@ save the file.
See below for a demo of this in action:
-.. image:: images/pw_watch_build_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_watch_build_demo.gif
:width: 800
:alt: build example using pw watch
@@ -268,7 +274,7 @@ failure.
To see a test failure, modify ``pw_status/status_test.cc`` to fail by changing
one of the strings in the "KnownString" test.
-.. image:: images/pw_watch_test_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_watch_test_demo.gif
:width: 800
:alt: example test failure using pw watch
@@ -279,7 +285,7 @@ will be re-built and re-run.
Try running the ``pw_status`` test manually:
-.. code:: bash
+.. code-block:: bash
$ ./out/pw_strict_host_{clang,gcc}_debug/obj/pw_status/test/status_test
@@ -296,13 +302,13 @@ build!
If you want to build JUST for the device, you can kick of watch with:
-.. code:: bash
+.. code-block:: bash
$ pw watch stm32f429i
This is equivalent to the following Ninja invocation:
-.. code:: bash
+.. code-block:: bash
$ ninja -C out stm32f429i
@@ -324,7 +330,7 @@ updates to ensure you have permissions to use the USB device. For example, on
Linux you may need to update your udev rules and ensure you're in the plugdev
and dialout groups.
-.. image:: images/stm32f429i-disc1_connected.jpg
+.. image:: https://storage.googleapis.com/pigweed-media/stm32f429i-disc1_connected.jpg
:width: 800
:alt: development boards connected via USB
@@ -335,7 +341,7 @@ test requests to a server running in the background. Launch the server in
another window using the command below (remember, you'll need to activate the
Pigweed environment first).
-.. code:: bash
+.. code-block:: bash
$ stm32f429i_disc1_test_server
@@ -347,18 +353,13 @@ need to relaunch this server.
Tell GN to use the testing server by enabling a build arg specific to the
stm32f429i-disc1 target.
-.. code:: bash
+.. code-block:: bash
$ gn args out
# Append this line to the file that opens in your editor to tell GN to run
# on-device unit tests.
pw_use_test_server = true
-**Note:** There are several additional dependencies required to test on device:
-libusb-compat, libftdi, and hidapi at the time of writing. On MacOS, these
-dependencies should be installed to the default homebrew location:
-``/usr/local/opt/``.
-
Done!
-----
Whenever you make code changes and trigger a build, all the affected unit tests
@@ -366,7 +367,7 @@ will be run across the attached boards!
See the demo below for an example of what this all looks like put together:
-.. image:: images/pw_watch_on_device_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_watch_on_device_demo.gif
:width: 800
:alt: pw watch running on-device tests
@@ -381,7 +382,7 @@ turn out. Once built, you can find the rendered HTML documentation at
You can explicitly build just the documentation with the command below.
-.. code:: bash
+.. code-block:: bash
$ ninja -C out docs
@@ -397,7 +398,7 @@ specify which target to build the test under (e.g. host, SM32F529I-DISC1).
This can be done by appending the GN path to the target toolchain in parenthesis
after the desired GN build step label as seen in the example below.
-.. code:: none
+.. code-block:: none
$ gn outputs out "//pw_status:status_test.run(//targets/host/pigweed_internal:pw_strict_host_clang_debug)"
pw_strict_host_clang_debug/obj/pw_status/status_test.run.pw_pystamp
@@ -412,13 +413,22 @@ not attempt to run.
In macOS and Linux, ``xargs`` can be used to turn this into a single command:
-.. code:: bash
+.. code-block:: bash
$ gn outputs out "//pw_status:status_test.run(//targets/host/pigweed_internal:pw_strict_host_clang_debug)" | xargs ninja -C out
Next steps
==========
+Editor setup
+-----------------------
+Check out the :ref:`module-pw_ide` for setting up editor configurations or run
+the following for a quick setup:
+
+.. code-block:: bash
+
+ pw ide sync
+
Check out other modules
-----------------------
If you'd like to see more of what Pigweed has to offer, dive into the
diff --git a/docs/glossary.rst b/docs/glossary.rst
new file mode 100644
index 000000000..a486a9b5e
--- /dev/null
+++ b/docs/glossary.rst
@@ -0,0 +1,52 @@
+.. _docs-glossary:
+
+========
+Glossary
+========
+This glossary defines terms that have specific meanings in the context of
+Pigweed.
+
+.. _docs-glossary-facade:
+
+------
+Facade
+------
+A facade is an API contract of a module that must be satisfied at compile-time,
+i.e. a swappable dependency that changes the implementation of an API at
+compile-time.
+
+Learn more:
+
+* :ref:`docs-facades`
+
+.. _docs-glossary-module:
+
+------
+Module
+------
+A Pigweed module is an open source library that addresses a common need for
+embedded software developers. For example, :ref:`module-pw_string` provides
+an API for string operations that is both safe and suitable for
+resource-constrained embedded systems.
+
+Modules are Pigweed's core products. Every directory that starts with ``pw_``
+in the `root directory of the upstream Pigweed repository
+<https://cs.opensource.google/pigweed/pigweed>`_ represents a single module.
+
+Modules are modular in the sense that you can use one module in your project
+without always needing to depend on the rest of the entire Pigweed codebase.
+Some modules may depend on other modules, however. The key idea of their
+modularity is that they're not tied to any "core" or platform layer.
+
+Other general rules about modules:
+
+* They strive to minimize policy decisions such as whether or not allocation
+ is required, buffer sizing limits, etc. Projects have control over these
+ decisions.
+* They don't instantiate any static objects.
+
+Learn more:
+
+* :ref:`List of modules <docs-module-guides>`
+* :ref:`docs-module-structure`
+* :ref:`docs-concepts-embedded-development-libraries`
diff --git a/docs/images/pw_env_setup_demo.gif b/docs/images/pw_env_setup_demo.gif
deleted file mode 100644
index c8942ac3b..000000000
--- a/docs/images/pw_env_setup_demo.gif
+++ /dev/null
Binary files differ
diff --git a/docs/images/pw_status_test.png b/docs/images/pw_status_test.png
deleted file mode 100644
index 336abff3e..000000000
--- a/docs/images/pw_status_test.png
+++ /dev/null
Binary files differ
diff --git a/docs/images/pw_watch_build_demo.gif b/docs/images/pw_watch_build_demo.gif
deleted file mode 100644
index 877d84931..000000000
--- a/docs/images/pw_watch_build_demo.gif
+++ /dev/null
Binary files differ
diff --git a/docs/images/pw_watch_on_device_demo.gif b/docs/images/pw_watch_on_device_demo.gif
deleted file mode 100644
index 68fa11898..000000000
--- a/docs/images/pw_watch_on_device_demo.gif
+++ /dev/null
Binary files differ
diff --git a/docs/images/pw_watch_test_demo.gif b/docs/images/pw_watch_test_demo.gif
deleted file mode 100644
index 83a6a0b85..000000000
--- a/docs/images/pw_watch_test_demo.gif
+++ /dev/null
Binary files differ
diff --git a/docs/images/pw_watch_test_demo2.gif b/docs/images/pw_watch_test_demo2.gif
deleted file mode 100644
index e9e9c65bc..000000000
--- a/docs/images/pw_watch_test_demo2.gif
+++ /dev/null
Binary files differ
diff --git a/docs/images/stm32f429i-disc1_connected.jpg b/docs/images/stm32f429i-disc1_connected.jpg
deleted file mode 100644
index 37643b56f..000000000
--- a/docs/images/stm32f429i-disc1_connected.jpg
+++ /dev/null
Binary files differ
diff --git a/docs/index.rst b/docs/index.rst
index 98e7805e8..eb3b74644 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,44 +1,45 @@
.. _docs-root:
.. highlight:: sh
-.. TODO(b/256680603) Remove query string from issue tracker link.
+.. TODO: b/256680603 - Remove query string from issue tracker link.
.. toctree::
:maxdepth: 1
:hidden:
Home <self>
- docs/getting_started
+ docs/overview
+ docs/get_started/index
docs/concepts/index
- module_guides
- docs/release_notes/index
+ targets
+ Modules <module_guides>
+ docs/module_structure
+ changelog
Mailing List <https://groups.google.com/forum/#!forum/pigweed>
Chat Room <https://discord.gg/M9NSeTA>
- docs/os_abstraction_layers
+ docs/os/index
docs/size_optimizations
- FAQ <docs/faq>
+ Code Editor Support <docs/editors>
third_party_support
Source Code <https://cs.pigweed.dev/pigweed>
Code Reviews <https://pigweed-review.googlesource.com>
Issue Tracker <https://issues.pigweed.dev/issues?q=status:open>
- docs/contributing
- docs/code_of_conduct
- docs/embedded_cpp_guide
- Style Guide <docs/style_guide>
+ docs/contributing/index
+ docs/infra/index
Automated Analysis <automated_analysis>
- targets
Build System <build_system>
SEEDs <seed/0000-index>
- docs/module_structure
+ kudzu/docs
+ Eng Blog <docs/blog/index>
=======
Pigweed
=======
Pigweed is an open source collection of embedded-targeted libraries--or as we
-like to call them, modules. These modules are building blocks and
-infrastructure that enable faster and more reliable development on
-small-footprint MMU-less 32-bit microcontrollers like the STMicroelectronics
-STM32L452 or the Nordic nRF52832.
+like to call them, :ref:`modules <docs-glossary-module>`. These modules are
+building blocks and infrastructure that enable faster and more reliable
+development on small-footprint MMU-less 32-bit microcontrollers like the
+STMicroelectronics STM32L452 or the Nordic nRF52832.
.. attention::
@@ -47,15 +48,29 @@ STM32L452 or the Nordic nRF52832.
in our `chat room <https://discord.gg/M9NSeTA>`_ or on the `mailing list
<https://groups.google.com/forum/#!forum/pigweed>`_.
+--------------------------
+Talk to us at Pigweed Live
+--------------------------
+.. pigweed-live::
+
+---------------------
+What's New In Pigweed
+---------------------
+.. include:: changelog.rst
+ :start-after: .. changelog_highlights_start
+ :end-before: .. changelog_highlights_end
+
+See :ref:`docs-changelog-latest` in our changelog for details.
+
+---------------
Getting Started
---------------
-Check out `Pigweed Sample Project <https://pigweed.googlesource.com/pigweed/sample_project/+/main/README.md>`_
-to see how to use Pigweed as a library in your broader project.
-
-Visit the :ref:`docs-getting-started` guide to learn how to bootstrap and
-activate a Pigweed environment, build Pigweed for a specific host or device,
-run tests, and more.
+Check out our :ref:`docs-get-started` landing page. We've got a guide that
+shows you how to use Pigweed in a new, Bazel-based project (the recommended
+path), sample code for GN-based projects, a tutorial on getting set up to
+contribute to upstream Pigweed, and more.
+------------------------
What does Pigweed offer?
------------------------
There are many modules in Pigweed; this section showcases a selection that
@@ -63,7 +78,7 @@ produce visual output. For more information about the different Pigweed module
offerings, refer to :ref:`docs-module-guides` section.
``pw_watch`` - Build, flash, run, & test on save
-------------------------------------------------
+================================================
In the web development space, file system watchers are prevalent. These
watchers trigger a web server reload on source change, making development much
faster. In the embedded space, file system watchers are less prevalent;
@@ -78,12 +93,12 @@ and verifying the test runs as expected. Once this is set up, you can attach
multiple devices to run tests in a distributed manner to reduce the time it
takes to run tests.
-.. image:: docs/images/pw_watch_on_device_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_watch_on_device_demo.gif
:width: 800
:alt: pw watch running on-device tests
``pw_presubmit`` - Vacuum lint on every commit
-----------------------------------------------
+==============================================
Presubmit checks are essential tools, but they take work to set up, and
projects don’t always get around to it. The :ref:`module-pw_presubmit` module
provides tools for setting up high quality presubmit checks for any project. We
@@ -96,12 +111,12 @@ With ``pw format``, you can format C, C++, Python, GN, and Go code according to
configurations defined by your project. ``pw format`` leverages existing tools
like ``clang-format``, and it’s simple to add support for new languages.
-.. image:: pw_presubmit/docs/pw_presubmit_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_presubmit_demo.gif
:width: 800
:alt: pw presubmit demo
``pw_env_setup`` - Cross platform embedded compiler setup
----------------------------------------------------------
+=========================================================
A classic problem in the embedded space is reducing the **time from git clone
to having a binary executing on a device**. An entire suite of tools is needed
for building non-trivial production embedded projects, and need to be
@@ -127,12 +142,12 @@ turn inflates a virtual environment. The tooling is installed into your
workspace, and makes no changes to your system. This tooling is designed to be
reused by any project.
-.. image:: docs/images/pw_env_setup_demo.gif
+.. image:: https://storage.googleapis.com/pigweed-media/pw_env_setup_demo.gif
:width: 800
:alt: pw environment setup demo
``pw_unit_test`` - Embedded testing for MCUs
---------------------------------------------
+============================================
Unit testing is important, and Pigweed offers a portable library that’s broadly
compatible with `Google Test <https://github.com/google/googletest>`_. Unlike
Google Test, :ref:`module-pw_unit_test` is built on top of embedded friendly
@@ -147,34 +162,66 @@ build, the result is a flexible and powerful setup that enables easily
developing code on your desktop (with tests), then running the same tests
on-device.
-.. image:: docs/images/pw_status_test.png
+.. image:: https://storage.googleapis.com/pigweed-media/pw_status_test.png
:width: 800
:alt: pw_status test run natively on host
And more!
----------
+=========
Here is a selection of interesting modules:
-- :ref:`module-pw_cpu_exception_cortex_m` - Robust low level hardware fault
- handler for ARM Cortex-M; the handler even has unit tests written in assembly
- to verify nested-hardware-fault handling!
+.. grid:: 3
+
+ .. grid-item-card:: :octicon:`cpu` pw_cpu_exception
+ :link: module-pw_cpu_exception_cortex_m
+ :link-type: ref
+
+ An interface for entering CPU exception handlers. Includes robust low
+ level hardware fault handling for ARM Cortex-M; the handler even has unit
+ tests written in assembly to verify nested-hardware-fault handling!
+
+ .. grid-item-card:: :octicon:`code-square` pw_polyfill
+ :link: module-pw_polyfill
+ :link-type: ref
+
+ This module makes it easier to work with different C++ standards in one
+ codebase.
+
+ .. grid-item-card:: :octicon:`container` pw_tokenizer
+ :link: module-pw_tokenizer
+ :link-type: ref
+
+ Replace string literals from log statements with 32-bit tokens, to reduce
+ flash use, reduce logging bandwidth, and save formatting cycles from log
+ statements at runtime.
+
+.. grid:: 3
+
+ .. grid-item-card:: :octicon:`database` pw_kvs
+ :link: module-pw_kvs
+ :link-type: ref
+
+ A key-value-store implementation for flash-backed persistent storage with
+ integrated wear levelling. This is a lightweight alternative to a file
+ system for embedded devices.
+
+ .. grid-item-card:: :octicon:`paper-airplane` pw_protobuf
+ :link: module-pw_protobuf
+ :link-type: ref
-- :ref:`module-pw_polyfill` - Similar to JavaScript “polyfill” libraries, this
- module provides selected C++17 standard library components that are compatible
- with C++14.
+ An early preview of our wire-format-oriented protocol buffer
+ implementation. This protobuf compiler makes a different set of
+ implementation tradeoffs than the most popular protocol buffer library in
+ this space, nanopb.
-- :ref:`module-pw_tokenizer` - Replace string literals from log statements with
- 32-bit tokens, to reduce flash use, reduce logging bandwidth, and save
- formatting cycles from log statements at runtime.
+ .. grid-item-card:: :octicon:`device-desktop` pw_console
+ :link: module-pw_console
+ :link-type: ref
-- :ref:`module-pw_kvs` - A key-value-store implementation for flash-backed
- persistent storage with integrated wear levelling. This is a lightweight
- alternative to a file system for embedded devices.
+ Interactive Python repl and log viewer designed to be a a complete
+ solution for interacting with hardware devices using :ref:`module-pw_rpc`
+ over a :ref:`module-pw_hdlc` transport.
-- :ref:`module-pw_protobuf` - An early preview of our wire-format-oriented
- protocol buffer implementation. This protobuf compiler makes a different set
- of implementation tradeoffs than the most popular protocol buffer library in
- this space, nanopb.
See the :ref:`docs-module-guides` for the complete list of modules and their
documentation.
diff --git a/docs/infra/ci_cq_intro.rst b/docs/infra/ci_cq_intro.rst
new file mode 100644
index 000000000..bda836a8c
--- /dev/null
+++ b/docs/infra/ci_cq_intro.rst
@@ -0,0 +1,376 @@
+.. _docs-ci-cq-intro:
+
+===========
+CI/CQ Intro
+===========
+
+.. _essentials:
+
+----------
+Essentials
+----------
+
+Submitting CLs
+==============
+The Gerrit ``Submit`` button is hidden, replaced by the ``Submit to CQ``
+button. The ``Submit`` button is still accessible behind the ``...`` button
+in the top right corner of the Gerrit UI, but in some cases requires elevated
+permissions.
+
+Triggering presubmits
+=====================
+Presubmits are not automatically run when a patch set is uploaded. Click
+``CQ Dry Run`` to trigger them. (You can also use
+``git push origin +HEAD:refs/for/main%l=Commit-Queue``).
+
+Presubmit validity duration
+===========================
+If you don't have recent passing results from a ``CQ Dry Run`` (within 24
+hours) then ``Submit to CQ`` will run presubmits. After any newly run
+presubmits pass this will submit the change.
+
+User interface
+==============
+If a presubmit fails you'll get a Gerrit comment with a link to the failing
+build. The status of tryjobs (pending, running, failed, passed, etc.) is
+shown directly in the Gerrit UI (see :ref:`tryjobs`).
+
+Auto-Submit
+===========
+If you want your change to be automatically submitted when all requirements
+are met (``Code-Review +2``, ``OWNERS``-approval, all comments resolved,
+etc.) set the ``Auto-Submit`` label to +1. If submission fails it will be
+retried a couple times with backoff and then the auto submit job will give up.
+
+
+.. _further-details:
+
+---------------
+Further Details
+---------------
+
+Applying changes in testing
+===========================
+Changes are always rebased on the most recent commit when tested. If they
+fail to rebase the build fails.
+``CQ Dry Run`` is the same as voting ``Commit-Queue +1`` label and
+``Submit to CQ`` is the same as voting ``Commit-Queue +2``. If you vote both
+``Code-Review +2`` and ``Commit-Queue +2`` on somebody's change you are
+submitting it for them.
+
+Post-Submit builders
+====================
+Jobs are run post-submission too and can be seen at
+https://ci.chromium.org/p/pigweed (for public projects) and
+https://ci.chromium.org/p/pigweed-internal (for internal projects). Builders can
+also be viewed from Pigweed's :ref:`builder visualization <docs-builder-viz>`.
+
+Non-``main`` branches
+=====================
+CQ is enabled for all branches. If you upload to an individual repository
+branch X and the manifest or superproject also has a branch X, that branch of
+the manifest will be used.
+
+Rollers
+=======
+Just because a change has been submitted doesn't mean it's live in the
+project. Submodules and Android Repo Tool projects often need to be
+:ref:`rolled <docs-rollers>` before they're in the most recent checkout of the
+project.
+
+Presubmit result
+================
+The ``Presubmit-Verified`` label is set at the completion of CQ runs. It does
+not block submission, but can be used by Copybara to see if CQ has passed.
+If it looks incorrect, do another CQ run and it will be updated.
+
+.. _docs-builder-viz:
+
+Builder Visualization
+=====================
+
+Pigweed's builder visualization simplifies the process of browsing, viewing, and
+triggering builders. The source-of-truth for it is Google-internal, but there's
+a public version without the Google-internal bits.
+
+ * `Builder Viz link for external contributors <https://pigweed.googlesource.com/infra/config/+/main/generated/pigweed/for_review_only/viz/index.md>`_
+ * `Builder Viz link for Googlers <https://pigweed-internal.googlesource.com/infra/config/+/main/generated/pigweed/for_review_only/viz/index.md>`_
+
+.. _tryjobs:
+
+-------
+Tryjobs
+-------
+The colors of tryjobs in the Gerrit UI indicate the status of the tryjob: gray
+is pending/running, green is passed, red is failed or cancelled, and purple is
+an infra failure.
+
+Some tryjobs are not yet stable and are run as "experimental". These can be
+viewed with ``Show Experimental Tryjobs``. Experimental tryjobs run with CQ but
+do not block it.
+
+Individual tryjobs can be run additional times using the ``Choose Tryjobs``
+dialog. This can also be used to run tryjobs that would not normally run on the
+change. Tryjobs ran this way can be used to satisfy CQ requirements, but don't
+block CQ.
+
+.. _prod-vs-dev:
+
+Prod vs Dev
+===========
+Most builders have "prod" and "dev" versions. The "prod" versions block changes
+in CQ and may cause emails to be sent out if they fail in CI. The "dev" builders
+test new VM images before they go to "prod", so if a "dev" builder is failing
+when a "prod" builder is not failing, then the "dev" builder is failing because
+of an upcoming VM change, and teams should take time to get the "dev" builder to
+pass. For most projects, "dev" builders show up on the far right of console
+views in the LUCI UI.
+
+.. _tryjobs-cli:
+
+Tryjobs CLI
+===========
+
+``bb`` command
+--------------
+The ``bb`` command is available in a bootstrapped Pigweed environment and the
+environments of many downstream projects. It is also available from
+`Chromium's depot tools <https://chromium.googlesource.com/chromium/tools/depot_tools.git>_`.
+
+
+Querying tryjobs
+----------------
+In addition to viewing tryjobs in the Gerrit UI, you can use the ``bb`` command
+to query the tryjobs that ran on a change. The command to use is
+``bb ls -cl $URL``, but ``$URL`` has two non-obvious requirements:
+
+* It needs to be a ".googlesource.com" URL and not a Google-internal version of
+ that URL.
+* It needs to include the patchset number.
+
+.. code-block:: bash
+
+ $ bb ls -cl https://pigweed-review.googlesource.com/c/pigweed/sample_project/+/53684/1 | egrep -v '^(Tag|By):'
+ http://ci.chromium.org/b/8841234941714219488 SUCCESS 'pigweed/try/sample-project-xref-generator'
+ Created on 2021-07-19 at 16:45:32, waited 14.8s, started at 16:45:47, ran for 2m43s, ended at 16:48:30
+ CL: https://pigweed-review.googlesource.com/c/pigweed/sample_project/+/53684/1
+
+ http://ci.chromium.org/b/8841234941714219504 INFRA_FAILURE 'pigweed/try/sample-project-renode-test'
+ Summary: Infra Failure: Step('run pigweed/pw_unit_test/renode/test.sh') (retcode: None)
+ Created on 2021-07-19 at 16:45:32, waited 51.6s, started at 16:46:24, ran for 2m40s, ended at 16:49:04
+ CL: https://pigweed-review.googlesource.com/c/pigweed/sample_project/+/53684/1
+
+To exclude non-experimental tryjobs, add ``-t cq_experimental:false`` to the
+command.
+
+Manually launching tryjobs
+--------------------------
+In most cases, individual tryjobs can be launched using
+``Choose Additional Tryjobs``. If any relevant tryjobs are not listed here
+please file a bug.
+
+The ``bb`` command can also be used to launch tryjobs, which can useful for
+tracking down race conditions by launching many copies of a tryjob. Please be
+careful using this, especially during working hours in California.
+
+.. code-block:: shell
+
+ URL="https://pigweed-review.googlesource.com/c/pigweed/sample_project/+/53684/1"
+ TRYJOB="pigweed/sample_project.try/sample-project-xref-generator"
+ for i in $(seq 1 25); do
+ bb add -cl "$URL" "$TRYJOB"
+ done
+
+.. _why-didnt-lintformat-catch:
+
+Why didn't lintformat catch this formatting change?
+===================================================
+Rolls of tools like clang can update the preferred format of clang-format. There
+are two possibilities for addressing this. First, the tool roll could be blocked
+until formatting passes. This could require coordinating several changes across
+many repositories. This is further complicated if the new formatting preferred
+by clang-format is not accepted by the previous version. Second, lintformat can
+be configured to only run on changed files. This means downstream project
+lintformat tryjobs would not be run on Pigweed changes, nor on rolls of Pigweed
+into these projects.
+
+The second choice was selected. This means when tools roll lintformat jobs may
+start failing in CI, but they only fail in CQ on changes that touch files
+currently failing in CI. Teams should watch their build alert email list and
+proactively fix lintformat failures when they come.
+
+.. _dependent-changes:
+
+-----------------
+Dependent changes
+-----------------
+
+.. _creating:
+
+Creating
+========
+To pull in other changes when testing a change add a ``patches.json`` file to
+the root of the repository. An example is below.
+
+.. code-block:: json
+
+ [
+ {
+ "gerrit_name": "pigweed",
+ "number": 123456
+ },
+ {
+ "gerrit_name": "pigweed",
+ "number": 654321
+ }
+ ]
+
+Patches can be uni- or bidirectional and are transitive. The tryjob will parse
+this file, and then parse any ``patches.json`` files found in the referenced
+changes. If requirements are truly one-way, don't list them as two-way. Only the
+Gerrit instance name (the part before "-review") is permitted. The repository
+name is not included.
+
+.. admonition:: Note
+ :class: warning
+
+ ``patches.json`` cannot be used for changes to *the same repo* on the same
+ Gerrit host (`b/230610752 <https://issuetracker.google.com/230610752>`_).
+ Just stack these changes instead.
+
+.. _submitting:
+
+Submitting
+==========
+Pigweed's infrastructure does not support submitting multiple changes together.
+The best option is to find a way to have changes not depend on each other and
+submit them separately, or to have a one-way requirement instead of codependent
+changes, and submit the changes in dependency order, waiting for any necessary
+rolls before submitting the next change.
+
+Pigweed-related Gerrit hosts are configured to reject submission of all changes
+containing ``patches.json`` files. If the dependency is one-way, then submit the
+change without dependencies, wait for it to roll (if necessary), remove
+``patches.json`` from the dependent change, and vote ``Commit-Queue +2``.
+
+If the changes are truly codependent—both (or all) changes need each other—then
+follow the instructions below.
+
+First, get both changes passing CQ with ``patches.json`` files.
+
+Second, if one of the codependent changes is a submodule and another is the
+parent project, update the submodule change to no longer include the
+``patches.json`` file. Then directly submit the change that lives in the child
+submodule, bypassing CQ. This will break the roller, but not the source tree, so
+others on your team are unaffected.
+
+.. admonition:: Note
+
+ For the main Pigweed repository, only core Pigweed team members can force
+ submit, and they must first
+ `request a temporary ACL <http://go/pwi-cookbook#bypass-cq>`_ to do so. This
+ process requires an associated bug, so have one on hand before reaching out
+ with a force submission request.
+
+
+Finally, once the change has merged into the child project, update the submodule
+pointer in the parent project:
+
+.. admonition:: Note
+ :class: warning
+
+ Some projects have limitations on submission outside of CQ. Reach out to a
+ core Pigweed team member to bypass CQ for Pigweed itself.
+
+#. Update your submodule pin to the submitted commit hash (in most cases
+ ``git submodule update --remote path/to/submodule`` should be sufficient,
+ but see the
+ `git submodule documentation <https://git-scm.com/book/en/v2/Git-Tools-Submodules>`_
+ for full details)
+#. Add that change to the parent project change (``git add path/to/submodule``)
+#. Remove the ``patches.json`` file from the change (``git rm patches.json``)
+#. Commit and push to Gerrit
+#. Click ``Submit to CQ``
+
+After this change is submitted the roller will start working again.
+
+If all changes are to submodules, remove the ``patches.json`` files from both
+changes and directly submit, bypassing CQ. Then create a manual roll change that
+updates the submodules in question
+(``git submodule update --remote submodule1 submodule2``
+should be sufficient), upload it, and ``Submit to CQ``.
+
+.. _details:
+
+Details
+=======
+Sometimes codependent changes must be made in multiple repositories within an
+Android Repo Tool workspace or across multiple submodules. This can be done with
+the ``patches.json`` files. Given a situation where pigweed change would break
+the sample_project, the ``patches.json`` files must each refer to the other
+change.
+
+Pigweed ``patches.json``
+ ``[{"gerrit_name": "pigweed", "number": B}]``
+
+Sample Project ``patches.json``
+ ``[{"gerrit_name": "pigweed", "number": A}]``
+
+When running tryjobs for change A, builders will attempt to patch in change B as
+well. For pure Pigweed tryjobs this fails but the build continues. For the
+tryjobs that are there to ensure Pigweed doesn't break the Sample Project, both
+change A and change B will be applied to the checkout.
+
+There is some validation of the format of the ``patches.json`` file, but there's
+no error checking on the resolution of the required changes. The assumption is
+that changes that actually require other changes to pass CQ will fail if those
+changes aren't patched into the workspace.
+
+Requirements are transitive. If A requires B and B requires C then tryjobs for A
+will attempt to patch in A, B, and C. Requirements can also be one-way. If a
+change has been submitted it's assumed to already be in the checkout and is not
+patched in, nor are any transitive requirements processed. Likewise, abandoned
+changes are ignored.
+
+.. _banned-codewords:
+
+Banned codewords
+================
+Sometimes the name of an internal Gerrit instance is a codeword we don't allow
+on the Pigweed Gerrit instance. For example, you may wish to do the following.
+
+Pigweed change A ``patches.json``
+ ``[{"gerrit_name": "secret-project", "number": B}]``
+
+Secret-Project change B ``patches.json``
+ ``[{"gerrit_name": "pigweed", "number": A}]``
+
+This will be rejected by the Pigweed Gerrit instance because using
+"secret-project" is banned on that Gerrit instance and you won't be able to
+push. Instead, do the following, using the
+`requires-helper <https://pigweed-internal.googlesource.com/requires-helper>`_
+repository on the Pigweed-Internal Gerrit instance.
+
+Pigweed change A ``patches.json``
+ ``[{"gerrit_name": "pigweed-internal", "number": C}]``
+
+Secret-Project change B ``patches.json``
+ ``[{"gerrit_name": "pigweed", "number": A}]``
+
+Pigweed-Internal change C ``patches.json``
+ ``[{"gerrit_name": "secret-project", "number": B}]``
+
+The ``pw requires`` command simplifies creation of the Pigweed-Internal change.
+In this case the command would be ``pw requires secret-project:B``. Run this
+inside the Pigweed repository after committing change A and it will create
+change C and add ``[{"gerrit_name": "pigweed-internal", "number": C}]`` to
+change A. Multiple changes can be handled by passing multiple arguments to
+``pw requires``.
+
+Public builders won't have access to the Pigweed-Internal Gerrit instance so
+they won't even be able to see the ``secret-project`` reference. Internal
+builders for other internal projects will see the ``secret-project`` reference
+but won't be able to resolve it. Builders having access to ``secret-project``
+will see all three changes and attempt to patch all three in. Pigweed-Internal
+change C is not included in any workspaces so it will never be patched in, but
+it transitively applies requirements to public changes.
diff --git a/docs/infra/index.rst b/docs/infra/index.rst
new file mode 100644
index 000000000..91b798eaa
--- /dev/null
+++ b/docs/infra/index.rst
@@ -0,0 +1,9 @@
+=====
+Infra
+=====
+
+.. toctree::
+ :maxdepth: 1
+
+ ci_cq_intro
+ rollers
diff --git a/docs/infra/rollers.rst b/docs/infra/rollers.rst
new file mode 100644
index 000000000..35538a193
--- /dev/null
+++ b/docs/infra/rollers.rst
@@ -0,0 +1,74 @@
+.. _docs-rollers:
+
+=======
+Rollers
+=======
+
+-----------------
+What is a roller?
+-----------------
+A roller is an infra job that updates the revision of a pinned project (or
+version of a pinned CIPD package) in a repository with submodules or an
+Android Repo Tool manifest. The Pigweed Infra team generally maintains the
+logic and configuration of rollers, while various project and/or package
+owners across are responsible for keeping them green.
+
+-----------
+Our rollers
+-----------
+Rollers from Pigweed into downstream projects can be seen on the
+`Pigweed Console <https://ci.chromium.org/p/pigweed/g/pigweed.pigweed/console>`_.
+Many more rollers are visible when logged in using an @google.com account than
+when not.
+
+Rollers specific to individual downstream projects can be found by browsing
+the :ref:`builder visualization <docs-builder-viz>` and looking under the
+"roll" columns.
+
+--------------------
+How do rollers work?
+--------------------
+Project rollers will poll Gitiles every 30s for new commits to watched
+repositories. Package rollers will poll CIPD at their configured frequencies
+for new packages which match their watched refs (e.g. 'latest').
+
+When a new change is detected, luci-scheduler emits a 'trigger' for the
+roller. The 'trigger' does not necessarily invoke the roller immediately as
+luci-scheduler is configured to only allow a single job of each roller to be
+running at once. It will batch triggers until there is no job running, then
+create a roller job with the properties of the latest trigger in the batch.
+
+Once the roller job begins, it creates a CL which updates the relevant
+project or package pin(s) and attempts to submit the CL via CQ. If the CQ run
+fails, then the roller will abandon that CL. If the CQ run succeeds, the CL is
+submitted and the roller succeeds.
+
+On most hosts, rollers vote on the ``Bot-Commit`` label which bypasses the
+``Code-Review`` requirement. On other hosts, rollers vote on ``Code-Review``,
+or Gerrit is configured to not enforce the ``Code-Review`` requirement for CLs
+uploaded by the roller.
+
+In the event of a CQ failure, if the roller attempts to re-roll the exact same
+revision or package(s), it will re-use the existing CL and only re-run the
+failing portions of CQ.
+
+-------------------------------
+How do I fix a roller breakage?
+-------------------------------
+Most rollers will fix themselves. Failing rollers are periodically rerun by
+the
+`"rerunner" builders <https://ci.chromium.org/ui/p/pigweed/g/rerunner/builders>`_,
+so any flaky tests or temporary failures should resolve themselves.
+
+However, if you are a Googler on a team using Pigweed's infrastructure, you
+should be able to manually trigger a roller by finding it in the Builder Viz
+and clicking the "scheduler" link. (You may need to log in to
+luci-scheduler.) This is batched with all other 'triggers' as to not disrupt
+the normal roller flow, but will allow you to restart the roll process.
+
+-----------------------------------------------
+How do I disable a roller? Create a new roller?
+-----------------------------------------------
+*(Googlers only)* See `go/pw-config-tasks <http://go/pw-config-tasks>`_. But most
+of the time the appropriate action is to file a bug at
+`go/pw-infra-bug <http://go/pw-infra-bug>`_.
diff --git a/docs/layout/page.html b/docs/layout/page.html
new file mode 100644
index 000000000..347cc9a6f
--- /dev/null
+++ b/docs/layout/page.html
@@ -0,0 +1,25 @@
+<!--
+Copyright 2023 The Pigweed Authors
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+{% extends "furo/page.html" %}
+
+{% block supermain %}
+ <div class="pw-main">
+ <div class="pw-content">
+ {{ super() }}
+ </div>
+ </div>
+{% endblock supermain %}
+
diff --git a/docs/mission.rst b/docs/mission.rst
new file mode 100644
index 000000000..eb85d87aa
--- /dev/null
+++ b/docs/mission.rst
@@ -0,0 +1,55 @@
+.. _docs-mission:
+
+======================
+Mission & Philosophies
+======================
+
+-----------
+Our Mission
+-----------
+.. pull-quote::
+
+ Create both *software* and *process* foundations for *sustained*,
+ *robust*, and *rapid* *embedded* product development with *large teams*.
+
+Each component of our mission has additional meaning:
+
+- **Software** - Our modular components and surrounding tooling are our
+ primary deliverable. The software is the product of our philosophies.
+- **Process** - Developing robust software processes is hard, and not something
+ all teams have time for. Our mission is to create reusable processes that can
+ save time and increase team happiness. Our approach is to offer tooling and
+ standards. For example, our integrated code formatting, module directory
+ structure, style guide, test processes, and more are all designed for reuse.
+- **Sustained** - Products often live a long time or are part of a series of
+ related products; and over time the requirements change. Our mission is to
+ make these sustained development situations easier. Our approach is to create
+ flexible and reusable foundations to gracefully handle changes. For example,
+ our porting abstractions make switching a microcontroller vendor easier.
+- **Robust** - Creating robust products is hard, yet important for finding
+ market and product success. Our mission is to assist teams in creating robust
+ products. Our approach is to:
+
+ #. Invest heavily in software reliability tooling and processes (unit testing,
+ sanitizing, formatting, etc)
+ #. Comprehensively apply said tooling and processes to Pigweed, making Pigweed
+ a robust foundation
+ #. Make said tooling available for downstream use, so teams can ensure code
+ they write is robust
+
+- **Rapid** - Creating products quickly is critical in today's market. Our
+ mission is to make shipping products faster with Pigweed. Our approach is to
+ accelerate product development by offering reliable foundations (less bug
+ hunting), having great onboarding tooling (low engineer churn/setup), offering
+ modules solving common but hard problems (RPC), etc.
+- **Embedded** - Our mission is to support software embedded in products such as
+ earbuds, watches, phones, thermostats, satellites, cars, drones, and related.
+- **Large teams** - Developing software with large teams in the embedded space
+ is increasingly frequent due to more capable embedded processors.
+ Traditionally, teams were 1-5 people, but are increasingly much larger
+ (50-200 person) teams. Our mission is to make developing larger products with
+ correspondingly larger teams easier and faster, with an eye to support rapid
+ prototyping where there is a chance to scale from an idea to production. Our
+ approach is the investment in common abstractions, common practices, common
+ standards, tooling around those standards, and a focus on automation and
+ repeatability.
diff --git a/docs/module_guides.rst b/docs/module_guides.rst
index 936a24443..8daf471d8 100644
--- a/docs/module_guides.rst
+++ b/docs/module_guides.rst
@@ -3,9 +3,9 @@
=============
Module Guides
=============
-Pigweed is a collection of embedded-focused and embedded-related modules.
-Modules may contain code from multiple languages, including C, C++, Go, Rust,
-Shell, Batch and Python.
+Pigweed is a collection of embedded-focused and embedded-related :ref:`modules
+<docs-glossary-module>`. Modules may contain code from multiple languages,
+including C, C++, Go, Rust, Shell, Batch and Python.
.. toctree::
:titlesonly:
diff --git a/docs/module_structure.rst b/docs/module_structure.rst
index 03b6fa924..597dd465e 100644
--- a/docs/module_structure.rst
+++ b/docs/module_structure.rst
@@ -3,9 +3,10 @@
----------------
Module Structure
----------------
-The Pigweed module structure is designed to keep as much code as possible for a
-particular slice of functionality in one place. That means including the code
-from multiple languages, as well as all the related documentation and tests.
+The Pigweed :ref:`module <docs-glossary-module>` structure is designed to keep
+as much code as possible for a particular slice of functionality in one place.
+That means including the code from multiple languages, as well as all the
+related documentation and tests.
Additionally, the structure is designed to limit the number of places a file
could go, so that when reading callsites it is obvious where a header is from.
@@ -13,82 +14,97 @@ That is where the duplicated ``<module>`` occurrences in file paths comes from.
Example module structure
------------------------
-.. code-block:: python
-
- pw_foo/...
-
- docs.rst # If there is just 1 docs file, call it docs.rst
- README.md # All modules must have a short README for gittiles
-
- BUILD.gn # GN build required
- BUILD # Bazel build required
-
- # C++ public headers; the repeated module name is required
- public/pw_foo/foo.h
- public/pw_foo/baz.h
-
- # Exposed private headers go under internal/
- public/pw_foo/internal/bar.h
- public/pw_foo/internal/qux.h
-
- # Public override headers must go in 'public_overrides'
- public_overrides/gtest/gtest.h
- public_overrides/string.h
-
- # Private headers go into <module>_*/...
- pw_foo_internal/zap.h
- pw_foo_private/zip.h
- pw_foo_secret/alxx.h
-
- # C++ implementations go in the root
- foo_impl.cc
- foo.cc
- baz.cc
- bar.cc
- zap.cc
- zip.cc
- alxx.cc
-
- # C++ tests also go in the root
- foo_test.cc
- bar_test.cc
- zip_test.cc
-
- # Python files go into 'py/<module>/...'
- py/BUILD.gn # Python packages are declared in GN using pw_python_package
- py/setup.py # Python files are structured as standard Python packages
- py/foo_test.py # Tests go in py/ but outside of the Python package
- py/bar_test.py
- py/pw_foo/__init__.py
- py/pw_foo/__main__.py
- py/pw_foo/bar.py
- py/pw_foo/py.typed # Indicates that this package has type annotations
-
- # Go files go into 'go/...'
- go/...
-
- # Examples go in examples/, mixing different languages
- examples/demo.py
- examples/demo.cc
- examples/demo.go
- examples/BUILD.gn
- examples/BUILD
-
- # Size reports go under size_report/
- size_report/BUILD.gn
- size_report/base.cc
- size_report/use_case_a.cc
- size_report/use_case_b.cc
-
- # Protobuf definition files go into <module>_protos/...
- pw_foo_protos/foo.proto
- pw_foo_protos/internal/zap.proto
-
- # Other directories are fine, but should be private.
- data/...
- graphics/...
- collection_of_tests/...
- code_relating_to_subfeature/...
+.. code-block:: text
+
+ pw_foo/...
+
+ docs.rst # Docs landing page (required)
+ concepts.rst # Conceptual docs (optional)
+ design.rst # Design docs (optional)
+ guides.rst # How-to guides (optional)
+ api.rst # API reference (optional)
+ cli.rst # CLI reference (optional)
+ gui.rst # GUI reference (optional)
+ tutorials/*.rst # Tutorials (optional)
+
+ BUILD.gn # GN build required
+ BUILD # Bazel build required
+
+ # C++ public headers; the repeated module name is required
+ public/pw_foo/foo.h
+ public/pw_foo/baz.h
+
+ # Exposed private headers go under internal/
+ public/pw_foo/internal/bar.h
+ public/pw_foo/internal/qux.h
+
+ # Public override headers must go in 'public_overrides'
+ public_overrides/gtest/gtest.h
+ public_overrides/string.h
+
+ # Private headers go into <module>_*/...
+ pw_foo_internal/zap.h
+ pw_foo_private/zip.h
+ pw_foo_secret/alxx.h
+
+ # C++ implementations go in the root
+ foo_impl.cc
+ foo.cc
+ baz.cc
+ bar.cc
+ zap.cc
+ zip.cc
+ alxx.cc
+
+ # C++ tests also go in the root
+ foo_test.cc
+ bar_test.cc
+ zip_test.cc
+
+ # Python files go into 'py/<module>/...'
+ py/BUILD.gn # Python packages are declared in GN using pw_python_package
+ py/setup.py # Python files are structured as standard Python packages
+ py/foo_test.py # Tests go in py/ but outside of the Python package
+ py/bar_test.py
+ py/pw_foo/__init__.py
+ py/pw_foo/__main__.py
+ py/pw_foo/bar.py
+ py/pw_foo/py.typed # Indicates that this package has type annotations
+
+ # Rust crates go into 'rust/...'
+ rust/BUILD.bazel
+ rust/crate_one.rs # Single file crates are in rust/<crate_name>.rs
+ rust/crate_two/lib.rs # Multi-file crate's top level source in:
+ # rust/<crate>/lib.rs
+ rust/crate_two/mod_one.rs # Multi-file crate's modules in:
+ rust/crate_two/mod_two.rs # rust/<crate>/<module_name>.rs
+ # Prefer not using mod.rs files.
+
+ # Go files go into 'go/...'
+ go/...
+
+ # Examples go in examples/, mixing different languages
+ examples/demo.py
+ examples/demo.cc
+ examples/demo.go
+ examples/BUILD.gn
+ examples/BUILD
+
+ # Size reports go under size_report/
+ size_report/BUILD.gn
+ size_report/base.cc
+ size_report/use_case_a.cc
+ size_report/use_case_b.cc
+
+ # Protobuf definition files go into <module>_protos/...
+ pw_foo_protos/foo.proto
+ pw_foo_protos/internal/zap.proto
+
+ # Other directories are fine, but should be private.
+ data/...
+ graphics/...
+ collection_of_tests/...
+ code_relating_to_subfeature/...
Module name
-----------
@@ -118,10 +134,10 @@ Examples:
.. code-block::
- pw_foo/...
- public/pw_foo/foo.h
- public/pw_foo/a_header.h
- public/pw_foo/baz.h
+ pw_foo/...
+ public/pw_foo/foo.h
+ public/pw_foo/a_header.h
+ public/pw_foo/baz.h
For headers that must be exposed due to C++ limitations (i.e. are included from
the public interface, but are not intended for use), place the headers in a
@@ -130,9 +146,9 @@ the public interface, but are not intended for use), place the headers in a
.. code-block::
- pw_foo/...
- public/pw_foo/internal/secret.h
- public/pw_foo/internal/business.h
+ pw_foo/...
+ public/pw_foo/internal/secret.h
+ public/pw_foo/internal/business.h
.. note::
@@ -159,15 +175,15 @@ For example, the ``pw_unit_test`` module provides a header override for
.. code-block::
- pw_unit_test/...
+ pw_unit_test/...
- public_overrides/gtest
- public_overrides/gtest/gtest.h
+ public_overrides/gtest
+ public_overrides/gtest/gtest.h
- public/pw_unit_test
- public/pw_unit_test/simple_printing_event_handler.h
- public/pw_unit_test/event_handler.h
- public/pw_unit_test/internal/framework.h
+ public/pw_unit_test
+ public/pw_unit_test/simple_printing_event_handler.h
+ public/pw_unit_test/event_handler.h
+ public/pw_unit_test/internal/framework.h
Note that the overrides are in a separate directory ``public_overrides``.
@@ -180,12 +196,12 @@ Example:
.. code-block::
- pw_unit_test/...
- main.cc
- framework.cc
- test.gni
- BUILD.gn
- README.md
+ pw_unit_test/...
+ main.cc
+ framework.cc
+ test.gni
+ BUILD.gn
+ README.md
.. _module-structure-compile-time-configuration:
@@ -226,16 +242,16 @@ on whether the header should be exposed by the module or not.
.. code-block::
- pw_foo/...
+ pw_foo/...
- # Publicly accessible configuration header
- public/pw_foo/config.h
+ # Publicly accessible configuration header
+ public/pw_foo/config.h
- # Internal configuration header that is included by other module headers
- public/pw_foo/internal/config.h
+ # Internal configuration header that is included by other module headers
+ public/pw_foo/internal/config.h
- # Internal configuration header
- pw_foo_private/config.h
+ # Internal configuration header
+ pw_foo_private/config.h
The configuration header is provided by a build system library. This library
acts as a :ref:`facade<docs-module-structure-facades>`. The facade uses a
@@ -245,37 +261,37 @@ system, the config facade is declared as follows:
.. code-block::
- declare_args() {
- # The build target that overrides the default configuration options for this
- # module. This should point to a source set that provides defines through a
- # public config (which may -include a file or add defines directly).
- pw_foo_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
- }
-
- # An example source set for each potential config header location follows.
-
- # Publicly accessible configuration header (most common)
- pw_source_set("config") {
- public = [ "public/pw_foo/config.h" ]
- public_configs = [ ":public_include_path" ]
- public_deps = [ pw_foo_CONFIG ]
- }
-
- # Internal configuration header that is included by other module headers
- pw_source_set("config") {
- sources = [ "public/pw_foo/internal/config.h" ]
- public_configs = [ ":public_include_path" ]
- public_deps = [ pw_foo_CONFIG ]
- visibility = [":*"] # Only allow this module to depend on ":config"
- friend = [":*"] # Allow this module to access the config.h header.
- }
-
- # Internal configuration header
- pw_source_set("config") {
- public = [ "pw_foo_private/config.h" ]
- public_deps = [ pw_foo_CONFIG ]
- visibility = [":*"] # Only allow this module to depend on ":config"
- }
+ declare_args() {
+ # The build target that overrides the default configuration options for this
+ # module. This should point to a source set that provides defines through a
+ # public config (which may -include a file or add defines directly).
+ pw_foo_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+ }
+
+ # An example source set for each potential config header location follows.
+
+ # Publicly accessible configuration header (most common)
+ pw_source_set("config") {
+ public = [ "public/pw_foo/config.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ pw_foo_CONFIG ]
+ }
+
+ # Internal configuration header that is included by other module headers
+ pw_source_set("config") {
+ sources = [ "public/pw_foo/internal/config.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ pw_foo_CONFIG ]
+ visibility = [":*"] # Only allow this module to depend on ":config"
+ friend = [":*"] # Allow this module to access the config.h header.
+ }
+
+ # Internal configuration header
+ pw_source_set("config") {
+ public = [ "pw_foo_private/config.h" ]
+ public_deps = [ pw_foo_CONFIG ]
+ visibility = [":*"] # Only allow this module to depend on ":config"
+ }
Overriding configuration
^^^^^^^^^^^^^^^^^^^^^^^^
@@ -296,32 +312,32 @@ This example shows two ways to configure a module in the GN build system.
.. code-block::
- # In the toolchain, set either pw_build_DEFAULT_MODULE_CONFIG or pw_foo_CONFIG
- pw_build_DEFAULT_MODULE_CONFIG = get_path_info(":define_overrides", "abspath")
+ # In the toolchain, set either pw_build_DEFAULT_MODULE_CONFIG or pw_foo_CONFIG
+ pw_build_DEFAULT_MODULE_CONFIG = get_path_info(":define_overrides", "abspath")
- # This configuration sets PW_FOO_INPUT_BUFFER_SIZE_BYTES using the -D flag.
- pw_source_set("define_overrides") {
- public_configs = [ ":define_options" ]
- }
+ # This configuration sets PW_FOO_INPUT_BUFFER_SIZE_BYTES using the -D flag.
+ pw_source_set("define_overrides") {
+ public_configs = [ ":define_options" ]
+ }
- config("define_options") {
- defines = [ "PW_FOO_INPUT_BUFFER_SIZE_BYTES=256" ]
- }
+ config("define_options") {
+ defines = [ "PW_FOO_INPUT_BUFFER_SIZE_BYTES=256" ]
+ }
- # This configuration sets PW_FOO_INPUT_BUFFER_SIZE_BYTES in a header file.
- pw_source_set("include_overrides") {
- public_configs = [ ":set_options_in_header_file" ]
+ # This configuration sets PW_FOO_INPUT_BUFFER_SIZE_BYTES in a header file.
+ pw_source_set("include_overrides") {
+ public_configs = [ ":set_options_in_header_file" ]
- # Header file with #define PW_FOO_INPUT_BUFFER_SIZE_BYTES 256
- sources = [ "my_config_overrides.h" ]
- }
+ # Header file with #define PW_FOO_INPUT_BUFFER_SIZE_BYTES 256
+ sources = [ "my_config_overrides.h" ]
+ }
- config("set_options_in_header_file") {
- cflags = [
- "-include",
- rebase_path("my_config_overrides.h", root_build_dir),
- ]
- }
+ config("set_options_in_header_file") {
+ cflags = [
+ "-include",
+ rebase_path("my_config_overrides.h", root_build_dir),
+ ]
+ }
.. admonition:: Why this config pattern is preferred
@@ -394,46 +410,23 @@ it's possible to use multiple backends for a module.
.. code-block::
- # pw_foo contains 2 facades, foo and bar
- pw_foo/...
- # Public headers
- # public/pw_foo/foo.h #includes pw_foo_backend/foo.h
- # public/pw_foo/bar.h #includes pw_foo_backend/bar.h
- public/pw_foo/foo.h
- public/pw_foo/bar.h
+ # pw_foo contains 2 facades, foo and bar
+ pw_foo/...
+ # Public headers
+ # public/pw_foo/foo.h #includes pw_foo_backend/foo.h
+ # public/pw_foo/bar.h #includes pw_foo_backend/bar.h
+ public/pw_foo/foo.h
+ public/pw_foo/bar.h
- pw_foo_backend/...
+ pw_foo_backend/...
- # Public override headers for facade1 and facade2 go in separate folders
- foo_public_overrides/pw_foo_backend/foo.h
- bar_public_overrides/pw_foo_backend/bar.h
+ # Public override headers for facade1 and facade2 go in separate folders
+ foo_public_overrides/pw_foo_backend/foo.h
+ bar_public_overrides/pw_foo_backend/bar.h
Documentation
-------------
-Documentation should go in the root module folder, typically in the
-``docs.rst`` file. There must be a docgen entry for the documentation in the
-``BUILD.gn`` file with the target name ``docs``; so the full target for the
-docs would be ``<module>:docs``.
-
-.. code-block::
-
- pw_example_module/...
-
- docs.rst
-
-For modules with more involved documentation, create a separate directory
-called ``docs/`` under the module root, and put the ``.rst`` files and other
-related files (like images and diagrams) there.
-
-.. code-block::
-
- pw_example_module/...
-
- docs/docs.rst
- docs/bar.rst
- docs/foo.rst
- docs/image/screenshot.png
- docs/image/diagram.svg
+See :ref:`seed-0102`.
Creating a new Pigweed module
-----------------------------
@@ -457,6 +450,7 @@ To create a new Pigweed module, follow the below steps.
- Add ``{pw_module_dir}/README.md`` that has a module summary
- Add ``{pw_module_dir}/docs.rst`` that contains the main module
documentation
+ - Add optional documentation as described in :ref:`seed-0102`
5. Add GN build support in ``{pw_module_dir}/BUILD.gn``
@@ -482,7 +476,7 @@ To create a new Pigweed module, follow the below steps.
10. Add the new module to CMake build
- - In ``/CMakeLists.txt`` add ``add_subdirectory(pw_new)``
+ - In ``/CMakeLists.txt`` add ``add_subdirectory(pw_new)``
11. Run :ref:`module-pw_module-module-check`
diff --git a/docs/os_abstraction_layers.rst b/docs/os/index.rst
index d2be7df76..4ed4a6acb 100644
--- a/docs/os_abstraction_layers.rst
+++ b/docs/os/index.rst
@@ -1,8 +1,14 @@
-.. _docs-os_abstraction_layers:
+.. _docs-os:
+
+==========
+OS Support
+==========
+
+.. toctree::
+ :hidden:
+
+ zephyr/index
-=====================
-OS Abstraction Layers
-=====================
Pigweed’s operating system abstraction layers are portable and configurable
building blocks, giving users full control while maintaining high performance
and low overhead.
@@ -30,8 +36,8 @@ Pigweed has ports for the following systems:
- **✔ Supported**
* - Baremetal
- *In Progress*
- * - `Zephyr <https://www.zephyrproject.org/>`_
- - Planned
+ * - :ref:`Zephyr <docs-os-zephyr>`
+ - *In Progress*
* - `CMSIS-RTOS API v2 & RTX5 <https://www.keil.com/pack/doc/CMSIS/RTOS2/html/index.html>`_
- Planned
diff --git a/docs/os/zephyr/index.rst b/docs/os/zephyr/index.rst
new file mode 100644
index 000000000..0d791ca6d
--- /dev/null
+++ b/docs/os/zephyr/index.rst
@@ -0,0 +1,47 @@
+.. _docs-os-zephyr:
+
+======
+Zephyr
+======
+Pigweed has preliminary support for `Zephyr <https://www.zephyrproject.org/>`_.
+See the docs for these modules for more information:
+
+- :ref:`pw_assert_zephyr <module-pw_assert_zephyr>`
+- :ref:`pw_chrono_zephyr <module-pw_chrono_zephyr>`
+- :ref:`pw_interrupt_zephyr <module-pw_interrupt_zephyr>`
+- :ref:`pw_log_zephyr <module-pw_log_zephyr>`
+- :ref:`pw_sync_zephyr <module-pw_sync_zephyr>`
+- :ref:`pw_sys_io_zephyr <module-pw_sys_io_zephyr>`
+- :ref:`pw_thread_zephyr <module-pw_thread_zephyr>`
+
+.. _docs-os-zephyr-get-started:
+
+-----------------------------------
+Get started with Zephyr and Pigweed
+-----------------------------------
+1. Complete the `Zephyr Getting Started Guide`_.
+2. Check out the `zds2023`_ repository for an example of a Zephyr starter
+ project that has been set up to use Pigweed.
+3. See :ref:`docs-os-zephyr-kconfig` to find the Kconfig options for
+ enabling individual Pigweed modules and features.
+
+-------
+Testing
+-------
+To test against Zephyr, first go through the `Zephyr Getting Started Guide`_.
+Once set up, simply invoke:
+
+.. code-block:: bash
+
+ $ . ${PW_ROOT}/activate.sh
+ $ ${ZEPHYR_BASE}/scripts/twister -T ${PW_ROOT}
+
+.. attention:: Testing has only been verified with `-p native_posix`. Proceed with caution.
+
+.. _Zephyr Getting Started Guide: https://docs.zephyrproject.org/latest/develop/getting_started/index.html#getting-started-guide
+.. _zds2023: https://github.com/yperess/zds2023
+
+.. toctree::
+ :hidden:
+
+ kconfig
diff --git a/docs/os/zephyr/kconfig.rst b/docs/os/zephyr/kconfig.rst
new file mode 100644
index 000000000..bff846038
--- /dev/null
+++ b/docs/os/zephyr/kconfig.rst
@@ -0,0 +1,12 @@
+.. _docs-os-zephyr-kconfig:
+
+========================
+Zephyr Kconfig reference
+========================
+This page is an auto-generated reference of all of the Kconfig-related
+options that are available when you use Pigweed with Zephyr.
+
+.. caution::
+ This document is a work in progress. If something looks incorrect or
+ incomplete, use the source links to double-check the information in the
+ Kconfig source code.
diff --git a/docs/overview.rst b/docs/overview.rst
new file mode 100644
index 000000000..d79306ea9
--- /dev/null
+++ b/docs/overview.rst
@@ -0,0 +1,138 @@
+.. _docs-overview:
+
+========
+Overview
+========
+
+Why Build Pigweed?
+==================
+.. note::
+
+ See our :ref:`docs-mission` for more details.
+
+Our goal is to make embedded software development efficient, robust, and
+heck, even delightful, for projects ranging from weekend Arduino experiements
+to commercial products selling in the millions.
+
+Embedded software development is notoriously arcane. Developers often have to
+track down vendor toolchains specific to the hardware they're targeting, write
+their code against hardware-specfic SDKs/HALs, and limit themselves to a small
+subset of C. Project teams are on their own to figure out how to set up a build
+system, automated testing, serial communication, and many other embedded
+project fundamentals. This is error prone and takes effort away from developing
+the actual product!
+
+There are solutions on the market that promise to solve all of these problems
+with a monolithic framework—just write your code against the framework and use
+hardware the framework supports, and you get an efficient embedded development
+environment. But this approach doesn't work well for existing projects that
+weren't built on the framework from the beginning or for projects that have
+specific needs the framework wasn't designed for. We know from experience that
+this approach alone doesn't meet our goal.
+
+So we have set out to build a platform that supports successful embedded
+developers at every scale by allowing them to adopt as much or as little of
+what Pigweed provides as they need, in the way that works best for their
+project.
+
+How Pigweed Works
+=================
+Pigweed provides four foundational pillars to support your embedded development:
+
+* :ref:`A comprehensive set of libraries for embedded development<docs-concepts-embedded-development-libraries>`
+* :ref:`A hermetic and replicable development environment<docs-concepts-development-environment>`
+* :ref:`A system for building, testing, and linting your project<docs-concepts-build-system>`
+* :ref:`A full framework for new projects that want a turn-key solution<docs-concepts-full-framework>`
+
+.. _docs-concepts-embedded-development-libraries:
+
+Embedded Development Libraries
+------------------------------
+Pigweed enables you to use modern C++ and software development best practices in
+your embedded project without compromising performance or increasing memory use
+compared to conventional embedded C.
+
+We provide libraries (:ref:`modules <docs-glossary-module>`) for
+:ref:`strings<module-pw_string>`, :ref:`time<module-pw_chrono>`,
+:ref:`assertions<module-pw_assert>`, :ref:`logging<module-pw_log>`,
+:ref:`serial communication<module-pw_spi>`, :ref:`remote procedure calls (RPC)
+<module-pw_rpc>`, and :ref:`much more<docs-module-guides>`.
+
+These modules are designed to work both on your host machine and on a wide
+variety of target devices. We achieve this by writing them in an inherently
+portable way, or through the facade/backend pattern. As a result, you can write
+most or all of your code to run transparently on your host machine and targets.
+
+.. _docs-concepts-development-environment:
+
+Development Environment
+-----------------------
+Managing toolchains, build systems, and other software needed for a project is
+complex. Pigweed provides all of this out of the box for Linux, Mac, and
+Windows systems in a sealed environment that leaves the rest of your system
+untouched. Getting new developers started is as simple as cloning your project
+repository and activating the Pigweed environment.
+
+.. _docs-concepts-build-system:
+
+Build System
+------------
+Pigweed modules are built to integrate seamlessly into projects using GN. We
+are rapidly expanding our good support for CMake and nascent support for Bazel
+so you can use your build system of choice. For new projects, Pigweed provides a
+build system you can integrate your own code into that works out of the box.
+
+.. _docs-concepts-full-framework:
+
+Full Framework (coming in 2022)
+-------------------------------
+For those who want a fully-integrated solution that provides everything Pigweed
+has to offer with an opinionated project structure, we are working diligently
+on a :ref:`Pigweed framework<module-pw_system>`. Stay tuned for more news to
+come! In the meantime, we invite you to discuss this and collaborate with us
+on `Discord <https://discord.gg/M9NSeTA>`_.
+
+.. _docs-concepts-right-for-my-project:
+
+Is Pigweed Right for My Project?
+================================
+Pigweed is still in its early stages, and while we have ambitious plans for it,
+Pigweed might not be the right fit for your project today. Here are some things
+to keep in mind:
+
+* Many individual modules are stable and are running on shipped devices today.
+ If any of those modules meet your needs, you should feel safe bringing them
+ into your project.
+
+* Some modules are in very early and active stages of development. They likely
+ have unstable APIs and may not work on all supported targets. If this is the
+ case, it will be indicated in the module's documentation. If you're interested
+ in contributing to the development of one of these modules, we encourage you
+ to experiment with them. Otherwise they aren't ready for use in most projects.
+
+* Setting up new projects to use Pigweed is currently not very easy, but we are
+ working to address that. In the meantime, join the Pigweed community on
+ `Discord <https://discord.gg/M9NSeTA>`_ to get help.
+
+Supported language versions
+===========================
+
+C++
+---
+All Pigweed code requires C++17 and is fully compatible with C++20. Pigweed
+defines GN toolchains for building with C++17 and C++20; see :ref:`target-host`
+target documentation for more information. For Bazel, the C++ standard version
+can be configured using the `--cxxopt flag
+<https://bazel.build/docs/user-manual#cxxopt>`_.
+
+.. _docs-concepts-python-version:
+
+Python
+------
+Pigweed officially supports Python 3.8, 3.9, 3.10, and 3.11.
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ mission
diff --git a/docs/python_build.rst b/docs/python_build.rst
index 8c8848ae5..2cce15a9c 100644
--- a/docs/python_build.rst
+++ b/docs/python_build.rst
@@ -5,7 +5,6 @@ Pigweed's GN Python Build
=========================
.. seealso::
-
- :bdg-ref-primary-line:`module-pw_build-python` for detailed template usage.
- :bdg-ref-primary-line:`module-pw_build` for other GN templates available
within Pigweed.
@@ -117,11 +116,14 @@ Managing Python Requirements
Build Time Python Virtualenv
----------------------------
-Pigweed's GN Python build infrastructure relies on a single build-only venv for
-executing Python code. This provides an isolated environment with a reproducible
-set of third party Python constraints where all Python tests and linting can
-run. All :ref:`module-pw_build-pw_python_action` targets are executed within
-this build venv.
+Pigweed's GN Python build infrastructure relies on `Python virtual environments
+<https://docs.python.org/3/library/venv.html>`_ for executing Python code. This
+provides a controlled isolated environment with a defined set of third party
+Python constraints where all Python tests, linting and
+:ref:`module-pw_build-pw_python_action` targets are executed.
+
+There must be at least one venv for Python defined in GN. There can be multiple
+venvs but one must be the designated default.
The default build venv is specified via a GN arg and is best set in the root
``.gn`` or ``BUILD.gn`` file. For example:
@@ -130,6 +132,13 @@ The default build venv is specified via a GN arg and is best set in the root
pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
+.. tip::
+ Additional :ref:`module-pw_build-pw_python_venv` targets can be created as
+ needed. The :ref:`module-pw_build-pw_python_action` template can take an
+ optional ``venv`` argument to specify which Python venv it should run
+ within. If not specified the target referred in the
+ ``pw_build_PYTHON_BUILD_VENV`` is used.
+
.. _docs-python-build-python-gn-requirements-files:
Third-party Python Requirements and Constraints
@@ -157,10 +166,10 @@ to add Python package dependencies:
.. code-block::
- pw_build_PIP_REQUIREMENTS = [
- # Project specific requirements
- "//tools/requirements.txt",
- ]
+ pw_build_PIP_REQUIREMENTS = [
+ # Project specific requirements
+ "//tools/requirements.txt",
+ ]
See the :ref:`docs-python-build-python-gn-structure` section below for a full
code listing.
@@ -182,47 +191,352 @@ project specific constraints can be appended to this list.
"//tools/constraints.txt",
]
+In-tree ``pw_python_package`` Requirements
+------------------------------------------
+A given venv inherits a project's requirements and constraint files by default
+via the ``pw_build_PIP_CONSTRAINTS`` and ``pw_build_PIP_REQUIREMENTS`` GN args
+as described above. This can be overridden if needed.
+
+``generated_requirements.txt``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+To ensure the requirements of in-tree :ref:`module-pw_build-pw_python_package`
+targets are installed :ref:`module-pw_build-pw_python_venv` introduces the
+``source_packages`` argument. This is a list of in-tree ``pw_python_package``
+GN targets expected to be used within the venv. When the venv is created each
+``pw_python_package``'s ``setup.cfg`` file is read to pull the
+``install_requires`` section for all third party dependencies. The full list of
+all in-tree packages and any in-tree transitive dependencies is then written to
+the out directory in a single ``generated_requirements.txt``.
+
+Take the ``//pw_build/py/gn_tests:downstream_tools_build_venv`` example below,
+its ``source package`` is a single ``pw_python_distribution`` package which
+bundles the ``pw_env_setup`` and ``pw_console`` ``pw_python_package``s. Those
+two packages each depend on a few other ``pw_python_package`` targets. The
+output ``generated_requirements.txt`` below merges all these package deps and
+adds ``-c`` lines for constraint files.
+
+.. seealso::
+ The pip documentation on the `Requirements File Format
+ <https://pip.pypa.io/en/stable/reference/requirements-file-format/#requirements-file-format>`_
+
+.. literalinclude:: pw_build/py/gn_tests/BUILD.gn
+ :start-after: [downstream-project-venv]
+ :end-before: [downstream-project-venv]
+
+.. code-block::
+ :caption: :octicon:`file;1em` out/python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/generated_requirements.txt
+ :name: generated_requirements
+
+ # Auto-generated requirements.txt from the following packages:
+ #
+ # //pw_arduino_build/py:py
+ # //pw_build/py/gn_tests:downstream_project_tools
+ # //pw_build/py:py
+ # //pw_cli/py:py
+ # //pw_console/py:py
+ # //pw_env_setup/py:py
+ # //pw_log_tokenized/py:py
+ # //pw_package/py:py
+ # //pw_presubmit/py:py
+ # //pw_stm32cube_build/py:py
+
+ # Constraint files:
+ -c ../../../../../../../pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+
+ black>=23.1.0
+ build>=0.8.0
+ coloredlogs
+ coverage
+ ipython
+ jinja2
+ mypy>=0.971
+ parameterized
+ pip-tools>=6.12.3
+ prompt-toolkit>=3.0.26
+ psutil
+ ptpython>=3.0.20
+ pygments
+ pylint>=2.9.3
+ pyperclip
+ pyserial>=3.5,<4.0
+ pyyaml
+ setuptools
+ six
+ toml
+ types-pygments
+ types-pyserial>=3.5,<4.0
+ types-pyyaml
+ types-setuptools
+ types-six
+ websockets
+ wheel
+ yapf>=0.31.0
+
+``compiled_requirements.txt``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The above ``generated_requirements.txt`` file is then fed into the
+``pip-compile`` command from `the pip-tools package
+<https://pypi.org/project/pip-tools>`_ to fully expand and pin each package with
+hashes. The resulting ``compiled_requirements.txt`` can then be used as the
+single Python requirements file for replicating this ``pw_python_venv``
+elsewhere. Each ``pw_python_venv`` will get this single file containing the
+exact versions of each required Python package.
+
+.. tip::
+ The ``compiled_requirements.txt`` generated by a ``pw_python_venv`` is used
+ by the :ref:`module-pw_build-pw_python_zip_with_setup` template when
+ producing a self contained zip of in-tree and third party Python packages.
+
+Below is a snippet of the ``compiled_requirements.txt`` for this
+:ref:`module-pw_build-pw_python_venv` target:
+``//pw_build/py/gn_tests:downstream_tools_build_venv``
+
+.. code-block::
+ :caption: :octicon:`file;1em` out/python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/compiled_requirements.txt
+ :name: compiled_requirements
+
+ #
+ # This file is autogenerated by pip-compile with Python 3.11
+ # by the following command:
+ #
+ # pip-compile --allow-unsafe --generate-hashes
+ # --output-file=python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/compiled_requirements.txt
+ # --resolver=backtracking
+ # python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/generated_requirements.txt
+ #
+ appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -c python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/../../../../../../../pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+ # ptpython
+ astroid==2.14.2 \
+ --hash=sha256:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153 \
+ --hash=sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf
+ # via
+ # -c python/gen/pw_build/py/gn_tests/downstream_tools_build_venv/../../../../../../../pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+ # pylint
+ ...
+
+The presence of hashes in the above example can be controlled via the
+``pip_generate_hashes`` arg to the :ref:`module-pw_build-pw_python_venv`
+template.
+
+Caching Python Packages for Offline Installation
+------------------------------------------------
+
+.. _docs-python-build-downloading-packages:
+
+Downloading Packages
+^^^^^^^^^^^^^^^^^^^^
+The :ref:`module-pw_build-pw_python_venv` target adds an optional sub target
+that will download all Python packages from remote servers into a local
+directory. The remote server is typically `pypi.org <https://pypi.org/>`_.
+
+Taking the ``//pw_build/py/gn_tests:downstream_tools_build_venv`` target as an
+example again let's build a local cache. To run the download target append
+``.vendor_wheels`` to the end of the ``pw_python_venv`` target name. In this
+example it would be
+``//pw_build/py/gn_tests:downstream_tools_build_venv.vendor_wheels``
+
+To build that one gn target with ninja, pass the output name from gn as a target
+name for ninja:
+
+.. code-block:: bash
+
+ gn gen out
+ ninja -C out \
+ $(gn ls out --as=output \
+ '//pw_build/py/gn_tests:downstream_tools_build_venv.vendor_wheels')
+
+This creates a ``wheels`` folder with all downloaded packages and a
+``pip_download_log.txt`` with verbose logs from running ``pip download``.
+
+.. code-block::
+ :caption: :octicon:`file-directory;1em` Vendor wheels output directory
+ :name: vendor-wheel-output
+
+ out/python/gen/pw_build/py/gn_tests/downstream_tools_build_venv.vendor_wheels/
+ ├── pip_download_log.txt
+ └── wheels
+ ├── appdirs-1.4.4-py2.py3-none-any.whl
+ ├── astroid-2.14.2-py3-none-any.whl
+ ├── backcall-0.2.0-py2.py3-none-any.whl
+ ├── black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ ├ ...
+ ├── websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl
+ ├── wheel-0.40.0-py3-none-any.whl
+ ├── wrapt-1.14.1.tar.gz
+ └── yapf-0.31.0-py2.py3-none-any.whl
+
+Note the above output has both Python wheel ``.whl`` and source distribution
+``.tar.gz`` files. The ``.whl`` may contain Python packages with precompiled C
+extensions. This is denoted by this part of the filename:
+``cp311-cp311-manylinux_2_17_x86_64.whl``. These binary packages are selected by
+the ``pip download`` command based on the host machine python version, OS, and
+CPU architecture.
+
+.. warning::
+ If you need to cache Python packages for multiple platforms the
+ ``.vendor_wheels`` target will need to be run for each combination of Python
+ version, host operating system and architecture. For example, look at `the
+ files available for numpy <https://pypi.org/project/cffi/#files>`_. Some
+ combinations are:
+
+ - cp311, manylinux_2_17_x86_64
+ - cp311, manylinux2014_x86_64
+ - cp311, macosx_11_0_arm64
+ - cp311, macosx_10_9_x86_64
+ - cp311, win_amd64
+ - cp311, win32
+
+ Plus all of the above duplicated for Python 3.10 and 3.9 (``cp310`` and
+ ``cp39``).
+
+ The output of multiple ``.vendor_wheels`` runs on different host systems can
+ all be merged into the same output directory.
+
+``.vendor_wheels`` can attempt to download binary packages for multiple
+platforms all at once by setting a GN arg:
+
+.. code-block::
+
+ pw_build_PYTHON_PIP_DOWNLOAD_ALL_PLATFORMS = true
+
+This will invoke `pip download
+<https://pip.pypa.io/en/stable/cli/pip_download/>`_ for each combination of
+platform, architecture and Python version. This can take a significant amount of
+time to complete. The current set of combinations is shown below:
+
+.. literalinclude:: pw_build/py/pw_build/generate_python_wheel_cache.py
+ :start-after: [wheel-platform-args]
+ :end-before: [wheel-platform-args]
+
+.. warning::
+ The set of Python packages that will be downloaded is determined by the
+ ``compiled_requirements.txt`` file. This file can only be generated for the
+ current host OS and Python version. `pip-tools
+ <https://pypi.org/project/pip-tools>`_ does not expand requirements for
+ `platform specific dependencies
+ <https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#platform-specific-dependencies>`_. For
+ example ipython defines these two requirements:
+
+ .. code-block::
+
+ appnope; sys_platform == "darwin"
+ colorama; sys_platform == "win32"
+
+ If pip-tools is run on Linux then the above packages will not appear in
+ ``compiled_requirements.txt`` and not downloaded by the ``.vendor_wheels``
+ target.
+
+.. _docs-python-build-installing-offline:
+
+Installing Offline
+^^^^^^^^^^^^^^^^^^
+Once the vendor wheel output is saved to a directory in your project you can use
+this as the default pip install location in two different ways.
+
+GN Args
+.......
+Setting these args in the ``//.gn`` file will add the relevant pip command line
+args to perform offline installations.
+
+.. code-block::
+
+ # Adds --no-index forcing pip to not reach out to the internet (pypi.org) to
+ # download packages. Using this option requires setting
+ # pw_build_PYTHON_PIP_INSTALL_FIND_LINKS as well.
+ pw_build_PYTHON_PIP_INSTALL_OFFLINE = true
+
+ # List of paths to folders containing Python wheels (*.whl) or source tar
+ # files (*.tar.gz). Pip will check each of these directories when looking for
+ # potential install candidates.
+ pw_build_PYTHON_PIP_INSTALL_FIND_LINKS = [
+ "//environment/cipd/packages/python_packages/universal",
+ "//environment/cipd/packages/python_packages/linux/cp311",
+ ]
+
+ # Optional: Adds '--no-cache-dir' forcing pip to ignore any previously cached
+ # Python packages. On most systems this is located in ~/.cache/pip/
+ pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE = false
+
+Using a ``.pip.conf`` File
+..........................
+1. Create a ``//pip.conf`` file containing:
+
+ .. code-block::
+ :caption: :octicon:`file;1em` //pip.conf
+ :name: pip-conf-file
+
+ [global]
+ # Disable searching pypi.org for packages
+ no-index = True
+ # Find packages in these directories:
+ find-links =
+ file://third_party/python_packages/universal
+ file://third_party/python_packages/linux/cp311
+
+ This tells pip to not search pypi.org for packages and only look in
+ ``third_party/python_packages/universal`` and
+ ``third_party/python_packages/linux/cp311``. These paths can be absolute or
+ are relative to the ``pip.conf`` file.
+
+2. In the project ``bootstrap.sh`` set ``PIP_CONFIG_FILE`` to the location of
+ this file.
+
+ .. code-block:: bash
+
+ export PIP_CONFIG_FILE="${PW_PROJECT_ROOT}/pip.conf"
+
+ With that environment var set all invocations of pip will apply the config file
+ settings above.
+
+.. seealso::
+ The ``pip`` `documentation on Configuration
+ <https://pip.pypa.io/en/stable/topics/configuration/>`_.
+
.. _docs-python-build-python-gn-structure:
-GN Structure for Python Code
-============================
+GN File Structure for Python Code
+=================================
Here is a full example of what is required to build Python packages using
Pigweed's GN build system. A brief file hierarchy is shown here with file
content following. See also :ref:`docs-python-build-structure` below for details
on the structure of Python packages.
.. code-block::
- :caption: :octicon:`file-directory;1em` Top level GN file hierarchy
- :name: gn-python-file-tree
-
- project_root/
- ├── .gn
- ├── BUILDCONFIG.gn
- ├── build_overrides/
- │ └── pigweed.gni
- ├── BUILD.gn
- │
- ├── python_package1/
- │ ├── BUILD.gn
- │ ├── setup.cfg
- │ ├── setup.py
- │ ├── pyproject.toml
- │ │
- │ ├── package_name/
- │ │ ├── module_a.py
- │ │ ├── module_b.py
- │ │ ├── py.typed
- │ │ └── nested_package/
- │ │ ├── py.typed
- │ │ └── module_c.py
- │ │
- │ ├── module_a_test.py
- │ └── module_c_test.py
- │
- ├── third_party/
- │ └── pigweed/
- │
- └── ...
+ :caption: :octicon:`file-directory;1em` Top level GN file hierarchy
+ :name: gn-python-file-tree
+
+ project_root/
+ ├── .gn
+ ├── BUILDCONFIG.gn
+ ├── build_overrides/
+ │ └── pigweed.gni
+ ├── BUILD.gn
+ │
+ ├── python_package1/
+ │ ├── BUILD.gn
+ │ ├── setup.cfg
+ │ ├── setup.py
+ │ ├── pyproject.toml
+ │ │
+ │ ├── package_name/
+ │ │ ├── module_a.py
+ │ │ ├── module_b.py
+ │ │ ├── py.typed
+ │ │ └── nested_package/
+ │ │ ├── py.typed
+ │ │ └── module_c.py
+ │ │
+ │ ├── module_a_test.py
+ │ └── module_c_test.py
+ │
+ ├── third_party/
+ │ └── pigweed/
+ │
+ └── ...
- :octicon:`file-directory;1em` project_root/
@@ -251,6 +565,15 @@ on the structure of Python packages.
pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
}
+ .. tip::
+
+ There are some additional gn args to control how pip installations are
+ performed during the build.
+
+ .. literalinclude:: pw_build/python_gn_args.gni
+ :start-after: [default-pip-gn-args]
+ :end-before: [default-pip-gn-args]
+
- :octicon:`file;1em` BUILDCONFIG.gn
.. code-block::
@@ -365,25 +688,25 @@ modules. A module's Python package is nested under a ``py/`` directory (see
:ref:`Pigweed Module Stucture <docs-module-structure>`).
.. code-block::
- :caption: :octicon:`file-directory;1em` Example layout of a Pigweed Python package.
- :name: python-file-tree
-
- module_name/
- ├── py/
- │ ├── BUILD.gn
- │ ├── setup.cfg
- │ ├── setup.py
- │ ├── pyproject.toml
- │ ├── package_name/
- │ │ ├── module_a.py
- │ │ ├── module_b.py
- │ │ ├── py.typed
- │ │ └── nested_package/
- │ │ ├── py.typed
- │ │ └── module_c.py
- │ ├── module_a_test.py
- │ └── module_c_test.py
- └── ...
+ :caption: :octicon:`file-directory;1em` Example layout of a Pigweed Python package.
+ :name: python-file-tree
+
+ module_name/
+ ├── py/
+ │ ├── BUILD.gn
+ │ ├── setup.cfg
+ │ ├── setup.py
+ │ ├── pyproject.toml
+ │ ├── package_name/
+ │ │ ├── module_a.py
+ │ │ ├── module_b.py
+ │ │ ├── py.typed
+ │ │ └── nested_package/
+ │ │ ├── py.typed
+ │ │ └── module_c.py
+ │ ├── module_a_test.py
+ │ └── module_c_test.py
+ └── ...
The ``BUILD.gn`` declares this package in GN. For upstream Pigweed, a presubmit
check in ensures that all Python files are listed in a ``BUILD.gn``.
@@ -393,19 +716,19 @@ above file tree ``setup.py`` and ``pyproject.toml`` files are stubs with the
following content:
.. code-block::
- :caption: :octicon:`file;1em` setup.py
- :name: setup-py-stub
+ :caption: :octicon:`file;1em` setup.py
+ :name: setup-py-stub
- import setuptools # type: ignore
- setuptools.setup() # Package definition in setup.cfg
+ import setuptools # type: ignore
+ setuptools.setup() # Package definition in setup.cfg
.. code-block::
- :caption: :octicon:`file;1em` pyproject.toml
- :name: pyproject-toml-stub
+ :caption: :octicon:`file;1em` pyproject.toml
+ :name: pyproject-toml-stub
- [build-system]
- requires = ['setuptools', 'wheel']
- build-backend = 'setuptools.build_meta'
+ [build-system]
+ requires = ['setuptools', 'wheel']
+ build-backend = 'setuptools.build_meta'
The stub ``setup.py`` file is there to support running ``pip install --editable``.
@@ -418,7 +741,6 @@ setuptools.
- ``setup.cfg`` examples at `Configuring setup() using setup.cfg files`_
- ``pyproject.toml`` background at `Build System Support - How to use it?`_
-
.. _module-pw_build-python-target:
pw_python_package targets
diff --git a/docs/release_notes/2022_jan.rst b/docs/release_notes/2022_jan.rst
deleted file mode 100644
index f7244e5e2..000000000
--- a/docs/release_notes/2022_jan.rst
+++ /dev/null
@@ -1,192 +0,0 @@
-.. _docs-release-notes-2022-jan:
-
-===================================
-Pigweed: What's New in January 2022
-===================================
-Happy new year from the Pigweed team! We’re excited to share what we’ve been up
-to this month and we’re really looking forward to what 2022 will bring to the
-Pigweed community.
-
-:ref:`Pigweed<docs-root>` is a collection of libraries and tools for building
-robust embedded software, efficiently. Pigweed allows you to write code that
-runs transparently on both your host machine and tiny 32-bit microcontrollers
-like those in the :ref:`STM32<target-stm32f429i-disc1>` and
-:ref:`Arduino<target-arduino>` families, while giving you the comforts of modern
-software development traditionally lacking in embedded systems, like
-:ref:`easy unit testing<module-pw_unit_test>`,
-:ref:`powerful build systems<docs-build-system>`,
-:ref:`flexible logging<module-pw_log>`, and
-:ref:`reliable communication<module-pw_rpc>`.
-
-.. admonition:: Note
- :class: warning
-
- Many Pigweed modules are already shipping in commercial products, but it is
- still an early access project. Find out if
- :ref:`Pigweed is right for you<docs-concepts-right-for-my-project>`.
-
-Pigweed is a free and open source project and we welcome contributions! Join us
-on `Discord <https://discord.gg/M9NSeTA>`_ to share feedback, ask questions, and
-get involved with the Pigweed community!
-
-------------------------------
-Experimental Pigweed framework
-------------------------------
-.. admonition:: tl;dr
- :class: checkmark
-
- We’re starting the “whole OS” framework version of Pigweed! It’s not ready
- for use yet but you might want to take a peek.
-
-Pigweed is designed to be highly modular—you can use as many or as few of the
-Pigweed modules as you need for your project, and those modules will work
-flexibly in any project structure. This works great when you want to add Pigweed
-super powers like hybrid host/target unit testing or RPC communication to an
-existing project. While Pigweed gives you nearly all of the tools you need to
-efficiently build a robust, reliable embedded project, until now we haven’t had
-a great solution for building a new project on Pigweed.
-
-The Pigweed framework assembles an opinionated project structure, build system,
-and development environment that does three key things:
-
-* Takes care of difficult but unproductive project plumbing issues like setting
- up a target toolchain and providing support for
- :ref:`OS abstractions<docs-os_abstraction_layers>`.
-
-* Configures Pigweed module backends that give you logging, asserts, threads,
- dynamic memory allocation, and more, that work transparently both on host and
- on target
-
-* Sets up a productive development environment with rich code analysis and
- powerful device interaction tools
-
-You can experiment with this right now by checking out the ``pw_system``
-:ref:`documentation<module-pw_system>`. The experimental configuration leverages
-FreeRTOS and runs on the STM32F429I Discovery board. With a
-:ref:`few simple commands<target-stm32f429i-disc1-stm32cube>`, you can have a
-complete embedded development environment set up and focus on building your
-product.
-
-.. warning::
-
- The Pigweed framework is still in very active development and you should
- expect breaking changes in the future. If you’re experimenting with it, we
- would love to hear from you! Join us on
- `Discord <https://discord.gg/M9NSeTA>`_!
-
--------------------------------------
-Support for plugins in ``pw_console``
--------------------------------------
-Teams that use Pigweed quickly come to rely on the
-:ref:`console<module-pw_console>` as a vital tool for interacting with their
-devices via RPC. It’s now possible to tailor the console to meet your project’s
-specific needs through a new :ref:`plugin interface<module-pw_console-plugins>`.
-You can build your own menus, window panes, keybindings, and clickable buttons
-to truly make ``pw_console`` your own.
-
-How are you using the Pigweed console in your project? Let us know on
-`Discord <https://discord.gg/M9NSeTA>`_!
-
-------------------------------------
-Expanded support for Bazel and CMake
-------------------------------------
-Pigweed’s primary build system is
-`GN (Generate Ninja) <https://gn.googlesource.com/gn>`_, but to make it easier
-to use Pigweed modules in existing projects, we have been expanding support for
-the `Bazel <https://bazel.build/>`_ and `CMake <https://cmake.org/>`_ build
-systems. Right now, the best way to determine which build systems a module
-supports is to look out for ``BUILD.gn``, ``BUILD.bazel`` and ``CMakeLists.txt``
-files (respectively) in module directories. While we work on improving build
-system support and documentation, check out the
-:ref:`build system documentation<docs-build-system>` for more detailed
-information and join us on Discord for support.
-
-----------------------------------------
-Changes to the RPC ``ChannelOutput`` API
-----------------------------------------
-RPC endpoints use :ref:`ChannelOutput<module-pw_rpc-ChannelOutput>` instances to
-send packets encoding RPC data. To send an encoded RPC packet, we need a buffer
-containing the packet’s data. In the past, we could request a buffer by doing
-something like this:
-
-.. code-block:: cpp
-
- auto buffer = pw::rpc::ChannelOutput::AcquireBuffer(buffer_size)
- // fill in the buffer here
- pw::rpc::ChannelOutput::SendAndReleaseBuffer(buffer)
-
-The ``ChannelOutput::AcquireBuffer`` and ``ChannelOutput::SendAndReleaseBuffer``
-methods are no longer part of ``ChannelOutput``’s public API, making its
-internal buffer private. Now, we create our own buffer and ``ChannelOutput`` is
-simply responsible for sending it:
-
-.. code-block:: cpp
-
- auto buffer = ... // create your own local buffer with RPC packet data
- pw::rpc::ChannelOutput::Send(buffer)
-
-This approach avoids several tricky concurrency issues related to buffer
-lifetimes, and simplifies the ``ChannelOutput`` API. It also opens up the
-possibility of projects managing RPC buffers in more flexible ways, e.g. via
-dynamically-allocated memory or separate shared memory mechanisms.
-
-.. warning::
-
- This is a breaking change if you update pw_rpc, but one that can be fixed
- quickly.
-
-We’re actively reviewing the RPC API with a view towards significantly improving
-it in the future. Share your input with us on
-`Discord <https://discord.gg/M9NSeTA>`_!
-
-------------
-More Updates
-------------
-* It’s now possible to generate a token database from a list of strings in a
- JSON file for ``pw_tokenizer``. This can be useful when you need to tokenize
- strings that can’t be parsed from compiled binaries.
-
-* ``pw_assert``‘s new ``pw_assert_tokenized`` backend provides a much more
- space-efficient implementation compared to using ``pw_assert_log`` with
- ``pw_log_tokenized``. However, there are trade offs to consider, so check out
- the :ref:`documentation<module-pw_assert_tokenized>`.
-
-* CMake builds now support compile-time module configuration similar to GN
- through the use of the ``pw_add_module_config`` and ``pw_set_module_config``
- functions.
-
-* In ``pw_build``, it is now possible to set a specific working directory for
- :ref:`pw_exec<module-pw_build-pw_exec>` actions.
-
-* ``pw_cpu_exception`` now supports the ARMv8M Mainline architecture in
- ``pw_cpu_exception_cortex_m``. This allows us to take advantage of stack limit
- boundary features in microcontrollers using that architecture, like Cortex M33
- and M35P.
-
-------------
-Get Involved
-------------
-.. tip::
-
- We welcome contributions from the community! Here are just a few
- opportunities to get involved.
-
-* Pigweed now includes GN build files for
- `TinyUSB <https://github.com/hathach/tinyusb>`_, a popular USB library for
- embedded systems. Projects can now include it by cloning the TinyUSB
- repository and configuring GN to build it. But right now, we lack interfaces
- between TinyUSB and Pigweed abstractions like pw_stream. This is a great
- opportunity to help get very useful functionality across the finish line.
-
-* We’re very interested in supporting the
- `Raspberry Pi Pico <https://www.raspberrypi.com/products/raspberry-pi-pico/>`_
- and the ecosystem of devices using the RP2040 microcontroller. We will be
- working in earnest on this in the coming months and welcome anyone who wants
- to lend a helping hand!
-
-* Evolving the Pigweed framework from its current experimental state to a
- relatively complete embedded project platform is one of our major focuses this
- year, and we want your help. That help can range from providing input on what
- you’re looking for in a framework, to building small projects with it and
- providing feedback, up to contributing directly to its development. Join us to
- talk about it on `Discord <https://discord.gg/M9NSeTA>`_!
diff --git a/docs/release_notes/index.rst b/docs/release_notes/index.rst
deleted file mode 100644
index 4340b002d..000000000
--- a/docs/release_notes/index.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-.. _docs-release-notes:
-
-=============
-Release Notes
-=============
-
-.. toctree::
- :titlesonly:
-
- January 2022 <2022_jan>
diff --git a/docs/style/commit_message.rst b/docs/style/commit_message.rst
new file mode 100644
index 000000000..5b4dbaff7
--- /dev/null
+++ b/docs/style/commit_message.rst
@@ -0,0 +1,296 @@
+.. _docs-pw-style-commit-message:
+
+====================
+Commit message style
+====================
+Pigweed commit message bodies and summaries are limited to 72 characters wide to
+improve readability, and should be prefixed with the name of the module that the
+commit is affecting. The commits should describe what is changed, and why. When
+writing long commit messages, consider whether the content should go in the
+documentation or code comments instead. For example:
+
+.. code-block:: none
+
+ pw_some_module: Short capitalized description
+
+ Details about the change here. Include a summary of what changed, and a clear
+ description of why the change is needed. Consider what parts of the commit
+ message are better suited for documentation or code.
+
+ - Added foo, to fix issue bar
+ - Improved speed of qux
+ - Refactored and extended qux's test suite
+
+-----------------------------
+Include both "what" and "why"
+-----------------------------
+It is important to include a "why" component in most commits. Sometimes, why is
+evident - for example, reducing memory usage, optimizing, or fixing a bug.
+Otherwise, err on the side of over-explaining why, not under-explaining why.
+
+When adding the "why" to a commit, also consider if that "why" content should go
+into the documentation or code comments.
+
+.. admonition:: **Yes**: Reasoning in commit message
+ :class: checkmark
+
+ .. code-block:: none
+
+ pw_sync_xrtos: Invoke deadlock detector
+
+ During locking, run the deadlock detector if there are enough cycles.
+ Though this costs performance, several bugs that went unnoticed would have
+ been caught by turning this on earlier. Take the small hit by default to
+ better catch issues going forward; see extended docs for details.
+
+.. admonition:: **No**: Reasoning omitted
+ :class: error
+
+ .. code-block:: none
+
+ pw_sync_xrtos: Invoke deadlock detector
+
+ During locking, run the deadlock detector if there are enough cycles.
+
+------------------
+Present imperative
+------------------
+Use present imperative style instead of passive descriptive.
+
+.. admonition:: **Yes**: Uses imperative style for subject and text.
+ :class: checkmark
+
+ .. code-block:: none
+
+ pw_something: Add foo and bar functions
+
+ This commit correctly uses imperative present-tense style.
+
+.. admonition:: **No**: Uses non-imperative style for subject and text.
+ :class: error
+
+ .. code-block:: none
+
+ pw_something: Adds more things
+
+ Use present tense imperative style for subjects and commit. The above
+ subject has a plural "Adds" which is incorrect; should be "Add".
+
+---------------------------------------
+Documentation instead of commit content
+---------------------------------------
+Consider whether any of the commit message content should go in the
+documentation or code comments and have the commit message reference it.
+Documentation and code comments are durable and discoverable; commit messages
+are rarely read after the change lands.
+
+.. admonition:: **Yes**: Created docs and comments
+ :class: checkmark
+
+ .. code-block:: none
+
+ pw_i2c: Add and enforce invariants
+
+ Precisely define the invariants around certain transaction states, and
+ extend the code to enforce them. See the newly extended documentation in
+ this change for details.
+
+.. admonition:: **No**: Important content only in commit message
+ :class: error
+
+ .. code-block:: none
+
+ pw_i2c: Add and enforce invariants
+
+ Add a new invariant such that before a transaction, the line must be high;
+ and after, the line must be low, due to XXX and YYY. Furthermore, users
+ should consider whether they could ever encounter XXX, and in that case
+ should ZZZ instead.
+
+---------------------------
+Lists instead of paragraphs
+---------------------------
+Use bulleted lists when multiple changes are in a single CL. Ideally, try to
+create smaller CLs so this isn't needed, but larger CLs are a practical reality.
+
+.. admonition:: **Yes**: Uses bulleted lists
+ :class: checkmark
+
+ .. code-block:: none
+
+ pw_complicated_module: Pre-work for refactor
+
+ Prepare for a bigger refactor by reworking some arguments before the larger
+ change. This change must land in downstream projects before the refactor to
+ enable a smooth transition to the new API.
+
+ - Add arguments to MyImportantClass::MyFunction
+ - Update MyImportantClass to handle precondition Y
+ - Add stub functions to be used during the transition
+
+.. admonition:: **No**: Long paragraph is hard to scan
+ :class: error
+
+ .. code-block:: none
+
+ pw_foo: Many things in a giant BWOT
+
+ This CL does A, B, and C. The commit message is a Big Wall Of Text
+ (BWOT), which we try to discourage in Pigweed. Also changes X and Y,
+ because Z and Q. Furthermore, in some cases, adds a new Foo (with Bar,
+ because we want to). Also refactors qux and quz.
+
+------------
+Subject line
+------------
+The subject line (first line of the commit) should take the form ``pw_<module>:
+Short description``. The module should not be capitalized, but the description
+should (unless the first word is an identifier). See below for the details.
+
+.. admonition:: **No**: Uses a non-standard ``[]`` to indicate module:
+ :class: error
+
+ .. code-block:: none
+
+ [pw_foo]: Do a thing
+
+.. admonition:: **No**: Has a period at the end of the subject
+ :class: error
+
+ .. code-block:: none
+
+ pw_bar: Do something great.
+
+.. admonition:: **No**: Puts extra stuff after the module which isn't a module.
+ :class: error
+
+ .. code-block:: none
+
+ pw_bar/byte_builder: Add more stuff to builder
+
+Multiple modules
+================
+Sometimes it is necessary to change code across multiple modules.
+
+#. **2-5 modules**: Use ``{}`` syntax shown below
+#. **>5 modules changed** - Omit the module names entirely
+#. **Changes mostly in one module** - If the commit mostly changes the
+ code in a single module with some small changes elsewhere, only list the
+ module receiving most of the content
+
+.. admonition:: **Yes**: Small number of modules affected; use {} syntax.
+ :class: checkmark
+
+ .. code-block:: none
+
+ pw_{foo, bar, baz}: Change something in a few places
+
+ When changes cross a few modules, include them with the syntax shown
+ above.
+
+.. admonition:: **Yes**: Many modules changed
+ :class: checkmark
+
+ .. code-block:: none
+
+ Change convention for how errors are handled
+
+ When changes cross many modules, skip the module name entirely.
+
+.. admonition:: **No**: Too many modules changed for subject
+ :class: error
+
+ .. code-block:: none
+
+ pw_{a, b, c, d, e, f, g, h, i, j}: Change convention for how errors are handled
+
+ When changes cross many modules, skip the module name entirely.
+
+Non-standard modules
+====================
+Most Pigweed modules follow the format of ``pw_<foo>``; however, some do not,
+such as targets. Targets are effectively modules, even though they're nested, so
+they get a ``/`` character.
+
+.. admonition:: **Yes**:
+ :class: checkmark
+
+ .. code-block:: none
+
+ targets/xyz123: Tweak support for XYZ's PQR
+
+ PQR is needed for reason ZXW; this adds a performant implementation.
+
+Capitalization
+==============
+The text after the ``:`` should be capitalized, provided the first word is not a
+case-sensitive symbol.
+
+.. admonition:: **No**: Doesn't capitalize the subject
+ :class: error
+
+ .. code-block:: none
+
+ pw_foo: do a thing
+
+ Above subject is incorrect, since it is a sentence style subject.
+
+.. admonition:: **Yes**: Doesn't capitalize the subject when subject's first
+ word is a lowercase identifier.
+ :class: checkmark
+
+ .. code-block:: none
+
+ pw_foo: std::unique_lock cleanup
+
+ This commit message demonstrates the subject when the subject has an
+ identifier for the first word. In that case, follow the identifier casing
+ instead of capitalizing.
+
+ However, imperative style subjects often have the identifier elsewhere in
+ the subject; for example:
+
+ .. code-block:: none
+
+ pw_foo: Improve use of std::unique_lock
+
+------
+Footer
+------
+We support a number of `git footers`_ in the commit message, such as ``Bug:
+123`` in the message below:
+
+.. code-block:: none
+
+ pw_something: Add foo and bar functions
+
+ Bug: 123
+
+You are encouraged to use the following footers when appropriate:
+
+* ``Bug``: Associates this commit with a bug (issue in our `bug tracker`_). The
+ bug will be automatically updated when the change is submitted. When a change
+ is relevant to more than one bug, include multiple ``Bug`` lines, like so:
+
+ .. code-block:: none
+
+ pw_something: Add foo and bar functions
+
+ Bug: 123
+ Bug: 456
+
+* ``Fixed`` or ``Fixes``: Like ``Bug``, but automatically closes the bug when
+ submitted.
+
+ .. code-block:: none
+
+ pw_something: Fix incorrect use of foo
+
+ Fixes: 123
+
+In addition, we support all of the `Chromium CQ footers`_, but those are
+relatively rarely useful.
+
+.. _bug tracker: https://bugs.chromium.org/p/pigweed/issues/list
+.. _Chromium CQ footers: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/infra/cq.md#options
+.. _git footers: https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/git-footers.html
diff --git a/docs/style/cpp.rst b/docs/style/cpp.rst
new file mode 100644
index 000000000..73b78f4f1
--- /dev/null
+++ b/docs/style/cpp.rst
@@ -0,0 +1,826 @@
+.. _docs-pw-style-cpp:
+
+=========
+C++ style
+=========
+The Pigweed C++ style guide is closely based on Google's external C++ Style
+Guide, which is found on the web at
+https://google.github.io/styleguide/cppguide.html. The Google C++ Style Guide
+applies to Pigweed except as described in this document.
+
+The Pigweed style guide only applies to Pigweed itself. It does not apply to
+projects that use Pigweed or to the third-party code included with Pigweed.
+Non-Pigweed code is free to use features restricted by Pigweed, such as dynamic
+memory allocation and the entirety of the C++ Standard Library.
+
+Recommendations in the :ref:`docs-embedded-cpp` are considered part of the
+Pigweed style guide, but are separated out since it covers more general
+embedded development beyond just C++ style.
+
+C++ standard
+============
+All Pigweed C++ code must compile with ``-std=c++17`` in Clang and GCC. C++20
+features may be used as long as the code still compiles unmodified with C++17.
+See ``pw_polyfill/language_feature_macros.h`` for macros that provide C++20
+features when supported.
+
+Compiler extensions should not be used unless wrapped in a macro or properly
+guarded in the preprocessor. See ``pw_processor/compiler.h`` for macros that
+wrap compiler-specific features.
+
+Automatic formatting
+====================
+Pigweed uses `clang-format <https://clang.llvm.org/docs/ClangFormat.html>`_ to
+automatically format Pigweed source code. A ``.clang-format`` configuration is
+provided with the Pigweed repository.
+
+Automatic formatting is essential to facilitate large-scale, automated changes
+in Pigweed. Therefore, all code in Pigweed is expected to be formatted with
+``clang-format`` prior to submission. Existing code may be reformatted at any
+time.
+
+If ``clang-format`` formats code in an undesirable or incorrect way, it can be
+disabled for the affected lines by adding ``// clang-format off``.
+``clang-format`` must then be re-enabled with a ``// clang-format on`` comment.
+
+.. code-block:: cpp
+
+ // clang-format off
+ constexpr int kMyMatrix[] = {
+ 100, 23, 0,
+ 0, 542, 38,
+ 1, 2, 201,
+ };
+ // clang-format on
+
+C Standard Library
+==================
+In C++ headers, always use the C++ versions of C Standard Library headers (e.g.
+``<cstdlib>`` instead of ``<stdlib.h>``). If the header is used by both C and
+C++ code, only the C header should be used.
+
+In C++ code, it is preferred to use C functions from the ``std`` namespace. For
+example, use ``std::memcpy`` instead of ``memcpy``. The C++ standard does not
+require the global namespace versions of the functions to be provided. Using
+``std::`` is more consistent with the C++ Standard Library and makes it easier
+to distinguish Pigweed functions from library functions.
+
+Within core Pigweed, do not use C standard library functions that allocate
+memory, such as ``std::malloc``. There are exceptions to this for when dynamic
+allocation is enabled for a system; Pigweed modules are allowed to add extra
+functionality when a heap is present; but this must be optional.
+
+C++ Standard Library
+====================
+Much of the C++ Standard Library is not a good fit for embedded software. Many
+of the classes and functions were not designed with the RAM, flash, and
+performance constraints of a microcontroller in mind. For example, simply
+adding the line ``#include <iostream>`` can increase the binary size by 150 KB!
+This is larger than many microcontrollers' entire internal storage.
+
+However, with appropriate caution, a limited set of standard C++ libraries can
+be used to great effect. Developers can leverage familiar, well-tested
+abstractions instead of writing their own. C++ library algorithms and classes
+can give equivalent or better performance than hand-written C code.
+
+A limited subset of the C++ Standard Library is permitted in Pigweed. To keep
+Pigweed small, flexible, and portable, functions that allocate dynamic memory
+must be avoided. Care must be exercised when using multiple instantiations of a
+template function, which can lead to code bloat.
+
+Permitted Headers
+-----------------
+.. admonition:: The following C++ Standard Library headers are always permitted:
+ :class: checkmark
+
+ * ``<array>``
+ * ``<complex>``
+ * ``<initializer_list>``
+ * ``<iterator>``
+ * ``<limits>``
+ * ``<optional>``
+ * ``<random>``
+ * ``<ratio>``
+ * ``<string_view>``
+ * ``<tuple>``
+ * ``<type_traits>``
+ * ``<utility>``
+ * ``<variant>``
+ * C Standard Library headers (``<c*>``)
+
+.. admonition:: With caution, parts of the following headers can be used:
+ :class: warning
+
+ * ``<algorithm>`` -- be wary of potential memory allocation
+ * ``<atomic>`` -- not all MCUs natively support atomic operations
+ * ``<bitset>`` -- conversions to or from strings are disallowed
+ * ``<functional>`` -- do **not** use ``std::function``; use
+ :ref:`module-pw_function`
+ * ``<mutex>`` -- can use ``std::lock_guard``, use :ref:`module-pw_sync` for
+ mutexes
+ * ``<new>`` -- for placement new
+ * ``<numeric>`` -- be wary of code size with multiple template instantiations
+
+.. admonition:: Never use any of these headers:
+ :class: error
+
+ * Dynamic containers (``<list>``, ``<map>``, ``<set>``, ``<vector>``, etc.)
+ * Streams (``<iostream>``, ``<ostream>``, ``<fstream>``, ``<sstream>`` etc.)
+ -- in some cases :ref:`module-pw_stream` can work instead
+ * ``<span>`` -- use :ref:`module-pw_span` instead. Downstream projects may
+ want to directly use ``std::span`` if it is available, but upstream must
+ use the ``pw::span`` version for compatability
+ * ``<string>`` -- can use :ref:`module-pw_string`
+ * ``<thread>`` -- can use :ref:`module-pw_thread`
+ * ``<future>`` -- eventually :ref:`module-pw_async` will offer this
+ * ``<exception>``, ``<stdexcept>`` -- no exceptions
+ * ``<memory>``, ``<scoped_allocator>`` -- no allocations
+ * ``<regex>``
+ * ``<valarray>``
+
+Headers not listed here should be carefully evaluated before they are used.
+
+These restrictions do not apply to third party code or to projects that use
+Pigweed.
+
+Combining C and C++
+===================
+Prefer to write C++ code over C code, using ``extern "C"`` for symbols that must
+have C linkage. ``extern "C"`` functions should be defined within C++
+namespaces to simplify referring to other code.
+
+C++ functions with no parameters do not include ``void`` in the parameter list.
+C functions with no parameters must include ``void``.
+
+.. code-block:: cpp
+
+ namespace pw {
+
+ bool ThisIsACppFunction() { return true; }
+
+ extern "C" int pw_ThisIsACFunction(void) { return -1; }
+
+ extern "C" {
+
+ int pw_ThisIsAlsoACFunction(void) {
+ return ThisIsACppFunction() ? 100 : 0;
+ }
+
+ } // extern "C"
+
+ } // namespace pw
+
+Comments
+========
+Prefer C++-style (``//``) comments over C-style comments (``/* */``). C-style
+comments should only be used for inline comments.
+
+.. code-block:: cpp
+
+ // Use C++-style comments, except where C-style comments are necessary.
+ // This returns a random number using an algorithm I found on the internet.
+ #define RANDOM_NUMBER() [] { \
+ return 4; /* chosen by fair dice roll */ \
+ }()
+
+Indent code in comments with two additional spaces, making a total of three
+spaces after the ``//``. All code blocks must begin and end with an empty
+comment line, even if the blank comment line is the last line in the block.
+
+.. code-block:: cpp
+
+ // Here is an example of code in comments.
+ //
+ // int indentation_spaces = 2;
+ // int total_spaces = 3;
+ //
+ // engine_1.thrust = RANDOM_NUMBER() * indentation_spaces + total_spaces;
+ //
+ bool SomeFunction();
+
+Control statements
+==================
+
+Loops and conditionals
+----------------------
+All loops and conditional statements must use braces, and be on their own line.
+
+.. admonition:: **Yes**: Always use braces for line conditionals and loops:
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ while (SomeCondition()) {
+ x += 2;
+ }
+ if (OtherCondition()) {
+ DoTheThing();
+ }
+
+
+.. admonition:: **No**: Missing braces
+ :class: error
+
+ .. code-block:: cpp
+
+ while (SomeCondition())
+ x += 2;
+ if (OtherCondition())
+ DoTheThing();
+
+.. admonition:: **No**: Statement on same line as condition
+ :class: error
+
+ .. code-block:: cpp
+
+ while (SomeCondition()) { x += 2; }
+ if (OtherCondition()) { DoTheThing(); }
+
+
+The syntax ``while (true)`` is preferred over ``for (;;)`` for infinite loops.
+
+.. admonition:: **Yes**:
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ while (true) {
+ DoSomethingForever();
+ }
+
+.. admonition:: **No**:
+ :class: error
+
+ .. code-block:: cpp
+
+ for (;;) {
+ DoSomethingForever();
+ }
+
+
+Prefer early exit with ``return`` and ``continue``
+--------------------------------------------------
+Prefer to exit early from functions and loops to simplify code. This is the
+same same conventions as `LLVM
+<https://llvm.org/docs/CodingStandards.html#use-early-exits-and-continue-to-simplify-code>`_.
+We find this approach is superior to the "one return per function" style for a
+multitude of reasons:
+
+* **Visually**, the code is easier to follow, and takes less horizontal screen
+ space.
+* It makes it clear what part of the code is the **"main business" versus "edge
+ case handling"**.
+* For **functions**, parameter checking is in its own section at the top of the
+ function, rather than scattered around in the fuction body.
+* For **loops**, element checking is in its own section at the top of the loop,
+ rather than scattered around in the loop body.
+* Commit **deltas are simpler to follow** in code reviews; since adding a new
+ parameter check or loop element condition doesn't cause an indentation change
+ in the rest of the function.
+
+The guidance applies in two cases:
+
+* **Function early exit** - Early exits are for function parameter checking
+ and edge case checking at the top. The main functionality follows.
+* **Loop early exit** - Early exits in loops are for skipping an iteration
+ due to some edge case with an item getting iterated over. Loops may also
+ contain function exits, which should be structured the same way (see example
+ below).
+
+.. admonition:: **Yes**: Exit early from functions; keeping the main handling
+ at the bottom and de-dentend.
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ Status DoSomething(Parameter parameter) {
+ // Parameter validation first; detecting incoming use errors.
+ PW_CHECK_INT_EQ(parameter.property(), 3, "Programmer error: frobnitz");
+
+ // Error case: Not in correct state.
+ if (parameter.other() == MyEnum::kBrokenState) {
+ LOG_ERROR("Device in strange state: %s", parametr.state_str());
+ return Status::InvalidPrecondition();
+ }
+
+ // Error case: Not in low power mode; shouldn't do anything.
+ if (parameter.power() != MyEnum::kLowPower) {
+ LOG_ERROR("Not in low power mode");
+ return Status::InvalidPrecondition();
+ }
+
+ // Main business for the function here.
+ MainBody();
+ MoreMainBodyStuff();
+ }
+
+.. admonition:: **No**: Main body of function is buried and right creeping.
+ Even though this is shorter than the version preferred by Pigweed due to
+ factoring the return statement, the logical structure is less obvious. A
+ function in Pigweed containing **nested conditionals indicates that
+ something complicated is happening with the flow**; otherwise it would have
+ the early bail structure; so pay close attention.
+ :class: error
+
+ .. code-block:: cpp
+
+ Status DoSomething(Parameter parameter) {
+ // Parameter validation first; detecting incoming use errors.
+ PW_CHECK_INT_EQ(parameter.property(), 3, "Programmer error: frobnitz");
+
+ // Error case: Not in correct state.
+ if (parameter.other() != MyEnum::kBrokenState) {
+ // Error case: Not in low power mode; shouldn't do anything.
+ if (parameter.power() == MyEnum::kLowPower) {
+ // Main business for the function here.
+ MainBody();
+ MoreMainBodyStuff();
+ } else {
+ LOG_ERROR("Not in low power mode");
+ }
+ } else {
+ LOG_ERROR("Device in strange state: %s", parametr.state_str());
+ }
+ return Status::InvalidPrecondition();
+ }
+
+.. admonition:: **Yes**: Bail early from loops; keeping the main handling at
+ the bottom and de-dentend.
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ for (int i = 0; i < LoopSize(); ++i) {
+ // Early skip of item based on edge condition.
+ if (!CommonCase()) {
+ continue;
+ }
+ // Early exit of function based on error case.
+ int my_measurement = GetSomeMeasurement();
+ if (my_measurement < 10) {
+ LOG_ERROR("Found something strange; bailing");
+ return Status::Unknown();
+ }
+
+ // Main body of the loop.
+ ProcessItem(my_items[i], my_measurement);
+ ProcessItemMore(my_items[i], my_measurement, other_details);
+ ...
+ }
+
+.. admonition:: **No**: Right-creeping code with the main body buried inside
+ some nested conditional. This makes it harder to understand what is the
+ main purpose of the loop versus what is edge case handling.
+ :class: error
+
+ .. code-block:: cpp
+
+ for (int i = 0; i < LoopSize(); ++i) {
+ if (CommonCase()) {
+ int my_measurement = GetSomeMeasurement();
+ if (my_measurement >= 10) {
+ // Main body of the loop.
+ ProcessItem(my_items[i], my_measurement);
+ ProcessItemMore(my_items[i], my_measurement, other_details);
+ ...
+ } else {
+ LOG_ERROR("Found something strange; bailing");
+ return Status::Unknown();
+ }
+ }
+ }
+
+There are cases where this structure doesn't work, and in those cases, it is
+fine to structure the code differently.
+
+No ``else`` after ``return`` or ``continue``
+--------------------------------------------
+Do not put unnecessary ``} else {`` blocks after blocks that terminate with a
+return, since this causes unnecessary rightward indentation creep. This
+guidance pairs with the preference for early exits to reduce code duplication
+and standardize loop/function structure.
+
+.. admonition:: **Yes**: No else after return or continue
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ // Note lack of else block due to return.
+ if (Failure()) {
+ DoTheThing();
+ return Status::ResourceExausted();
+ }
+
+ // Note lack of else block due to continue.
+ while (MyCondition()) {
+ if (SomeEarlyBail()) {
+ continue;
+ }
+ // Main handling of item
+ ...
+ }
+
+ DoOtherThing();
+ return OkStatus();
+
+.. admonition:: **No**: Else after return needlessly creeps right
+ :class: error
+
+ .. code-block:: cpp
+
+ if (Failure()) {
+ DoTheThing();
+ return Status::ResourceExausted();
+ } else {
+ while (MyCondition()) {
+ if (SomeEarlyBail()) {
+ continue;
+ } else {
+ // Main handling of item
+ ...
+ }
+ }
+ DoOtherThing();
+ return OkStatus();
+ }
+
+Include guards
+==============
+The first non-comment line of every header file must be ``#pragma once``. Do
+not use traditional macro include guards. The ``#pragma once`` should come
+directly after the Pigweed copyright block, with no blank line, followed by a
+blank, like this:
+
+.. code-block:: cpp
+
+ // Copyright 2021 The Pigweed Authors
+ //
+ // Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ // use this file except in compliance with the License. You may obtain a copy of
+ // the License at
+ //
+ // https://www.apache.org/licenses/LICENSE-2.0
+ //
+ // Unless required by applicable law or agreed to in writing, software
+ // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ // License for the specific language governing permissions and limitations under
+ // the License.
+ #pragma once
+
+ // Header file-level comment goes here...
+
+Memory allocation
+=================
+Dynamic memory allocation can be problematic. Heap allocations and deallocations
+occupy valuable CPU cycles. Memory usage becomes nondeterministic, which can
+result in a system crashing without a clear culprit.
+
+To keep Pigweed portable, core Pigweed code is not permitted to dynamically
+(heap) allocate memory, such as with ``malloc`` or ``new``. All memory should be
+allocated with automatic (stack) or static (global) storage duration. Pigweed
+must not use C++ libraries that use dynamic allocation.
+
+Projects that use Pigweed are free to use dynamic allocation, provided they
+have selected a target that enables the heap.
+
+Naming
+======
+Entities shall be named according to the `Google style guide
+<https://google.github.io/styleguide/cppguide.html>`_, with the following
+additional requirements.
+
+C++ code
+--------
+* All Pigweed C++ code must be in the ``pw`` namespace. Namespaces for modules
+ should be nested under ``pw``. For example, ``pw::string::Format()``.
+* Whenever possible, private code should be in a source (.cc) file and placed in
+ anonymous namespace nested under ``pw``.
+* If private code must be exposed in a header file, it must be in a namespace
+ nested under ``pw``. The namespace may be named for its subsystem or use a
+ name that designates it as private, such as ``internal``.
+* Template arguments for non-type names (e.g. ``template <int kFooBar>``) should
+ follow the constexpr and const variable Google naming convention, which means
+ k prefixed camel case (e.g. ``kCamelCase``). This matches the Google C++
+ style for variable naming, however the wording in the official style guide
+ isn't explicit for template arguments and could be interpreted to use
+ ``foo_bar`` style naming. For consistency with other variables whose value is
+ always fixed for the duration of the program, the naming convention is
+ ``kCamelCase``, and so that is the style we use in Pigweed.
+* Trivial membor accessors should be named with ``snake_case()``. The Google
+ C++ style allows either ``snake_case()`` or ``CapsCase()``, but Pigweed
+ always uses ``snake_case()``.
+* Abstract base classes should be named generically, with derived types named
+ specifically. For example, ``Stream`` is an abstract base, and
+ ``SocketStream`` and ``StdioStream`` are an implementations of that
+ interface. Any prefix or postfix indicating whether something is abstract or
+ concrete is not permitted; for example, ``IStream`` or ``SocketStreamImpl``
+ are both not permitted. These pre-/post-fixes add additional visual noise and
+ are irrelevant to consumers of these interfaces.
+
+C code
+------
+In general, C symbols should be prefixed with the module name. If the symbol is
+not associated with a module, use just ``pw`` as the module name. Facade
+backends may chose to prefix symbols with the facade's name to help reduce the
+length of the prefix.
+
+* Public names used by C code must be prefixed with the module name (e.g.
+ ``pw_tokenizer_*``).
+* If private code must be exposed in a header, private names used by C code must
+ be prefixed with an underscore followed by the module name (e.g.
+ ``_pw_assert_*``).
+* Avoid writing C source (.c) files in Pigweed. Prefer to write C++ code with C
+ linkage using ``extern "C"``. Within C source, private C functions and
+ variables must be named with the ``_pw_my_module_*`` prefix and should be
+ declared ``static`` whenever possible; for example,
+ ``_pw_my_module_MyPrivateFunction``.
+* The C prefix rules apply to
+
+ * C functions (``int pw_foo_FunctionName(void);``),
+ * variables used by C code (``int pw_foo_variable_name;``),
+ * constant variables used by C code (``const int pw_foo_kConstantName;``),
+ * structs used by C code (``typedef struct {} pw_foo_StructName;``), and
+ * all of the above for ``extern "C"`` names in C++ code.
+
+ The prefix does not apply to struct members, which use normal Google style.
+
+Preprocessor macros
+-------------------
+* Public Pigweed macros must be prefixed with the module name (e.g.
+ ``PW_MY_MODULE_*``).
+* Private Pigweed macros must be prefixed with an underscore followed by the
+ module name (e.g. ``_PW_MY_MODULE_*``). (This style may change, see
+ `b/234886184 <https://issuetracker.google.com/issues/234886184>`_).
+
+**Example**
+
+.. code-block:: cpp
+
+ namespace pw::my_module {
+ namespace nested_namespace {
+
+ // C++ names (types, variables, functions) must be in the pw namespace.
+ // They are named according to the Google style guide.
+ constexpr int kGlobalConstant = 123;
+
+ // Prefer using functions over extern global variables.
+ extern int global_variable;
+
+ class Class {};
+
+ void Function();
+
+ extern "C" {
+
+ // Public Pigweed code used from C must be prefixed with pw_.
+ extern const int pw_my_module_kGlobalConstant;
+
+ extern int pw_my_module_global_variable;
+
+ void pw_my_module_Function(void);
+
+ typedef struct {
+ int member_variable;
+ } pw_my_module_Struct;
+
+ // Private Pigweed code used from C must be prefixed with _pw_.
+ extern const int _pw_my_module_kPrivateGlobalConstant;
+
+ extern int _pw_my_module_private_global_variable;
+
+ void _pw_my_module_PrivateFunction(void);
+
+ typedef struct {
+ int member_variable;
+ } _pw_my_module_PrivateStruct;
+
+ } // extern "C"
+
+ // Public macros must be prefixed with PW_.
+ #define PW_MY_MODULE_PUBLIC_MACRO(arg) arg
+
+ // Private macros must be prefixed with _PW_.
+ #define _PW_MY_MODULE_PRIVATE_MACRO(arg) arg
+
+ } // namespace nested_namespace
+ } // namespace pw::my_module
+
+See :ref:`docs-pw-style-macros` for details about macro usage.
+
+Namespace scope formatting
+==========================
+All non-indented blocks (namespaces, ``extern "C"`` blocks, and preprocessor
+conditionals) must have a comment on their closing line with the
+contents of the starting line.
+
+All nested namespaces should be declared together with no blank lines between
+them.
+
+.. code-block:: cpp
+
+ #include "some/header.h"
+
+ namespace pw::nested {
+ namespace {
+
+ constexpr int kAnonConstantGoesHere = 0;
+
+ } // namespace
+
+ namespace other {
+
+ const char* SomeClass::yes = "no";
+
+ bool ThisIsAFunction() {
+ #if PW_CONFIG_IS_SET
+ return true;
+ #else
+ return false;
+ #endif // PW_CONFIG_IS_SET
+ }
+
+ extern "C" {
+
+ const int pw_kSomeConstant = 10;
+ int pw_some_global_variable = 600;
+
+ void pw_CFunction() { ... }
+
+ } // extern "C"
+
+ } // namespace
+ } // namespace pw::nested
+
+Using directives for literals
+=============================
+`Using-directives
+<https://en.cppreference.com/w/cpp/language/namespace#Using-directives>`_ (e.g.
+``using namespace ...``) are permitted in implementation files only for the
+purposes of importing literals such as ``std::chrono_literals`` or
+``pw::bytes::unit_literals``. Namespaces that contain any symbols other than
+literals are not permitted in a using-directive. This guidance also has no
+impact on `using-declarations
+<https://en.cppreference.com/w/cpp/language/namespace#Using-declarations>`_
+(e.g. ``using foo::Bar;``).
+
+Rationale: Literals improve code readability, making units clearer at the point
+of definition.
+
+.. code-block:: cpp
+
+ using namespace std::chrono; // Not allowed
+ using namespace std::literals::chrono_literals; // Allowed
+
+ constexpr std::chrono::duration delay = 250ms;
+
+Pointers and references
+=======================
+For pointer and reference types, place the asterisk or ampersand next to the
+type.
+
+.. code-block:: cpp
+
+ int* const number = &that_thing;
+ constexpr const char* kString = "theory!"
+
+ bool FindTheOneRing(const Region& where_to_look) { ... }
+
+Prefer storing references over storing pointers. Pointers are required when the
+pointer can change its target or may be ``nullptr``. Otherwise, a reference or
+const reference should be used.
+
+.. _docs-pw-style-macros:
+
+Preprocessor macros
+===================
+Macros should only be used when they significantly improve upon the C++ code
+they replace. Macros should make code more readable, robust, and safe, or
+provide features not possible with standard C++, such as stringification, line
+number capturing, or conditional compilation. When possible, use C++ constructs
+like constexpr variables in place of macros. Never use macros as constants,
+except when a string literal is needed or the value must be used by C code.
+
+When macros are needed, the macros should be accompanied with extensive tests
+to ensure the macros are hard to use wrong.
+
+Stand-alone statement macros
+----------------------------
+Macros that are standalone statements must require the caller to terminate the
+macro invocation with a semicolon (see `Swalling the Semicolon
+<https://gcc.gnu.org/onlinedocs/cpp/Swallowing-the-Semicolon.html>`_). For
+example, the following does *not* conform to Pigweed's macro style:
+
+.. code-block:: cpp
+
+ // BAD! Definition has built-in semicolon.
+ #define PW_LOG_IF_BAD(mj) \
+ CallSomeFunction(mj);
+
+ // BAD! Compiles without error; semicolon is missing.
+ PW_LOG_IF_BAD("foo")
+
+Here's how to do this instead:
+
+.. code-block:: cpp
+
+ // GOOD; requires semicolon to compile.
+ #define PW_LOG_IF_BAD(mj) \
+ CallSomeFunction(mj)
+
+ // GOOD; fails to compile due to lacking semicolon.
+ PW_LOG_IF_BAD("foo")
+
+For macros in function scope that do not already require a semicolon, the
+contents can be placed in a ``do { ... } while (0)`` loop.
+
+.. code-block:: cpp
+
+ #define PW_LOG_IF_BAD(mj) \
+ do { \
+ if (mj.Bad()) { \
+ Log(#mj " is bad") \
+ } \
+ } while (0)
+
+Standalone macros at global scope that do not already require a semicolon can
+add a ``static_assert`` declaration statement as their last line.
+
+.. code-block:: cpp
+
+ #define PW_NEAT_THING(thing) \
+ bool IsNeat_##thing() { return true; } \
+ static_assert(true, "Macros must be terminated with a semicolon")
+
+Private macros in public headers
+--------------------------------
+Private macros in public headers must be prefixed with ``_PW_``, even if they
+are undefined after use; this prevents collisions with downstream users. For
+example:
+
+.. code-block:: cpp
+
+ #define _PW_MY_SPECIAL_MACRO(op) ...
+ ...
+ // Code that uses _PW_MY_SPECIAL_MACRO()
+ ...
+ #undef _PW_MY_SPECIAL_MACRO
+
+Macros in private implementation files (.cc)
+--------------------------------------------
+Macros within .cc files that should only be used within one file should be
+undefined after their last use; for example:
+
+.. code-block:: cpp
+
+ #define DEFINE_OPERATOR(op) \
+ T operator ## op(T x, T y) { return x op y; } \
+ static_assert(true, "Macros must be terminated with a semicolon") \
+
+ DEFINE_OPERATOR(+);
+ DEFINE_OPERATOR(-);
+ DEFINE_OPERATOR(/);
+ DEFINE_OPERATOR(*);
+
+ #undef DEFINE_OPERATOR
+
+Preprocessor conditional statements
+===================================
+When using macros for conditional compilation, prefer to use ``#if`` over
+``#ifdef``. This checks the value of the macro rather than whether it exists.
+
+* ``#if`` handles undefined macros equivalently to ``#ifdef``. Undefined
+ macros expand to 0 in preprocessor conditional statements.
+* ``#if`` evaluates false for macros defined as 0, while ``#ifdef`` evaluates
+ true.
+* Macros defined using compiler flags have a default value of 1 in GCC and
+ Clang, so they work equivalently for ``#if`` and ``#ifdef``.
+* Macros defined to an empty statement cause compile-time errors in ``#if``
+ statements, which avoids ambiguity about how the macro should be used.
+
+All ``#endif`` statements should be commented with the expression from their
+corresponding ``#if``. Do not indent within preprocessor conditional statements.
+
+.. code-block:: cpp
+
+ #if USE_64_BIT_WORD
+ using Word = uint64_t;
+ #else
+ using Word = uint32_t;
+ #endif // USE_64_BIT_WORD
+
+Unsigned integers
+=================
+Unsigned integers are permitted in Pigweed. Aim for consistency with existing
+code and the C++ Standard Library. Be very careful mixing signed and unsigned
+integers.
+
+Features not in the C++ standard
+================================
+Avoid features not available in standard C++. This includes compiler extensions
+and features from other standards like POSIX.
+
+For example, use ``ptrdiff_t`` instead of POSIX's ``ssize_t``, unless
+interacting with a POSIX API in intentionally non-portable code. Never use
+POSIX functions with suitable standard or Pigweed alternatives, such as
+``strnlen`` (use ``pw::string::NullTerminatedLength`` instead).
diff --git a/docs/style/doxygen.rst b/docs/style/doxygen.rst
new file mode 100644
index 000000000..d36bcf119
--- /dev/null
+++ b/docs/style/doxygen.rst
@@ -0,0 +1,253 @@
+.. _docs-pw-style-doxygen:
+
+===========================
+Doxygen documentation style
+===========================
+Doxygen comments in C, C++, and Java are surfaced in Sphinx using `Breathe
+<https://breathe.readthedocs.io/en/latest/index.html>`_.
+
+.. note::
+
+ Sources with doxygen comment blocks must be added to the
+ ``_doxygen_input_files`` list in ``//docs/BUILD.gn`` to be processed.
+
+Breathe provides various `directives
+<https://breathe.readthedocs.io/en/latest/directives.html>`_ for bringing
+Doxygen comments into Sphinx. These include the following:
+
+- `doxygenfile
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygenfile>`_ --
+ Documents a source file. May limit to specific types of symbols with
+ ``:sections:``.
+
+ .. code-block:: rst
+
+ .. doxygenfile:: pw_rpc/internal/config.h
+ :sections: define, func
+
+- `doxygenclass
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygenclass>`_ --
+ Documents a class and optionally its members.
+
+ .. code-block:: rst
+
+ .. doxygenclass:: pw::sync::BinarySemaphore
+ :members:
+
+- `doxygentypedef
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygentypedef>`_ --
+ Documents an alias (``typedef`` or ``using`` statement).
+
+ .. code-block:: rst
+
+ .. doxygentypedef:: pw::Function
+
+- `doxygenfunction
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygenfunction>`_ --
+ Documents a source file. Can be filtered to limit to specific types of
+ symbols.
+
+ .. code-block:: rst
+
+ .. doxygenfunction:: pw::tokenizer::EncodeArgs
+
+- `doxygendefine
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygendefine>`_ --
+ Documents a preprocessor macro.
+
+ .. code-block:: rst
+
+ .. doxygendefine:: PW_TOKENIZE_STRING
+
+.. admonition:: See also
+
+ `All Breathe directives for use in RST files <https://breathe.readthedocs.io/en/latest/directives.html>`_
+
+Example Doxygen Comment Block
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Start a Doxygen comment block using ``///`` (three forward slashes).
+
+.. code-block:: cpp
+
+ /// This is the documentation comment for the `PW_LOCK_RETURNED()` macro. It
+ /// describes how to use the macro.
+ ///
+ /// Doxygen comments can refer to other symbols using Sphinx cross
+ /// references. For example, @cpp_class{pw::InlineBasicString}, which is
+ /// shorthand for @crossref{cpp,class,pw::InlineBasicString}, links to a C++
+ /// class. @crossref{py,func,pw_tokenizer.proto.detokenize_fields} links to a
+ /// Python function.
+ ///
+ /// @param[out] dest The memory area to copy to.
+ /// @param[in] src The memory area to copy from.
+ /// @param[in] n The number of bytes to copy
+ ///
+ /// @retval OK KVS successfully initialized.
+ /// @retval DATA_LOSS KVS initialized and is usable, but contains corrupt data.
+ /// @retval UNKNOWN Unknown error. KVS is not initialized.
+ ///
+ /// @rst
+ /// The ``@rst`` and ``@endrst`` commands form a block block of
+ /// reStructuredText that is rendered in Sphinx.
+ ///
+ /// .. warning::
+ /// this is a warning admonition
+ ///
+ /// .. code-block:: cpp
+ ///
+ /// void release(ptrdiff_t update = 1);
+ /// @endrst
+ ///
+ /// Example code block using Doxygen markup below. To override the language
+ /// use `@code{.cpp}`
+ ///
+ /// @code
+ /// class Foo {
+ /// public:
+ /// Mutex* mu() PW_LOCK_RETURNED(mu) { return &mu; }
+ ///
+ /// private:
+ /// Mutex mu;
+ /// };
+ /// @endcode
+ ///
+ /// @b The first word in this sentence is bold (The).
+ ///
+ #define PW_LOCK_RETURNED(x) __attribute__((lock_returned(x)))
+
+Doxygen Syntax
+^^^^^^^^^^^^^^
+Pigweed prefers to use RST wherever possible, but there are a few Doxygen
+syntatic elements that may be needed.
+
+Common Doxygen commands for use within a comment block:
+
+- ``@rst`` To start a reStructuredText block. This is a custom alias for
+ ``\verbatim embed:rst:leading-asterisk``. This must be paired with
+ ``@endrst``.
+- `@code <https://www.doxygen.nl/manual/commands.html#cmdcode>`_ Start a code
+ block. This must be paired with ``@endcode``.
+- `@param <https://www.doxygen.nl/manual/commands.html#cmdparam>`_ Document
+ parameters, this may be repeated.
+- `@pre <https://www.doxygen.nl/manual/commands.html#cmdpre>`_ Starts a
+ paragraph where the precondition of an entity can be described.
+- `@post <https://www.doxygen.nl/manual/commands.html#cmdpost>`_ Starts a
+ paragraph where the postcondition of an entity can be described.
+- `@return <https://www.doxygen.nl/manual/commands.html#cmdreturn>`_ Single
+ paragraph to describe return value(s).
+- `@retval <https://www.doxygen.nl/manual/commands.html#cmdretval>`_ Document
+ return values by name. This is rendered as a definition list.
+- `@note <https://www.doxygen.nl/manual/commands.html#cmdnote>`_ Add a note
+ admonition to the end of documentation.
+- `@b <https://www.doxygen.nl/manual/commands.html#cmdb>`_ To bold one word.
+
+.. tip:
+
+ Did you add Doxygen comments and now your build is failing because Doxygen
+ says it can't find the class you decorated? Make sure your ``@code`` blocks
+ are paired with ``@endcode`` blocks and your ``@rst`` blocks are paired
+ with ``@endrst`` blocks!
+
+Doxygen provides `structural commands
+<https://doxygen.nl/manual/docblocks.html#structuralcommands>`_ that associate a
+comment block with a particular symbol. Example of these include ``@class``,
+``@struct``, ``@def``, ``@fn``, and ``@file``. Do not use these unless
+necessary, since they are redundant with the declarations themselves.
+
+One case where structural commands are necessary is when a single comment block
+describes multiple symbols. To group multiple symbols into a single comment
+block, include a structural commands for each symbol on its own line. For
+example, the following comment documents two macros:
+
+.. code-block:: cpp
+
+ /// @def PW_ASSERT_EXCLUSIVE_LOCK
+ /// @def PW_ASSERT_SHARED_LOCK
+ ///
+ /// Documents functions that dynamically check to see if a lock is held, and
+ /// fail if it is not held.
+
+.. seealso:: `All Doxygen commands <https://www.doxygen.nl/manual/commands.html>`_
+
+Cross-references
+^^^^^^^^^^^^^^^^
+Pigweed provides Doxygen aliases for creating Sphinx cross references within
+Doxygen comments. These should be used when referring to other symbols, such as
+functions, classes, or macros.
+
+.. inclusive-language: disable
+
+The basic alias is ``@crossref``, which supports any `Sphinx domain
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html>`_.
+For readability, aliases for commonnly used types are provided.
+
+.. inclusive-language: enable
+
+- ``@crossref{domain,type,identifier}`` Inserts a cross reference of any type in
+ any Sphinx domain. For example, ``@crossref{c,func,foo}`` is equivalent to
+ ``:c:func:`foo``` and links to the documentation for the C function ``foo``,
+ if it exists.
+- ``@c_macro{identifier}`` Equivalent to ``:c:macro:`identifier```.
+- ``@cpp_func{identifier}`` Equivalent to ``:cpp:func:`identifier```.
+- ``@cpp_class{identifier}`` Equivalent to ``:cpp:class:`identifier```.
+- ``@cpp_type{identifier}`` Equivalent to ``:cpp:type:`identifier```.
+
+.. note::
+
+ Use the `@` aliases described above for all cross references. Doxygen
+ provides other methods for cross references, but Sphinx cross references
+ offer several advantages:
+
+ - Sphinx cross references work for all identifiers known to Sphinx, including
+ those documented with e.g. ``.. cpp:class::`` or extracted from Python.
+ Doxygen references can only refer to identifiers known to Doxygen.
+ - Sphinx cross references always use consistent formatting. Doxygen cross
+ references sometimes render as plain text instead of code-style monospace,
+ or include ``()`` in macros that shouldn't have them.
+ - Sphinx cross references can refer to symbols that have not yet been
+ documented. They will be formatted correctly and become links once the
+ symbols are documented.
+ - Using Sphinx cross references in Doxygen comments makes cross reference
+ syntax more consistent within Doxygen comments and between RST and
+ Doxygen.
+
+Create cross reference links elsewhere in the document to symbols documented
+with Doxygen using standard Sphinx cross references, such as ``:cpp:class:`` and
+``:cpp:func:``.
+
+.. code-block:: rst
+
+ - :cpp:class:`pw::sync::BinarySemaphore::BinarySemaphore`
+ - :cpp:func:`pw::sync::BinarySemaphore::try_acquire`
+
+.. seealso::
+ - `C++ cross reference link syntax`_
+ - `C cross reference link syntax`_
+ - `Python cross reference link syntax`_
+
+.. inclusive-language: disable
+
+.. _C++ cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing
+.. _C cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing-c-constructs
+.. _Python cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing-python-objects
+
+.. inclusive-language: enable
+
+Status codes in Doxygen comments
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Use the following syntax when referring to ``pw_status`` codes in Doxygen
+comments:
+
+.. code-block::
+
+ @pw_status{YOUR_STATUS_CODE_HERE}
+
+Replace ``YOUR_STATUS_CODE_HERE`` with a valid ``pw_status`` code.
+
+This syntax ensures that Doxygen links back to the status code's
+reference documentation in :ref:`module-pw_status`.
+
+.. note::
+
+ The guidance in this section only applies to Doxygen comments in C++ header
+ files.
diff --git a/docs/style/sphinx.rst b/docs/style/sphinx.rst
new file mode 100644
index 000000000..06a6a82ed
--- /dev/null
+++ b/docs/style/sphinx.rst
@@ -0,0 +1,632 @@
+.. _docs-pw-style-sphinx:
+
+==========================
+Sphinx documentation style
+==========================
+.. note::
+
+ Pigweed's documentation style guide came after much of the documentation was
+ written, so Pigweed's docs don't yet 100% conform to this style guide. When
+ updating docs, please update them to match the style guide.
+
+Pigweed documentation is written using the `reStructuredText
+<https://docutils.sourceforge.io/rst.html>`_ markup language and processed by
+`Sphinx`_. We use the `Furo theme <https://github.com/pradyunsg/furo>`_ along
+with the `sphinx-design <https://sphinx-design.readthedocs.io/en/furo-theme/>`_
+extension.
+
+.. _Sphinx: https://www.sphinx-doc.org/
+
+Syntax Reference Links
+======================
+.. admonition:: See also
+ :class: seealso
+
+ - `reStructuredText Primer`_
+
+ - `reStructuredText Directives <https://docutils.sourceforge.io/docs/ref/rst/directives.html>`_
+
+ - `Furo Reference <https://pradyunsg.me/furo/reference/>`_
+
+ - `Sphinx-design Reference <https://sphinx-design.readthedocs.io/en/furo-theme/>`_
+
+ReST is flexible, supporting formatting the same logical document in a few ways
+(for example headings, blank lines). Pigweed has the following restrictions to
+make our documentation consistent.
+
+.. inclusive-language: disable
+
+.. _reStructuredText Primer: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
+
+.. inclusive-language: enable
+
+Headings
+========
+Use headings according to the following hierarchy, with the shown characters
+for the ReST heading syntax.
+
+.. code-block:: rst
+
+ ==================================
+ Document Title: Two Bars of Equals
+ ==================================
+ Document titles use equals ("====="), above and below. Capitalize the words
+ in the title, except for 'a', 'of', and 'the'.
+
+ ---------------------------
+ Major Sections Within a Doc
+ ---------------------------
+ Major sections use hyphens ("----"), above and below. Capitalize the words in
+ the title, except for 'a', 'of', and 'the'.
+
+ Heading 1 - For Sections Within a Doc
+ =====================================
+ These should be title cased. Use a single equals bar ("====").
+
+ Heading 2 - for subsections
+ ---------------------------
+ Subsections use hyphens ("----"). In many cases, these headings may be
+ sentence-like. In those cases, only the first letter should be capitalized.
+ For example, FAQ subsections would have a title with "Why does the X do the
+ Y?"; note the sentence capitalization (but not title capitalization).
+
+ Heading 3 - for subsubsections
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ Use the caret symbol ("^^^^") for subsubsections.
+
+ Note: Generally don't go beyond heading 3.
+
+ Heading 4 - for subsubsubsections
+ .................................
+ Don't use this heading level, but if you must, use period characters
+ ("....") for the heading.
+
+Do not put blank lines after headings.
+--------------------------------------
+.. admonition:: **Yes**: No blank after heading
+ :class: checkmark
+
+ .. code-block:: rst
+
+ Here is a heading
+ -----------------
+ Note that there is no blank line after the heading separator!
+
+.. admonition:: **No**: Unnecessary blank line
+ :class: error
+
+ .. code-block:: rst
+
+ Here is a heading
+ -----------------
+
+ There is a totally unnecessary blank line above this one. Don't do this.
+
+Do not put multiple blank lines before a heading.
+-------------------------------------------------
+.. admonition:: **Yes**: Just one blank after section content before the next heading
+ :class: checkmark
+
+ .. code-block:: rst
+
+ There is some text here in the section before the next. It's just here to
+ illustrate the spacing standard. Note that there is just one blank line
+ after this paragraph.
+
+ Just one blank!
+ ---------------
+ There is just one blank line before the heading.
+
+.. admonition:: **No**: Extra blank lines
+ :class: error
+
+ .. code-block:: rst
+
+ There is some text here in the section before the next. It's just here to
+ illustrate the spacing standard. Note that there are too many blank lines
+ after this paragraph; there should be just one.
+
+
+
+ Too many blanks
+ ---------------
+ There are too many blanks before the heading for this section.
+
+Directives
+==========
+Indent directives 3 spaces; and put a blank line between the directive and the
+content. This aligns the directive content with the directive name.
+
+.. admonition:: **Yes**: Three space indent for directives; and nested
+ :class: checkmark
+
+ .. code-block:: none
+
+ Here is a paragraph that has some content. After this content is a
+ directive.
+
+ .. my_directive::
+
+ Note that this line's start aligns with the "m" above. The 3-space
+ alignment accounts for the ".. " prefix for directives, to vertically
+ align the directive name with the content.
+
+ This indentation must continue for nested directives.
+
+ .. nested_directive::
+
+ Here is some nested directive content.
+
+.. admonition:: **No**: One space, two spaces, four spaces, or other indents
+ for directives
+ :class: error
+
+ .. code-block:: none
+
+ Here is a paragraph with some content.
+
+ .. my_directive::
+
+ The indentation here is incorrect! It's one space short; doesn't align
+ with the directive name above.
+
+ .. nested_directive::
+
+ This isn't indented correctly either; it's too much (4 spaces).
+
+.. admonition:: **No**: Missing blank between directive and content.
+ :class: error
+
+ .. code-block:: none
+
+ Here is a paragraph with some content.
+
+ .. my_directive::
+ Note the lack of blank line above here.
+
+Tables
+======
+Consider using ``.. list-table::`` syntax, which is more maintainable and
+easier to edit for complex tables (`details
+<https://docutils.sourceforge.io/docs/ref/rst/directives.html#list-table>`_).
+
+Code Snippets
+=============
+Use code blocks from actual source code files wherever possible. This helps keep
+documentation fresh and removes duplicate code examples. There are a few ways to
+do this with Sphinx.
+
+The `literalinclude`_ directive creates a code blocks from source files. Entire
+files can be included or a just a subsection. The best way to do this is with
+the ``:start-after:`` and ``:end-before:`` options.
+
+Example:
+
+.. card::
+
+ Documentation Source (``.rst`` file)
+ ^^^
+
+ .. code-block:: rst
+
+ .. literalinclude:: run_doxygen.py
+ :start-after: [doxygen-environment-variables]
+ :end-before: [doxygen-environment-variables]
+
+.. card::
+
+ Source File
+ ^^^
+
+ .. code-block::
+
+ # DOCSTAG: [doxygen-environment-variables]
+ env = os.environ.copy()
+ env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
+ env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
+ env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
+ # DOCSTAG: [doxygen-environment-variables]
+
+.. card::
+
+ Rendered Output
+ ^^^
+
+ .. code-block::
+
+ env = os.environ.copy()
+ env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
+ env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
+ env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
+
+.. inclusive-language: disable
+
+.. _literalinclude: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude
+
+.. inclusive-language: enable
+
+Generating API documentation from source
+========================================
+Whenever possible, document APIs in the source code and use Sphinx to generate
+documentation for them. This keeps the documentation in sync with the code and
+reduces duplication.
+
+Python
+------
+Include Python API documentation from docstrings with `autodoc directives`_.
+Example:
+
+.. inclusive-language: disable
+
+.. _autodoc directives: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directives
+
+.. inclusive-language: enable
+
+.. code-block:: rst
+
+ .. automodule:: pw_cli.log
+ :members:
+
+ .. automodule:: pw_console.embed
+ :members: PwConsoleEmbed
+ :undoc-members:
+ :show-inheritance:
+
+ .. autoclass:: pw_console.log_store.LogStore
+ :members: __init__
+ :undoc-members:
+ :show-inheritance:
+
+Include argparse command line help with the `argparse
+<https://sphinx-argparse.readthedocs.io/en/latest/usage.html>`_
+directive. Example:
+
+.. code-block:: rst
+
+ .. argparse::
+ :module: pw_watch.watch
+ :func: get_parser
+ :prog: pw watch
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
+
+Customize the depth of a page's table of contents
+=================================================
+Put ``:tocdepth: X`` on the first line of the page, where ``X`` equals how many
+levels of section heading you want to show in the page's table of contents. See
+``//docs/changelog.rst`` for an example.
+
+Changelog
+=========
+This section explains how we update the changelog.
+
+#. On the Friday before Pigweed Live, use
+ `changelog <https://kaycebasques.github.io/changelog/>`_ to generate a first
+ draft of the changelog.
+
+#. Copy-paste the reStructuredText output from the changelog tool to the top
+ of ``//docs/changelog.rst``.
+
+#. Delete these lines from the previous update in ``changelog.rst``
+ (which is no longer the latest update):
+
+ * ``.. _docs-changelog-latest:``
+ * ``.. changelog_highlights_start``
+ * ``.. changelog_highlights_end``
+
+#. Polish up the auto-generated first draft into something more readable:
+
+ * Don't change the section headings. The text in each section heading
+ should map to one of the categories that we allow in our commit
+ messages, such as ``bazel``, ``docs``, ``pw_base64``, and so on.
+ * Add a 1-paragraph summary to each section.
+ * Focus on features, important bug fixes, and breaking changes. Delete
+ internal commits that Pigweed customers won't care about.
+
+#. Push your change up to Gerrit and kick off a dry run. After a few minutes
+ the docs will get staged.
+
+#. Copy the rendered content from the staging site into the Pigweed Live
+ Google Doc.
+
+#. Make sure to land the changelog updates the same week as Pigweed Live.
+
+There is no need to update ``//docs/index.rst``. The ``What's new in Pigweed``
+content on the homepage is pulled from the changelog (that's what the
+``docs-changelog-latest``, ``changelog_highlights_start``, and
+``changelog_highlights_end`` labels are for).
+
+Why "changelog" and not "release notes"?
+----------------------------------------
+Because Pigweed doesn't have releases.
+
+Why organize by module / category?
+----------------------------------
+Why is the changelog organized by category / module? Why not the usual
+3 top-level sections: features, fixes, breaking changes?
+
+* Because some Pigweed customers only use a few modules. Organizing by module
+ helps them filter out all the changes that aren't relevant to them faster.
+* If we keep the changelog section heading text fairly structured, we may
+ be able to present the changelog in other interesting ways. For example,
+ it should be possible to collect every ``pw_base64`` section in the changelog
+ and then provide a changelog for only ``pw_base64`` over in the ``pw_base64``
+ docs.
+* The changelog tool is easily able to organize by module / category due to
+ how we annotate our commits. We will not be able to publish changelog updates
+ every 2 weeks if there is too much manual work involved.
+
+.. _docs-site-scroll:
+
+Site nav scrolling
+==================
+We have had recurring issues with scrolling on pigweed.dev. This section
+provides context on the issue and fix(es).
+
+
+The behavior we want:
+
+* The page that you're currently on should be visible in the site nav.
+* URLs with deep links (e.g. ``pigweed.dev/pw_allocator/#size-report``) should
+ instantly jump to the target section (e.g. ``#size-report``).
+* There should be no scrolling animations anywhere on the site. Scrolls should
+ happen instantly.
+
+.. _furo.js: https://github.com/pradyunsg/furo/blob/main/src/furo/assets/scripts/furo.js
+
+A few potential issues at play:
+
+* Our theme (Furo) has non-configurable scrolling logic. See `furo.js`_.
+* There seems to be a race condition between Furo's scrolling behavior and our
+ text-to-diagram tool, Mermaid, which uses JavaScript to render the diagrams
+ on page load. However, we also saw issues on pages that didn't have any
+ diagrams, so that can't be the site-wide root cause.
+
+.. _scrollTop: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop
+.. _scrollIntoView: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
+.. _scroll-behavior: https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior
+
+Our current fix:
+
+* In ``//docs/_static/js/pigweed.js`` we manually scroll the site nav and main
+ content via `scrollTop`_. Note that we previously tried `scrollIntoView`_
+ but it was not an acceptable fix because the main content would always scroll
+ down about 50 pixels, even when a deep link was not present in the URL.
+ We also manually control when Mermaid renders its diagrams.
+* In ``//docs/_static/css/pigweed.css`` we use an aggressive CSS rule
+ to ensure that `scroll-behavior`_ is set to ``auto`` (i.e. instant scrolling)
+ for all elements on the site.
+
+Background:
+
+* `Tracking issue <https://issues.pigweed.dev/issues/303261476>`_
+* `First fix <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162410>`_
+* `Second fix <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/162990>`_
+* `Third fix <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168555>`_
+* `Fourth fix <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178591>`_
+
+.. _docs-pw-style-cta:
+
+Call-to-action buttons on sales pitch pages (docs.rst)
+======================================================
+Use the following directive to put call-to-action buttons on a ``docs.rst``
+page:
+
+.. code-block::
+
+ .. grid:: 2
+
+ .. grid-item-card:: :octicon:`zap` Get started & guides
+ :link: <REF>
+ :link-type: ref
+ :class-item: sales-pitch-cta-primary
+
+ Learn how to integrate <MODULE> into your project and implement
+ common use cases.
+
+ .. grid-item-card:: :octicon:`info` API reference
+ :link: <REF>
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ Get detailed reference information about the <MODULE> API.
+
+ .. grid:: 2
+
+ .. grid-item-card:: :octicon:`info` CLI reference
+ :link: <REF>
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ Get usage information about <MODULE> command line utilities.
+
+ .. grid-item-card:: :octicon:`table` Design
+ :link: <REF>
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ Read up on how <MODULE> is designed.
+
+* Remove cards for content that does not exist. For example, if the module
+ doesn't have a CLI reference, remove the card for that doc.
+* Replace ``<REF>`` and ``<MODULE>``. Don't change anything else. We want
+ a consistent call-to-action experience across all the modules.
+
+Copy-to-clipboard feature on code blocks
+========================================
+
+.. _sphinx-copybutton: https://sphinx-copybutton.readthedocs.io/en/latest/
+.. _Remove copybuttons using a CSS selector: https://sphinx-copybutton.readthedocs.io/en/latest/use.html#remove-copybuttons-using-a-css-selector
+
+The copy-to-clipboard feature on code blocks is powered by `sphinx-copybutton`_.
+
+``sphinx-copybutton`` recognizes ``$`` as an input prompt and automatically
+removes it.
+
+There is a workflow for manually removing the copy-to-clipboard button for a
+particular code block but it has not been implemented yet. See
+`Remove copybuttons using a CSS selector`_.
+
+Grouping related content with tabs
+==================================
+Use the ``tab-set`` directive to group related content together. This feature is
+powered by `sphinx-design Tabs
+<https://sphinx-design.readthedocs.io/en/furo-theme/tabs.html>`_
+
+Tabs for code-only content
+--------------------------
+Use the ``tabs`` and ``code-tab`` directives together. Example:
+
+.. code-block:: rst
+
+ .. tab-set-code::
+
+ .. code-block:: c++
+
+ // C++ code...
+
+ .. code-block:: python
+
+ # Python code...
+
+Rendered output:
+
+.. tab-set-code::
+
+ .. code-block:: c++
+
+ // C++ code...
+
+ .. code-block:: python
+
+ # Python code...
+
+Tabs for all other content
+--------------------------
+Use the ``tabs`` and ``tab-item`` directives together. Example:
+
+.. code-block:: rst
+
+ .. tab-set::
+
+ .. tab-item:: Linux
+
+ Linux instructions...
+
+ .. tab-item:: Windows
+
+ Windows instructions...
+
+Rendered output:
+
+.. tab-set::
+
+ .. tab-item:: Linux
+
+ Linux instructions...
+
+ .. tab-item:: Windows
+
+ Windows instructions...
+
+Tab synchronization
+-------------------
+Tabs are synchronized in two ways:
+
+1. ``tab-set-code::`` ``code-block`` languages names.
+2. ``tab-item::`` ``:sync:`` values.
+
+For Example:
+
+.. code-block:: rst
+
+ .. tabs-set-code::
+
+ .. code-block:: c++
+
+ // C++ code...
+
+ .. code-block:: py
+
+ # Python code...
+
+ .. tabs-set-code::
+
+ .. code-block:: c++
+
+ // More C++ code...
+
+ .. code-block:: py
+
+ # More Python code...
+
+ .. tab-set::
+
+ .. tab-item:: Linux
+ :sync: key1
+
+ Linux instructions...
+
+ .. tab-item:: Windows
+ :sync: key2
+
+ Windows instructions...
+
+ .. tab-set::
+
+ .. tab-item:: Linux
+ :sync: key1
+
+ More Linux instructions...
+
+ .. tab-item:: Windows
+ :sync: key2
+
+ More Windows instructions...
+
+Rendered output:
+
+.. tab-set-code::
+
+ .. code-block:: c++
+
+ // C++ code...
+
+ .. code-block:: py
+
+ # Python code...
+
+.. tab-set-code::
+
+ .. code-block:: c++
+
+ // More C++ code...
+
+ .. code-block:: py
+
+ # More Python code...
+
+.. tab-set::
+
+ .. tab-item:: Linux
+ :sync: key1
+
+ Linux instructions...
+
+ .. tab-item:: Windows
+ :sync: key2
+
+ Windows instructions...
+
+.. tab-set::
+
+ .. tab-item:: Linux
+ :sync: key1
+
+ More Linux instructions...
+
+ .. tab-item:: Windows
+ :sync: key2
+
+ More Windows instructions...
diff --git a/docs/style_guide.rst b/docs/style_guide.rst
index 956ac2713..552f5a1d2 100644
--- a/docs/style_guide.rst
+++ b/docs/style_guide.rst
@@ -3,869 +3,44 @@
===========
Style Guide
===========
-- :ref:`cpp-style`
-- :ref:`owners-style`
-- :ref:`python-style`
-- :ref:`documentation-style`
-- :ref:`commit-style`
+.. grid:: 1
-.. tip::
- Pigweed runs ``pw format`` as part of ``pw presubmit`` to perform some code
- formatting checks. To speed up the review process, consider adding ``pw
- presubmit`` as a git push hook using the following command:
- ``pw presubmit --install``
-
-.. _cpp-style:
-
----------
-C++ style
----------
-The Pigweed C++ style guide is closely based on Google's external C++ Style
-Guide, which is found on the web at
-https://google.github.io/styleguide/cppguide.html. The Google C++ Style Guide
-applies to Pigweed except as described in this document.
-
-The Pigweed style guide only applies to Pigweed itself. It does not apply to
-projects that use Pigweed or to the third-party code included with Pigweed.
-Non-Pigweed code is free to use features restricted by Pigweed, such as dynamic
-memory allocation and the entirety of the C++ Standard Library.
-
-Recommendations in the :doc:`embedded_cpp_guide` are considered part of the
-Pigweed style guide, but are separated out since it covers more general
-embedded development beyond just C++ style.
-
-C++ standard
-============
-All Pigweed C++ code must compile with ``-std=c++17`` in Clang and GCC. C++20
-features may be used as long as the code still compiles unmodified with C++17.
-See ``pw_polyfill/language_feature_macros.h`` for macros that provide C++20
-features when supported.
-
-Compiler extensions should not be used unless wrapped in a macro or properly
-guarded in the preprocessor. See ``pw_processor/compiler.h`` for macros that
-wrap compiler-specific features.
-
-Automatic formatting
-====================
-Pigweed uses `clang-format <https://clang.llvm.org/docs/ClangFormat.html>`_ to
-automatically format Pigweed source code. A ``.clang-format`` configuration is
-provided with the Pigweed repository.
-
-Automatic formatting is essential to facilitate large-scale, automated changes
-in Pigweed. Therefore, all code in Pigweed is expected to be formatted with
-``clang-format`` prior to submission. Existing code may be reformatted at any
-time.
-
-If ``clang-format`` formats code in an undesirable or incorrect way, it can be
-disabled for the affected lines by adding ``// clang-format off``.
-``clang-format`` must then be re-enabled with a ``// clang-format on`` comment.
-
-.. code-block:: cpp
-
- // clang-format off
- constexpr int kMyMatrix[] = {
- 100, 23, 0,
- 0, 542, 38,
- 1, 2, 201,
- };
- // clang-format on
-
-C Standard Library
-==================
-In C++ headers, always use the C++ versions of C Standard Library headers (e.g.
-``<cstdlib>`` instead of ``<stdlib.h>``). If the header is used by both C and
-C++ code, only the C header should be used.
-
-In C++ code, it is preferred to use C functions from the ``std`` namespace. For
-example, use ``std::memcpy`` instead of ``memcpy``. The C++ standard does not
-require the global namespace versions of the functions to be provided. Using
-``std::`` is more consistent with the C++ Standard Library and makes it easier
-to distinguish Pigweed functions from library functions.
-
-Within core Pigweed, do not use C standard library functions that allocate
-memory, such as ``std::malloc``. There are exceptions to this for when dynamic
-allocation is enabled for a system; Pigweed modules are allowed to add extra
-functionality when a heap is present; but this must be optional.
-
-C++ Standard Library
-====================
-Much of the C++ Standard Library is not a good fit for embedded software. Many
-of the classes and functions were not designed with the RAM, flash, and
-performance constraints of a microcontroller in mind. For example, simply
-adding the line ``#include <iostream>`` can increase the binary size by 150 KB!
-This is larger than many microcontrollers' entire internal storage.
-
-However, with appropriate caution, a limited set of standard C++ libraries can
-be used to great effect. Developers can leverage familiar, well-tested
-abstractions instead of writing their own. C++ library algorithms and classes
-can give equivalent or better performance than hand-written C code.
-
-A limited subset of the C++ Standard Library is permitted in Pigweed. To keep
-Pigweed small, flexible, and portable, functions that allocate dynamic memory
-must be avoided. Care must be exercised when using multiple instantiations of a
-template function, which can lead to code bloat.
-
-Permitted Headers
------------------
-.. admonition:: The following C++ Standard Library headers are always permitted:
- :class: checkmark
-
- * ``<array>``
- * ``<complex>``
- * ``<initializer_list>``
- * ``<iterator>``
- * ``<limits>``
- * ``<optional>``
- * ``<random>``
- * ``<ratio>``
- * ``<string_view>``
- * ``<tuple>``
- * ``<type_traits>``
- * ``<utility>``
- * ``<variant>``
- * C Standard Library headers (``<c*>``)
-
-.. admonition:: With caution, parts of the following headers can be used:
- :class: warning
-
- * ``<algorithm>`` -- be wary of potential memory allocation
- * ``<atomic>`` -- not all MCUs natively support atomic operations
- * ``<bitset>`` -- conversions to or from strings are disallowed
- * ``<functional>`` -- do **not** use ``std::function``; use
- :ref:`module-pw_function`
- * ``<mutex>`` -- can use ``std::lock_guard``, use :ref:`module-pw_sync` for
- mutexes
- * ``<new>`` -- for placement new
- * ``<numeric>`` -- be wary of code size with multiple template instantiations
-
-.. admonition:: Never use any of these headers:
- :class: error
-
- * Dynamic containers (``<list>``, ``<map>``, ``<set>``, ``<vector>``, etc.)
- * Streams (``<iostream>``, ``<ostream>``, ``<fstream>``, ``<sstream>`` etc.)
- -- in some cases :ref:`module-pw_stream` can work instead
- * ``<span>`` -- use :ref:`module-pw_span` instead. Downstream projects may
- want to directly use ``std::span`` if it is available, but upstream must
- use the ``pw::span`` version for compatability
- * ``<string>`` -- can use :ref:`module-pw_string`
- * ``<thread>`` -- can use :ref:`module-pw_thread`
- * ``<future>`` -- eventually :ref:`module-pw_async` will offer this
- * ``<exception>``, ``<stdexcept>`` -- no exceptions
- * ``<memory>``, ``<scoped_allocator>`` -- no allocations
- * ``<regex>``
- * ``<valarray>``
-
-Headers not listed here should be carefully evaluated before they are used.
-
-These restrictions do not apply to third party code or to projects that use
-Pigweed.
-
-Combining C and C++
-===================
-Prefer to write C++ code over C code, using ``extern "C"`` for symbols that must
-have C linkage. ``extern "C"`` functions should be defined within C++
-namespaces to simplify referring to other code.
-
-C++ functions with no parameters do not include ``void`` in the parameter list.
-C functions with no parameters must include ``void``.
-
-.. code-block:: cpp
-
- namespace pw {
-
- bool ThisIsACppFunction() { return true; }
-
- extern "C" int pw_ThisIsACFunction(void) { return -1; }
-
- extern "C" {
-
- int pw_ThisIsAlsoACFunction(void) {
- return ThisIsACppFunction() ? 100 : 0;
- }
-
- } // extern "C"
-
- } // namespace pw
-
-Comments
-========
-Prefer C++-style (``//``) comments over C-style comments (``/* */``). C-style
-comments should only be used for inline comments.
-
-.. code-block:: cpp
-
- // Use C++-style comments, except where C-style comments are necessary.
- // This returns a random number using an algorithm I found on the internet.
- #define RANDOM_NUMBER() [] { \
- return 4; /* chosen by fair dice roll */ \
- }()
-
-Indent code in comments with two additional spaces, making a total of three
-spaces after the ``//``. All code blocks must begin and end with an empty
-comment line, even if the blank comment line is the last line in the block.
-
-.. code-block:: cpp
-
- // Here is an example of code in comments.
- //
- // int indentation_spaces = 2;
- // int total_spaces = 3;
- //
- // engine_1.thrust = RANDOM_NUMBER() * indentation_spaces + total_spaces;
- //
- bool SomeFunction();
-
-Control statements
-==================
-
-Loops and conditionals
-----------------------
-All loops and conditional statements must use braces, and be on their own line.
-
-.. admonition:: **Yes**: Always use braces for line conditionals and loops:
- :class: checkmark
-
- .. code:: cpp
-
- while (SomeCondition()) {
- x += 2;
- }
- if (OtherCondition()) {
- DoTheThing();
- }
-
-
-.. admonition:: **No**: Missing braces
- :class: error
-
- .. code:: cpp
-
- while (SomeCondition())
- x += 2;
- if (OtherCondition())
- DoTheThing();
-
-.. admonition:: **No**: Statement on same line as condition
- :class: error
-
- .. code:: cpp
-
- while (SomeCondition()) { x += 2; }
- if (OtherCondition()) { DoTheThing(); }
-
-
-The syntax ``while (true)`` is preferred over ``for (;;)`` for infinite loops.
-
-.. admonition:: **Yes**:
- :class: checkmark
-
- .. code:: cpp
-
- while (true) {
- DoSomethingForever();
- }
-
-.. admonition:: **No**:
- :class: error
-
- .. code:: cpp
-
- for (;;) {
- DoSomethingForever();
- }
-
-
-Prefer early exit with ``return`` and ``continue``
---------------------------------------------------
-Prefer to exit early from functions and loops to simplify code. This is the
-same same conventions as `LLVM
-<https://llvm.org/docs/CodingStandards.html#use-early-exits-and-continue-to-simplify-code>`_.
-We find this approach is superior to the "one return per function" style for a
-multitude of reasons:
-
-* **Visually**, the code is easier to follow, and takes less horizontal screen
- space.
-* It makes it clear what part of the code is the **"main business" versus "edge
- case handling"**.
-* For **functions**, parameter checking is in its own section at the top of the
- function, rather than scattered around in the fuction body.
-* For **loops**, element checking is in its own section at the top of the loop,
- rather than scattered around in the loop body.
-* Commit **deltas are simpler to follow** in code reviews; since adding a new
- parameter check or loop element condition doesn't cause an indentation change
- in the rest of the function.
-
-The guidance applies in two cases:
-
-* **Function early exit** - Early exits are for function parameter checking
- and edge case checking at the top. The main functionality follows.
-* **Loop early exit** - Early exits in loops are for skipping an iteration
- due to some edge case with an item getting iterated over. Loops may also
- contain function exits, which should be structured the same way (see example
- below).
-
-.. admonition:: **Yes**: Exit early from functions; keeping the main handling
- at the bottom and de-dentend.
- :class: checkmark
-
- .. code:: cpp
-
- Status DoSomething(Parameter parameter) {
- // Parameter validation first; detecting incoming use errors.
- PW_CHECK_INT_EQ(parameter.property(), 3, "Programmer error: frobnitz");
-
- // Error case: Not in correct state.
- if (parameter.other() == MyEnum::kBrokenState) {
- LOG_ERROR("Device in strange state: %s", parametr.state_str());
- return Status::InvalidPrecondition();
- }
-
- // Error case: Not in low power mode; shouldn't do anything.
- if (parameter.power() != MyEnum::kLowPower) {
- LOG_ERROR("Not in low power mode");
- return Status::InvalidPrecondition();
- }
-
- // Main business for the function here.
- MainBody();
- MoreMainBodyStuff();
- }
-
-.. admonition:: **No**: Main body of function is buried and right creeping.
- Even though this is shorter than the version preferred by Pigweed due to
- factoring the return statement, the logical structure is less obvious. A
- function in Pigweed containing **nested conditionals indicates that
- something complicated is happening with the flow**; otherwise it would have
- the early bail structure; so pay close attention.
- :class: error
-
- .. code:: cpp
-
- Status DoSomething(Parameter parameter) {
- // Parameter validation first; detecting incoming use errors.
- PW_CHECK_INT_EQ(parameter.property(), 3, "Programmer error: frobnitz");
-
- // Error case: Not in correct state.
- if (parameter.other() != MyEnum::kBrokenState) {
- // Error case: Not in low power mode; shouldn't do anything.
- if (parameter.power() == MyEnum::kLowPower) {
- // Main business for the function here.
- MainBody();
- MoreMainBodyStuff();
- } else {
- LOG_ERROR("Not in low power mode");
- }
- } else {
- LOG_ERROR("Device in strange state: %s", parametr.state_str());
- }
- return Status::InvalidPrecondition();
- }
-
-.. admonition:: **Yes**: Bail early from loops; keeping the main handling at
- the bottom and de-dentend.
- :class: checkmark
-
- .. code:: cpp
-
- for (int i = 0; i < LoopSize(); ++i) {
- // Early skip of item based on edge condition.
- if (!CommonCase()) {
- continue;
- }
- // Early exit of function based on error case.
- int my_measurement = GetSomeMeasurement();
- if (my_measurement < 10) {
- LOG_ERROR("Found something strange; bailing");
- return Status::Unknown();
- }
-
- // Main body of the loop.
- ProcessItem(my_items[i], my_measurement);
- ProcessItemMore(my_items[i], my_measurement, other_details);
- ...
- }
-
-.. admonition:: **No**: Right-creeping code with the main body buried inside
- some nested conditional. This makes it harder to understand what is the
- main purpose of the loop versus what is edge case handling.
- :class: error
-
- .. code:: cpp
-
- for (int i = 0; i < LoopSize(); ++i) {
- if (CommonCase()) {
- int my_measurement = GetSomeMeasurement();
- if (my_measurement >= 10) {
- // Main body of the loop.
- ProcessItem(my_items[i], my_measurement);
- ProcessItemMore(my_items[i], my_measurement, other_details);
- ...
- } else {
- LOG_ERROR("Found something strange; bailing");
- return Status::Unknown();
- }
- }
- }
-
-There are cases where this structure doesn't work, and in those cases, it is
-fine to structure the code differently.
-
-No ``else`` after ``return`` or ``continue``
---------------------------------------------
-Do not put unnecessary ``} else {`` blocks after blocks that terminate with a
-return, since this causes unnecessary rightward indentation creep. This
-guidance pairs with the preference for early exits to reduce code duplication
-and standardize loop/function structure.
-
-.. admonition:: **Yes**: No else after return or continue
- :class: checkmark
-
- .. code:: cpp
-
- // Note lack of else block due to return.
- if (Failure()) {
- DoTheThing();
- return Status::ResourceExausted();
- }
-
- // Note lack of else block due to continue.
- while (MyCondition()) {
- if (SomeEarlyBail()) {
- continue;
- }
- // Main handling of item
- ...
- }
-
- DoOtherThing();
- return OkStatus();
-
-.. admonition:: **No**: Else after return needlessly creeps right
- :class: error
-
- .. code:: cpp
-
- if (Failure()) {
- DoTheThing();
- return Status::ResourceExausted();
- } else {
- while (MyCondition()) {
- if (SomeEarlyBail()) {
- continue;
- } else {
- // Main handling of item
- ...
- }
- }
- DoOtherThing();
- return OkStatus();
- }
-
-Include guards
-==============
-The first non-comment line of every header file must be ``#pragma once``. Do
-not use traditional macro include guards. The ``#pragma once`` should come
-directly after the Pigweed copyright block, with no blank line, followed by a
-blank, like this:
-
-.. code-block:: cpp
-
- // Copyright 2021 The Pigweed Authors
- //
- // Licensed under the Apache License, Version 2.0 (the "License"); you may not
- // use this file except in compliance with the License. You may obtain a copy of
- // the License at
- //
- // https://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- // License for the specific language governing permissions and limitations under
- // the License.
- #pragma once
+ .. grid-item-card:: :octicon:`diff-added` C++ style
+ :link: docs-pw-style-cpp
+ :link-type: ref
+ :class-item: sales-pitch-cta-primary
- // Header file-level comment goes here...
+ Our C++ style guide: an extension of the Google C++ style with further
+ restrictions and guidance for embedded
-Memory allocation
-=================
-Dynamic memory allocation can be problematic. Heap allocations and deallocations
-occupy valuable CPU cycles. Memory usage becomes nondeterministic, which can
-result in a system crashing without a clear culprit.
+.. grid:: 3
-To keep Pigweed portable, core Pigweed code is not permitted to dynamically
-(heap) allocate memory, such as with ``malloc`` or ``new``. All memory should be
-allocated with automatic (stack) or static (global) storage duration. Pigweed
-must not use C++ libraries that use dynamic allocation.
+ .. grid-item-card:: :octicon:`rocket` Commit messages
+ :link: docs-pw-style-commit-message
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-Projects that use Pigweed are free to use dynamic allocation, provided they
-have selected a target that enables the heap.
+ How to format commit messages for Pigweed
-Naming
-======
-Entities shall be named according to the `Google style guide
-<https://google.github.io/styleguide/cppguide.html>`_, with the following
-additional requirements.
-
-C++ code
---------
-* All Pigweed C++ code must be in the ``pw`` namespace. Namespaces for modules
- should be nested under ``pw``. For example, ``pw::string::Format()``.
-* Whenever possible, private code should be in a source (.cc) file and placed in
- anonymous namespace nested under ``pw``.
-* If private code must be exposed in a header file, it must be in a namespace
- nested under ``pw``. The namespace may be named for its subsystem or use a
- name that designates it as private, such as ``internal``.
-* Template arguments for non-type names (e.g. ``template <int kFooBar>``) should
- follow the constexpr and const variable Google naming convention, which means
- k prefixed camel case (e.g. ``kCamelCase``). This matches the Google C++
- style for variable naming, however the wording in the official style guide
- isn't explicit for template arguments and could be interpreted to use
- ``foo_bar`` style naming. For consistency with other variables whose value is
- always fixed for the duration of the program, the naming convention is
- ``kCamelCase``, and so that is the style we use in Pigweed.
-* Trivial membor accessors should be named with ``snake_case()``. The Google
- C++ style allows either ``snake_case()`` or ``CapsCase()``, but Pigweed
- always uses ``snake_case()``.
-* Abstract base classes should be named generically, with derived types named
- specifically. For example, ``Stream`` is an abstract base, and
- ``SocketStream`` and ``StdioStream`` are an implementations of that
- interface. Any prefix or postfix indicating whether something is abstract or
- concrete is not permitted; for example, ``IStream`` or ``SocketStreamImpl``
- are both not permitted. These pre-/post-fixes add additional visual noise and
- are irrelevant to consumers of these interfaces.
-
-C code
-------
-In general, C symbols should be prefixed with the module name. If the symbol is
-not associated with a module, use just ``pw`` as the module name. Facade
-backends may chose to prefix symbols with the facade's name to help reduce the
-length of the prefix.
-
-* Public names used by C code must be prefixed with the module name (e.g.
- ``pw_tokenizer_*``).
-* If private code must be exposed in a header, private names used by C code must
- be prefixed with an underscore followed by the module name (e.g.
- ``_pw_assert_*``).
-* Avoid writing C source (.c) files in Pigweed. Prefer to write C++ code with C
- linkage using ``extern "C"``. Within C source, private C functions and
- variables must be named with the ``_pw_my_module_*`` prefix and should be
- declared ``static`` whenever possible; for example,
- ``_pw_my_module_MyPrivateFunction``.
-* The C prefix rules apply to
-
- * C functions (``int pw_foo_FunctionName(void);``),
- * variables used by C code (``int pw_foo_variable_name;``),
- * constant variables used by C code (``const int pw_foo_kConstantName;``),
- * structs used by C code (``typedef struct {} pw_foo_StructName;``), and
- * all of the above for ``extern "C"`` names in C++ code.
-
- The prefix does not apply to struct members, which use normal Google style.
-
-Preprocessor macros
--------------------
-* Public Pigweed macros must be prefixed with the module name (e.g.
- ``PW_MY_MODULE_*``).
-* Private Pigweed macros must be prefixed with an underscore followed by the
- module name (e.g. ``_PW_MY_MODULE_*``). (This style may change, see
- `b/234886184 <https://issuetracker.google.com/issues/234886184>`_).
-
-**Example**
-
-.. code-block:: cpp
-
- namespace pw::my_module {
- namespace nested_namespace {
-
- // C++ names (types, variables, functions) must be in the pw namespace.
- // They are named according to the Google style guide.
- constexpr int kGlobalConstant = 123;
-
- // Prefer using functions over extern global variables.
- extern int global_variable;
-
- class Class {};
-
- void Function();
-
- extern "C" {
-
- // Public Pigweed code used from C must be prefixed with pw_.
- extern const int pw_my_module_kGlobalConstant;
-
- extern int pw_my_module_global_variable;
-
- void pw_my_module_Function(void);
-
- typedef struct {
- int member_variable;
- } pw_my_module_Struct;
-
- // Private Pigweed code used from C must be prefixed with _pw_.
- extern const int _pw_my_module_kPrivateGlobalConstant;
-
- extern int _pw_my_module_private_global_variable;
-
- void _pw_my_module_PrivateFunction(void);
-
- typedef struct {
- int member_variable;
- } _pw_my_module_PrivateStruct;
-
- } // extern "C"
-
- // Public macros must be prefixed with PW_.
- #define PW_MY_MODULE_PUBLIC_MACRO(arg) arg
-
- // Private macros must be prefixed with _PW_.
- #define _PW_MY_MODULE_PRIVATE_MACRO(arg) arg
-
- } // namespace nested_namespace
- } // namespace pw::my_module
-
-See :ref:`docs-pw-style-macros` for details about macro usage.
-
-Namespace scope formatting
-==========================
-All non-indented blocks (namespaces, ``extern "C"`` blocks, and preprocessor
-conditionals) must have a comment on their closing line with the
-contents of the starting line.
-
-All nested namespaces should be declared together with no blank lines between
-them.
-
-.. code-block:: cpp
-
- #include "some/header.h"
-
- namespace pw::nested {
- namespace {
-
- constexpr int kAnonConstantGoesHere = 0;
-
- } // namespace
-
- namespace other {
-
- const char* SomeClass::yes = "no";
-
- bool ThisIsAFunction() {
- #if PW_CONFIG_IS_SET
- return true;
- #else
- return false;
- #endif // PW_CONFIG_IS_SET
- }
-
- extern "C" {
-
- const int pw_kSomeConstant = 10;
- int pw_some_global_variable = 600;
-
- void pw_CFunction() { ... }
-
- } // extern "C"
-
- } // namespace
- } // namespace pw::nested
-
-Using directives for literals
-=============================
-`Using-directives
-<https://en.cppreference.com/w/cpp/language/namespace#Using-directives>`_ (e.g.
-``using namespace ...``) are permitted in implementation files only for the
-purposes of importing literals such as ``std::chrono_literals`` or
-``pw::bytes::unit_literals``. Namespaces that contain any symbols other than
-literals are not permitted in a using-directive. This guidance also has no
-impact on `using-declarations
-<https://en.cppreference.com/w/cpp/language/namespace#Using-declarations>`_
-(e.g. ``using foo::Bar;``).
-
-Rationale: Literals improve code readability, making units clearer at the point
-of definition.
-
-.. code-block:: cpp
-
- using namespace std::chrono; // Not allowed
- using namespace std::literals::chrono_literals; // Allowed
-
- constexpr std::chrono::duration delay = 250ms;
-
-Pointers and references
-=======================
-For pointer and reference types, place the asterisk or ampersand next to the
-type.
-
-.. code-block:: cpp
-
- int* const number = &that_thing;
- constexpr const char* kString = "theory!"
-
- bool FindTheOneRing(const Region& where_to_look) { ... }
-
-Prefer storing references over storing pointers. Pointers are required when the
-pointer can change its target or may be ``nullptr``. Otherwise, a reference or
-const reference should be used.
-
-.. _docs-pw-style-macros:
-
-Preprocessor macros
-===================
-Macros should only be used when they significantly improve upon the C++ code
-they replace. Macros should make code more readable, robust, and safe, or
-provide features not possible with standard C++, such as stringification, line
-number capturing, or conditional compilation. When possible, use C++ constructs
-like constexpr variables in place of macros. Never use macros as constants,
-except when a string literal is needed or the value must be used by C code.
-
-When macros are needed, the macros should be accompanied with extensive tests
-to ensure the macros are hard to use wrong.
-
-Stand-alone statement macros
-----------------------------
-Macros that are standalone statements must require the caller to terminate the
-macro invocation with a semicolon (see `Swalling the Semicolon
-<https://gcc.gnu.org/onlinedocs/cpp/Swallowing-the-Semicolon.html>`_). For
-example, the following does *not* conform to Pigweed's macro style:
-
-.. code-block:: cpp
-
- // BAD! Definition has built-in semicolon.
- #define PW_LOG_IF_BAD(mj) \
- CallSomeFunction(mj);
-
- // BAD! Compiles without error; semicolon is missing.
- PW_LOG_IF_BAD("foo")
-
-Here's how to do this instead:
-
-.. code-block:: cpp
-
- // GOOD; requires semicolon to compile.
- #define PW_LOG_IF_BAD(mj) \
- CallSomeFunction(mj)
-
- // GOOD; fails to compile due to lacking semicolon.
- PW_LOG_IF_BAD("foo")
-
-For macros in function scope that do not already require a semicolon, the
-contents can be placed in a ``do { ... } while (0)`` loop.
-
-.. code-block:: cpp
-
- #define PW_LOG_IF_BAD(mj) \
- do { \
- if (mj.Bad()) { \
- Log(#mj " is bad") \
- } \
- } while (0)
-
-Standalone macros at global scope that do not already require a semicolon can
-add a ``static_assert`` declaration statement as their last line.
-
-.. code-block:: cpp
-
- #define PW_NEAT_THING(thing) \
- bool IsNeat_##thing() { return true; } \
- static_assert(true, "Macros must be terminated with a semicolon")
-
-Private macros in public headers
---------------------------------
-Private macros in public headers must be prefixed with ``_PW_``, even if they
-are undefined after use; this prevents collisions with downstream users. For
-example:
-
-.. code-block:: cpp
-
- #define _PW_MY_SPECIAL_MACRO(op) ...
- ...
- // Code that uses _PW_MY_SPECIAL_MACRO()
- ...
- #undef _PW_MY_SPECIAL_MACRO
-
-Macros in private implementation files (.cc)
---------------------------------------------
-Macros within .cc files that should only be used within one file should be
-undefined after their last use; for example:
-
-.. code-block:: cpp
-
- #define DEFINE_OPERATOR(op) \
- T operator ## op(T x, T y) { return x op y; } \
- static_assert(true, "Macros must be terminated with a semicolon") \
-
- DEFINE_OPERATOR(+);
- DEFINE_OPERATOR(-);
- DEFINE_OPERATOR(/);
- DEFINE_OPERATOR(*);
-
- #undef DEFINE_OPERATOR
-
-Preprocessor conditional statements
-===================================
-When using macros for conditional compilation, prefer to use ``#if`` over
-``#ifdef``. This checks the value of the macro rather than whether it exists.
-
-* ``#if`` handles undefined macros equivalently to ``#ifdef``. Undefined
- macros expand to 0 in preprocessor conditional statements.
-* ``#if`` evaluates false for macros defined as 0, while ``#ifdef`` evaluates
- true.
-* Macros defined using compiler flags have a default value of 1 in GCC and
- Clang, so they work equivalently for ``#if`` and ``#ifdef``.
-* Macros defined to an empty statement cause compile-time errors in ``#if``
- statements, which avoids ambiguity about how the macro should be used.
-
-All ``#endif`` statements should be commented with the expression from their
-corresponding ``#if``. Do not indent within preprocessor conditional statements.
-
-.. code-block:: cpp
-
- #if USE_64_BIT_WORD
- using Word = uint64_t;
- #else
- using Word = uint32_t;
- #endif // USE_64_BIT_WORD
-
-Unsigned integers
-=================
-Unsigned integers are permitted in Pigweed. Aim for consistency with existing
-code and the C++ Standard Library. Be very careful mixing signed and unsigned
-integers.
-
-Features not in the C++ standard
-================================
-Avoid features not available in standard C++. This includes compiler extensions
-and features from other standards like POSIX.
-
-For example, use ``ptrdiff_t`` instead of POSIX's ``ssize_t``, unless
-interacting with a POSIX API in intentionally non-portable code. Never use
-POSIX functions with suitable standard or Pigweed alternatives, such as
-``strnlen`` (use ``pw::string::NullTerminatedLength`` instead).
-
-.. _owners-style:
-
---------------------
-Code Owners (OWNERS)
---------------------
-If you use Gerrit for source code hosting and have the
-`code-owners <https://android-review.googlesource.com/plugins/code-owners/Documentation/backend-find-owners.html>`__
-plug-in enabled Pigweed can help you enforce consistent styling on those files
-and also detects some errors.
+ .. grid-item-card:: :octicon:`code-square` Sphinx
+ :link: docs-pw-style-sphinx
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-The styling follows these rules.
+ Our website and module documentation is built with Sphinx
-#. Content is grouped by type of line (Access grant, include, etc).
-#. Each grouping is sorted alphabetically.
-#. Groups are placed the following order with a blank line separating each
- grouping.
+ .. grid-item-card:: :octicon:`code-square` Doxygen
+ :link: docs-pw-style-doxygen
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
- * "set noparent" line
- * "include" lines
- * "file:" lines
- * user grants (some examples: "*", "foo@example.com")
- * "per-file:" lines
+ How to structure reference documentation for C++ code
-This plugin will, by default, act upon any file named "OWNERS".
+.. tip::
+ Pigweed runs ``pw format`` as part of ``pw presubmit`` to perform some code
+ formatting checks. To speed up the review process, consider adding ``pw
+ presubmit`` as a git push hook using the following command:
+ ``pw presubmit --install``
.. _python-style:
@@ -950,677 +125,35 @@ ambiguity with other build systems or tooling.
Pigweed's Bazel files follow the `Bazel style guide
<https://docs.bazel.build/versions/main/skylark/build-style.html>`_.
-.. _documentation-style:
-
--------------
-Documentation
--------------
-.. note::
-
- Pigweed's documentation style guide came after much of the documentation was
- written, so Pigweed's docs don't yet 100% conform to this style guide. When
- updating docs, please update them to match the style guide.
-
-Pigweed documentation is written using the `reStructuredText
-<https://docutils.sourceforge.io/rst.html>`_ markup language and processed by
-`Sphinx`_. We use the `Furo theme <https://github.com/pradyunsg/furo>`_ along
-with the `sphinx-design <https://sphinx-design.readthedocs.io/en/furo-theme/>`_
-extension.
-
-Syntax Reference Links
-======================
-.. admonition:: See also
- :class: seealso
-
- - `reStructuredText Primer`_
-
- - `reStructuredText Directives <https://docutils.sourceforge.io/docs/ref/rst/directives.html>`_
-
- - `Furo Reference <https://pradyunsg.me/furo/reference/>`_
-
- - `Sphinx-design Reference <https://sphinx-design.readthedocs.io/en/furo-theme/>`_
-
-ReST is flexible, supporting formatting the same logical document in a few ways
-(for example headings, blank lines). Pigweed has the following restrictions to
-make our documentation consistent.
-
-Headings
-========
-Use headings according to the following hierarchy, with the shown characters
-for the ReST heading syntax.
-
-.. code:: rst
-
- ==================================
- Document Title: Two Bars of Equals
- ==================================
- Document titles use equals ("====="), above and below. Capitalize the words
- in the title, except for 'a', 'of', and 'the'.
-
- ---------------------------
- Major Sections Within a Doc
- ---------------------------
- Major sections use hyphens ("----"), above and below. Capitalize the words in
- the title, except for 'a', 'of', and 'the'.
-
- Heading 1 - For Sections Within a Doc
- =====================================
- These should be title cased. Use a single equals bar ("====").
-
- Heading 2 - for subsections
- ---------------------------
- Subsections use hyphens ("----"). In many cases, these headings may be
- sentence-like. In those cases, only the first letter should be capitalized.
- For example, FAQ subsections would have a title with "Why does the X do the
- Y?"; note the sentence capitalization (but not title capitalization).
-
- Heading 3 - for subsubsections
- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Use the caret symbol ("^^^^") for subsubsections.
-
- Note: Generally don't go beyond heading 3.
-
- Heading 4 - for subsubsubsections
- .................................
- Don't use this heading level, but if you must, use period characters
- ("....") for the heading.
-
-Do not put blank lines after headings.
---------------------------------------
-.. admonition:: **Yes**: No blank after heading
- :class: checkmark
-
- .. code:: rst
-
- Here is a heading
- -----------------
- Note that there is no blank line after the heading separator!
-
-.. admonition:: **No**: Unnecessary blank line
- :class: error
-
- .. code:: rst
-
- Here is a heading
- -----------------
-
- There is a totally unnecessary blank line above this one. Don't do this.
-
-Do not put multiple blank lines before a heading.
--------------------------------------------------
-.. admonition:: **Yes**: Just one blank after section content before the next heading
- :class: checkmark
-
- .. code:: rst
-
- There is some text here in the section before the next. It's just here to
- illustrate the spacing standard. Note that there is just one blank line
- after this paragraph.
-
- Just one blank!
- ---------------
- There is just one blank line before the heading.
-
-.. admonition:: **No**: Extra blank lines
- :class: error
-
- .. code:: rst
-
- There is some text here in the section before the next. It's just here to
- illustrate the spacing standard. Note that there are too many blank lines
- after this paragraph; there should be just one.
-
-
-
- Too many blanks
- ---------------
- There are too many blanks before the heading for this section.
-
-Directives
-==========
-Indent directives 3 spaces; and put a blank line between the directive and the
-content. This aligns the directive content with the directive name.
-
-.. admonition:: **Yes**: Three space indent for directives; and nested
- :class: checkmark
-
- .. code:: none
-
- Here is a paragraph that has some content. After this content is a
- directive.
-
- .. my_directive::
-
- Note that this line's start aligns with the "m" above. The 3-space
- alignment accounts for the ".. " prefix for directives, to vertically
- align the directive name with the content.
-
- This indentation must continue for nested directives.
-
- .. nested_directive::
-
- Here is some nested directive content.
-
-.. admonition:: **No**: One space, two spaces, four spaces, or other indents
- for directives
- :class: error
-
- .. code:: none
-
- Here is a paragraph with some content.
-
- .. my_directive::
-
- The indentation here is incorrect! It's one space short; doesn't align
- with the directive name above.
-
- .. nested_directive::
-
- This isn't indented correctly either; it's too much (4 spaces).
-
-.. admonition:: **No**: Missing blank between directive and content.
- :class: error
-
- .. code:: none
-
- Here is a paragraph with some content.
-
- .. my_directive::
- Note the lack of blank line above here.
-
-Tables
-======
-Consider using ``.. list-table::`` syntax, which is more maintainable and
-easier to edit for complex tables (`details
-<https://docutils.sourceforge.io/docs/ref/rst/directives.html#list-table>`_).
-
-Code Blocks
-===========
-Use code blocks from actual source code files wherever possible. This helps keep
-documentation fresh and removes duplicate code examples. There are a few ways to
-do this with Sphinx.
-
-Snippets
---------
-The `literalinclude`_ directive creates a code blocks from source files. Entire
-files can be included or a just a subsection. The best way to do this is with
-the ``:start-after:`` and ``:end-before:`` options.
-
-Example:
-
-.. card::
-
- Documentation Source (``.rst`` file)
- ^^^
-
- .. code-block:: rst
-
- .. literalinclude:: run_doxygen.py
- :start-after: [doxygen-environment-variables]
- :end-before: [doxygen-environment-variables]
-
-.. card::
-
- Source File
- ^^^
-
- .. code-block::
-
- # DOCSTAG: [doxygen-environment-variables]
- env = os.environ.copy()
- env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
- env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
- env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
- # DOCSTAG: [doxygen-environment-variables]
-
-.. card::
-
- Rendered Output
- ^^^
-
- .. code-block::
-
- env = os.environ.copy()
- env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
- env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
- env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
-
-Python
-------
-Include Python API documentation from docstrings with `autodoc directives`_.
-Example:
-
-.. code-block:: rst
-
- .. automodule:: pw_cli.log
- :members:
-
- .. automodule:: pw_console.embed
- :members: PwConsoleEmbed
- :undoc-members:
- :show-inheritance:
-
- .. autoclass:: pw_console.log_store.LogStore
- :members: __init__
- :undoc-members:
- :show-inheritance:
-
-Include argparse command line help with the `argparse
-<https://sphinx-argparse.readthedocs.io/en/latest/usage.html>`_
-directive. Example:
-
-.. code-block:: rst
-
- .. argparse::
- :module: pw_watch.watch
- :func: get_parser
- :prog: pw watch
- :nodefaultconst:
- :nodescription:
- :noepilog:
-
-
-Doxygen
--------
-Doxygen comments in C, C++, and Java are surfaced in Sphinx using `Breathe
-<https://breathe.readthedocs.io/en/latest/index.html>`_.
-
-.. note::
-
- Sources with doxygen comment blocks must be added to the
- ``_doxygen_input_files`` list in ``//docs/BUILD.gn`` to be processed.
-
-Breathe provides various `directives
-<https://breathe.readthedocs.io/en/latest/directives.html>`_ for bringing
-Doxygen comments into Sphinx. These include the following:
-
-- `doxygenfile
- <https://breathe.readthedocs.io/en/latest/directives.html#doxygenfile>`_ --
- Documents everything in a source file.
-- `doxygenclass
- <https://breathe.readthedocs.io/en/latest/directives.html#doxygenclass>`_ --
- Documents a class and its members.
-
- .. code-block:: rst
-
- .. doxygenclass:: pw::sync::BinarySemaphore
- :members:
-
-.. admonition:: See also
-
- `Breathe directives to use in RST files <https://breathe.readthedocs.io/en/latest/directives.html>`_
-
-Example Doxygen Comment Block
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Start a Doxygen comment block using ``///`` (three forward slashes).
-
-.. code-block:: cpp
-
- /// This is the documentation comment for the `PW_LOCK_RETURNED()` macro. It
- /// describes how to use the macro.
- ///
- /// Doxygen comments can refer to other symbols using Sphinx cross
- /// references. For example, @cpp_class{pw::InlineBasicString}, which is
- /// shorthand for @crossref{cpp,class,pw::InlineBasicString}, links to a C++
- /// class. @crossref{py,func,pw_tokenizer.proto.detokenize_fields} links to a
- /// Python function.
- ///
- /// @param[out] dest The memory area to copy to.
- /// @param[in] src The memory area to copy from.
- /// @param[in] n The number of bytes to copy
- ///
- /// @retval OK KVS successfully initialized.
- /// @retval DATA_LOSS KVS initialized and is usable, but contains corrupt data.
- /// @retval UNKNOWN Unknown error. KVS is not initialized.
- ///
- /// @rst
- /// The ``@rst`` and ``@endrst`` commands form a block block of
- /// reStructuredText that is rendered in Sphinx.
- ///
- /// .. warning::
- /// this is a warning admonition
- ///
- /// .. code-block:: cpp
- ///
- /// void release(ptrdiff_t update = 1);
- /// @endrst
- ///
- /// Example code block using Doxygen markup below. To override the language
- /// use `@code{.cpp}`
- ///
- /// @code
- /// class Foo {
- /// public:
- /// Mutex* mu() PW_LOCK_RETURNED(mu) { return &mu; }
- ///
- /// private:
- /// Mutex mu;
- /// };
- /// @endcode
- ///
- /// @b The first word in this sentence is bold (The).
- ///
- #define PW_LOCK_RETURNED(x) __attribute__((lock_returned(x)))
-
-Doxygen Syntax
-^^^^^^^^^^^^^^
-Pigweed prefers to use RST wherever possible, but there are a few Doxygen
-syntatic elements that may be needed.
-
-Common Doxygen commands for use within a comment block:
-
-- ``@rst`` To start a reStructuredText block. This is a custom alias for
- ``\verbatim embed:rst:leading-asterisk``.
-- `@code <https://www.doxygen.nl/manual/commands.html#cmdcode>`_ Start a code
- block.
-- `@param <https://www.doxygen.nl/manual/commands.html#cmdparam>`_ Document
- parameters, this may be repeated.
-- `@pre <https://www.doxygen.nl/manual/commands.html#cmdpre>`_ Starts a
- paragraph where the precondition of an entity can be described.
-- `@post <https://www.doxygen.nl/manual/commands.html#cmdpost>`_ Starts a
- paragraph where the postcondition of an entity can be described.
-- `@return <https://www.doxygen.nl/manual/commands.html#cmdreturn>`_ Single
- paragraph to describe return value(s).
-- `@retval <https://www.doxygen.nl/manual/commands.html#cmdretval>`_ Document
- return values by name. This is rendered as a definition list.
-- `@note <https://www.doxygen.nl/manual/commands.html#cmdnote>`_ Add a note
- admonition to the end of documentation.
-- `@b <https://www.doxygen.nl/manual/commands.html#cmdb>`_ To bold one word.
-
-Doxygen provides `structural commands
-<https://doxygen.nl/manual/docblocks.html#structuralcommands>`_ that associate a
-comment block with a particular symbol. Example of these include ``@class``,
-``@struct``, ``@def``, ``@fn``, and ``@file``. Do not use these unless
-necessary, since they are redundant with the declarations themselves.
-
-One case where structural commands are necessary is when a single comment block
-describes multiple symbols. To group multiple symbols into a single comment
-block, include a structural commands for each symbol on its own line. For
-example, the following comment documents two macros:
-
-.. code-block:: cpp
-
- /// @def PW_ASSERT_EXCLUSIVE_LOCK
- /// @def PW_ASSERT_SHARED_LOCK
- ///
- /// Documents functions that dynamically check to see if a lock is held, and
- /// fail if it is not held.
-
-.. seealso:: `All Doxygen commands <https://www.doxygen.nl/manual/commands.html>`_
-
-Cross-references
-^^^^^^^^^^^^^^^^
-Pigweed provides Doxygen aliases for creating Sphinx cross references within
-Doxygen comments. These should be used when referring to other symbols, such as
-functions, classes, or macros.
-
-.. inclusive-language: disable
-
-The basic alias is ``@crossref``, which supports any `Sphinx domain
-<https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html>`_.
-For readability, aliases for commonnly used types are provided.
-
-.. inclusive-language: enable
-
-- ``@crossref{domain,type,identifier}`` Inserts a cross reference of any type in
- any Sphinx domain. For example, ``@crossref{c,func,foo}`` is equivalent to
- ``:c:func:`foo``` and links to the documentation for the C function ``foo``,
- if it exists.
-- ``@c_macro{identifier}`` Equivalent to ``:c:macro:`identifier```.
-- ``@cpp_func{identifier}`` Equivalent to ``:cpp:func:`identifier```.
-- ``@cpp_class{identifier}`` Equivalent to ``:cpp:class:`identifier```.
-- ``@cpp_type{identifier}`` Equivalent to ``:cpp:type:`identifier```.
-
-.. note::
-
- Use the `@` aliases described above for all cross references. Doxygen
- provides other methods for cross references, but Sphinx cross references
- offer several advantages:
-
- - Sphinx cross references work for all identifiers known to Sphinx, including
- those documented with e.g. ``.. cpp:class::`` or extracted from Python.
- Doxygen references can only refer to identifiers known to Doxygen.
- - Sphinx cross references always use consistent formatting. Doxygen cross
- references sometimes render as plain text instead of code-style monospace,
- or include ``()`` in macros that shouldn't have them.
- - Sphinx cross references can refer to symbols that have not yet been
- documented. They will be formatted correctly and become links once the
- symbols are documented.
- - Using Sphinx cross references in Doxygen comments makes cross reference
- syntax more consistent within Doxygen comments and between RST and
- Doxygen.
-
-Create cross reference links elsewhere in the document to symbols documented
-with Doxygen using standard Sphinx cross references, such as ``:cpp:class:`` and
-``:cpp:func:``.
-
-.. code-block:: rst
-
- - :cpp:class:`pw::sync::BinarySemaphore::BinarySemaphore`
- - :cpp:func:`pw::sync::BinarySemaphore::try_acquire`
-
-.. seealso::
- - `C++ cross reference link syntax`_
- - `C cross reference link syntax`_
- - `Python cross reference link syntax`_
-
-.. _Sphinx: https://www.sphinx-doc.org/
-
-.. inclusive-language: disable
-
-.. _reStructuredText Primer: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
-.. _literalinclude: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude
-.. _C++ cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing
-.. _C cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing-c-constructs
-.. _Python cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing-python-objects
-.. _autodoc directives: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directives
-
-.. inclusive-language: enable
-
-.. _commit-style:
-
---------------
-Commit message
---------------
-Pigweed commit message bodies and summaries are limited to 72 characters wide
-to improve readability. Commit summaries should also be prefixed with the name
-of the module that the commit is affecting. :ref:`Examples
-<docs-contributing-commit-message-examples>` of well and ill-formed commit
-messages are provided below.
-
-Consider the following when writing a commit message:
-
-#. **Documentation and comments are better** - Consider whether the commit
- message contents would be better expressed in the documentation or code
- comments. Docs and code comments are durable and readable later; commit
- messages are rarely read after the change lands.
-#. **Include why the change is made, not just what the change is** - It is
- important to include a "why" component in most commits. Sometimes, why is
- evident - for example, reducing memory usage, or optimizing. But it is often
- not. Err on the side of over-explaining why, not under-explaining why.
-
-.. _docs-contributing-commit-message-examples:
-
-Pigweed commit messages should conform to the following style:
-
-.. admonition:: **Yes**:
- :class: checkmark
-
- .. code:: none
-
- pw_some_module: Short capitalized description
-
- Details about the change here. Include a summary of the what, and a clear
- description of why the change is needed for future maintainers.
-
- Consider what parts of the commit message are better suited for
- documentation.
-
-.. admonition:: **Yes**: Small number of modules affected; use {} syntax.
- :class: checkmark
-
- .. code:: none
-
- pw_{foo, bar, baz}: Change something in a few places
-
- When changes cross a few modules, include them with the syntax shown
- above.
-
-.. admonition:: **Yes**: Targets are effectively modules, even though they're
- nested, so they get a ``/`` character.
- :class: checkmark
-
- .. code:: none
-
- targets/xyz123: Tweak support for XYZ's PQR
-
-.. admonition:: **Yes**: Uses imperative style for subject and text.
- :class: checkmark
-
- .. code:: none
-
- pw_something: Add foo and bar functions
-
- This commit correctly uses imperative present-tense style.
-
-.. admonition:: **No**: Uses non-imperative style for subject and text.
- :class: error
-
- .. code:: none
-
- pw_something: Adds more things
-
- Use present tense imperative style for subjects and commit. The above
- subject has a plural "Adds" which is incorrect; should be "Add".
-
-.. admonition:: **Yes**: Use bulleted lists when multiple changes are in a
- single CL. Prefer smaller CLs, but larger CLs are a practical reality.
- :class: checkmark
-
- .. code:: none
-
- pw_complicated_module: Pre-work for refactor
-
- Prepare for a bigger refactor by reworking some arguments before the larger
- change. This change must land in downstream projects before the refactor to
- enable a smooth transition to the new API.
-
- - Add arguments to MyImportantClass::MyFunction
- - Update MyImportantClass to handle precondition Y
- - Add stub functions to be used during the transition
-
-.. admonition:: **No**: Run on paragraph instead of bulleted list
- :class: error
-
- .. code:: none
-
- pw_foo: Many things in a giant BWOT
-
- This CL does A, B, and C. The commit message is a Big Wall Of Text
- (BWOT), which we try to discourage in Pigweed. Also changes X and Y,
- because Z and Q. Furthermore, in some cases, adds a new Foo (with Bar,
- because we want to). Also refactors qux and quz.
-
-.. admonition:: **No**: Doesn't capitalize the subject
- :class: error
-
- .. code:: none
-
- pw_foo: do a thing
-
- Above subject is incorrect, since it is a sentence style subject.
-
-.. admonition:: **Yes**: Doesn't capitalize the subject when subject's first
- word is a lowercase identifier.
- :class: checkmark
-
- .. code:: none
-
- pw_foo: std::unique_lock cleanup
-
- This commit message demonstrates the subject when the subject has an
- identifier for the first word. In that case, follow the identifier casing
- instead of capitalizing.
-
- However, imperative style subjects often have the identifier elsewhere in
- the subject; for example:
-
- .. code:: none
-
- pw_foo: Improve use of std::unique_lock
-
-.. admonition:: **No**: Uses a non-standard ``[]`` to indicate module:
- :class: error
-
- .. code:: none
-
- [pw_foo]: Do a thing
-
-.. admonition:: **No**: Has a period at the end of the subject
- :class: error
-
- .. code:: none
-
- pw_bar: Do something great.
-
-.. admonition:: **No**: Puts extra stuff after the module which isn't a module.
- :class: error
-
- .. code:: none
-
- pw_bar/byte_builder: Add more stuff to builder
-
-Footer
-======
-We support a number of `git footers`_ in the commit message, such as ``Bug:
-123`` in the message below:
-
-.. code:: none
-
- pw_something: Add foo and bar functions
-
- Bug: 123
-
-You are encouraged to use the following footers when appropriate:
-
-* ``Bug``: Associates this commit with a bug (issue in our `bug tracker`_). The
- bug will be automatically updated when the change is submitted. When a change
- is relevant to more than one bug, include multiple ``Bug`` lines, like so:
-
- .. code:: none
-
- pw_something: Add foo and bar functions
-
- Bug: 123
- Bug: 456
-
-* ``Fixed`` or ``Fixes``: Like ``Bug``, but automatically closes the bug when
- submitted.
-
- .. code:: none
-
- pw_something: Fix incorrect use of foo
-
- Fixes: 123
+.. _owners-style:
-In addition, we support all of the `Chromium CQ footers`_, but those are
-relatively rarely useful.
+--------------------
+Code Owners (OWNERS)
+--------------------
+If you use Gerrit for source code hosting and have the
+`code-owners <https://android-review.googlesource.com/plugins/code-owners/Documentation/backend-find-owners.html>`__
+plug-in enabled Pigweed can help you enforce consistent styling on those files
+and also detects some errors.
-.. _bug tracker: https://bugs.chromium.org/p/pigweed/issues/list
-.. _Chromium CQ footers: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/infra/cq.md#options
-.. _git footers: https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/git-footers.html
+The styling follows these rules.
-Copy-to-clipboard feature on code blocks
-========================================
+#. Content is grouped by type of line (Access grant, include, etc).
+#. Each grouping is sorted alphabetically.
+#. Groups are placed the following order with a blank line separating each
+ grouping.
-.. _sphinx-copybutton: https://sphinx-copybutton.readthedocs.io/en/latest/
-.. _Remove copybuttons using a CSS selector: https://sphinx-copybutton.readthedocs.io/en/latest/use.html#remove-copybuttons-using-a-css-selector
+ * "set noparent" line
+ * "include" lines
+ * "file:" lines
+ * user grants (some examples: "*", "foo@example.com")
+ * "per-file:" lines
-The copy-to-clipboard feature on code blocks is powered by `sphinx-copybutton`_.
+This plugin will, by default, act upon any file named "OWNERS".
-``sphinx-copybutton`` recognizes ``$`` as an input prompt and automatically
-removes it.
+.. toctree::
+ :hidden:
-There is a workflow for manually removing the copy-to-clipboard button for a
-particular code block but it has not been implemented yet. See
-`Remove copybuttons using a CSS selector`_.
+ C++ <style/cpp>
+ Commit message <style/commit_message>
+ Doxygen documentation <style/doxygen>
+ Sphinx documentation <style/sphinx>
diff --git a/docs/targets.rst b/docs/targets.rst
index 111718774..47aca6dad 100644
--- a/docs/targets.rst
+++ b/docs/targets.rst
@@ -1,8 +1,8 @@
.. _docs-targets:
-=======
-Targets
-=======
+================
+Hardware targets
+================
Pigweed is designed to be portable to many different hardware platforms.
Pigweed's GN build comes with an extensible target system that allows it to be
configured to build for any number of platforms, which all build simultaneously.
@@ -49,9 +49,7 @@ build argument defined in Pigweed. Some notable arguments include:
template. This is typically used to set compiler flags, optimization levels,
global #defines, etc.
* ``default_public_deps``: List of GN targets which are added as a dependency
- to all ``pw_*`` GN targets. This is used to add global module dependencies;
- for example, in upstream, ``pw_polyfill`` is added here to provide C++17
- features in C++14 code.
+ to all ``pw_*`` GN targets. This is used to add global module dependencies.
* Facade backends: Pigweed defines facades to provide a common interface for
core system features such as logging without assuming an implementation.
When building a Pigweed target, the implementations for each of these must be
@@ -69,31 +67,31 @@ targets.
.. code-block::
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
- import("$dir_pw_toolchain/generate_toolchain.gni")
+ import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
+ import("$dir_pw_toolchain/generate_toolchain.gni")
- my_target_scope = {
- # Use Pigweed's Cortex M4 toolchain as a base.
- _toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_debug
+ my_target_scope = {
+ # Use Pigweed's Cortex M4 toolchain as a base.
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_debug
- # Forward everything except the defaults scope from that toolchain.
- forward_variables_from(_toolchain_base, "*", [ "defaults" ])
+ # Forward everything except the defaults scope from that toolchain.
+ forward_variables_from(_toolchain_base, "*", [ "defaults" ])
- defaults = {
- # Forward everything from the base toolchain's defaults.
- forward_variables_from(_toolchain_base.defaults, "*")
+ defaults = {
+ # Forward everything from the base toolchain's defaults.
+ forward_variables_from(_toolchain_base.defaults, "*")
- # Extend with custom build arguments for the target.
- pw_log_BACKEND = dir_pw_log_tokenized
- }
- }
+ # Extend with custom build arguments for the target.
+ pw_log_BACKEND = dir_pw_log_tokenized
+ }
+ }
- # Create the actual GN toolchain from the scope.
- generate_toolchain("my_target") {
- forward_variables_from(my_target_scope, "*")
- }
+ # Create the actual GN toolchain from the scope.
+ generate_toolchain("my_target") {
+ forward_variables_from(my_target_scope, "*")
+ }
Upstream targets
================
@@ -104,3 +102,8 @@ The following is a list of targets used for upstream Pigweed development.
:glob:
targets/*/target_docs
+
+Work-in-progress targets
+------------------------
+You can see a list of hardware targets that are under development but not
+yet landed `here <https://issues.pigweed.dev/issues/300646347/dependencies>`_.
diff --git a/docs/templates/docs/api.rst b/docs/templates/docs/api.rst
deleted file mode 100644
index 7ce13f745..000000000
--- a/docs/templates/docs/api.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-.. _module-MODULE_NAME-api:
-
-=============
-API Reference
-=============
-..
- If a module includes a developer-facing API, this is the place to include API
- documentation. This doc may be omitted for experimental modules that are not
- yet ready for public use.
-
- A module may have APIs for multiple languages. If so, replace this file with
- an `api` directory and an `index.rst` file that links to separate docs for
- each supported language.
diff --git a/docs/templates/docs/cli.rst b/docs/templates/docs/cli.rst
deleted file mode 100644
index a3afc0185..000000000
--- a/docs/templates/docs/cli.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. _module-MODULE_NAME-cli:
-
-=============
-CLI Reference
-=============
-..
- If the module has a command-line interface, document that here. For Python
- CLIs, the `sphinx-argparse package
- <https://sphinx-argparse.readthedocs.io/en/stable/usage.html>`_ makes this
- very simple and is already included in Pigweed. This doc may be omitted for
- experimental modules that are not yet ready for public use.
diff --git a/docs/templates/docs/concepts.rst b/docs/templates/docs/concepts.rst
deleted file mode 100644
index 9dde7063b..000000000
--- a/docs/templates/docs/concepts.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-.. _module-MODULE_NAME-concepts:
-
-========
-Concepts
-========
-..
- This doc describes fundamental concepts related to the problem the module
- solves which are independent of the module's solution. Topics related to this
- module's solution and specific design should be in ``design.rst``.
diff --git a/docs/templates/docs/design.rst b/docs/templates/docs/design.rst
deleted file mode 100644
index b214229c3..000000000
--- a/docs/templates/docs/design.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-.. _module-MODULE_NAME-design:
-
-======
-Design
-======
-..
- This doc provides background on how a module works internally, the assumptions
- inherent in its design, why this particular design was chosen over others, and
- other topics of that nature.
-
- This doc is only required when the "Design considerations" section in the
- index becomes large enough that a separate doc is needed to maintain
- readability. If *this* doc becomes too large, it can be replaced with a
- ``design`` subdirectory.
diff --git a/docs/templates/docs/docs.rst b/docs/templates/docs/docs.rst
deleted file mode 100644
index f223d3fef..000000000
--- a/docs/templates/docs/docs.rst
+++ /dev/null
@@ -1,117 +0,0 @@
-.. _module-MODULE_NAME:
-
-===========
-MODULE_NAME
-===========
-..
- A short description of the module. Aim for three sentences at most that
- clearly convey what the module does.
-
-.. card::
-
- :octicon:`comment-discussion` Status:
- :bdg-primary:`Experimental`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Unstable`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Stable`
- :octicon:`kebab-horizontal`
- :bdg-primary:`Current`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Deprecated`
-
- :octicon:`multi-select` Backends: :ref:`module-pw_rpc`
-
- :octicon:`copy` Facade: :ref:`module-pw_rpc`
-
-----------
-Background
-----------
-..
- Describe *what* problem this module solves. You can make references to other
- existing solutions to highlight why their approaches were not suitable, but
- avoid describing *how* this module solves the problem in this section.
-
- For some modules, this section may need to include fundamental concepts about
- the problem being solved. Those should be mentioned briefly here, but covered
- more extensively in ``concepts.rst``.
-
-------------
-Our solution
-------------
-..
- Describe *how* this module addresses the problems highlighted in section
- above. This is a great place to include artifacts other than text to more
- effectively deliver the message. For example, consider including images or
- animations for UI-related modules, or code samples illustrating a superior API
- in libraries.
-
-Design considerations
----------------------
-..
- Briefly explain any pertinent design considerations and trade-offs. For
- example, perhaps this module optimizes for very small RAM and Flash usage at
- the expense of additional CPU time, where other solutions might make a
- different choice.
-
- Many modules will have more extensive documentation on design and
- implementation details. If so, that additional content should be in
- ``design.rst`` and linked to from this section.
-
-Size report
------------
-..
- If this module includes code that runs on target, include the size report(s)
- here.
-
-Roadmap
--------
-..
- What are the broad future plans for this module? What remains to be built, and
- what are some known issues? Are there opportunities for contribution?
-
----------------
-Who this is for
----------------
-..
- Highlight the use cases that are appropriate for this module. This is the
- place to make the sales pitch-why should developers use this module, and what
- makes this module stand out from alternative solutions that try to address
- this problem.
-
---------------------
-Is it right for you?
---------------------
-..
- This module may not solve all problems or be appropriate for all
- circumstances. This is the place to add those caveats. Highly-experimental
- modules that are under very active development might be a poor fit for any
- developer right now. If so, mention that here.
-
----------------
-Getting started
----------------
-..
- Explain how developers use this module in their project, including any
- prerequisites and conditions. This section should cover the fastest and
- simplest ways to get started (for example, "add this dependency in GN",
- "include this header"). More complex and specific use cases should be covered
- in guides that are linked in this section.
-
---------
-Contents
---------
-.. toctree::
- :maxdepth: 1
-
- concepts
- design
- guides
- api
- cli
- gui
-
-.. toctree::
- :maxdepth: 2
-
- tutorials/index
diff --git a/docs/templates/docs/gui.rst b/docs/templates/docs/gui.rst
deleted file mode 100644
index fa63ff7d6..000000000
--- a/docs/templates/docs/gui.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-.. _module-MODULE_NAME-gui:
-
-=============
-GUI Reference
-=============
-..
- If the module has a graphic interface of any kind (including text-mode
- interfaces and web applications), document its usage here. This doc may be
- omitted for experimental modules that are not yet ready for public use.
diff --git a/docs/templates/docs/guides.rst b/docs/templates/docs/guides.rst
deleted file mode 100644
index 50348dd3f..000000000
--- a/docs/templates/docs/guides.rst
+++ /dev/null
@@ -1,15 +0,0 @@
-.. _module-MODULE_NAME-guides:
-
-======
-Guides
-======
-..
- Guides are task-oriented. They are essentially steps or recipe a user should
- follow to complete a particular goal.
-
- Each module should have a minimum of two guides: one guide on how to integrate
- the module with the user's project, and one guide on the most common use case
- for the module. Most modules ought to have many more guides than this.
-
- If this document becomes to large and difficult to navigate, replace it with a
- ``guides`` subdirectory.
diff --git a/docs/templates/docs/tutorials/index.rst b/docs/templates/docs/tutorials/index.rst
deleted file mode 100644
index 037bec8bb..000000000
--- a/docs/templates/docs/tutorials/index.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-.. _module-MODULE_NAME-codelabs:
-
-=========
-Tutorials
-=========
-..
- Tutorials are primarily learning-oriented; they are designed to teach the user
- about a concept or feature so that they can achieve basic competence.
-
- If your module has tutorials, this index should link to each of them so that
- they're populated in the table of contents. Also consider adding narrative
- instead of simply adding a bulleted list. Guide the user through the
- curriculum they should follow to become knowledgable about the module.
diff --git a/jest.config.ts b/jest.config.ts
index 0e04b80cf..7608e0a4c 100644
--- a/jest.config.ts
+++ b/jest.config.ts
@@ -12,21 +12,17 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {pathsToModuleNameMapper} from "ts-jest";
-import type {InitialOptionsTsJest} from 'ts-jest/dist/types';
+import { pathsToModuleNameMapper } from 'ts-jest';
+import type { InitialOptionsTsJest } from 'ts-jest/dist/types';
const paths = {
- "pigweedjs/pw_*": [
- "./pw_*/ts"
- ],
- "pigweedjs/protos/*": [
- "./dist/protos/*"
- ]
-}
+ 'pigweedjs/pw_*': ['./pw_*/ts'],
+ 'pigweedjs/protos/*': ['./dist/protos/*'],
+};
const config: InitialOptionsTsJest = {
preset: 'ts-jest/presets/js-with-ts',
- testRegex: "(/__tests__/.*|(\\_|/)(test|spec))\\.[jt]sx?$",
- moduleNameMapper: pathsToModuleNameMapper(paths, {prefix: '<rootDir>/'})
-}
+ testRegex: '(/__tests__/.*|(\\_|/)(test|spec))\\.[jt]sx?$',
+ moduleNameMapper: pathsToModuleNameMapper(paths, { prefix: '<rootDir>/' }),
+};
-export default config
+export default config;
diff --git a/pw_arduino_build/py/setup.py b/kudzu/BUILD.gn
index 484362f6a..b5540fba4 100644
--- a/pw_arduino_build/py/setup.py
+++ b/kudzu/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,8 +11,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_arduino_build"""
-import setuptools # type: ignore
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_docgen/docs.gni")
-setuptools.setup() # Package definition in setup.cfg
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/kudzu/docs.rst b/kudzu/docs.rst
new file mode 100644
index 000000000..36046a034
--- /dev/null
+++ b/kudzu/docs.rst
@@ -0,0 +1,17 @@
+.. _docs-kudzu:
+
+=====
+Kudzu
+=====
+.. card::
+ :img-background: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-diagonal.jpg
+ :link: https://storage.googleapis.com/pigweed-media/kudzu-finished-photo-diagonal.jpg
+ :img-alt: A single Kudzu badge face-up on a table viewed at a diagonal.
+
+Kudzu is a just-for-fun project that the Pigweed team made for Maker Faire
+2023. While Kudzu is open source, its hardware isn't publicly available.
+Pigweed users may find the source code to be a helpful example of a complex
+Pigweed integration. Learn more:
+
+* `Kudzu source code <https://pigweed.googlesource.com/pigweed/kudzu/+/refs/heads/main>`_
+* :ref:`docs-blog-01-kudzu`
diff --git a/package-lock.json b/package-lock.json
index d00b93c9e..5953576d0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,21 @@
{
"name": "pigweedjs",
- "version": "0.0.7",
+ "version": "0.0.13",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pigweedjs",
- "version": "0.0.7",
+ "version": "0.0.13",
"license": "Apache-2.0",
"dependencies": {
"@protobuf-ts/protoc": "^2.7.0",
+ "buffer": "^6.0.3",
"google-protobuf": "^3.17.3",
"long": "^5.2.1",
- "object-path": "^0.11.8",
+ "postcss": "^8.4.24",
+ "prettier": "^3.0.1",
+ "rollup-plugin-postcss": "^4.0.2",
"ts-protoc-gen": "^0.15.0"
},
"bin": {
@@ -31,14 +34,13 @@
"@types/node": "^16.0.1",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
+ "@typescript-eslint/eslint-plugin": "^6.4.0",
"ansi_up": "^5.1.0",
"arg": "^5.0.2",
"base64-js": "^1.5.1",
- "buffer": "^6.0.3",
- "crc": "^4.1.1",
"debug": "^4.3.2",
- "eslint": "^7.30.0",
- "eslint-plugin-react": "^7.24.0",
+ "eslint": "^8.47.0",
+ "eslint-plugin-lit-a11y": "^4.1.0",
"grpc-tools": "^1.11.2",
"grpc-web": "^1.2.1",
"gts": "^3.1.0",
@@ -51,7 +53,6 @@
"requirejs": "^2.3.6",
"rimraf": "^3.0.2",
"rollup": "^2.52.8",
- "rollup-plugin-dts": "^4.2.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-polyfills": "^0.2.1",
@@ -65,6 +66,15 @@
"typescript": "^4.3.5"
}
},
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -81,8 +91,9 @@
},
"node_modules/@babel/code-frame": {
"version": "7.12.11",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+ "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/highlight": "^7.10.4"
}
@@ -714,7 +725,7 @@
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.9"
@@ -728,31 +739,84 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
+ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
"node_modules/@eslint/eslintrc": {
- "version": "0.4.3",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
- "debug": "^4.1.1",
- "espree": "^7.3.0",
- "globals": "^13.9.0",
- "ignore": "^4.0.6",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^3.13.1",
- "minimatch": "^3.0.4",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/@eslint/eslintrc/node_modules/ignore": {
- "version": "4.0.6",
+ "node_modules/@eslint/eslintrc/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz",
+ "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
"dev": true,
- "license": "MIT",
"engines": {
- "node": ">= 4"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@grpc/grpc-js": {
@@ -817,22 +881,37 @@
}
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.5.0",
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+ "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"dev": true,
- "license": "Apache-2.0",
"dependencies": {
- "@humanwhocodes/object-schema": "^1.2.0",
+ "@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
- "minimatch": "^3.0.4"
+ "minimatch": "^3.0.5"
},
"engines": {
"node": ">=10.10.0"
}
},
- "node_modules/@humanwhocodes/object-schema": {
- "version": "1.2.0",
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
- "license": "BSD-3-Clause"
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
@@ -1300,7 +1379,7 @@
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.0.8",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1318,12 +1397,12 @@
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
@@ -1521,8 +1600,9 @@
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
"run-parallel": "^1.1.9"
@@ -1533,16 +1613,18 @@
},
"node_modules/@nodelib/fs.stat": {
"version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 8"
}
},
"node_modules/@nodelib/fs.walk": {
"version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
"fastq": "^1.6.0"
@@ -1765,24 +1847,32 @@
"node": ">= 10"
}
},
+ "node_modules/@trysound/sax": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
+ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@tsconfig/node16": {
"version": "1.0.3",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -1903,9 +1993,10 @@
}
},
"node_modules/@types/json-schema": {
- "version": "7.0.9",
- "dev": true,
- "license": "MIT"
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
+ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
+ "dev": true
},
"node_modules/@types/long": {
"version": "4.0.1",
@@ -1919,7 +2010,7 @@
},
"node_modules/@types/node": {
"version": "16.11.4",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/normalize-package-data": {
@@ -1984,6 +2075,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/semver": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+ "dev": true
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -2012,29 +2109,33 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "4.33.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/experimental-utils": "4.33.0",
- "@typescript-eslint/scope-manager": "4.33.0",
- "debug": "^4.3.1",
- "functional-red-black-tree": "^1.0.1",
- "ignore": "^5.1.8",
- "regexpp": "^3.1.0",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.0.tgz",
+ "integrity": "sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.4.0",
+ "@typescript-eslint/type-utils": "6.4.0",
+ "@typescript-eslint/utils": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^4.0.0",
- "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha",
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -2042,10 +2143,70 @@
}
}
},
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz",
+ "integrity": "sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz",
+ "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz",
+ "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@typescript-eslint/experimental-utils": {
"version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
+ "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.7",
"@typescript-eslint/scope-manager": "4.33.0",
@@ -2065,10 +2226,85 @@
"eslint": "*"
}
},
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
+ "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
+ "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
+ "dev": true,
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
+ "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
+ "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": {
"version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^2.0.0"
},
@@ -2083,24 +2319,27 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.3.0.tgz",
+ "integrity": "sha512-ibP+y2Gr6p0qsUkhs7InMdXrwldjxZw66wpcQq9/PzAroM45wdwyu81T+7RibNCh8oc0AgrsyCwJByncY0Ongg==",
"dev": true,
- "license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "4.33.0",
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/typescript-estree": "4.33.0",
- "debug": "^4.3.1"
+ "@typescript-eslint/scope-manager": "6.3.0",
+ "@typescript-eslint/types": "6.3.0",
+ "@typescript-eslint/typescript-estree": "6.3.0",
+ "@typescript-eslint/visitor-keys": "6.3.0",
+ "debug": "^4.3.4"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ "eslint": "^7.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"typescript": {
@@ -2109,27 +2348,127 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.3.0.tgz",
+ "integrity": "sha512-WlNFgBEuGu74ahrXzgefiz/QlVb+qg8KDTpknKwR7hMH+lQygWyx0CQFoUmMn1zDkQjTBBIn75IxtWss77iBIQ==",
"dev": true,
- "license": "MIT",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0"
+ "@typescript-eslint/types": "6.3.0",
+ "@typescript-eslint/visitor-keys": "6.3.0"
},
"engines": {
- "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.4.0.tgz",
+ "integrity": "sha512-TvqrUFFyGY0cX3WgDHcdl2/mMCWCDv/0thTtx/ODMY1QhEiyFtv/OlLaNIiYLwRpAxAtOLOY9SUf1H3Q3dlwAg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "6.4.0",
+ "@typescript-eslint/utils": "6.4.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz",
+ "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz",
+ "integrity": "sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz",
+ "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.3.0.tgz",
+ "integrity": "sha512-K6TZOvfVyc7MO9j60MkRNWyFSf86IbOatTKGrpTQnzarDZPYPVy0oe3myTMq7VjhfsUAbNUW8I5s+2lZvtx1gg==",
"dev": true,
- "license": "MIT",
+ "peer": true,
"engines": {
- "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -2137,20 +2476,104 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.3.0.tgz",
+ "integrity": "sha512-Xh4NVDaC4eYKY4O3QGPuQNp5NxBAlEvNQYOqJquR2MePNxO11E5K3t5x4M4Mx53IZvtpW+mBxIT0s274fLUocg==",
"dev": true,
- "license": "BSD-2-Clause",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0",
- "debug": "^4.3.1",
- "globby": "^11.0.3",
- "is-glob": "^4.0.1",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
+ "@typescript-eslint/types": "6.3.0",
+ "@typescript-eslint/visitor-keys": "6.3.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.4.0.tgz",
+ "integrity": "sha512-BvvwryBQpECPGo8PwF/y/q+yacg8Hn/2XS+DqL/oRsOPK+RPt29h5Ui5dqOKHDlbXrAeHUTnyG3wZA0KTDxRZw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.4.0",
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/typescript-estree": "6.4.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz",
+ "integrity": "sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz",
+ "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==",
+ "dev": true,
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz",
+ "integrity": "sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
@@ -2162,22 +2585,66 @@
}
}
},
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz",
+ "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "6.4.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^16.0.0 || >=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.3.0.tgz",
+ "integrity": "sha512-kEhRRj7HnvaSjux1J9+7dBen15CdWmDnwrpyiHsFX6Qx2iW5LOBUgNefOFeh2PjWPlNwN8TOn6+4eBU3J/gupw==",
"dev": true,
- "license": "MIT",
+ "peer": true,
"dependencies": {
- "@typescript-eslint/types": "4.33.0",
- "eslint-visitor-keys": "^2.0.0"
+ "@typescript-eslint/types": "6.3.0",
+ "eslint-visitor-keys": "^3.4.1"
},
"engines": {
- "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ "node": "^16.0.0 || >=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -2229,15 +2696,16 @@
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
- "license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-walk": {
"version": "8.2.0",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@@ -2256,8 +2724,9 @@
},
"node_modules/ajv": {
"version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -2286,9 +2755,10 @@
}
},
"node_modules/ansi-colors": {
- "version": "4.1.1",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -2328,7 +2798,6 @@
},
"node_modules/ansi-styles": {
"version": "4.3.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -2396,48 +2865,30 @@
"dev": true,
"license": "MIT"
},
- "node_modules/array-includes": {
- "version": "3.1.4",
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "is-string": "^1.0.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "dequal": "^2.0.3"
}
},
"node_modules/array-union": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
},
- "node_modules/array.prototype.flatmap": {
- "version": "1.2.5",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/arrify": {
"version": "1.0.1",
"dev": true,
@@ -2459,8 +2910,9 @@
},
"node_modules/astral-regex": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2490,6 +2942,21 @@
"node": ">= 4.5.0"
}
},
+ "node_modules/axe-core": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz",
+ "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
+ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==",
+ "dev": true
+ },
"node_modules/babel-jest": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz",
@@ -2593,7 +3060,6 @@
},
"node_modules/base64-js": {
"version": "1.5.1",
- "dev": true,
"funding": [
{
"type": "github",
@@ -2631,6 +3097,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ },
"node_modules/boxen": {
"version": "5.1.2",
"dev": true,
@@ -2794,10 +3265,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.1.tgz",
- "integrity": "sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==",
- "dev": true,
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"funding": [
{
"type": "opencollective",
@@ -2806,14 +3276,17 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
- "caniuse-lite": "^1.0.30001359",
- "electron-to-chromium": "^1.4.172",
- "node-releases": "^2.0.5",
- "update-browserslist-db": "^1.0.4"
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
+ "node-releases": "^2.0.12",
+ "update-browserslist-db": "^1.0.11"
},
"bin": {
"browserslist": "cli.js"
@@ -2846,7 +3319,8 @@
},
"node_modules/buffer": {
"version": "6.0.3",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
@@ -2861,7 +3335,6 @@
"url": "https://feross.org/support"
}
],
- "license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
@@ -2987,11 +3460,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/caniuse-api": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
+ "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
+ "dependencies": {
+ "browserslist": "^4.0.0",
+ "caniuse-lite": "^1.0.0",
+ "lodash.memoize": "^4.1.2",
+ "lodash.uniq": "^4.5.0"
+ }
+ },
"node_modules/caniuse-lite": {
- "version": "1.0.30001365",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001365.tgz",
- "integrity": "sha512-VDQZ8OtpuIPMBA4YYvZXECtXbddMCUFJk1qu8Mqxfm/SZJNSr1cy4IuLCOL7RJ/YASrvJcYg1Zh+UEUQ5m6z8Q==",
- "dev": true,
+ "version": "1.0.30001509",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz",
+ "integrity": "sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==",
"funding": [
{
"type": "opencollective",
@@ -3000,13 +3483,15 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
- ],
- "peer": true
+ ]
},
"node_modules/chalk": {
"version": "4.1.2",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -3147,7 +3632,6 @@
},
"node_modules/color-convert": {
"version": "2.0.1",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -3158,7 +3642,6 @@
},
"node_modules/color-name": {
"version": "1.1.4",
- "dev": true,
"license": "MIT"
},
"node_modules/color-support": {
@@ -3169,6 +3652,11 @@
"color-support": "bin.js"
}
},
+ "node_modules/colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
+ },
"node_modules/colors": {
"version": "1.4.0",
"dev": true,
@@ -3189,6 +3677,14 @@
"node": ">= 0.8"
}
},
+ "node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
"node_modules/commondir": {
"version": "1.0.1",
"dev": true,
@@ -3245,6 +3741,14 @@
"safe-buffer": "~5.1.0"
}
},
+ "node_modules/concat-with-sourcemaps": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz",
+ "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==",
+ "dependencies": {
+ "source-map": "^0.6.1"
+ }
+ },
"node_modules/configstore": {
"version": "5.0.1",
"dev": true,
@@ -3294,17 +3798,6 @@
"node": ">= 0.4.0"
}
},
- "node_modules/crc": {
- "version": "4.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "peerDependencies": {
- "buffer": ">=6.0.3"
- }
- },
"node_modules/create-ecdh": {
"version": "4.0.4",
"dev": true,
@@ -3341,7 +3834,7 @@
},
"node_modules/create-require": {
"version": "1.1.1",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/cross-spawn": {
@@ -3386,6 +3879,58 @@
"node": ">=8"
}
},
+ "node_modules/css-declaration-sorter": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
+ "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==",
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.9"
+ }
+ },
+ "node_modules/css-select": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-select/node_modules/domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/css-vendor": {
"version": "2.0.8",
"dev": true,
@@ -3395,6 +3940,112 @@
"is-in-browser": "^1.0.2"
}
},
+ "node_modules/css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cssnano": {
+ "version": "5.1.15",
+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
+ "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==",
+ "dependencies": {
+ "cssnano-preset-default": "^5.2.14",
+ "lilconfig": "^2.0.3",
+ "yaml": "^1.10.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/cssnano"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/cssnano-preset-default": {
+ "version": "5.2.14",
+ "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz",
+ "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==",
+ "dependencies": {
+ "css-declaration-sorter": "^6.3.1",
+ "cssnano-utils": "^3.1.0",
+ "postcss-calc": "^8.2.3",
+ "postcss-colormin": "^5.3.1",
+ "postcss-convert-values": "^5.1.3",
+ "postcss-discard-comments": "^5.1.2",
+ "postcss-discard-duplicates": "^5.1.0",
+ "postcss-discard-empty": "^5.1.1",
+ "postcss-discard-overridden": "^5.1.0",
+ "postcss-merge-longhand": "^5.1.7",
+ "postcss-merge-rules": "^5.1.4",
+ "postcss-minify-font-values": "^5.1.0",
+ "postcss-minify-gradients": "^5.1.1",
+ "postcss-minify-params": "^5.1.4",
+ "postcss-minify-selectors": "^5.2.1",
+ "postcss-normalize-charset": "^5.1.0",
+ "postcss-normalize-display-values": "^5.1.0",
+ "postcss-normalize-positions": "^5.1.1",
+ "postcss-normalize-repeat-style": "^5.1.1",
+ "postcss-normalize-string": "^5.1.0",
+ "postcss-normalize-timing-functions": "^5.1.0",
+ "postcss-normalize-unicode": "^5.1.1",
+ "postcss-normalize-url": "^5.1.0",
+ "postcss-normalize-whitespace": "^5.1.1",
+ "postcss-ordered-values": "^5.1.3",
+ "postcss-reduce-initial": "^5.1.2",
+ "postcss-reduce-transforms": "^5.1.0",
+ "postcss-svgo": "^5.1.0",
+ "postcss-unique-selectors": "^5.1.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/cssnano-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
+ "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/csso": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
+ "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
+ "dependencies": {
+ "css-tree": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -3473,9 +4124,10 @@
}
},
"node_modules/debug": {
- "version": "4.3.2",
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ms": "2.1.2"
},
@@ -3574,17 +4226,6 @@
"abstract-leveldown": "~0.12.1"
}
},
- "node_modules/define-properties": {
- "version": "1.1.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "object-keys": "^1.0.12"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -3599,6 +4240,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/des.js": {
"version": "1.0.1",
"dev": true,
@@ -3631,7 +4281,7 @@
},
"node_modules/diff": {
"version": "4.0.2",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
@@ -3658,8 +4308,9 @@
},
"node_modules/dir-glob": {
"version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"path-type": "^4.0.0"
},
@@ -3667,17 +4318,6 @@
"node": ">=8"
}
},
- "node_modules/doctrine": {
- "version": "2.1.0",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "esutils": "^2.0.2"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/dom-helpers": {
"version": "5.2.1",
"dev": true,
@@ -3689,7 +4329,6 @@
},
"node_modules/dom-serializer": {
"version": "1.3.2",
- "dev": true,
"license": "MIT",
"dependencies": {
"domelementtype": "^2.0.1",
@@ -3700,9 +4339,43 @@
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
+ "node_modules/dom5": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dom5/-/dom5-3.0.1.tgz",
+ "integrity": "sha512-JPFiouQIr16VQ4dX6i0+Hpbg3H2bMKPmZ+WZgBOSSvOPx9QHwwY8sPzeM2baUtViESYto6wC2nuZOMC/6gulcA==",
+ "dev": true,
+ "dependencies": {
+ "@types/parse5": "^2.2.34",
+ "clone": "^2.1.0",
+ "parse5": "^4.0.0"
+ }
+ },
+ "node_modules/dom5/node_modules/@types/parse5": {
+ "version": "2.2.34",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-2.2.34.tgz",
+ "integrity": "sha512-p3qOvaRsRpFyEmaS36RtLzpdxZZnmxGuT1GMgzkTtTJVFuEw7KFjGK83MFODpJExgX1bEzy9r0NYjMC3IMfi7w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/dom5/node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/dom5/node_modules/parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+ "dev": true
+ },
"node_modules/domelementtype": {
"version": "2.2.0",
- "dev": true,
"funding": [
{
"type": "github",
@@ -3734,7 +4407,6 @@
},
"node_modules/domhandler": {
"version": "4.2.2",
- "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.2.0"
@@ -3748,7 +4420,6 @@
},
"node_modules/domutils": {
"version": "2.8.0",
- "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^1.0.1",
@@ -3781,11 +4452,9 @@
"license": "BSD-3-Clause"
},
"node_modules/electron-to-chromium": {
- "version": "1.4.186",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.186.tgz",
- "integrity": "sha512-YoVeFrGd/7ROjz4R9uPoND1K/hSRC/xADy9639ZmIZeJSaBnKdYx3I6LMPsY7CXLpK7JFgKQVzeZ/dk2br6Eaw==",
- "dev": true,
- "peer": true
+ "version": "1.4.442",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.442.tgz",
+ "integrity": "sha512-RkrZF//Ya+0aJq2NM3OdisNh5ZodZq1rdXOS96G8DdDgpDKqKE81yTbbQ3F/4CKm1JBPsGu1Lp/akkna2xO06Q=="
},
"node_modules/elliptic": {
"version": "6.5.4",
@@ -3828,11 +4497,13 @@
}
},
"node_modules/enquirer": {
- "version": "2.3.6",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "ansi-colors": "^4.1.1"
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8.6"
@@ -3840,7 +4511,6 @@
},
"node_modules/entities": {
"version": "2.2.0",
- "dev": true,
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
@@ -3870,58 +4540,8 @@
"is-arrayish": "^0.2.1"
}
},
- "node_modules/es-abstract": {
- "version": "1.19.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.1.1",
- "get-symbol-description": "^1.0.0",
- "has": "^1.0.3",
- "has-symbols": "^1.0.2",
- "internal-slot": "^1.0.3",
- "is-callable": "^1.2.4",
- "is-negative-zero": "^2.0.1",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.1",
- "is-string": "^1.0.7",
- "is-weakref": "^1.0.1",
- "object-inspect": "^1.11.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.2",
- "string.prototype.trimend": "^1.0.4",
- "string.prototype.trimstart": "^1.0.4",
- "unbox-primitive": "^1.0.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/es-to-primitive": {
- "version": "1.2.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/escalade": {
"version": "3.1.1",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4017,56 +4637,54 @@
}
},
"node_modules/eslint": {
- "version": "7.32.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "7.12.11",
- "@eslint/eslintrc": "^0.4.3",
- "@humanwhocodes/config-array": "^0.5.0",
- "ajv": "^6.10.0",
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz",
+ "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "^8.47.0",
+ "@humanwhocodes/config-array": "^0.11.10",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
- "debug": "^4.0.1",
+ "debug": "^4.3.2",
"doctrine": "^3.0.0",
- "enquirer": "^2.3.5",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^5.1.1",
- "eslint-utils": "^2.1.0",
- "eslint-visitor-keys": "^2.0.0",
- "espree": "^7.3.1",
- "esquery": "^1.4.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^5.1.2",
- "globals": "^13.6.0",
- "ignore": "^4.0.6",
- "import-fresh": "^3.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
- "js-yaml": "^3.13.1",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
- "minimatch": "^3.0.4",
+ "minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
- "optionator": "^0.9.1",
- "progress": "^2.0.0",
- "regexpp": "^3.1.0",
- "semver": "^7.2.1",
- "strip-ansi": "^6.0.0",
- "strip-json-comments": "^3.1.0",
- "table": "^6.0.9",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
@@ -4101,6 +4719,75 @@
"eslint": ">=4.19.1"
}
},
+ "node_modules/eslint-plugin-lit": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.9.1.tgz",
+ "integrity": "sha512-XFFVufVxYJwqRB9sLkDXB7SvV1xi9hrC4HRFEdX1h9+iZ3dm4x9uS7EuT9uaXs6zR3DEgcojia1F7pmvWbc4Gg==",
+ "dev": true,
+ "dependencies": {
+ "parse5": "^6.0.1",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "requireindex": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "eslint": ">= 5"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-4.1.0.tgz",
+ "integrity": "sha512-tJUYhKUZ1+NwOBzNHRX7ZETvGgMxWA+m9TsM23oN1qp4hQacTYBafeOhXHh6zyGnHaBIWzmCxW/G7doHeetkAw==",
+ "dev": true,
+ "dependencies": {
+ "aria-query": "^5.1.3",
+ "axe-core": "^4.3.3",
+ "axobject-query": "^2.2.0",
+ "dom5": "^3.0.1",
+ "emoji-regex": "^10.2.1",
+ "eslint-plugin-lit": "^1.6.0",
+ "eslint-rule-extender": "0.0.1",
+ "language-tags": "^1.0.5",
+ "parse5": "^7.1.2",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "requireindex": "~1.2.0"
+ },
+ "peerDependencies": {
+ "eslint": ">= 5"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/emoji-regex": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz",
+ "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==",
+ "dev": true
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "dev": true,
+ "dependencies": {
+ "entities": "^4.4.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/eslint-plugin-node": {
"version": "11.1.0",
"dev": true,
@@ -4148,51 +4835,17 @@
}
}
},
- "node_modules/eslint-plugin-react": {
- "version": "7.26.1",
+ "node_modules/eslint-rule-extender": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-rule-extender/-/eslint-rule-extender-0.0.1.tgz",
+ "integrity": "sha512-F0j1Twve3lamL3J0rRSVAynlp58sDPG39JFcQrM+u9Na7PmCgiPHNODh6YE9mduaGcsn3NBqbf6LZRj0cLr8Ng==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "array-includes": "^3.1.3",
- "array.prototype.flatmap": "^1.2.4",
- "doctrine": "^2.1.0",
- "estraverse": "^5.2.0",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.0.4",
- "object.entries": "^1.1.4",
- "object.fromentries": "^2.0.4",
- "object.hasown": "^1.0.0",
- "object.values": "^1.1.4",
- "prop-types": "^15.7.2",
- "resolve": "^2.0.0-next.3",
- "semver": "^6.3.0",
- "string.prototype.matchall": "^4.0.5"
- },
"engines": {
- "node": ">=4"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/resolve": {
- "version": "2.0.0-next.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-core-module": "^2.2.0",
- "path-parse": "^1.0.6"
+ "node": ">=10"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/eslint-plugin-react/node_modules/semver": {
- "version": "6.3.0",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
+ "type": "github",
+ "url": "https://github.com/sponsors/kaicataldo"
}
},
"node_modules/eslint-scope": {
@@ -4267,33 +4920,97 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/eslint/node_modules/ignore": {
- "version": "4.0.6",
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
- "license": "MIT",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
"engines": {
- "node": ">= 4"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/eslint/node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
}
},
"node_modules/espree": {
- "version": "7.3.1",
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
- "license": "BSD-2-Clause",
"dependencies": {
- "acorn": "^7.4.0",
- "acorn-jsx": "^5.3.1",
- "eslint-visitor-keys": "^1.3.0"
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
},
"engines": {
- "node": "^10.12.0 || >=12.0.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree/node_modules/acorn": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
}
},
"node_modules/espree/node_modules/eslint-visitor-keys": {
- "version": "1.3.0",
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
- "license": "Apache-2.0",
"engines": {
- "node": ">=4"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
@@ -4309,9 +5026,10 @@
}
},
"node_modules/esquery": {
- "version": "1.4.0",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -4367,7 +5085,6 @@
},
"node_modules/eventemitter3": {
"version": "4.0.7",
- "dev": true,
"license": "MIT"
},
"node_modules/evp_bytestokey": {
@@ -4462,8 +5179,9 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
},
"node_modules/fast-diff": {
"version": "1.2.0",
@@ -4471,9 +5189,10 @@
"license": "Apache-2.0"
},
"node_modules/fast-glob": {
- "version": "3.2.7",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3",
@@ -4482,7 +5201,7 @@
"micromatch": "^4.0.4"
},
"engines": {
- "node": ">=8"
+ "node": ">=8.6.0"
}
},
"node_modules/fast-json-stable-stringify": {
@@ -4496,9 +5215,10 @@
"license": "MIT"
},
"node_modules/fastq": {
- "version": "1.13.0",
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true,
- "license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
}
@@ -4549,6 +5269,22 @@
"node": ">=8"
}
},
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.0.4",
"dev": true,
@@ -4647,7 +5383,6 @@
},
"node_modules/function-bind": {
"version": "1.1.1",
- "dev": true,
"license": "MIT"
},
"node_modules/functional-red-black-tree": {
@@ -4720,6 +5455,14 @@
"node": ">=4"
}
},
+ "node_modules/generic-names": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz",
+ "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==",
+ "dependencies": {
+ "loader-utils": "^3.2.0"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -4772,21 +5515,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/get-symbol-description": {
- "version": "1.0.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/glob": {
"version": "7.2.0",
"dev": true,
@@ -4832,9 +5560,10 @@
}
},
"node_modules/globals": {
- "version": "13.11.0",
+ "version": "13.21.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+ "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
"dev": true,
- "license": "MIT",
"dependencies": {
"type-fest": "^0.20.2"
},
@@ -4846,15 +5575,16 @@
}
},
"node_modules/globby": {
- "version": "11.0.4",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
- "fast-glob": "^3.1.1",
- "ignore": "^5.1.4",
- "merge2": "^1.3.0",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
"slash": "^3.0.0"
},
"engines": {
@@ -4906,6 +5636,12 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
},
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
"node_modules/grpc-tools": {
"version": "1.11.2",
"dev": true,
@@ -4955,60 +5691,340 @@
"typescript": ">=3"
}
},
- "node_modules/hard-rejection": {
- "version": "2.1.0",
+ "node_modules/gts/node_modules/@eslint/eslintrc": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+ "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
"dev": true,
- "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
"engines": {
- "node": ">=6"
+ "node": "^10.12.0 || >=12.0.0"
}
},
- "node_modules/has": {
- "version": "1.0.3",
+ "node_modules/gts/node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/gts/node_modules/@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+ "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "function-bind": "^1.1.1"
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
},
"engines": {
- "node": ">= 0.4.0"
+ "node": ">=10.10.0"
}
},
- "node_modules/has-bigints": {
- "version": "1.0.1",
+ "node_modules/gts/node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz",
+ "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==",
"dev": true,
- "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": "4.33.0",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "debug": "^4.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.1.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^4.0.0",
+ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
}
},
- "node_modules/has-flag": {
+ "node_modules/gts/node_modules/@typescript-eslint/parser": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz",
+ "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
+ "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/types": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
+ "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
+ "dev": true,
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
+ "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/gts/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
+ "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/gts/node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/escape-string-regexp": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
- "license": "MIT",
"engines": {
- "node": ">=8"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/has-symbols": {
- "version": "1.0.2",
+ "node_modules/gts/node_modules/eslint": {
+ "version": "7.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
"dev": true,
- "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
"engines": {
- "node": ">= 0.4"
+ "node": "^10.12.0 || >=12.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/ljharb"
+ "url": "https://opencollective.com/eslint"
}
},
- "node_modules/has-tostringtag": {
- "version": "1.0.0",
+ "node_modules/gts/node_modules/eslint/node_modules/ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/gts/node_modules/espree": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+ "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/gts/node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gts/node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/hard-rejection": {
+ "version": "2.1.0",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "license": "MIT",
"dependencies": {
- "has-symbols": "^1.0.2"
+ "function-bind": "^1.1.1"
},
"engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
"node": ">= 0.4"
},
"funding": {
@@ -5265,6 +6281,22 @@
"node": ">=0.10.0"
}
},
+ "node_modules/icss-replace-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+ "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg=="
+ },
+ "node_modules/icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
"node_modules/idb-wrapper": {
"version": "1.7.2",
"dev": true,
@@ -5272,7 +6304,8 @@
},
"node_modules/ieee754": {
"version": "1.2.1",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
@@ -5286,21 +6319,33 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ],
- "license": "BSD-3-Clause"
+ ]
},
"node_modules/ignore": {
- "version": "5.1.8",
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 4"
}
},
+ "node_modules/import-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
+ "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==",
+ "dependencies": {
+ "import-from": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -5312,6 +6357,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/import-from": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
+ "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/import-from/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/import-lazy": {
"version": "2.1.0",
"dev": true,
@@ -5435,19 +6499,6 @@
"executioner": "^2.0.1"
}
},
- "node_modules/internal-slot": {
- "version": "1.0.3",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.1.0",
- "has": "^1.0.3",
- "side-channel": "^1.0.4"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/is": {
"version": "0.2.7",
"dev": true,
@@ -5460,32 +6511,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/is-bigint": {
- "version": "1.0.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-bigints": "^1.0.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-boolean-object": {
- "version": "1.1.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-builtin-module": {
"version": "3.1.0",
"dev": true,
@@ -5497,17 +6522,6 @@
"node": ">=6"
}
},
- "node_modules/is-callable": {
- "version": "1.2.4",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-ci": {
"version": "2.0.0",
"dev": true,
@@ -5521,7 +6535,6 @@
},
"node_modules/is-core-module": {
"version": "2.8.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"has": "^1.0.3"
@@ -5530,20 +6543,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/is-date-object": {
- "version": "1.0.5",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-extglob": {
"version": "2.1.1",
"dev": true,
@@ -5606,17 +6605,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/is-negative-zero": {
- "version": "2.0.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-npm": {
"version": "5.0.0",
"dev": true,
@@ -5636,20 +6624,6 @@
"node": ">=0.12.0"
}
},
- "node_modules/is-number-object": {
- "version": "1.0.6",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-obj": {
"version": "2.0.0",
"dev": true,
@@ -5692,29 +6666,6 @@
"@types/estree": "*"
}
},
- "node_modules/is-regex": {
- "version": "1.1.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-shared-array-buffer": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-stream": {
"version": "2.0.1",
"dev": true,
@@ -5726,50 +6677,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-string": {
- "version": "1.0.7",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-tostringtag": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/is-symbol": {
- "version": "1.0.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.2"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-typedarray": {
"version": "1.0.0",
"dev": true,
"license": "MIT"
},
- "node_modules/is-weakref": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/is-yarn-global": {
"version": "0.3.0",
"dev": true,
@@ -6672,8 +7584,9 @@
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -6774,18 +7687,6 @@
"jss": "10.8.1"
}
},
- "node_modules/jsx-ast-utils": {
- "version": "3.2.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-includes": "^3.1.3",
- "object.assign": "^4.1.2"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/keyv": {
"version": "3.1.0",
"dev": true,
@@ -6812,6 +7713,21 @@
"node": ">=6"
}
},
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.8.tgz",
+ "integrity": "sha512-aWAZwgPLS8hJ20lNPm9HNVs4inexz6S2sQa3wx/+ycuutMNE5/IfYxiWYBbi+9UWCQVaXYCOPUl6gFrPR7+jGg==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ }
+ },
"node_modules/latest-version": {
"version": "5.1.0",
"dev": true,
@@ -6992,8 +7908,9 @@
},
"node_modules/levn": {
"version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@@ -7002,11 +7919,42 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.1.6",
"dev": true,
"license": "MIT"
},
+ "node_modules/loader-utils": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
+ "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
+ "engines": {
+ "node": ">= 12.13.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/lodash": {
"version": "4.17.21",
"dev": true,
@@ -7014,19 +7962,12 @@
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/lodash.clonedeep": {
- "version": "4.5.0",
- "dev": true,
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
- "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
- "dev": true
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -7035,8 +7976,14 @@
},
"node_modules/lodash.truncate": {
"version": "4.4.2",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "dev": true
+ },
+ "node_modules/lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
},
"node_modules/long": {
"version": "5.2.1",
@@ -7110,7 +8057,7 @@
},
"node_modules/make-error": {
"version": "1.3.6",
- "dev": true,
+ "devOptional": true,
"license": "ISC"
},
"node_modules/makeerror": {
@@ -7145,6 +8092,11 @@
"safe-buffer": "^5.1.2"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
+ },
"node_modules/meow": {
"version": "9.0.0",
"dev": true,
@@ -7188,8 +8140,9 @@
},
"node_modules/merge2": {
"version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 8"
}
@@ -7285,9 +8238,10 @@
"license": "MIT"
},
"node_modules/minimatch": {
- "version": "3.0.4",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
- "license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -7368,6 +8322,23 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"dev": true,
@@ -7405,11 +8376,9 @@
"peer": true
},
"node_modules/node-releases": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
- "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
- "dev": true,
- "peer": true
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz",
+ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ=="
},
"node_modules/nopt": {
"version": "5.0.0",
@@ -7478,6 +8447,17 @@
"set-blocking": "^2.0.0"
}
},
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/nwsapi": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz",
@@ -7500,96 +8480,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/object-keys": {
- "version": "1.1.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object-path": {
- "version": "0.11.8",
- "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
- "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==",
- "engines": {
- "node": ">= 10.12.0"
- }
- },
- "node_modules/object.assign": {
- "version": "4.1.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "has-symbols": "^1.0.1",
- "object-keys": "^1.1.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.entries": {
- "version": "1.1.5",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
- "node_modules/object.fromentries": {
- "version": "2.0.5",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.hasown": {
- "version": "1.1.0",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/object.values": {
- "version": "1.1.5",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/octal": {
"version": "1.0.0",
"dev": true,
@@ -7626,16 +8516,17 @@
}
},
"node_modules/optionator": {
- "version": "0.9.1",
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true,
- "license": "MIT",
"dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.3"
+ "type-check": "^0.4.0"
},
"engines": {
"node": ">= 0.8.0"
@@ -7657,6 +8548,70 @@
"node": ">=6"
}
},
+ "node_modules/p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "dependencies": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "dependencies": {
+ "p-finally": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/p-try": {
"version": "2.2.0",
"dev": true,
@@ -7689,8 +8644,9 @@
},
"node_modules/parent-module": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
- "license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
@@ -7744,6 +8700,15 @@
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
},
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
+ "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
+ "dev": true,
+ "dependencies": {
+ "parse5": "^6.0.1"
+ }
+ },
"node_modules/path-exists": {
"version": "4.0.0",
"dev": true,
@@ -7770,13 +8735,13 @@
},
"node_modules/path-parse": {
"version": "1.0.7",
- "dev": true,
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -7810,9 +8775,7 @@
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true,
- "peer": true
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"node_modules/picomatch": {
"version": "2.3.0",
@@ -7825,6 +8788,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
+ "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/pirates": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
@@ -7935,10 +8909,545 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/postcss": {
+ "version": "8.4.24",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+ "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-calc": {
+ "version": "8.2.4",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz",
+ "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.9",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.2"
+ }
+ },
+ "node_modules/postcss-colormin": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz",
+ "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0",
+ "colord": "^2.9.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-convert-values": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
+ "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-comments": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
+ "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-duplicates": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
+ "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-empty": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
+ "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-discard-overridden": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
+ "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+ "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+ "dependencies": {
+ "lilconfig": "^2.0.5",
+ "yaml": "^1.10.2"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-merge-longhand": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
+ "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0",
+ "stylehacks": "^5.1.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-merge-rules": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz",
+ "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0",
+ "cssnano-utils": "^3.1.0",
+ "postcss-selector-parser": "^6.0.5"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-font-values": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz",
+ "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-gradients": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz",
+ "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==",
+ "dependencies": {
+ "colord": "^2.9.1",
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-params": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
+ "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-minify-selectors": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz",
+ "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.5"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-modules": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz",
+ "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==",
+ "dependencies": {
+ "generic-names": "^4.0.0",
+ "icss-replace-symbols": "^1.1.0",
+ "lodash.camelcase": "^4.3.0",
+ "postcss-modules-extract-imports": "^3.0.0",
+ "postcss-modules-local-by-default": "^4.0.0",
+ "postcss-modules-scope": "^3.0.0",
+ "postcss-modules-values": "^4.0.0",
+ "string-hash": "^1.1.1"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-modules-extract-imports": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
+ "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-local-by-default": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz",
+ "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==",
+ "dependencies": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-scope": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
+ "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "dependencies": {
+ "icss-utils": "^5.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >= 14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/postcss-normalize-charset": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
+ "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-display-values": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz",
+ "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-positions": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz",
+ "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-repeat-style": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz",
+ "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-string": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz",
+ "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-timing-functions": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz",
+ "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-unicode": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
+ "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz",
+ "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==",
+ "dependencies": {
+ "normalize-url": "^6.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-normalize-url/node_modules/normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/postcss-normalize-whitespace": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz",
+ "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-ordered-values": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz",
+ "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==",
+ "dependencies": {
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-reduce-initial": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz",
+ "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-reduce-transforms": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz",
+ "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.13",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
+ "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-svgo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz",
+ "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==",
+ "dependencies": {
+ "postcss-value-parser": "^4.2.0",
+ "svgo": "^2.7.0"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-unique-selectors": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz",
+ "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==",
+ "dependencies": {
+ "postcss-selector-parser": "^6.0.5"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
@@ -7952,14 +9461,17 @@
}
},
"node_modules/prettier": {
- "version": "2.4.1",
- "dev": true,
- "license": "MIT",
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
+ "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ==",
"bin": {
- "prettier": "bin-prettier.js"
+ "prettier": "bin/prettier.cjs"
},
"engines": {
- "node": ">=10.13.0"
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prettier-linter-helpers": {
@@ -8018,12 +9530,21 @@
},
"node_modules/progress": {
"version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
+ "node_modules/promise.series": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz",
+ "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ==",
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
"node_modules/prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -8121,6 +9642,8 @@
},
"node_modules/queue-microtask": {
"version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
@@ -8135,8 +9658,7 @@
"type": "consulting",
"url": "https://feross.org/support"
}
- ],
- "license": "MIT"
+ ]
},
"node_modules/quick-lru": {
"version": "4.0.1",
@@ -8386,21 +9908,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/regexp.prototype.flags": {
- "version": "1.3.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/regexpp": {
"version": "3.2.0",
"dev": true,
@@ -8444,12 +9951,22 @@
},
"node_modules/require-from-string": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.5"
+ }
+ },
"node_modules/requirejs": {
"version": "2.3.6",
"dev": true,
@@ -8469,7 +9986,6 @@
},
"node_modules/resolve": {
"version": "1.20.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.2.0",
@@ -8504,8 +10020,9 @@
},
"node_modules/resolve-from": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=4"
}
@@ -8542,8 +10059,9 @@
},
"node_modules/reusify": {
"version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
- "license": "MIT",
"engines": {
"iojs": ">=1.0.0",
"node": ">=0.10.0"
@@ -8586,50 +10104,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/rollup-plugin-dts": {
- "version": "4.2.2",
- "dev": true,
- "license": "LGPL-3.0",
- "dependencies": {
- "magic-string": "^0.26.1"
- },
- "engines": {
- "node": ">=v12.22.11"
- },
- "funding": {
- "url": "https://github.com/sponsors/Swatinem"
- },
- "optionalDependencies": {
- "@babel/code-frame": "^7.16.7"
- },
- "peerDependencies": {
- "rollup": "^2.55",
- "typescript": "^4.1"
- }
- },
- "node_modules/rollup-plugin-dts/node_modules/@babel/code-frame": {
- "version": "7.16.7",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@babel/highlight": "^7.16.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/rollup-plugin-dts/node_modules/magic-string": {
- "version": "0.26.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "sourcemap-codec": "^1.4.8"
- },
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/rollup-plugin-inject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
@@ -8701,6 +10175,32 @@
"rollup-plugin-inject": "^3.0.0"
}
},
+ "node_modules/rollup-plugin-postcss": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz",
+ "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "concat-with-sourcemaps": "^1.1.0",
+ "cssnano": "^5.0.1",
+ "import-cwd": "^3.0.0",
+ "p-queue": "^6.6.2",
+ "pify": "^5.0.0",
+ "postcss-load-config": "^3.0.0",
+ "postcss-modules": "^4.0.0",
+ "promise.series": "^0.2.0",
+ "resolve": "^1.19.0",
+ "rollup-pluginutils": "^2.8.2",
+ "safe-identifier": "^0.4.2",
+ "style-inject": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "postcss": "8.x"
+ }
+ },
"node_modules/rollup-plugin-sourcemaps": {
"version": "0.6.3",
"dev": true,
@@ -8724,7 +10224,6 @@
},
"node_modules/rollup-pluginutils": {
"version": "2.8.2",
- "dev": true,
"license": "MIT",
"dependencies": {
"estree-walker": "^0.6.1"
@@ -8732,7 +10231,6 @@
},
"node_modules/rollup-pluginutils/node_modules/estree-walker": {
"version": "0.6.1",
- "dev": true,
"license": "MIT"
},
"node_modules/run-async": {
@@ -8745,6 +10243,8 @@
},
"node_modules/run-parallel": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
@@ -8760,7 +10260,6 @@
"url": "https://feross.org/support"
}
],
- "license": "MIT",
"dependencies": {
"queue-microtask": "^1.2.2"
}
@@ -8797,6 +10296,11 @@
],
"license": "MIT"
},
+ "node_modules/safe-identifier": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
+ "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w=="
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"dev": true,
@@ -8829,9 +10333,10 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.3.5",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
- "license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
@@ -8933,8 +10438,9 @@
},
"node_modules/slice-ansi": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
- "license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -8949,12 +10455,19 @@
},
"node_modules/source-map": {
"version": "0.6.1",
- "dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/source-map-resolve": {
"version": "0.6.0",
"dev": true,
@@ -9013,6 +10526,12 @@
"dev": true,
"license": "BSD-3-Clause"
},
+ "node_modules/stable": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
+ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility"
+ },
"node_modules/stack-utils": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
@@ -9055,6 +10574,11 @@
"node": ">=0.6.19"
}
},
+ "node_modules/string-hash": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
+ "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A=="
+ },
"node_modules/string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -9087,48 +10611,6 @@
"node": ">=8"
}
},
- "node_modules/string.prototype.matchall": {
- "version": "4.0.6",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "has-symbols": "^1.0.2",
- "internal-slot": "^1.0.3",
- "regexp.prototype.flags": "^1.3.1",
- "side-channel": "^1.0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimend": {
- "version": "1.0.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/string.prototype.trimstart": {
- "version": "1.0.4",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/strip-ansi": {
"version": "6.0.1",
"dev": true,
@@ -9180,6 +10662,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-inject": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz",
+ "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw=="
+ },
"node_modules/style-to-js": {
"version": "1.1.0",
"dev": true,
@@ -9196,9 +10683,23 @@
"inline-style-parser": "0.1.1"
}
},
+ "node_modules/stylehacks": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
+ "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
+ "dependencies": {
+ "browserslist": "^4.21.4",
+ "postcss-selector-parser": "^6.0.4"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.15"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -9221,6 +10722,26 @@
"node": ">=8"
}
},
+ "node_modules/svgo": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
+ "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
+ "dependencies": {
+ "@trysound/sax": "0.2.0",
+ "commander": "^7.2.0",
+ "css-select": "^4.1.3",
+ "css-tree": "^1.1.3",
+ "csso": "^4.2.0",
+ "picocolors": "^1.0.0",
+ "stable": "^0.1.8"
+ },
+ "bin": {
+ "svgo": "bin/svgo"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -9228,12 +10749,12 @@
"dev": true
},
"node_modules/table": {
- "version": "6.7.2",
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
"dev": true,
- "license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
- "lodash.clonedeep": "^4.5.0",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
@@ -9244,9 +10765,10 @@
}
},
"node_modules/table/node_modules/ajv": {
- "version": "8.6.3",
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -9260,8 +10782,9 @@
},
"node_modules/table/node_modules/json-schema-traverse": {
"version": "1.0.0",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
},
"node_modules/tar": {
"version": "6.1.11",
@@ -9417,6 +10940,18 @@
"node": ">=8"
}
},
+ "node_modules/ts-api-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz",
+ "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.13.0"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.2.0"
+ }
+ },
"node_modules/ts-jest": {
"version": "28.0.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.5.tgz",
@@ -9467,7 +11002,7 @@
},
"node_modules/ts-node": {
"version": "10.8.1",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@cspotcode/source-map-support": "^0.8.0",
@@ -9509,7 +11044,7 @@
},
"node_modules/ts-node/node_modules/acorn": {
"version": "8.7.1",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -9520,7 +11055,7 @@
},
"node_modules/ts-node/node_modules/arg": {
"version": "4.1.3",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/ts-protoc-gen": {
@@ -9561,8 +11096,9 @@
},
"node_modules/tsutils": {
"version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"tslib": "^1.8.1"
},
@@ -9575,13 +11111,15 @@
},
"node_modules/tsutils/node_modules/tslib": {
"version": "1.14.1",
- "dev": true,
- "license": "0BSD"
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
},
"node_modules/type-check": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
- "license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
@@ -9624,7 +11162,7 @@
},
"node_modules/typescript": {
"version": "4.4.4",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -9634,20 +11172,6 @@
"node": ">=4.2.0"
}
},
- "node_modules/unbox-primitive": {
- "version": "1.0.1",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.1",
- "has-bigints": "^1.0.1",
- "has-symbols": "^1.0.2",
- "which-boxed-primitive": "^1.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/union": {
"version": "0.5.0",
"dev": true,
@@ -9693,10 +11217,9 @@
}
},
"node_modules/update-browserslist-db": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz",
- "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==",
- "dev": true,
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
"funding": [
{
"type": "opencollective",
@@ -9705,15 +11228,18 @@
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
}
],
- "peer": true,
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
},
"bin": {
- "browserslist-lint": "cli.js"
+ "update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
@@ -9748,8 +11274,9 @@
},
"node_modules/uri-js": {
"version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
- "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
@@ -9772,17 +11299,17 @@
},
"node_modules/util-deprecate": {
"version": "1.0.2",
- "dev": true,
"license": "MIT"
},
"node_modules/v8-compile-cache": {
"version": "2.3.0",
- "dev": true,
- "license": "MIT"
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/validate-npm-package-license": {
@@ -9891,21 +11418,6 @@
"node": ">= 8"
}
},
- "node_modules/which-boxed-primitive": {
- "version": "1.0.2",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/wide-align": {
"version": "1.1.5",
"dev": true,
@@ -10029,6 +11541,14 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/yargs": {
"version": "16.2.0",
"dev": true,
@@ -10056,14 +11576,32 @@
},
"node_modules/yn": {
"version": "3.1.1",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
},
"dependencies": {
+ "@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true
+ },
"@ampproject/remapping": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
@@ -10077,6 +11615,8 @@
},
"@babel/code-frame": {
"version": "7.12.11",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+ "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.10.4"
@@ -10562,7 +12102,7 @@
},
"@cspotcode/source-map-support": {
"version": "0.8.1",
- "dev": true,
+ "devOptional": true,
"requires": {
"@jridgewell/trace-mapping": "0.3.9"
}
@@ -10571,27 +12111,63 @@
"version": "0.8.0",
"dev": true
},
+ "@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
+ }
+ }
+ },
+ "@eslint-community/regexpp": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
+ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
+ "dev": true
+ },
"@eslint/eslintrc": {
- "version": "0.4.3",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
- "debug": "^4.1.1",
- "espree": "^7.3.0",
- "globals": "^13.9.0",
- "ignore": "^4.0.6",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^3.13.1",
- "minimatch": "^3.0.4",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"dependencies": {
- "ignore": {
- "version": "4.0.6",
- "dev": true
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
}
}
},
+ "@eslint/js": {
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz",
+ "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
+ "dev": true
+ },
"@grpc/grpc-js": {
"version": "1.4.1",
"dev": true,
@@ -10639,16 +12215,26 @@
}
},
"@humanwhocodes/config-array": {
- "version": "0.5.0",
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+ "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"dev": true,
"requires": {
- "@humanwhocodes/object-schema": "^1.2.0",
+ "@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
- "minimatch": "^3.0.4"
+ "minimatch": "^3.0.5"
}
},
+ "@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true
+ },
"@humanwhocodes/object-schema": {
- "version": "1.2.0",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
"@istanbuljs/load-nyc-config": {
@@ -11035,7 +12621,7 @@
},
"@jridgewell/resolve-uri": {
"version": "3.0.8",
- "dev": true
+ "devOptional": true
},
"@jridgewell/set-array": {
"version": "1.1.2",
@@ -11046,11 +12632,11 @@
},
"@jridgewell/sourcemap-codec": {
"version": "1.4.14",
- "dev": true
+ "devOptional": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.9",
- "dev": true,
+ "devOptional": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
"@jridgewell/sourcemap-codec": "^1.4.10"
@@ -11160,6 +12746,8 @@
},
"@nodelib/fs.scandir": {
"version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "2.0.5",
@@ -11168,10 +12756,14 @@
},
"@nodelib/fs.stat": {
"version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true
},
"@nodelib/fs.walk": {
"version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"requires": {
"@nodelib/fs.scandir": "2.1.5",
@@ -11333,21 +12925,26 @@
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
"dev": true
},
+ "@trysound/sax": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
+ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
+ },
"@tsconfig/node10": {
"version": "1.0.9",
- "dev": true
+ "devOptional": true
},
"@tsconfig/node12": {
"version": "1.0.11",
- "dev": true
+ "devOptional": true
},
"@tsconfig/node14": {
"version": "1.0.3",
- "dev": true
+ "devOptional": true
},
"@tsconfig/node16": {
"version": "1.0.3",
- "dev": true
+ "devOptional": true
},
"@types/babel__core": {
"version": "7.1.19",
@@ -11463,7 +13060,9 @@
}
},
"@types/json-schema": {
- "version": "7.0.9",
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
+ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==",
"dev": true
},
"@types/long": {
@@ -11476,7 +13075,7 @@
},
"@types/node": {
"version": "16.11.4",
- "dev": true
+ "devOptional": true
},
"@types/normalize-package-data": {
"version": "2.4.1",
@@ -11533,6 +13132,12 @@
"version": "0.16.2",
"dev": true
},
+ "@types/semver": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+ "dev": true
+ },
"@types/stack-utils": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
@@ -11561,21 +13166,62 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "4.33.0",
- "dev": true,
- "requires": {
- "@typescript-eslint/experimental-utils": "4.33.0",
- "@typescript-eslint/scope-manager": "4.33.0",
- "debug": "^4.3.1",
- "functional-red-black-tree": "^1.0.1",
- "ignore": "^5.1.8",
- "regexpp": "^3.1.0",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.4.0.tgz",
+ "integrity": "sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/regexpp": "^4.5.1",
+ "@typescript-eslint/scope-manager": "6.4.0",
+ "@typescript-eslint/type-utils": "6.4.0",
+ "@typescript-eslint/utils": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.4",
+ "natural-compare": "^1.4.0",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz",
+ "integrity": "sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz",
+ "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==",
+ "dev": true
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz",
+ "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
+ }
}
},
"@typescript-eslint/experimental-utils": {
"version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz",
+ "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.7",
@@ -11586,8 +13232,51 @@
"eslint-utils": "^3.0.0"
},
"dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
+ "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
+ "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
+ "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
+ "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
"eslint-utils": {
"version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^2.0.0"
@@ -11596,46 +13285,186 @@
}
},
"@typescript-eslint/parser": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.3.0.tgz",
+ "integrity": "sha512-ibP+y2Gr6p0qsUkhs7InMdXrwldjxZw66wpcQq9/PzAroM45wdwyu81T+7RibNCh8oc0AgrsyCwJByncY0Ongg==",
"dev": true,
+ "peer": true,
"requires": {
- "@typescript-eslint/scope-manager": "4.33.0",
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/typescript-estree": "4.33.0",
- "debug": "^4.3.1"
+ "@typescript-eslint/scope-manager": "6.3.0",
+ "@typescript-eslint/types": "6.3.0",
+ "@typescript-eslint/typescript-estree": "6.3.0",
+ "@typescript-eslint/visitor-keys": "6.3.0",
+ "debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.3.0.tgz",
+ "integrity": "sha512-WlNFgBEuGu74ahrXzgefiz/QlVb+qg8KDTpknKwR7hMH+lQygWyx0CQFoUmMn1zDkQjTBBIn75IxtWss77iBIQ==",
"dev": true,
+ "peer": true,
"requires": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0"
+ "@typescript-eslint/types": "6.3.0",
+ "@typescript-eslint/visitor-keys": "6.3.0"
+ }
+ },
+ "@typescript-eslint/type-utils": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.4.0.tgz",
+ "integrity": "sha512-TvqrUFFyGY0cX3WgDHcdl2/mMCWCDv/0thTtx/ODMY1QhEiyFtv/OlLaNIiYLwRpAxAtOLOY9SUf1H3Q3dlwAg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/typescript-estree": "6.4.0",
+ "@typescript-eslint/utils": "6.4.0",
+ "debug": "^4.3.4",
+ "ts-api-utils": "^1.0.1"
+ },
+ "dependencies": {
+ "@typescript-eslint/types": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz",
+ "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz",
+ "integrity": "sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz",
+ "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
+ }
}
},
"@typescript-eslint/types": {
- "version": "4.33.0",
- "dev": true
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.3.0.tgz",
+ "integrity": "sha512-K6TZOvfVyc7MO9j60MkRNWyFSf86IbOatTKGrpTQnzarDZPYPVy0oe3myTMq7VjhfsUAbNUW8I5s+2lZvtx1gg==",
+ "dev": true,
+ "peer": true
},
"@typescript-eslint/typescript-estree": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.3.0.tgz",
+ "integrity": "sha512-Xh4NVDaC4eYKY4O3QGPuQNp5NxBAlEvNQYOqJquR2MePNxO11E5K3t5x4M4Mx53IZvtpW+mBxIT0s274fLUocg==",
"dev": true,
+ "peer": true,
"requires": {
- "@typescript-eslint/types": "4.33.0",
- "@typescript-eslint/visitor-keys": "4.33.0",
- "debug": "^4.3.1",
- "globby": "^11.0.3",
- "is-glob": "^4.0.1",
- "semver": "^7.3.5",
- "tsutils": "^3.21.0"
+ "@typescript-eslint/types": "6.3.0",
+ "@typescript-eslint/visitor-keys": "6.3.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ }
+ },
+ "@typescript-eslint/utils": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.4.0.tgz",
+ "integrity": "sha512-BvvwryBQpECPGo8PwF/y/q+yacg8Hn/2XS+DqL/oRsOPK+RPt29h5Ui5dqOKHDlbXrAeHUTnyG3wZA0KTDxRZw==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@types/json-schema": "^7.0.12",
+ "@types/semver": "^7.5.0",
+ "@typescript-eslint/scope-manager": "6.4.0",
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/typescript-estree": "6.4.0",
+ "semver": "^7.5.4"
+ },
+ "dependencies": {
+ "@typescript-eslint/scope-manager": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.4.0.tgz",
+ "integrity": "sha512-TUS7vaKkPWDVvl7GDNHFQMsMruD+zhkd3SdVW0d7b+7Zo+bd/hXJQ8nsiUZMi1jloWo6c9qt3B7Sqo+flC1nig==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.4.0.tgz",
+ "integrity": "sha512-+FV9kVFrS7w78YtzkIsNSoYsnOtrYVnKWSTVXoL1761CsCRv5wpDOINgsXpxD67YCLZtVQekDDyaxfjVWUJmmg==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.4.0.tgz",
+ "integrity": "sha512-iDPJArf/K2sxvjOR6skeUCNgHR/tCQXBsa+ee1/clRKr3olZjZ/dSkXPZjG6YkPtnW6p5D1egeEPMCW6Gn4yLA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "@typescript-eslint/visitor-keys": "6.4.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.5.4",
+ "ts-api-utils": "^1.0.1"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.4.0.tgz",
+ "integrity": "sha512-yJSfyT+uJm+JRDWYRYdCm2i+pmvXJSMtPR9Cq5/XQs4QIgNoLcoRtDdzsLbLsFM/c6um6ohQkg/MLxWvoIndJA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "6.4.0",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
+ }
}
},
"@typescript-eslint/visitor-keys": {
- "version": "4.33.0",
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.3.0.tgz",
+ "integrity": "sha512-kEhRRj7HnvaSjux1J9+7dBen15CdWmDnwrpyiHsFX6Qx2iW5LOBUgNefOFeh2PjWPlNwN8TOn6+4eBU3J/gupw==",
"dev": true,
+ "peer": true,
"requires": {
- "@typescript-eslint/types": "4.33.0",
- "eslint-visitor-keys": "^2.0.0"
+ "@typescript-eslint/types": "6.3.0",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "peer": true
+ }
}
},
"abab": {
@@ -11679,12 +13508,14 @@
},
"acorn-jsx": {
"version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"requires": {}
},
"acorn-walk": {
"version": "8.2.0",
- "dev": true
+ "devOptional": true
},
"agent-base": {
"version": "6.0.2",
@@ -11695,6 +13526,8 @@
},
"ajv": {
"version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@@ -11715,7 +13548,9 @@
}
},
"ansi-colors": {
- "version": "4.1.1",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
"dev": true
},
"ansi-escapes": {
@@ -11737,7 +13572,6 @@
},
"ansi-styles": {
"version": "4.3.0",
- "dev": true,
"requires": {
"color-convert": "^2.0.1"
}
@@ -11785,30 +13619,27 @@
"version": "5.0.2",
"dev": true
},
- "array-includes": {
- "version": "3.1.4",
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "is-string": "^1.0.7"
+ "dequal": "^2.0.3"
}
},
"array-union": {
"version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true
},
- "array.prototype.flatmap": {
- "version": "1.2.5",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.0"
- }
- },
"arrify": {
"version": "1.0.1",
"dev": true
@@ -11825,6 +13656,8 @@
},
"astral-regex": {
"version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
"async": {
@@ -11844,6 +13677,18 @@
"version": "2.1.2",
"dev": true
},
+ "axe-core": {
+ "version": "4.7.2",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.2.tgz",
+ "integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==",
+ "dev": true
+ },
+ "axobject-query": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
+ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==",
+ "dev": true
+ },
"babel-jest": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz",
@@ -11924,8 +13769,7 @@
"dev": true
},
"base64-js": {
- "version": "1.5.1",
- "dev": true
+ "version": "1.5.1"
},
"basic-auth": {
"version": "1.1.0",
@@ -11942,6 +13786,11 @@
"version": "4.12.0",
"dev": true
},
+ "boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ },
"boxen": {
"version": "5.1.2",
"dev": true,
@@ -12079,16 +13928,14 @@
}
},
"browserslist": {
- "version": "4.21.1",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.1.tgz",
- "integrity": "sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==",
- "dev": true,
- "peer": true,
+ "version": "4.21.9",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz",
+ "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==",
"requires": {
- "caniuse-lite": "^1.0.30001359",
- "electron-to-chromium": "^1.4.172",
- "node-releases": "^2.0.5",
- "update-browserslist-db": "^1.0.4"
+ "caniuse-lite": "^1.0.30001503",
+ "electron-to-chromium": "^1.4.431",
+ "node-releases": "^2.0.12",
+ "update-browserslist-db": "^1.0.11"
}
},
"bs-logger": {
@@ -12112,7 +13959,8 @@
},
"buffer": {
"version": "6.0.3",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
@@ -12191,16 +14039,24 @@
}
}
},
+ "caniuse-api": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz",
+ "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==",
+ "requires": {
+ "browserslist": "^4.0.0",
+ "caniuse-lite": "^1.0.0",
+ "lodash.memoize": "^4.1.2",
+ "lodash.uniq": "^4.5.0"
+ }
+ },
"caniuse-lite": {
- "version": "1.0.30001365",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001365.tgz",
- "integrity": "sha512-VDQZ8OtpuIPMBA4YYvZXECtXbddMCUFJk1qu8Mqxfm/SZJNSr1cy4IuLCOL7RJ/YASrvJcYg1Zh+UEUQ5m6z8Q==",
- "dev": true,
- "peer": true
+ "version": "1.0.30001509",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz",
+ "integrity": "sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA=="
},
"chalk": {
"version": "4.1.2",
- "dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -12295,19 +14151,22 @@
},
"color-convert": {
"version": "2.0.1",
- "dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
- "version": "1.1.4",
- "dev": true
+ "version": "1.1.4"
},
"color-support": {
"version": "1.1.3",
"dev": true
},
+ "colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
+ },
"colors": {
"version": "1.4.0",
"dev": true
@@ -12321,6 +14180,11 @@
"delayed-stream": "~1.0.0"
}
},
+ "commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="
+ },
"commondir": {
"version": "1.0.1",
"dev": true
@@ -12369,6 +14233,14 @@
}
}
},
+ "concat-with-sourcemaps": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz",
+ "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==",
+ "requires": {
+ "source-map": "^0.6.1"
+ }
+ },
"configstore": {
"version": "5.0.1",
"dev": true,
@@ -12408,11 +14280,6 @@
"version": "2.0.1",
"dev": true
},
- "crc": {
- "version": "4.1.1",
- "dev": true,
- "requires": {}
- },
"create-ecdh": {
"version": "4.0.4",
"dev": true,
@@ -12446,7 +14313,7 @@
},
"create-require": {
"version": "1.1.1",
- "dev": true
+ "devOptional": true
},
"cross-spawn": {
"version": "7.0.3",
@@ -12478,6 +14345,43 @@
"version": "2.0.0",
"dev": true
},
+ "css-declaration-sorter": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz",
+ "integrity": "sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew==",
+ "requires": {}
+ },
+ "css-select": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+ "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+ "requires": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.0.1",
+ "domhandler": "^4.3.1",
+ "domutils": "^2.8.0",
+ "nth-check": "^2.0.1"
+ },
+ "dependencies": {
+ "domhandler": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+ "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+ "requires": {
+ "domelementtype": "^2.2.0"
+ }
+ }
+ }
+ },
+ "css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "requires": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ }
+ },
"css-vendor": {
"version": "2.0.8",
"dev": true,
@@ -12486,6 +14390,76 @@
"is-in-browser": "^1.0.2"
}
},
+ "css-what": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
+ "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw=="
+ },
+ "cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
+ },
+ "cssnano": {
+ "version": "5.1.15",
+ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz",
+ "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==",
+ "requires": {
+ "cssnano-preset-default": "^5.2.14",
+ "lilconfig": "^2.0.3",
+ "yaml": "^1.10.2"
+ }
+ },
+ "cssnano-preset-default": {
+ "version": "5.2.14",
+ "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz",
+ "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==",
+ "requires": {
+ "css-declaration-sorter": "^6.3.1",
+ "cssnano-utils": "^3.1.0",
+ "postcss-calc": "^8.2.3",
+ "postcss-colormin": "^5.3.1",
+ "postcss-convert-values": "^5.1.3",
+ "postcss-discard-comments": "^5.1.2",
+ "postcss-discard-duplicates": "^5.1.0",
+ "postcss-discard-empty": "^5.1.1",
+ "postcss-discard-overridden": "^5.1.0",
+ "postcss-merge-longhand": "^5.1.7",
+ "postcss-merge-rules": "^5.1.4",
+ "postcss-minify-font-values": "^5.1.0",
+ "postcss-minify-gradients": "^5.1.1",
+ "postcss-minify-params": "^5.1.4",
+ "postcss-minify-selectors": "^5.2.1",
+ "postcss-normalize-charset": "^5.1.0",
+ "postcss-normalize-display-values": "^5.1.0",
+ "postcss-normalize-positions": "^5.1.1",
+ "postcss-normalize-repeat-style": "^5.1.1",
+ "postcss-normalize-string": "^5.1.0",
+ "postcss-normalize-timing-functions": "^5.1.0",
+ "postcss-normalize-unicode": "^5.1.1",
+ "postcss-normalize-url": "^5.1.0",
+ "postcss-normalize-whitespace": "^5.1.1",
+ "postcss-ordered-values": "^5.1.3",
+ "postcss-reduce-initial": "^5.1.2",
+ "postcss-reduce-transforms": "^5.1.0",
+ "postcss-svgo": "^5.1.0",
+ "postcss-unique-selectors": "^5.1.1"
+ }
+ },
+ "cssnano-utils": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
+ "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==",
+ "requires": {}
+ },
+ "csso": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
+ "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
+ "requires": {
+ "css-tree": "^1.1.2"
+ }
+ },
"cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@@ -12552,7 +14526,9 @@
}
},
"debug": {
- "version": "4.3.2",
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
@@ -12617,13 +14593,6 @@
"abstract-leveldown": "~0.12.1"
}
},
- "define-properties": {
- "version": "1.1.3",
- "dev": true,
- "requires": {
- "object-keys": "^1.0.12"
- }
- },
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -12634,6 +14603,12 @@
"version": "1.0.0",
"dev": true
},
+ "dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true
+ },
"des.js": {
"version": "1.0.1",
"dev": true,
@@ -12655,7 +14630,7 @@
},
"diff": {
"version": "4.0.2",
- "dev": true
+ "devOptional": true
},
"diff-sequences": {
"version": "28.1.1",
@@ -12674,18 +14649,13 @@
},
"dir-glob": {
"version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true,
"requires": {
"path-type": "^4.0.0"
}
},
- "doctrine": {
- "version": "2.1.0",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
"dom-helpers": {
"version": "5.2.1",
"dev": true,
@@ -12696,16 +14666,48 @@
},
"dom-serializer": {
"version": "1.3.2",
- "dev": true,
"requires": {
"domelementtype": "^2.0.1",
"domhandler": "^4.2.0",
"entities": "^2.0.0"
}
},
+ "dom5": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dom5/-/dom5-3.0.1.tgz",
+ "integrity": "sha512-JPFiouQIr16VQ4dX6i0+Hpbg3H2bMKPmZ+WZgBOSSvOPx9QHwwY8sPzeM2baUtViESYto6wC2nuZOMC/6gulcA==",
+ "dev": true,
+ "requires": {
+ "@types/parse5": "^2.2.34",
+ "clone": "^2.1.0",
+ "parse5": "^4.0.0"
+ },
+ "dependencies": {
+ "@types/parse5": {
+ "version": "2.2.34",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-2.2.34.tgz",
+ "integrity": "sha512-p3qOvaRsRpFyEmaS36RtLzpdxZZnmxGuT1GMgzkTtTJVFuEw7KFjGK83MFODpJExgX1bEzy9r0NYjMC3IMfi7w==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "dev": true
+ },
+ "parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+ "dev": true
+ }
+ }
+ },
"domelementtype": {
- "version": "2.2.0",
- "dev": true
+ "version": "2.2.0"
},
"domexception": {
"version": "4.0.0",
@@ -12726,14 +14728,12 @@
},
"domhandler": {
"version": "4.2.2",
- "dev": true,
"requires": {
"domelementtype": "^2.2.0"
}
},
"domutils": {
"version": "2.8.0",
- "dev": true,
"requires": {
"dom-serializer": "^1.0.1",
"domelementtype": "^2.2.0",
@@ -12756,11 +14756,9 @@
"dev": true
},
"electron-to-chromium": {
- "version": "1.4.186",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.186.tgz",
- "integrity": "sha512-YoVeFrGd/7ROjz4R9uPoND1K/hSRC/xADy9639ZmIZeJSaBnKdYx3I6LMPsY7CXLpK7JFgKQVzeZ/dk2br6Eaw==",
- "dev": true,
- "peer": true
+ "version": "1.4.442",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.442.tgz",
+ "integrity": "sha512-RkrZF//Ya+0aJq2NM3OdisNh5ZodZq1rdXOS96G8DdDgpDKqKE81yTbbQ3F/4CKm1JBPsGu1Lp/akkna2xO06Q=="
},
"elliptic": {
"version": "6.5.4",
@@ -12794,15 +14792,17 @@
}
},
"enquirer": {
- "version": "2.3.6",
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
"dev": true,
"requires": {
- "ansi-colors": "^4.1.1"
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
}
},
"entities": {
- "version": "2.2.0",
- "dev": true
+ "version": "2.2.0"
},
"errno": {
"version": "0.1.8",
@@ -12824,44 +14824,8 @@
"is-arrayish": "^0.2.1"
}
},
- "es-abstract": {
- "version": "1.19.1",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "get-intrinsic": "^1.1.1",
- "get-symbol-description": "^1.0.0",
- "has": "^1.0.3",
- "has-symbols": "^1.0.2",
- "internal-slot": "^1.0.3",
- "is-callable": "^1.2.4",
- "is-negative-zero": "^2.0.1",
- "is-regex": "^1.1.4",
- "is-shared-array-buffer": "^1.0.1",
- "is-string": "^1.0.7",
- "is-weakref": "^1.0.1",
- "object-inspect": "^1.11.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.2",
- "string.prototype.trimend": "^1.0.4",
- "string.prototype.trimstart": "^1.0.4",
- "unbox-primitive": "^1.0.1"
- }
- },
- "es-to-primitive": {
- "version": "1.2.1",
- "dev": true,
- "requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- }
- },
"escalade": {
- "version": "3.1.1",
- "dev": true
+ "version": "3.1.1"
},
"escape-goat": {
"version": "2.1.1",
@@ -12926,49 +14890,48 @@
}
},
"eslint": {
- "version": "7.32.0",
- "dev": true,
- "requires": {
- "@babel/code-frame": "7.12.11",
- "@eslint/eslintrc": "^0.4.3",
- "@humanwhocodes/config-array": "^0.5.0",
- "ajv": "^6.10.0",
+ "version": "8.47.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz",
+ "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "^8.47.0",
+ "@humanwhocodes/config-array": "^0.11.10",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
- "debug": "^4.0.1",
+ "debug": "^4.3.2",
"doctrine": "^3.0.0",
- "enquirer": "^2.3.5",
"escape-string-regexp": "^4.0.0",
- "eslint-scope": "^5.1.1",
- "eslint-utils": "^2.1.0",
- "eslint-visitor-keys": "^2.0.0",
- "espree": "^7.3.1",
- "esquery": "^1.4.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^5.1.2",
- "globals": "^13.6.0",
- "ignore": "^4.0.6",
- "import-fresh": "^3.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
- "js-yaml": "^3.13.1",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
- "minimatch": "^3.0.4",
+ "minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
- "optionator": "^0.9.1",
- "progress": "^2.0.0",
- "regexpp": "^3.1.0",
- "semver": "^7.2.1",
- "strip-ansi": "^6.0.0",
- "strip-json-comments": "^3.1.0",
- "table": "^6.0.9",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
},
"dependencies": {
"doctrine": {
@@ -12982,9 +14945,39 @@
"version": "4.0.0",
"dev": true
},
- "ignore": {
- "version": "4.0.6",
+ "eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
}
}
},
@@ -13001,6 +14994,59 @@
"regexpp": "^3.0.0"
}
},
+ "eslint-plugin-lit": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.9.1.tgz",
+ "integrity": "sha512-XFFVufVxYJwqRB9sLkDXB7SvV1xi9hrC4HRFEdX1h9+iZ3dm4x9uS7EuT9uaXs6zR3DEgcojia1F7pmvWbc4Gg==",
+ "dev": true,
+ "requires": {
+ "parse5": "^6.0.1",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "requireindex": "^1.2.0"
+ }
+ },
+ "eslint-plugin-lit-a11y": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-4.1.0.tgz",
+ "integrity": "sha512-tJUYhKUZ1+NwOBzNHRX7ZETvGgMxWA+m9TsM23oN1qp4hQacTYBafeOhXHh6zyGnHaBIWzmCxW/G7doHeetkAw==",
+ "dev": true,
+ "requires": {
+ "aria-query": "^5.1.3",
+ "axe-core": "^4.3.3",
+ "axobject-query": "^2.2.0",
+ "dom5": "^3.0.1",
+ "emoji-regex": "^10.2.1",
+ "eslint-plugin-lit": "^1.6.0",
+ "eslint-rule-extender": "0.0.1",
+ "language-tags": "^1.0.5",
+ "parse5": "^7.1.2",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "requireindex": "~1.2.0"
+ },
+ "dependencies": {
+ "emoji-regex": {
+ "version": "10.2.1",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz",
+ "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==",
+ "dev": true
+ },
+ "entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true
+ },
+ "parse5": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
+ "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==",
+ "dev": true,
+ "requires": {
+ "entities": "^4.4.0"
+ }
+ }
+ }
+ },
"eslint-plugin-node": {
"version": "11.1.0",
"dev": true,
@@ -13026,39 +15072,11 @@
"prettier-linter-helpers": "^1.0.0"
}
},
- "eslint-plugin-react": {
- "version": "7.26.1",
- "dev": true,
- "requires": {
- "array-includes": "^3.1.3",
- "array.prototype.flatmap": "^1.2.4",
- "doctrine": "^2.1.0",
- "estraverse": "^5.2.0",
- "jsx-ast-utils": "^2.4.1 || ^3.0.0",
- "minimatch": "^3.0.4",
- "object.entries": "^1.1.4",
- "object.fromentries": "^2.0.4",
- "object.hasown": "^1.0.0",
- "object.values": "^1.1.4",
- "prop-types": "^15.7.2",
- "resolve": "^2.0.0-next.3",
- "semver": "^6.3.0",
- "string.prototype.matchall": "^4.0.5"
- },
- "dependencies": {
- "resolve": {
- "version": "2.0.0-next.3",
- "dev": true,
- "requires": {
- "is-core-module": "^2.2.0",
- "path-parse": "^1.0.6"
- }
- },
- "semver": {
- "version": "6.3.0",
- "dev": true
- }
- }
+ "eslint-rule-extender": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-rule-extender/-/eslint-rule-extender-0.0.1.tgz",
+ "integrity": "sha512-F0j1Twve3lamL3J0rRSVAynlp58sDPG39JFcQrM+u9Na7PmCgiPHNODh6YE9mduaGcsn3NBqbf6LZRj0cLr8Ng==",
+ "dev": true
},
"eslint-scope": {
"version": "5.1.1",
@@ -13092,16 +15110,26 @@
"dev": true
},
"espree": {
- "version": "7.3.1",
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
"dev": true,
"requires": {
- "acorn": "^7.4.0",
- "acorn-jsx": "^5.3.1",
- "eslint-visitor-keys": "^1.3.0"
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
},
"dependencies": {
+ "acorn": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "dev": true
+ },
"eslint-visitor-keys": {
- "version": "1.3.0",
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true
}
}
@@ -13111,7 +15139,9 @@
"dev": true
},
"esquery": {
- "version": "1.4.0",
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"dev": true,
"requires": {
"estraverse": "^5.1.0"
@@ -13150,8 +15180,7 @@
}
},
"eventemitter3": {
- "version": "4.0.7",
- "dev": true
+ "version": "4.0.7"
},
"evp_bytestokey": {
"version": "1.0.3",
@@ -13224,6 +15253,8 @@
},
"fast-deep-equal": {
"version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fast-diff": {
@@ -13231,7 +15262,9 @@
"dev": true
},
"fast-glob": {
- "version": "3.2.7",
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
"dev": true,
"requires": {
"@nodelib/fs.stat": "^2.0.2",
@@ -13250,7 +15283,9 @@
"dev": true
},
"fastq": {
- "version": "1.13.0",
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"dev": true,
"requires": {
"reusify": "^1.0.4"
@@ -13287,6 +15322,16 @@
"to-regex-range": "^5.0.1"
}
},
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
"flat-cache": {
"version": "3.0.4",
"dev": true,
@@ -13345,8 +15390,7 @@
"dev": true
},
"function-bind": {
- "version": "1.1.1",
- "dev": true
+ "version": "1.1.1"
},
"functional-red-black-tree": {
"version": "1.0.1",
@@ -13399,6 +15443,14 @@
}
}
},
+ "generic-names": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/generic-names/-/generic-names-4.0.0.tgz",
+ "integrity": "sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A==",
+ "requires": {
+ "loader-utils": "^3.2.0"
+ }
+ },
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -13430,14 +15482,6 @@
"version": "6.0.1",
"dev": true
},
- "get-symbol-description": {
- "version": "1.0.0",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "get-intrinsic": "^1.1.1"
- }
- },
"glob": {
"version": "7.2.0",
"dev": true,
@@ -13465,21 +15509,25 @@
}
},
"globals": {
- "version": "13.11.0",
+ "version": "13.21.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+ "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
}
},
"globby": {
- "version": "11.0.4",
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"requires": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
- "fast-glob": "^3.1.1",
- "ignore": "^5.1.4",
- "merge2": "^1.3.0",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
"slash": "^3.0.0"
}
},
@@ -13518,6 +15566,12 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
},
+ "graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
"grpc-tools": {
"version": "1.11.2",
"dev": true,
@@ -13549,6 +15603,209 @@
"rimraf": "^3.0.2",
"update-notifier": "^5.0.0",
"write-file-atomic": "^3.0.3"
+ },
+ "dependencies": {
+ "@eslint/eslintrc": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+ "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ }
+ }
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+ "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@typescript-eslint/eslint-plugin": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz",
+ "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "4.33.0",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "debug": "^4.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.1.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/parser": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz",
+ "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "debug": "^4.3.1"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz",
+ "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz",
+ "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz",
+ "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz",
+ "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true
+ },
+ "eslint": {
+ "version": "7.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
+ }
+ }
+ },
+ "espree": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+ "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true
+ }
+ }
+ },
+ "prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "dev": true
+ }
}
},
"hard-rejection": {
@@ -13557,30 +15814,17 @@
},
"has": {
"version": "1.0.3",
- "dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
- "has-bigints": {
- "version": "1.0.1",
- "dev": true
- },
"has-flag": {
- "version": "4.0.0",
- "dev": true
+ "version": "4.0.0"
},
"has-symbols": {
"version": "1.0.2",
"dev": true
},
- "has-tostringtag": {
- "version": "1.0.0",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.2"
- }
- },
"has-unicode": {
"version": "2.0.1",
"dev": true
@@ -13764,26 +16008,65 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "icss-replace-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz",
+ "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg=="
+ },
+ "icss-utils": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+ "requires": {}
+ },
"idb-wrapper": {
"version": "1.7.2",
"dev": true
},
"ieee754": {
"version": "1.2.1",
- "dev": true
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
},
"ignore": {
- "version": "5.1.8",
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"dev": true
},
+ "import-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
+ "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==",
+ "requires": {
+ "import-from": "^3.0.0"
+ }
+ },
"import-fresh": {
"version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
}
},
+ "import-from": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
+ "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
+ "requires": {
+ "resolve-from": "^5.0.0"
+ },
+ "dependencies": {
+ "resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="
+ }
+ }
+ },
"import-lazy": {
"version": "2.1.0",
"dev": true
@@ -13870,15 +16153,6 @@
"executioner": "^2.0.1"
}
},
- "internal-slot": {
- "version": "1.0.3",
- "dev": true,
- "requires": {
- "get-intrinsic": "^1.1.0",
- "has": "^1.0.3",
- "side-channel": "^1.0.4"
- }
- },
"is": {
"version": "0.2.7",
"dev": true
@@ -13887,21 +16161,6 @@
"version": "0.2.1",
"dev": true
},
- "is-bigint": {
- "version": "1.0.4",
- "dev": true,
- "requires": {
- "has-bigints": "^1.0.1"
- }
- },
- "is-boolean-object": {
- "version": "1.1.2",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- }
- },
"is-builtin-module": {
"version": "3.1.0",
"dev": true,
@@ -13909,10 +16168,6 @@
"builtin-modules": "^3.0.0"
}
},
- "is-callable": {
- "version": "1.2.4",
- "dev": true
- },
"is-ci": {
"version": "2.0.0",
"dev": true,
@@ -13922,18 +16177,10 @@
},
"is-core-module": {
"version": "2.8.0",
- "dev": true,
"requires": {
"has": "^1.0.3"
}
},
- "is-date-object": {
- "version": "1.0.5",
- "dev": true,
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
"is-extglob": {
"version": "2.1.1",
"dev": true
@@ -13972,10 +16219,6 @@
"version": "1.0.0",
"dev": true
},
- "is-negative-zero": {
- "version": "2.0.1",
- "dev": true
- },
"is-npm": {
"version": "5.0.0",
"dev": true
@@ -13984,13 +16227,6 @@
"version": "7.0.0",
"dev": true
},
- "is-number-object": {
- "version": "1.0.6",
- "dev": true,
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
"is-obj": {
"version": "2.0.0",
"dev": true
@@ -14020,47 +16256,14 @@
"@types/estree": "*"
}
},
- "is-regex": {
- "version": "1.1.4",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-shared-array-buffer": {
- "version": "1.0.1",
- "dev": true
- },
"is-stream": {
"version": "2.0.1",
"dev": true
},
- "is-string": {
- "version": "1.0.7",
- "dev": true,
- "requires": {
- "has-tostringtag": "^1.0.0"
- }
- },
- "is-symbol": {
- "version": "1.0.4",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.2"
- }
- },
"is-typedarray": {
"version": "1.0.0",
"dev": true
},
- "is-weakref": {
- "version": "1.0.1",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.0"
- }
- },
"is-yarn-global": {
"version": "0.3.0",
"dev": true
@@ -14774,6 +16977,8 @@
},
"json-schema-traverse": {
"version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"json-stable-stringify-without-jsonify": {
@@ -14856,14 +17061,6 @@
"jss": "10.8.1"
}
},
- "jsx-ast-utils": {
- "version": "3.2.1",
- "dev": true,
- "requires": {
- "array-includes": "^3.1.3",
- "object.assign": "^4.1.2"
- }
- },
"keyv": {
"version": "3.1.0",
"dev": true,
@@ -14882,6 +17079,21 @@
"dev": true,
"peer": true
},
+ "language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "language-tags": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.8.tgz",
+ "integrity": "sha512-aWAZwgPLS8hJ20lNPm9HNVs4inexz6S2sQa3wx/+ycuutMNE5/IfYxiWYBbi+9UWCQVaXYCOPUl6gFrPR7+jGg==",
+ "dev": true,
+ "requires": {
+ "language-subtag-registry": "^0.3.20"
+ }
+ },
"latest-version": {
"version": "5.1.0",
"dev": true,
@@ -15044,33 +17256,48 @@
},
"levn": {
"version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"requires": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
}
},
+ "lilconfig": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
+ "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="
+ },
"lines-and-columns": {
"version": "1.1.6",
"dev": true
},
+ "loader-utils": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
+ "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw=="
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
"lodash": {
"version": "4.17.21",
"dev": true
},
"lodash.camelcase": {
- "version": "4.3.0",
- "dev": true
- },
- "lodash.clonedeep": {
- "version": "4.5.0",
- "dev": true
+ "version": "4.3.0"
},
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
- "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
- "dev": true
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="
},
"lodash.merge": {
"version": "4.6.2",
@@ -15078,8 +17305,15 @@
},
"lodash.truncate": {
"version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
"dev": true
},
+ "lodash.uniq": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
+ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="
+ },
"long": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
@@ -15129,7 +17363,7 @@
},
"make-error": {
"version": "1.3.6",
- "dev": true
+ "devOptional": true
},
"makeerror": {
"version": "1.0.12",
@@ -15158,6 +17392,11 @@
"safe-buffer": "^5.1.2"
}
},
+ "mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
+ },
"meow": {
"version": "9.0.0",
"dev": true,
@@ -15188,6 +17427,8 @@
},
"merge2": {
"version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true
},
"micromatch": {
@@ -15246,7 +17487,9 @@
"dev": true
},
"minimatch": {
- "version": "3.0.4",
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
@@ -15302,6 +17545,11 @@
"version": "0.0.8",
"dev": true
},
+ "nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
+ },
"natural-compare": {
"version": "1.4.0",
"dev": true
@@ -15329,11 +17577,9 @@
"peer": true
},
"node-releases": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
- "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
- "dev": true,
- "peer": true
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz",
+ "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ=="
},
"nopt": {
"version": "5.0.0",
@@ -15378,6 +17624,14 @@
"set-blocking": "^2.0.0"
}
},
+ "nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "requires": {
+ "boolbase": "^1.0.0"
+ }
+ },
"nwsapi": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz",
@@ -15392,60 +17646,6 @@
"version": "1.11.0",
"dev": true
},
- "object-keys": {
- "version": "1.1.1",
- "dev": true
- },
- "object-path": {
- "version": "0.11.8",
- "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
- "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA=="
- },
- "object.assign": {
- "version": "4.1.2",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.0",
- "define-properties": "^1.1.3",
- "has-symbols": "^1.0.1",
- "object-keys": "^1.1.1"
- }
- },
- "object.entries": {
- "version": "1.1.5",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- }
- },
- "object.fromentries": {
- "version": "2.0.5",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- }
- },
- "object.hasown": {
- "version": "1.1.0",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- }
- },
- "object.values": {
- "version": "1.1.5",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1"
- }
- },
"octal": {
"version": "1.0.0",
"dev": true
@@ -15469,15 +17669,17 @@
"dev": true
},
"optionator": {
- "version": "0.9.1",
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
"dev": true,
"requires": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.3"
+ "type-check": "^0.4.0"
}
},
"os-tmpdir": {
@@ -15488,6 +17690,46 @@
"version": "1.1.0",
"dev": true
},
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow=="
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "p-queue": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
+ "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
+ "requires": {
+ "eventemitter3": "^4.0.4",
+ "p-timeout": "^3.2.0"
+ }
+ },
+ "p-timeout": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
+ "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
+ "requires": {
+ "p-finally": "^1.0.0"
+ }
+ },
"p-try": {
"version": "2.2.0",
"dev": true
@@ -15510,6 +17752,8 @@
},
"parent-module": {
"version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"requires": {
"callsites": "^3.0.0"
@@ -15551,6 +17795,15 @@
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
},
+ "parse5-htmlparser2-tree-adapter": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
+ "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
+ "dev": true,
+ "requires": {
+ "parse5": "^6.0.1"
+ }
+ },
"path-exists": {
"version": "4.0.0",
"dev": true
@@ -15564,11 +17817,12 @@
"dev": true
},
"path-parse": {
- "version": "1.0.7",
- "dev": true
+ "version": "1.0.7"
},
"path-type": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true
},
"pause-stream": {
@@ -15592,14 +17846,17 @@
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
- "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
- "dev": true,
- "peer": true
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
"picomatch": {
"version": "2.3.0",
"dev": true
},
+ "pify": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz",
+ "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA=="
+ },
"pirates": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
@@ -15686,8 +17943,321 @@
}
}
},
+ "postcss": {
+ "version": "8.4.24",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+ "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+ "requires": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "postcss-calc": {
+ "version": "8.2.4",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz",
+ "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==",
+ "requires": {
+ "postcss-selector-parser": "^6.0.9",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-colormin": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz",
+ "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0",
+ "colord": "^2.9.1",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-convert-values": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz",
+ "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-discard-comments": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz",
+ "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==",
+ "requires": {}
+ },
+ "postcss-discard-duplicates": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz",
+ "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==",
+ "requires": {}
+ },
+ "postcss-discard-empty": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz",
+ "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==",
+ "requires": {}
+ },
+ "postcss-discard-overridden": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz",
+ "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==",
+ "requires": {}
+ },
+ "postcss-load-config": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz",
+ "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==",
+ "requires": {
+ "lilconfig": "^2.0.5",
+ "yaml": "^1.10.2"
+ }
+ },
+ "postcss-merge-longhand": {
+ "version": "5.1.7",
+ "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz",
+ "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0",
+ "stylehacks": "^5.1.1"
+ }
+ },
+ "postcss-merge-rules": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz",
+ "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0",
+ "cssnano-utils": "^3.1.0",
+ "postcss-selector-parser": "^6.0.5"
+ }
+ },
+ "postcss-minify-font-values": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz",
+ "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-minify-gradients": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz",
+ "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==",
+ "requires": {
+ "colord": "^2.9.1",
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-minify-params": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz",
+ "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-minify-selectors": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz",
+ "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==",
+ "requires": {
+ "postcss-selector-parser": "^6.0.5"
+ }
+ },
+ "postcss-modules": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/postcss-modules/-/postcss-modules-4.3.1.tgz",
+ "integrity": "sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q==",
+ "requires": {
+ "generic-names": "^4.0.0",
+ "icss-replace-symbols": "^1.1.0",
+ "lodash.camelcase": "^4.3.0",
+ "postcss-modules-extract-imports": "^3.0.0",
+ "postcss-modules-local-by-default": "^4.0.0",
+ "postcss-modules-scope": "^3.0.0",
+ "postcss-modules-values": "^4.0.0",
+ "string-hash": "^1.1.1"
+ }
+ },
+ "postcss-modules-extract-imports": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
+ "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
+ "requires": {}
+ },
+ "postcss-modules-local-by-default": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz",
+ "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==",
+ "requires": {
+ "icss-utils": "^5.0.0",
+ "postcss-selector-parser": "^6.0.2",
+ "postcss-value-parser": "^4.1.0"
+ }
+ },
+ "postcss-modules-scope": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz",
+ "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==",
+ "requires": {
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
+ "postcss-modules-values": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+ "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+ "requires": {
+ "icss-utils": "^5.0.0"
+ }
+ },
+ "postcss-normalize-charset": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz",
+ "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==",
+ "requires": {}
+ },
+ "postcss-normalize-display-values": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz",
+ "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-normalize-positions": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz",
+ "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-normalize-repeat-style": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz",
+ "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-normalize-string": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz",
+ "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-normalize-timing-functions": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz",
+ "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-normalize-unicode": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz",
+ "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-normalize-url": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz",
+ "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==",
+ "requires": {
+ "normalize-url": "^6.0.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "dependencies": {
+ "normalize-url": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="
+ }
+ }
+ },
+ "postcss-normalize-whitespace": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz",
+ "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-ordered-values": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz",
+ "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==",
+ "requires": {
+ "cssnano-utils": "^3.1.0",
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-reduce-initial": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz",
+ "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "caniuse-api": "^3.0.0"
+ }
+ },
+ "postcss-reduce-transforms": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz",
+ "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0"
+ }
+ },
+ "postcss-selector-parser": {
+ "version": "6.0.13",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz",
+ "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==",
+ "requires": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ }
+ },
+ "postcss-svgo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz",
+ "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==",
+ "requires": {
+ "postcss-value-parser": "^4.2.0",
+ "svgo": "^2.7.0"
+ }
+ },
+ "postcss-unique-selectors": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz",
+ "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==",
+ "requires": {
+ "postcss-selector-parser": "^6.0.5"
+ }
+ },
+ "postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
+ },
"prelude-ls": {
"version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
"prepend-http": {
@@ -15695,8 +18265,9 @@
"dev": true
},
"prettier": {
- "version": "2.4.1",
- "dev": true
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.1.tgz",
+ "integrity": "sha512-fcOWSnnpCrovBsmFZIGIy9UqK2FaI7Hqax+DIO0A9UxeVoY4iweyaFjS5TavZN97Hfehph0nhsZnjlVKzEQSrQ=="
},
"prettier-linter-helpers": {
"version": "1.0.0",
@@ -15741,8 +18312,15 @@
},
"progress": {
"version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
"dev": true
},
+ "promise.series": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/promise.series/-/promise.series-0.2.0.tgz",
+ "integrity": "sha512-VWQJyU2bcDTgZw8kpfBpB/ejZASlCrzwz5f2hjb/zlujOEB4oeiAhHygAWq8ubsX2GVkD4kCU5V2dwOTaCY5EQ=="
+ },
"prompts": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
@@ -15819,6 +18397,8 @@
},
"queue-microtask": {
"version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true
},
"quick-lru": {
@@ -15995,14 +18575,6 @@
"version": "0.13.9",
"dev": true
},
- "regexp.prototype.flags": {
- "version": "1.3.1",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- }
- },
"regexpp": {
"version": "3.2.0",
"dev": true
@@ -16027,6 +18599,14 @@
},
"require-from-string": {
"version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true
+ },
+ "requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
"dev": true
},
"requirejs": {
@@ -16039,7 +18619,6 @@
},
"resolve": {
"version": "1.20.0",
- "dev": true,
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
@@ -16066,6 +18645,8 @@
},
"resolve-from": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true
},
"resolve.exports": {
@@ -16092,6 +18673,8 @@
},
"reusify": {
"version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true
},
"rimraf": {
@@ -16116,31 +18699,6 @@
"fsevents": "~2.3.2"
}
},
- "rollup-plugin-dts": {
- "version": "4.2.2",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.16.7",
- "magic-string": "^0.26.1"
- },
- "dependencies": {
- "@babel/code-frame": {
- "version": "7.16.7",
- "dev": true,
- "optional": true,
- "requires": {
- "@babel/highlight": "^7.16.7"
- }
- },
- "magic-string": {
- "version": "0.26.2",
- "dev": true,
- "requires": {
- "sourcemap-codec": "^1.4.8"
- }
- }
- }
- },
"rollup-plugin-inject": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
@@ -16206,6 +18764,26 @@
"rollup-plugin-inject": "^3.0.0"
}
},
+ "rollup-plugin-postcss": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz",
+ "integrity": "sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w==",
+ "requires": {
+ "chalk": "^4.1.0",
+ "concat-with-sourcemaps": "^1.1.0",
+ "cssnano": "^5.0.1",
+ "import-cwd": "^3.0.0",
+ "p-queue": "^6.6.2",
+ "pify": "^5.0.0",
+ "postcss-load-config": "^3.0.0",
+ "postcss-modules": "^4.0.0",
+ "promise.series": "^0.2.0",
+ "resolve": "^1.19.0",
+ "rollup-pluginutils": "^2.8.2",
+ "safe-identifier": "^0.4.2",
+ "style-inject": "^0.3.0"
+ }
+ },
"rollup-plugin-sourcemaps": {
"version": "0.6.3",
"dev": true,
@@ -16216,14 +18794,12 @@
},
"rollup-pluginutils": {
"version": "2.8.2",
- "dev": true,
"requires": {
"estree-walker": "^0.6.1"
},
"dependencies": {
"estree-walker": {
- "version": "0.6.1",
- "dev": true
+ "version": "0.6.1"
}
}
},
@@ -16233,6 +18809,8 @@
},
"run-parallel": {
"version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"requires": {
"queue-microtask": "^1.2.2"
@@ -16255,6 +18833,11 @@
"version": "5.2.1",
"dev": true
},
+ "safe-identifier": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
+ "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w=="
+ },
"safer-buffer": {
"version": "2.1.2",
"dev": true
@@ -16281,7 +18864,9 @@
"dev": true
},
"semver": {
- "version": "7.3.5",
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
@@ -16351,6 +18936,8 @@
},
"slice-ansi": {
"version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
@@ -16359,8 +18946,12 @@
}
},
"source-map": {
- "version": "0.6.1",
- "dev": true
+ "version": "0.6.1"
+ },
+ "source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
},
"source-map-resolve": {
"version": "0.6.0",
@@ -16409,6 +19000,11 @@
"version": "1.0.3",
"dev": true
},
+ "stable": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
+ "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="
+ },
"stack-utils": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
@@ -16441,6 +19037,11 @@
"version": "0.1.2",
"dev": true
},
+ "string-hash": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
+ "integrity": "sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A=="
+ },
"string-length": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@@ -16465,36 +19066,6 @@
"strip-ansi": "^6.0.1"
}
},
- "string.prototype.matchall": {
- "version": "4.0.6",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3",
- "es-abstract": "^1.19.1",
- "get-intrinsic": "^1.1.1",
- "has-symbols": "^1.0.2",
- "internal-slot": "^1.0.3",
- "regexp.prototype.flags": "^1.3.1",
- "side-channel": "^1.0.4"
- }
- },
- "string.prototype.trimend": {
- "version": "1.0.4",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- }
- },
- "string.prototype.trimstart": {
- "version": "1.0.4",
- "dev": true,
- "requires": {
- "call-bind": "^1.0.2",
- "define-properties": "^1.1.3"
- }
- },
"strip-ansi": {
"version": "6.0.1",
"dev": true,
@@ -16524,6 +19095,11 @@
"version": "3.1.1",
"dev": true
},
+ "style-inject": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/style-inject/-/style-inject-0.3.0.tgz",
+ "integrity": "sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw=="
+ },
"style-to-js": {
"version": "1.1.0",
"dev": true,
@@ -16538,9 +19114,17 @@
"inline-style-parser": "0.1.1"
}
},
+ "stylehacks": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz",
+ "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==",
+ "requires": {
+ "browserslist": "^4.21.4",
+ "postcss-selector-parser": "^6.0.4"
+ }
+ },
"supports-color": {
"version": "7.2.0",
- "dev": true,
"requires": {
"has-flag": "^4.0.0"
}
@@ -16556,6 +19140,20 @@
"supports-color": "^7.0.0"
}
},
+ "svgo": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz",
+ "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==",
+ "requires": {
+ "@trysound/sax": "0.2.0",
+ "commander": "^7.2.0",
+ "css-select": "^4.1.3",
+ "css-tree": "^1.1.3",
+ "csso": "^4.2.0",
+ "picocolors": "^1.0.0",
+ "stable": "^0.1.8"
+ }
+ },
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -16563,11 +19161,12 @@
"dev": true
},
"table": {
- "version": "6.7.2",
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
"dev": true,
"requires": {
"ajv": "^8.0.1",
- "lodash.clonedeep": "^4.5.0",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
@@ -16575,7 +19174,9 @@
},
"dependencies": {
"ajv": {
- "version": "8.6.3",
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@@ -16586,6 +19187,8 @@
},
"json-schema-traverse": {
"version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
}
}
@@ -16699,6 +19302,13 @@
"version": "3.0.1",
"dev": true
},
+ "ts-api-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz",
+ "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==",
+ "dev": true,
+ "requires": {}
+ },
"ts-jest": {
"version": "28.0.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.5.tgz",
@@ -16725,7 +19335,7 @@
},
"ts-node": {
"version": "10.8.1",
- "dev": true,
+ "devOptional": true,
"requires": {
"@cspotcode/source-map-support": "^0.8.0",
"@tsconfig/node10": "^1.0.7",
@@ -16744,11 +19354,11 @@
"dependencies": {
"acorn": {
"version": "8.7.1",
- "dev": true
+ "devOptional": true
},
"arg": {
"version": "4.1.3",
- "dev": true
+ "devOptional": true
}
}
},
@@ -16775,6 +19385,8 @@
},
"tsutils": {
"version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
@@ -16782,12 +19394,16 @@
"dependencies": {
"tslib": {
"version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
}
}
},
"type-check": {
"version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"requires": {
"prelude-ls": "^1.2.1"
@@ -16816,17 +19432,7 @@
},
"typescript": {
"version": "4.4.4",
- "dev": true
- },
- "unbox-primitive": {
- "version": "1.0.1",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.1",
- "has-bigints": "^1.0.1",
- "has-symbols": "^1.0.2",
- "which-boxed-primitive": "^1.0.2"
- }
+ "devOptional": true
},
"union": {
"version": "0.5.0",
@@ -16858,11 +19464,9 @@
"dev": true
},
"update-browserslist-db": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz",
- "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==",
- "dev": true,
- "peer": true,
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
"requires": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
@@ -16890,6 +19494,8 @@
},
"uri-js": {
"version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"requires": {
"punycode": "^2.1.0"
@@ -16907,16 +19513,17 @@
}
},
"util-deprecate": {
- "version": "1.0.2",
- "dev": true
+ "version": "1.0.2"
},
"v8-compile-cache": {
"version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"v8-compile-cache-lib": {
"version": "3.0.1",
- "dev": true
+ "devOptional": true
},
"validate-npm-package-license": {
"version": "3.0.4",
@@ -17003,17 +19610,6 @@
"isexe": "^2.0.0"
}
},
- "which-boxed-primitive": {
- "version": "1.0.2",
- "dev": true,
- "requires": {
- "is-bigint": "^1.0.1",
- "is-boolean-object": "^1.1.0",
- "is-number-object": "^1.0.4",
- "is-string": "^1.0.5",
- "is-symbol": "^1.0.3"
- }
- },
"wide-align": {
"version": "1.1.5",
"dev": true,
@@ -17090,6 +19686,11 @@
"version": "4.0.0",
"dev": true
},
+ "yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
+ },
"yargs": {
"version": "16.2.0",
"dev": true,
@@ -17109,6 +19710,12 @@
},
"yn": {
"version": "3.1.1",
+ "devOptional": true
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
}
}
diff --git a/package.json b/package.json
index b3eebb567..a856970be 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "pigweedjs",
- "version": "0.0.8",
+ "version": "0.0.13",
"description": "An open source collection of embedded-targeted libraries",
"author": "The Pigweed Authors",
"license": "Apache-2.0",
@@ -8,7 +8,8 @@
"exports": {
".": "./dist/index.mjs",
"./protos": "./dist/protos/collection.umd.js",
- "./protos/*": "./dist/protos/*"
+ "./protos/*": "./dist/protos/*",
+ "./logging": "./dist/logging.mjs"
},
"bin": {
"pw_protobuf_compiler": "./dist/bin/pw_protobuf_compiler.js"
@@ -39,14 +40,13 @@
"@types/node": "^16.0.1",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
+ "@typescript-eslint/eslint-plugin": "^6.4.0",
"ansi_up": "^5.1.0",
"arg": "^5.0.2",
"base64-js": "^1.5.1",
- "buffer": "^6.0.3",
- "crc": "^4.1.1",
"debug": "^4.3.2",
- "eslint": "^7.30.0",
- "eslint-plugin-react": "^7.24.0",
+ "eslint": "^8.47.0",
+ "eslint-plugin-lit-a11y": "^4.1.0",
"grpc-tools": "^1.11.2",
"grpc-web": "^1.2.1",
"gts": "^3.1.0",
@@ -59,7 +59,6 @@
"requirejs": "^2.3.6",
"rimraf": "^3.0.2",
"rollup": "^2.52.8",
- "rollup-plugin-dts": "^4.2.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
"rollup-plugin-node-polyfills": "^0.2.1",
@@ -73,10 +72,13 @@
"typescript": "^4.3.5"
},
"dependencies": {
+ "buffer": "^6.0.3",
"@protobuf-ts/protoc": "^2.7.0",
"google-protobuf": "^3.17.3",
"long": "^5.2.1",
- "object-path": "^0.11.8",
+ "postcss": "^8.4.24",
+ "prettier": "^3.0.1",
+ "rollup-plugin-postcss": "^4.0.2",
"ts-protoc-gen": "^0.15.0"
},
"config": {
diff --git a/pigweed.json b/pigweed.json
index 7a4623e5d..9222ad2a1 100644
--- a/pigweed.json
+++ b/pigweed.json
@@ -2,8 +2,102 @@
"pw": {
"pw_presubmit": {
"format": {
- "python_formatter": "black"
+ "python_formatter": "black",
+ "black_config_file": "$pw_env{PW_ROOT}/.black.toml",
+ "exclude": [
+ "\\bthird_party/fuchsia/repo",
+ "\\bthird_party/.*\\.json$",
+ "\\bpackage\\.lock$",
+ "\\bpw_web/log-viewer/package(-lock)?\\.json",
+ "^patches\\.json$"
+ ]
}
+ },
+ "pw_cli": {
+ "plugins": {
+ "build": {
+ "module": "pw_build.pigweed_upstream_build",
+ "function": "pigweed_upstream_main"
+ },
+ "console": {
+ "module": "pw_system.console",
+ "function": "main"
+ },
+ "emu": {
+ "module": "pw_emu.__main__",
+ "function": "main"
+ },
+ "format": {
+ "module": "pw_presubmit.format_code",
+ "function": "main"
+ },
+ "heap-viewer": {
+ "module": "pw_allocator.heap_viewer",
+ "function": "main"
+ },
+ "ide": {
+ "module": "pw_ide.__main__",
+ "function": "main"
+ },
+ "package": {
+ "module": "pw_package.pigweed_packages",
+ "function": "main"
+ },
+ "presubmit": {
+ "module": "pw_presubmit.pigweed_presubmit",
+ "function": "main"
+ },
+ "requires": {
+ "module": "pw_cli.requires",
+ "function": "main"
+ },
+ "rpc": {
+ "module": "pw_hdlc.rpc_console",
+ "function": "main"
+ },
+ "update": {
+ "module": "pw_software_update.cli",
+ "function": "main"
+ }
+ }
+ },
+ "pw_env_setup": {
+ "root_variable": "PW_ROOT",
+ "relative_pigweed_root": ".",
+ "rosetta": "allow",
+ "gni_file": "build_overrides/pigweed_environment.gni",
+ "cipd_package_files": [
+ "pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json"
+ ],
+ "virtualenv": {
+ "gn_targets": [
+ "python.install"
+ ],
+ "gn_root": ".",
+ "requirements": [
+ "pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt"
+ ],
+ "constraints": [
+ "pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list"
+ ],
+ "pip_install_find_links": [
+ "${PW_MSRV_PYTHON_CIPD_INSTALL_DIR}/pip_cache"
+ ]
+ }
+ },
+ "pw_package": {
+ "allow_middleware_only_packages": true
+ },
+ "pw_doctor": {
+ "new_bug_url": "https://issues.pigweed.dev/new"
+ },
+ "pw_emu": {
+ "target_files": [
+ "pw_emu/qemu-lm3s6965evb.json",
+ "pw_emu/qemu-stm32vldiscovery.json",
+ "pw_emu/qemu-netduinoplus2.json",
+ "pw_emu/renode-stm32f4_discovery.json"
+ ]
}
}
}
diff --git a/pw_alignment/docs.rst b/pw_alignment/docs.rst
index 8d4958db1..2f1e177b3 100644
--- a/pw_alignment/docs.rst
+++ b/pw_alignment/docs.rst
@@ -12,20 +12,21 @@ preventing the compiler from emitting libcalls to builtin atomics
functions and instead use native atomic instructions. This is especially
useful for any pigweed user that uses ``std::atomic``.
-Take for example `std::atomic<std::optional<bool>>`. Accessing the underlying object
-would normally involve a call to a builtin `__atomic_*` function provided by a builtins
+Take for example ``std::atomic<std::optional<bool>>``. Accessing the underlying object
+would normally involve a call to a builtin ``__atomic_*`` function provided by a builtins
library. However, if the compiler can determine that the size of the object is the same
-as its alignment, then it can replace a libcall to `__atomic_*` with native instructions.
+as its alignment, then it can replace a libcall to ``__atomic_*`` with native instructions.
There can be certain situations where a compiler might not be able to assert this.
-Depending on the implementation of `std::optional<bool>`, this object could
+Depending on the implementation of ``std::optional<bool>``, this object could
have a size of 2 bytes but an alignment of 1 byte which wouldn't satisfy the constraint.
Basic usage
-----------
-`pw_alignment` provides a wrapper class `pw::NaturallyAligned` for enforcing natural alignment without any
+``pw_alignment`` provides a wrapper class ``pw::NaturallyAligned`` for enforcing
+natural alignment without any
changes to how the object is used. Since this is commonly used with atomics, an
-aditional class `pw::AlignedAtomic` is provided for simplifying things.
+aditional class ``pw::AlignedAtomic`` is provided for simplifying things.
.. code-block:: c++
@@ -39,4 +40,4 @@ aditional class `pw::AlignedAtomic` is provided for simplifying things.
std::atomic<pw::NaturallyAligned<std::optional<bool>>> nat_aligned_obj;
// Shorter spelling for the same as above.
- std::AlignedAtomic<std::optional<bool>> also_nat_aligned_obj;
+ pw::AlignedAtomic<std::optional<bool>> also_nat_aligned_obj;
diff --git a/pw_alignment/public/pw_alignment/alignment.h b/pw_alignment/public/pw_alignment/alignment.h
index e295c5d0c..7c173bdb0 100644
--- a/pw_alignment/public/pw_alignment/alignment.h
+++ b/pw_alignment/public/pw_alignment/alignment.h
@@ -56,14 +56,15 @@ constexpr size_t bit_ceil(size_t x) noexcept {
// The NaturallyAligned class is a wrapper class for ensuring the object is
// aligned to a power of 2 bytes greater than or equal to its size.
template <typename T>
-struct [[gnu::aligned(bit_ceil(sizeof(T)))]] NaturallyAligned
- : public T{NaturallyAligned() : T(){} NaturallyAligned(const T& t) :
- T(t){} template <class U>
- NaturallyAligned(const U& u) : T(u){} NaturallyAligned
- operator=(T other){return T::operator=(other);
-} // namespace pw
-}
-;
+struct [[gnu::aligned(bit_ceil(sizeof(T)))]] NaturallyAligned : public T {
+ NaturallyAligned() : T() {}
+ NaturallyAligned(const T& t) : T(t) {}
+ template <class U>
+ NaturallyAligned(const U& u) : T(u) {}
+ NaturallyAligned operator=(T other) {
+ return T::operator=(other);
+ } // namespace pw
+};
// This is a convenience wrapper for ensuring the object held by std::atomic is
// naturally aligned. Ensuring the underlying objects's alignment is natural
diff --git a/pw_allocator/Android.bp b/pw_allocator/Android.bp
new file mode 100644
index 000000000..a5aeca728
--- /dev/null
+++ b/pw_allocator/Android.bp
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library {
+ name: "pw_allocator",
+ cpp_std: "c++20",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ ],
+ export_static_lib_headers: [
+ "pw_metric",
+ "pw_status",
+ ],
+ host_supported: true,
+ srcs: [
+ "allocator.cc",
+ "allocator_metric_proxy.cc",
+ "fallback_allocator.cc",
+ "libc_allocator.cc",
+ "split_free_list_allocator.cc",
+ ],
+ static_libs: [
+ "pw_base64",
+ "pw_bytes",
+ "pw_containers",
+ "pw_metric",
+ "pw_status",
+ "pw_tokenizer",
+ "pw_tokenizer_base64",
+ ],
+}
diff --git a/pw_allocator/BUILD.bazel b/pw_allocator/BUILD.bazel
index 71f255ccc..c7c7d9ae3 100644
--- a/pw_allocator/BUILD.bazel
+++ b/pw_allocator/BUILD.bazel
@@ -23,18 +23,68 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "block",
+ name = "allocator",
+ srcs = [
+ "allocator.cc",
+ ],
+ hdrs = [
+ "public/pw_allocator/allocator.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "allocator_metric_proxy",
srcs = [
- "block.cc",
+ "allocator_metric_proxy.cc",
],
hdrs = [
+ "public/pw_allocator/allocator_metric_proxy.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ "//pw_assert",
+ "//pw_metric:metric",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "block",
+ srcs = ["block.cc"],
+ hdrs = [
"public/pw_allocator/block.h",
],
includes = ["public"],
deps = [
"//pw_assert",
+ "//pw_bytes",
+ "//pw_result",
"//pw_span",
"//pw_status",
+ "//third_party/fuchsia:stdcompat",
+ ],
+)
+
+pw_cc_library(
+ name = "fallback_allocator",
+ srcs = [
+ "fallback_allocator.cc",
+ ],
+ hdrs = [
+ "public/pw_allocator/fallback_allocator.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ "//pw_assert",
+ "//pw_status",
],
)
@@ -70,6 +120,110 @@ pw_cc_library(
],
)
+pw_cc_library(
+ name = "libc_allocator",
+ srcs = [
+ "libc_allocator.cc",
+ ],
+ hdrs = [
+ "public/pw_allocator/libc_allocator.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "null_allocator",
+ hdrs = [
+ "public/pw_allocator/null_allocator.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ ],
+)
+
+pw_cc_library(
+ name = "simple_allocator",
+ hdrs = [
+ "public/pw_allocator/simple_allocator.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ ":block",
+ "//pw_bytes",
+ ],
+)
+
+pw_cc_library(
+ name = "split_free_list_allocator",
+ srcs = [
+ "split_free_list_allocator.cc",
+ ],
+ hdrs = [
+ "public/pw_allocator/split_free_list_allocator.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ ":block",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_result",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "allocator_testing",
+ srcs = [
+ "allocator_testing.cc",
+ ],
+ hdrs = [
+ "public/pw_allocator/allocator_testing.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":allocator",
+ ":block",
+ ":simple_allocator",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "allocator_test",
+ srcs = [
+ "allocator_test.cc",
+ ],
+ deps = [
+ ":allocator",
+ ":allocator_testing",
+ "//pw_bytes",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "allocator_metric_proxy_test",
+ srcs = [
+ "allocator_metric_proxy_test.cc",
+ ],
+ deps = [
+ ":allocator_metric_proxy",
+ ":allocator_testing",
+ "//pw_unit_test",
+ ],
+)
+
pw_cc_test(
name = "block_test",
srcs = [
@@ -83,6 +237,19 @@ pw_cc_test(
)
pw_cc_test(
+ name = "fallback_allocator_test",
+ srcs = [
+ "fallback_allocator_test.cc",
+ ],
+ deps = [
+ ":allocator_testing",
+ ":fallback_allocator",
+ "//pw_status",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "freelist_test",
srcs = [
"freelist_test.cc",
@@ -104,3 +271,62 @@ pw_cc_test(
":freelist_heap",
],
)
+
+pw_cc_test(
+ name = "libc_allocator_test",
+ srcs = [
+ "libc_allocator_test.cc",
+ ],
+ deps = [
+ ":libc_allocator",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "null_allocator_test",
+ srcs = [
+ "null_allocator_test.cc",
+ ],
+ deps = [
+ ":null_allocator",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "simple_allocator_test",
+ srcs = [
+ "simple_allocator_test.cc",
+ ],
+ deps = [
+ ":allocator_testing",
+ ":simple_allocator",
+ ],
+)
+
+pw_cc_test(
+ name = "split_free_list_allocator_test",
+ srcs = [
+ "split_free_list_allocator_test.cc",
+ ],
+ deps = [
+ ":allocator_testing",
+ ":split_free_list_allocator",
+ "//pw_bytes",
+ "//pw_containers:vector",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "unique_ptr_test",
+ srcs = [
+ "unique_ptr_test.cc",
+ ],
+ deps = [
+ ":allocator",
+ ":allocator_testing",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_allocator/BUILD.gn b/pw_allocator/BUILD.gn
index f335199a4..e7039f0a1 100644
--- a/pw_allocator/BUILD.gn
+++ b/pw_allocator/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
@@ -34,24 +35,62 @@ config("enable_heap_poison") {
group("pw_allocator") {
public_deps = [
+ ":allocator",
":block",
":freelist",
":freelist_heap",
]
}
+pw_source_set("allocator") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/allocator.h" ]
+ public_deps = [ dir_pw_status ]
+ deps = [
+ dir_pw_assert,
+ dir_pw_bytes,
+ ]
+ sources = [ "allocator.cc" ]
+}
+
+pw_source_set("allocator_metric_proxy") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/allocator_metric_proxy.h" ]
+ public_deps = [
+ ":allocator",
+ dir_pw_metric,
+ dir_pw_status,
+ ]
+ deps = [ dir_pw_assert ]
+ sources = [ "allocator_metric_proxy.cc" ]
+}
+
pw_source_set("block") {
public_configs = [ ":default_config" ]
configs = [ ":enable_heap_poison" ]
public = [ "public/pw_allocator/block.h" ]
public_deps = [
- dir_pw_assert,
+ "$dir_pw_third_party/fuchsia:stdcompat",
+ dir_pw_bytes,
+ dir_pw_result,
dir_pw_span,
dir_pw_status,
]
+ deps = [ dir_pw_assert ]
sources = [ "block.cc" ]
}
+pw_source_set("fallback_allocator") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/fallback_allocator.h" ]
+ public_deps = [
+ ":allocator",
+ dir_pw_assert,
+ dir_pw_status,
+ ]
+ sources = [ "fallback_allocator.cc" ]
+}
+
pw_source_set("freelist") {
public_configs = [ ":default_config" ]
configs = [ ":enable_heap_poison" ]
@@ -80,12 +119,112 @@ pw_source_set("freelist_heap") {
sources = [ "freelist_heap.cc" ]
}
+pw_source_set("libc_allocator") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/libc_allocator.h" ]
+ public_deps = [ ":allocator" ]
+ deps = [
+ dir_pw_assert,
+ dir_pw_bytes,
+ ]
+ sources = [ "libc_allocator.cc" ]
+}
+
+pw_source_set("null_allocator") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/null_allocator.h" ]
+ public_deps = [ ":allocator" ]
+}
+
+pw_source_set("simple_allocator") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/simple_allocator.h" ]
+ public_deps = [
+ ":allocator",
+ ":block",
+ dir_pw_bytes,
+ ]
+}
+
+pw_source_set("split_free_list_allocator") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_allocator/split_free_list_allocator.h" ]
+ public_deps = [
+ ":allocator",
+ ":block",
+ dir_pw_bytes,
+ dir_pw_result,
+ dir_pw_status,
+ ]
+ deps = [ dir_pw_assert ]
+ sources = [ "split_free_list_allocator.cc" ]
+}
+
+pw_size_diff("allocator_size_report") {
+ title = "Sizes of various pw_allocator implementations"
+ binaries = [
+ {
+ target = "size_report:split_free_list_allocator"
+ base = "$dir_pw_bloat:bloat_base"
+ label = "SplitFreeListAllocator"
+ },
+ {
+ target = "size_report:split_free_list_allocator_with_unique_ptr"
+ base = "size_report:split_free_list_allocator"
+ label = "Allocator::MakeUnique and UniquePtr"
+ },
+ {
+ target = "size_report:split_free_list_allocator_with_metric_proxy"
+ base = "size_report:split_free_list_allocator"
+ label = "AllocatorMetricProxy wrapping another allocator"
+ },
+ ]
+}
+
pw_test_group("tests") {
tests = [
+ ":allocator_test",
+ ":allocator_metric_proxy_test",
":block_test",
+ ":fallback_allocator_test",
":freelist_test",
":freelist_heap_test",
+ ":libc_allocator_test",
+ ":null_allocator_test",
+ ":simple_allocator_test",
+ ":split_free_list_allocator_test",
+ ":unique_ptr_test",
+ ]
+}
+
+pw_source_set("allocator_testing") {
+ public = [ "public/pw_allocator/allocator_testing.h" ]
+ public_deps = [
+ ":allocator",
+ ":block",
+ ":simple_allocator",
+ dir_pw_bytes,
+ dir_pw_unit_test,
+ ]
+ deps = [ dir_pw_assert ]
+ sources = [ "allocator_testing.cc" ]
+}
+
+pw_test("allocator_test") {
+ deps = [
+ ":allocator",
+ ":allocator_testing",
+ dir_pw_bytes,
+ ]
+ sources = [ "allocator_test.cc" ]
+}
+
+pw_test("allocator_metric_proxy_test") {
+ deps = [
+ ":allocator_metric_proxy",
+ ":allocator_testing",
]
+ sources = [ "allocator_metric_proxy_test.cc" ]
}
pw_test("block_test") {
@@ -97,6 +236,15 @@ pw_test("block_test") {
sources = [ "block_test.cc" ]
}
+pw_test("fallback_allocator_test") {
+ deps = [
+ ":allocator_testing",
+ ":fallback_allocator",
+ dir_pw_status,
+ ]
+ sources = [ "fallback_allocator_test.cc" ]
+}
+
pw_test("freelist_test") {
configs = [ ":enable_heap_poison" ]
deps = [
@@ -113,7 +261,46 @@ pw_test("freelist_heap_test") {
sources = [ "freelist_heap_test.cc" ]
}
+pw_test("libc_allocator_test") {
+ deps = [ ":libc_allocator" ]
+ sources = [ "libc_allocator_test.cc" ]
+}
+
+pw_test("null_allocator_test") {
+ deps = [ ":null_allocator" ]
+ sources = [ "null_allocator_test.cc" ]
+}
+
+pw_test("simple_allocator_test") {
+ configs = [ ":enable_heap_poison" ]
+ deps = [
+ ":allocator_testing",
+ ":simple_allocator",
+ ]
+ sources = [ "simple_allocator_test.cc" ]
+}
+
+pw_test("split_free_list_allocator_test") {
+ configs = [ ":enable_heap_poison" ]
+ deps = [
+ ":allocator_testing",
+ ":split_free_list_allocator",
+ "$dir_pw_containers:vector",
+ dir_pw_bytes,
+ ]
+ sources = [ "split_free_list_allocator_test.cc" ]
+}
+
+pw_test("unique_ptr_test") {
+ deps = [ ":allocator_testing" ]
+ sources = [ "unique_ptr_test.cc" ]
+}
+
pw_doc_group("docs") {
- inputs = [ "doc_resources/pw_allocator_heap_visualizer_demo.png" ]
+ inputs = [
+ "doc_resources/pw_allocator_heap_visualizer_demo.png",
+ "public/pw_allocator/simple_allocator.h",
+ ]
sources = [ "docs.rst" ]
+ report_deps = [ ":allocator_size_report" ]
}
diff --git a/pw_allocator/CMakeLists.txt b/pw_allocator/CMakeLists.txt
index 8a4d7b491..6f923edc7 100644
--- a/pw_allocator/CMakeLists.txt
+++ b/pw_allocator/CMakeLists.txt
@@ -14,19 +14,65 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+pw_add_library(pw_allocator.allocator STATIC
+ HEADERS
+ public/pw_allocator/allocator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_status
+ PRIVATE_DEPS
+ pw_assert
+ pw_bytes
+ SOURCES
+ allocator.cc
+)
+
+pw_add_library(pw_allocator.allocator_metric_proxy STATIC
+ HEADERS
+ public/pw_allocator/allocator_metric_proxy.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.allocator
+ pw_metric
+ pw_status
+ PRIVATE_DEPS
+ pw_assert
+ SOURCES
+ allocator_metric_proxy.cc
+)
+
pw_add_library(pw_allocator.block STATIC
HEADERS
public/pw_allocator/block.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_assert
+ pw_bytes
+ pw_result
pw_span
pw_status
+ pw_third_party.fuchsia.stdcompat
+ PRIVATE_DEPS
+ pw_assert
SOURCES
block.cc
)
+pw_add_library(pw_allocator.fallback_allocator STATIC
+ HEADERS
+ public/pw_allocator/fallback_allocator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.allocator
+ pw_assert
+ pw_status
+ SOURCES
+ fallback_allocator.cc
+)
+
pw_add_library(pw_allocator.freelist STATIC
HEADERS
public/pw_allocator/freelist.h
@@ -56,6 +102,96 @@ pw_add_library(pw_allocator.freelist_heap STATIC
freelist_heap.cc
)
+pw_add_library(pw_allocator.libc_allocator STATIC
+ SOURCES
+ libc_allocator.cc
+ HEADERS
+ public/pw_allocator/libc_allocator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.allocator
+ pw_status
+ PRIVATE_DEPS
+ pw_assert
+ pw_bytes
+)
+
+pw_add_library(pw_allocator.null_allocator INTERFACE
+ HEADERS
+ public/pw_allocator/null_allocator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.allocator
+)
+
+pw_add_library(pw_allocator.simple_allocator INTERFACE
+ HEADERS
+ public/pw_allocator/simple_allocator.h
+ PUBLIC_DEPS
+ pw_allocator.allocator
+ pw_allocator.block
+ pw_bytes
+)
+
+pw_add_library(pw_allocator.split_free_list_allocator STATIC
+ HEADERS
+ public/pw_allocator/split_free_list_allocator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.allocator
+ pw_allocator.block
+ pw_bytes
+ pw_result
+ pw_status
+ PRIVATE_DEPS
+ pw_assert
+ SOURCES
+ split_free_list_allocator.cc
+)
+
+pw_add_library(pw_allocator.allocator_testing STATIC
+ HEADERS
+ public/pw_allocator/allocator_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.allocator
+ pw_allocator.block
+ pw_allocator.simple_allocator
+ pw_bytes
+ PRIVATE_DEPS
+ pw_assert
+ pw_unit_test
+ SOURCES
+ allocator_testing.cc
+)
+
+pw_add_test(pw_allocator.allocator_test
+ SOURCES
+ allocator_test.cc
+ PRIVATE_DEPS
+ pw_allocator.allocator
+ pw_allocator.allocator_testing
+ pw_bytes
+ GROUPS
+ modules
+ pw_allocator
+)
+
+pw_add_test(pw_allocator.allocator_metric_proxy_test
+ SOURCES
+ allocator_metric_proxy_test.cc
+ PRIVATE_DEPS
+ pw_allocator.allocator_metric_proxy
+ pw_allocator.allocator_testing
+ GROUPS
+ modules
+ pw_allocator
+)
+
pw_add_test(pw_allocator.block_test
SOURCES
block_test.cc
@@ -67,6 +203,18 @@ pw_add_test(pw_allocator.block_test
pw_allocator
)
+pw_add_test(pw_allocator.fallback_allocator_test
+ PRIVATE_DEPS
+ pw_allocator.allocator_testing
+ pw_allocator.fallback_allocator
+ pw_status
+ SOURCES
+ fallback_allocator_test.cc
+ GROUPS
+ modules
+ pw_allocator
+)
+
pw_add_test(pw_allocator.freelist_test
SOURCES
freelist_test.cc
@@ -88,3 +236,58 @@ pw_add_test(pw_allocator.freelist_heap_test
modules
pw_allocator
)
+
+pw_add_test(pw_allocator.libc_allocator_test
+ SOURCES
+ libc_allocator_test.cc
+ PRIVATE_DEPS
+ pw_allocator.libc_allocator
+ pw_unit_test
+ GROUPS
+ modules
+ pw_allocator
+)
+
+pw_add_test(pw_allocator.null_allocator_test
+ SOURCES
+ null_allocator_test.cc
+ PRIVATE_DEPS
+ pw_allocator.null_allocator
+ pw_unit_test
+ GROUPS
+ modules
+ pw_allocator
+)
+
+pw_add_test(pw_allocator.simple_allocator_test
+ SOURCES
+ simple_allocator_test.cc
+ PRIVATE_DEPS
+ pw_allocator.allocator_testing
+ pw_allocator.simple_allocator
+)
+
+pw_add_test(pw_allocator.split_free_list_allocator_test
+ SOURCES
+ split_free_list_allocator_test.cc
+ PRIVATE_DEPS
+ pw_allocator.allocator_testing
+ pw_allocator.split_free_list_allocator
+ pw_containers.vector
+ pw_bytes
+ pw_unit_test
+ GROUPS
+ modules
+ pw_allocator
+)
+
+pw_add_test(pw_allocator.unique_ptr_test
+ SOURCES
+ unique_ptr_test.cc
+ PRIVATE_DEPS
+ pw_allocator.allocator
+ pw_allocator.allocator_testing
+ GROUPS
+ modules
+ pw_allocator
+)
diff --git a/pw_allocator/allocator.cc b/pw_allocator/allocator.cc
new file mode 100644
index 000000000..f63bacd69
--- /dev/null
+++ b/pw_allocator/allocator.cc
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/allocator.h"
+
+#include <algorithm>
+#include <cstring>
+
+#include "pw_assert/check.h"
+#include "pw_bytes/alignment.h"
+
+namespace pw::allocator {
+
+void* Allocator::DoReallocate(void* ptr, Layout layout, size_t new_size) {
+ if (new_size == 0) {
+ return nullptr;
+ }
+ if (Resize(ptr, layout, new_size)) {
+ return ptr;
+ }
+ void* new_ptr = DoAllocate(Layout(new_size, layout.alignment()));
+ if (new_ptr == nullptr) {
+ return nullptr;
+ }
+ if (ptr != nullptr && layout.size() != 0) {
+ std::memcpy(new_ptr, ptr, layout.size());
+ DoDeallocate(ptr, layout);
+ }
+ return new_ptr;
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/allocator_metric_proxy.cc b/pw_allocator/allocator_metric_proxy.cc
new file mode 100644
index 000000000..10ca68d3a
--- /dev/null
+++ b/pw_allocator/allocator_metric_proxy.cc
@@ -0,0 +1,78 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/allocator_metric_proxy.h"
+
+#include <cstddef>
+
+#include "pw_assert/check.h"
+#include "pw_metric/metric.h"
+
+namespace pw::allocator {
+
+void AllocatorMetricProxy::Initialize(Allocator& allocator) {
+ PW_DCHECK(allocator_ == nullptr);
+ allocator_ = &allocator;
+ // Manually add the metrics to the metric group to allow the constructor to
+ // remain constexpr.
+ memusage_.Add(used_);
+ memusage_.Add(peak_);
+ memusage_.Add(count_);
+}
+
+Status AllocatorMetricProxy::DoQuery(const void* ptr, Layout layout) const {
+ PW_DCHECK_NOTNULL(allocator_);
+ return allocator_->Query(ptr, layout);
+}
+
+void* AllocatorMetricProxy::DoAllocate(Layout layout) {
+ PW_DCHECK_NOTNULL(allocator_);
+ void* ptr = allocator_->Allocate(layout);
+ if (ptr == nullptr) {
+ return nullptr;
+ }
+ used_.Increment(layout.size());
+ if (used_.value() > peak_.value()) {
+ peak_.Set(used_.value());
+ }
+ count_.Increment();
+ return ptr;
+}
+
+void AllocatorMetricProxy::DoDeallocate(void* ptr, Layout layout) {
+ PW_DCHECK_NOTNULL(allocator_);
+ allocator_->Deallocate(ptr, layout);
+ if (ptr == nullptr) {
+ return;
+ }
+ PW_DCHECK_UINT_GE(used_.value(), layout.size());
+ PW_DCHECK_UINT_NE(count_.value(), 0);
+ used_.Set(used_.value() - layout.size());
+ count_.Set(count_.value() - 1);
+}
+
+bool AllocatorMetricProxy::DoResize(void* ptr, Layout layout, size_t new_size) {
+ PW_DCHECK_NOTNULL(allocator_);
+ if (!allocator_->Resize(ptr, layout, new_size)) {
+ return false;
+ }
+ PW_DCHECK_UINT_GE(used_.value(), layout.size());
+ used_.Set(used_.value() - layout.size() + new_size);
+ if (used_.value() > peak_.value()) {
+ peak_.Set(used_.value());
+ }
+ return true;
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/allocator_metric_proxy_test.cc b/pw_allocator/allocator_metric_proxy_test.cc
new file mode 100644
index 000000000..cfc94b2f0
--- /dev/null
+++ b/pw_allocator/allocator_metric_proxy_test.cc
@@ -0,0 +1,175 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/allocator_metric_proxy.h"
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator_testing.h"
+
+namespace pw::allocator {
+namespace {
+
+// Test fixtures.
+
+class AllocatorMetricProxyTest : public ::testing::Test {
+ protected:
+ AllocatorMetricProxyTest() : allocator(0) {}
+
+ void SetUp() override { allocator.Initialize(*wrapped); }
+
+ AllocatorMetricProxy allocator;
+
+ private:
+ test::AllocatorForTestWithBuffer<256> wrapped;
+};
+
+// Unit tests.
+
+TEST_F(AllocatorMetricProxyTest, InitiallyZero) {
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), 0U);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, MetricsInitialized) {
+ auto& memusage = allocator.memusage();
+ EXPECT_EQ(memusage.metrics().size(), 3U);
+ EXPECT_EQ(memusage.children().size(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, AllocateDeallocate) {
+ constexpr Layout layout = Layout::Of<uint32_t[2]>();
+ void* ptr = allocator.Allocate(layout);
+ ASSERT_NE(ptr, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ allocator.Deallocate(ptr, layout);
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, AllocateFailure) {
+ constexpr Layout layout = Layout::Of<uint32_t[0x10000000U]>();
+ void* ptr = allocator.Allocate(layout);
+ EXPECT_EQ(ptr, nullptr);
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), 0U);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, AllocateDeallocateMultiple) {
+ constexpr Layout layout1 = Layout::Of<uint32_t[3]>();
+ void* ptr1 = allocator.Allocate(layout1);
+ ASSERT_NE(ptr1, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 3);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 3);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ constexpr Layout layout2 = Layout::Of<uint32_t[2]>();
+ void* ptr2 = allocator.Allocate(layout2);
+ ASSERT_NE(ptr2, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.count(), 2U);
+
+ allocator.Deallocate(ptr1, layout1);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ allocator.Deallocate(ptr2, layout2);
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, ResizeLarger) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[3]>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 3);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 3);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ constexpr Layout new_layout = Layout::Of<uint32_t[5]>();
+ EXPECT_TRUE(allocator.Resize(ptr, old_layout, new_layout.size()));
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ allocator.Deallocate(ptr, new_layout);
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 5);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, ResizeSmaller) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[2]>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ constexpr Layout new_layout = Layout::Of<uint32_t>();
+ EXPECT_TRUE(allocator.Resize(ptr, old_layout, new_layout.size()));
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t));
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ allocator.Deallocate(ptr, new_layout);
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+TEST_F(AllocatorMetricProxyTest, Reallocate) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[2]>();
+ void* ptr1 = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr1, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ // Make a second allocation to force reallocation.
+ void* ptr2 = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr2, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 4);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 4);
+ EXPECT_EQ(allocator.count(), 2U);
+
+ // Reallocating allocates before deallocating, leading to higher peaks.
+ constexpr Layout new_layout = Layout::Of<uint32_t[4]>();
+ void* new_ptr = allocator.Reallocate(ptr1, old_layout, new_layout.size());
+ EXPECT_NE(new_ptr, nullptr);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 6);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 8);
+ EXPECT_EQ(allocator.count(), 2U);
+
+ allocator.Deallocate(new_ptr, new_layout);
+ EXPECT_EQ(allocator.used(), sizeof(uint32_t) * 2);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 8);
+ EXPECT_EQ(allocator.count(), 1U);
+
+ allocator.Deallocate(ptr2, old_layout);
+ EXPECT_EQ(allocator.used(), 0U);
+ EXPECT_EQ(allocator.peak(), sizeof(uint32_t) * 8);
+ EXPECT_EQ(allocator.count(), 0U);
+}
+
+} // namespace
+} // namespace pw::allocator
diff --git a/pw_allocator/allocator_test.cc b/pw_allocator/allocator_test.cc
new file mode 100644
index 000000000..c41356ca8
--- /dev/null
+++ b/pw_allocator/allocator_test.cc
@@ -0,0 +1,166 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/allocator.h"
+
+#include <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator_testing.h"
+#include "pw_bytes/alignment.h"
+
+namespace pw::allocator {
+namespace {
+
+// Test fixtures.
+
+class AllocatorTest : public ::testing::Test {
+ protected:
+ void SetUp() override { EXPECT_EQ(allocator.Init(buffer), OkStatus()); }
+ void TearDown() override { allocator.DeallocateAll(); }
+
+ test::AllocatorForTest allocator;
+
+ private:
+ std::array<std::byte, 256> buffer = {};
+};
+
+// Unit tests
+
+TEST_F(AllocatorTest, ReallocateNull) {
+ constexpr Layout old_layout = Layout::Of<uint32_t>();
+ size_t new_size = old_layout.size();
+ void* new_ptr = allocator.Reallocate(nullptr, old_layout, new_size);
+
+ // Resize should fail and Reallocate should call Allocate.
+ EXPECT_EQ(allocator.allocate_size(), new_size);
+
+ // Deallocate should not be called.
+ EXPECT_EQ(allocator.deallocate_ptr(), nullptr);
+ EXPECT_EQ(allocator.deallocate_size(), 0U);
+
+ // Overall, Reallocate should succeed.
+ EXPECT_NE(new_ptr, nullptr);
+}
+
+TEST_F(AllocatorTest, ReallocateZeroNewSize) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[3]>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_EQ(allocator.allocate_size(), old_layout.size());
+ ASSERT_NE(ptr, nullptr);
+ allocator.ResetParameters();
+
+ size_t new_size = 0;
+ void* new_ptr = allocator.Reallocate(ptr, old_layout, new_size);
+
+ // Reallocate does not call Resize, Allocate, or Deallocate.
+ EXPECT_EQ(allocator.resize_ptr(), nullptr);
+ EXPECT_EQ(allocator.resize_old_size(), 0U);
+ EXPECT_EQ(allocator.resize_new_size(), 0U);
+ EXPECT_EQ(allocator.allocate_size(), 0U);
+ EXPECT_EQ(allocator.deallocate_ptr(), nullptr);
+ EXPECT_EQ(allocator.deallocate_size(), 0U);
+
+ // Overall, Reallocate should fail.
+ EXPECT_EQ(new_ptr, nullptr);
+}
+
+TEST_F(AllocatorTest, ReallocateSame) {
+ constexpr Layout layout = Layout::Of<uint32_t[3]>();
+ void* ptr = allocator.Allocate(layout);
+ ASSERT_EQ(allocator.allocate_size(), layout.size());
+ ASSERT_NE(ptr, nullptr);
+ allocator.ResetParameters();
+
+ void* new_ptr = allocator.Reallocate(ptr, layout, layout.size());
+
+ // Reallocate should call Resize.
+ EXPECT_EQ(allocator.resize_ptr(), ptr);
+ EXPECT_EQ(allocator.resize_old_size(), layout.size());
+ EXPECT_EQ(allocator.resize_new_size(), layout.size());
+
+ // Allocate should not be called.
+ EXPECT_EQ(allocator.allocate_size(), 0U);
+
+ // Deallocate should not be called.
+ EXPECT_EQ(allocator.deallocate_ptr(), nullptr);
+ EXPECT_EQ(allocator.deallocate_size(), 0U);
+
+ // Overall, Reallocate should succeed.
+ EXPECT_EQ(new_ptr, ptr);
+}
+
+TEST_F(AllocatorTest, ReallocateSmaller) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[3]>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_EQ(allocator.allocate_size(), old_layout.size());
+ ASSERT_NE(ptr, nullptr);
+ allocator.ResetParameters();
+
+ size_t new_size = sizeof(uint32_t);
+ void* new_ptr = allocator.Reallocate(ptr, old_layout, new_size);
+
+ // Reallocate should call Resize.
+ EXPECT_EQ(allocator.resize_ptr(), ptr);
+ EXPECT_EQ(allocator.resize_old_size(), old_layout.size());
+ EXPECT_EQ(allocator.resize_new_size(), new_size);
+
+ // Allocate should not be called.
+ EXPECT_EQ(allocator.allocate_size(), 0U);
+
+ // Deallocate should not be called.
+ EXPECT_EQ(allocator.deallocate_ptr(), nullptr);
+ EXPECT_EQ(allocator.deallocate_size(), 0U);
+
+ // Overall, Reallocate should succeed.
+ EXPECT_EQ(new_ptr, ptr);
+}
+
+TEST_F(AllocatorTest, ReallocateLarger) {
+ constexpr Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_EQ(allocator.allocate_size(), old_layout.size());
+ ASSERT_NE(ptr, nullptr);
+
+ // The abstraction is a bit leaky here: This tests relies on the details of
+ // `Resize` in order to get it to fail and fallback to
+ // allocate/copy/deallocate. Allocate a second block, which should prevent the
+ // first one from being able to grow.
+ void* next = allocator.Allocate(old_layout);
+ ASSERT_EQ(allocator.allocate_size(), old_layout.size());
+ ASSERT_NE(next, nullptr);
+ allocator.ResetParameters();
+
+ size_t new_size = sizeof(uint32_t[3]);
+ void* new_ptr = allocator.Reallocate(ptr, old_layout, new_size);
+
+ // Reallocate should call Resize.
+ EXPECT_EQ(allocator.resize_ptr(), ptr);
+ EXPECT_EQ(allocator.resize_old_size(), old_layout.size());
+ EXPECT_EQ(allocator.resize_new_size(), new_size);
+
+ // Resize should fail and Reallocate should call Allocate.
+ EXPECT_EQ(allocator.allocate_size(), new_size);
+
+ // Deallocate should also be called.
+ EXPECT_EQ(allocator.deallocate_ptr(), ptr);
+ EXPECT_EQ(allocator.deallocate_size(), old_layout.size());
+
+ // Overall, Reallocate should succeed.
+ EXPECT_NE(new_ptr, nullptr);
+ EXPECT_NE(new_ptr, ptr);
+}
+
+} // namespace
+} // namespace pw::allocator
diff --git a/pw_allocator/allocator_testing.cc b/pw_allocator/allocator_testing.cc
new file mode 100644
index 000000000..269c47d1d
--- /dev/null
+++ b/pw_allocator/allocator_testing.cc
@@ -0,0 +1,81 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/allocator_testing.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::allocator::test {
+
+AllocatorForTest::~AllocatorForTest() {
+ for (auto* block : allocator_.blocks()) {
+ PW_DCHECK(
+ !block->Used(),
+ "The block at %p was still in use when its allocator was "
+ "destroyed. All memory allocated by an allocator must be released "
+ "before the allocator goes out of scope.",
+ static_cast<void*>(block));
+ }
+}
+
+Status AllocatorForTest::Init(ByteSpan bytes) {
+ ResetParameters();
+ return allocator_.Init(bytes);
+}
+
+void AllocatorForTest::Exhaust() {
+ for (auto* block : allocator_.blocks()) {
+ block->MarkUsed();
+ }
+}
+
+void AllocatorForTest::ResetParameters() {
+ allocate_size_ = 0;
+ deallocate_ptr_ = nullptr;
+ deallocate_size_ = 0;
+ resize_ptr_ = nullptr;
+ resize_old_size_ = 0;
+ resize_new_size_ = 0;
+}
+
+void AllocatorForTest::DeallocateAll() {
+ for (auto* block : allocator_.blocks()) {
+ BlockType::Free(block);
+ }
+ ResetParameters();
+}
+
+Status AllocatorForTest::DoQuery(const void* ptr, Layout layout) const {
+ return allocator_.Query(ptr, layout);
+}
+
+void* AllocatorForTest::DoAllocate(Layout layout) {
+ allocate_size_ = layout.size();
+ return allocator_.Allocate(layout);
+}
+
+void AllocatorForTest::DoDeallocate(void* ptr, Layout layout) {
+ deallocate_ptr_ = ptr;
+ deallocate_size_ = layout.size();
+ return allocator_.Deallocate(ptr, layout);
+}
+
+bool AllocatorForTest::DoResize(void* ptr, Layout layout, size_t new_size) {
+ resize_ptr_ = ptr;
+ resize_old_size_ = layout.size();
+ resize_new_size_ = new_size;
+ return allocator_.Resize(ptr, layout, new_size);
+}
+
+} // namespace pw::allocator::test
diff --git a/pw_allocator/block.cc b/pw_allocator/block.cc
index ec0afc22a..f09876fde 100644
--- a/pw_allocator/block.cc
+++ b/pw_allocator/block.cc
@@ -14,242 +14,69 @@
#include "pw_allocator/block.h"
-#include <cstring>
-
#include "pw_assert/check.h"
-#include "pw_span/span.h"
namespace pw::allocator {
-Status Block::Init(const span<std::byte> region, Block** block) {
- // Ensure the region we're given is aligned and sized accordingly
- if (reinterpret_cast<uintptr_t>(region.data()) % alignof(Block) != 0) {
- return Status::InvalidArgument();
- }
-
- if (region.size() < sizeof(Block)) {
- return Status::InvalidArgument();
- }
-
- union {
- Block* block;
- std::byte* bytes;
- } aliased;
- aliased.bytes = region.data();
-
- // Make "next" point just past the end of this block; forming a linked list
- // with the following storage. Since the space between this block and the
- // next are implicitly part of the raw data, size can be computed by
- // subtracting the pointers.
- aliased.block->next_ =
- reinterpret_cast<Block*>(region.data() + region.size_bytes());
- aliased.block->MarkLast();
-
- aliased.block->prev_ = nullptr;
- *block = aliased.block;
#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
- (*block)->PoisonBlock();
-#endif // PW_ALLOCATOR_POISON_ENABLE
- return OkStatus();
-}
-Status Block::Split(size_t head_block_inner_size, Block** new_block) {
- if (new_block == nullptr) {
- return Status::InvalidArgument();
- }
-
- // Don't split used blocks.
- // TODO(jgarside): Relax this restriction? Flag to enable/disable this check?
- if (Used()) {
- return Status::FailedPrecondition();
- }
-
- // First round the head_block_inner_size up to a alignof(Block) bounary.
- // This ensures that the next block header is aligned accordingly.
- // Alignment must be a power of two, hence align()-1 will return the
- // remainder.
- auto align_bit_mask = alignof(Block) - 1;
- size_t aligned_head_block_inner_size = head_block_inner_size;
- if ((head_block_inner_size & align_bit_mask) != 0) {
- aligned_head_block_inner_size =
- (head_block_inner_size & ~align_bit_mask) + alignof(Block);
- }
-
- // (1) Are we trying to allocate a head block larger than the current head
- // block? This may happen because of the alignment above.
- if (aligned_head_block_inner_size > InnerSize()) {
- return Status::OutOfRange();
- }
-
- // (2) Does the resulting block have enough space to store the header?
- // TODO(jgarside): What to do if the returned section is empty (i.e. remaining
- // size == sizeof(Block))?
- if (InnerSize() - aligned_head_block_inner_size <
- sizeof(Block) + 2 * PW_ALLOCATOR_POISON_OFFSET) {
- return Status::ResourceExhausted();
- }
-
- // Create the new block inside the current one.
- Block* new_next = reinterpret_cast<Block*>(
- // From the current position...
- reinterpret_cast<intptr_t>(this) +
- // skip past the current header...
- sizeof(*this) +
- // add the poison bytes before usable space ...
- PW_ALLOCATOR_POISON_OFFSET +
- // into the usable bytes by the new inner size...
- aligned_head_block_inner_size +
- // add the poison bytes after the usable space ...
- PW_ALLOCATOR_POISON_OFFSET);
-
- // If we're inserting in the middle, we need to update the current next
- // block to point to what we're inserting
- if (!Last()) {
- Next()->prev_ = new_next;
- }
-
- // Copy next verbatim so the next block also gets the "last"-ness
- new_next->next_ = next_;
- new_next->prev_ = this;
-
- // Update the current block to point to the new head.
- next_ = new_next;
-
- *new_block = next_;
-
-#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
- PoisonBlock();
- (*new_block)->PoisonBlock();
-#endif // PW_ALLOCATOR_POISON_ENABLE
-
- return OkStatus();
+void BaseBlock::Poison(void* block, size_t header_size, size_t outer_size) {
+ auto* start = reinterpret_cast<std::byte*>(block);
+ std::memcpy(
+ start + header_size - kPoisonOffset, kPoisonPattern, kPoisonOffset);
+ std::memcpy(
+ start + outer_size - kPoisonOffset, kPoisonPattern, kPoisonOffset);
}
-Status Block::MergeNext() {
- // Anything to merge with?
- if (Last()) {
- return Status::OutOfRange();
- }
-
- // Is this or the next block in use?
- if (Used() || Next()->Used()) {
- return Status::FailedPrecondition();
- }
-
- // Simply enough, this block's next pointer becomes the next block's
- // next pointer. We then need to re-wire the "next next" block's prev
- // pointer to point back to us though.
- next_ = Next()->next_;
-
- // Copying the pointer also copies the "last" status, so this is safe.
- if (!Last()) {
- Next()->prev_ = this;
- }
-
- return OkStatus();
+bool BaseBlock::CheckPoison(const void* block,
+ size_t header_size,
+ size_t outer_size) {
+ const auto* start = reinterpret_cast<const std::byte*>(block);
+ return std::memcmp(start + header_size - kPoisonOffset,
+ kPoisonPattern,
+ kPoisonOffset) == 0 &&
+ std::memcmp(start + outer_size - kPoisonOffset,
+ kPoisonPattern,
+ kPoisonOffset) == 0;
}
-Status Block::MergePrev() {
- // We can't merge if we have no previous. After that though, merging with
- // the previous block is just MergeNext from the previous block.
- if (prev_ == nullptr) {
- return Status::OutOfRange();
- }
-
- // WARNING: This class instance will still exist, but technically be invalid
- // after this has been invoked. Be careful when doing anything with `this`
- // After doing the below.
- return prev_->MergeNext();
-}
+#else // PW_ALLOCATOR_POISON_ENABLE
-// TODO(b/234875269): Add stack tracing to locate which call to the heap
-// operation caused the corruption.
-// TODO(jgarside): Add detailed information to log report and leave succinct
-// messages in the crash message.
-void Block::CrashIfInvalid() {
- switch (CheckStatus()) {
- case VALID:
- break;
- case MISALIGNED:
- PW_DCHECK(false,
- "The block at address %p is not aligned.",
- static_cast<void*>(this));
- break;
- case NEXT_MISMATCHED:
- PW_DCHECK(false,
- "The 'prev' field in the next block (%p) does not match the "
- "address of the current block (%p).",
- static_cast<void*>(Next()->Prev()),
- static_cast<void*>(this));
- break;
- case PREV_MISMATCHED:
- PW_DCHECK(false,
- "The 'next' field in the previous block (%p) does not match "
- "the address of the current block (%p).",
- static_cast<void*>(Prev()->Next()),
- static_cast<void*>(this));
- break;
- case POISON_CORRUPTED:
- PW_DCHECK(false,
- "The poisoned pattern in the block at %p is corrupted.",
- static_cast<void*>(this));
- break;
- }
-}
+void BaseBlock::Poison(void*, size_t, size_t) {}
-// This function will return a Block::BlockStatus that is either VALID or
-// indicates the reason why the Block is invalid. If the Block is invalid at
-// multiple points, this function will only return one of the reasons.
-Block::BlockStatus Block::CheckStatus() const {
- // Make sure the Block is aligned.
- if (reinterpret_cast<uintptr_t>(this) % alignof(Block) != 0) {
- return BlockStatus::MISALIGNED;
- }
+bool BaseBlock::CheckPoison(const void*, size_t, size_t) { return true; }
- // Test if the prev/next pointer for this Block matches.
- if (!Last() && (this >= Next() || this != Next()->Prev())) {
- return BlockStatus::NEXT_MISMATCHED;
- }
+#endif // PW_ALLOCATOR_POISON_ENABLE
- if (Prev() && (this <= Prev() || this != Prev()->Next())) {
- return BlockStatus::PREV_MISMATCHED;
- }
+// TODO: b/234875269 - Add stack tracing to locate which call to the heap
+// operation caused the corruption in the methods below.
-#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
- if (!this->CheckPoisonBytes()) {
- return BlockStatus::POISON_CORRUPTED;
- }
-#endif // PW_ALLOCATOR_POISON_ENABLE
- return BlockStatus::VALID;
+void BaseBlock::CrashMisaligned(uintptr_t addr) {
+ PW_DCHECK(false,
+ "The block at address %p is not aligned.",
+ reinterpret_cast<void*>(addr));
}
-// Paint sizeof(void*) bytes before and after the usable space in Block as the
-// randomized function pattern.
-void Block::PoisonBlock() {
-#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
- std::byte* front_region = reinterpret_cast<std::byte*>(this) + sizeof(*this);
- memcpy(front_region, POISON_PATTERN, PW_ALLOCATOR_POISON_OFFSET);
+void BaseBlock::CrashNextMismatched(uintptr_t addr, uintptr_t next_prev) {
+ PW_DCHECK(false,
+ "The 'prev' field in the next block (%p) does not match the "
+ "address of the current block (%p).",
+ reinterpret_cast<void*>(next_prev),
+ reinterpret_cast<void*>(addr));
+}
- std::byte* end_region =
- reinterpret_cast<std::byte*>(Next()) - PW_ALLOCATOR_POISON_OFFSET;
- memcpy(end_region, POISON_PATTERN, PW_ALLOCATOR_POISON_OFFSET);
-#endif // PW_ALLOCATOR_POISON_ENABLE
+void BaseBlock::CrashPrevMismatched(uintptr_t addr, uintptr_t prev_next) {
+ PW_DCHECK(false,
+ "The 'next' field in the previous block (%p) does not match "
+ "the address of the current block (%p).",
+ reinterpret_cast<void*>(prev_next),
+ reinterpret_cast<void*>(addr));
}
-bool Block::CheckPoisonBytes() const {
-#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
- std::byte* front_region = reinterpret_cast<std::byte*>(
- reinterpret_cast<intptr_t>(this) + sizeof(*this));
- if (std::memcmp(front_region, POISON_PATTERN, PW_ALLOCATOR_POISON_OFFSET)) {
- return false;
- }
- std::byte* end_region = reinterpret_cast<std::byte*>(
- reinterpret_cast<intptr_t>(this->Next()) - PW_ALLOCATOR_POISON_OFFSET);
- if (std::memcmp(end_region, POISON_PATTERN, PW_ALLOCATOR_POISON_OFFSET)) {
- return false;
- }
-#endif // PW_ALLOCATOR_POISON_ENABLE
- return true;
+void BaseBlock::CrashPoisonCorrupted(uintptr_t addr) {
+ PW_DCHECK(false,
+ "The poisoned pattern in the block at %p is corrupted.",
+ reinterpret_cast<void*>(addr));
}
} // namespace pw::allocator
diff --git a/pw_allocator/block_test.cc b/pw_allocator/block_test.cc
index 06772bd99..fd2906d3f 100644
--- a/pw_allocator/block_test.cc
+++ b/pw_allocator/block_test.cc
@@ -14,6 +14,7 @@
#include "pw_allocator/block.h"
+#include <cstdint>
#include <cstring>
#include "gtest/gtest.h"
@@ -23,101 +24,143 @@ using std::byte;
namespace pw::allocator {
-TEST(Block, CanCreateSingleBlock) {
+const size_t kCapacity = 0x20000;
+
+template <typename BlockType>
+void CanCreateSingleBlock() {
constexpr size_t kN = 200;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- auto status = Block::Init(span(bytes, kN), &block);
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- ASSERT_EQ(status, OkStatus());
EXPECT_EQ(block->OuterSize(), kN);
- EXPECT_EQ(block->InnerSize(),
- kN - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET);
+ EXPECT_EQ(block->InnerSize(), kN - BlockType::kBlockOverhead);
EXPECT_EQ(block->Prev(), nullptr);
- EXPECT_EQ(block->Next(), (Block*)((uintptr_t)block + kN));
- EXPECT_EQ(block->Used(), false);
- EXPECT_EQ(block->Last(), true);
+ EXPECT_EQ(block->Next(), nullptr);
+ EXPECT_FALSE(block->Used());
+ EXPECT_TRUE(block->Last());
+}
+TEST(GenericBlockTest, CanCreateSingleBlock) {
+ CanCreateSingleBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanCreateSingleBlock) {
+ CanCreateSingleBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotCreateUnalignedSingleBlock) {
+template <typename BlockType>
+void CannotCreateUnalignedSingleBlock() {
constexpr size_t kN = 1024;
// Force alignment, so we can un-force it below
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
byte* byte_ptr = bytes;
- Block* block = nullptr;
- auto status = Block::Init(span(byte_ptr + 1, kN - 1), &block);
-
- EXPECT_EQ(status, Status::InvalidArgument());
+ Result<BlockType*> result = BlockType::Init(span(byte_ptr + 1, kN - 1));
+ EXPECT_FALSE(result.ok());
+ EXPECT_EQ(result.status(), Status::InvalidArgument());
+}
+TEST(GenericBlockTest, CannotCreateUnalignedSingleBlock) {
+ CannotCreateUnalignedSingleBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotCreateUnalignedSingleBlock) {
+ CannotCreateUnalignedSingleBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotCreateTooSmallBlock) {
+template <typename BlockType>
+void CannotCreateTooSmallBlock() {
constexpr size_t kN = 2;
- alignas(Block*) byte bytes[kN];
- Block* block = nullptr;
- auto status = Block::Init(span(bytes, kN), &block);
+ alignas(BlockType*) byte bytes[kN];
- EXPECT_EQ(status, Status::InvalidArgument());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ EXPECT_FALSE(result.ok());
+ EXPECT_EQ(result.status(), Status::ResourceExhausted());
+}
+TEST(GenericBlockTest, CannotCreateTooSmallBlock) {
+ CannotCreateTooSmallBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotCreateTooSmallBlock) {
+ CannotCreateTooSmallBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanSplitBlock) {
+TEST(CustomBlockTest, CannotCreateTooLargeBlock) {
+ using BlockType = Block<uint16_t, 512>;
+ constexpr size_t kN = 1024;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ EXPECT_FALSE(result.ok());
+ EXPECT_EQ(result.status(), Status::OutOfRange());
+}
+
+template <typename BlockType>
+void CanSplitBlock() {
constexpr size_t kN = 1024;
constexpr size_t kSplitN = 512;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* next_block = nullptr;
- auto status = block->Split(kSplitN, &next_block);
+ result = BlockType::Split(block1, kSplitN);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
- ASSERT_EQ(status, OkStatus());
- EXPECT_EQ(block->InnerSize(), kSplitN);
- EXPECT_EQ(block->OuterSize(),
- kSplitN + sizeof(Block) + 2 * PW_ALLOCATOR_POISON_OFFSET);
- EXPECT_EQ(block->Last(), false);
+ EXPECT_EQ(block1->InnerSize(), kSplitN);
+ EXPECT_EQ(block1->OuterSize(), kSplitN + BlockType::kBlockOverhead);
+ EXPECT_FALSE(block1->Last());
- EXPECT_EQ(next_block->OuterSize(),
- kN - kSplitN - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET);
- EXPECT_EQ(next_block->Used(), false);
- EXPECT_EQ(next_block->Last(), true);
+ EXPECT_EQ(block2->OuterSize(), kN - kSplitN - BlockType::kBlockOverhead);
+ EXPECT_FALSE(block2->Used());
+ EXPECT_TRUE(block2->Last());
- EXPECT_EQ(block->Next(), next_block);
- EXPECT_EQ(next_block->Prev(), block);
+ EXPECT_EQ(block1->Next(), block2);
+ EXPECT_EQ(block2->Prev(), block1);
+}
+TEST(GenericBlockTest, CanSplitBlock) { CanSplitBlock<Block<>>(); }
+TEST(CustomBlockTest, CanSplitBlock) {
+ CanSplitBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanSplitBlockUnaligned) {
+template <typename BlockType>
+void CanSplitBlockUnaligned() {
constexpr size_t kN = 1024;
constexpr size_t kSplitN = 513;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- // We should split at sizeof(Block) + kSplitN bytes. Then
- // we need to round that up to an alignof(Block*) boundary.
+ // We should split at sizeof(BlockType) + kSplitN bytes. Then
+ // we need to round that up to an alignof(BlockType*) boundary.
uintptr_t split_addr = ((uintptr_t)&bytes) + kSplitN;
- split_addr += alignof(Block*) - (split_addr % alignof(Block*));
+ split_addr += alignof(BlockType*) - (split_addr % alignof(BlockType*));
uintptr_t split_len = split_addr - (uintptr_t)&bytes;
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplitN);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ EXPECT_EQ(block1->InnerSize(), split_len);
+ EXPECT_EQ(block1->OuterSize(), split_len + BlockType::kBlockOverhead);
- Block* next_block = nullptr;
- auto status = block->Split(kSplitN, &next_block);
+ EXPECT_EQ(block2->OuterSize(), kN - block1->OuterSize());
+ EXPECT_FALSE(block2->Used());
- ASSERT_EQ(status, OkStatus());
- EXPECT_EQ(block->InnerSize(), split_len);
- EXPECT_EQ(block->OuterSize(),
- split_len + sizeof(Block) + 2 * PW_ALLOCATOR_POISON_OFFSET);
- EXPECT_EQ(next_block->OuterSize(),
- kN - split_len - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET);
- EXPECT_EQ(next_block->Used(), false);
- EXPECT_EQ(block->Next(), next_block);
- EXPECT_EQ(next_block->Prev(), block);
+ EXPECT_EQ(block1->Next(), block2);
+ EXPECT_EQ(block2->Prev(), block1);
+}
+TEST(GenericBlockTest, CanSplitBlockUnaligned) { CanSplitBlock<Block<>>(); }
+TEST(CustomBlockTest, CanSplitBlockUnaligned) {
+ CanSplitBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanSplitMidBlock) {
+template <typename BlockType>
+void CanSplitMidBlock() {
// Split once, then split the original block again to ensure that the
// pointers get rewired properly.
// I.e.
@@ -130,282 +173,990 @@ TEST(Block, CanSplitMidBlock) {
constexpr size_t kN = 1024;
constexpr size_t kSplit1 = 512;
constexpr size_t kSplit2 = 256;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* block2 = nullptr;
- ASSERT_EQ(OkStatus(), block->Split(kSplit1, &block2));
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
- Block* block3 = nullptr;
- ASSERT_EQ(OkStatus(), block->Split(kSplit2, &block3));
+ result = BlockType::Split(block1, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
- EXPECT_EQ(block->Next(), block3);
+ EXPECT_EQ(block1->Next(), block3);
+ EXPECT_EQ(block3->Prev(), block1);
EXPECT_EQ(block3->Next(), block2);
EXPECT_EQ(block2->Prev(), block3);
- EXPECT_EQ(block3->Prev(), block);
+}
+TEST(GenericBlockTest, CanSplitMidBlock) { CanSplitMidBlock<Block<>>(); }
+TEST(CustomBlockTest, CanSplitMidBlock) {
+ CanSplitMidBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotSplitBlockWithoutHeaderSpace) {
- constexpr size_t kN = 1024;
- constexpr size_t kSplitN =
- kN - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET - 1;
- alignas(Block*) byte bytes[kN];
-
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+template <typename BlockType>
+void CannotSplitTooSmallBlock() {
+ constexpr size_t kN = 64;
+ constexpr size_t kSplitN = kN + 1;
+ alignas(BlockType*) byte bytes[kN];
- Block* next_block = nullptr;
- auto status = block->Split(kSplitN, &next_block);
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- EXPECT_EQ(status, Status::ResourceExhausted());
- EXPECT_EQ(next_block, nullptr);
+ result = BlockType::Split(block, kSplitN);
+ EXPECT_EQ(result.status(), Status::OutOfRange());
}
-TEST(Block, MustProvideNextBlockPointer) {
+template <typename BlockType>
+void CannotSplitBlockWithoutHeaderSpace() {
constexpr size_t kN = 1024;
- constexpr size_t kSplitN = kN - sizeof(Block) - 1;
- alignas(Block*) byte bytes[kN];
+ constexpr size_t kSplitN = kN - BlockType::kBlockOverhead - 1;
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- auto status = block->Split(kSplitN, nullptr);
- EXPECT_EQ(status, Status::InvalidArgument());
+ result = BlockType::Split(block, kSplitN);
+ EXPECT_EQ(result.status(), Status::ResourceExhausted());
+}
+TEST(GenericBlockTest, CannotSplitBlockWithoutHeaderSpace) {
+ CannotSplitBlockWithoutHeaderSpace<Block<>>();
+}
+TEST(CustomBlockTest, CannotSplitBlockWithoutHeaderSpace) {
+ CannotSplitBlockWithoutHeaderSpace<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotMakeBlockLargerInSplit) {
+template <typename BlockType>
+void CannotMakeBlockLargerInSplit() {
// Ensure that we can't ask for more space than the block actually has...
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
-
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ alignas(BlockType*) byte bytes[kN];
- Block* next_block = nullptr;
- auto status = block->Split(block->InnerSize() + 1, &next_block);
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- EXPECT_EQ(status, Status::OutOfRange());
+ result = BlockType::Split(block, block->InnerSize() + 1);
+ EXPECT_EQ(result.status(), Status::OutOfRange());
+}
+TEST(GenericBlockTest, CannotMakeBlockLargerInSplit) {
+ CannotMakeBlockLargerInSplit<Block<>>();
+}
+TEST(CustomBlockTest, CannotMakeBlockLargerInSplit) {
+ CannotMakeBlockLargerInSplit<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotMakeSecondBlockLargerInSplit) {
+template <typename BlockType>
+void CannotMakeSecondBlockLargerInSplit() {
// Ensure that the second block in split is at least of the size of header.
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- Block* next_block = nullptr;
- auto status = block->Split(
- block->InnerSize() - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET + 1,
- &next_block);
-
- ASSERT_EQ(status, Status::ResourceExhausted());
- EXPECT_EQ(next_block, nullptr);
+ result = BlockType::Split(block,
+ block->InnerSize() - BlockType::kBlockOverhead + 1);
+ EXPECT_EQ(result.status(), Status::ResourceExhausted());
+}
+TEST(GenericBlockTest, CannotMakeSecondBlockLargerInSplit) {
+ CannotMakeSecondBlockLargerInSplit<Block<>>();
+}
+TEST(CustomBlockTest, CannotMakeSecondBlockLargerInSplit) {
+ CannotMakeSecondBlockLargerInSplit<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanMakeZeroSizeFirstBlock) {
+template <typename BlockType>
+void CanMakeZeroSizeFirstBlock() {
// This block does support splitting with zero payload size.
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
-
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ alignas(BlockType*) byte bytes[kN];
- Block* next_block = nullptr;
- auto status = block->Split(0, &next_block);
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- ASSERT_EQ(status, OkStatus());
+ result = BlockType::Split(block, 0);
+ ASSERT_EQ(result.status(), OkStatus());
EXPECT_EQ(block->InnerSize(), static_cast<size_t>(0));
}
+TEST(GenericBlockTest, CanMakeZeroSizeFirstBlock) {
+ CanMakeZeroSizeFirstBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanMakeZeroSizeFirstBlock) {
+ CanMakeZeroSizeFirstBlock<Block<uint32_t, kCapacity>>();
+}
-TEST(Block, CanMakeZeroSizeSecondBlock) {
+template <typename BlockType>
+void CanMakeZeroSizeSecondBlock() {
// Likewise, the split block can be zero-width.
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* next_block = nullptr;
- auto status = block->Split(
- block->InnerSize() - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET,
- &next_block);
+ result =
+ BlockType::Split(block1, block1->InnerSize() - BlockType::kBlockOverhead);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
- ASSERT_EQ(status, OkStatus());
- EXPECT_EQ(next_block->InnerSize(), static_cast<size_t>(0));
+ EXPECT_EQ(block2->InnerSize(), static_cast<size_t>(0));
+}
+TEST(GenericBlockTest, CanMakeZeroSizeSecondBlock) {
+ CanMakeZeroSizeSecondBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanMakeZeroSizeSecondBlock) {
+ CanMakeZeroSizeSecondBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanMarkBlockUsed) {
+template <typename BlockType>
+void CanMarkBlockUsed() {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
block->MarkUsed();
- EXPECT_EQ(block->Used(), true);
+ EXPECT_TRUE(block->Used());
- // Mark used packs that data into the next pointer. Check that it's still
- // valid
- EXPECT_EQ(block->Next(), (Block*)((uintptr_t)block + kN));
+ // Size should be unaffected.
+ EXPECT_EQ(block->OuterSize(), kN);
block->MarkFree();
- EXPECT_EQ(block->Used(), false);
+ EXPECT_FALSE(block->Used());
+}
+TEST(GenericBlockTest, CanMarkBlockUsed) { CanMarkBlockUsed<Block<>>(); }
+TEST(CustomBlockTest, CanMarkBlockUsed) {
+ CanMarkBlockUsed<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotSplitUsedBlock) {
+template <typename BlockType>
+void CannotSplitUsedBlock() {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ constexpr size_t kSplitN = 512;
+ alignas(BlockType*) byte bytes[kN];
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
block->MarkUsed();
+ result = BlockType::Split(block, kSplitN);
+ EXPECT_EQ(result.status(), Status::FailedPrecondition());
+}
+TEST(GenericBlockTest, CannotSplitUsedBlock) {
+ CannotSplitUsedBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotSplitUsedBlock) {
+ CannotSplitUsedBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanAllocFirstFromAlignedBlock() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSize = 256;
+ constexpr size_t kAlign = 32;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ // Make sure the block's usable space is aligned.
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ size_t pad_inner_size = AlignUp(addr, kAlign) - addr;
+ if (pad_inner_size != 0) {
+ if (pad_inner_size < BlockType::kHeaderSize) {
+ pad_inner_size += kAlign;
+ }
+ pad_inner_size -= BlockType::kHeaderSize;
+ result = BlockType::Split(block, pad_inner_size);
+ EXPECT_EQ(result.status(), OkStatus());
+ block = *result;
+ }
+
+ // Allocate from the front of the block.
+ BlockType* prev = block->Prev();
+ EXPECT_EQ(BlockType::AllocFirst(block, kSize, kAlign), OkStatus());
+ EXPECT_EQ(block->InnerSize(), kSize);
+ addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ EXPECT_EQ(addr % kAlign, 0U);
+ EXPECT_TRUE(block->Used());
+
+ // No new padding block was allocated.
+ EXPECT_EQ(prev, block->Prev());
+
+ // Extra was split from the end of the block.
+ EXPECT_FALSE(block->Last());
+}
+TEST(GenericBlockTest, CanAllocFirstFromAlignedBlock) {
+ CanAllocFirstFromAlignedBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanAllocFirstFromAlignedBlock) {
+ CanAllocFirstFromAlignedBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanAllocFirstFromUnalignedBlock() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSize = 256;
+ constexpr size_t kAlign = 32;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ // Make sure the block's usable space is not aligned.
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ size_t pad_inner_size = AlignUp(addr, kAlign) - addr + (kAlign / 2);
+ if (pad_inner_size < BlockType::kHeaderSize) {
+ pad_inner_size += kAlign;
+ }
+ pad_inner_size -= BlockType::kHeaderSize;
+ result = BlockType::Split(block, pad_inner_size);
+ EXPECT_EQ(result.status(), OkStatus());
+ block = *result;
+
+ // Allocate from the front of the block.
+ BlockType* prev = block->Prev();
+ EXPECT_EQ(BlockType::AllocFirst(block, kSize, kAlign), OkStatus());
+ EXPECT_EQ(block->InnerSize(), kSize);
+ addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ EXPECT_EQ(addr % kAlign, 0U);
+ EXPECT_TRUE(block->Used());
+
+ // A new padding block was allocated.
+ EXPECT_LT(prev, block->Prev());
+
+ // Extra was split from the end of the block.
+ EXPECT_FALSE(block->Last());
+}
+TEST(GenericBlockTest, CanAllocFirstFromUnalignedBlock) {
+ CanAllocFirstFromUnalignedBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanAllocFirstFromUnalignedBlock) {
+ CanAllocFirstFromUnalignedBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CannotAllocFirstTooSmallBlock() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kAlign = 32;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ // Make sure the block's usable space is not aligned.
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ size_t pad_inner_size = AlignUp(addr, kAlign) - addr + (kAlign / 2);
+ if (pad_inner_size < BlockType::kHeaderSize) {
+ pad_inner_size += kAlign;
+ }
+ pad_inner_size -= BlockType::kHeaderSize;
+ result = BlockType::Split(block, pad_inner_size);
+ EXPECT_EQ(result.status(), OkStatus());
+ block = *result;
+
+ // Cannot allocate without room to a split a block for alignment.
+ EXPECT_EQ(BlockType::AllocFirst(block, block->InnerSize(), kAlign),
+ Status::OutOfRange());
+}
+TEST(GenericBlockTest, CannotAllocFirstTooSmallBlock) {
+ CannotAllocFirstTooSmallBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotAllocFirstTooSmallBlock) {
+ CannotAllocFirstTooSmallBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanAllocLast() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSize = 256;
+ constexpr size_t kAlign = 32;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ // Allocate from the back of the block.
+ EXPECT_EQ(BlockType::AllocLast(block, kSize, kAlign), OkStatus());
+ EXPECT_GE(block->InnerSize(), kSize);
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ EXPECT_EQ(addr % kAlign, 0U);
+ EXPECT_TRUE(block->Used());
+
+ // Extra was split from the front of the block.
+ EXPECT_FALSE(block->Prev()->Used());
+ EXPECT_TRUE(block->Last());
+}
+TEST(GenericBlockTest, CanAllocLast) { CanAllocLast<Block<>>(); }
+TEST(CustomBlockTest, CanAllocLast) {
+ CanAllocLast<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CannotAllocLastFromTooSmallBlock() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kAlign = 32;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ // Make sure the block's usable space is not aligned.
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ size_t pad_inner_size = AlignUp(addr, kAlign) - addr + (kAlign / 2);
+ if (pad_inner_size < BlockType::kHeaderSize) {
+ pad_inner_size += kAlign;
+ }
+ pad_inner_size -= BlockType::kHeaderSize;
+ result = BlockType::Split(block, pad_inner_size);
+ EXPECT_EQ(result.status(), OkStatus());
+ block = *result;
+
+ // Cannot allocate without room to a split a block for alignment.
+ EXPECT_EQ(BlockType::AllocLast(block, block->InnerSize(), kAlign),
+ Status::ResourceExhausted());
+}
- Block* next_block = nullptr;
- auto status = block->Split(512, &next_block);
- EXPECT_EQ(status, Status::FailedPrecondition());
+TEST(GenericBlockTest, CannotAllocLastFromTooSmallBlock) {
+ CannotAllocLastFromTooSmallBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotAllocLastFromTooSmallBlock) {
+ CannotAllocLastFromTooSmallBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanMergeWithNextBlock) {
+template <typename BlockType>
+void CanMergeWithNextBlock() {
// Do the three way merge from "CanSplitMidBlock", and let's
// merge block 3 and 2
constexpr size_t kN = 1024;
constexpr size_t kSplit1 = 512;
constexpr size_t kSplit2 = 256;
- alignas(Block*) byte bytes[kN];
-
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ alignas(BlockType*) byte bytes[kN];
- Block* block2 = nullptr;
- ASSERT_EQ(OkStatus(), block->Split(kSplit1, &block2));
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* block3 = nullptr;
- ASSERT_EQ(OkStatus(), block->Split(kSplit2, &block3));
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
- EXPECT_EQ(block3->MergeNext(), OkStatus());
+ result = BlockType::Split(block1, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
- EXPECT_EQ(block->Next(), block3);
- EXPECT_EQ(block3->Prev(), block);
- EXPECT_EQ(block->InnerSize(), kSplit2);
+ EXPECT_EQ(BlockType::MergeNext(block3), OkStatus());
- // The resulting "right hand" block should have an outer size of 1024 - 256 -
- // sizeof(Block) - 2*PW_ALLOCATOR_POISON_OFFSET, which accounts for the first
- // block.
- EXPECT_EQ(block3->OuterSize(),
- kN - kSplit2 - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET);
+ EXPECT_EQ(block1->Next(), block3);
+ EXPECT_EQ(block3->Prev(), block1);
+ EXPECT_EQ(block1->InnerSize(), kSplit2);
+ EXPECT_EQ(block3->OuterSize(), kN - block1->OuterSize());
+}
+TEST(GenericBlockTest, CanMergeWithNextBlock) {
+ CanMergeWithNextBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanMergeWithNextBlock) {
+ CanMergeWithNextBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotMergeWithFirstOrLastBlock) {
+template <typename BlockType>
+void CannotMergeWithFirstOrLastBlock() {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ constexpr size_t kSplitN = 512;
+ alignas(BlockType*) byte bytes[kN];
// Do a split, just to check that the checks on Next/Prev are
// different...
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplitN);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
- Block* next_block = nullptr;
- ASSERT_EQ(OkStatus(), block->Split(512, &next_block));
+ EXPECT_EQ(BlockType::MergeNext(block2), Status::OutOfRange());
- EXPECT_EQ(next_block->MergeNext(), Status::OutOfRange());
- EXPECT_EQ(block->MergePrev(), Status::OutOfRange());
+ BlockType* block0 = block1->Prev();
+ EXPECT_EQ(BlockType::MergeNext(block0), Status::OutOfRange());
+}
+TEST(GenericBlockTest, CannotMergeWithFirstOrLastBlock) {
+ CannotMergeWithFirstOrLastBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotMergeWithFirstOrLastBlock) {
+ CannotMergeWithFirstOrLastBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CannotMergeUsedBlock) {
+template <typename BlockType>
+void CannotMergeUsedBlock() {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ constexpr size_t kSplitN = 512;
+ alignas(BlockType*) byte bytes[kN];
// Do a split, just to check that the checks on Next/Prev are
// different...
- Block* block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ result = BlockType::Split(block, kSplitN);
+ ASSERT_EQ(result.status(), OkStatus());
+
+ block->MarkUsed();
+ EXPECT_EQ(BlockType::MergeNext(block), Status::FailedPrecondition());
+}
+TEST(GenericBlockTest, CannotMergeUsedBlock) {
+ CannotMergeUsedBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotMergeUsedBlock) {
+ CannotMergeUsedBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanFreeSingleBlock() {
+ constexpr size_t kN = 1024;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ block->MarkUsed();
+ BlockType::Free(block);
+ EXPECT_FALSE(block->Used());
+ EXPECT_EQ(block->OuterSize(), kN);
+}
+TEST(GenericBlockTest, CanFreeSingleBlock) { CanFreeSingleBlock<Block<>>(); }
+TEST(CustomBlockTest, CanFreeSingleBlock) {
+ CanFreeSingleBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanFreeBlockWithoutMerging() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 256;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
+
+ block1->MarkUsed();
+ block2->MarkUsed();
+ block3->MarkUsed();
+
+ BlockType::Free(block2);
+ EXPECT_FALSE(block2->Used());
+ EXPECT_NE(block2->Prev(), nullptr);
+ EXPECT_FALSE(block2->Last());
+}
+TEST(GenericBlockTest, CanFreeBlockWithoutMerging) {
+ CanFreeBlockWithoutMerging<Block<>>();
+}
+TEST(CustomBlockTest, CanFreeBlockWithoutMerging) {
+ CanFreeBlockWithoutMerging<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanFreeBlockAndMergeWithPrev() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 256;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
+
+ block2->MarkUsed();
+ block3->MarkUsed();
+
+ BlockType::Free(block2);
+ EXPECT_FALSE(block2->Used());
+ EXPECT_EQ(block2->Prev(), nullptr);
+ EXPECT_FALSE(block2->Last());
+}
+TEST(GenericBlockTest, CanFreeBlockAndMergeWithPrev) {
+ CanFreeBlockAndMergeWithPrev<Block<>>();
+}
+TEST(CustomBlockTest, CanFreeBlockAndMergeWithPrev) {
+ CanFreeBlockAndMergeWithPrev<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanFreeBlockAndMergeWithNext() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 256;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+
+ block1->MarkUsed();
+ block2->MarkUsed();
+
+ BlockType::Free(block2);
+ EXPECT_FALSE(block2->Used());
+ EXPECT_NE(block2->Prev(), nullptr);
+ EXPECT_TRUE(block2->Last());
+}
+TEST(GenericBlockTest, CanFreeBlockAndMergeWithNext) {
+ CanFreeBlockAndMergeWithNext<Block<>>();
+}
+TEST(CustomBlockTest, CanFreeBlockAndMergeWithNext) {
+ CanFreeBlockAndMergeWithNext<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanFreeUsedBlockAndMergeWithBoth() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 256;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* next_block = nullptr;
- ASSERT_EQ(OkStatus(), block->Split(512, &next_block));
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+
+ block2->MarkUsed();
+
+ BlockType::Free(block2);
+ EXPECT_FALSE(block2->Used());
+ EXPECT_EQ(block2->Prev(), nullptr);
+ EXPECT_TRUE(block2->Last());
+}
+TEST(GenericBlockTest, CanFreeUsedBlockAndMergeWithBoth) {
+ CanFreeUsedBlockAndMergeWithBoth<Block<>>();
+}
+TEST(CustomBlockTest, CanFreeUsedBlockAndMergeWithBoth) {
+ CanFreeUsedBlockAndMergeWithBoth<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanResizeBlockSameSize() {
+ constexpr size_t kN = 1024;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
block->MarkUsed();
- EXPECT_EQ(block->MergeNext(), Status::FailedPrecondition());
- EXPECT_EQ(next_block->MergePrev(), Status::FailedPrecondition());
+ EXPECT_EQ(BlockType::Resize(block, block->InnerSize()), OkStatus());
+}
+TEST(GenericBlockTest, CanResizeBlockSameSize) {
+ CanResizeBlockSameSize<Block<>>();
+}
+TEST(CustomBlockTest, CanResizeBlockSameSize) {
+ CanResizeBlockSameSize<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CannotResizeFreeBlock() {
+ constexpr size_t kN = 1024;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ EXPECT_EQ(BlockType::Resize(block, block->InnerSize()),
+ Status::FailedPrecondition());
+}
+TEST(GenericBlockTest, CannotResizeFreeBlock) {
+ CannotResizeFreeBlock<Block<>>();
+}
+TEST(CustomBlockTest, CannotResizeFreeBlock) {
+ CannotResizeFreeBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanResizeBlockSmallerWithNextFree() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ block1->MarkUsed();
+ size_t block2_inner_size = block2->InnerSize();
+
+ // Shrink by less than the minimum needed for a block. The extra should be
+ // added to the subsequent block.
+ size_t delta = BlockType::kBlockOverhead / 2;
+ size_t new_inner_size = block1->InnerSize() - delta;
+ EXPECT_EQ(BlockType::Resize(block1, new_inner_size), OkStatus());
+ EXPECT_EQ(block1->InnerSize(), new_inner_size);
+
+ block2 = block1->Next();
+ EXPECT_GE(block2->InnerSize(), block2_inner_size + delta);
+}
+TEST(GenericBlockTest, CanResizeBlockSmallerWithNextFree) {
+ CanResizeBlockSmallerWithNextFree<Block<>>();
+}
+TEST(CustomBlockTest, CanResizeBlockSmallerWithNextFree) {
+ CanResizeBlockSmallerWithNextFree<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanResizeBlockLargerWithNextFree() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ block1->MarkUsed();
+ size_t block2_inner_size = block2->InnerSize();
+
+ // Grow by less than the minimum needed for a block. The extra should be
+ // added to the subsequent block.
+ size_t delta = BlockType::kBlockOverhead / 2;
+ size_t new_inner_size = block1->InnerSize() + delta;
+ EXPECT_EQ(BlockType::Resize(block1, new_inner_size), OkStatus());
+ EXPECT_EQ(block1->InnerSize(), new_inner_size);
+
+ block2 = block1->Next();
+ EXPECT_GE(block2->InnerSize(), block2_inner_size - delta);
+}
+TEST(GenericBlockTest, CanResizeBlockLargerWithNextFree) {
+ CanResizeBlockLargerWithNextFree<Block<>>();
+}
+TEST(CustomBlockTest, CanResizeBlockLargerWithNextFree) {
+ CanResizeBlockLargerWithNextFree<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CannotResizeBlockMuchLargerWithNextFree() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 256;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
+
+ block1->MarkUsed();
+ block3->MarkUsed();
+
+ size_t new_inner_size = block1->InnerSize() + block2->OuterSize() + 1;
+ EXPECT_EQ(BlockType::Resize(block1, new_inner_size), Status::OutOfRange());
+}
+TEST(GenericBlockTest, CannotResizeBlockMuchLargerWithNextFree) {
+ CannotResizeBlockMuchLargerWithNextFree<Block<>>();
+}
+TEST(CustomBlockTest, CannotResizeBlockMuchLargerWithNextFree) {
+ CannotResizeBlockMuchLargerWithNextFree<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanCheckValidBlock) {
+template <typename BlockType>
+void CanResizeBlockSmallerWithNextUsed() {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ constexpr size_t kSplit1 = 512;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* first_block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &first_block), OkStatus());
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
- Block* second_block = nullptr;
- ASSERT_EQ(OkStatus(), first_block->Split(512, &second_block));
+ block1->MarkUsed();
+ block2->MarkUsed();
- Block* third_block = nullptr;
- ASSERT_EQ(OkStatus(), second_block->Split(256, &third_block));
+ // Shrink the block.
+ size_t delta = kSplit1 / 2;
+ size_t new_inner_size = block1->InnerSize() - delta;
+ EXPECT_EQ(BlockType::Resize(block1, new_inner_size), OkStatus());
+ EXPECT_EQ(block1->InnerSize(), new_inner_size);
- EXPECT_EQ(first_block->IsValid(), true);
- EXPECT_EQ(second_block->IsValid(), true);
- EXPECT_EQ(third_block->IsValid(), true);
+ block2 = block1->Next();
+ EXPECT_EQ(block2->OuterSize(), delta);
+}
+TEST(GenericBlockTest, CanResizeBlockSmallerWithNextUsed) {
+ CanResizeBlockSmallerWithNextUsed<Block<>>();
+}
+TEST(CustomBlockTest, CanResizeBlockSmallerWithNextUsed) {
+ CanResizeBlockSmallerWithNextUsed<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanCheckInalidBlock) {
+template <typename BlockType>
+void CannotResizeBlockLargerWithNextUsed() {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ constexpr size_t kSplit1 = 512;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- Block* first_block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &first_block), OkStatus());
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
- Block* second_block = nullptr;
- ASSERT_EQ(OkStatus(), first_block->Split(512, &second_block));
+ block1->MarkUsed();
+ block2->MarkUsed();
- Block* third_block = nullptr;
- ASSERT_EQ(OkStatus(), second_block->Split(256, &third_block));
+ size_t delta = BlockType::kBlockOverhead / 2;
+ size_t new_inner_size = block1->InnerSize() + delta;
+ EXPECT_EQ(BlockType::Resize(block1, new_inner_size), Status::OutOfRange());
+}
+TEST(GenericBlockTest, CannotResizeBlockLargerWithNextUsed) {
+ CannotResizeBlockLargerWithNextUsed<Block<>>();
+}
+TEST(CustomBlockTest, CannotResizeBlockLargerWithNextUsed) {
+ CannotResizeBlockLargerWithNextUsed<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanCheckValidBlock() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 256;
+ alignas(BlockType*) byte bytes[kN];
- Block* fourth_block = nullptr;
- ASSERT_EQ(OkStatus(), third_block->Split(128, &fourth_block));
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
- std::byte* next_ptr = reinterpret_cast<std::byte*>(first_block);
- memcpy(next_ptr, second_block, sizeof(void*));
- EXPECT_EQ(first_block->IsValid(), false);
- EXPECT_EQ(second_block->IsValid(), false);
- EXPECT_EQ(third_block->IsValid(), true);
- EXPECT_EQ(fourth_block->IsValid(), true);
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
+
+ EXPECT_TRUE(block1->IsValid());
+ block1->CrashIfInvalid();
+
+ EXPECT_TRUE(block2->IsValid());
+ block2->CrashIfInvalid();
+
+ EXPECT_TRUE(block3->IsValid());
+ block3->CrashIfInvalid();
+}
+TEST(GenericBlockTest, CanCheckValidBlock) { CanCheckValidBlock<Block<>>(); }
+TEST(CustomBlockTest, CanCheckValidBlock) {
+ CanCheckValidBlock<Block<uint32_t, kCapacity>>();
+}
+
+template <typename BlockType>
+void CanCheckInvalidBlock() {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplit1 = 512;
+ constexpr size_t kSplit2 = 128;
+ constexpr size_t kSplit3 = 256;
+ alignas(BlockType*) byte bytes[kN];
+ memset(bytes, 0, sizeof(bytes));
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+
+ result = BlockType::Split(block1, kSplit1);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ result = BlockType::Split(block2, kSplit2);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block3 = *result;
+
+ result = BlockType::Split(block3, kSplit3);
+ ASSERT_EQ(result.status(), OkStatus());
#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
- std::byte fault_poison[PW_ALLOCATOR_POISON_OFFSET] = {std::byte(0)};
- std::byte* front_poison =
- reinterpret_cast<std::byte*>(third_block) + sizeof(*third_block);
- memcpy(front_poison, fault_poison, PW_ALLOCATOR_POISON_OFFSET);
- EXPECT_EQ(third_block->IsValid(), false);
-
- std::byte* end_poison =
- reinterpret_cast<std::byte*>(fourth_block) + sizeof(*fourth_block);
- memcpy(end_poison, fault_poison, PW_ALLOCATOR_POISON_OFFSET);
- EXPECT_EQ(fourth_block->IsValid(), false);
+ // Corrupt a byte in the poisoned header.
+ EXPECT_TRUE(block1->IsValid());
+ bytes[BlockType::kHeaderSize - 1] = std::byte(0xFF);
+ EXPECT_FALSE(block1->IsValid());
+
+ // Corrupt a byte in the poisoned footer.
+ EXPECT_TRUE(block2->IsValid());
+ bytes[block1->OuterSize() + block2->OuterSize() - 1] = std::byte(0xFF);
+ EXPECT_FALSE(block2->IsValid());
#endif // PW_ALLOCATOR_POISON_ENABLE
+
+ // Corrupt a Block header.
+ // This must not touch memory outside the original region, or the test may
+ // (correctly) abort when run with address sanitizer.
+ // To remain as agostic to the internals of `Block` as possible, the test
+ // copies a smaller block's header to a larger block.
+ EXPECT_TRUE(block3->IsValid());
+ auto* src = reinterpret_cast<std::byte*>(block2);
+ auto* dst = reinterpret_cast<std::byte*>(block3);
+ std::memcpy(dst, src, sizeof(BlockType));
+ EXPECT_FALSE(block3->IsValid());
+}
+TEST(GenericBlockTest, CanCheckInvalidBlock) {
+ CanCheckInvalidBlock<Block<>>();
+}
+TEST(CustomBlockTest, CanCheckInvalidBlock) {
+ CanCheckInvalidBlock<Block<uint32_t, kCapacity>>();
}
-TEST(Block, CanPoisonBlock) {
-#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
+TEST(CustomBlockTest, CustomFlagsInitiallyZero) {
constexpr size_t kN = 1024;
- alignas(Block*) byte bytes[kN];
+ using BlockType = Block<uint16_t, kN>;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- Block* first_block = nullptr;
- EXPECT_EQ(Block::Init(span(bytes, kN), &first_block), OkStatus());
+ EXPECT_EQ(block->GetFlags(), 0U);
+}
- Block* second_block = nullptr;
- ASSERT_EQ(OkStatus(), first_block->Split(512, &second_block));
+TEST(CustomBlockTest, SetCustomFlags) {
+ constexpr size_t kN = 1024;
+ using BlockType = Block<uint16_t, kN>;
+ alignas(BlockType*) byte bytes[kN];
- Block* third_block = nullptr;
- ASSERT_EQ(OkStatus(), second_block->Split(256, &third_block));
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
- EXPECT_EQ(first_block->IsValid(), true);
- EXPECT_EQ(second_block->IsValid(), true);
- EXPECT_EQ(third_block->IsValid(), true);
-#endif // PW_ALLOCATOR_POISON_ENABLE
+ block->SetFlags(1);
+ EXPECT_EQ(block->GetFlags(), 1U);
+}
+
+TEST(CustomBlockTest, SetAllCustomFlags) {
+ constexpr size_t kN = 1024;
+ using BlockType = Block<uint16_t, kN>;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ // `1024/alignof(uint16_t)` is `0x200`, which leaves 6 bits available for
+ // flags per offset field. After 1 builtin field, this leaves 2*5 available
+ // for custom flags.
+ block->SetFlags((uint16_t(1) << 10) - 1);
+ EXPECT_EQ(block->GetFlags(), 0x3FFU);
+}
+
+TEST(CustomBlockTest, ClearCustomFlags) {
+ constexpr size_t kN = 1024;
+ using BlockType = Block<uint16_t, kN>;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ block->SetFlags(0x155);
+ block->SetFlags(0x2AA, 0x333);
+ EXPECT_EQ(block->GetFlags(), 0x2EEU);
+}
+
+TEST(CustomBlockTest, FlagsNotCopiedOnSplit) {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplitN = 512;
+ using BlockType = Block<uint16_t, kN>;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block1 = *result;
+ block1->SetFlags(0x137);
+
+ result = BlockType::Split(block1, kSplitN);
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block2 = *result;
+
+ EXPECT_EQ(block1->GetFlags(), 0x137U);
+ EXPECT_EQ(block2->GetFlags(), 0U);
+}
+
+TEST(CustomBlockTest, FlagsPreservedByMergeNext) {
+ constexpr size_t kN = 1024;
+ constexpr size_t kSplitN = 512;
+ using BlockType = Block<uint16_t, kN>;
+ alignas(BlockType*) byte bytes[kN];
+
+ Result<BlockType*> result = BlockType::Init(span(bytes, kN));
+ ASSERT_EQ(result.status(), OkStatus());
+ BlockType* block = *result;
+
+ result = BlockType::Split(block, kSplitN);
+ ASSERT_EQ(result.status(), OkStatus());
+
+ block->SetFlags(0x137);
+ EXPECT_EQ(BlockType::MergeNext(block), OkStatus());
+ EXPECT_EQ(block->GetFlags(), 0x137U);
}
} // namespace pw::allocator
diff --git a/pw_allocator/docs.rst b/pw_allocator/docs.rst
index 81dcb06cf..0d5545ccf 100644
--- a/pw_allocator/docs.rst
+++ b/pw_allocator/docs.rst
@@ -7,19 +7,58 @@ pw_allocator
This module provides various building blocks
for a dynamic allocator. This is composed of the following parts:
-- ``block``: An implementation of a linked list of memory blocks, supporting
- splitting and merging of blocks.
+- ``block``: An implementation of a doubly-linked list of memory blocks,
+ supporting splitting and merging of blocks.
- ``freelist``: A freelist, suitable for fast lookups of available memory chunks
(i.e. ``block`` s).
-
-Heap Integrity Check
-====================
-The ``Block`` class provides two check functions:
-
-- ``bool Block::IsValid()``: Returns ``true`` is the given block is valid and
- ``false`` otherwise.
-- ``void Block::CrashIfInvalid()``: Crash the program and output the reason why
- the check fails using ``PW_DCHECK``.
+- ``allocator``: An interface for memory allocators. Several concrete
+ implementations are also provided.
+
+Block
+=====
+.. doxygenclass:: pw::allocator::Block
+ :members:
+
+FreeList
+========
+.. doxygenclass:: pw::allocator::FreeList
+ :members:
+
+Allocator
+=========
+.. doxygenclass:: pw::allocator::Allocator
+ :members:
+
+Example
+-------
+As an example, the following implements a simple allocator that tracks memory
+using ``Block``.
+
+.. literalinclude:: public/pw_allocator/simple_allocator.h
+ :language: cpp
+ :linenos:
+ :start-after: [pw_allocator_examples_simple_allocator]
+ :end-before: [pw_allocator_examples_simple_allocator]
+
+Other Implemetations
+--------------------
+Provided implementations of the ``Allocator`` interface include:
+
+- ``AllocatorMetricProxy``: Wraps another allocator and records its usage.
+- ``FallbackAllocator``: Dispatches first to a primary allocator, and, if that
+ fails, to a secondary alloator.
+- ``LibCAllocator``: Uses ``malloc``, ``realloc``, and ``free``. This should
+ only be used if the ``libc`` in use provides those functions.
+- ``NullAllocator``: Always fails. This may be useful if allocations should be
+ disallowed under specific circumstances.
+- ``SplitFreeListAllocator``: Tracks memory using ``Block``, and splits large
+ and small allocations between the front and back, respectively, of it memory
+ region in order to reduce fragmentation.
+
+UniquePtr
+=========
+.. doxygenclass:: pw::allocator::UniquePtr
+ :members:
Heap Poisoning
==============
@@ -28,7 +67,7 @@ By default, this module disables heap poisoning since it requires extra space.
User can enable heap poisoning by enabling the ``pw_allocator_POISON_HEAP``
build arg.
-.. code:: sh
+.. code-block:: sh
$ gn args out
# Modify and save the args file to use heap poison.
@@ -57,7 +96,7 @@ Usage
The heap visualizer can be launched from a shell using the Pigweed environment.
-.. code:: sh
+.. code-block:: sh
$ pw heap-viewer --dump-file <directory of dump file> --heap-low-address
<hex address of heap lower address> --heap-high-address <hex address of heap
@@ -71,7 +110,7 @@ The required arguments are:
represented as ``f <memory address>``. For example, a dump file should look
like:
- .. code:: sh
+ .. code-block:: sh
m 20 0x20004450 # malloc 20 bytes, the pointer is 0x20004450
m 8 0x2000447c # malloc 8 bytes, the pointer is 0x2000447c
@@ -82,13 +121,13 @@ The required arguments are:
- ``--heap-low-address`` is the start of the heap. For example:
- .. code:: sh
+ .. code-block:: sh
--heap-low-address 0x20004440
- ``--heap-high-address`` is the end of the heap. For example:
- .. code:: sh
+ .. code-block:: sh
--heap-high-address 0x20006040
@@ -101,5 +140,11 @@ Options include the following:
- ``--pointer-size <integer of pointer size>``: The size of a pointer on the
machine where ``malloc/free`` is called. The default value is ``4``.
-Note, this module, and its documentation, is currently incomplete and
-experimental.
+.. _module-pw_allocator-size:
+
+Size report
+===========
+``pw_allocator`` provides some of its own implementations of the ``Allocator``
+interface, whos costs are shown below.
+
+.. include:: allocator_size_report
diff --git a/pw_allocator/fallback_allocator.cc b/pw_allocator/fallback_allocator.cc
new file mode 100644
index 000000000..1e3faf666
--- /dev/null
+++ b/pw_allocator/fallback_allocator.cc
@@ -0,0 +1,55 @@
+
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/fallback_allocator.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::allocator {
+
+void FallbackAllocator::Initialize(Allocator& primary, Allocator& secondary) {
+ primary_ = &primary;
+ secondary_ = &secondary;
+}
+
+Status FallbackAllocator::DoQuery(const void* ptr, Layout layout) const {
+ PW_DCHECK(primary_ != nullptr && secondary_ != nullptr);
+ auto status = primary_->Query(ptr, layout);
+ return status.ok() ? status : secondary_->Query(ptr, layout);
+}
+
+void* FallbackAllocator::DoAllocate(Layout layout) {
+ PW_DCHECK(primary_ != nullptr && secondary_ != nullptr);
+ void* ptr = primary_->Allocate(layout);
+ return ptr != nullptr ? ptr : secondary_->Allocate(layout);
+}
+
+void FallbackAllocator::DoDeallocate(void* ptr, Layout layout) {
+ PW_DCHECK(primary_ != nullptr && secondary_ != nullptr);
+ if (primary_->Query(ptr, layout).ok()) {
+ primary_->Deallocate(ptr, layout);
+ } else {
+ secondary_->Deallocate(ptr, layout);
+ }
+}
+
+bool FallbackAllocator::DoResize(void* ptr, Layout layout, size_t new_size) {
+ PW_DCHECK(primary_ != nullptr && secondary_ != nullptr);
+ return primary_->Query(ptr, layout).ok()
+ ? primary_->Resize(ptr, layout, new_size)
+ : secondary_->Resize(ptr, layout, new_size);
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/fallback_allocator_test.cc b/pw_allocator/fallback_allocator_test.cc
new file mode 100644
index 000000000..c623fa652
--- /dev/null
+++ b/pw_allocator/fallback_allocator_test.cc
@@ -0,0 +1,221 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/fallback_allocator.h"
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator_testing.h"
+#include "pw_status/status.h"
+
+namespace pw::allocator {
+namespace {
+
+// Test fixtures.
+
+class FallbackAllocatorTest : public ::testing::Test {
+ protected:
+ void SetUp() override { allocator.Initialize(*primary, *secondary); }
+
+ void TearDown() override {
+ primary->DeallocateAll();
+ secondary->DeallocateAll();
+ }
+
+ test::AllocatorForTestWithBuffer<128> primary;
+ test::AllocatorForTestWithBuffer<128> secondary;
+ FallbackAllocator allocator;
+};
+
+// Unit tests.
+
+TEST_F(FallbackAllocatorTest, QueryValidPrimary) {
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = primary->Allocate(layout);
+ EXPECT_TRUE(primary->Query(ptr, layout).ok());
+ EXPECT_EQ(secondary->Query(ptr, layout), Status::OutOfRange());
+ EXPECT_TRUE(allocator.Query(ptr, layout).ok());
+}
+
+TEST_F(FallbackAllocatorTest, QueryValidSecondary) {
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = secondary->Allocate(layout);
+ EXPECT_FALSE(primary->Query(ptr, layout).ok());
+ EXPECT_TRUE(secondary->Query(ptr, layout).ok());
+ EXPECT_TRUE(allocator.Query(ptr, layout).ok());
+}
+
+TEST_F(FallbackAllocatorTest, QueryInvalidPtr) {
+ std::array<std::byte, 128> buffer = {};
+ test::AllocatorForTest other;
+ ASSERT_EQ(other.Init(buffer), OkStatus());
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = other.Allocate(layout);
+ EXPECT_FALSE(primary->Query(ptr, layout).ok());
+ EXPECT_FALSE(secondary->Query(ptr, layout).ok());
+ EXPECT_FALSE(allocator.Query(ptr, layout).ok());
+ other.DeallocateAll();
+}
+
+TEST_F(FallbackAllocatorTest, AllocateFromPrimary) {
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(layout);
+ EXPECT_NE(ptr, nullptr);
+ EXPECT_EQ(primary->allocate_size(), layout.size());
+ EXPECT_EQ(secondary->allocate_size(), 0U);
+}
+
+TEST_F(FallbackAllocatorTest, AllocateFromSecondary) {
+ primary->Exhaust();
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(layout);
+ EXPECT_NE(ptr, nullptr);
+ EXPECT_EQ(primary->allocate_size(), layout.size());
+ EXPECT_EQ(secondary->allocate_size(), layout.size());
+}
+
+TEST_F(FallbackAllocatorTest, AllocateFailure) {
+ Layout layout = Layout::Of<uint32_t[0x10000]>();
+ void* ptr = allocator.Allocate(layout);
+ EXPECT_EQ(ptr, nullptr);
+ EXPECT_EQ(primary->allocate_size(), layout.size());
+ EXPECT_EQ(secondary->allocate_size(), layout.size());
+}
+
+TEST_F(FallbackAllocatorTest, DeallocateUsingPrimary) {
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(layout);
+ ASSERT_NE(ptr, nullptr);
+ allocator.Deallocate(ptr, layout);
+ EXPECT_EQ(primary->deallocate_ptr(), ptr);
+ EXPECT_EQ(primary->deallocate_size(), layout.size());
+ EXPECT_EQ(secondary->deallocate_ptr(), nullptr);
+ EXPECT_EQ(secondary->deallocate_size(), 0U);
+}
+
+TEST_F(FallbackAllocatorTest, DeallocateUsingSecondary) {
+ primary->Exhaust();
+ Layout layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(layout);
+ ASSERT_NE(ptr, nullptr);
+ allocator.Deallocate(ptr, layout);
+ EXPECT_EQ(primary->deallocate_ptr(), nullptr);
+ EXPECT_EQ(primary->deallocate_size(), 0U);
+ EXPECT_EQ(secondary->deallocate_ptr(), ptr);
+ EXPECT_EQ(secondary->deallocate_size(), layout.size());
+}
+
+TEST_F(FallbackAllocatorTest, ResizePrimary) {
+ Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+
+ size_t new_size = sizeof(uint32_t[3]);
+ EXPECT_TRUE(allocator.Resize(ptr, old_layout, new_size));
+ EXPECT_EQ(primary->resize_ptr(), ptr);
+ EXPECT_EQ(primary->resize_old_size(), old_layout.size());
+ EXPECT_EQ(primary->resize_new_size(), new_size);
+
+ // Secondary should not be touched.
+ EXPECT_EQ(secondary->resize_ptr(), nullptr);
+ EXPECT_EQ(secondary->resize_old_size(), 0U);
+ EXPECT_EQ(secondary->resize_new_size(), 0U);
+}
+
+TEST_F(FallbackAllocatorTest, ResizePrimaryFailure) {
+ Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+ primary->Exhaust();
+
+ size_t new_size = sizeof(uint32_t[3]);
+ EXPECT_FALSE(allocator.Resize(ptr, old_layout, new_size));
+ EXPECT_EQ(primary->resize_ptr(), ptr);
+ EXPECT_EQ(primary->resize_old_size(), old_layout.size());
+ EXPECT_EQ(primary->resize_new_size(), new_size);
+
+ // Secondary should not be touched.
+ EXPECT_EQ(secondary->resize_ptr(), nullptr);
+ EXPECT_EQ(secondary->resize_old_size(), 0U);
+ EXPECT_EQ(secondary->resize_new_size(), 0U);
+}
+
+TEST_F(FallbackAllocatorTest, ResizeSecondary) {
+ primary->Exhaust();
+ Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+
+ size_t new_size = sizeof(uint32_t[3]);
+ EXPECT_TRUE(allocator.Resize(ptr, old_layout, new_size));
+ EXPECT_EQ(secondary->resize_ptr(), ptr);
+ EXPECT_EQ(secondary->resize_old_size(), old_layout.size());
+ EXPECT_EQ(secondary->resize_new_size(), new_size);
+
+ // Primary should not be touched.
+ EXPECT_EQ(primary->resize_ptr(), nullptr);
+ EXPECT_EQ(primary->resize_old_size(), 0U);
+ EXPECT_EQ(primary->resize_new_size(), 0U);
+}
+
+TEST_F(FallbackAllocatorTest, ResizeSecondaryFailure) {
+ primary->Exhaust();
+ Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+ secondary->Exhaust();
+
+ size_t new_size = sizeof(uint32_t[3]);
+ EXPECT_FALSE(allocator.Resize(ptr, old_layout, new_size));
+ EXPECT_EQ(secondary->resize_ptr(), ptr);
+ EXPECT_EQ(secondary->resize_old_size(), old_layout.size());
+ EXPECT_EQ(secondary->resize_new_size(), new_size);
+
+ // Primary should not be touched.
+ EXPECT_EQ(primary->resize_ptr(), nullptr);
+ EXPECT_EQ(primary->resize_old_size(), 0U);
+ EXPECT_EQ(primary->resize_new_size(), 0U);
+}
+
+TEST_F(FallbackAllocatorTest, ReallocateSameAllocator) {
+ Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr1 = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr1, nullptr);
+
+ // Claim subsequent memeory to force reallocation.
+ void* ptr2 = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr2, nullptr);
+
+ size_t new_size = sizeof(uint32_t[3]);
+ void* new_ptr = allocator.Reallocate(ptr1, old_layout, new_size);
+ EXPECT_NE(new_ptr, nullptr);
+ EXPECT_EQ(primary->deallocate_ptr(), ptr1);
+ EXPECT_EQ(primary->deallocate_size(), old_layout.size());
+ EXPECT_EQ(primary->allocate_size(), new_size);
+}
+
+TEST_F(FallbackAllocatorTest, ReallocateDifferentAllocator) {
+ Layout old_layout = Layout::Of<uint32_t>();
+ void* ptr = allocator.Allocate(old_layout);
+ primary->Exhaust();
+
+ size_t new_size = sizeof(uint32_t[3]);
+ void* new_ptr = allocator.Reallocate(ptr, old_layout, new_size);
+ EXPECT_NE(new_ptr, nullptr);
+ EXPECT_EQ(primary->deallocate_ptr(), ptr);
+ EXPECT_EQ(primary->deallocate_size(), old_layout.size());
+ EXPECT_EQ(secondary->allocate_size(), new_size);
+}
+
+} // namespace
+} // namespace pw::allocator
diff --git a/pw_allocator/freelist_heap.cc b/pw_allocator/freelist_heap.cc
index fcb8f3c50..c57eeba6a 100644
--- a/pw_allocator/freelist_heap.cc
+++ b/pw_allocator/freelist_heap.cc
@@ -23,13 +23,15 @@ namespace pw::allocator {
FreeListHeap::FreeListHeap(span<std::byte> region, FreeList& freelist)
: freelist_(freelist), heap_stats_() {
- Block* block;
+ auto result = BlockType::Init(region);
PW_CHECK_OK(
- Block::Init(region, &block),
+ result.status(),
"Failed to initialize FreeListHeap region; misaligned or too small");
+ BlockType* block = *result;
+ block->CrashIfInvalid();
freelist_.AddChunk(BlockToSpan(block))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
region_ = region;
heap_stats_.total_bytes = region.size();
@@ -44,18 +46,17 @@ void* FreeListHeap::Allocate(size_t size) {
return nullptr;
}
freelist_.RemoveChunk(chunk)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
- Block* chunk_block = Block::FromUsableSpace(chunk.data());
+ BlockType* chunk_block = BlockType::FromUsableSpace(chunk.data());
chunk_block->CrashIfInvalid();
// Split that chunk. If there's a leftover chunk, add it to the freelist
- Block* leftover;
- auto status = chunk_block->Split(size, &leftover);
- if (status == PW_STATUS_OK) {
- freelist_.AddChunk(BlockToSpan(leftover))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ Result<BlockType*> result = BlockType::Split(chunk_block, size);
+ if (result.ok()) {
+ freelist_.AddChunk(BlockToSpan(*result))
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
chunk_block->MarkUsed();
@@ -75,7 +76,7 @@ void FreeListHeap::Free(void* ptr) {
return;
}
- Block* chunk_block = Block::FromUsableSpace(bytes);
+ BlockType* chunk_block = BlockType::FromUsableSpace(bytes);
chunk_block->CrashIfInvalid();
size_t size_freed = chunk_block->InnerSize();
@@ -86,8 +87,8 @@ void FreeListHeap::Free(void* ptr) {
}
chunk_block->MarkFree();
// Can we combine with the left or right blocks?
- Block* prev = chunk_block->Prev();
- Block* next = nullptr;
+ BlockType* prev = chunk_block->Prev();
+ BlockType* next = nullptr;
if (!chunk_block->Last()) {
next = chunk_block->Next();
@@ -96,23 +97,21 @@ void FreeListHeap::Free(void* ptr) {
if (prev != nullptr && !prev->Used()) {
// Remove from freelist and merge
freelist_.RemoveChunk(BlockToSpan(prev))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
- chunk_block->MergePrev()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
-
- // chunk_block is now invalid; prev now encompasses it.
- chunk_block = prev;
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
+ chunk_block = chunk_block->Prev();
+ BlockType::MergeNext(chunk_block)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
if (next != nullptr && !next->Used()) {
freelist_.RemoveChunk(BlockToSpan(next))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
- chunk_block->MergeNext()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
+ BlockType::MergeNext(chunk_block)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
// Add back to the freelist
freelist_.AddChunk(BlockToSpan(chunk_block))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
heap_stats_.bytes_allocated -= size_freed;
heap_stats_.cumulative_freed += size_freed;
@@ -139,7 +138,7 @@ void* FreeListHeap::Realloc(void* ptr, size_t size) {
return nullptr;
}
- Block* chunk_block = Block::FromUsableSpace(bytes);
+ BlockType* chunk_block = BlockType::FromUsableSpace(bytes);
if (!chunk_block->Used()) {
return nullptr;
}
diff --git a/pw_allocator/freelist_heap_test.cc b/pw_allocator/freelist_heap_test.cc
index 0d3353c3b..de8f2047e 100644
--- a/pw_allocator/freelist_heap_test.cc
+++ b/pw_allocator/freelist_heap_test.cc
@@ -22,7 +22,7 @@ namespace pw::allocator {
TEST(FreeListHeap, CanAllocate) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -30,13 +30,13 @@ TEST(FreeListHeap, CanAllocate) {
ASSERT_NE(ptr, nullptr);
// In this case, the allocator should be returning us the start of the chunk.
- EXPECT_EQ(ptr, &buf[0] + sizeof(Block) + PW_ALLOCATOR_POISON_OFFSET);
+ EXPECT_EQ(ptr, &buf[0] + FreeListHeap::BlockType::kHeaderSize);
}
TEST(FreeListHeap, AllocationsDontOverlap) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -58,7 +58,7 @@ TEST(FreeListHeap, CanFreeAndRealloc) {
// and get that value back again.
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -71,7 +71,7 @@ TEST(FreeListHeap, CanFreeAndRealloc) {
TEST(FreeListHeap, ReturnsNullWhenAllocationTooLarge) {
constexpr size_t N = 2048;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -80,19 +80,18 @@ TEST(FreeListHeap, ReturnsNullWhenAllocationTooLarge) {
TEST(FreeListHeap, ReturnsNullWhenFull) {
constexpr size_t N = 2048;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
- EXPECT_NE(
- allocator.Allocate(N - sizeof(Block) - 2 * PW_ALLOCATOR_POISON_OFFSET),
- nullptr);
+ EXPECT_NE(allocator.Allocate(N - FreeListHeap::BlockType::kBlockOverhead),
+ nullptr);
EXPECT_EQ(allocator.Allocate(1), nullptr);
}
TEST(FreeListHeap, ReturnedPointersAreAligned) {
constexpr size_t N = 2048;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -118,7 +117,7 @@ TEST(FreeListHeap, CannotFreeNonOwnedPointer) {
// We can cheat; create a heap, allocate it all, and try and return something
// random to it. Try allocating again, and check that we get nullptr back.
constexpr size_t N = 2048;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -145,7 +144,7 @@ TEST(FreeListHeap, CanRealloc) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
constexpr size_t kNewAllocSize = 768;
- alignas(Block) std::byte buf[N] = {std::byte(1)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(1)};
FreeListHeapBuffer allocator(buf);
@@ -160,7 +159,7 @@ TEST(FreeListHeap, ReallocHasSameContent) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = sizeof(int);
constexpr size_t kNewAllocSize = sizeof(int) * 2;
- alignas(Block) std::byte buf[N] = {std::byte(1)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(1)};
// Data inside the allocated block.
std::byte data1[kAllocSize];
// Data inside the reallocated block.
@@ -184,7 +183,7 @@ TEST(FreeListHeap, ReturnsNullReallocFreedPointer) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
constexpr size_t kNewAllocSize = 256;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -199,7 +198,7 @@ TEST(FreeListHeap, ReallocSmallerSize) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
constexpr size_t kNewAllocSize = 256;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -214,7 +213,7 @@ TEST(FreeListHeap, ReallocTooLarge) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 512;
constexpr size_t kNewAllocSize = 4096;
- alignas(Block) std::byte buf[N] = {std::byte(0)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(0)};
FreeListHeapBuffer allocator(buf);
@@ -231,7 +230,7 @@ TEST(FreeListHeap, CanCalloc) {
constexpr size_t kAllocSize = 128;
constexpr size_t kNum = 4;
constexpr int size = kNum * kAllocSize;
- alignas(Block) std::byte buf[N] = {std::byte(1)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(1)};
constexpr std::byte zero{0};
FreeListHeapBuffer allocator(buf);
@@ -250,7 +249,7 @@ TEST(FreeListHeap, CanCallocWeirdSize) {
constexpr size_t kAllocSize = 143;
constexpr size_t kNum = 3;
constexpr int size = kNum * kAllocSize;
- alignas(Block) std::byte buf[N] = {std::byte(132)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(132)};
constexpr std::byte zero{0};
FreeListHeapBuffer allocator(buf);
@@ -267,7 +266,7 @@ TEST(FreeListHeap, CanCallocWeirdSize) {
TEST(FreeListHeap, CallocTooLarge) {
constexpr size_t N = 2048;
constexpr size_t kAllocSize = 2049;
- alignas(Block) std::byte buf[N] = {std::byte(1)};
+ alignas(FreeListHeap::BlockType) std::byte buf[N] = {std::byte(1)};
FreeListHeapBuffer allocator(buf);
diff --git a/pw_allocator/libc_allocator.cc b/pw_allocator/libc_allocator.cc
new file mode 100644
index 000000000..8469bcdae
--- /dev/null
+++ b/pw_allocator/libc_allocator.cc
@@ -0,0 +1,51 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/libc_allocator.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdlib>
+#include <cstring>
+
+#include "pw_assert/check.h"
+#include "pw_bytes/alignment.h"
+
+namespace pw::allocator {
+
+void* LibCAllocator::DoAllocate(Layout layout) {
+ // TODO: b/301930507 - `aligned_alloc` is not portable. Return null for larger
+ // allocations for now.
+ return layout.alignment() <= alignof(std::max_align_t)
+ ? std::malloc(layout.size())
+ : nullptr;
+}
+
+void LibCAllocator::DoDeallocate(void* ptr, Layout) { std::free(ptr); }
+
+bool LibCAllocator::DoResize(void*, Layout layout, size_t new_size) {
+ // `realloc` may move memory, even when shrinking. Only return true if no
+ // change is needed.
+ return layout.size() == new_size;
+}
+
+void* LibCAllocator::DoReallocate(void* ptr, Layout layout, size_t new_size) {
+ // TODO: b/301930507 - `aligned_alloc` is not portable. Return null for larger
+ // allocations for now.
+ return layout.alignment() <= alignof(std::max_align_t)
+ ? std::realloc(ptr, new_size)
+ : nullptr;
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/libc_allocator_test.cc b/pw_allocator/libc_allocator_test.cc
new file mode 100644
index 000000000..15eaac212
--- /dev/null
+++ b/pw_allocator/libc_allocator_test.cc
@@ -0,0 +1,77 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/libc_allocator.h"
+
+#include <cstring>
+
+#include "gtest/gtest.h"
+
+namespace pw::allocator {
+
+// Test fixtures.
+
+class LibCAllocatorTest : public ::testing::Test {
+ protected:
+ LibCAllocator allocator;
+};
+
+// Unit tests.
+
+TEST_F(LibCAllocatorTest, AllocateDeallocate) {
+ constexpr Layout layout = Layout::Of<std::byte[64]>();
+ void* ptr = allocator.Allocate(layout);
+ ASSERT_NE(ptr, nullptr);
+ // Check that the pointer can be dereferenced.
+ memset(ptr, 0xAB, layout.size());
+ allocator.Deallocate(ptr, layout);
+}
+
+TEST_F(LibCAllocatorTest, AllocateLargeAlignment) {
+ /// TODO: b/301930507 - `aligned_alloc` is not portable. As a result, this
+ /// allocator has a maximum alignment of `std::align_max_t`.
+ size_t size = 16;
+ size_t alignment = alignof(std::max_align_t) * 2;
+ void* ptr = allocator.Allocate(Layout(size, alignment));
+ EXPECT_EQ(ptr, nullptr);
+}
+
+TEST_F(LibCAllocatorTest, Resize) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[4]>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+ constexpr Layout new_layout = Layout::Of<uint32_t[3]>();
+ EXPECT_FALSE(allocator.Resize(ptr, old_layout, new_layout.size()));
+ allocator.Deallocate(ptr, old_layout);
+}
+
+TEST_F(LibCAllocatorTest, ResizeSame) {
+ constexpr Layout layout = Layout::Of<uint32_t[4]>();
+ void* ptr = allocator.Allocate(layout);
+ ASSERT_NE(ptr, nullptr);
+ EXPECT_TRUE(allocator.Resize(ptr, layout, layout.size()));
+ allocator.Deallocate(ptr, layout);
+}
+
+TEST_F(LibCAllocatorTest, Reallocate) {
+ constexpr Layout old_layout = Layout::Of<uint32_t[4]>();
+ void* ptr = allocator.Allocate(old_layout);
+ ASSERT_NE(ptr, nullptr);
+ constexpr Layout new_layout = Layout::Of<uint32_t[3]>();
+ void* new_ptr = allocator.Reallocate(ptr, old_layout, new_layout.size());
+ ASSERT_NE(new_ptr, nullptr);
+ allocator.Deallocate(new_ptr, new_layout);
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/null_allocator_test.cc b/pw_allocator/null_allocator_test.cc
new file mode 100644
index 000000000..0b319646a
--- /dev/null
+++ b/pw_allocator/null_allocator_test.cc
@@ -0,0 +1,38 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/null_allocator.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::allocator {
+
+TEST(NullAllocatorTest, Allocate) {
+ NullAllocator allocator;
+ // Allocate should fail, regardless of size and alignment.
+ for (size_t size = 1; size < 0x100; size <<= 1) {
+ for (size_t alignment = 1; alignment < 0x100; alignment <<= 1) {
+ EXPECT_EQ(allocator.Allocate(Layout(size, alignment)), nullptr);
+ }
+ }
+}
+
+TEST(NullAllocatorTest, Resize) {
+ NullAllocator allocator;
+ // It is not possible to get a valid pointer from Allocate.
+ constexpr Layout layout = Layout::Of<uint8_t>();
+ EXPECT_FALSE(allocator.Resize(this, layout, 1));
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/allocator.h b/pw_allocator/public/pw_allocator/allocator.h
new file mode 100644
index 000000000..a36d3ea3e
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/allocator.h
@@ -0,0 +1,380 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <optional>
+#include <utility>
+
+#include "pw_status/status.h"
+
+namespace pw::allocator {
+/// Describes the layout of a block of memory.
+///
+/// Layouts are passed to allocators, and consist of a size and a power-of-two
+/// alignment. Layouts can be constructed for a type `T` using `Layout::Of`.
+///
+/// Example:
+///
+/// @code{.cpp}
+/// struct MyStruct {
+/// uint8_t field1[3];
+/// uint32_t field2[3];
+/// };
+/// constexpr Layout layout_for_struct = Layout::Of<MyStruct>();
+/// @endcode
+class Layout {
+ public:
+ constexpr Layout(size_t size, size_t alignment = alignof(std::max_align_t))
+ : size_(size), alignment_(alignment) {}
+
+ /// Creates a Layout for the given type.
+ template <typename T>
+ static constexpr Layout Of() {
+ return Layout(sizeof(T), alignof(T));
+ }
+
+ constexpr Layout Extend(size_t size) {
+ return Layout(size_ + size, alignment_);
+ }
+
+ size_t size() const { return size_; }
+ size_t alignment() const { return alignment_; }
+
+ private:
+ size_t size_;
+ size_t alignment_;
+};
+
+template <typename T>
+class UniquePtr;
+
+/// Abstract interface for memory allocation.
+///
+/// This is the most generic and fundamental interface provided by the
+/// module, representing any object capable of dynamic memory allocation. Other
+/// interfaces may inherit from a base generic Allocator and provide different
+/// allocator properties.
+///
+/// The interface makes no guarantees about its implementation. Consumers of the
+/// generic interface must not make any assumptions around allocator behavior,
+/// thread safety, or performance.
+///
+/// NOTE: This interface is in development and should not be considered stable.
+class Allocator {
+ public:
+ constexpr Allocator() = default;
+ virtual ~Allocator() = default;
+
+ /// Asks the allocator if it is capable of realloating or deallocating a given
+ /// pointer.
+ ///
+ /// NOTE: This method is in development and should not be considered stable.
+ /// Do NOT use it in its current form to determine if this allocator can
+ /// deallocate pointers. Callers MUST only `Deallocate` memory using the same
+ /// `Allocator` they used to `Allocate` it. This method is currently for
+ /// internal use only.
+ ///
+ /// TODO: b/301677395 - Add dynamic type information to support a
+ /// `std::pmr`-style `do_is_equal`. Without this information, it is not
+ /// possible to determine whether another allocator has applied additional
+ /// constraints to memory that otherwise may appear to be associated with this
+ /// allocator.
+ ///
+ /// @param[in] ptr The pointer to be queried.
+ /// @param[in] layout Describes the memory pointed at by `ptr`.
+ ///
+ /// @retval UNIMPLEMENTED This object cannot recognize allocated
+ /// pointers.
+ /// @retval OUT_OF_RANGE Pointer cannot be re/deallocated by this
+ /// object.
+ /// @retval OK This object can re/deallocate the pointer.
+ Status Query(const void* ptr, Layout layout) const {
+ return DoQuery(ptr, layout);
+ }
+
+ /// Allocates a block of memory with the specified size and alignment.
+ ///
+ /// Returns `nullptr` if the allocation cannot be made, or the `layout` has a
+ /// size of 0.
+ ///
+ /// @param[in] layout Describes the memory to be allocated.
+ void* Allocate(Layout layout) { return DoAllocate(layout); }
+
+ template <typename T, typename... Args>
+ std::optional<UniquePtr<T>> MakeUnique(Args&&... args) {
+ static constexpr Layout kStaticLayout = Layout::Of<T>();
+ void* void_ptr = Allocate(kStaticLayout);
+ if (void_ptr == nullptr) {
+ return std::nullopt;
+ }
+ T* ptr = new (void_ptr) T(std::forward<Args>(args)...);
+ return std::make_optional<UniquePtr<T>>(
+ UniquePtr<T>::kPrivateConstructor, ptr, &kStaticLayout, this);
+ }
+
+ /// Releases a previously-allocated block of memory.
+ ///
+ /// The given pointer must have been previously obtained from a call to either
+ /// `Allocate` or `Reallocate`; otherwise the behavior is undefined.
+ ///
+ /// @param[in] ptr Pointer to previously-allocated memory.
+ /// @param[in] layout Describes the memory to be deallocated.
+ void Deallocate(void* ptr, Layout layout) {
+ return DoDeallocate(ptr, layout);
+ }
+
+ /// Modifies the size of an previously-allocated block of memory without
+ /// copying any data.
+ ///
+ /// Returns true if its size was changed without copying data to a new
+ /// allocation; otherwise returns false.
+ ///
+ /// In particular, it always returns true if the `old_layout.size()` equals
+ /// `new_size`, and always returns false if the given pointer is null, the
+ /// `old_layout.size()` is 0, or the `new_size` is 0.
+ ///
+ /// @param[in] ptr Pointer to previously-allocated memory.
+ /// @param[in] old_layout Describes the previously-allocated memory.
+ /// @param[in] new_size Requested new size for the memory allocation.
+ bool Resize(void* ptr, Layout layout, size_t new_size) {
+ if (ptr == nullptr || layout.size() == 0 || new_size == 0) {
+ return false;
+ }
+ return DoResize(ptr, layout, new_size);
+ }
+
+ /// Modifies the size of a previously-allocated block of memory.
+ ///
+ /// Returns pointer to the modified block of memory, or `nullptr` if the
+ /// memory could not be modified.
+ ///
+ /// The data stored by the memory being modified must be trivially
+ /// copyable. If it is not, callers should themselves attempt to `Resize`,
+ /// then `Allocate`, move the data, and `Deallocate` as needed.
+ ///
+ /// If `nullptr` is returned, the block of memory is unchanged. In particular,
+ /// if the `new_layout` has a size of 0, the given pointer will NOT be
+ /// deallocated.
+ ///
+ /// Unlike `Resize`, providing a null pointer or a `old_layout` with a size of
+ /// 0 will return a new allocation.
+ ///
+ /// @param[in] ptr Pointer to previously-allocated memory.
+ /// @param[in] layout Describes the previously-allocated memory.
+ /// @param[in] new_size Requested new size for the memory allocation.
+ void* Reallocate(void* ptr, Layout layout, size_t new_size) {
+ return DoReallocate(ptr, layout, new_size);
+ }
+
+ private:
+ /// Virtual `Query` function that can be overridden by derived classes.
+ ///
+ /// The default implementation of this method simply returns `UNIMPLEMENTED`.
+ /// Allocators which dispatch to other allocators need to override this method
+ /// in order to be able to direct reallocations and deallocations to
+ /// appropriate allocator.
+ virtual Status DoQuery(const void*, Layout) const {
+ return Status::Unimplemented();
+ }
+
+ /// Virtual `Allocate` function implemented by derived classes.
+ virtual void* DoAllocate(Layout layout) = 0;
+
+ /// Virtual `Deallocate` function implemented by derived classes.
+ virtual void DoDeallocate(void* ptr, Layout layout) = 0;
+
+ /// Virtual `Resize` function implemented by derived classes.
+ ///
+ /// The default implementation simply returns `false`, indicating that
+ /// resizing is not supported.
+ virtual bool DoResize(void* /*ptr*/, Layout /*layout*/, size_t /*new_size*/) {
+ return false;
+ }
+
+ /// Virtual `Reallocate` function that can be overridden by derived classes.
+ ///
+ /// The default implementation will first try to `Resize` the data. If that is
+ /// unsuccessful, it will allocate an entirely new block, copy existing data,
+ /// and deallocate the given block.
+ virtual void* DoReallocate(void* ptr, Layout layout, size_t new_size);
+};
+
+/// An RAII pointer to a value of type ``T`` stored within an ``Allocator``.
+///
+/// This is analogous to ``std::unique_ptr``, but includes a few differences
+/// in order to support ``Allocator`` and encourage safe usage. Most notably,
+/// ``UniquePtr<T>`` cannot be constructed from a ``T*``.
+template <typename T>
+class UniquePtr {
+ public:
+ /// Creates an empty (``nullptr``) instance.
+ ///
+ /// NOTE: Instances of this type are most commonly constructed using
+ /// ``Allocator::MakeUnique``.
+ constexpr UniquePtr()
+ : value_(nullptr), layout_(nullptr), allocator_(nullptr) {}
+
+ /// Creates an empty (``nullptr``) instance.
+ ///
+ /// NOTE: Instances of this type are most commonly constructed using
+ /// ``Allocator::MakeUnique``.
+ constexpr UniquePtr(std::nullptr_t)
+ : value_(nullptr), layout_(nullptr), allocator_(nullptr) {}
+
+ /// Move-constructs a ``UniquePtr<T>`` from a ``UniquePtr<U>``.
+ ///
+ /// This allows not only pure move construction where ``T == U``, but also
+ /// converting construction where ``T`` is a base class of ``U``, like
+ /// ``UniquePtr<Base> base(allocator.MakeUnique<Child>());``.
+ template <typename U>
+ UniquePtr(UniquePtr<U>&& other) noexcept
+ : value_(other.value_),
+ layout_(other.layout_),
+ allocator_(other.allocator_) {
+ static_assert(
+ std::is_assignable_v<T*&, U*>,
+ "Attempted to construct a UniquePtr<T> from a UniquePtr<U> where "
+ "U* is not assignable to T*.");
+ other.Release();
+ }
+
+ /// Move-assigns a ``UniquePtr<T>`` from a ``UniquePtr<U>``.
+ ///
+ /// This operation destructs and deallocates any value currently stored in
+ /// ``this``.
+ ///
+ /// This allows not only pure move assignment where ``T == U``, but also
+ /// converting assignment where ``T`` is a base class of ``U``, like
+ /// ``UniquePtr<Base> base = allocator.MakeUnique<Child>();``.
+ template <typename U>
+ UniquePtr& operator=(UniquePtr<U>&& other) noexcept {
+ static_assert(std::is_assignable_v<T*&, U*>,
+ "Attempted to assign a UniquePtr<U> to a UniquePtr<T> where "
+ "U* is not assignable to T*.");
+ Reset();
+ value_ = other.value_;
+ layout_ = other.layout_;
+ allocator_ = other.allocator_;
+ other.Release();
+ }
+
+ /// Sets this ``UniquePtr`` to null, destructing and deallocating any
+ /// currently-held value.
+ ///
+ /// After this function returns, this ``UniquePtr`` will be in an "empty"
+ /// (``nullptr``) state until a new value is assigned.
+ UniquePtr& operator=(std::nullptr_t) { Reset(); }
+
+ /// Destructs and deallocates any currently-held value.
+ ~UniquePtr() { Reset(); }
+
+ /// Sets this ``UniquePtr`` to an "empty" (``nullptr``) value without
+ /// destructing any currently-held value or deallocating any underlying
+ /// memory.
+ void Release() {
+ value_ = nullptr;
+ layout_ = nullptr;
+ allocator_ = nullptr;
+ }
+
+ /// Destructs and deallocates any currently-held value.
+ ///
+ /// After this function returns, this ``UniquePtr`` will be in an "empty"
+ /// (``nullptr``) state until a new value is assigned.
+ void Reset() {
+ if (value_ != nullptr) {
+ value_->~T();
+ allocator_->Deallocate(value_, *layout_);
+ Release();
+ }
+ }
+
+ /// ``operator bool`` is not provided in order to ensure that there is no
+ /// confusion surrounding ``if (foo)`` vs. ``if (*foo)``.
+ ///
+ /// ``nullptr`` checking should instead use ``if (foo == nullptr)``.
+ explicit operator bool() const = delete;
+
+ /// Returns whether this ``UniquePtr`` is in an "empty" (``nullptr``) state.
+ bool operator==(std::nullptr_t) const { return value_ == nullptr; }
+
+ /// Returns whether this ``UniquePtr`` is not in an "empty" (``nullptr``)
+ /// state.
+ bool operator!=(std::nullptr_t) const { return value_ != nullptr; }
+
+ /// Returns the underlying (possibly null) pointer.
+ T* get() { return value_; }
+ /// Returns the underlying (possibly null) pointer.
+ const T* get() const { return value_; }
+
+ /// Permits accesses to members of ``T`` via ``my_unique_ptr->Member``.
+ ///
+ /// The behavior of this operation is undefined if this ``UniquePtr`` is in an
+ /// "empty" (``nullptr``) state.
+ T* operator->() { return value_; }
+ const T* operator->() const { return value_; }
+
+ /// Returns a reference to any underlying value.
+ ///
+ /// The behavior of this operation is undefined if this ``UniquePtr`` is in an
+ /// "empty" (``nullptr``) state.
+ T& operator*() { return *value_; }
+ const T& operator*() const { return *value_; }
+
+ private:
+ /// A pointer to the contained value.
+ T* value_;
+
+ /// The ``layout_` with which ``value_``'s allocation was initially created.
+ ///
+ /// Unfortunately this is not simply ``Layout::Of<T>()`` since ``T`` may be
+ /// a base class of the original allocated type.
+ const Layout* layout_;
+
+ /// The ``allocator_`` in which ``value_`` is stored.
+ /// This must be tracked in order to deallocate the memory upon destruction.
+ Allocator* allocator_;
+
+ /// Allow converting move constructor and assignment to access fields of
+ /// this class.
+ ///
+ /// Without this, ``UniquePtr<U>`` would not be able to access fields of
+ /// ``UniquePtr<T>``.
+ template <typename U>
+ friend class UniquePtr;
+
+ class PrivateConstructorType {};
+ static constexpr PrivateConstructorType kPrivateConstructor{};
+
+ public:
+ /// Private constructor that is public only for use with `emplace` and
+ /// other in-place construction functions.
+ ///
+ /// Constructs a ``UniquePtr`` from an already-allocated value.
+ ///
+ /// NOTE: Instances of this type are most commonly constructed using
+ /// ``Allocator::MakeUnique``.
+ UniquePtr(PrivateConstructorType,
+ T* value,
+ const Layout* layout,
+ Allocator* allocator)
+ : value_(value), layout_(layout), allocator_(allocator) {}
+
+ // Allow construction with ``kPrivateConstructor`` to the implementation
+ // of ``MakeUnique``.
+ friend class Allocator;
+};
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/allocator_metric_proxy.h b/pw_allocator/public/pw_allocator/allocator_metric_proxy.h
new file mode 100644
index 000000000..b4cc3a561
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/allocator_metric_proxy.h
@@ -0,0 +1,71 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+
+#include "pw_allocator/allocator.h"
+#include "pw_metric/metric.h"
+#include "pw_status/status.h"
+
+namespace pw::allocator {
+
+/// Wraps an `Allocator` and records details of its usage.
+///
+/// In order for this object to record memory usage metrics correctly, all calls
+/// to, e.g., `Allocate`, `Deallocate`, etc. must be made through it and not the
+/// allocator it wraps.
+///
+/// As a rule, the wrapped allocator is always invoked before any conditions are
+/// asserted by this class, with the exception of checking that a wrapped
+/// allocator has been set via `Initialize`. This allows the wrapped allocator
+/// to issue a more detailed error in case of misuse.
+class AllocatorMetricProxy : public Allocator {
+ public:
+ constexpr explicit AllocatorMetricProxy(metric::Token token)
+ : memusage_(token) {}
+
+ /// Sets the wrapped allocator.
+ ///
+ /// This must be called exactly once and before any other method.
+ ///
+ /// @param[in] allocator The allocator to wrap and record.
+ void Initialize(Allocator& allocator);
+
+ metric::Group& memusage() { return memusage_; }
+ size_t used() const { return used_.value(); }
+ size_t peak() const { return peak_.value(); }
+ size_t count() const { return count_.value(); }
+
+ private:
+ /// @copydoc Allocator::Query
+ Status DoQuery(const void* ptr, Layout layout) const override;
+
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout layout) override;
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void* ptr, Layout layout) override;
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void* ptr, Layout layout, size_t new_size) override;
+
+ metric::Group memusage_;
+ Allocator* allocator_ = nullptr;
+ PW_METRIC(used_, "used", 0U);
+ PW_METRIC(peak_, "peak", 0U);
+ PW_METRIC(count_, "count", 0U);
+};
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/allocator_testing.h b/pw_allocator/public/pw_allocator/allocator_testing.h
new file mode 100644
index 000000000..f5c84c9ee
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/allocator_testing.h
@@ -0,0 +1,131 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator.h"
+#include "pw_allocator/block.h"
+#include "pw_allocator/simple_allocator.h"
+#include "pw_bytes/span.h"
+
+namespace pw::allocator::test {
+
+/// Simple memory allocator for testing.
+///
+/// This allocator records the most recent parameters passed to the `Allocator`
+/// interface methods, and returns them via accessors.
+class AllocatorForTest : public Allocator {
+ public:
+ constexpr AllocatorForTest() = default;
+ ~AllocatorForTest() override;
+
+ size_t allocate_size() const { return allocate_size_; }
+ void* deallocate_ptr() const { return deallocate_ptr_; }
+ size_t deallocate_size() const { return deallocate_size_; }
+ void* resize_ptr() const { return resize_ptr_; }
+ size_t resize_old_size() const { return resize_old_size_; }
+ size_t resize_new_size() const { return resize_new_size_; }
+
+ /// Provides memory for the allocator to allocate from.
+ Status Init(ByteSpan bytes);
+
+ /// Allocates all the memory from this object.
+ void Exhaust();
+
+ /// Resets the recorded parameters to an initial state.
+ void ResetParameters();
+
+ /// This frees all memory associated with this allocator.
+ void DeallocateAll();
+
+ private:
+ using BlockType = Block<>;
+
+ /// @copydoc Allocator::Query
+ Status DoQuery(const void* ptr, Layout layout) const override;
+
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout layout) override;
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void* ptr, Layout layout) override;
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void* ptr, Layout layout, size_t new_size) override;
+
+ SimpleAllocator allocator_;
+ size_t allocate_size_ = 0;
+ void* deallocate_ptr_ = nullptr;
+ size_t deallocate_size_ = 0;
+ void* resize_ptr_ = nullptr;
+ size_t resize_old_size_ = 0;
+ size_t resize_new_size_ = 0;
+};
+
+/// Wraps a default-constructed type a buffer holding a region of memory.
+///
+/// Although the type is arbitrary, the intended purpose of of this class is to
+/// provide allocators with memory to use when testing.
+///
+/// This class uses composition instead of inheritance in order to allow the
+/// wrapped type's destructor to reference the memory without risk of a
+/// use-after-free. As a result, the specific methods of the wrapped type
+/// are not directly accesible. Instead, they can be accessed using the `*` and
+/// `->` operators, e.g.
+///
+/// @code{.cpp}
+/// WithBuffer<MyAllocator, 256> allocator;
+/// allocator->MethodSpecificToMyAllocator();
+/// @endcode
+///
+/// Note that this class does NOT initialize the allocator, since initialization
+/// is not specified as part of the `Allocator` interface and may vary from
+/// allocator to allocator. As a result, typical usgae includes deriving a class
+/// that initializes the wrapped allocator with the buffer in a constructor. See
+/// `AllocatorForTestWithBuffer` below for an example.
+///
+/// @tparam T The wrapped object.
+/// @tparam kBufferSize The size of the backing memory, in bytes.
+/// @tparam AlignType Buffer memory will be aligned to this type's
+/// alignment boundary.
+template <typename T, size_t kBufferSize, typename AlignType = uint8_t>
+class WithBuffer {
+ public:
+ static constexpr size_t kCapacity = kBufferSize;
+
+ std::byte* data() { return buffer_.data(); }
+ size_t size() const { return buffer_.size(); }
+
+ T& operator*() { return obj_; }
+ T* operator->() { return &obj_; }
+
+ private:
+ alignas(AlignType) std::array<std::byte, kBufferSize> buffer_;
+ T obj_;
+};
+
+/// An `AllocatorForTest` that is automatically initialized on construction.
+template <size_t kBufferSize>
+class AllocatorForTestWithBuffer
+ : public WithBuffer<AllocatorForTest, kBufferSize> {
+ public:
+ AllocatorForTestWithBuffer() {
+ EXPECT_EQ((*this)->Init(ByteSpan(this->data(), this->size())), OkStatus());
+ }
+};
+
+} // namespace pw::allocator::test
diff --git a/pw_allocator/public/pw_allocator/block.h b/pw_allocator/public/pw_allocator/block.h
index 09b08b74f..4b5e7e594 100644
--- a/pw_allocator/public/pw_allocator/block.h
+++ b/pw_allocator/public/pw_allocator/block.h
@@ -11,245 +11,848 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-
-// WARNING: This code is a experimental WIP & exploration only, and is far from
-// usable.
#pragma once
#include <cstdint>
+#include <cstring>
+#include "lib/stdcompat/bit.h"
+#include "pw_bytes/alignment.h"
+#include "pw_bytes/span.h"
+#include "pw_result/result.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::allocator {
+/// Representation-independent base class of Block.
+///
+/// This class contains static methods which do not depend on the template
+/// parameters of ``Block`` that are used to encode block information. This
+/// reduces the amount of code generated for ``Block``s with different
+/// parameters.
+///
+/// This class should not be used directly. Instead, see ``Block``.
+class BaseBlock {
+ public:
#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
-// Add poison offset of sizeof(void*) bytes before and after usable space in all
-// Blocks.
-#define PW_ALLOCATOR_POISON_OFFSET sizeof(void*)
+ // Add poison offset of 8 bytes before and after usable space in all
+ // Blocks.
+ static constexpr size_t kPoisonOffset = 8;
#else
-// Set the poison offset to 0 bytes; will not add poisson space before and
-// after usable space in all Blocks.
-#define PW_ALLOCATOR_POISON_OFFSET static_cast<size_t>(0)
+ // Set the poison offset to 0 bytes; will not add poison space before and
+ // after usable space in all Blocks.
+ static constexpr size_t kPoisonOffset = 0;
#endif // PW_ALLOCATOR_POISON_ENABLE
-// The "Block" type is intended to be a building block component for
-// allocators. In the this design, there is an explicit pointer to next and
-// prev from the block header; the size is not encoded. The below diagram shows
-// what this would look like for two blocks.
-//
-// .------+---------------------------------.-----------------------------
-// | Block A (first) | Block B (second)
-//
-// +------+------+--------------------------+------+------+---------------
-// | Next | Prev | usable space | Next | Prev | usable space..
-// +------+------+--------------------------+------+--+---+---------------
-// ^ | ^ |
-// | '-------------------------------------' |
-// | |
-// '----------- Block B's prev points to Block A -----'
-//
-// One use for these blocks is to use them as allocations, where each block
-// represents an allocation handed out by malloc(). These blocks could also be
-// used as part of a slab or buddy allocator.
-//
-// Each block also contains flags for whether it is the last block (i.e. whether
-// the "next" pointer points to a valid block, or just denotes the end of this
-// block), and whether the block is in use. These are encoded into the last two
-// bits of the "next" pointer, as follows:
-//
-// .-----------------------------------------------------------------------.
-// | Block |
-// +-----------------------------------------------------------------------+
-// | Next | Prev | usable space |
-// +----------------+------+------+ + |
-// | Ptr[N..2] | Last | Used | | |
-// +----------------+------+------+------+---------------------------------+
-// ^
-// |
-// '----------- Next() = Next & ~0x3 --------------------------------->
-//
-// The first block in a chain is denoted by a nullptr "prev" field, and the last
-// block is denoted by the "Last" bit being set.
-//
-// Note, This block class requires that the given block is aligned to a
-// alignof(Block*) boundary. Because of this alignment requirement, each
-// returned block will also be aligned to a alignof(Block*) boundary, and the
-// size will always be rounded up to a multiple of alignof(Block*).
-//
-// This class must be constructed using the static Init call.
-class Block final {
- public:
// No copy/move
- Block(const Block& other) = delete;
- Block& operator=(const Block& other) = delete;
- Block(Block&& other) = delete;
- Block& operator=(Block&& other) = delete;
-
- // Create the first block for a given memory region.
- // Note that the start of the given memory region must be aligned to an
- // alignof(Block) boundary.
- // Returns:
- // INVALID_ARGUMENT if the given region is unaligned to too small, or
- // OK otherwise.
- static Status Init(const span<std::byte> region, Block** block);
-
- // Returns a pointer to a Block, given a pointer to the start of the usable
- // space inside the block (i.e. the opposite operation to UsableSpace()). In
- // reality, this method just subtracts the appropriate amount from
- // usable_space to point to the start of the owning block.
- //
- // Be aware that this method does not do any checking; passing a random
- // pointer will return a non-null pointer.
+ BaseBlock(const BaseBlock& other) = delete;
+ BaseBlock& operator=(const BaseBlock& other) = delete;
+ BaseBlock(BaseBlock&& other) = delete;
+ BaseBlock& operator=(BaseBlock&& other) = delete;
+
+ protected:
+ enum BlockStatus {
+ kValid,
+ kMisaligned,
+ kPrevMismatched,
+ kNextMismatched,
+ kPoisonCorrupted,
+ };
+
+#if defined(PW_ALLOCATOR_POISON_ENABLE) && PW_ALLOCATOR_POISON_ENABLE
+ static constexpr std::byte kPoisonPattern[kPoisonOffset] = {
+ std::byte{0x92},
+ std::byte{0x88},
+ std::byte{0x0a},
+ std::byte{0x00},
+ std::byte{0xec},
+ std::byte{0xdc},
+ std::byte{0xae},
+ std::byte{0x4e},
+ };
+#endif // PW_ALLOCATOR_POISON_ENABLE
+
+ BaseBlock() = default;
+
+ /// Poisons the block's guard regions, if poisoning is enabled.
+ ///
+ /// Does nothing if poisoning is disabled.
+ static void Poison(void* block, size_t header_size, size_t outer_size);
+
+ /// Returns whether the block's guard regions are untouched, if poisoning is
+ /// enabled.
+ ///
+ /// Trivially returns true if poisoning is disabled.
+ static bool CheckPoison(const void* block,
+ size_t header_size,
+ size_t outer_size);
+
+ static void CrashMisaligned(uintptr_t addr);
+ static void CrashNextMismatched(uintptr_t addr, uintptr_t next_prev);
+ static void CrashPrevMismatched(uintptr_t addr, uintptr_t prev_next);
+ static void CrashPoisonCorrupted(uintptr_t addr);
+
+ // Associated types
+
+ /// Iterator for a list of blocks.
+ ///
+ /// This class is templated both on the concrete block type, as well as on a
+ /// function that can advance the iterator to the next element. This class
+ /// cannot be instantiated directly. Instead, use the `begin` and `end`
+ /// methods of `Block::Range` or `Block::ReverseRange`.
+ template <typename BlockType, BlockType* (*Advance)(const BlockType*)>
+ class BaseIterator {
+ public:
+ BaseIterator& operator++() {
+ if (block_ != nullptr) {
+ block_ = Advance(block_);
+ }
+ return *this;
+ }
+
+ bool operator!=(const BaseIterator& other) {
+ return block_ != other.block_;
+ }
+
+ BlockType* operator*() { return block_; }
+
+ protected:
+ BaseIterator(BlockType* block) : block_(block) {}
+
+ private:
+ BlockType* block_;
+ };
+
+ /// Represents a range of blocks in a list.
+ ///
+ /// This class is templated both on the concrete block and iterator types.
+ /// This class cannot be instantiated directly. Instead, use `Block::Range` or
+ /// `Block::ReverseRange`.
+ template <typename BlockType, typename IteratorType>
+ class BaseRange {
+ public:
+ IteratorType& begin() { return begin_; }
+ IteratorType& end() { return end_; }
+
+ protected:
+ BaseRange(BlockType* begin_inclusive, BlockType* end_exclusive)
+ : begin_(begin_inclusive), end_(end_exclusive) {}
+
+ private:
+ IteratorType begin_;
+ IteratorType end_;
+ };
+};
+
+/// @brief Represents a region of memory as an element of a doubly linked list.
+///
+/// Typically, an application will start with a single block representing a
+/// contiguous region of memory returned from a call to `Init`. This block can
+/// be split into smaller blocks that refer to their neighbors. Neighboring
+/// blocks can be merged. These behaviors allows ``Allocator``s to track
+/// allocated memory with a small amount of overhead. See
+/// pw_allocator_private/simple_allocator.h for an example.
+///
+/// Blocks will always be aligned to a `kAlignment boundary. Block sizes will
+/// always be rounded up to a multiple of `kAlignment`.
+///
+/// The blocks do not encode their size. Instead, they encode the offsets to the
+/// next and previous blocks. These offsets are encoded using the type given by
+/// the template parameter `T`. The encoded offsets are simply the offsets
+/// divded by the minimum alignment.
+///
+/// Optionally, callers may add guard regions to block by defining
+/// `PW_ALLOCATOR_POISON_ENABLE`. These guard regions will be set to a known
+/// whenever a block is created and checked when that block is merged. This can
+/// catch heap overflows where consumers write beyond the end of the usable
+/// space.
+///
+/// As an example, the diagram below represents two contiguous
+/// `Block<uint32_t, ...>`s with heap poisoning enabled and
+/// `alignof(uint32_t) == 4`. The indices indicate byte offsets.
+///
+/// @code{.unparsed}
+/// Block 1:
+/// +--------------------------------------+----------------+----------------+
+/// | Header | <Usable space> | Footer |
+/// +----------+----------+----------------+----------------+----------------+
+/// | Prev | Next | | | |
+/// | 0....3 | 4......7 | 8...........15 | 16.........271 | 272........280 |
+/// | 00000000 | 00000046 | kPoisonPattern | <Usable space> | kPoisonPattern |
+/// +----------+----------+----------------+----------------+----------------+
+///
+/// Block 2:
+/// +--------------------------------------+----------------+----------------+
+/// | Header | <Usable space> | Footer |
+/// +----------+----------+----------------+----------------+----------------+
+/// | Prev | Next | | | |
+/// | 0....3 | 4......7 | 8...........15 | 16........1039 | 1040......1056 |
+/// | 00000046 | 00000106 | kPoisonPattern | <Usable space> | kPoisonPattern |
+/// +----------+----------+----------------+----------------+----------------+
+/// @endcode
+///
+/// The overall size of the block (e.g. 280 bytes) is given by its next offset
+/// multiplied by the alignment (e.g. 0x106 * 4). Also, the next offset of a
+/// block matches the previous offset of its next block. The first block in a
+/// list is denoted by having a previous offset of `0`.
+///
+/// Each block also encodes flags. Builtin flags indicate whether the block is
+/// in use and whether it is the last block in the list. The last block will
+/// still have a next offset that denotes its size.
+///
+/// Depending on `kMaxSize`, some bits of type `T` may not be needed to
+/// encode an offset. Additional bits of both the previous and next offsets may
+/// be used for setting custom flags.
+///
+/// For example, for a `Block<uint32_t, 0x10000>`, on a platform where
+/// `alignof(uint32_t) == 4`, the fully encoded bits would be:
+///
+/// @code{.unparsed}
+/// +-------------------------------------------------------------------------+
+/// | block: |
+/// +------------------------------------+------------------------------------+
+/// | .prev_ | .next_: |
+/// +---------------+------+-------------+---------------+------+-------------+
+/// | MSB | | LSB | MSB | | LSB |
+/// | 31.........16 | 15 | 14........0 | 31.........16 | 15 | 14........0 |
+/// | custom_flags1 | used | prev_offset | custom_flags2 | last | next_offset |
+/// +---------------+------+-------------+---------------+------+-------------+
+/// @endcode
+///
+/// @tparam UintType Unsigned integral type used to encode offsets and flags.
+/// @tparam kMaxSize Largest offset that can be addressed by this block. Bits
+/// of `UintType` not needed for offsets are available as
+/// flags.
+template <typename UintType = uintptr_t,
+ size_t kMaxSize = std::numeric_limits<uintptr_t>::max()>
+class Block final : public BaseBlock {
+ public:
+ static_assert(std::is_unsigned_v<UintType>);
+ static_assert(kMaxSize <= std::numeric_limits<UintType>::max());
+
+ static constexpr size_t kCapacity = kMaxSize;
+ static constexpr size_t kHeaderSize = sizeof(Block) + kPoisonOffset;
+ static constexpr size_t kFooterSize = kPoisonOffset;
+ static constexpr size_t kBlockOverhead = kHeaderSize + kFooterSize;
+ static constexpr size_t kAlignment = alignof(Block);
+
+ /// @brief Creates the first block for a given memory region.
+ ///
+ /// @pre The start of the given memory region must be aligned to an
+ /// `kAlignment` boundary.
+ ///
+ /// @retval OK Returns a block representing the region.
+ /// @retval INVALID_ARGUMENT The region is unaligned.
+ /// @retval RESOURCE_EXHAUSTED The region is too small for a block.
+ /// @retval OUT_OF_RANGE The region is larger than `kMaxSize`.
+ static Result<Block*> Init(ByteSpan region);
+
+ /// @returns A pointer to a `Block`, given a pointer to the start of the
+ /// usable space inside the block.
+ ///
+ /// This is the inverse of `UsableSpace()`.
+ ///
+ /// @warning This method does not do any checking; passing a random
+ /// pointer will return a non-null pointer.
static Block* FromUsableSpace(std::byte* usable_space) {
- return reinterpret_cast<Block*>(usable_space - sizeof(Block) -
- PW_ALLOCATOR_POISON_OFFSET);
+ // Perform memory laundering to prevent the compiler from tracing the memory
+ // used to store the block and to avoid optimaztions that may be invalidated
+ // by the use of placement-new to create blocks in `Init` and `Split`.
+ return std::launder(reinterpret_cast<Block*>(usable_space - kHeaderSize));
}
- // Size including the header.
- size_t OuterSize() const {
- return reinterpret_cast<intptr_t>(Next()) -
- reinterpret_cast<intptr_t>(this);
- }
+ /// @returns The total size of the block in bytes, including the header.
+ size_t OuterSize() const { return GetOffset(next_); }
- // Usable bytes inside the block.
- size_t InnerSize() const {
- return OuterSize() - sizeof(*this) - 2 * PW_ALLOCATOR_POISON_OFFSET;
- }
+ /// @returns The number of usable bytes inside the block.
+ size_t InnerSize() const { return OuterSize() - kBlockOverhead; }
- // Return the usable space inside this block.
+ /// @returns A pointer to the usable space inside this block.
std::byte* UsableSpace() {
- return reinterpret_cast<std::byte*>(this) + sizeof(*this) +
- PW_ALLOCATOR_POISON_OFFSET;
- }
-
- // Split this block, such that this block has an inner size of
- // `head_block_inner_size`, and return a new block in the remainder of the
- // space in `new_block`.
- //
- // The "remainder" block will be aligned to a alignof(Block*) boundary (and
- // `head_block_inner_size` will be rounded up). If the remaining space is not
- // large enough to store a new `Block` after rounding, no splitting will
- // occur.
- //
- // This may return the following:
- // OK: The split completed successfully.
- // INVALID_ARGUMENT: new_block is null
- // FAILED_PRECONDITION: This block is in use and cannot be split.
- // OUT_OF_RANGE: The requested size for "this" block is greater than the
- // current inner_size.
- // RESOURCE_EXHAUSTED: The split cannot occur because the "remainder" block
- // would not be large enough to store a block header.
- Status Split(size_t head_block_inner_size, Block** new_block);
-
- // Merge this block with the one that comes after it.
- // This function will not merge blocks if either are in use.
- //
- // This may return the following:
- // OK: Merge was successful.
- // OUT_OF_RANGE: Attempting to merge the "last" block.
- // FAILED_PRECONDITION: The blocks could not be merged because one of them
- // was in use.
- Status MergeNext();
-
- // Merge this block with the one that comes before it.
- // This function will not merge blocks if either are in use.
- //
- // Warning: merging with a previous block will invalidate this block instance.
- // do not perform any operations on this instance after merging.
- //
- // This may return the following:
- // OK: Merge was successful.
- // OUT_OF_RANGE: Attempting to merge the "first" block.
- // FAILED_PRECONDITION: The blocks could not be merged because one of them
- // was in use.
- Status MergePrev();
-
- // Returns whether this block is in-use or not
- bool Used() const { return (NextAsUIntPtr() & kInUseFlag) == kInUseFlag; }
-
- // Returns whether this block is the last block or
- // not (i.e. whether NextBlock points to a valid block or not).
- // This is needed because NextBlock points to the end of this block,
- // whether there is a valid block there or not.
- bool Last() const { return (NextAsUIntPtr() & kLastFlag) == kLastFlag; }
-
- // Mark this block as in-use
- void MarkUsed() {
- next_ = reinterpret_cast<Block*>((NextAsUIntPtr() | kInUseFlag));
- }
-
- // Mark this block as free
- void MarkFree() {
- next_ = reinterpret_cast<Block*>((NextAsUIntPtr() & ~kInUseFlag));
- }
-
- // Mark this block as the last one in the chain.
- void MarkLast() {
- next_ = reinterpret_cast<Block*>((NextAsUIntPtr() | kLastFlag));
- }
-
- // Clear the "last" bit from this block.
- void ClearLast() {
- next_ = reinterpret_cast<Block*>((NextAsUIntPtr() & ~kLastFlag));
- }
-
- // Fetch the block immediately after this one.
- // Note: you should also check Last(); this function may return a valid
- // block, even if one does not exist.
- Block* Next() const {
- return reinterpret_cast<Block*>(
- (NextAsUIntPtr() & ~(kInUseFlag | kLastFlag)));
- }
-
- // Return the block immediately before this one. This will return nullptr
- // if this is the "first" block.
- Block* Prev() const { return prev_; }
-
- // Return true if the block is aligned, the prev/next field matches with the
- // previous and next block, and the poisoned bytes is not damaged. Otherwise,
- // return false to indicate this block is corrupted.
- bool IsValid() const { return CheckStatus() == BlockStatus::VALID; }
-
- // Uses PW_DCHECK to log information about the reason if a block is invalid.
- // This function will do nothing if the block is valid.
+ // Accessing a dynamic type through a glvalue of std::byte is always well-
+ // defined to allow for object representation.
+ return reinterpret_cast<std::byte*>(this) + kHeaderSize;
+ }
+
+ /// Splits an aligned block from the start of the block, and marks it as used.
+ ///
+ /// If successful, `block` will be replaced by a block that has an inner
+ /// size of at least `inner_size`, and whose starting address is aligned to an
+ /// `alignment` boundary. If unsuccessful, `block` will be unmodified.
+ ///
+ /// This method is static in order to consume and replace the given block
+ /// pointer with a pointer to the new, smaller block. In total, up to two
+ /// additional blocks may be created: one to pad the returned block to an
+ /// alignment boundary and one for the trailing space.
+ ///
+ /// @pre The block must not be in use.
+ ///
+ /// @retval OK The split completed successfully.
+ /// @retval FAILED_PRECONDITION This block is in use and cannot be split.
+ /// @retval OUT_OF_RANGE The requested size plus padding needed for
+ /// alignment is greater than the current size.
+ static Status AllocFirst(Block*& block, size_t inner_size, size_t alignment);
+
+ /// Splits an aligned block from the end of the block, and marks it as used.
+ ///
+ /// If successful, `block` will be replaced by a block that has an inner
+ /// size of at least `inner_size`, and whose starting address is aligned to an
+ /// `alignment` boundary. If unsuccessful, `block` will be unmodified.
+ ///
+ /// This method is static in order to consume and replace the given block
+ /// pointer with a pointer to the new, smaller block. An additional block may
+ /// be created for the leading space.
+ ///
+ /// @pre The block must not be in use.v
+ ///
+ /// @retval OK The split completed successfully.
+ /// @retval FAILED_PRECONDITION This block is in use and cannot be split.
+ /// @retval OUT_OF_RANGE The requested size is greater than the
+ /// current size.
+ /// @retval RESOURCE_EXHAUSTED The remaining space is too small to hold a
+ /// new block.
+ static Status AllocLast(Block*& block, size_t inner_size, size_t alignment);
+
+ /// Marks the block as free and merges it with any free neighbors.
+ ///
+ /// This method is static in order to consume and replace the given block
+ /// pointer. If neither member is free, the returned pointer will point to the
+ /// original block. Otherwise, it will point to the new, larger block created
+ /// by merging adjacent free blocks together.
+ static void Free(Block*& block);
+
+ /// Grows or shrinks the block.
+ ///
+ /// If successful, `block` may be merged with the block after it in order to
+ /// provide additional memory (when growing) or to merge released memory (when
+ /// shrinking). If unsuccessful, `block` will be unmodified.
+ ///
+ /// This method is static in order to consume and replace the given block
+ /// pointer with a pointer to the new, smaller block.
+ ///
+ /// @pre The block must be in use.
+ ///
+ /// @retval OK The resize completed successfully.
+ /// @retval FAILED_PRECONDITION This block is not in use.
+ /// @retval OUT_OF_RANGE The requested size is greater than the
+ /// available space.
+ static Status Resize(Block*& block, size_t new_inner_size);
+
+ /// Attempts to split this block.
+ ///
+ /// If successful, the block will have an inner size of `new_inner_size`,
+ /// rounded up to a `kAlignment` boundary. The remaining space will be
+ /// returned as a new block.
+ ///
+ /// This method may fail if the remaining space is too small to hold a new
+ /// block. If this method fails for any reason, the original block is
+ /// unmodified.
+ ///
+ /// This method is static in order to consume and replace the given block
+ /// pointer with a pointer to the new, smaller block.
+ ///
+ /// @pre The block must not be in use.
+ ///
+ /// @retval OK The split completed successfully.
+ /// @retval FAILED_PRECONDITION This block is in use and cannot be split.
+ /// @retval OUT_OF_RANGE The requested size for this block is greater
+ /// than the current `inner_size`.
+ /// @retval RESOURCE_EXHAUSTED The remaining space is too small to hold a
+ /// new block.
+ static Result<Block*> Split(Block*& block, size_t new_inner_size);
+
+ /// Merges this block with the one that comes after it.
+ ///
+ /// This method is static in order to consume and replace the given block
+ /// pointer with a pointer to the new, larger block.
+ ///
+ /// @pre The blocks must not be in use.
+ ///
+ /// @retval OK The merge was successful.
+ /// @retval OUT_OF_RANGE The given block is the last block.
+ /// @retval FAILED_PRECONDITION One or more of the blocks is in use.
+ static Status MergeNext(Block*& block);
+
+ /// Fetches the block immediately after this one.
+ ///
+ /// For performance, this always returns a block pointer, even if the returned
+ /// pointer is invalid. The pointer is valid if and only if `Last()` is false.
+ ///
+ /// Typically, after calling `Init` callers may save a pointer past the end of
+ /// the list using `Next()`. This makes it easy to subsequently iterate over
+ /// the list:
+ /// @code{.cpp}
+ /// auto result = Block<>::Init(byte_span);
+ /// Block<>* begin = *result;
+ /// Block<>* end = begin->Next();
+ /// ...
+ /// for (auto* block = begin; block != end; block = block->Next()) {
+ /// // Do something which each block.
+ /// }
+ /// @endcode
+ Block* Next() const;
+
+ /// @copydoc `Next`.
+ static Block* NextBlock(const Block* block) { return block->Next(); }
+
+ /// @returns The block immediately before this one, or a null pointer if this
+ /// is the first block.
+ Block* Prev() const;
+
+ /// @copydoc `Prev`.
+ static Block* PrevBlock(const Block* block) { return block->Prev(); }
+
+ /// Indicates whether the block is in use.
+ ///
+ /// @returns `true` if the block is in use or `false` if not.
+ bool Used() const { return (prev_ & kBuiltinFlag) != 0; }
+
+ /// Indicates whether this block is the last block or not (i.e. whether
+ /// `Next()` points to a valid block or not). This is needed because
+ /// `Next()` points to the end of this block, whether there is a valid
+ /// block there or not.
+ ///
+ /// @returns `true` is this is the last block or `false` if not.
+ bool Last() const { return (next_ & kBuiltinFlag) != 0; }
+
+ /// Marks this block as in use.
+ void MarkUsed() { prev_ |= kBuiltinFlag; }
+
+ /// Marks this block as free.
+ void MarkFree() { prev_ &= ~kBuiltinFlag; }
+
+ /// Marks this block as the last one in the chain.
+ void MarkLast() { next_ |= kBuiltinFlag; }
+
+ /// Clears the last bit from this block.
+ void ClearLast() { next_ &= ~kBuiltinFlag; }
+
+ /// Sets (and clears) custom flags for this block.
+ ///
+ /// The number of bits available for custom flags depends on the capacity of
+ /// the block, and is given by `kCustomFlagBits`. Only this many of the least
+ /// significant bits of `flags_to_set` and `flags_to_clear` are considered;
+ /// any others are ignored. Refer to the class level documentation for the
+ /// exact bit layout.
+ ///
+ /// Custom flags are not copied when a block is split, and are unchanged when
+ /// merging for the block that remains valid after the merge.
+ ///
+ /// If `flags_to_clear` are provided, these bits will be cleared before
+ /// setting the `flags_to_set`. As a consequence, if a bit is set in both
+ /// `flags_to_set` and `flags_to_clear`, it will be set upon return.
+ ///
+ /// @param[in] flags_to_set Bit flags to enable.
+ /// @param[in] flags_to_clear Bit flags to disable.
+ void SetFlags(UintType flags_to_set, UintType flags_to_clear = 0);
+
+ /// Returns the custom flags previously set on this block.
+ UintType GetFlags();
+
+ /// @brief Checks if a block is valid.
+ ///
+ /// @returns `true` if and only if the following conditions are met:
+ /// * The block is aligned.
+ /// * The prev/next fields match with the previous and next blocks.
+ /// * The poisoned bytes are not damaged (if poisoning is enabled).
+ bool IsValid() const { return CheckStatus() == BlockStatus::kValid; }
+
+ /// @brief Crashes with an informtaional message if a block is invalid.
+ ///
+ /// Does nothing if the block is valid.
void CrashIfInvalid();
private:
- static constexpr uintptr_t kInUseFlag = 0x1;
- static constexpr uintptr_t kLastFlag = 0x2;
- static constexpr std::byte POISON_PATTERN[8] = {std::byte{0x92},
- std::byte{0x88},
- std::byte{0x0a},
- std::byte{0x00},
- std::byte{0xec},
- std::byte{0xdc},
- std::byte{0xae},
- std::byte{0x4e}};
- enum BlockStatus {
- VALID,
- MISALIGNED,
- PREV_MISMATCHED,
- NEXT_MISMATCHED,
- POISON_CORRUPTED
- };
+ static constexpr UintType kMaxOffset = UintType(kMaxSize / kAlignment);
+ static constexpr size_t kCustomFlagBitsPerField =
+ cpp20::countl_zero(kMaxOffset) - 1;
+ static constexpr size_t kCustomFlagBits = kCustomFlagBitsPerField * 2;
+ static constexpr size_t kOffsetBits = cpp20::bit_width(kMaxOffset);
+ static constexpr UintType kBuiltinFlag = UintType(1) << kOffsetBits;
+ static constexpr UintType kOffsetMask = kBuiltinFlag - 1;
+ static constexpr size_t kCustomFlagShift = kOffsetBits + 1;
+ static constexpr UintType kCustomFlagMask = ~(kOffsetMask | kBuiltinFlag);
+
+ Block(size_t prev_offset, size_t next_offset);
- Block() = default;
+ /// Consumes the block and returns as a span of bytes.
+ static ByteSpan AsBytes(Block*&& block);
- // Helper to reduce some of the casting nesting in the block management
- // functions.
- uintptr_t NextAsUIntPtr() const { return reinterpret_cast<uintptr_t>(next_); }
+ /// Consumes the span of bytes and uses it to construct and return a block.
+ static Block* AsBlock(size_t prev_offset, ByteSpan bytes);
- void PoisonBlock();
- bool CheckPoisonBytes() const;
+ /// Returns a `BlockStatus` that is either kValid or indicates the reason why
+ /// the block is invalid.
+ ///
+ /// If the block is invalid at multiple points, this function will only return
+ /// one of the reasons.
BlockStatus CheckStatus() const;
- // Note: Consider instead making these next/prev offsets from the current
- // block, with templated type for the offset size. There are some interesting
- // tradeoffs here; perhaps a pool of small allocations could use 1-byte
- // next/prev offsets to reduce size further.
- Block* next_;
- Block* prev_;
+ /// Extracts the offset portion from `next_` or `prev_`.
+ static size_t GetOffset(UintType packed) {
+ return static_cast<size_t>(packed & kOffsetMask) * kAlignment;
+ }
+
+ /// Overwrites the offset portion of `next_` or `prev_`.
+ static void SetOffset(UintType& field, size_t offset) {
+ field = (field & ~kOffsetMask) | static_cast<UintType>(offset) / kAlignment;
+ }
+
+ UintType next_ = 0;
+ UintType prev_ = 0;
+
+ public:
+ // Associated types.
+
+ /// Represents an iterator that moves forward through a list of blocks.
+ ///
+ /// This class is not typically instantiated directly, but rather using a
+ /// range-based for-loop using `Block::Range`.
+ class Iterator : public BaseIterator<Block, NextBlock> {
+ public:
+ Iterator(Block* block) : BaseIterator<Block, NextBlock>(block) {}
+ };
+
+ /// Represents an iterator that moves forward through a list of blocks.
+ ///
+ /// This class is not typically instantiated directly, but rather using a
+ /// range-based for-loop using `Block::ReverseRange`.
+ class ReverseIterator : public BaseIterator<Block, PrevBlock> {
+ public:
+ ReverseIterator(Block* block) : BaseIterator<Block, PrevBlock>(block) {}
+ };
+
+ /// Represents a range of blocks that can be iterated over.
+ ///
+ /// The typical usage of this class is in a range-based for-loop, e.g.
+ /// @code{.cpp}
+ /// for (auto* block : Range(first, last)) { ... }
+ /// @endcode
+ class Range : public BaseRange<Block, Iterator> {
+ public:
+ /// Constructs a range including `begin` and all valid following blocks.
+ explicit Range(Block* begin) : BaseRange<Block, Iterator>(begin, nullptr) {}
+
+ /// Constructs a range of blocks from `begin` to `end`, inclusively.
+ Range(Block* begin_inclusive, Block* end_inclusive)
+ : BaseRange<Block, Iterator>(begin_inclusive, end_inclusive->Next()) {}
+ };
+
+ /// Represents a range of blocks that can be iterated over in the reverse
+ /// direction.
+ ///
+ /// The typical usage of this class is in a range-based for-loop, e.g.
+ /// @code{.cpp}
+ /// for (auto* block : ReverseRange(last, first)) { ... }
+ /// @endcode
+ class ReverseRange : public BaseRange<Block, ReverseIterator> {
+ public:
+ /// Constructs a range including `rbegin` and all valid preceding blocks.
+ explicit ReverseRange(Block* rbegin)
+ : BaseRange<Block, ReverseIterator>(rbegin, nullptr) {}
+
+ /// Constructs a range of blocks from `rbegin` to `rend`, inclusively.
+ ReverseRange(Block* rbegin_inclusive, Block* rend_inclusive)
+ : BaseRange<Block, ReverseIterator>(rbegin_inclusive,
+ rend_inclusive->Prev()) {}
+ };
};
+// Public template method implementations.
+
+template <typename UintType, size_t kMaxSize>
+Result<Block<UintType, kMaxSize>*> Block<UintType, kMaxSize>::Init(
+ ByteSpan region) {
+ if (reinterpret_cast<uintptr_t>(region.data()) % kAlignment != 0) {
+ return Status::InvalidArgument();
+ }
+ if (region.size() < kBlockOverhead) {
+ return Status::ResourceExhausted();
+ }
+ if (kMaxSize < region.size()) {
+ return Status::OutOfRange();
+ }
+ Block* block = AsBlock(0, region);
+ block->MarkLast();
+ BaseBlock::Poison(block, kHeaderSize, block->OuterSize());
+ return block;
+}
+
+template <typename UintType, size_t kMaxSize>
+Status Block<UintType, kMaxSize>::AllocFirst(Block*& block,
+ size_t inner_size,
+ size_t alignment) {
+ if (block->Used()) {
+ return Status::FailedPrecondition();
+ }
+ // Check if padding will be needed at the front to align the usable space.
+ size_t pad_outer_size = 0;
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ if (addr % alignment != 0) {
+ pad_outer_size = AlignUp(addr + kBlockOverhead, alignment) - addr;
+ inner_size += pad_outer_size;
+ }
+
+ // Split the block to get the requested usable space. It is not an error if
+ // the block is too small to split off a new trailing block.
+ Result<Block*> result = Block::Split(block, inner_size);
+ if (!result.ok() && result.status() != Status::ResourceExhausted()) {
+ return result.status();
+ }
+
+ // If present, split the padding off the front. Since this space was included
+ // in the previous split, it should always succeed.
+ if (pad_outer_size != 0) {
+ result = Block::Split(block, pad_outer_size - kBlockOverhead);
+ block = *result;
+ }
+
+ block->MarkUsed();
+ return OkStatus();
+}
+
+template <typename UintType, size_t kMaxSize>
+Status Block<UintType, kMaxSize>::AllocLast(Block*& block,
+ size_t inner_size,
+ size_t alignment) {
+ if (block->Used()) {
+ return Status::FailedPrecondition();
+ }
+ // Find the last address that is aligned and is followed by enough space for
+ // block overhead and the requested size.
+ if (block->InnerSize() < inner_size) {
+ return Status::OutOfRange();
+ }
+ alignment = std::max(alignment, kAlignment);
+ auto addr = reinterpret_cast<uintptr_t>(block->UsableSpace());
+ uintptr_t next =
+ AlignDown(addr + (block->InnerSize() - inner_size), alignment);
+ if (next != addr) {
+ if (next < addr + kBlockOverhead) {
+ // A split is needed, but no block will fit.
+ return Status::ResourceExhausted();
+ }
+ size_t pad_inner_size = next - (addr + kBlockOverhead);
+ Result<Block*> result = Block::Split(block, pad_inner_size);
+ if (!result.ok()) {
+ return result.status();
+ }
+ block = *result;
+ }
+ block->MarkUsed();
+ return OkStatus();
+}
+
+template <typename UintType, size_t kMaxSize>
+void Block<UintType, kMaxSize>::Free(Block*& block) {
+ block->MarkFree();
+ Block* prev = block->Prev();
+ if (Block::MergeNext(prev).ok()) {
+ block = prev;
+ }
+ Block::MergeNext(block).IgnoreError();
+}
+
+template <typename UintType, size_t kMaxSize>
+Status Block<UintType, kMaxSize>::Resize(Block*& block, size_t new_inner_size) {
+ if (!block->Used()) {
+ return Status::FailedPrecondition();
+ }
+ size_t old_inner_size = block->InnerSize();
+ size_t aligned_inner_size = AlignUp(new_inner_size, kAlignment);
+ if (old_inner_size == aligned_inner_size) {
+ return OkStatus();
+ }
+
+ // Treat the block as free and try to combine it with the next block. At most
+ // one free block is expecte to follow this block.
+ block->MarkFree();
+ MergeNext(block).IgnoreError();
+
+ // Try to split off a block of the requested size.
+ Status status = Block::Split(block, aligned_inner_size).status();
+
+ // It is not an error if the split fails because the remainder is too small
+ // for a block.
+ if (status == Status::ResourceExhausted()) {
+ status = OkStatus();
+ }
+
+ // Otherwise, restore the original block on failure.
+ if (!status.ok()) {
+ Split(block, old_inner_size).IgnoreError();
+ }
+ block->MarkUsed();
+ return status;
+}
+
+template <typename UintType, size_t kMaxSize>
+Result<Block<UintType, kMaxSize>*> Block<UintType, kMaxSize>::Split(
+ Block*& block, size_t new_inner_size) {
+ if (block->Used()) {
+ return Status::FailedPrecondition();
+ }
+ size_t old_inner_size = block->InnerSize();
+ size_t aligned_inner_size = AlignUp(new_inner_size, kAlignment);
+ if (old_inner_size < new_inner_size || old_inner_size < aligned_inner_size) {
+ return Status::OutOfRange();
+ }
+ if (old_inner_size - aligned_inner_size < kBlockOverhead) {
+ return Status::ResourceExhausted();
+ }
+ size_t prev_offset = GetOffset(block->prev_);
+ size_t outer_size1 = aligned_inner_size + kBlockOverhead;
+ bool is_last = block->Last();
+ UintType flags = block->GetFlags();
+ ByteSpan bytes = AsBytes(std::move(block));
+ Block* block1 = AsBlock(prev_offset, bytes.subspan(0, outer_size1));
+ Block* block2 = AsBlock(outer_size1, bytes.subspan(outer_size1));
+ size_t outer_size2 = block2->OuterSize();
+ if (is_last) {
+ block2->MarkLast();
+ } else {
+ SetOffset(block2->Next()->prev_, outer_size2);
+ }
+ block1->SetFlags(flags);
+ BaseBlock::Poison(block1, kHeaderSize, outer_size1);
+ BaseBlock::Poison(block2, kHeaderSize, outer_size2);
+ block = std::move(block1);
+ return block2;
+}
+
+template <typename UintType, size_t kMaxSize>
+Status Block<UintType, kMaxSize>::MergeNext(Block*& block) {
+ if (block == nullptr || block->Last()) {
+ return Status::OutOfRange();
+ }
+ Block* next = block->Next();
+ if (block->Used() || next->Used()) {
+ return Status::FailedPrecondition();
+ }
+ size_t prev_offset = GetOffset(block->prev_);
+ bool is_last = next->Last();
+ UintType flags = block->GetFlags();
+ ByteSpan prev_bytes = AsBytes(std::move(block));
+ ByteSpan next_bytes = AsBytes(std::move(next));
+ size_t next_offset = prev_bytes.size() + next_bytes.size();
+ std::byte* merged = ::new (prev_bytes.data()) std::byte[next_offset];
+ block = AsBlock(prev_offset, ByteSpan(merged, next_offset));
+ if (is_last) {
+ block->MarkLast();
+ } else {
+ SetOffset(block->Next()->prev_, GetOffset(block->next_));
+ }
+ block->SetFlags(flags);
+ return OkStatus();
+}
+
+template <typename UintType, size_t kMaxSize>
+Block<UintType, kMaxSize>* Block<UintType, kMaxSize>::Next() const {
+ size_t offset = GetOffset(next_);
+ uintptr_t addr = Last() ? 0 : reinterpret_cast<uintptr_t>(this) + offset;
+ // See the note in `FromUsableSpace` about memory laundering.
+ return std::launder(reinterpret_cast<Block*>(addr));
+}
+
+template <typename UintType, size_t kMaxSize>
+Block<UintType, kMaxSize>* Block<UintType, kMaxSize>::Prev() const {
+ size_t offset = GetOffset(prev_);
+ uintptr_t addr =
+ (offset == 0) ? 0 : reinterpret_cast<uintptr_t>(this) - offset;
+ // See the note in `FromUsableSpace` about memory laundering.
+ return std::launder(reinterpret_cast<Block*>(addr));
+}
+
+template <typename UintType, size_t kMaxSize>
+void Block<UintType, kMaxSize>::SetFlags(UintType flags_to_set,
+ UintType flags_to_clear) {
+ UintType hi_flags_to_set = flags_to_set >> kCustomFlagBitsPerField;
+ hi_flags_to_set <<= kCustomFlagShift;
+ UintType hi_flags_to_clear = (flags_to_clear >> kCustomFlagBitsPerField)
+ << kCustomFlagShift;
+ UintType lo_flags_to_set =
+ (flags_to_set & ((UintType(1) << kCustomFlagBitsPerField) - 1))
+ << kCustomFlagShift;
+ UintType lo_flags_to_clear =
+ (flags_to_clear & ((UintType(1) << kCustomFlagBitsPerField) - 1))
+ << kCustomFlagShift;
+ prev_ = (prev_ & ~hi_flags_to_clear) | hi_flags_to_set;
+ next_ = (next_ & ~lo_flags_to_clear) | lo_flags_to_set;
+}
+
+template <typename UintType, size_t kMaxSize>
+UintType Block<UintType, kMaxSize>::GetFlags() {
+ UintType hi_flags = (prev_ & kCustomFlagMask) >> kCustomFlagShift;
+ UintType lo_flags = (next_ & kCustomFlagMask) >> kCustomFlagShift;
+ return (hi_flags << kCustomFlagBitsPerField) | lo_flags;
+}
+
+// Private template method implementations.
+
+template <typename UintType, size_t kMaxSize>
+Block<UintType, kMaxSize>::Block(size_t prev_offset, size_t next_offset)
+ : BaseBlock() {
+ SetOffset(prev_, prev_offset);
+ SetOffset(next_, next_offset);
+}
+
+template <typename UintType, size_t kMaxSize>
+ByteSpan Block<UintType, kMaxSize>::AsBytes(Block*&& block) {
+ size_t block_size = block->OuterSize();
+ std::byte* bytes = ::new (std::move(block)) std::byte[block_size];
+ return {bytes, block_size};
+}
+
+template <typename UintType, size_t kMaxSize>
+Block<UintType, kMaxSize>* Block<UintType, kMaxSize>::AsBlock(
+ size_t prev_offset, ByteSpan bytes) {
+ return ::new (bytes.data()) Block(prev_offset, bytes.size());
+}
+
+template <typename UintType, size_t kMaxSize>
+typename Block<UintType, kMaxSize>::BlockStatus
+Block<UintType, kMaxSize>::CheckStatus() const {
+ // Make sure the Block is aligned.
+ if (reinterpret_cast<uintptr_t>(this) % kAlignment != 0) {
+ return BlockStatus::kMisaligned;
+ }
+
+ // Test if the prev/next pointer for this Block matches.
+ if (!Last() && (this >= Next() || this != Next()->Prev())) {
+ return BlockStatus::kNextMismatched;
+ }
+
+ if (Prev() && (this <= Prev() || this != Prev()->Next())) {
+ return BlockStatus::kPrevMismatched;
+ }
+
+ if (!CheckPoison(this, kHeaderSize, OuterSize())) {
+ return BlockStatus::kPoisonCorrupted;
+ }
+
+ return BlockStatus::kValid;
+}
+
+template <typename UintType, size_t kMaxSize>
+void Block<UintType, kMaxSize>::CrashIfInvalid() {
+ uintptr_t addr = reinterpret_cast<uintptr_t>(this);
+ switch (CheckStatus()) {
+ case kValid:
+ break;
+ case kMisaligned:
+ CrashMisaligned(addr);
+ break;
+ case kNextMismatched:
+ CrashNextMismatched(addr, reinterpret_cast<uintptr_t>(Next()->Prev()));
+ break;
+ case kPrevMismatched:
+ CrashPrevMismatched(addr, reinterpret_cast<uintptr_t>(Prev()->Next()));
+ break;
+ case kPoisonCorrupted:
+ CrashPoisonCorrupted(addr);
+ break;
+ }
+}
+
} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/fallback_allocator.h b/pw_allocator/public/pw_allocator/fallback_allocator.h
new file mode 100644
index 000000000..5707c1aee
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/fallback_allocator.h
@@ -0,0 +1,51 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_allocator/allocator.h"
+#include "pw_status/status.h"
+
+namespace pw::allocator {
+
+/// This class simply dispatches between a primary and secondary allocator. Any
+/// attempt to allocate memory will first be handled by the primary allocator.
+/// If it cannot allocate memory, e.g. because it is out of memory, the
+/// secondary alloator will try to allocate memory instead.
+class FallbackAllocator : public Allocator {
+ public:
+ constexpr FallbackAllocator() = default;
+
+ /// Sets the primary and secondary allocators.
+ ///
+ /// It is an error to call any method without calling this method first.
+ void Initialize(Allocator& primary, Allocator& secondary);
+
+ private:
+ /// @copydoc Allocator::Query
+ Status DoQuery(const void* ptr, Layout layout) const override;
+
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout layout) override;
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void* ptr, Layout layout) override;
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void* ptr, Layout layout, size_t new_size) override;
+
+ Allocator* primary_ = nullptr;
+ Allocator* secondary_ = nullptr;
+};
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/freelist.h b/pw_allocator/public/pw_allocator/freelist.h
index a5a50416e..f8200ea6f 100644
--- a/pw_allocator/public/pw_allocator/freelist.h
+++ b/pw_allocator/public/pw_allocator/freelist.h
@@ -24,34 +24,38 @@ namespace pw::allocator {
template <size_t kNumBuckets>
class FreeListBuffer;
-// Basic freelist implementation for an allocator.
-// This implementation buckets by chunk size, with a list of user-provided
-// buckets. Each bucket is a linked list of storage chunks. Because this
-// freelist uses the added chunks themselves as list nodes, there is lower bound
-// of sizeof(FreeList.FreeListNode) bytes for chunks which can be added to this
-// freelist. There is also an implicit bucket for "everything else", for chunks
-// which do not fit into a bucket.
-//
-// Each added chunk will be added to the smallest bucket under which it fits. If
-// it does not fit into any user-provided bucket, it will be added to the
-// default bucket.
-//
-// As an example, assume that the FreeList is configured with buckets of sizes
-// {64, 128, 256 and 512} bytes. The internal state may look like the following.
-//
-// bucket[0] (64B) --> chunk[12B] --> chunk[42B] --> chunk[64B] --> NULL
-// bucket[1] (128B) --> chunk[65B] --> chunk[72B] --> NULL
-// bucket[2] (256B) --> NULL
-// bucket[3] (512B) --> chunk[312B] --> chunk[512B] --> chunk[416B] --> NULL
-// bucket[4] (implicit) --> chunk[1024B] --> chunk[513B] --> NULL
-//
-// Note that added chunks should be aligned to a 4-byte boundary.
-//
-// This class is split into two parts; FreeList implements all of the
-// logic, and takes in pointers for two pw::Vector instances for its storage.
-// This prevents us from having to specialise this class for every kMaxSize
-// parameter for the vector. FreeListBuffer then provides the storage for these
-// two pw::Vector instances and instantiates FreeListInternal.
+/// Basic [freelist](https://en.wikipedia.org/wiki/Free_list) implementation
+/// for an allocator. This implementation buckets by chunk size, with a list
+/// of user-provided buckets. Each bucket is a linked list of storage chunks.
+/// Because this freelist uses the added chunks themselves as list nodes, there
+/// is a lower bound of `sizeof(FreeList.FreeListNode)` bytes for chunks which
+/// can be added to this freelist. There is also an implicit bucket for
+/// "everything else", for chunks which do not fit into a bucket.
+///
+/// Each added chunk will be added to the smallest bucket under which it fits.
+/// If it does not fit into any user-provided bucket, it will be added to the
+/// default bucket.
+///
+/// As an example, assume that the `FreeList` is configured with buckets of
+/// sizes {64, 128, 256, and 512} bytes. The internal state may look like the
+/// following:
+///
+/// @code{.unparsed}
+/// bucket[0] (64B) --> chunk[12B] --> chunk[42B] --> chunk[64B] --> NULL
+/// bucket[1] (128B) --> chunk[65B] --> chunk[72B] --> NULL
+/// bucket[2] (256B) --> NULL
+/// bucket[3] (512B) --> chunk[312B] --> chunk[512B] --> chunk[416B] --> NULL
+/// bucket[4] (implicit) --> chunk[1024B] --> chunk[513B] --> NULL
+/// @endcode
+///
+/// Note that added chunks should be aligned to a 4-byte boundary.
+///
+/// This class is split into two parts:
+/// * `FreeList` implements all of the logic, and takes in pointers for two
+/// `pw::Vector` instances for its storage. This prevents us from having to
+/// specialise this class for every `kMaxSize` parameter for the vector.
+/// * `FreeListBuffer` then provides the storage for these two `pw::Vector`
+/// instances and instantiates `FreeListInternal`.
class FreeList {
public:
// Remove copy/move ctors
@@ -60,22 +64,30 @@ class FreeList {
FreeList& operator=(const FreeList& other) = delete;
FreeList& operator=(FreeList&& other) = delete;
- // Adds a chunk to this freelist. Returns:
- // OK: The chunk was added successfully
- // OUT_OF_RANGE: The chunk could not be added for size reasons (e.g. if
- // the chunk is too small to store the FreeListNode).
+ /// Adds a chunk to this freelist.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The chunk was added successfully.
+ /// * @pw_status{OUT_OF_RANGE} - The chunk could not be added for size
+ /// reasons (e.g. the chunk is too small to store the `FreeListNode`).
Status AddChunk(span<std::byte> chunk);
- // Finds an eligible chunk for an allocation of size `size`. Note that this
- // will return the first allocation possible within a given bucket, it does
- // not currently optimize for finding the smallest chunk. Returns a span
- // representing the chunk. This will be "valid" on success, and will have size
- // = 0 on failure (if there were no chunks available for that allocation).
+ /// Finds an eligible chunk for an allocation of size `size`.
+ ///
+ /// @note This returns the first allocation possible within a given bucket;
+ /// It does not currently optimize for finding the smallest chunk.
+ ///
+ /// @returns
+ /// * On success - A span representing the chunk.
+ /// * On failure (e.g. there were no chunks available for that allocation) -
+ /// A span with a size of 0.
span<std::byte> FindChunk(size_t size) const;
- // Remove a chunk from this freelist. Returns:
- // OK: The chunk was removed successfully
- // NOT_FOUND: The chunk could not be found in this freelist.
+ /// Removes a chunk from this freelist.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The chunk was removed successfully.
+ /// * @pw_status{NOT_FOUND} - The chunk could not be found in this freelist.
Status RemoveChunk(span<std::byte> chunk);
private:
diff --git a/pw_allocator/public/pw_allocator/freelist_heap.h b/pw_allocator/public/pw_allocator/freelist_heap.h
index 673302627..5c6b12b74 100644
--- a/pw_allocator/public/pw_allocator/freelist_heap.h
+++ b/pw_allocator/public/pw_allocator/freelist_heap.h
@@ -24,6 +24,8 @@ namespace pw::allocator {
class FreeListHeap {
public:
+ using BlockType = Block<>;
+
template <size_t kNumBuckets>
friend class FreeListHeapBuffer;
struct HeapStats {
@@ -44,7 +46,7 @@ class FreeListHeap {
void LogHeapStats();
private:
- span<std::byte> BlockToSpan(Block* block) {
+ span<std::byte> BlockToSpan(BlockType* block) {
return span<std::byte>(block->UsableSpace(), block->InnerSize());
}
diff --git a/pw_allocator/public/pw_allocator/libc_allocator.h b/pw_allocator/public/pw_allocator/libc_allocator.h
new file mode 100644
index 000000000..70f67f286
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/libc_allocator.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_allocator/allocator.h"
+
+namespace pw::allocator {
+
+/// Memory allocator that uses `malloc` and `free`.
+///
+/// TODO: b/301930507 - `aligned_alloc` is not portable. As a result, this
+/// allocator has a maximum alignment of `std::align_max_t`.
+class LibCAllocator : public Allocator {
+ public:
+ constexpr LibCAllocator() = default;
+
+ private:
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout layout) override;
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void* ptr, Layout layout) override;
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void* ptr, Layout layout, size_t new_size) override;
+
+ /// @copydoc Allocator::Reallocate
+ void* DoReallocate(void* ptr, Layout layout, size_t new_size) override;
+};
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/null_allocator.h b/pw_allocator/public/pw_allocator/null_allocator.h
new file mode 100644
index 000000000..b4c3ba6aa
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/null_allocator.h
@@ -0,0 +1,41 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_allocator/allocator.h"
+
+namespace pw::allocator {
+
+/// A memory allocator that always fails to allocate memory.
+///
+/// A null allocator may be useful as part of a larger framework if allocation
+/// should be disallowed under certain circumstances. For example, a function
+/// that returns different allocators based on an input parameter may return a
+/// null allocator when given an invalid or unsupported parameter value.
+class NullAllocator : public Allocator {
+ public:
+ constexpr NullAllocator() = default;
+
+ private:
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout) override { return nullptr; }
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void*, Layout) override {}
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void*, Layout, size_t) override { return false; }
+};
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/simple_allocator.h b/pw_allocator/public/pw_allocator/simple_allocator.h
new file mode 100644
index 000000000..73662d452
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/simple_allocator.h
@@ -0,0 +1,87 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_allocator/allocator.h"
+#include "pw_allocator/block.h"
+
+namespace pw::allocator {
+
+// DOCSTAG: [pw_allocator_examples_simple_allocator]
+/// Simple allocator that uses a list of `Block`s.
+class SimpleAllocator : public Allocator {
+ public:
+ using Block = pw::allocator::Block<>;
+ using Range = typename Block::Range;
+
+ constexpr SimpleAllocator() = default;
+
+ /// Initialize this allocator to allocate memory from `region`.
+ Status Init(ByteSpan region) {
+ auto result = Block::Init(region);
+ if (result.ok()) {
+ blocks_ = *result;
+ }
+ return result.status();
+ }
+
+ /// Return the range of blocks for this allocator.
+ Range blocks() { return Range(blocks_); }
+
+ private:
+ /// @copydoc Allocator::Query
+ Status DoQuery(const void* ptr, Layout) const override {
+ for (auto* block : Range(blocks_)) {
+ if (block->UsableSpace() == ptr) {
+ return OkStatus();
+ }
+ }
+ return Status::OutOfRange();
+ }
+
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout layout) override {
+ for (auto* block : Range(blocks_)) {
+ if (Block::AllocFirst(block, layout.size(), layout.alignment()).ok()) {
+ return block->UsableSpace();
+ }
+ }
+ return nullptr;
+ }
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void* ptr, Layout) override {
+ if (ptr == nullptr) {
+ return;
+ }
+ auto* bytes = static_cast<std::byte*>(ptr);
+ Block* block = Block::FromUsableSpace(bytes);
+ Block::Free(block);
+ }
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void* ptr, Layout, size_t new_size) override {
+ if (ptr == nullptr) {
+ return false;
+ }
+ auto* bytes = static_cast<std::byte*>(ptr);
+ Block* block = Block::FromUsableSpace(bytes);
+ return Block::Resize(block, new_size).ok();
+ }
+
+ Block* blocks_ = nullptr;
+};
+// DOCSTAG: [pw_allocator_examples_simple_allocator]
+
+} // namespace pw::allocator
diff --git a/pw_allocator/public/pw_allocator/split_free_list_allocator.h b/pw_allocator/public/pw_allocator/split_free_list_allocator.h
new file mode 100644
index 000000000..46fd9d971
--- /dev/null
+++ b/pw_allocator/public/pw_allocator/split_free_list_allocator.h
@@ -0,0 +1,288 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+
+#include "pw_allocator/allocator.h"
+#include "pw_allocator/block.h"
+#include "pw_bytes/alignment.h"
+#include "pw_bytes/span.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace pw::allocator {
+
+/// Block-independent base class of SplitFreeListAllocator.
+///
+/// This class contains static methods which do not depend on the template
+/// parameters of ``SplitFreeListAllocator`` that are used to determine block
+/// type. This allows the methods to be defined in a separate source file and
+/// use macros that cannot be used in headers, e.g. PW_CHECK.
+///
+/// This class should not be used directly. Instead, see
+/// ``SplitFreeListAllocator``.
+class BaseSplitFreeListAllocator : public Allocator {
+ protected:
+ constexpr BaseSplitFreeListAllocator() = default;
+
+ /// Crashes with an informational method that the given block is allocated.
+ ///
+ /// This method is meant to be called by ``SplitFreeListAllocator``s
+ /// destructor. There must not be any outstanding allocations from an
+ /// when it is destroyed.
+ static void CrashOnAllocated(void* allocated);
+};
+
+/// This memory allocator uses a free list to track unallocated blocks, with a
+/// twist: Allocations above or below a given threshold are taken from
+/// respectively lower or higher addresses from within the allocator's memory
+/// region. This has the effect of decreasing fragmentation as similarly-sized
+/// allocations are grouped together.
+///
+/// NOTE!! Do NOT use memory returned from this allocator as the backing for
+/// another allocator. If this is done, the `Query` method will incorrectly
+/// think pointers returned by that alloator were created by this one, and
+/// report that this allocator can de/reallocate them.
+template <typename BlockType = Block<>>
+class SplitFreeListAllocator : public BaseSplitFreeListAllocator {
+ public:
+ using Range = typename BlockType::Range;
+
+ constexpr SplitFreeListAllocator() = default;
+ ~SplitFreeListAllocator() override;
+
+ // Not copyable.
+ SplitFreeListAllocator(const SplitFreeListAllocator&) = delete;
+ SplitFreeListAllocator& operator=(const SplitFreeListAllocator&) = delete;
+
+ /// Sets the memory region to be used by this allocator, and the threshold at
+ /// which allocations are considerd "large" or "small". Large and small
+ /// allocations return lower and higher addresses, respectively.
+ ///
+ /// @param[in] region The memory region for this allocator.
+ /// @param[in] threshold Allocations of this size of larger are
+ /// "large" and come from lower addresses.
+ /// @retval OK The allocator is initialized.
+ /// @retval INVALID_ARGUMENT The memory region is null.
+ /// @retval RESOURCE_EXHAUSTED The region is too small for `BlockType`.
+ /// @retval OUT_OF_RANGE The region too large for `BlockType`.
+ Status Init(ByteSpan region, size_t threshold);
+
+ /// Returns an iterable range of blocks tracking the memory of this allocator.
+ Range blocks() const;
+
+ private:
+ using ReverseRange = typename BlockType::ReverseRange;
+
+ /// @copydoc Allocator::Dispatch
+ Status DoQuery(const void* ptr, Layout layout) const override;
+
+ /// @copydoc Allocator::Allocate
+ void* DoAllocate(Layout layout) override;
+
+ /// Allocate a large chunk of memory.
+ ///
+ /// Allocations larger than the threshold will be allocated from lower
+ /// addresses. If a block needs to be fragmented, the returned pointer will be
+ /// from the lower-addressed part of the block.
+ ///
+ /// @param[in] layout Describes the memory to be allocated.
+ void* DoAllocateLarge(Layout layout);
+
+ /// Allocate a small chunk of memory.
+ ///
+ /// Allocations smaller than the threshold will be allocated from higher
+ /// addresses. If a block needs to be fragmented, the returned pointer will be
+ /// from the higher-addressed part of the block.
+ ///
+ /// @param[in] layout Describes the memory to be allocated.
+ void* DoAllocateSmall(Layout layout);
+
+ /// @copydoc Allocator::Deallocate
+ void DoDeallocate(void* ptr, Layout layout) override;
+
+ /// @copydoc Allocator::Resize
+ bool DoResize(void* ptr, Layout layout, size_t new_size) override;
+
+ // Represents the entire memory region for this allocator.
+ void* begin_ = nullptr;
+ void* end_ = nullptr;
+
+ // Represents the range of blocks that include free blocks. Blocks outside
+ // this range are guaranteed to be in use. These are effectively cached values
+ // used to speed up searching for free blocks.
+ BlockType* first_free_ = nullptr;
+ BlockType* last_free_ = nullptr;
+
+ // The boundary between what are consider "small" and "large" allocations.
+ size_t threshold_ = 0;
+};
+
+// Template method implementations
+
+template <typename BlockType>
+SplitFreeListAllocator<BlockType>::~SplitFreeListAllocator() {
+ auto* begin = BlockType::FromUsableSpace(static_cast<std::byte*>(begin_));
+ for (auto* block : Range(begin)) {
+ if (block->Used()) {
+ CrashOnAllocated(block);
+ }
+ }
+}
+
+template <typename BlockType>
+typename BlockType::Range SplitFreeListAllocator<BlockType>::blocks() const {
+ auto* begin = BlockType::FromUsableSpace(static_cast<std::byte*>(begin_));
+ return Range(begin);
+}
+
+template <typename BlockType>
+Status SplitFreeListAllocator<BlockType>::Init(ByteSpan region,
+ size_t threshold) {
+ if (region.data() == nullptr) {
+ return Status::InvalidArgument();
+ }
+ if (BlockType::kCapacity < region.size()) {
+ return Status::OutOfRange();
+ }
+
+ // Blocks need to be aligned. Find the first aligned address, and use as much
+ // of the memory region as possible.
+ auto addr = reinterpret_cast<uintptr_t>(region.data());
+ auto aligned = AlignUp(addr, BlockType::kAlignment);
+ Result<BlockType*> result = BlockType::Init(region.subspan(aligned - addr));
+ if (!result.ok()) {
+ return result.status();
+ }
+
+ // Initially, the block list is a single free block.
+ BlockType* block = *result;
+ begin_ = block->UsableSpace();
+ end_ = reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(begin_) +
+ block->InnerSize());
+ first_free_ = block;
+ last_free_ = block;
+
+ threshold_ = threshold;
+ return OkStatus();
+}
+
+template <typename BlockType>
+Status SplitFreeListAllocator<BlockType>::DoQuery(const void* ptr,
+ Layout) const {
+ return (ptr < begin_ || end_ <= ptr) ? Status::OutOfRange() : OkStatus();
+}
+
+template <typename BlockType>
+void* SplitFreeListAllocator<BlockType>::DoAllocate(Layout layout) {
+ if (begin_ == nullptr || layout.size() == 0) {
+ return nullptr;
+ }
+ size_t size = layout.size();
+ size_t alignment = std::max(layout.alignment(), BlockType::kAlignment);
+ layout = Layout(size, alignment);
+ return size < threshold_ ? DoAllocateSmall(layout) : DoAllocateLarge(layout);
+}
+
+template <typename BlockType>
+void* SplitFreeListAllocator<BlockType>::DoAllocateSmall(Layout layout) {
+ // Update the cached last free block.
+ while (last_free_->Used() && first_free_ != last_free_) {
+ last_free_ = last_free_->Prev();
+ }
+ // Search backwards for the first block that can hold this allocation.
+ for (auto* block : ReverseRange(last_free_, first_free_)) {
+ if (BlockType::AllocLast(block, layout.size(), layout.alignment()).ok()) {
+ return block->UsableSpace();
+ }
+ }
+ // No valid block found.
+ return nullptr;
+}
+
+template <typename BlockType>
+void* SplitFreeListAllocator<BlockType>::DoAllocateLarge(Layout layout) {
+ // Update the cached first free block.
+ while (first_free_->Used() && first_free_ != last_free_) {
+ first_free_ = first_free_->Next();
+ }
+ // Search forwards for the first block that can hold this allocation.
+ for (auto* block : Range(first_free_, last_free_)) {
+ if (BlockType::AllocFirst(block, layout.size(), layout.alignment()).ok()) {
+ // A new last free block may be split off the end of the allocated block.
+ if (last_free_ <= block) {
+ last_free_ = block->Last() ? block : block->Next();
+ }
+ return block->UsableSpace();
+ }
+ }
+ // No valid block found.
+ return nullptr;
+}
+
+template <typename BlockType>
+void SplitFreeListAllocator<BlockType>::DoDeallocate(void* ptr, Layout) {
+ // Do nothing if uninitialized or no memory block pointer.
+ if (begin_ == nullptr || ptr < begin_ || end_ <= ptr) {
+ return;
+ }
+ auto* block = BlockType::FromUsableSpace(static_cast<std::byte*>(ptr));
+ block->CrashIfInvalid();
+
+ // Free the block and merge it with its neighbors, if possible.
+ BlockType::Free(block);
+
+ // Update the first and/or last free block pointers.
+ if (block < first_free_) {
+ first_free_ = block;
+ }
+ if (block->Last() || last_free_ < block->Next()) {
+ last_free_ = block;
+ }
+}
+
+template <typename BlockType>
+bool SplitFreeListAllocator<BlockType>::DoResize(void* ptr,
+ Layout layout,
+ size_t new_size) {
+ // Fail to resize is uninitialized or invalid parameters.
+ if (begin_ == nullptr || !DoQuery(ptr, layout).ok()) {
+ return false;
+ }
+
+ // Ensure that this allocation came from this object.
+ auto* block = BlockType::FromUsableSpace(static_cast<std::byte*>(ptr));
+ block->CrashIfInvalid();
+
+ bool next_is_first_free = !block->Last() && block->Next() == first_free_;
+ bool next_is_last_free = !block->Last() && block->Next() == last_free_;
+ if (!BlockType::Resize(block, new_size).ok()) {
+ return false;
+ }
+
+ // The block after this one may have grown or shrunk.
+ if (next_is_first_free) {
+ first_free_ = block->Last() ? block : block->Next();
+ }
+ if (next_is_last_free) {
+ last_free_ = block->Last() ? block : block->Next();
+ }
+ return true;
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/py/BUILD.gn b/pw_allocator/py/BUILD.gn
index afd151d00..53b7fd6ef 100644
--- a/pw_allocator/py/BUILD.gn
+++ b/pw_allocator/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_allocator/__init__.py",
diff --git a/pw_allocator/py/pw_allocator/heap_viewer.py b/pw_allocator/py/pw_allocator/heap_viewer.py
index 074fb2bc9..3b926d68a 100644
--- a/pw_allocator/py/pw_allocator/heap_viewer.py
+++ b/pw_allocator/py/pw_allocator/heap_viewer.py
@@ -144,7 +144,7 @@ def visualize(
pointer_size=4,
):
"""Visualization of heap usage."""
- # TODO(b/235282507): Add standarized mechanisms to produce dump file and
+ # TODO: b/235282507 - Add standarized mechanisms to produce dump file and
# read heap information from dump file.
aligned_bytes = pointer_size
header_size = pointer_size * 2
diff --git a/pw_allocator/simple_allocator_test.cc b/pw_allocator/simple_allocator_test.cc
new file mode 100644
index 000000000..4904bfe3b
--- /dev/null
+++ b/pw_allocator/simple_allocator_test.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/simple_allocator.h"
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator_testing.h"
+
+namespace pw::allocator {
+
+// Size of the memory region to use in the tests below.
+constexpr size_t kCapacity = 256;
+
+// An `SimpleAllocator` that is automatically initialized on construction.
+class SimpleAllocatorWithBuffer
+ : public test::WithBuffer<SimpleAllocator, kCapacity> {
+ public:
+ SimpleAllocatorWithBuffer() {
+ EXPECT_EQ((*this)->Init(ByteSpan(this->data(), this->size())), OkStatus());
+ }
+};
+
+// This is not meant to be a rigorous unit test of individual behaviors, as much
+// as simply a way to demonstrate and exercise the simple allocator.
+TEST(SimpleAllocatorTest, AllocateResizeDeallocate) {
+ SimpleAllocatorWithBuffer allocator;
+
+ // Can allocate usable memory.
+ constexpr size_t kSize1 = kCapacity / 4;
+ constexpr Layout layout1 = Layout::Of<std::byte[kSize1]>();
+ auto* ptr = static_cast<std::byte*>(allocator->Allocate(layout1));
+ ASSERT_NE(ptr, nullptr);
+ memset(ptr, 0x5A, kSize1);
+
+ // Can shrink memory. Contents are preserved.
+ constexpr size_t kSize2 = kCapacity / 8;
+ constexpr Layout layout2 = Layout::Of<std::byte[kSize2]>();
+ EXPECT_TRUE(allocator->Resize(ptr, layout1, layout2.size()));
+ for (size_t i = 0; i < kSize2; ++i) {
+ EXPECT_EQ(size_t(ptr[i]), 0x5Au);
+ }
+
+ // Can grow memory. Contents are preserved.
+ constexpr size_t kSize3 = kCapacity / 2;
+ constexpr Layout layout3 = Layout::Of<std::byte[kSize3]>();
+ EXPECT_TRUE(allocator->Resize(ptr, layout2, layout3.size()));
+ for (size_t i = 0; i < kSize2; ++i) {
+ EXPECT_EQ(size_t(ptr[i]), 0x5Au);
+ }
+
+ // Can free memory.
+ allocator->Deallocate(ptr, layout3);
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/size_report/BUILD.bazel b/pw_allocator/size_report/BUILD.bazel
new file mode 100644
index 000000000..3f1e5345b
--- /dev/null
+++ b/pw_allocator/size_report/BUILD.bazel
@@ -0,0 +1,54 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_binary(
+ name = "split_free_list_allocator",
+ srcs = ["split_free_list_allocator.cc"],
+ deps = [
+ "//pw_allocator:allocator_metric_proxy",
+ "//pw_allocator:split_free_list_allocator",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
+
+pw_cc_binary(
+ name = "split_free_list_allocator_with_unique_ptr",
+ srcs = ["split_free_list_allocator.cc"],
+ copts = ["-DSIZE_REPORT_UNIQUE_PTR=1"],
+ deps = [
+ "//pw_allocator:allocator_metric_proxy",
+ "//pw_allocator:split_free_list_allocator",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
+
+pw_cc_binary(
+ name = "split_free_list_allocator_with_metric_proxy",
+ srcs = ["split_free_list_allocator.cc"],
+ copts = ["-DSIZE_REPORT_METRIC_PROXY=1"],
+ deps = [
+ "//pw_allocator:allocator_metric_proxy",
+ "//pw_allocator:split_free_list_allocator",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
diff --git a/pw_allocator/size_report/BUILD.gn b/pw_allocator/size_report/BUILD.gn
new file mode 100644
index 000000000..1f73b0a68
--- /dev/null
+++ b/pw_allocator/size_report/BUILD.gn
@@ -0,0 +1,46 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_executable("split_free_list_allocator") {
+ sources = [ "split_free_list_allocator.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:allocator_metric_proxy",
+ "..:split_free_list_allocator",
+ ]
+}
+
+pw_executable("split_free_list_allocator_with_unique_ptr") {
+ sources = [ "split_free_list_allocator.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:allocator_metric_proxy",
+ "..:split_free_list_allocator",
+ ]
+ defines = [ "SIZE_REPORT_UNIQUE_PTR=1" ]
+}
+
+pw_executable("split_free_list_allocator_with_metric_proxy") {
+ sources = [ "split_free_list_allocator.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:allocator_metric_proxy",
+ "..:split_free_list_allocator",
+ ]
+ defines = [ "SIZE_REPORT_METRIC_PROXY=1" ]
+}
diff --git a/pw_allocator/size_report/split_free_list_allocator.cc b/pw_allocator/size_report/split_free_list_allocator.cc
new file mode 100644
index 000000000..42a51017c
--- /dev/null
+++ b/pw_allocator/size_report/split_free_list_allocator.cc
@@ -0,0 +1,131 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This size report uses pw::string::Format and std::snprintf to write a single
+// printf-style string to a buffer. The number of bytes written is returned.
+//
+// This compares the overhead of using pw::string::Format to directly calling
+// std::snprintf and determining the number of bytes written. It demonstrates
+// that the code for using pw::string::Format is much simpler.
+
+#include "pw_allocator/split_free_list_allocator.h"
+
+#include "pw_bloat/bloat_this_binary.h"
+
+#ifdef SIZE_REPORT_METRIC_PROXY
+#include "pw_allocator/allocator_metric_proxy.h"
+#endif // SIZE_REPORT_METRIC_PROXY
+
+namespace {
+
+pw::allocator::SplitFreeListAllocator allocator;
+
+#ifdef SIZE_REPORT_METRIC_PROXY
+pw::allocator::AllocatorMetricProxy proxy(0);
+#endif // SIZE_REPORT_METRIC_PROXY
+
+constexpr void* kFakeMemoryRegionStart = &allocator;
+constexpr size_t kFakeMemoryRegionSize = 4096;
+
+constexpr size_t kSplitFreeListThreshold = 128;
+
+} // namespace
+
+int main() {
+ pw::bloat::BloatThisBinary();
+
+ allocator.Init(
+ pw::ByteSpan(reinterpret_cast<std::byte*>(kFakeMemoryRegionStart),
+ kFakeMemoryRegionSize),
+ kSplitFreeListThreshold);
+
+ struct Foo {
+ char name[16];
+ };
+ struct Bar : public Foo {
+ int number;
+ };
+
+ // Small allocation.
+ Foo* foo =
+ static_cast<Foo*>(allocator.Allocate(pw::allocator::Layout::Of<Foo>()));
+ if (foo == nullptr) {
+ return 1;
+ }
+
+ foo->name[0] = '\0';
+
+ // Reallocate.
+ Bar* bar = static_cast<Bar*>(
+ allocator.Reallocate(foo, pw::allocator::Layout::Of<Foo>(), sizeof(Bar)));
+ if (bar == nullptr) {
+ return 1;
+ }
+
+ bar->number = 4;
+
+ // Large allocation.
+ struct Baz {
+ std::byte data[kSplitFreeListThreshold * 2];
+ };
+ Baz* baz =
+ static_cast<Baz*>(allocator.Allocate(pw::allocator::Layout::Of<Baz>()));
+ if (baz == nullptr) {
+ return 1;
+ }
+
+ baz->data[kSplitFreeListThreshold] = std::byte(0xf1);
+
+ // Deallocate.
+ allocator.Deallocate(bar, pw::allocator::Layout::Of<Bar>());
+ allocator.Deallocate(baz, pw::allocator::Layout::Of<Baz>());
+
+#ifdef SIZE_REPORT_UNIQUE_PTR
+
+ struct Point {
+ int x;
+ int y;
+
+ Point(int xx, int yy) : x(xx), y(yy) {}
+ };
+
+ {
+ std::optional<pw::allocator::UniquePtr<Point>> maybe_point =
+ allocator.MakeUnique<Point>(3, 4);
+ if (!maybe_point.has_value()) {
+ return 1;
+ }
+
+ pw::allocator::UniquePtr<Point> point = *maybe_point;
+ point->x = point->y * 2;
+ }
+
+#endif // SIZE_REPORT_UNIQUE_PTR
+
+#ifdef SIZE_REPORT_METRIC_PROXY
+ proxy.Initialize(allocator);
+
+ Foo* foo2 =
+ static_cast<Foo*>(proxy.Allocate(pw::allocator::Layout::Of<Foo>()));
+ if (foo2 == nullptr) {
+ return 1;
+ }
+
+ foo2->name[1] = 'a';
+
+ proxy.Deallocate(foo2, pw::allocator::Layout::Of<Foo>());
+#endif // SIZE_REPORT_METRIC_PROXY
+
+ return 0;
+}
diff --git a/pw_allocator/split_free_list_allocator.cc b/pw_allocator/split_free_list_allocator.cc
new file mode 100644
index 000000000..adae4da15
--- /dev/null
+++ b/pw_allocator/split_free_list_allocator.cc
@@ -0,0 +1,29 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/split_free_list_allocator.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::allocator {
+
+void BaseSplitFreeListAllocator::CrashOnAllocated(void* allocated) {
+ PW_DCHECK(false,
+ "The block at %p was still in use when its allocator was "
+ "destroyed. All memory allocated by an allocator must be released "
+ "before the allocator goes out of scope.",
+ allocated);
+}
+
+} // namespace pw::allocator
diff --git a/pw_allocator/split_free_list_allocator_test.cc b/pw_allocator/split_free_list_allocator_test.cc
new file mode 100644
index 000000000..c519849d6
--- /dev/null
+++ b/pw_allocator/split_free_list_allocator_test.cc
@@ -0,0 +1,420 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_allocator/split_free_list_allocator.h"
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator_testing.h"
+#include "pw_allocator/block.h"
+#include "pw_bytes/alignment.h"
+#include "pw_bytes/span.h"
+#include "pw_containers/vector.h"
+
+namespace pw::allocator {
+namespace {
+
+// Test fixtures.
+
+// Size of the memory region to use in the tests below.
+static constexpr size_t kCapacity = 256;
+
+// Minimum size of a "large" allocation; allocation less than this size are
+// considered "small".
+static constexpr size_t kThreshold = 64;
+
+// An `SplitFreeListAllocator` that is automatically initialized on
+// construction.
+using BlockType = Block<uint16_t, kCapacity>;
+class SplitFreeListAllocatorWithBuffer
+ : public test::
+ WithBuffer<SplitFreeListAllocator<BlockType>, kCapacity, BlockType> {
+ public:
+ SplitFreeListAllocatorWithBuffer() {
+ EXPECT_EQ((*this)->Init(ByteSpan(this->data(), this->size()), kThreshold),
+ OkStatus());
+ }
+};
+
+// Test case fixture that allows individual tests to cache allocations and
+// release them automatically on tear-down.
+class SplitFreeListAllocatorTest : public ::testing::Test {
+ protected:
+ static constexpr size_t kMaxSize = kCapacity - BlockType::kBlockOverhead;
+ static constexpr size_t kNumPtrs = 16;
+
+ void SetUp() override {
+ for (size_t i = 0; i < kNumPtrs; ++i) {
+ ptrs_[i] = nullptr;
+ }
+ }
+
+ // This method simply ensures the memory is usable by writing to it.
+ void UseMemory(void* ptr, size_t size) { memset(ptr, 0x5a, size); }
+
+ void TearDown() override {
+ for (size_t i = 0; i < kNumPtrs; ++i) {
+ if (ptrs_[i] != nullptr) {
+ // `SplitFreeListAllocator::Deallocate` doesn't actually use the layout,
+ // as the information it needs is encoded in the blocks.
+ allocator_->Deallocate(ptrs_[i], Layout::Of<void*>());
+ }
+ }
+ }
+
+ SplitFreeListAllocatorWithBuffer allocator_;
+
+ // Tests can store allocations in this array to have them automatically
+ // freed in `TearDown`, including on ASSERT failure. If pointers are manually
+ // deallocated, they should be set to null in the array.
+ void* ptrs_[kNumPtrs];
+};
+
+// Unit tests.
+
+TEST_F(SplitFreeListAllocatorTest, InitUnaligned) {
+ // The test fixture uses aligned memory to make it easier to reason about
+ // allocations, but that isn't strictly required.
+ SplitFreeListAllocator<Block<>> unaligned;
+ ByteSpan bytes(allocator_.data(), allocator_.size());
+ EXPECT_EQ(unaligned.Init(bytes.subspan(1), kThreshold), OkStatus());
+}
+
+TEST_F(SplitFreeListAllocatorTest, AllocateLarge) {
+ constexpr Layout layout = Layout::Of<std::byte[kThreshold]>();
+ ptrs_[0] = allocator_->Allocate(layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+ EXPECT_GE(ptrs_[0], allocator_.data());
+ EXPECT_LT(ptrs_[0], allocator_.data() + allocator_.size());
+ UseMemory(ptrs_[0], layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, AllocateSmall) {
+ // Returned pointer should not be from the beginning, but should still be in
+ // range. Exact pointer depends on allocator's minimum allocation size.
+ constexpr Layout layout = Layout::Of<uint8_t>();
+ ptrs_[0] = allocator_->Allocate(layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+ EXPECT_GT(ptrs_[0], allocator_.data());
+ EXPECT_LT(ptrs_[0], allocator_.data() + allocator_.size());
+ UseMemory(ptrs_[0], layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, AllocateTooLarge) {
+ ptrs_[0] = allocator_->Allocate(Layout::Of<std::byte[kCapacity * 2]>());
+ EXPECT_EQ(ptrs_[0], nullptr);
+}
+
+TEST_F(SplitFreeListAllocatorTest, AllocateLargeAlignment) {
+ constexpr size_t kSize = sizeof(uint32_t);
+ constexpr size_t kAlignment = 64;
+ ptrs_[0] = allocator_->Allocate(Layout(kSize, kAlignment));
+ ASSERT_NE(ptrs_[0], nullptr);
+ EXPECT_EQ(reinterpret_cast<uintptr_t>(ptrs_[0]) % kAlignment, 0U);
+ UseMemory(ptrs_[0], kSize);
+
+ ptrs_[1] = allocator_->Allocate(Layout(kSize, kAlignment));
+ ASSERT_NE(ptrs_[1], nullptr);
+ EXPECT_EQ(reinterpret_cast<uintptr_t>(ptrs_[1]) % kAlignment, 0U);
+ UseMemory(ptrs_[1], kSize);
+}
+
+TEST_F(SplitFreeListAllocatorTest, AllocateFromUnaligned) {
+ SplitFreeListAllocator<Block<>> unaligned;
+ ByteSpan bytes(allocator_.data(), allocator_.size());
+ ASSERT_EQ(unaligned.Init(bytes.subspan(1), kThreshold), OkStatus());
+
+ constexpr Layout layout = Layout::Of<std::byte[kThreshold + 8]>();
+ void* ptr = unaligned.Allocate(layout);
+ ASSERT_NE(ptr, nullptr);
+ UseMemory(ptr, layout.size());
+ unaligned.Deallocate(ptr, layout);
+}
+
+TEST_F(SplitFreeListAllocatorTest, AllocateAlignmentFailure) {
+ // Determine the total number of available bytes.
+ auto base = reinterpret_cast<uintptr_t>(allocator_.data());
+ uintptr_t addr = AlignUp(base, BlockType::kAlignment);
+ size_t outer_size = allocator_.size() - (addr - base);
+
+ // The first block is large....
+ addr += BlockType::kBlockOverhead + kThreshold;
+
+ // The next block is not aligned...
+ constexpr size_t kAlignment = 128;
+ uintptr_t next = AlignUp(addr + BlockType::kBlockOverhead, kAlignment / 4);
+ if (next % kAlignment == 0) {
+ next += kAlignment / 4;
+ }
+
+ // And the last block consumes the remaining space.
+ // size_t outer_size = allocator_->begin()->OuterSize();
+ size_t inner_size1 = next - addr;
+ size_t inner_size2 = kThreshold * 2;
+ size_t inner_size3 =
+ outer_size - (BlockType::kBlockOverhead * 3 + inner_size1 + inner_size2);
+
+ // Allocate all the blocks.
+ ptrs_[0] = allocator_->Allocate(Layout(inner_size1, 1));
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ ptrs_[1] = allocator_->Allocate(Layout(inner_size2, 1));
+ ASSERT_NE(ptrs_[1], nullptr);
+
+ ptrs_[2] = allocator_->Allocate(Layout(inner_size3, 1));
+ ASSERT_NE(ptrs_[2], nullptr);
+
+ // If done correctly, the second block's usable space should be unaligned.
+ EXPECT_NE(reinterpret_cast<uintptr_t>(ptrs_[1]) % kAlignment, 0U);
+
+ // Free the second region. This leaves an unaligned region available.
+ allocator_->Deallocate(ptrs_[1], Layout(inner_size2, 1));
+ ptrs_[1] = nullptr;
+
+ // The allocator should be unable to create an aligned region..
+ ptrs_[3] = allocator_->Allocate(Layout(inner_size2, kAlignment));
+ EXPECT_EQ(ptrs_[3], nullptr);
+}
+
+TEST_F(SplitFreeListAllocatorTest, DeallocateNull) {
+ constexpr Layout layout = Layout::Of<uint8_t>();
+ allocator_->Deallocate(nullptr, layout);
+}
+
+TEST_F(SplitFreeListAllocatorTest, DeallocateShuffled) {
+ constexpr Layout layout = Layout::Of<std::byte[32]>();
+ // Allocate until the pool is exhausted.
+ for (size_t i = 0; i < kNumPtrs; ++i) {
+ ptrs_[i] = allocator_->Allocate(layout);
+ if (ptrs_[i] == nullptr) {
+ break;
+ }
+ }
+ // Mix up the order of allocations.
+ for (size_t i = 0; i < kNumPtrs; ++i) {
+ if (i % 2 == 0 && i + 1 < kNumPtrs) {
+ std::swap(ptrs_[i], ptrs_[i + 1]);
+ }
+ if (i % 3 == 0 && i + 2 < kNumPtrs) {
+ std::swap(ptrs_[i], ptrs_[i + 2]);
+ }
+ }
+ // Deallocate everything.
+ for (size_t i = 0; i < kNumPtrs; ++i) {
+ allocator_->Deallocate(ptrs_[i], layout);
+ ptrs_[i] = nullptr;
+ }
+}
+
+TEST_F(SplitFreeListAllocatorTest, IterateOverBlocks) {
+ constexpr Layout layout1 = Layout::Of<std::byte[32]>();
+ constexpr Layout layout2 = Layout::Of<std::byte[16]>();
+
+ // Allocate eight blocks of alternating sizes. After this, the will also be a
+ // ninth, unallocated block of the remaining memory.
+ for (size_t i = 0; i < 4; ++i) {
+ ptrs_[i] = allocator_->Allocate(layout1);
+ ASSERT_NE(ptrs_[i], nullptr);
+ ptrs_[i + 4] = allocator_->Allocate(layout2);
+ ASSERT_NE(ptrs_[i + 4], nullptr);
+ }
+
+ // Deallocate every other block. After this there will be four more
+ // unallocated blocks, for a total of five.
+ for (size_t i = 0; i < 4; ++i) {
+ allocator_->Deallocate(ptrs_[i], layout1);
+ }
+
+ // Count the blocks. The unallocated ones vary in size, but the allocated ones
+ // should all be the same.
+ size_t free_count = 0;
+ size_t used_count = 0;
+ for (auto* block : allocator_->blocks()) {
+ if (block->Used()) {
+ EXPECT_GE(block->InnerSize(), layout2.size());
+ ++used_count;
+ } else {
+ ++free_count;
+ }
+ }
+ EXPECT_EQ(used_count, 4U);
+ EXPECT_EQ(free_count, 5U);
+}
+
+TEST_F(SplitFreeListAllocatorTest, QueryLargeValid) {
+ constexpr Layout layout = Layout::Of<std::byte[kThreshold * 2]>();
+ ptrs_[0] = allocator_->Allocate(layout);
+ EXPECT_EQ(allocator_->Query(ptrs_[0], layout), OkStatus());
+}
+
+TEST_F(SplitFreeListAllocatorTest, QuerySmallValid) {
+ constexpr Layout layout = Layout::Of<uint8_t>();
+ ptrs_[0] = allocator_->Allocate(layout);
+ EXPECT_EQ(allocator_->Query(ptrs_[0], layout), OkStatus());
+}
+
+TEST_F(SplitFreeListAllocatorTest, QueryInvalidPtr) {
+ constexpr Layout layout = Layout::Of<SplitFreeListAllocatorTest>();
+ EXPECT_EQ(allocator_->Query(this, layout), Status::OutOfRange());
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeNull) {
+ constexpr Layout old_layout = Layout::Of<uint8_t>();
+ size_t new_size = 1;
+ EXPECT_FALSE(allocator_->Resize(nullptr, old_layout, new_size));
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeSame) {
+ constexpr Layout old_layout = Layout::Of<uint32_t>();
+ ptrs_[0] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ constexpr Layout new_layout = Layout::Of<uint32_t>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[0], old_layout, new_layout.size()));
+ ASSERT_NE(ptrs_[0], nullptr);
+ UseMemory(ptrs_[0], new_layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeLargeSmaller) {
+ constexpr Layout old_layout = Layout::Of<std::byte[kMaxSize]>();
+ ptrs_[0] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ // Shrinking always succeeds.
+ constexpr Layout new_layout = Layout::Of<std::byte[kThreshold]>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[0], old_layout, new_layout.size()));
+ ASSERT_NE(ptrs_[0], nullptr);
+ UseMemory(ptrs_[0], new_layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeLargeLarger) {
+ constexpr Layout old_layout = Layout::Of<std::byte[kThreshold]>();
+ ptrs_[0] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ // Nothing after ptr, so `Resize` should succeed.
+ constexpr Layout new_layout = Layout::Of<std::byte[kMaxSize]>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[0], old_layout, new_layout.size()));
+ ASSERT_NE(ptrs_[0], nullptr);
+ UseMemory(ptrs_[0], new_layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeLargeLargerFailure) {
+ constexpr Layout old_layout = Layout::Of<std::byte[kThreshold]>();
+ ptrs_[0] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ ptrs_[1] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[1], nullptr);
+
+ // Memory after ptr is already allocated, so `Resize` should fail.
+ EXPECT_FALSE(allocator_->Resize(ptrs_[0], old_layout, kMaxSize));
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeLargeSmallerAcrossThreshold) {
+ constexpr Layout old_layout = Layout::Of<std::byte[kThreshold]>();
+ ptrs_[0] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ // Shrinking succeeds, and the pointer is unchanged even though it is now
+ // below the threshold.
+ constexpr Layout new_layout = Layout::Of<std::byte[kThreshold / 4]>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[0], old_layout, new_layout.size()));
+ ASSERT_NE(ptrs_[0], nullptr);
+ UseMemory(ptrs_[0], new_layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeSmallSmaller) {
+ constexpr Layout old_layout = Layout::Of<uint32_t>();
+ ptrs_[0] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ // Shrinking always succeeds.
+ constexpr Layout new_layout = Layout::Of<uint8_t>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[0], old_layout, new_layout.size()));
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeSmallLarger) {
+ // First, allocate a trailing block.
+ constexpr Layout layout1 = Layout::Of<std::byte[kThreshold / 4]>();
+ ptrs_[0] = allocator_->Allocate(layout1);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ // Next allocate the memory to be resized.
+ constexpr Layout old_layout = Layout::Of<std::byte[kThreshold / 4]>();
+ ptrs_[1] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[1], nullptr);
+
+ // Now free the trailing block.
+ allocator_->Deallocate(ptrs_[0], layout1);
+ ptrs_[0] = nullptr;
+
+ // And finally, resize. Since the memory after the block is available and big
+ // enough, `Resize` should succeed.
+ constexpr Layout new_layout = Layout::Of<std::byte[kThreshold / 2]>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[1], old_layout, new_layout.size()));
+ ASSERT_NE(ptrs_[1], nullptr);
+ UseMemory(ptrs_[1], new_layout.size());
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeSmallLargerFailure) {
+ // First, allocate a trailing block.
+ constexpr Layout layout1 = Layout::Of<std::byte[kThreshold / 4]>();
+ ptrs_[0] = allocator_->Allocate(layout1);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ // Next allocate the memory to be resized.
+ constexpr Layout old_layout = Layout::Of<std::byte[kThreshold / 4]>();
+ ptrs_[1] = allocator_->Allocate(old_layout);
+ ASSERT_NE(ptrs_[1], nullptr);
+
+ // Now free the trailing block.
+ allocator_->Deallocate(ptrs_[0], layout1);
+ ptrs_[0] = nullptr;
+
+ // And finally, resize. Since the memory after the block is available but not
+ // big enough, `Resize` should fail.
+ size_t new_size = 48;
+ EXPECT_FALSE(allocator_->Resize(ptrs_[1], old_layout, new_size));
+}
+
+TEST_F(SplitFreeListAllocatorTest, ResizeSmallLargerAcrossThreshold) {
+ // First, allocate several trailing block.
+ constexpr Layout layout1 = Layout::Of<std::byte[kThreshold / 2]>();
+ ptrs_[0] = allocator_->Allocate(layout1);
+ ASSERT_NE(ptrs_[0], nullptr);
+
+ ptrs_[1] = allocator_->Allocate(layout1);
+ ASSERT_NE(ptrs_[1], nullptr);
+
+ // Next allocate the memory to be resized.
+ constexpr Layout old_layout = Layout::Of<std::byte[kThreshold / 4]>();
+ ptrs_[2] = allocator_->Allocate(old_layout);
+ EXPECT_NE(ptrs_[2], nullptr);
+
+ // Now free the trailing blocks.
+ allocator_->Deallocate(ptrs_[0], layout1);
+ ptrs_[0] = nullptr;
+ allocator_->Deallocate(ptrs_[1], layout1);
+ ptrs_[1] = nullptr;
+
+ // Growing succeeds, and the pointer is unchanged even though it is now
+ // above the threshold.
+ constexpr Layout new_layout = Layout::Of<std::byte[kThreshold]>();
+ EXPECT_TRUE(allocator_->Resize(ptrs_[2], old_layout, new_layout.size()));
+ ASSERT_NE(ptrs_[2], nullptr);
+ UseMemory(ptrs_[2], new_layout.size());
+}
+
+} // namespace
+} // namespace pw::allocator
diff --git a/pw_allocator/unique_ptr_test.cc b/pw_allocator/unique_ptr_test.cc
new file mode 100644
index 000000000..c0aa57cce
--- /dev/null
+++ b/pw_allocator/unique_ptr_test.cc
@@ -0,0 +1,149 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_allocator/allocator.h"
+#include "pw_allocator/allocator_testing.h"
+
+namespace pw::allocator {
+namespace {
+
+using FakeAllocWithBuffer = test::AllocatorForTestWithBuffer<256>;
+
+TEST(UniquePtr, DefaultInitializationIsNullptr) {
+ UniquePtr<int> empty;
+ EXPECT_EQ(empty.get(), nullptr);
+}
+
+TEST(UniquePtr, OperatorEqNullptrOnEmptyUniquePtrSucceeds) {
+ UniquePtr<int> empty;
+ EXPECT_TRUE(empty == nullptr);
+ EXPECT_FALSE(empty != nullptr);
+}
+
+TEST(UniquePtr, OperatorEqNullptrAfterMakeUniqueFails) {
+ FakeAllocWithBuffer alloc;
+ std::optional<UniquePtr<int>> ptr_opt = alloc->MakeUnique<int>(5);
+ ASSERT_TRUE(ptr_opt.has_value());
+ UniquePtr<int> ptr = std::move(*ptr_opt);
+ EXPECT_TRUE(ptr != nullptr);
+ EXPECT_FALSE(ptr == nullptr);
+}
+
+TEST(UniquePtr, OperatorEqNullptrAfterMakeUniqueNullptrTypeFails) {
+ FakeAllocWithBuffer alloc;
+ std::optional<UniquePtr<std::nullptr_t>> ptr_opt =
+ alloc->MakeUnique<std::nullptr_t>(nullptr);
+ ASSERT_TRUE(ptr_opt.has_value());
+ UniquePtr<std::nullptr_t> ptr = std::move(*ptr_opt);
+ EXPECT_TRUE(ptr != nullptr);
+ EXPECT_FALSE(ptr == nullptr);
+ EXPECT_TRUE(*ptr == nullptr);
+ EXPECT_FALSE(*ptr != nullptr);
+}
+
+TEST(UniquePtr, MakeUniqueForwardsConstructorArguments) {
+ class MoveOnly {
+ public:
+ MoveOnly(int value) : value_(value) {}
+ MoveOnly(MoveOnly&) = delete;
+ MoveOnly(MoveOnly&&) {}
+ int Value() const { return value_; }
+
+ private:
+ int value_;
+ };
+
+ class BuiltWithMoveOnly {
+ public:
+ BuiltWithMoveOnly() = delete;
+ BuiltWithMoveOnly(MoveOnly&& mo) : value_(mo.Value()) {}
+ int Value() const { return value_; }
+
+ private:
+ int value_;
+ };
+
+ FakeAllocWithBuffer alloc;
+ MoveOnly mo(6);
+ std::optional<UniquePtr<BuiltWithMoveOnly>> ptr =
+ alloc->MakeUnique<BuiltWithMoveOnly>(std::move(mo));
+ ASSERT_TRUE(ptr.has_value());
+ EXPECT_EQ((*ptr)->Value(), 6);
+}
+
+TEST(UniquePtr, MoveConstructsFromSubClassAndFreesTotalSize) {
+ struct Base {};
+ struct LargerSub : public Base {
+ std::array<std::byte, 128> mem;
+ };
+ FakeAllocWithBuffer alloc;
+ std::optional<UniquePtr<LargerSub>> ptr_opt = alloc->MakeUnique<LargerSub>();
+ ASSERT_TRUE(ptr_opt.has_value());
+ EXPECT_EQ(alloc->allocate_size(), 128ul);
+ UniquePtr<LargerSub> ptr = std::move(*ptr_opt);
+ UniquePtr<Base> base_ptr(std::move(ptr));
+
+ EXPECT_EQ(alloc->deallocate_size(), 0ul);
+ // The size that is deallocated here should be the size of the larger
+ // subclass, not the size of the smaller base class.
+ base_ptr.Reset();
+ EXPECT_EQ(alloc->deallocate_size(), 128ul);
+}
+
+TEST(UniquePtr, MoveAssignsFromSubClassAndFreesTotalSize) {
+ struct Base {};
+ struct LargerSub : public Base {
+ std::array<std::byte, 128> mem;
+ };
+ FakeAllocWithBuffer alloc;
+ std::optional<UniquePtr<LargerSub>> ptr_opt = alloc->MakeUnique<LargerSub>();
+ ASSERT_TRUE(ptr_opt.has_value());
+ EXPECT_EQ(alloc->allocate_size(), 128ul);
+ UniquePtr<LargerSub> ptr = std::move(*ptr_opt);
+ UniquePtr<Base> base_ptr = std::move(ptr);
+
+ EXPECT_EQ(alloc->deallocate_size(), 0ul);
+ // The size that is deallocated here should be the size of the larger
+ // subclass, not the size of the smaller base class.
+ base_ptr.Reset();
+ EXPECT_EQ(alloc->deallocate_size(), 128ul);
+}
+
+TEST(UniquePtr, DestructorDestroysAndFrees) {
+ int count = 0;
+ class DestructorCounter {
+ public:
+ DestructorCounter(int& count) : count_(&count) {}
+ ~DestructorCounter() { (*count_)++; }
+
+ private:
+ int* count_;
+ };
+ FakeAllocWithBuffer alloc;
+ std::optional<UniquePtr<DestructorCounter>> ptr_opt =
+ alloc->MakeUnique<DestructorCounter>(count);
+ ASSERT_TRUE(ptr_opt.has_value());
+
+ EXPECT_EQ(count, 0);
+ EXPECT_EQ(alloc->deallocate_size(), 0ul);
+ ptr_opt.reset(); // clear the optional, destroying the UniquePtr
+ EXPECT_EQ(count, 1);
+ EXPECT_EQ(alloc->deallocate_size(), sizeof(DestructorCounter));
+}
+
+} // namespace
+} // namespace pw::allocator
diff --git a/pw_analog/BUILD.bazel b/pw_analog/BUILD.bazel
index 5c699c310..ad96f2502 100644
--- a/pw_analog/BUILD.bazel
+++ b/pw_analog/BUILD.bazel
@@ -50,6 +50,7 @@ pw_cc_library(
pw_cc_library(
name = "microvolt_input_gmock",
+ testonly = True,
hdrs = [
"public/pw_analog/microvolt_input_gmock.h",
],
@@ -62,6 +63,7 @@ pw_cc_library(
pw_cc_library(
name = "analog_input_gmock",
+ testonly = True,
hdrs = [
"public/pw_analog/analog_input_gmock.h",
],
diff --git a/pw_analog/BUILD.gn b/pw_analog/BUILD.gn
index dd2761acc..a60e8a550 100644
--- a/pw_analog/BUILD.gn
+++ b/pw_analog/BUILD.gn
@@ -88,5 +88,10 @@ pw_test("microvolt_input_test") {
}
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "docs.rst",
+ "public/pw_analog/analog_input_gmock.h",
+ "public/pw_analog/microvolt_input.h",
+ "public/pw_analog/microvolt_input_gmock.h",
+ ]
}
diff --git a/pw_analog/docs.rst b/pw_analog/docs.rst
index 19285e09f..84cee3578 100644
--- a/pw_analog/docs.rst
+++ b/pw_analog/docs.rst
@@ -1,19 +1,20 @@
.. _module-pw_analog:
----------
+=========
pw_analog
----------
+=========
.. warning::
This module is under construction and may not be ready for use.
pw_analog contains interfaces and utility functions for using the ADC.
+--------
Features
-========
+--------
pw::analog::AnalogInput
------------------------
+=======================
The common interface for obtaining ADC samples. This interface represents
a single analog input or channel. Users will need to supply their own ADC
driver implementation in order to configure and enable the ADC peripheral.
@@ -21,7 +22,7 @@ Users are responsible for managing multithreaded access to the ADC driver if the
ADC services multiple channels.
pw::analog::MicrovoltInput
---------------------------
+==========================
The common interface for obtaining voltage samples in microvolts. This interface
represents a single voltage input or channel. Users will need to supply their
own ADC driver implementation in order to configure and enable the ADC
@@ -30,9 +31,35 @@ enable the ADC peripheral where needed. Users are responsible for managing
multithreaded access to the ADC driver if the ADC services multiple channels.
pw::analog::GmockAnalogInput
--------------------------------
+============================
gMock of AnalogInput used for testing and mocking out the AnalogInput.
pw::analog::GmockMicrovoltInput
--------------------------------
+===============================
gMock of MicrovoltInput used for testing and mocking out the MicrovoltInput.
+
+-------------
+API reference
+-------------
+
+pw::analog::AnalogInput
+=======================
+.. doxygenclass:: pw::analog::AnalogInput
+ :members:
+
+pw::analog::GmockAnalogInput
+============================
+.. literalinclude:: public/pw_analog/analog_input_gmock.h
+ :start-after: #pragma once
+ :end-before: } // namespace pw::analog
+
+pw::analog::MicrovoltInput
+==========================
+.. doxygenclass:: pw::analog::MicrovoltInput
+ :members:
+
+pw::analog::GmockMicrovoltInput
+===============================
+.. literalinclude:: public/pw_analog/microvolt_input_gmock.h
+ :start-after: #pragma once
+ :end-before: } // namespace pw::analog
diff --git a/pw_analog/public/pw_analog/analog_input.h b/pw_analog/public/pw_analog/analog_input.h
index 39024a0bb..0eaea1a71 100644
--- a/pw_analog/public/pw_analog/analog_input.h
+++ b/pw_analog/public/pw_analog/analog_input.h
@@ -18,56 +18,58 @@
namespace pw::analog {
-// Base interface for getting ADC samples from one ADC channel in a thread
-// safe manner.
-//
-// The ADC backend interface is up to the user to define and implement for now.
-// This gives the flexibility for the ADC driver implementation.
-//
-// AnalogInput controls a specific input/channel where the ADC peripheral may be
-// shared across multiple channels that may be controlled by multiple threads.
-// The implementer of this pure virtual interface is responsible for ensuring
-// thread safety and access at the driver level.
+/// Base interface for getting analog-to-digital (ADC) samples from one ADC
+/// channel in a thread-safe manner.
+///
+/// The ADC backend interface is up to the user to define and implement for now.
+/// This gives flexibility for the ADC driver implementation.
+///
+/// `AnalogInput` controls a specific input / channel where the ADC peripheral
+/// may be shared across multiple channels that may be controlled by multiple
+/// threads. The implementer of this pure virtual interface is responsible for
+/// ensuring thread safety and access at the driver level.
class AnalogInput {
public:
- // Limits struct that specifies the min and max of the sample range.
- // These values do not change at run time.
+ /// Specifies the sample range.
+ /// These values do not change at runtime.
struct Limits {
+ /// The minimum of the sample range.
int32_t min;
+ /// The maximum of the sample range.
int32_t max;
};
virtual ~AnalogInput() = default;
- // Blocks until the specified timeout duration has elapsed or the ADC sample
- // has been returned, whichever comes first.
- //
- // This method is thread safe.
- //
- // Returns:
- // Sample.
- // ResourceExhuasted: ADC peripheral in use.
- // DeadlineExceedded: Timed out waiting for a sample.
- // Other statuses left up to the implementer.
+ /// Blocks until the specified timeout duration has elapsed or the ADC sample
+ /// has been returned, whichever comes first.
+ ///
+ /// This method is thread safe.
+ ///
+ /// @returns
+ /// * A sample on success.
+ /// * @pw_status{RESOURCE_EXHAUSTED} - ADC peripheral in use.
+ /// * @pw_status{DEADLINE_EXCEEDED} - Timed out waiting for a sample.
+ /// * Other statuses left up to the implementer.
Result<int32_t> TryReadFor(chrono::SystemClock::duration timeout) {
return TryReadUntil(chrono::SystemClock::TimePointAfterAtLeast(timeout));
}
- // Blocks until the deadline time has been reached or the ADC sample
- // has been returned, whichever comes first.
- //
- // This method is thread safe.
- //
- // Returns:
- // Sample.
- // ResourceExhuasted: ADC peripheral in use.
- // DeadlineExceedded: Timed out waiting for a sample.
- // Other statuses left up to the implementer.
+ /// Blocks until the deadline time has been reached or the ADC sample
+ /// has been returned, whichever comes first.
+ ///
+ /// This method is thread safe.
+ ///
+ /// @returns
+ /// * A sample on success.
+ /// * @pw_status{RESOURCE_EXHAUSTED} - ADC peripheral in use.
+ /// * @pw_status{DEADLINE_EXCEEDED} - Timed out waiting for a sample.
+ /// * Other statuses left up to the implementer.
virtual Result<int32_t> TryReadUntil(
chrono::SystemClock::time_point deadline) = 0;
- // Returns the range of the ADC sample.
- // These values do not change at run time.
+ /// @returns The range of the ADC sample. These values do not change at
+ /// runtime.
virtual Limits GetLimits() const = 0;
};
diff --git a/pw_analog/public/pw_analog/microvolt_input.h b/pw_analog/public/pw_analog/microvolt_input.h
index a40e17ba0..a38eb5b89 100644
--- a/pw_analog/public/pw_analog/microvolt_input.h
+++ b/pw_analog/public/pw_analog/microvolt_input.h
@@ -19,52 +19,52 @@
namespace pw::analog {
-// The common interface for obtaining voltage samples in microvolts. This
-// interface represents a single voltage input or channel. Users will need to
-// supply their own ADC driver implementation in order to configure and enable
-// the ADC peripheral in order to provide the reference voltages and to
-// configure and enable the ADC peripheral where needed. Users are responsible
-// for managing multithreaded access to the ADC driver if the ADC services
-// multiple channels.
+/// The common interface for obtaining voltage samples in microvolts. This
+/// interface represents a single voltage input or channel. Users will need to
+/// supply their own ADC driver implementation in order to provide the reference
+/// voltages and to configure and enable the ADC peripheral where needed. Users
+/// are responsible for managing multi-threaded access to the ADC driver if the
+/// ADC services multiple channels.
class MicrovoltInput : public AnalogInput {
public:
- // Specifies the max and min microvolt range the analog input can measure.
- // The reference voltage difference cannot be bigger than sizeof(int32_t)
- // which should be just above 2000V.
- // * These values do not change at run time.
- // * Inversion of min/max is supported.
+ /// Specifies the maximum and minimum microvolt range the analog input can
+ /// measure. The reference voltage difference cannot be bigger than
+ /// `sizeof(int32_t)` which should be just above 2000V. These values do not
+ /// change at run time. Inversion of `min` or `max` is supported.
struct References {
- int32_t max_voltage_uv; // Microvolts at AnalogInput::Limits::max
- int32_t min_voltage_uv; // Microvolts at AnalogInput::Limits::min.
+ /// Microvolts at `AnalogInput::Limits::max`.
+ int32_t max_voltage_uv;
+ /// Microvolts at `AnalogInput::Limits::min`.
+ int32_t min_voltage_uv;
};
~MicrovoltInput() override = default;
- // Blocks until the specified timeout duration has elapsed or the voltage
- // sample has been returned, whichever comes first.
- //
- // This method is thread safe.
- //
- // Returns:
- // Microvolts (uV).
- // ResourceExhuasted: ADC peripheral in use.
- // DeadlineExceedded: Timed out waiting for a sample.
- // Other statuses left up to the implementer.
+ /// Blocks until the specified timeout duration has elapsed or the voltage
+ /// sample has been returned, whichever comes first.
+ ///
+ /// This method is thread-safe.
+ ///
+ /// @returns
+ /// * A voltage sample in microvolts (uV) on success.
+ /// * @pw_status{RESOURCE_EXHAUSTED} - ADC peripheral in use.
+ /// * @pw_status{DEADLINE_EXCEEDED} - Timed out waiting for a sample.
+ /// * Other statuses left up to the implementer.
Result<int32_t> TryReadMicrovoltsFor(chrono::SystemClock::duration timeout) {
return TryReadMicrovoltsUntil(
chrono::SystemClock::TimePointAfterAtLeast(timeout));
}
- // Blocks until the deadline time has been reached or the voltage sample has
- // been returned, whichever comes first.
- //
- // This method is thread safe.
- //
- // Returns:
- // Microvolts (uV).
- // ResourceExhuasted: ADC peripheral in use.
- // DeadlineExceedded: Timed out waiting for a sample.
- // Other statuses left up to the implementer.
+ /// Blocks until the deadline time has been reached or the voltage sample has
+ /// been returned, whichever comes first.
+ ///
+ /// This method is thread-safe.
+ ///
+ /// @returns
+ /// * A voltage sample in microvolts (uV) on success.
+ /// * @pw_status{RESOURCE_EXHAUSTED} - ADC peripheral in use.
+ /// * @pw_status{DEADLINE_EXCEEDED} - Timed out waiting for a sample.
+ /// * Other statuses left up to the implementer.
Result<int32_t> TryReadMicrovoltsUntil(
chrono::SystemClock::time_point deadline) {
PW_TRY_ASSIGN(const int32_t sample, TryReadUntil(deadline));
diff --git a/pw_android_toolchain/docs.rst b/pw_android_toolchain/docs.rst
index 8dbc661dd..1a79c2621 100644
--- a/pw_android_toolchain/docs.rst
+++ b/pw_android_toolchain/docs.rst
@@ -49,7 +49,7 @@ and ensure that ``current_cpu`` is set in the toolchain ``defaults``.
For example:
-.. code::
+.. code-block::
import("//build_overrides/pigweed.gni")
@@ -85,7 +85,7 @@ convenience, ``pw_generate_android_toolchains`` does not require that
``current_cpu`` is set. If any toolchain scope in the list does not set it, a
toolchain for each supported target will be generated.
-.. code::
+.. code-block::
# Generate arm_*, arm64_*, x64_*, and x86_* for each scope in the list.
pw_generate_android_toolchains("target_toolchains) {
diff --git a/pw_android_toolchain/toolchains.gni b/pw_android_toolchain/toolchains.gni
index 219932872..288984aad 100644
--- a/pw_android_toolchain/toolchains.gni
+++ b/pw_android_toolchain/toolchains.gni
@@ -23,21 +23,18 @@ pw_toolchain_android = {
name = "android_debug"
defaults = {
default_configs = _android + [ "$dir_pw_build:optimize_debugging" ]
- remove_default_configs = [ "$dir_pw_build:relative_paths" ]
}
}
speed_optimized = {
name = "android_speed_optimized"
defaults = {
default_configs = _android + [ "$dir_pw_build:optimize_speed" ]
- remove_default_configs = [ "$dir_pw_build:relative_paths" ]
}
}
size_optimized = {
name = "android_size_optimized"
defaults = {
default_configs = _android + [ "$dir_pw_build:optimize_size" ]
- remove_default_configs = [ "$dir_pw_build:relative_paths" ]
}
}
}
diff --git a/pw_arduino_build/arduino.gni b/pw_arduino_build/arduino.gni
index 6ac859f7a..7c50f3d9a 100644
--- a/pw_arduino_build/arduino.gni
+++ b/pw_arduino_build/arduino.gni
@@ -23,9 +23,9 @@ declare_args() {
# Expected args for an Arduino build:
pw_arduino_build_CORE_NAME = ""
- # TODO(tonymd): "teensy/avr" here should match the folders in this dir:
+ # TODO(tonymd): "avr/1.58.1" here should match the folders in this dir:
# "../third_party/arduino/cores/$pw_arduino_build_CORE_NAME/hardware/*")
- # For teensy: "teensy/avr", for adafruit-samd: "samd/1.6.2"
+ # For teensy: "avr/1.58.1", for adafruit-samd: "samd/1.6.2"
pw_arduino_build_PACKAGE_NAME = ""
pw_arduino_build_BOARD = ""
@@ -61,9 +61,6 @@ if (pw_arduino_build_CORE_PATH != "") {
_arduino_selected_core_path + " --arduino-package-name " +
pw_arduino_build_PACKAGE_NAME + " list-boards")
- _compiler_path_override =
- rebase_path(pw_env_setup_CIPD_PIGWEED + "/bin", root_build_dir)
-
arduino_core_library_path = "$_arduino_selected_core_path/hardware/" +
"$pw_arduino_build_PACKAGE_NAME/libraries"
@@ -72,9 +69,19 @@ if (pw_arduino_build_CORE_PATH != "") {
_arduino_selected_core_path,
"--arduino-package-name",
pw_arduino_build_PACKAGE_NAME,
- "--compiler-path-override",
- _compiler_path_override,
+ ]
+
+ if (defined(pw_env_setup_CIPD_PIGWEED)) {
+ _compiler_path_override =
+ rebase_path(pw_env_setup_CIPD_PIGWEED + "/bin", root_build_dir)
+
+ arduino_global_args += [
+ "--compiler-path-override",
+ _compiler_path_override,
+ ]
+ }
+ arduino_global_args += [
# Save config files to "out/arduino_debug/gen/arduino_builder_config.json"
"--config-file",
rebase_path(root_gen_dir, root_build_dir) + "/arduino_builder_config.json",
diff --git a/pw_arduino_build/py/BUILD.bazel b/pw_arduino_build/py/BUILD.bazel
index d69fbb0e0..4f449563b 100644
--- a/pw_arduino_build/py/BUILD.bazel
+++ b/pw_arduino_build/py/BUILD.bazel
@@ -43,6 +43,7 @@ py_test(
],
deps = [
":pw_arduino_build",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -54,5 +55,6 @@ py_test(
],
deps = [
":pw_arduino_build",
+ "@python_packages_parameterized//:pkg",
],
)
diff --git a/pw_arduino_build/py/BUILD.gn b/pw_arduino_build/py/BUILD.gn
index bdcce99de..cab35880f 100644
--- a/pw_arduino_build/py/BUILD.gn
+++ b/pw_arduino_build/py/BUILD.gn
@@ -20,13 +20,14 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_arduino_build/__init__.py",
"pw_arduino_build/__main__.py",
"pw_arduino_build/builder.py",
"pw_arduino_build/core_installer.py",
+ "pw_arduino_build/core_patches/__init__.py",
+ "pw_arduino_build/core_patches/teensy/__init__.py",
"pw_arduino_build/file_operations.py",
"pw_arduino_build/log.py",
"pw_arduino_build/teensy_detector.py",
diff --git a/pw_arduino_build/py/pw_arduino_build/__main__.py b/pw_arduino_build/py/pw_arduino_build/__main__.py
index 0811bef48..4338b538b 100644
--- a/pw_arduino_build/py/pw_arduino_build/__main__.py
+++ b/pw_arduino_build/py/pw_arduino_build/__main__.py
@@ -26,9 +26,17 @@ from collections import OrderedDict
from pathlib import Path
from typing import List
-from pw_arduino_build import core_installer, log
-from pw_arduino_build.builder import ArduinoBuilder
-from pw_arduino_build.file_operations import decode_file_json
+try:
+ from pw_arduino_build import core_installer, log
+ from pw_arduino_build.builder import ArduinoBuilder
+ from pw_arduino_build.file_operations import decode_file_json
+
+except ImportError:
+ # Load from this directory if pw_arduino_build is not available.
+ import core_installer # type: ignore
+ import log # type: ignore
+ from builder import ArduinoBuilder # type: ignore
+ from file_operations import decode_file_json # type: ignore
_LOG = logging.getLogger(__name__)
diff --git a/pw_arduino_build/py/pw_arduino_build/builder.py b/pw_arduino_build/py/pw_arduino_build/builder.py
index 6d268b40b..7684e3b84 100755
--- a/pw_arduino_build/py/pw_arduino_build/builder.py
+++ b/pw_arduino_build/py/pw_arduino_build/builder.py
@@ -26,7 +26,12 @@ from collections import OrderedDict
from pathlib import Path
from typing import List
-from pw_arduino_build import file_operations
+try:
+ from pw_arduino_build import file_operations
+
+except ImportError:
+ # Load from this directory if pw_arduino_build is not available.
+ import file_operations # type: ignore
_LOG = logging.getLogger(__name__)
@@ -250,6 +255,19 @@ class ArduinoBuilder:
self.library_names = []
self.library_names.append("SrcWrapper")
+ # Surround args in quotes if they contain any quote characters.
+ # Example:
+ # before: -DVARIANT_H="variant_generic.h"
+ # after: "-DVARIANT_H=\"variant_generic.h\""
+ build_info_args = []
+ for arg in self.platform["build.info.flags"].split():
+ if '"' not in arg:
+ build_info_args.append(arg)
+ continue
+ new_arg = arg.replace('"', '\\"')
+ build_info_args.append(f'"{new_arg}"')
+ self.platform["build.info.flags"] = ' '.join(build_info_args)
+
def _copy_default_menu_options_to_build_variables(self):
# Clear existing options
self.menu_options["selected"] = {}
@@ -329,7 +347,7 @@ class ArduinoBuilder:
# Set the {build.core.path} variable that pointing to a sub-core
# folder. For Teensys this is:
- # 'teensy/hardware/teensy/avr/cores/teensy{3,4}'. For other cores
+ # 'teensy/hardware/avr/1.58.1/cores/teensy{3,4}'. For other cores
# it's typically just the 'arduino' folder. For example:
# 'arduino-samd/hardware/samd/1.8.8/cores/arduino'
core_path = Path(self.package_path) / "cores"
@@ -571,8 +589,10 @@ class ArduinoBuilder:
self.build_variant_path = bvp
self.board[self.selected_board]["build.variant.path"] = bvp
# Add the variant folder as an include directory
- # (used in stm32l4 core)
- self.variant_includes = "-I{}".format(bvp)
+ # This is used in the stm32l4 and stm32duino cores. Include
+ # directories should be surrounded in quotes in case they contain
+ # spaces or parens.
+ self.variant_includes = "\"-I{}\"".format(bvp)
_LOG.debug("PLATFORM INITIAL: %s", _pretty_format(self.platform))
diff --git a/pw_arduino_build/py/pw_arduino_build/core_installer.py b/pw_arduino_build/py/pw_arduino_build/core_installer.py
index dcb1fe186..758072894 100644
--- a/pw_arduino_build/py/pw_arduino_build/core_installer.py
+++ b/pw_arduino_build/py/pw_arduino_build/core_installer.py
@@ -15,19 +15,20 @@
"""Arduino Core Installer."""
import argparse
+import importlib.resources
import logging
-import operator
import os
+from pathlib import Path
import platform
import shutil
-import stat
-import subprocess
-import sys
-import time
-from pathlib import Path
-from typing import Dict, List
+from typing import Dict
-from pw_arduino_build import file_operations
+try:
+ from pw_arduino_build import file_operations
+
+except ImportError:
+ # Load this directory if pw_arduino_build is not available.
+ import file_operations # type: ignore
_LOG = logging.getLogger(__name__)
@@ -36,79 +37,70 @@ class ArduinoCoreNotSupported(Exception):
"""Exception raised when a given core can not be installed."""
+class ArduinoCoreInstallationFailed(Exception):
+ """Exception raised when a given core failed to be installed."""
+
+
_ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
# pylint: disable=line-too-long
"teensy": {
+ "all": {
+ "core": {
+ "version": "1.58.1",
+ "url": "https://www.pjrc.com/teensy/td_158-1/teensy-package.tar.bz2",
+ "file_name": "teensy-package.tar.bz2",
+ "sha256": "3a3f3728045621d25068c5b5dfc24bf171550127e9fae4d0e8be2574c6636cff",
+ }
+ },
"Linux": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linux64.tar.xz",
- "file_name": "arduino-1.8.13-linux64.tar.xz",
- "sha256": "1b20d0ec850a2a63488009518725f058668bb6cb48c321f82dcf47dc4299b4ad",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux64",
- "sha256": "2e6cd99a757bc80593ea3de006de4cc934bcb0a6ec74cad8ec327f0289d40f0b",
- "file_name": "TeensyduinoInstall.linux64",
+ "teensy-tools": {
+ "url": "https://www.pjrc.com/teensy/td_158/teensy-tools-linux64.tar.bz2",
+ "file_name": "teensy-tools-linux64.tar.bz2",
+ "sha256": "0a272575ca42b4532ab89516df160e1d68e449fe1538c0bd71dbb768f1b3c0b6",
+ "version": "1.58.0",
},
},
# TODO(tonymd): Handle 32-bit Linux Install?
"Linux32": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linux32.tar.xz",
- "file_name": "arduino-1.8.13-linux32.tar.xz",
- "sha256": "",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linux32",
- "file_name": "TeensyduinoInstall.linux32",
- "sha256": "",
+ "teensy-tools": {
+ "url": "https://www.pjrc.com/teensy/td_158/teensy-tools-linux32.tar.bz2",
+ "file_name": "teensy-tools-linux32.tar.bz2",
+ "sha256": "995d974935c8118ad6d4c191206453dd8f57c1e299264bb4cffcc62c96c6d077",
+ "version": "1.58.0",
},
},
# TODO(tonymd): Handle ARM32 (Raspberry Pi) Install?
"LinuxARM32": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxarm.tar.xz",
- "file_name": "arduino-1.8.13-linuxarm.tar.xz",
- "sha256": "",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxarm",
- "file_name": "TeensyduinoInstall.linuxarm",
- "sha256": "",
+ "teensy-tools": {
+ "url": "https://www.pjrc.com/teensy/td_158/teensy-tools-linuxarm.tar.bz2",
+ "file_name": "teensy-tools-linuxarm.tar.bz2",
+ "sha256": "88cf8e55549f5d5937fa7dbc763cad49bd3680d4e5185b318c667f541035e633",
+ "version": "1.58.0",
},
},
# TODO(tonymd): Handle ARM64 Install?
"LinuxARM64": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-linuxaarch64.tar.xz",
- "file_name": "arduino-1.8.13-linuxaarch64.tar.xz",
- "sha256": "",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.linuxaarch64",
- "file_name": "TeensyduinoInstall.linuxaarch64",
- "sha256": "",
+ "teensy-tools": {
+ "url": "https://www.pjrc.com/teensy/td_158/teensy-tools-linuxaarch64.tar.bz2",
+ "file_name": "teensy-tools-linuxaarch64.tar.bz2",
+ "sha256": "a20b1c5e91fe51c3b6591e4cfcf711d4a4c0a0bb5120c59d1c8dd8d32ae44e31",
+ "version": "1.58.0",
},
},
"Darwin": {
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/Teensyduino_MacOS_Catalina.zip",
- "file_name": "Teensyduino_MacOS_Catalina.zip",
- "sha256": "401ef42c6e83e621cdda20191a4ef9b7db8a214bede5a94a9e26b45f79c64fe2",
+ "teensy-tools": {
+ "url": "https://www.pjrc.com/teensy/td_158/teensy-tools-macos.tar.bz2",
+ "file_name": "teensy-tools-macos.tar.bz2",
+ "sha256": "d386412e38fe6dd6c5d849c2b1f8eea00cbf7bc3659fb6ba9f83cebfb736924b",
+ "version": "1.58.0",
},
},
"Windows": {
- "arduino-ide": {
- "url": "https://downloads.arduino.cc/arduino-1.8.13-windows.zip",
- "file_name": "arduino-1.8.13-windows.zip",
- "sha256": "78d3e96827b9e9b31b43e516e601c38d670d29f12483e88cbf6d91a0f89ef524",
- },
- "teensyduino": {
- "url": "https://www.pjrc.com/teensy/td_153/TeensyduinoInstall.exe",
- # The installer should be named 'Teensyduino.exe' instead of
- # 'TeensyduinoInstall.exe' to trigger a non-admin installation.
- "file_name": "Teensyduino.exe",
- "sha256": "88f58681e5c4772c54e462bc88280320e4276e5b316dcab592fe38d96db990a1",
+ "teensy-tools": {
+ "url": "https://www.pjrc.com/teensy/td_158/teensy-tools-windows.tar.bz2",
+ "file_name": "teensy-tools-windows.tar.bz2",
+ "sha256": "206315ddc82381d2da92da9f633a1719e00c0e8f5432acfed434573409a48de1",
+ "version": "1.58.0",
},
},
},
@@ -141,10 +133,10 @@ _ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
"stm32duino": {
"all": {
"core": {
- "version": "1.9.0",
- "url": "https://github.com/stm32duino/Arduino_Core_STM32/archive/1.9.0.tar.gz",
- "file_name": "stm32duino-1.9.0.tar.gz",
- "sha256": "4f75ba7a117d90392e8f67c58d31d22393749b9cdd3279bc21e7261ec06c62bf",
+ "version": "2.6.0",
+ "url": "https://github.com/stm32duino/Arduino_Core_STM32/archive/2.6.0.tar.gz",
+ "file_name": "stm32duino-2.6.0.tar.gz",
+ "sha256": "53f37df1202b1bccfb353e4775200f63b36d487fe734fdb8ca9bfa00c2f3429f",
}
},
"Linux": {},
@@ -171,12 +163,7 @@ def install_core(prefix, core_name):
os.makedirs(cache_dir, exist_ok=True)
if core_name == "teensy":
- if platform.system() == "Linux":
- install_teensy_core_linux(install_prefix, install_dir, cache_dir)
- elif platform.system() == "Darwin":
- install_teensy_core_mac(install_prefix, install_dir, cache_dir)
- elif platform.system() == "Windows":
- install_teensy_core_windows(install_prefix, install_dir, cache_dir)
+ install_teensy_core(install_prefix, install_dir, cache_dir)
apply_teensy_patches(install_dir)
elif core_name == "adafruit-samd":
install_adafruit_samd_core(install_prefix, install_dir, cache_dir)
@@ -196,158 +183,59 @@ def supported_cores():
return _ARDUINO_CORE_ARTIFACTS.keys()
-def get_windows_process_names() -> List[str]:
- result = subprocess.run("wmic process get description", capture_output=True)
- output = result.stdout.decode().splitlines()
- return [line.strip() for line in output if line]
-
-
-def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
- """Download and install Teensyduino artifacts for Windows."""
- teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
-
- arduino_artifact = teensy_artifacts["arduino-ide"]
- arduino_zipfile = file_operations.download_to_cache(
- url=arduino_artifact["url"],
- expected_sha256sum=arduino_artifact["sha256"],
+def install_teensy_core(_install_prefix: str, install_dir: str, cache_dir: str):
+ """Install teensy core files and tools."""
+ # Install the Teensy core source files
+ artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"]["all"]["core"]
+ core_tarfile = file_operations.download_to_cache(
+ url=artifacts["url"],
+ expected_sha256sum=artifacts["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=arduino_artifact["file_name"],
+ downloaded_file_name=artifacts["file_name"],
)
- teensyduino_artifact = teensy_artifacts["teensyduino"]
- teensyduino_installer = file_operations.download_to_cache(
- url=teensyduino_artifact["url"],
- expected_sha256sum=teensyduino_artifact["sha256"],
- cache_directory=cache_dir,
- downloaded_file_name=teensyduino_artifact["file_name"],
+ package_path = os.path.join(
+ install_dir, "hardware", "avr", artifacts["version"]
)
+ os.makedirs(package_path, exist_ok=True)
+ file_operations.extract_archive(core_tarfile, package_path, cache_dir)
- file_operations.extract_archive(arduino_zipfile, install_dir, cache_dir)
-
- # "teensy" here should match args.core_name
- teensy_core_dir = os.path.join(install_prefix, "teensy")
-
- # Change working directory for installation
- original_working_dir = os.getcwd()
- os.chdir(install_prefix)
+ expected_files = [
+ Path(package_path) / 'boards.txt',
+ Path(package_path) / 'platform.txt',
+ ]
- install_command = [teensyduino_installer, "--dir=teensy"]
- _LOG.info(" Running: %s", " ".join(install_command))
- _LOG.info(
- " Please click yes on the Windows 'User Account Control' " "dialog."
- )
- _LOG.info(" You should see: 'Verified publisher: PRJC.COM LLC'")
-
- def wait_for_process(
- process_name, timeout=30, result_operator=operator.truth
- ):
- start_time = time.time()
- while result_operator(process_name in get_windows_process_names()):
- time.sleep(1)
- if time.time() > start_time + timeout:
- _LOG.error(
- "Error: Installation Failed.\n"
- "Please click yes on the Windows 'User Account Control' "
- "dialog."
- )
- sys.exit(1)
-
- # Run Teensyduino installer with admin rights (non-blocking)
- # User Account Control (UAC) will prompt the user for consent
- import ctypes # pylint: disable=import-outside-toplevel
-
- ctypes.windll.shell32.ShellExecuteW(
- None, # parent window handle
- "runas", # operation
- teensyduino_installer, # file to run
- subprocess.list2cmdline(install_command), # command parameters
- install_prefix, # working directory
- 1,
- ) # Display mode (SW_SHOWNORMAL: Activates and displays a window)
-
- # Wait for teensyduino_installer to start running
- wait_for_process("TeensyduinoInstall.exe", result_operator=operator.not_)
-
- _LOG.info("Waiting for TeensyduinoInstall.exe to finish.")
- # Wait till teensyduino_installer is finished
- wait_for_process("TeensyduinoInstall.exe", timeout=360)
-
- if not os.path.exists(os.path.join(teensy_core_dir, "hardware", "teensy")):
- _LOG.error(
- "Error: Installation Failed.\n"
- "Please try again and ensure Teensyduino is installed in "
- "the folder:\n"
- "%s",
- teensy_core_dir,
+ if any(not expected_file.is_file() for expected_file in expected_files):
+ expected_files_str = "".join(
+ list(f" {expected_file}\n" for expected_file in expected_files)
)
- sys.exit(1)
- else:
- _LOG.info("Install complete!")
-
- file_operations.remove_empty_directories(install_dir)
- os.chdir(original_working_dir)
-
-
-def install_teensy_core_mac(unused_install_prefix, install_dir, cache_dir):
- """Download and install Teensyduino artifacts for Mac."""
- teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
-
- teensyduino_artifact = teensy_artifacts["teensyduino"]
- teensyduino_zip = file_operations.download_to_cache(
- url=teensyduino_artifact["url"],
- expected_sha256sum=teensyduino_artifact["sha256"],
- cache_directory=cache_dir,
- downloaded_file_name=teensyduino_artifact["file_name"],
- )
- extracted_files = file_operations.extract_archive(
- teensyduino_zip,
- install_dir,
- cache_dir,
- remove_single_toplevel_folder=False,
- )
- toplevel_folder = sorted(extracted_files)[0]
- os.symlink(
- os.path.join(toplevel_folder, "Contents", "Java", "hardware"),
- os.path.join(install_dir, "hardware"),
- target_is_directory=True,
- )
-
-
-def install_teensy_core_linux(install_prefix, install_dir, cache_dir):
- """Download and install Teensyduino artifacts for Windows."""
- teensy_artifacts = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
+ raise ArduinoCoreInstallationFailed(
+ "\n\nError: Installation Failed.\n"
+ "Please remove the package:\n\n"
+ " pw package remove teensy\n\n"
+ "Try again and ensure the following files exist:\n\n"
+ + expected_files_str
+ )
- arduino_artifact = teensy_artifacts["arduino-ide"]
- arduino_tarfile = file_operations.download_to_cache(
- url=arduino_artifact["url"],
- expected_sha256sum=arduino_artifact["sha256"],
- cache_directory=cache_dir,
- downloaded_file_name=arduino_artifact["file_name"],
- )
+ teensy_tools = _ARDUINO_CORE_ARTIFACTS["teensy"][platform.system()]
- teensyduino_artifact = teensy_artifacts["teensyduino"]
- teensyduino_installer = file_operations.download_to_cache(
- url=teensyduino_artifact["url"],
- expected_sha256sum=teensyduino_artifact["sha256"],
- cache_directory=cache_dir,
- downloaded_file_name=teensyduino_artifact["file_name"],
- )
+ for tool_name, artifacts in teensy_tools.items():
+ tool_tarfile = file_operations.download_to_cache(
+ url=artifacts["url"],
+ expected_sha256sum=artifacts["sha256"],
+ cache_directory=cache_dir,
+ downloaded_file_name=artifacts["file_name"],
+ )
- file_operations.extract_archive(arduino_tarfile, install_dir, cache_dir)
- os.chmod(
- teensyduino_installer,
- os.stat(teensyduino_installer).st_mode | stat.S_IEXEC,
- )
+ tool_path = os.path.join(
+ install_dir, "tools", tool_name, artifacts["version"]
+ )
- original_working_dir = os.getcwd()
- os.chdir(install_prefix)
- # "teensy" here should match args.core_name
- install_command = [teensyduino_installer, "--dir=teensy"]
- subprocess.run(install_command)
+ os.makedirs(tool_path, exist_ok=True)
+ file_operations.extract_archive(tool_tarfile, tool_path, cache_dir)
- file_operations.remove_empty_directories(install_dir)
- os.chdir(original_working_dir)
+ return True
def apply_teensy_patches(install_dir):
@@ -357,19 +245,28 @@ def apply_teensy_patches(install_dir):
# Resolve paths since `git apply` doesn't work if a path is beyond a
# symbolic link.
patch_root_path = (
- Path(install_dir) / "hardware/teensy/avr/cores"
+ Path(install_dir) / "hardware/avr/1.58.1/cores"
).resolve()
- # Get all *.diff files relative to this python file's parent directory.
- patch_file_paths = sorted(
- (Path(__file__).parent / "core_patches/teensy").glob("*.diff")
+ # Get all *.diff files for the teensy core.
+ patches_python_package = 'pw_arduino_build.core_patches.teensy'
+
+ patch_file_names = sorted(
+ patch
+ for patch in importlib.resources.contents(patches_python_package)
+ if Path(patch).suffix in ['.diff']
)
# Apply each patch file.
- for diff_path in patch_file_paths:
- file_operations.git_apply_patch(
- patch_root_path.as_posix(), diff_path.as_posix(), unsafe_paths=True
- )
+ for diff_name in patch_file_names:
+ with importlib.resources.path(
+ patches_python_package, diff_name
+ ) as diff_path:
+ file_operations.git_apply_patch(
+ patch_root_path.as_posix(),
+ diff_path.as_posix(),
+ unsafe_paths=True,
+ )
def install_arduino_samd_core(
diff --git a/pw_arduino_build/py/pw_arduino_build/core_patches/__init__.py b/pw_arduino_build/py/pw_arduino_build/core_patches/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/core_patches/__init__.py
diff --git a/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.53-cpp17.diff b/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.53-cpp17.diff
deleted file mode 100644
index 0487eaa99..000000000
--- a/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.53-cpp17.diff
+++ /dev/null
@@ -1,70 +0,0 @@
-diff --git a/teensy3/WCharacter.h b/teensy3/WCharacter.h
-index 5bfe697..7c500c1 100644
---- a/teensy3/WCharacter.h
-+++ b/teensy3/WCharacter.h
-@@ -61,7 +61,7 @@ inline boolean isAlpha(int c)
- // that fits into the ASCII character set.
- inline boolean isAscii(int c)
- {
-- return ( isascii (c) == 0 ? false : true);
-+ return ((c & ~0x7F) != 0 ? false : true);
- }
-
-
-@@ -143,7 +143,7 @@ inline boolean isHexadecimalDigit(int c)
- // ASCII character set, by clearing the high-order bits.
- inline int toAscii(int c)
- {
-- return toascii (c);
-+ return (c & 0x7F);
- }
-
-
-diff --git a/teensy3/avr_functions.h b/teensy3/avr_functions.h
-index 977c5e9..55c426c 100644
---- a/teensy3/avr_functions.h
-+++ b/teensy3/avr_functions.h
-@@ -95,7 +95,7 @@ static inline void eeprom_update_block(const void *buf, void *addr, uint32_t len
- char * ultoa(unsigned long val, char *buf, int radix);
- char * ltoa(long val, char *buf, int radix);
-
--#if defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2)
-+#if defined(__STRICT_ANSI__) || (defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2))
- static inline char * utoa(unsigned int val, char *buf, int radix) __attribute__((always_inline, unused));
- static inline char * utoa(unsigned int val, char *buf, int radix) { return ultoa(val, buf, radix); }
- static inline char * itoa(int val, char *buf, int radix) __attribute__((always_inline, unused));
-diff --git a/teensy4/WCharacter.h b/teensy4/WCharacter.h
-index 5bfe697..7c500c1 100644
---- a/teensy4/WCharacter.h
-+++ b/teensy4/WCharacter.h
-@@ -61,7 +61,7 @@ inline boolean isAlpha(int c)
- // that fits into the ASCII character set.
- inline boolean isAscii(int c)
- {
-- return ( isascii (c) == 0 ? false : true);
-+ return ((c & ~0x7F) != 0 ? false : true);
- }
-
-
-@@ -143,7 +143,7 @@ inline boolean isHexadecimalDigit(int c)
- // ASCII character set, by clearing the high-order bits.
- inline int toAscii(int c)
- {
-- return toascii (c);
-+ return (c & 0x7F);
- }
-
-
-diff --git a/teensy4/avr_functions.h b/teensy4/avr_functions.h
-index fb6ca11..3b34391 100644
---- a/teensy4/avr_functions.h
-+++ b/teensy4/avr_functions.h
-@@ -97,7 +97,7 @@ static inline void eeprom_update_block(const void *buf, void *addr, uint32_t len
- char * ultoa(unsigned long val, char *buf, int radix);
- char * ltoa(long val, char *buf, int radix);
-
--#if defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2)
-+#if defined(__STRICT_ANSI__) || (defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2))
- static inline char * utoa(unsigned int val, char *buf, int radix) __attribute__((always_inline, unused));
- static inline char * utoa(unsigned int val, char *buf, int radix) { return ultoa(val, buf, radix); }
- static inline char * itoa(int val, char *buf, int radix) __attribute__((always_inline, unused));
diff --git a/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.58.1-cpp17.diff b/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.58.1-cpp17.diff
new file mode 100644
index 000000000..dd8439ef1
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/01-teensyduino_1.58.1-cpp17.diff
@@ -0,0 +1,38 @@
+diff --git a/teensy3/avr_functions.h b/teensy3/avr_functions.h
+index a03ee53..55ac283 100644
+--- a/teensy3/avr_functions.h
++++ b/teensy3/avr_functions.h
+@@ -97,12 +97,12 @@ char * ltoa(long val, char *buf, int radix);
+ char * ulltoa(unsigned long long val, char *buf, int radix);
+ char * lltoa(long long val, char *buf, int radix);
+
+-/* #if defined(__STRICT_ANSI__) || (defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2))
++#if defined(__STRICT_ANSI__) || (defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2))
+ static inline char * utoa(unsigned int val, char *buf, int radix) __attribute__((always_inline, unused));
+ static inline char * utoa(unsigned int val, char *buf, int radix) { return ultoa(val, buf, radix); }
+ static inline char * itoa(int val, char *buf, int radix) __attribute__((always_inline, unused));
+ static inline char * itoa(int val, char *buf, int radix) { return ltoa(val, buf, radix); }
+-#endif */
++#endif
+
+ char * dtostrf(float val, int width, unsigned int precision, char *buf);
+
+diff --git a/teensy4/avr_functions.h b/teensy4/avr_functions.h
+index 6d692ff..4524614 100644
+--- a/teensy4/avr_functions.h
++++ b/teensy4/avr_functions.h
+@@ -99,12 +99,12 @@ char * ltoa(long val, char *buf, int radix);
+ char * ulltoa(unsigned long long val, char *buf, int radix);
+ char * lltoa(long long val, char *buf, int radix);
+
+-/* #if defined(__STRICT_ANSI__) || (defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2))
++#if defined(__STRICT_ANSI__) || (defined(_NEWLIB_VERSION) && (__NEWLIB__ < 2 || __NEWLIB__ == 2 && __NEWLIB_MINOR__ < 2))
+ static inline char * utoa(unsigned int val, char *buf, int radix) __attribute__((always_inline, unused));
+ static inline char * utoa(unsigned int val, char *buf, int radix) { return ultoa(val, buf, radix); }
+ static inline char * itoa(int val, char *buf, int radix) __attribute__((always_inline, unused));
+ static inline char * itoa(int val, char *buf, int radix) { return ltoa(val, buf, radix); }
+-#endif */
++#endif
+
+ char * dtostrf(float val, int width, unsigned int precision, char *buf);
+
diff --git a/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/02-teensy4_nonstatic_flash_functions.diff b/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/02-teensy4_nonstatic_flash_functions.diff
deleted file mode 100644
index 251f5507a..000000000
--- a/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/02-teensy4_nonstatic_flash_functions.diff
+++ /dev/null
@@ -1,42 +0,0 @@
-diff --git a/teensy4/eeprom.c b/teensy4/eeprom.c
-index dde1809..9cdfcd0 100644
---- a/teensy4/eeprom.c
-+++ b/teensy4/eeprom.c
-@@ -54,8 +54,8 @@
- // Conversation about how this code works & what the upper limits are
- // https://forum.pjrc.com/threads/57377?p=214566&viewfull=1#post214566
-
--static void flash_write(void *addr, const void *data, uint32_t len);
--static void flash_erase_sector(void *addr);
-+void flash_write(void *addr, const void *data, uint32_t len);
-+void flash_erase_sector(void *addr);
-
- static uint8_t initialized=0;
- static uint16_t sector_index[FLASH_SECTORS];
-@@ -217,7 +217,7 @@ void eeprom_write_block(const void *buf, void *addr, uint32_t len)
- #define PINS1 FLEXSPI_LUT_NUM_PADS_1
- #define PINS4 FLEXSPI_LUT_NUM_PADS_4
-
--static void flash_wait()
-+void flash_wait()
- {
- FLEXSPI_LUT60 = LUT0(CMD_SDR, PINS1, 0x05) | LUT1(READ_SDR, PINS1, 1); // 05 = read status
- FLEXSPI_LUT61 = 0;
-@@ -239,7 +239,7 @@ static void flash_wait()
- }
-
- // write bytes into flash memory (which is already erased to 0xFF)
--static void flash_write(void *addr, const void *data, uint32_t len)
-+void flash_write(void *addr, const void *data, uint32_t len)
- {
- __disable_irq();
- FLEXSPI_LUTKEY = FLEXSPI_LUTKEY_VALUE;
-@@ -279,7 +279,7 @@ static void flash_write(void *addr, const void *data, uint32_t len)
- }
-
- // erase a 4K sector
--static void flash_erase_sector(void *addr)
-+void flash_erase_sector(void *addr)
- {
- __disable_irq();
- FLEXSPI_LUTKEY = FLEXSPI_LUTKEY_VALUE;
diff --git a/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/__init__.py b/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_arduino_build/py/pw_arduino_build/core_patches/teensy/__init__.py
diff --git a/pw_arduino_build/py/pw_arduino_build/teensy_detector.py b/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
index d00e68ec4..e5b4b7b0d 100644
--- a/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
+++ b/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
@@ -82,7 +82,7 @@ def detect_boards(arduino_package_path=False) -> list:
boards = []
detect_command = [
- (teensy_core / "hardware" / "tools" / "teensy_ports")
+ (teensy_core / "tools" / "teensy-tools" / "1.58.0" / "teensy_ports")
.absolute()
.as_posix(),
"-L",
diff --git a/pw_arduino_build/py/setup.cfg b/pw_arduino_build/py/setup.cfg
index f59cba7f4..a6799442a 100644
--- a/pw_arduino_build/py/setup.cfg
+++ b/pw_arduino_build/py/setup.cfg
@@ -25,7 +25,6 @@ install_requires =
pyserial>=3.5,<4.0
types-pyserial>=3.5,<4.0
coloredlogs
- parameterized
[options.entry_points]
console_scripts =
@@ -37,6 +36,5 @@ console_scripts =
[options.package_data]
pw_arduino_build =
- core_patches/teensy/01-teensyduino_1.53-cpp17.diff
- core_patches/teensy/02-teensy4_nonstatic_flash_functions.diff
+ core_patches/teensy/01-teensyduino_1.58.1-cpp17.diff
py.typed
diff --git a/pw_assert/BUILD.bazel b/pw_assert/BUILD.bazel
index f6d1f8616..8832be023 100644
--- a/pw_assert/BUILD.bazel
+++ b/pw_assert/BUILD.bazel
@@ -44,9 +44,21 @@ pw_cc_facade(
pw_cc_library(
name = "pw_assert",
+ hdrs = [
+ "assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h",
+ "public/pw_assert/assert.h",
+ "public/pw_assert/check.h",
+ "public/pw_assert/internal/check_impl.h",
+ "public/pw_assert/short.h",
+ ],
+ includes = [
+ "assert_compatibility_public_overrides",
+ "public",
+ ],
deps = [
- ":facade",
- "@pigweed_config//:pw_assert_backend",
+ ":config",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_assert_backend",
],
)
@@ -101,12 +113,18 @@ pw_cc_library(
pw_cc_library(
name = "backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = [
"@pigweed//pw_assert_basic",
],
)
+alias(
+ name = "backend_impl_multiplexer",
+ actual = "@pigweed//pw_assert_basic:impl",
+ visibility = ["@pigweed//targets:__pkg__"],
+)
+
pw_cc_test(
name = "assert_facade_test",
srcs = [
diff --git a/pw_assert/assert_backend_compile_test.cc b/pw_assert/assert_backend_compile_test.cc
index 888499984..fab91d1be 100644
--- a/pw_assert/assert_backend_compile_test.cc
+++ b/pw_assert/assert_backend_compile_test.cc
@@ -33,7 +33,7 @@
// the test has failed. Obviously manually verifying these is a pain
// and so this is not a suitable test for production.
//
-// TODO(b/235289499): Add verification of the actually recorded asserts
+// TODO: b/235289499 - Add verification of the actually recorded asserts
// statements.
#include "gtest/gtest.h"
diff --git a/pw_assert/assert_backend_compile_test_c.c b/pw_assert/assert_backend_compile_test_c.c
index f0c9d05e5..a13205649 100644
--- a/pw_assert/assert_backend_compile_test_c.c
+++ b/pw_assert/assert_backend_compile_test_c.c
@@ -61,6 +61,7 @@ static const int z = 10;
static int Add3(int a, int b, int c) { return a + b + c; }
+// NOLINTNEXTLINE(google-readability-function-size)
void AssertBackendCompileTestsInC(void) {
{ // TEST(Crash, WithAndWithoutMessageArguments)
MAYBE_SKIP_TEST;
diff --git a/pw_assert/docs.rst b/pw_assert/docs.rst
index f411e8184..6fd97310c 100644
--- a/pw_assert/docs.rst
+++ b/pw_assert/docs.rst
@@ -490,6 +490,8 @@ common for the ``pw_assert`` backend to cause circular dependencies. Because of
this, assert backends may avoid declaring explicit dependencies, instead relying
on include paths to access header files.
+GN
+--
In GN, the ``pw_assert`` backend's full implementation with true dependencies is
made available through the ``$dir_pw_assert:impl`` group. When
``pw_assert_BACKEND`` is set, ``$dir_pw_assert:impl`` must be listed in the
@@ -506,6 +508,31 @@ to directly provide dependencies through include paths only, rather than GN
``public_deps``. In this case, GN header checking can be disabled with
``check_includes = false``.
+Bazel
+-----
+In Bazel, assert backends may break dependency cycles by placing the full
+implementation in an ``impl`` target, like ``//pw_assert_basic:impl`` or
+``//pw_assert_tokenized:impl``. The ``//targets:pw_assert_backend_impl``
+label flag should be set to the ``impl`` target required by the assert backend
+used by the platform.
+
+You must add a dependency on the ``@pigweed//targets:pw_assert_backend_impl``
+target to any binary using ``pw_assert``. You can do this in a few ways:
+
+1. Use ``pw_cc_binary``, one of the :ref:`module-pw_build-bazel-wrapper-rules`
+ provided by Pigweed, instead of native ``cc_binary``. This wrapper adds the
+ required dependency.
+
+1. Use `link_extra_lib
+ <https://bazel.build/reference/be/c-cpp#cc_binary.link_extra_lib>`_: set
+ the ``@bazel_tools//tools/cpp:link_extra_lib`` label flag to point to
+ ``@pigweed//targets:pw_assert_backend_impl``, probably using `bazelrc
+ <https://bazel.build/run/bazelrc>`_. Note that this is only supported in
+ Bazel 7.0.0 or newer.
+
+1. Add ``@pigweed//targets:pw_assert_backend_impl`` directly to the ``deps``
+ of every embedded ``cc_binary`` in your project.
+
.. _module-pw_assert-backend_api:
-----------
@@ -800,7 +827,7 @@ Available Assert Backends
``abort`` standard library functions to implement the assert facade. Prints
the assert expression, evaluated arguments if any, file/line, function name,
and user message, then aborts. Only suitable for targets that support these
- standard library functions. Compatible with C++14.
+ standard library functions.
- ``pw_assert_basic`` - **Stable** - The assert basic module is a simple assert
handler that displays the failed assert line and the values of captured
arguments. Output is directed to ``pw_sys_io``. This module is a great
diff --git a/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h b/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h
index b62a59585..272ad203d 100644
--- a/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h
+++ b/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h
@@ -37,16 +37,16 @@
PW_ASSERT_PRINT_EXPRESSION("CHECK", \
arg_a_str " " comparison_op_str " " arg_b_str); \
fprintf(stderr, \
- " EVALUATED CONDITION\n\n " arg_a_str " (=" type_fmt \
- ") " comparison_op_str " " arg_b_str " (=" type_fmt \
+ " \033[33mEVALUATED CONDITION\033[0m\n\n " arg_a_str \
+ " (=" type_fmt ") " comparison_op_str " " arg_b_str " (=" type_fmt \
")" \
".\n\n", \
arg_a_val, \
arg_b_val); \
_PW_ASSERT_PRINT_MESSAGE_AND_ABORT(__VA_ARGS__)
-#define _PW_ASSERT_PRINT_MESSAGE_AND_ABORT(...) \
- fprintf(stderr, " MESSAGE\n\n " __VA_ARGS__); \
- fprintf(stderr, "\n\n"); \
- fflush(stderr); \
+#define _PW_ASSERT_PRINT_MESSAGE_AND_ABORT(...) \
+ fprintf(stderr, " \033[33mMESSAGE\033[0m\n\n " __VA_ARGS__); \
+ fprintf(stderr, "\n\n"); \
+ fflush(stderr); \
abort()
diff --git a/pw_assert/public/pw_assert/internal/libc_assert.h b/pw_assert/public/pw_assert/internal/libc_assert.h
index 865650ed1..11a5a5880 100644
--- a/pw_assert/public/pw_assert/internal/libc_assert.h
+++ b/pw_assert/public/pw_assert/internal/libc_assert.h
@@ -23,7 +23,7 @@ extern "C++" {
#include "pw_preprocessor/util.h"
#ifdef __cplusplus
-} // extern "C++"
+} // extern "C++"
#endif // __cplusplus
// Provide static_assert() on >=C11
diff --git a/pw_assert/public/pw_assert/internal/print_and_abort.h b/pw_assert/public/pw_assert/internal/print_and_abort.h
index be231f166..5112a1ac8 100644
--- a/pw_assert/public/pw_assert/internal/print_and_abort.h
+++ b/pw_assert/public/pw_assert/internal/print_and_abort.h
@@ -30,6 +30,22 @@
#define _PW_ASSERT_ABORT_FUNCTION __func__
#endif // __GNUC__
+// clang-format off
+#define _PW_ASSERT_CRASH_BANNER \
+ "\n" \
+ " ▄████▄ ██▀███ ▄▄▄ ██████ ██░ ██ \n" \
+ " ▒██▀ ▀█ ▓██ ▒ ██▒ ▒████▄ ▒██ ▒ ▓██░ ██▒\n" \
+ " ▒▓█ 💥 ▄ ▓██ ░▄█ ▒ ▒██ ▀█▄ ░ ▓██▄ ▒██▀▀██░\n" \
+ " ▒▓▓▄ ▄██▒ ▒██▀▀█▄ ░██▄▄▄▄██ ▒ ██▒ ░▓█ ░██ \n" \
+ " ▒ ▓███▀ ░ ░██▓ ▒██▒ ▓█ ▓██▒ ▒██████▒▒ ░▓█▒░██▓\n" \
+ " ░ ░▒ ▒ ░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒\n" \
+ " ░ ▒ ░▒ ░ ▒░ ▒ ▒▒ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░\n" \
+ " ░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░\n" \
+ " ░ ░ ░ ░ ░ ░ ░ ░ ░\n" \
+ " ░\n" \
+ "\n"
+// clang-format on
+
// This assert implementation prints the file path, line number, and assert
// expression using printf. Uses ANSI escape codes for colors.
//
@@ -39,19 +55,20 @@
fflush(stderr); \
abort()
-#define PW_ASSERT_PRINT_EXPRESSION(macro, expression) \
- fflush(stdout); \
+#define PW_ASSERT_PRINT_EXPRESSION(macro, expression) \
+ fflush(stdout); \
+ fprintf(stderr, "\033[31m" _PW_ASSERT_CRASH_BANNER "\033[0m"); \
fprintf(stderr, \
"\033[41m\033[37m\033[1m%s:%d:\033[0m " \
"\033[1m" \
_PW_ASSERT_MACRO(macro) \
" " \
"\033[31mFAILED!\033[0m\n\n" \
- " FAILED ASSERTION\n\n" \
+ " \033[33mFAILED ASSERTION\033[0m\n\n" \
" %s\n\n" \
- " FILE & LINE\n\n" \
+ " \033[33mFILE & LINE\033[0m\n\n" \
" %s:%d\n\n" \
- " FUNCTION\n\n" \
+ " \033[33mFUNCTION\033[0m\n\n" \
" %s\n\n", \
__FILE__, \
__LINE__, \
diff --git a/pw_assert_basic/BUILD.bazel b/pw_assert_basic/BUILD.bazel
index 7308a580d..b19cd2bbe 100644
--- a/pw_assert_basic/BUILD.bazel
+++ b/pw_assert_basic/BUILD.bazel
@@ -14,6 +14,7 @@
load(
"//pw_build:pigweed.bzl",
+ "pw_cc_facade",
"pw_cc_library",
)
@@ -21,8 +22,15 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+# Note: to avoid circular dependencies, this target only includes the headers
+# for pw_assert_basic. The source file and its dependencies are in the separate
+# ":impl" target.
+#
+# If you point @pigweed//targets:pw_assert_backend to //pw_assert_basic, then
+# @pigweed//targets:pw_assert_backend_impl should point to
+# //pw_assert_basic:impl.
pw_cc_library(
- name = "headers",
+ name = "pw_assert_basic",
hdrs = [
"public/pw_assert_basic/assert_basic.h",
"public_overrides/pw_assert_backend/check_backend.h",
@@ -32,24 +40,28 @@ pw_cc_library(
"public_overrides",
],
deps = [
+ ":handler_facade",
"//pw_preprocessor",
],
)
pw_cc_library(
- name = "pw_assert_basic",
+ name = "impl",
srcs = [
"assert_basic.cc",
],
deps = [
- ":headers",
+ ":pw_assert_basic",
":pw_assert_basic_handler",
"//pw_assert:facade",
"//pw_preprocessor",
],
+ # Other libraries may not always depend on this library, even if it is
+ # necessary at link time.
+ alwayslink = 1,
)
-pw_cc_library(
+pw_cc_facade(
name = "handler_facade",
hdrs = [
"public/pw_assert_basic/handler.h",
@@ -66,9 +78,12 @@ pw_cc_library(
],
deps = [
":handler_facade",
- ":headers",
+ ":pw_assert_basic",
"//pw_preprocessor",
"//pw_string:builder",
"//pw_sys_io",
],
+ # Other libraries may not always depend on this library, even if it is
+ # necessary at link time.
+ alwayslink = 1,
)
diff --git a/pw_assert_basic/BUILD.gn b/pw_assert_basic/BUILD.gn
index 4c5244317..8398c5f00 100644
--- a/pw_assert_basic/BUILD.gn
+++ b/pw_assert_basic/BUILD.gn
@@ -37,7 +37,7 @@ pw_facade("handler") {
public = [ "public/pw_assert_basic/handler.h" ]
}
-# TODO(b/235149326): This backend implements pw_assert's check backend and the
+# TODO: b/235149326 - This backend implements pw_assert's check backend and the
# temporary compatibility C ABI (pw_assert_HandleFailure()).
#
# pw_assert_basic only provides the backend's interface. The implementation is
diff --git a/pw_assert_basic/CMakeLists.txt b/pw_assert_basic/CMakeLists.txt
index 64c93d0a7..adc5ab94b 100644
--- a/pw_assert_basic/CMakeLists.txt
+++ b/pw_assert_basic/CMakeLists.txt
@@ -26,7 +26,7 @@ pw_add_facade(pw_assert_basic.handler INTERFACE
pw_preprocessor
)
-# TODO(b/235149326): This backend implements pw_assert's check backend and the
+# TODO: b/235149326 - This backend implements pw_assert's check backend and the
# temporary compatibility C ABI (pw_assert_HandleFailure()).
pw_add_library(pw_assert_basic.check_backend STATIC
HEADERS
diff --git a/pw_assert_basic/basic_handler.cc b/pw_assert_basic/basic_handler.cc
index 046b10087..f850fa626 100644
--- a/pw_assert_basic/basic_handler.cc
+++ b/pw_assert_basic/basic_handler.cc
@@ -72,7 +72,7 @@ static const char* kCrashBanner[] = {
static void WriteLine(const std::string_view& s) {
pw::sys_io::WriteLine(s)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
typedef pw::StringBuffer<150> Buffer;
diff --git a/pw_assert_log/Android.bp b/pw_assert_log/Android.bp
index 146c57546..ffa80ed50 100644
--- a/pw_assert_log/Android.bp
+++ b/pw_assert_log/Android.bp
@@ -27,3 +27,28 @@ cc_library_headers {
],
host_supported: true,
}
+
+filegroup {
+ name: "pw_assert_log_src_files",
+ srcs: [
+ "assert_log.cc",
+ ],
+}
+
+cc_defaults {
+ name: "pw_assert_log_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ ],
+ srcs: [
+ ":pw_assert_log_src_files"
+ ],
+}
diff --git a/pw_assert_log/BUILD.bazel b/pw_assert_log/BUILD.bazel
index e5b794737..2f58bdfde 100644
--- a/pw_assert_log/BUILD.bazel
+++ b/pw_assert_log/BUILD.bazel
@@ -56,3 +56,6 @@ pw_cc_library(
"//pw_preprocessor",
],
)
+
+# There is no "impl" target: pw_assert_log doesn't have potential circular
+# dependencies.
diff --git a/pw_assert_log/BUILD.gn b/pw_assert_log/BUILD.gn
index b62387315..5b601c48e 100644
--- a/pw_assert_log/BUILD.gn
+++ b/pw_assert_log/BUILD.gn
@@ -48,14 +48,14 @@ pw_source_set("check_backend") {
"$dir_pw_preprocessor",
]
sources = [
- # TODO(b/235149326): assert_log.cc implements the assert compatibility
+ # TODO: b/235149326 - assert_log.cc implements the assert compatibility
# backend, but nothing for check_backend.
"assert_log.cc",
"public/pw_assert_log/check_log.h",
]
}
-# TODO(b/235149326): Remove this deprecated alias.
+# TODO: b/235149326 - Remove this deprecated alias.
group("pw_assert_log") {
public_deps = [ ":check_backend" ]
}
diff --git a/pw_assert_log/public/pw_assert_log/check_log.h b/pw_assert_log/public/pw_assert_log/check_log.h
index 158cf6af6..d73c50285 100644
--- a/pw_assert_log/public/pw_assert_log/check_log.h
+++ b/pw_assert_log/public/pw_assert_log/check_log.h
@@ -35,14 +35,13 @@
// Die with a message with several attributes included. This assert frontend
// funnels everything into the logger, which is responsible for displaying the
// log, then crashing/rebooting the device.
-#define PW_HANDLE_ASSERT_FAILURE(condition_string, message, ...) \
- do { \
- PW_LOG(PW_LOG_LEVEL_FATAL, \
- PW_LOG_MODULE_NAME, \
- PW_LOG_FLAGS, \
- "Check failed: " condition_string ". " message, \
- __VA_ARGS__); \
- PW_UNREACHABLE; \
+#define PW_HANDLE_ASSERT_FAILURE(condition_string, ...) \
+ do { \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "Check failed: " condition_string ". " __VA_ARGS__); \
+ PW_UNREACHABLE; \
} while (0)
// Sample assert failure message produced by the below implementation:
@@ -53,23 +52,23 @@
// clang-format off
// This is too hairy for clang format to handle and retain readability.
-#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
- arg_a_val, \
- comparison_op_str, \
- arg_b_str, \
- arg_b_val, \
- type_fmt, \
- message, ...) \
- do { \
- PW_LOG(PW_LOG_LEVEL_FATAL, \
- PW_LOG_MODULE_NAME, \
- PW_LOG_FLAGS, \
- "Check failed: " \
- arg_a_str " (=" type_fmt ") " \
- comparison_op_str " " \
- arg_b_str " (=" type_fmt ")" \
- ". " message, \
- arg_a_val, arg_b_val, __VA_ARGS__); \
- PW_UNREACHABLE; \
+#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
+ arg_a_val, \
+ comparison_op_str, \
+ arg_b_str, \
+ arg_b_val, \
+ type_fmt, \
+ message, ...) \
+ do { \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "Check failed: " \
+ arg_a_str " (=" type_fmt ") " \
+ comparison_op_str " " \
+ arg_b_str " (=" type_fmt ")" \
+ ". " message, \
+ PW_DROP_LAST_ARG_IF_EMPTY(arg_a_val, arg_b_val, __VA_ARGS__)); \
+ PW_UNREACHABLE; \
} while(0)
// clang-format on
diff --git a/pw_assert_tokenized/BUILD.bazel b/pw_assert_tokenized/BUILD.bazel
index c3c49141f..fe6e77412 100644
--- a/pw_assert_tokenized/BUILD.bazel
+++ b/pw_assert_tokenized/BUILD.bazel
@@ -21,17 +21,20 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+# Note: to avoid circular dependencies, this target only includes the headers
+# for pw_assert_tokenized. The source file and its dependencies are in the separate
+# ":impl" target.
+#
+# If you point the @pigweed//targets:pw_assert_backend to
+# //pw_assert_tokenized, then @pigweed//targets:pw_assert_backend_impl should
+# point to //pw_assert_tokenized:impl.
pw_cc_library(
name = "pw_assert_tokenized",
- srcs = [
- "log_handler.cc",
- ],
hdrs = [
"assert_public_overrides/pw_assert_backend/assert_backend.h",
"check_public_overrides/pw_assert_backend/check_backend.h",
"public/pw_assert_tokenized/assert_tokenized.h",
"public/pw_assert_tokenized/check_tokenized.h",
- "public/pw_assert_tokenized/handler.h",
],
includes = [
"assert_public_overrides",
@@ -39,11 +42,37 @@ pw_cc_library(
"public",
],
deps = [
- "//pw_assert",
- "//pw_base64",
- "//pw_bytes",
+ ":handler",
+ "//pw_assert:facade",
"//pw_log_tokenized",
"//pw_preprocessor",
"//pw_tokenizer",
],
)
+
+pw_cc_library(
+ name = "handler",
+ hdrs = [
+ "public/pw_assert_tokenized/handler.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_library(
+ name = "impl",
+ srcs = [
+ "log_handler.cc",
+ ],
+ deps = [
+ ":handler",
+ "//pw_base64",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_log_tokenized",
+ "//pw_span",
+ ],
+ alwayslink = 1,
+)
diff --git a/pw_assert_tokenized/docs.rst b/pw_assert_tokenized/docs.rst
index 2458b5ad3..59ba7038e 100644
--- a/pw_assert_tokenized/docs.rst
+++ b/pw_assert_tokenized/docs.rst
@@ -18,16 +18,20 @@ the reported tokens.
number of the assert statement. By default, it is passed to the logging system
to produce a string like this:
- PW_ASSERT() or PW_DASSERT() failure at
- pw_result/public/pw_result/result.h:63
+ .. code-block:: text
+
+ PW_ASSERT() or PW_DASSERT() failure at
+ pw_result/public/pw_result/result.h:63
* **PW_CHECK_\*()**: The ``PW_CHECK_*()`` macros work in contexts where
tokenization is fully supported, so they are able to capture the CHECK
statement expression and any provided string literal in addition to the file
name in the pw_log_tokenized key/value format:
- "■msg♦Check failure: \*unoptimizable >= 0, Ensure this CHECK logic
- stays■module♦KVS■file♦pw_kvs/size_report/base.cc"
+ .. code-block:: text
+
+ "■msg♦Check failure: \*unoptimizable >= 0, Ensure this CHECK logic
+ stays■module♦KVS■file♦pw_kvs/size_report/base.cc"
Evaluated values of ``PW_CHECK_*()`` statements are not captured, and any
string formatting arguments are also not captured. This minimizes call-site
@@ -66,29 +70,29 @@ Example file name token database setup
.. code-block::
- pw_executable("main") {
- deps = [
- # ...
- ]
- sources = [ "main.cc" ]
- }
-
- pw_tokenizer_database("log_tokens") {
- database = "tools/tokenized_logs.csv"
- deps = [
- ":source_file_names",
- ":main",
- ]
- optional_paths = [ "$root_build_dir/**/*.elf" ]
- input_databases = [ "$target_gen_dir/source_file_names.json" ]
- }
-
- # Extracts all source/header file names from "main" and its transitive
- # dependencies for tokenization.
- pw_relative_source_file_names("source_file_names") {
- deps = [ ":main" ]
- outputs = [ "$target_gen_dir/source_file_names.json" ]
- }
+ pw_executable("main") {
+ deps = [
+ # ...
+ ]
+ sources = [ "main.cc" ]
+ }
+
+ pw_tokenizer_database("log_tokens") {
+ database = "tools/tokenized_logs.csv"
+ deps = [
+ ":source_file_names",
+ ":main",
+ ]
+ optional_paths = [ "$root_build_dir/**/*.elf" ]
+ input_databases = [ "$target_gen_dir/source_file_names.json" ]
+ }
+
+ # Extracts all source/header file names from "main" and its transitive
+ # dependencies for tokenization.
+ pw_relative_source_file_names("source_file_names") {
+ deps = [ ":main" ]
+ outputs = [ "$target_gen_dir/source_file_names.json" ]
+ }
.. warning::
diff --git a/pw_assert_zephyr/CMakeLists.txt b/pw_assert_zephyr/CMakeLists.txt
index 3f06d6099..b1b5819d8 100644
--- a/pw_assert_zephyr/CMakeLists.txt
+++ b/pw_assert_zephyr/CMakeLists.txt
@@ -14,10 +14,6 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(NOT CONFIG_PIGWEED_ASSERT)
- return()
-endif()
-
pw_add_library(pw_assert_zephyr.check INTERFACE
HEADERS
public/pw_assert_zephyr/check_zephyr.h
@@ -27,9 +23,8 @@ pw_add_library(pw_assert_zephyr.check INTERFACE
public_overrides
PUBLIC_DEPS
pw_preprocessor
- zephyr_interface
)
-zephyr_link_libraries(pw_assert_zephyr.check)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_ASSERT pw_assert_zephyr.check)
pw_add_library(pw_assert_zephyr.assert INTERFACE
HEADERS
@@ -38,7 +33,5 @@ pw_add_library(pw_assert_zephyr.assert INTERFACE
PUBLIC_INCLUDES
public
public_overrides
- PUBLIC_DEPS
- zephyr_interface
)
-zephyr_link_libraries(pw_assert_zephyr.assert)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_ASSERT pw_assert_zephyr.assert)
diff --git a/pw_assert_zephyr/Kconfig b/pw_assert_zephyr/Kconfig
index de2290cdc..59e41b7cc 100644
--- a/pw_assert_zephyr/Kconfig
+++ b/pw_assert_zephyr/Kconfig
@@ -12,6 +12,12 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_assert"
+
config PIGWEED_ASSERT
- bool "Enable Pigweed assert library (pw_assert)"
+ bool "Link pw_assert library"
select PIGWEED_PREPROCESSOR
+ help
+ See :ref:`module-pw_assert` for module details.
+
+endmenu
diff --git a/pw_assert_zephyr/docs.rst b/pw_assert_zephyr/docs.rst
index b70a15ea2..12778922c 100644
--- a/pw_assert_zephyr/docs.rst
+++ b/pw_assert_zephyr/docs.rst
@@ -17,5 +17,5 @@ To enable the assert module, set ``CONFIG_PIGWEED_ASSERT=y``. After that,
Zephyr's assert configs can be used to control the behavior via CONFIG_ASSERT_
and CONFIG_ASSERT_LEVEL_.
-.. _CONFIG_ASSERT: https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_ASSERT.html#std-kconfig-CONFIG_ASSERT
-.. _CONFIG_ASSERT_LEVEL: https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_ASSERT_LEVEL.html#std-kconfig-CONFIG_ASSERT_LEVEL
+.. _CONFIG_ASSERT: https://docs.zephyrproject.org/latest/kconfig.html#CONFIG_ASSERT
+.. _CONFIG_ASSERT_LEVEL: https://docs.zephyrproject.org/latest/kconfig.html#CONFIG_ASSERT_LEVEL
diff --git a/pw_async/BUILD.bazel b/pw_async/BUILD.bazel
index 2ae446d6b..4c1481100 100644
--- a/pw_async/BUILD.bazel
+++ b/pw_async/BUILD.bazel
@@ -12,14 +12,116 @@
# License for the specific language governing permissions and limitations under
# the License.
-filegroup(
- name = "pw_async_files",
- srcs = [
- "fake_dispatcher_test.cc",
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_facade",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "dispatcher",
+ hdrs = [
"public/pw_async/dispatcher.h",
- "public/pw_async/fake_dispatcher.h",
- "public/pw_async/fake_dispatcher_fixture.h",
- "public/pw_async/internal/types.h",
- "public/pw_async/task.h",
+ "public/pw_async/function_dispatcher.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":types",
+ "//pw_chrono:system_clock",
+ "//pw_function",
+ "//pw_status",
+ ],
+)
+
+pw_cc_facade(
+ name = "task_facade",
+ hdrs = ["public/pw_async/task.h"],
+ includes = ["public"],
+ deps = [
+ ":types",
+ "//pw_chrono:system_clock",
+ "//pw_function",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "task",
+ hdrs = ["public/pw_async/task.h"],
+ includes = ["public"],
+ deps = [
+ ":types",
+ "//pw_chrono:system_clock",
+ "//pw_function",
+ "//pw_status",
+ "@pigweed//targets:pw_async_task_backend",
+ ],
+)
+
+pw_cc_library(
+ name = "types",
+ hdrs = [
+ "public/pw_async/context.h",
+ "public/pw_async/task_function.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_function",
+ "//pw_status",
+ ],
+)
+
+pw_cc_facade(
+ name = "fake_dispatcher_facade",
+ hdrs = ["public/pw_async/fake_dispatcher.h"],
+ includes = ["public"],
+ deps = [":dispatcher"],
+)
+
+pw_cc_library(
+ name = "fake_dispatcher",
+ hdrs = ["public/pw_async/fake_dispatcher.h"],
+ includes = ["public"],
+ deps = [
+ ":dispatcher",
+ "@pigweed//targets:pw_async_fake_dispatcher_backend",
+ ],
+)
+
+pw_cc_test(
+ name = "fake_dispatcher_test",
+ srcs = ["fake_dispatcher_test.cc"],
+ deps = [
+ ":fake_dispatcher",
+ "//pw_containers:vector",
+ "//pw_log",
+ "//pw_string:to_string",
+ "//pw_sync:timed_thread_notification",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_library(
+ name = "fake_dispatcher_fixture",
+ hdrs = ["public/pw_async/fake_dispatcher_fixture.h"],
+ includes = ["public"],
+ deps = [":fake_dispatcher"],
+)
+
+pw_cc_library(
+ name = "heap_dispatcher",
+ srcs = ["heap_dispatcher.cc"],
+ hdrs = ["public/pw_async/heap_dispatcher.h"],
+ includes = ["public"],
+ deps = [
+ ":dispatcher",
+ ":task",
+ ":types",
+ "//pw_result",
],
)
diff --git a/pw_async/BUILD.gn b/pw_async/BUILD.gn
index 483b9b5e6..59ccdd046 100644
--- a/pw_async/BUILD.gn
+++ b/pw_async/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_async/async.gni")
import("$dir_pw_async/backend.gni")
import("$dir_pw_async/fake_dispatcher_fixture.gni")
import("$dir_pw_async/fake_dispatcher_test.gni")
+import("$dir_pw_async/heap_dispatcher.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
@@ -30,10 +31,15 @@ config("public_include_path") {
pw_source_set("dispatcher") {
public_configs = [ ":public_include_path" ]
public_deps = [
+ ":types",
"$dir_pw_chrono:system_clock",
dir_pw_function,
+ dir_pw_status,
+ ]
+ public = [
+ "public/pw_async/dispatcher.h",
+ "public/pw_async/function_dispatcher.h",
]
- public = [ "public/pw_async/dispatcher.h" ]
visibility = [
":*",
"$dir_pw_async_basic:*",
@@ -44,13 +50,27 @@ pw_facade("task") {
backend = pw_async_TASK_BACKEND
public_configs = [ ":public_include_path" ]
public_deps = [
+ ":types",
"$dir_pw_chrono:system_clock",
dir_pw_function,
dir_pw_status,
]
+ public = [ "public/pw_async/task.h" ]
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+pw_source_set("types") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ dir_pw_function,
+ dir_pw_status,
+ ]
public = [
- "public/pw_async/internal/types.h",
- "public/pw_async/task.h",
+ "public/pw_async/context.h",
+ "public/pw_async/task_function.h",
]
visibility = [
":*",
@@ -77,6 +97,14 @@ fake_dispatcher_fixture("fake_dispatcher_fixture") {
] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
}
+pw_async_heap_dispatcher_source_set("heap_dispatcher") {
+ task_backend = ":task"
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
pw_test_group("tests") {
}
@@ -85,10 +113,12 @@ pw_doc_group("docs") {
}
# Satisfy source_is_in_build_files presubmit step
-pw_source_set("fake_dispatcher_test") {
+pw_source_set("satisfy_presubmit") {
sources = [
"fake_dispatcher_test.cc",
+ "heap_dispatcher.cc",
"public/pw_async/fake_dispatcher_fixture.h",
+ "public/pw_async/heap_dispatcher.h",
]
visibility = []
}
diff --git a/pw_async/CMakeLists.txt b/pw_async/CMakeLists.txt
new file mode 100644
index 000000000..126a05854
--- /dev/null
+++ b/pw_async/CMakeLists.txt
@@ -0,0 +1,55 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_async/backend.cmake)
+
+pw_add_library(pw_async.types INTERFACE
+ HEADERS
+ public/pw_async/context.h
+ public/pw_async/task_function.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_function
+ pw_status
+)
+
+pw_add_facade(pw_async.task INTERFACE
+ BACKEND
+ pw_async.task_BACKEND
+ HEADERS
+ public/pw_async/task.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_async.types
+ pw_function
+ pw_status
+)
+
+pw_add_facade(pw_async.dispatcher INTERFACE
+ BACKEND
+ pw_async.dispatcher_BACKEND
+ HEADERS
+ public/pw_async/dispatcher.h
+ public/pw_async/function_dispatcher.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_async.types
+ pw_chrono.system_clock
+ pw_function
+ pw_status
+)
diff --git a/pw_async/OWNERS b/pw_async/OWNERS
new file mode 100644
index 000000000..95e4875d7
--- /dev/null
+++ b/pw_async/OWNERS
@@ -0,0 +1,2 @@
+benlawson@google.com
+cramertj@google.com
diff --git a/pw_async/backend.cmake b/pw_async/backend.cmake
new file mode 100644
index 000000000..b9ddc0287
--- /dev/null
+++ b/pw_async/backend.cmake
@@ -0,0 +1,21 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_async module.
+pw_add_backend_variable(pw_async.task_BACKEND)
+pw_add_backend_variable(pw_async.dispatcher_BACKEND)
diff --git a/pw_async/docs.rst b/pw_async/docs.rst
index e1a34e009..8ead20e04 100644
--- a/pw_async/docs.rst
+++ b/pw_async/docs.rst
@@ -33,6 +33,11 @@ Dispatcher API
Task API
==============
+.. doxygenstruct:: pw::async::Context
+ :members:
+
+.. doxygentypedef:: pw::async::TaskFunction
+
.. doxygenclass:: pw::async::Task
:members:
@@ -41,8 +46,17 @@ Facade API
Task
----
-The Task type represents a work item that is submitted to a Dispatcher. The Task
-facade enables Dispatcher backends to specify custom state and methods.
+The ``Task`` type represents a work item that can be submitted to and executed
+by a ``Dispatcher``.
+
+To run work on a ``Dispatcher`` event loop, a ``Task`` can be constructed from
+a function or lambda (see ``pw::async::TaskFunction``) and submitted to run
+using the ``pw::async::Dispatcher::Post`` method (and its siblings, ``PostAt``
+etc.).
+
+The ``Task`` facade enables backends to provide custom storage containers for
+``Task`` s, as well as to keep per- ``Task`` data alongside the ``TaskFunction``
+(such as ``next`` pointers for intrusive linked-lists of ``Task``).
The active Task backend is configured with the GN variable
``pw_async_TASK_BACKEND``. The specified target must define a class
@@ -50,6 +64,9 @@ The active Task backend is configured with the GN variable
that meets the interface requirements in ``public/pw_async/task.h``. Task will
then trivially wrap ``NativeTask``.
+The bazel build provides the ``pw_async_task_backend`` label flag to configure
+the active Task backend.
+
FakeDispatcher
--------------
The FakeDispatcher facade is a utility for simulating a real Dispatcher
@@ -58,13 +75,16 @@ code that uses Dispatcher. FakeDispatcher is a facade instead of a concrete
implementation because it depends on Task state for processing tasks, which
varies across Task backends.
-The active Task backend is configured with the GN variable
+The active FakeDispatcher backend is configured with the GN variable
``pw_async_FAKE_DISPATCHER_BACKEND``. The specified target must define a class
``pw::async::test::backend::NativeFakeDispatcher`` in the header
``pw_async_backend/fake_dispatcher.h`` that meets the interface requirements in
``public/pw_async/task.h``. FakeDispatcher will then trivially wrap
``NativeFakeDispatcher``.
+The bazel build provides the ``pw_async_fake_dispatcher_backend`` label flag to
+configure the FakeDispatcher backend.
+
Testing FakeDispatcher
^^^^^^^^^^^^^^^^^^^^^^
The GN template ``fake_dispatcher_tests`` in ``fake_dispatcher_tests.gni``
@@ -72,6 +92,16 @@ creates a test target that tests a FakeDispatcher backend. This enables
one test suite to be shared across FakeDispatcher backends and ensures
conformance.
+FunctionDispatcher
+------------------
+.. doxygenclass:: pw::async::FunctionDispatcher
+ :members:
+
+HeapDispatcher
+--------------
+.. doxygenclass:: pw::async::HeapDispatcher
+ :members:
+
Design
======
@@ -169,6 +199,5 @@ Roadmap
-------
- Stabilize Task cancellation API
- Utility for dynamically allocated Tasks
-- Bazel support
- CMake support
- Support for C++20 coroutines
diff --git a/pw_async/fake_dispatcher_fixture.gni b/pw_async/fake_dispatcher_fixture.gni
index a58be16ac..7e8fda760 100644
--- a/pw_async/fake_dispatcher_fixture.gni
+++ b/pw_async/fake_dispatcher_fixture.gni
@@ -28,6 +28,7 @@ template("fake_dispatcher_fixture") {
assert(defined(invoker.backend))
pw_source_set(target_name) {
+ testonly = pw_unit_test_TESTONLY
public = [ "$dir_pw_async/public/pw_async/fake_dispatcher_fixture.h" ]
public_deps = [
"$dir_pw_unit_test",
diff --git a/pw_async/fake_dispatcher_test.cc b/pw_async/fake_dispatcher_test.cc
index 3674f4cb0..3e5113aa1 100644
--- a/pw_async/fake_dispatcher_test.cc
+++ b/pw_async/fake_dispatcher_test.cc
@@ -14,176 +14,316 @@
#include "pw_async/fake_dispatcher.h"
#include "gtest/gtest.h"
-#include "pw_thread/thread.h"
-#include "pw_thread_stl/options.h"
+#include "pw_containers/vector.h"
+#include "pw_string/to_string.h"
#define ASSERT_OK(status) ASSERT_EQ(OkStatus(), status)
#define ASSERT_CANCELLED(status) ASSERT_EQ(Status::Cancelled(), status)
using namespace std::chrono_literals;
+struct CallCounts {
+ int ok = 0;
+ int cancelled = 0;
+ bool operator==(const CallCounts& other) const {
+ return ok == other.ok && cancelled == other.cancelled;
+ }
+};
+
+namespace pw {
+template <>
+StatusWithSize ToString<CallCounts>(const CallCounts& value,
+ span<char> buffer) {
+ return string::Format(buffer,
+ "CallCounts {.ok = %d, .cancelled = %d}",
+ value.ok,
+ value.cancelled);
+}
+} // namespace pw
+
namespace pw::async::test {
+namespace {
+
+struct CallCounter {
+ CallCounts counts;
+ auto fn() {
+ return [this](Context&, Status status) {
+ if (status.ok()) {
+ this->counts.ok++;
+ } else if (status.IsCancelled()) {
+ this->counts.cancelled++;
+ }
+ };
+ }
+};
-TEST(FakeDispatcher, PostTasks) {
+TEST(FakeDispatcher, UnpostedTasksDontRun) {
FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{});
+}
- int count = 0;
- auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
- ASSERT_OK(status);
- ++count;
- };
+TEST(FakeDispatcher, PostedTaskRunsOnce) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+}
- Task task(inc_count);
+TEST(FakeDispatcher, TaskPostedTwiceBeforeRunningRunsOnce) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
dispatcher.Post(task);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+}
- Task task2(inc_count);
- dispatcher.Post(task2);
+TEST(FakeDispatcher, TaskRepostedAfterRunningRunsTwice) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+ dispatcher.Post(task);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 2});
+}
- Task task3(inc_count);
- dispatcher.Post(task3);
+TEST(FakeDispatcher, TwoPostedTasksEachRunOnce) {
+ FakeDispatcher dispatcher;
+ CallCounter counter_1;
+ Task task_1(counter_1.fn());
+ CallCounter counter_2;
+ Task task_2(counter_2.fn());
+ dispatcher.Post(task_1);
+ dispatcher.Post(task_2);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter_1.counts, CallCounts{.ok = 1});
+ EXPECT_EQ(counter_2.counts, CallCounts{.ok = 1});
+}
- // Should not run; RunUntilIdle() does not advance time.
- Task task4([&count]([[maybe_unused]] Context& c, Status status) {
- ASSERT_CANCELLED(status);
- ++count;
- });
- dispatcher.PostAfter(task4, 1ms);
+TEST(FakeDispatcher, PostedTasksRunInOrderForFairness) {
+ FakeDispatcher dispatcher;
+ pw::Vector<uint8_t, 3> task_run_order;
+ Task task_1([&task_run_order](auto...) { task_run_order.push_back(1); });
+ Task task_2([&task_run_order](auto...) { task_run_order.push_back(2); });
+ Task task_3([&task_run_order](auto...) { task_run_order.push_back(3); });
+ dispatcher.Post(task_1);
+ dispatcher.Post(task_2);
+ dispatcher.Post(task_3);
+ dispatcher.RunUntilIdle();
+ pw::Vector<uint8_t, 3> expected_run_order({1, 2, 3});
+ EXPECT_EQ(task_run_order, expected_run_order);
+}
+TEST(FakeDispatcher, RequestStopQueuesPreviouslyPostedTaskWithCancel) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ dispatcher.RequestStop();
dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.cancelled = 1});
+}
+
+TEST(FakeDispatcher, RequestStopQueuesNewlyPostedTaskWithCancel) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
dispatcher.RequestStop();
+ dispatcher.Post(task);
dispatcher.RunUntilIdle();
- ASSERT_EQ(count, 4);
+ EXPECT_EQ(counter.counts, CallCounts{.cancelled = 1});
}
-// Lambdas can only capture one ptr worth of memory without allocating, so we
-// group the data we want to share between tasks and their containing tests
-// inside one struct.
-struct TaskPair {
- Task task_a;
- Task task_b;
- int count = 0;
-};
+TEST(FakeDispatcher, RunUntilIdleDoesNotRunFutureTask) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ // Should not run; RunUntilIdle() does not advance time.
+ Task task(counter.fn());
+ dispatcher.PostAfter(task, 1ms);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{});
+}
-TEST(FakeDispatcher, DelayedTasks) {
+TEST(FakeDispatcher, PostAfterRunsTasksInSequence) {
FakeDispatcher dispatcher;
- TaskPair tp;
+ pw::Vector<uint8_t, 3> task_run_order;
+ Task task_1([&task_run_order](auto...) { task_run_order.push_back(1); });
+ Task task_2([&task_run_order](auto...) { task_run_order.push_back(2); });
+ Task task_3([&task_run_order](auto...) { task_run_order.push_back(3); });
+ dispatcher.PostAfter(task_1, 50ms);
+ dispatcher.PostAfter(task_2, 25ms);
+ dispatcher.PostAfter(task_3, 100ms);
+ dispatcher.RunFor(125ms);
+ pw::Vector<uint8_t, 3> expected_run_order({2, 1, 3});
+ EXPECT_EQ(task_run_order, expected_run_order);
+}
- Task task0([&tp]([[maybe_unused]] Context& c, Status status) {
- ASSERT_OK(status);
- tp.count = tp.count * 10 + 4;
- });
- dispatcher.PostAfter(task0, 200ms);
+TEST(FakeDispatcher, PostAfterWithEarlierTimeRunsSooner) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.PostAfter(task, 100ms);
+ dispatcher.PostAfter(task, 50ms);
+ dispatcher.RunFor(60ms);
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+}
- Task task1([&tp]([[maybe_unused]] Context& c, Status status) {
- ASSERT_OK(status);
- tp.count = tp.count * 10 + 1;
- c.dispatcher->PostAfter(tp.task_a, 50ms);
- c.dispatcher->PostAfter(tp.task_b, 25ms);
- });
- dispatcher.PostAfter(task1, 100ms);
+TEST(FakeDispatcher, PostAfterWithLaterTimeRunsSooner) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.PostAfter(task, 50ms);
+ dispatcher.PostAfter(task, 100ms);
+ dispatcher.RunFor(60ms);
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+}
- tp.task_a.set_function([&tp]([[maybe_unused]] Context& c, Status status) {
- ASSERT_OK(status);
- tp.count = tp.count * 10 + 3;
- });
- tp.task_b.set_function([&tp]([[maybe_unused]] Context& c, Status status) {
- ASSERT_OK(status);
- tp.count = tp.count * 10 + 2;
- });
+TEST(FakeDispatcher, PostThenPostAfterRunsImmediately) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ dispatcher.PostAfter(task, 50ms);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+}
- dispatcher.RunFor(200ms);
- dispatcher.RequestStop();
+TEST(FakeDispatcher, PostAfterThenPostRunsImmediately) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.PostAfter(task, 50ms);
+ dispatcher.Post(task);
dispatcher.RunUntilIdle();
- ASSERT_EQ(tp.count, 1234);
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
}
-TEST(FakeDispatcher, CancelTasks) {
+TEST(FakeDispatcher, CancelAfterPostStopsTaskFromRunning) {
FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ EXPECT_TRUE(dispatcher.Cancel(task));
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{});
+}
- auto shouldnt_run = []([[maybe_unused]] Context& c,
- [[maybe_unused]] Status status) { FAIL(); };
+TEST(FakeDispatcher, CancelAfterPostAfterStopsTaskFromRunning) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.PostAfter(task, 50ms);
+ EXPECT_TRUE(dispatcher.Cancel(task));
+ dispatcher.RunFor(60ms);
+ EXPECT_EQ(counter.counts, CallCounts{});
+}
- TaskPair tp;
- // This task gets canceled in cancel_task.
- tp.task_a.set_function(shouldnt_run);
- dispatcher.PostAfter(tp.task_a, 40ms);
+TEST(FakeDispatcher, CancelAfterPostAndPostAfterStopsTaskFromRunning) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ dispatcher.PostAfter(task, 50ms);
+ EXPECT_TRUE(dispatcher.Cancel(task));
+ dispatcher.RunFor(60ms);
+ EXPECT_EQ(counter.counts, CallCounts{});
+}
- // This task gets canceled immediately.
- Task task1(shouldnt_run);
- dispatcher.PostAfter(task1, 10ms);
- ASSERT_TRUE(dispatcher.Cancel(task1));
+TEST(FakeDispatcher, PostAgainAfterCancelRuns) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
+ EXPECT_TRUE(dispatcher.Cancel(task));
+ dispatcher.Post(task);
+ dispatcher.RunUntilIdle();
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+}
- // This task cancels the first task.
- Task cancel_task([&tp](Context& c, Status status) {
- ASSERT_OK(status);
- ASSERT_TRUE(c.dispatcher->Cancel(tp.task_a));
- ++tp.count;
- });
- dispatcher.PostAfter(cancel_task, 20ms);
+TEST(FakeDispatcher, CancelWithoutPostReturnsFalse) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ EXPECT_FALSE(dispatcher.Cancel(task));
+}
- dispatcher.RunFor(50ms);
- dispatcher.RequestStop();
+TEST(FakeDispatcher, CancelAfterRunningReturnsFalse) {
+ FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task(counter.fn());
+ dispatcher.Post(task);
dispatcher.RunUntilIdle();
- ASSERT_EQ(tp.count, 1);
+ EXPECT_EQ(counter.counts, CallCounts{.ok = 1});
+ EXPECT_FALSE(dispatcher.Cancel(task));
}
-// Test RequestStop() from inside task.
-TEST(FakeDispatcher, RequestStopInsideTask) {
+TEST(FakeDispatcher, CancelInsideOtherTaskCancelsTaskWithoutRunningIt) {
FakeDispatcher dispatcher;
- int count = 0;
- auto cancelled_cb = [&count]([[maybe_unused]] Context& c, Status status) {
- ASSERT_CANCELLED(status);
- ++count;
- };
+ CallCounter cancelled_task_counter;
+ Task cancelled_task(cancelled_task_counter.fn());
- // These tasks are never executed and cleaned up in RequestStop().
- Task task0(cancelled_cb), task1(cancelled_cb);
- dispatcher.PostAfter(task0, 20ms);
- dispatcher.PostAfter(task1, 21ms);
-
- Task stop_task([&count]([[maybe_unused]] Context& c, Status status) {
+ Task canceling_task([&cancelled_task](Context& c, Status status) {
ASSERT_OK(status);
- ++count;
- static_cast<FakeDispatcher*>(c.dispatcher)->RequestStop();
- static_cast<FakeDispatcher*>(c.dispatcher)->RunUntilIdle();
+ ASSERT_TRUE(c.dispatcher->Cancel(cancelled_task));
});
- dispatcher.Post(stop_task);
+ dispatcher.Post(canceling_task);
+ dispatcher.Post(cancelled_task);
dispatcher.RunUntilIdle();
- ASSERT_EQ(count, 3);
+
+ // NOTE: the cancelled task is *not* run with `Cancel`.
+ // This is likely to produce strange behavior, and this contract should
+ // be revisited and carefully documented.
+ EXPECT_EQ(cancelled_task_counter.counts, CallCounts{});
}
-TEST(FakeDispatcher, PeriodicTasks) {
+TEST(FakeDispatcher, CancelInsideCurrentTaskFails) {
FakeDispatcher dispatcher;
- int count = 0;
- Task periodic_task([&count]([[maybe_unused]] Context& c, Status status) {
+ Task self_cancel_task;
+ self_cancel_task.set_function([&self_cancel_task](Context& c, Status status) {
ASSERT_OK(status);
- ++count;
+ ASSERT_FALSE(c.dispatcher->Cancel(self_cancel_task));
});
- dispatcher.PostPeriodicAt(periodic_task, 20ms, dispatcher.now() + 50ms);
+ dispatcher.Post(self_cancel_task);
+ dispatcher.RunUntilIdle();
+}
+
+TEST(FakeDispatcher, RequestStopInsideOtherTaskCancelsOtherTask) {
+ FakeDispatcher dispatcher;
+
+ // This task is never executed and is cleaned up in RequestStop().
+ CallCounter task_counter;
+ Task task(task_counter.fn());
- // Cancel periodic task after it has run thrice, at +50ms, +70ms, and +90ms.
- Task cancel_task([&periodic_task](Context& c, Status status) {
+ int stop_count = 0;
+ Task stop_task([&stop_count]([[maybe_unused]] Context& c, Status status) {
ASSERT_OK(status);
- c.dispatcher->Cancel(periodic_task);
+ stop_count++;
+ static_cast<FakeDispatcher*>(c.dispatcher)->RequestStop();
});
- dispatcher.PostAfter(cancel_task, 100ms);
- dispatcher.RunFor(300ms);
- dispatcher.RequestStop();
+ dispatcher.Post(stop_task);
+ dispatcher.Post(task);
+
dispatcher.RunUntilIdle();
- ASSERT_EQ(count, 3);
+ EXPECT_EQ(stop_count, 1);
+ EXPECT_EQ(task_counter.counts, CallCounts{.cancelled = 1});
}
TEST(FakeDispatcher, TasksCancelledByDispatcherDestructor) {
- int count = 0;
- auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
- ASSERT_CANCELLED(status);
- ++count;
- };
- Task task0(inc_count), task1(inc_count), task2(inc_count);
+ CallCounter counter;
+ Task task0(counter.fn()), task1(counter.fn()), task2(counter.fn());
{
FakeDispatcher dispatcher;
@@ -192,25 +332,21 @@ TEST(FakeDispatcher, TasksCancelledByDispatcherDestructor) {
dispatcher.PostAfter(task2, 10s);
}
- ASSERT_EQ(count, 3);
+ ASSERT_EQ(counter.counts, CallCounts{.cancelled = 3});
}
TEST(DispatcherBasic, TasksCancelledByRunFor) {
- int count = 0;
- auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
- ASSERT_CANCELLED(status);
- ++count;
- };
- Task task0(inc_count), task1(inc_count), task2(inc_count);
-
FakeDispatcher dispatcher;
+ CallCounter counter;
+ Task task0(counter.fn()), task1(counter.fn()), task2(counter.fn());
dispatcher.PostAfter(task0, 10s);
dispatcher.PostAfter(task1, 10s);
dispatcher.PostAfter(task2, 10s);
dispatcher.RequestStop();
dispatcher.RunFor(5s);
- ASSERT_EQ(count, 3);
+ ASSERT_EQ(counter.counts, CallCounts{.cancelled = 3});
}
+} // namespace
} // namespace pw::async::test
diff --git a/pw_async/fake_dispatcher_test.gni b/pw_async/fake_dispatcher_test.gni
index 33a32ee33..0cc2995e4 100644
--- a/pw_async/fake_dispatcher_test.gni
+++ b/pw_async/fake_dispatcher_test.gni
@@ -33,6 +33,8 @@ template("fake_dispatcher_test") {
pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "" &&
pw_thread_THREAD_BACKEND != ""
deps = [
+ "$dir_pw_containers:vector",
+ "$dir_pw_string:to_string",
"$dir_pw_sync:timed_thread_notification",
"$dir_pw_thread:thread",
dir_pw_log,
diff --git a/pw_async/heap_dispatcher.cc b/pw_async/heap_dispatcher.cc
new file mode 100644
index 000000000..e9f51a30b
--- /dev/null
+++ b/pw_async/heap_dispatcher.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_async/heap_dispatcher.h"
+
+#include "pw_async/task.h"
+#include "pw_result/result.h"
+
+namespace pw::async {
+
+namespace {
+
+// TODO: b/277793223 - Optimize to avoid double virtual indirection and double
+// allocation. In situations in which pw::Function is large enough and the
+// captures are small enough, we could eliminate this by reshaping the task as
+// just a pw::Function.
+struct TaskAndFunction {
+ static Result<TaskAndFunction*> New(TaskFunction&& task) {
+ // std::nothrow causes new to return a nullptr on failure instead of
+ // throwing.
+ TaskAndFunction* t = new (std::nothrow) TaskAndFunction();
+ if (!t) {
+ return Status::ResourceExhausted();
+ }
+ t->func = std::move(task);
+
+ // Closure captures must not include references, as that would be UB due to
+ // the `delete` at the end of the function. See
+ // https://reviews.llvm.org/D48239.
+ t->task.set_function([t](Context& ctx, Status status) {
+ t->func(ctx, status);
+
+ // Delete must appear at the very end of this closure to avoid
+ // use-after-free of captures or Context.task.
+ delete t;
+ });
+
+ return t;
+ }
+ Task task;
+ TaskFunction func;
+};
+} // namespace
+
+Status HeapDispatcher::PostAt(TaskFunction&& task_func,
+ chrono::SystemClock::time_point time) {
+ Result<TaskAndFunction*> result = TaskAndFunction::New(std::move(task_func));
+ if (!result.ok()) {
+ return result.status();
+ }
+ dispatcher_.PostAt((*result)->task, time);
+ return Status();
+}
+
+} // namespace pw::async
diff --git a/pw_async/heap_dispatcher.gni b/pw_async/heap_dispatcher.gni
new file mode 100644
index 000000000..674a4fc3a
--- /dev/null
+++ b/pw_async/heap_dispatcher.gni
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+# Creates a HeapDispatcher source set for a specified Task backend.
+#
+# Parameters
+#
+# task_backend (required)
+# [target] The Task backend.
+template("pw_async_heap_dispatcher_source_set") {
+ assert(defined(invoker.task_backend))
+
+ pw_source_set(target_name) {
+ public = [ "$dir_pw_async/public/pw_async/heap_dispatcher.h" ]
+ sources = [ "$dir_pw_async/heap_dispatcher.cc" ]
+ deps = [ "$dir_pw_result" ]
+ public_deps = [
+ "$dir_pw_async:dispatcher",
+ "$dir_pw_async:types",
+ invoker.task_backend,
+ ]
+ forward_variables_from(invoker, [ "visibility" ])
+ }
+}
diff --git a/pw_async/public/pw_async/context.h b/pw_async/public/pw_async/context.h
new file mode 100644
index 000000000..845d11374
--- /dev/null
+++ b/pw_async/public/pw_async/context.h
@@ -0,0 +1,32 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::async {
+
+class Dispatcher;
+class Task;
+
+/// Contextual information provided by a `Dispatcher` to a running task.
+struct Context {
+ /// The `Dispatcher` running the current `Task`.
+ Dispatcher* dispatcher;
+ /// The current ``Task`` being executed.
+ Task* task;
+};
+
+} // namespace pw::async
diff --git a/pw_async/public/pw_async/dispatcher.h b/pw_async/public/pw_async/dispatcher.h
index 8faaccd67..fb0dfdd13 100644
--- a/pw_async/public/pw_async/dispatcher.h
+++ b/pw_async/public/pw_async/dispatcher.h
@@ -19,39 +19,66 @@ namespace pw::async {
class Task;
-/// Asynchronous Dispatcher abstract class. A default implementation is provided
-/// in pw_async_basic.
+/// Abstract base class for an asynchronous dispatcher loop.
///
-/// Dispatcher implements VirtualSystemClock so the Dispatcher's time can be
-/// injected into other modules under test. This is useful for consistently
-/// simulating time when using FakeDispatcher (rather than using
-/// chrono::SimulatedSystemClock separately).
+/// `Dispatcher`s run many short, non-blocking units of work on a single thread.
+/// This approach has a number of advantages compared with executing concurrent
+/// tasks on separate threads:
+///
+/// - `Dispatcher`s can make more efficient use of system resources, since they
+/// don't need to maintain separate thread stacks.
+/// - `Dispatcher`s can run on systems without thread support, such as no-RTOS
+/// embedded environments.
+/// - `Dispatcher`s allow tasks to communicate with one another without the
+/// synchronization overhead of locks, atomics, fences, or `volatile`.
+///
+/// Thread support: `Dispatcher` methods may be safely invoked from any thread,
+/// but the resulting tasks will always execute on a single thread. Whether
+/// or not methods may be invoked from interrupt context is
+/// implementation-defined.
+///
+/// `VirtualSystemClock`: `Dispatcher` implements `VirtualSystemClock` in order
+/// to provide a consistent source of (possibly mocked) time information to
+/// tasks.
+///
+/// A simple default dispatcher implementation is provided by `pw_async_basic`.
class Dispatcher : public chrono::VirtualSystemClock {
public:
~Dispatcher() override = default;
- /// Post caller owned |task|.
- virtual void Post(Task& task) = 0;
+ /// Post caller-owned |task| to be run on the dispatch loop.
+ ///
+ /// Posted tasks execute in the order they are posted. This ensures that
+ /// tasks can re-post themselves and yield in order to allow other tasks the
+ /// opportunity to execute.
+ ///
+ /// A given |task| must only be posted to a single `Dispatcher`.
+ virtual void Post(Task& task) { PostAt(task, now()); }
/// Post caller owned |task| to be run after |delay|.
- virtual void PostAfter(Task& task, chrono::SystemClock::duration delay) = 0;
+ ///
+ /// If |task| was already posted to run at an earlier time (before |delay|
+ /// would expire), |task| must be run at the earlier time, and |task|
+ /// *may* also be run at the later time.
+ virtual void PostAfter(Task& task, chrono::SystemClock::duration delay) {
+ PostAt(task, now() + delay);
+ }
/// Post caller owned |task| to be run at |time|.
+ ///
+ /// If |task| was already posted to run before |time|,
+ /// |task| must be run at the earlier time, and |task| *may* also be run at
+ /// the later time.
virtual void PostAt(Task& task, chrono::SystemClock::time_point time) = 0;
- /// Post caller owned |task| to be run immediately then rerun at a regular
- /// |interval|.
- virtual void PostPeriodic(Task& task,
- chrono::SystemClock::duration interval) = 0;
- /// Post caller owned |task| to be run at |time| then rerun at a regular
- /// |interval|. |interval| must not be zero.
- virtual void PostPeriodicAt(Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point time) = 0;
-
- /// Returns true if |task| is succesfully canceled.
- /// If cancelation fails, the task may be running or completed.
- /// Periodic tasks may be posted once more after they are canceled.
+ /// Prevent a `Post`ed task from starting.
+ ///
+ /// Returns:
+ /// true: the task was successfully canceled and will not be run by the
+ /// dispatcher until `Post`ed again.
+ /// false: the task could not be cancelled because it either was not
+ /// posted, already ran, or is currently running on the `Dispatcher`
+ /// thread.
virtual bool Cancel(Task& task) = 0;
};
diff --git a/pw_async/public/pw_async/fake_dispatcher.h b/pw_async/public/pw_async/fake_dispatcher.h
index 43931d032..a1b85ae74 100644
--- a/pw_async/public/pw_async/fake_dispatcher.h
+++ b/pw_async/public/pw_async/fake_dispatcher.h
@@ -18,30 +18,39 @@
namespace pw::async::test {
-/// FakeDispatcher is a facade for an implementation of Dispatcher that is used
-/// in unit tests. FakeDispatcher uses simulated time. RunUntil() and RunFor()
-/// advance time immediately, and now() returns the current simulated time.
+/// `FakeDispatcher` is a `Dispatcher` implementation for use in unit tests.
///
-/// To support various Task backends, FakeDispatcher wraps a
-/// backend::NativeFakeDispatcher that implements standard FakeDispatcher
-/// behavior using backend::NativeTask objects.
+/// Threading: `FakeDispatcher` is *NOT* thread-safe and, unlike other
+/// `Dispatcher` implementations. This means that tasks must not be posted from
+/// multiple threads at once, and tasks cannot be posted from other threads
+/// while the dispatcher is executing.
+///
+/// Time: `FakeDispatcher` uses simulated time. `RunUntil()` and `RunFor()`
+/// advance time immediately, and `now()` returns the current simulated time.
+///
+/// To support various `Task` backends, `FakeDispatcher` wraps a
+/// `backend::NativeFakeDispatcher` that implements standard `FakeDispatcher`
+/// behavior using `backend::NativeTask` objects.
class FakeDispatcher final : public Dispatcher {
public:
FakeDispatcher() : native_dispatcher_(*this) {}
/// Execute all runnable tasks and return without advancing simulated time.
- void RunUntilIdle() { native_dispatcher_.RunUntilIdle(); }
+ /// Returns true iff any tasks were invoked during the run.
+ bool RunUntilIdle() { return native_dispatcher_.RunUntilIdle(); }
/// Run the dispatcher until Now() has reached `end_time`, executing all tasks
/// that come due before then.
- void RunUntil(chrono::SystemClock::time_point end_time) {
- native_dispatcher_.RunUntil(end_time);
+ /// Returns true iff any tasks were invoked during the run.
+ bool RunUntil(chrono::SystemClock::time_point end_time) {
+ return native_dispatcher_.RunUntil(end_time);
}
/// Run the Dispatcher until `duration` has elapsed, executing all tasks that
/// come due in that period.
- void RunFor(chrono::SystemClock::duration duration) {
- native_dispatcher_.RunFor(duration);
+ /// Returns true iff any tasks were invoked during the run.
+ bool RunFor(chrono::SystemClock::duration duration) {
+ return native_dispatcher_.RunFor(duration);
}
/// Stop processing tasks. After calling RequestStop(), the next time the
@@ -57,15 +66,6 @@ class FakeDispatcher final : public Dispatcher {
void PostAt(Task& task, chrono::SystemClock::time_point time) override {
native_dispatcher_.PostAt(task, time);
}
- void PostPeriodic(Task& task,
- chrono::SystemClock::duration interval) override {
- native_dispatcher_.PostPeriodic(task, interval);
- }
- void PostPeriodicAt(Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) override {
- native_dispatcher_.PostPeriodicAt(task, interval, start_time);
- }
bool Cancel(Task& task) override { return native_dispatcher_.Cancel(task); }
// VirtualSystemClock overrides:
diff --git a/pw_async/public/pw_async/fake_dispatcher_fixture.h b/pw_async/public/pw_async/fake_dispatcher_fixture.h
index b32efeb92..b8c02c6a6 100644
--- a/pw_async/public/pw_async/fake_dispatcher_fixture.h
+++ b/pw_async/public/pw_async/fake_dispatcher_fixture.h
@@ -28,11 +28,11 @@ namespace pw::async::test {
/// MyClass obj(dispatcher());
///
/// obj.ScheduleSomeTasks();
-/// RunUntilIdle();
+/// EXPECT_TRUE(RunUntilIdle());
/// EXPECT_TRUE(some condition);
///
/// obj.ScheduleTaskToRunIn30Seconds();
-/// RunFor(30s);
+/// EXPECT_TRUE(RunFor(30s));
/// EXPECT_TRUE(task ran);
/// }
/// @endcode
@@ -45,18 +45,21 @@ class FakeDispatcherFixture : public ::testing::Test {
chrono::SystemClock::time_point now() { return dispatcher_.now(); }
/// Dispatches all tasks with due times up until `now()`.
- void RunUntilIdle() { dispatcher_.RunUntilIdle(); }
+ /// Returns true iff any tasks were invoked during the run.
+ bool RunUntilIdle() { return dispatcher_.RunUntilIdle(); }
/// Dispatches all tasks with due times up to `end_time`, progressively
/// advancing the fake clock.
- void RunUntil(chrono::SystemClock::time_point end_time) {
- dispatcher_.RunUntil(end_time);
+ /// Returns true iff any tasks were invoked during the run.
+ bool RunUntil(chrono::SystemClock::time_point end_time) {
+ return dispatcher_.RunUntil(end_time);
}
/// Dispatches all tasks with due times up to `now() + duration`,
/// progressively advancing the fake clock.
- void RunFor(chrono::SystemClock::duration duration) {
- dispatcher_.RunFor(duration);
+ /// Returns true iff any tasks were invoked during the run.
+ bool RunFor(chrono::SystemClock::duration duration) {
+ return dispatcher_.RunFor(duration);
}
private:
diff --git a/pw_async/public/pw_async/function_dispatcher.h b/pw_async/public/pw_async/function_dispatcher.h
new file mode 100644
index 000000000..e3316e10e
--- /dev/null
+++ b/pw_async/public/pw_async/function_dispatcher.h
@@ -0,0 +1,52 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <utility>
+
+#include "pw_async/dispatcher.h"
+#include "pw_async/task_function.h"
+#include "pw_status/status.h"
+
+namespace pw::async {
+
+/// FunctionDispatcher extends Dispatcher with Post*() methods that take a
+/// TaskFunction instead of a Task. This implies that Tasks are allocated or
+/// are taken from a Task pool. Tasks are owned and managed by the Dispatcher.
+class FunctionDispatcher : public Dispatcher {
+ public:
+ ~FunctionDispatcher() override = default;
+
+ // Prevent hiding of overloaded virtual methods.
+ using Dispatcher::Post;
+ using Dispatcher::PostAfter;
+ using Dispatcher::PostAt;
+
+ /// Post dispatcher owned |task_func| function.
+ virtual Status Post(TaskFunction&& task_func) {
+ return PostAt(std::move(task_func), now());
+ }
+
+ /// Post dispatcher owned |task_func| function to be run after |delay|.
+ virtual Status PostAfter(TaskFunction&& task_func,
+ chrono::SystemClock::duration delay) {
+ return PostAt(std::move(task_func), now() + delay);
+ }
+
+ /// Post dispatcher owned |task_func| function to be run at |time|.
+ virtual Status PostAt(TaskFunction&& task_func,
+ chrono::SystemClock::time_point time) = 0;
+};
+
+} // namespace pw::async
diff --git a/pw_async/public/pw_async/heap_dispatcher.h b/pw_async/public/pw_async/heap_dispatcher.h
new file mode 100644
index 000000000..199aa9059
--- /dev/null
+++ b/pw_async/public/pw_async/heap_dispatcher.h
@@ -0,0 +1,48 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_async/function_dispatcher.h"
+
+namespace pw::async {
+
+/// HeapDispatcher wraps an existing Dispatcher and allocates Task objects on
+/// the heap before posting them to the existing Dispatcher. After Tasks run,
+/// they are automatically freed.
+class HeapDispatcher final : public FunctionDispatcher {
+ public:
+ HeapDispatcher(Dispatcher& dispatcher) : dispatcher_(dispatcher) {}
+ ~HeapDispatcher() override = default;
+
+ // FunctionDispatcher overrides:
+ Status PostAt(TaskFunction&& task_func,
+ chrono::SystemClock::time_point time) override;
+
+ // Dispatcher overrides:
+ inline void PostAt(Task& task,
+ chrono::SystemClock::time_point time) override {
+ return dispatcher_.PostAt(task, time);
+ }
+ inline bool Cancel(Task& task) override { return dispatcher_.Cancel(task); }
+
+ // VirtualSystemClock overrides:
+ inline chrono::SystemClock::time_point now() override {
+ return dispatcher_.now();
+ }
+
+ private:
+ Dispatcher& dispatcher_;
+};
+
+} // namespace pw::async
diff --git a/pw_async/public/pw_async/internal/types.h b/pw_async/public/pw_async/internal/types.h
deleted file mode 100644
index c8b7ea287..000000000
--- a/pw_async/public/pw_async/internal/types.h
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2023 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#pragma once
-
-#include "pw_function/function.h"
-#include "pw_status/status.h"
-
-namespace pw::async {
-
-class Dispatcher;
-class Task;
-
-struct Context {
- Dispatcher* dispatcher;
- Task* task;
-};
-
-// A TaskFunction is a unit of work that is wrapped by a Task and executed on a
-// Dispatcher.
-//
-// TaskFunctions take a `Context` as their first argument. Before executing a
-// Task, the Dispatcher sets the pointer to itself and to the Task in `Context`.
-//
-// TaskFunctions take a `Status` as their second argument. When a Task is
-// running as normal, |status| == PW_STATUS_OK. If a Task will not be able to
-// run as scheduled, the Dispatcher will still invoke the TaskFunction with
-// |status| == PW_STATUS_CANCELLED. This provides an opportunity to reclaim
-// resources held by the Task.
-//
-// A Task will not run as scheduled if, for example, it is still waiting when
-// the Dispatcher shuts down.
-using TaskFunction = Function<void(Context&, Status)>;
-
-} // namespace pw::async
diff --git a/pw_async/public/pw_async/task.h b/pw_async/public/pw_async/task.h
index c1fa8e665..82618f5cf 100644
--- a/pw_async/public/pw_async/task.h
+++ b/pw_async/public/pw_async/task.h
@@ -15,7 +15,8 @@
#include <optional>
-#include "pw_async/internal/types.h"
+#include "pw_async/context.h"
+#include "pw_async/task_function.h"
#include "pw_async_backend/task.h"
namespace pw::async {
@@ -24,16 +25,16 @@ namespace test {
class FakeDispatcher;
}
-/// A Task represents a unit of work (TaskFunction) that can be executed on a
-/// Dispatcher. To support various Dispatcher backends, it wraps a
+/// A `Task` represents a unit of work (`TaskFunction`) that can be executed on
+/// a `Dispatcher`. To support various `Dispatcher` backends, it wraps a
/// `backend::NativeTask`, which contains backend-specific state and methods.
class Task final {
public:
- /// The default constructor creates a Task without a function.
- /// `set_function()` must be called before posting the Task.
+ /// The default constructor creates a `Task` without a function.
+ /// `set_function()` must be called before posting the `Task`.
Task() : native_type_(*this) {}
- /// Constructs a Task that calls `f` when executed on a Dispatcher.
+ /// Constructs a Task that calls `f` when executed on a `Dispatcher`.
explicit Task(TaskFunction&& f) : native_type_(*this, std::move(f)) {}
Task(const Task&) = delete;
@@ -41,8 +42,8 @@ class Task final {
Task(Task&&) = delete;
Task& operator=(Task&&) = delete;
- /// Configure the TaskFunction after construction. This MUST NOT be called
- /// while this Task is pending in a Dispatcher.
+ /// Configure the `TaskFunction` after construction. This MUST NOT be called
+ /// while this `Task` is pending in a `Dispatcher`.
void set_function(TaskFunction&& f) {
native_type_.set_function(std::move(f));
}
@@ -50,8 +51,8 @@ class Task final {
/// Executes this task.
void operator()(Context& ctx, Status status) { native_type_(ctx, status); }
- /// Returns the inner NativeTask containing backend-specific state. Only
- /// Dispatcher backends or non-portable code should call these methods!
+ /// Returns the inner `NativeTask` containing backend-specific state. Only
+ /// `Dispatcher` backends or non-portable code should call these methods!
backend::NativeTask& native_type() { return native_type_; }
const backend::NativeTask& native_type() const { return native_type_; }
diff --git a/pw_async/public/pw_async/task_function.h b/pw_async/public/pw_async/task_function.h
new file mode 100644
index 000000000..3c4f4706e
--- /dev/null
+++ b/pw_async/public/pw_async/task_function.h
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_async/context.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::async {
+
+/// A `TaskFunction` is a unit of work that is wrapped by a Task and executed on
+/// a `Dispatcher`.
+///
+/// `TaskFunction`s take a `Context` as their first argument. Before executing a
+/// `Task`, the `Dispatcher` sets the pointer to itself and to the `Task` in
+/// `Context`.
+///
+/// `TaskFunction`s take a `Status` as their second argument. When a Task is
+/// running as normal, |status| is `PW_STATUS_OK`. If a `Task` will not be able
+/// to run as scheduled, the `Dispatcher` will still invoke the `TaskFunction`
+/// with |status| `PW_STATUS_CANCELLED`. This provides an opportunity to reclaim
+/// resources held by the Task.
+///
+/// A `Task` will not run as scheduled if, for example, it is still waiting when
+/// the `Dispatcher` shuts down.
+using TaskFunction = Function<void(Context&, Status)>;
+
+} // namespace pw::async
diff --git a/pw_async_basic/BUILD.bazel b/pw_async_basic/BUILD.bazel
index 8a1fb3aca..b490bc186 100644
--- a/pw_async_basic/BUILD.bazel
+++ b/pw_async_basic/BUILD.bazel
@@ -12,19 +12,88 @@
# License for the specific language governing permissions and limitations under
# the License.
-filegroup(
- name = "pw_async_files",
- srcs = [
- "dispatcher.cc",
- "dispatcher_test.cc",
- "fake_dispatcher.cc",
- "fake_dispatcher_fixture_test.cc",
- "public/pw_async_basic/dispatcher.h",
- "public/pw_async_basic/fake_dispatcher.h",
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Backend for //pw_async:task
+pw_cc_library(
+ name = "task",
+ hdrs = [
"public/pw_async_basic/task.h",
- "public_overrides/pw_async_backend/fake_dispatcher.h",
"public_overrides/pw_async_backend/task.h",
- "size_report/post_1_task.cc",
- "size_report/task.cc",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ deps = [
+ "//pw_async:task_facade",
+ "//pw_containers:intrusive_list",
+ ],
+)
+
+# Backend for //pw_async:fake_dispatcher
+pw_cc_library(
+ name = "fake_dispatcher",
+ srcs = ["fake_dispatcher.cc"],
+ hdrs = [
+ "public/pw_async_basic/fake_dispatcher.h",
+ "public_overrides/pw_async_backend/fake_dispatcher.h",
+ ],
+ includes = [
+ "public",
+ "public_override",
+ ],
+ deps = [
+ "//pw_async:fake_dispatcher_facade",
+ "//pw_async:task",
+ "//pw_log",
+ ],
+)
+
+pw_cc_test(
+ name = "fake_dispatcher_fixture_test",
+ srcs = ["fake_dispatcher_fixture_test.cc"],
+ deps = ["//pw_async:fake_dispatcher_fixture"],
+)
+
+pw_cc_library(
+ name = "dispatcher",
+ srcs = ["dispatcher.cc"],
+ hdrs = ["public/pw_async_basic/dispatcher.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_async:dispatcher",
+ "//pw_async:task",
+ "//pw_containers:intrusive_list",
+ "//pw_sync:interrupt_spin_lock",
+ "//pw_sync:timed_thread_notification",
+ "//pw_thread:thread_core",
+ ],
+)
+
+pw_cc_test(
+ name = "dispatcher_test",
+ srcs = ["dispatcher_test.cc"],
+ deps = [
+ ":dispatcher",
+ "//pw_log",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_test(
+ name = "heap_dispatcher_test",
+ srcs = ["heap_dispatcher_test.cc"],
+ deps = [
+ "//pw_async:fake_dispatcher_fixture",
+ "//pw_async:heap_dispatcher",
],
)
diff --git a/pw_async_basic/BUILD.gn b/pw_async_basic/BUILD.gn
index 9ccce5e85..e4fca69fb 100644
--- a/pw_async_basic/BUILD.gn
+++ b/pw_async_basic/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_async/async.gni")
import("$dir_pw_async/fake_dispatcher_fixture.gni")
import("$dir_pw_async/fake_dispatcher_test.gni")
+import("$dir_pw_async/heap_dispatcher.gni")
import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
@@ -103,7 +104,6 @@ pw_source_set("dispatcher") {
"$dir_pw_sync:interrupt_spin_lock",
"$dir_pw_sync:timed_thread_notification",
"$dir_pw_thread:thread_core",
- dir_pw_log,
]
visibility = [
":*",
@@ -122,11 +122,34 @@ pw_test("dispatcher_test") {
sources = [ "dispatcher_test.cc" ]
}
+# This target cannot be labeled "heap_dispatcher" or else the outpath Ninja uses
+# for heap_dispatcher.cc will collide with $dir_pw_async:heap_dispatcher.
+pw_async_heap_dispatcher_source_set("heap_dispatcher_basic") {
+ task_backend = ":task"
+ visibility = [ ":*" ]
+}
+
+group("heap_dispatcher") {
+ public_deps = [ ":heap_dispatcher_basic" ]
+ visibility = [ ":*" ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+pw_test("heap_dispatcher_test") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ sources = [ "heap_dispatcher_test.cc" ]
+ deps = [
+ ":fake_dispatcher_fixture",
+ ":heap_dispatcher",
+ ]
+}
+
pw_test_group("tests") {
tests = [
":dispatcher_test",
":fake_dispatcher_test",
":fake_dispatcher_fixture_test",
+ ":heap_dispatcher_test",
]
}
diff --git a/pw_async_basic/CMakeLists.txt b/pw_async_basic/CMakeLists.txt
new file mode 100644
index 000000000..80d83fa8b
--- /dev/null
+++ b/pw_async_basic/CMakeLists.txt
@@ -0,0 +1,43 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_async_basic.task_backend INTERFACE
+ HEADERS
+ public/pw_async_basic/task.h
+ public_overrides/pw_async_backend/task.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_async.task.facade
+ pw_containers.intrusive_list
+)
+
+pw_add_library(pw_async_basic.dispatcher_backend STATIC
+ HEADERS
+ public/pw_async_basic/dispatcher.h
+ SOURCES
+ dispatcher.cc
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_async_basic.task_backend
+ pw_async.dispatcher.facade
+ pw_containers.intrusive_list
+ pw_sync.interrupt_spin_lock
+ pw_sync.timed_thread_notification
+ pw_thread.thread_core
+)
diff --git a/pw_async_basic/dispatcher.cc b/pw_async_basic/dispatcher.cc
index 682ff4f6d..4b51956a1 100644
--- a/pw_async_basic/dispatcher.cc
+++ b/pw_async_basic/dispatcher.cc
@@ -13,16 +13,14 @@
// the License.
#include "pw_async_basic/dispatcher.h"
-#include "pw_assert/check.h"
+#include <mutex>
+
#include "pw_chrono/system_clock.h"
-#include "pw_log/log.h"
using namespace std::chrono_literals;
namespace pw::async {
-const chrono::SystemClock::duration SLEEP_DURATION = 5s;
-
BasicDispatcher::~BasicDispatcher() {
RequestStop();
lock_.lock();
@@ -70,13 +68,16 @@ void BasicDispatcher::MaybeSleep() {
// Sleep until a notification is received or until the due time of the
// next task. Notifications are sent when tasks are posted or 'stop' is
// requested.
- chrono::SystemClock::time_point wake_time =
- task_queue_.empty() ? now() + SLEEP_DURATION
- : task_queue_.front().due_time_;
-
+ std::optional<chrono::SystemClock::time_point> wake_time = std::nullopt;
+ if (!task_queue_.empty()) {
+ wake_time = task_queue_.front().due_time_;
+ }
lock_.unlock();
- PW_LOG_DEBUG("no task due; waiting for signal");
- timed_notification_.try_acquire_until(wake_time);
+ if (wake_time.has_value()) {
+ timed_notification_.try_acquire_until(*wake_time);
+ } else {
+ timed_notification_.acquire();
+ }
lock_.lock();
}
}
@@ -87,12 +88,7 @@ void BasicDispatcher::ExecuteDueTasks() {
backend::NativeTask& task = task_queue_.front();
task_queue_.pop_front();
- if (task.interval().has_value()) {
- PostTaskInternal(task, task.due_time_ + task.interval().value());
- }
-
lock_.unlock();
- PW_LOG_DEBUG("running task");
Context ctx{this, &task.task_};
task(ctx, OkStatus());
lock_.lock();
@@ -100,52 +96,27 @@ void BasicDispatcher::ExecuteDueTasks() {
}
void BasicDispatcher::RequestStop() {
- std::lock_guard lock(lock_);
- PW_LOG_DEBUG("stop requested");
- stop_requested_ = true;
+ {
+ std::lock_guard lock(lock_);
+ stop_requested_ = true;
+ }
timed_notification_.release();
}
void BasicDispatcher::DrainTaskQueue() {
- PW_LOG_DEBUG("draining task queue");
while (!task_queue_.empty()) {
backend::NativeTask& task = task_queue_.front();
task_queue_.pop_front();
lock_.unlock();
- PW_LOG_DEBUG("running cancelled task");
Context ctx{this, &task.task_};
task(ctx, Status::Cancelled());
lock_.lock();
}
}
-void BasicDispatcher::Post(Task& task) { PostAt(task, now()); }
-
-void BasicDispatcher::PostAfter(Task& task,
- chrono::SystemClock::duration delay) {
- PostAt(task, now() + delay);
-}
-
void BasicDispatcher::PostAt(Task& task, chrono::SystemClock::time_point time) {
- lock_.lock();
- PW_LOG_DEBUG("posting task");
PostTaskInternal(task.native_type(), time);
- lock_.unlock();
-}
-
-void BasicDispatcher::PostPeriodic(Task& task,
- chrono::SystemClock::duration interval) {
- PostPeriodicAt(task, interval, now());
-}
-
-void BasicDispatcher::PostPeriodicAt(
- Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) {
- PW_DCHECK(interval != chrono::SystemClock::duration::zero());
- task.native_type().set_interval(interval);
- PostAt(task, start_time);
}
bool BasicDispatcher::Cancel(Task& task) {
@@ -155,6 +126,7 @@ bool BasicDispatcher::Cancel(Task& task) {
void BasicDispatcher::PostTaskInternal(
backend::NativeTask& task, chrono::SystemClock::time_point time_due) {
+ lock_.lock();
task.due_time_ = time_due;
auto it_front = task_queue_.begin();
auto it_behind = task_queue_.before_begin();
@@ -163,6 +135,7 @@ void BasicDispatcher::PostTaskInternal(
++it_behind;
}
task_queue_.insert_after(it_behind, task);
+ lock_.unlock();
timed_notification_.release();
}
diff --git a/pw_async_basic/dispatcher_test.cc b/pw_async_basic/dispatcher_test.cc
index 3f72bbb4f..bd4108e5e 100644
--- a/pw_async_basic/dispatcher_test.cc
+++ b/pw_async_basic/dispatcher_test.cc
@@ -14,6 +14,7 @@
#include "pw_async_basic/dispatcher.h"
#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
#include "pw_log/log.h"
#include "pw_sync/thread_notification.h"
#include "pw_thread/thread.h"
diff --git a/pw_async_basic/docs.rst b/pw_async_basic/docs.rst
index 0363a2683..76924074a 100644
--- a/pw_async_basic/docs.rst
+++ b/pw_async_basic/docs.rst
@@ -20,20 +20,20 @@ First, set the following GN variables:
.. code-block::
- pw_async_TASK_BACKEND="$dir_pw_async_basic:task"
- pw_async_FAKE_DISPATCHER_BACKEND="$dir_pw_async_basic:fake_dispatcher"
+ pw_async_TASK_BACKEND="$dir_pw_async_basic:task"
+ pw_async_FAKE_DISPATCHER_BACKEND="$dir_pw_async_basic:fake_dispatcher"
Next, create a target that depends on ``//pw_async_basic:dispatcher``:
.. code-block::
- pw_executable("hello_world") {
- sources = [ "hello_world.cc" ]
- deps = [
- "//pw_async_basic:dispatcher",
- ]
- }
+ pw_executable("hello_world") {
+ sources = [ "hello_world.cc" ]
+ deps = [
+ "//pw_async_basic:dispatcher",
+ ]
+ }
Next, construct and use a ``BasicDispatcher``.
diff --git a/pw_async_basic/fake_dispatcher.cc b/pw_async_basic/fake_dispatcher.cc
index cbba89e98..6e649faee 100644
--- a/pw_async_basic/fake_dispatcher.cc
+++ b/pw_async_basic/fake_dispatcher.cc
@@ -29,47 +29,50 @@ NativeFakeDispatcher::~NativeFakeDispatcher() {
DrainTaskQueue();
}
-void NativeFakeDispatcher::RunUntilIdle() {
- ExecuteDueTasks();
+bool NativeFakeDispatcher::RunUntilIdle() {
+ bool tasks_ran = ExecuteDueTasks();
if (stop_requested_) {
- DrainTaskQueue();
+ tasks_ran |= DrainTaskQueue();
}
+ return tasks_ran;
}
-void NativeFakeDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
+bool NativeFakeDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
+ bool tasks_ran = false;
while (!task_queue_.empty() && task_queue_.front().due_time() <= end_time &&
!stop_requested_) {
now_ = task_queue_.front().due_time();
- ExecuteDueTasks();
+ tasks_ran |= ExecuteDueTasks();
}
if (stop_requested_) {
- DrainTaskQueue();
- return;
+ tasks_ran |= DrainTaskQueue();
+ return tasks_ran;
}
if (now_ < end_time) {
now_ = end_time;
}
+ return tasks_ran;
}
-void NativeFakeDispatcher::RunFor(chrono::SystemClock::duration duration) {
- RunUntil(now() + duration);
+bool NativeFakeDispatcher::RunFor(chrono::SystemClock::duration duration) {
+ return RunUntil(now() + duration);
}
-void NativeFakeDispatcher::ExecuteDueTasks() {
+bool NativeFakeDispatcher::ExecuteDueTasks() {
+ bool task_ran = false;
while (!task_queue_.empty() && task_queue_.front().due_time() <= now() &&
!stop_requested_) {
::pw::async::backend::NativeTask& task = task_queue_.front();
task_queue_.pop_front();
- if (task.interval().has_value()) {
- PostTaskInternal(task, task.due_time() + task.interval().value());
- }
-
Context ctx{&dispatcher_, &task.task_};
task(ctx, OkStatus());
+
+ task_ran = true;
}
+ return task_ran;
}
void NativeFakeDispatcher::RequestStop() {
@@ -77,7 +80,8 @@ void NativeFakeDispatcher::RequestStop() {
stop_requested_ = true;
}
-void NativeFakeDispatcher::DrainTaskQueue() {
+bool NativeFakeDispatcher::DrainTaskQueue() {
+ bool task_ran = false;
while (!task_queue_.empty()) {
::pw::async::backend::NativeTask& task = task_queue_.front();
task_queue_.pop_front();
@@ -85,7 +89,10 @@ void NativeFakeDispatcher::DrainTaskQueue() {
PW_LOG_DEBUG("running cancelled task");
Context ctx{&dispatcher_, &task.task_};
task(ctx, Status::Cancelled());
+
+ task_ran = true;
}
+ return task_ran;
}
void NativeFakeDispatcher::Post(Task& task) { PostAt(task, now()); }
@@ -101,19 +108,6 @@ void NativeFakeDispatcher::PostAt(Task& task,
PostTaskInternal(task.native_type(), time);
}
-void NativeFakeDispatcher::PostPeriodic(
- Task& task, chrono::SystemClock::duration interval) {
- PostPeriodicAt(task, interval, now());
-}
-
-void NativeFakeDispatcher::PostPeriodicAt(
- Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) {
- task.native_type().set_interval(interval);
- PostAt(task, start_time);
-}
-
bool NativeFakeDispatcher::Cancel(Task& task) {
return task_queue_.remove(task.native_type());
}
@@ -121,10 +115,19 @@ bool NativeFakeDispatcher::Cancel(Task& task) {
void NativeFakeDispatcher::PostTaskInternal(
::pw::async::backend::NativeTask& task,
chrono::SystemClock::time_point time_due) {
+ if (!task.unlisted()) {
+ if (task.due_time() <= time_due) {
+ // No need to repost a task that was already queued to run.
+ return;
+ }
+ // The task needs its time updated, so we have to move it to
+ // a different part of the list.
+ task.unlist();
+ }
task.set_due_time(time_due);
auto it_front = task_queue_.begin();
auto it_behind = task_queue_.before_begin();
- while (it_front != task_queue_.end() && time_due > it_front->due_time()) {
+ while (it_front != task_queue_.end() && time_due >= it_front->due_time()) {
++it_front;
++it_behind;
}
diff --git a/pw_async_basic/fake_dispatcher_fixture_test.cc b/pw_async_basic/fake_dispatcher_fixture_test.cc
index 5262c3438..dfdf9b393 100644
--- a/pw_async_basic/fake_dispatcher_fixture_test.cc
+++ b/pw_async_basic/fake_dispatcher_fixture_test.cc
@@ -13,6 +13,8 @@
// the License.
#include "pw_async/fake_dispatcher_fixture.h"
+#include <chrono>
+
#include "gtest/gtest.h"
namespace pw::async {
@@ -20,7 +22,7 @@ namespace {
using FakeDispatcherFixture = test::FakeDispatcherFixture;
-TEST_F(FakeDispatcherFixture, PostTasks) {
+TEST_F(FakeDispatcherFixture, PostTasksAndStop) {
int count = 0;
auto inc_count = [&count](Context& /*c*/, Status /*status*/) { ++count; };
@@ -28,8 +30,25 @@ TEST_F(FakeDispatcherFixture, PostTasks) {
dispatcher().Post(task);
ASSERT_EQ(count, 0);
- RunUntilIdle();
+ EXPECT_TRUE(RunUntilIdle());
ASSERT_EQ(count, 1);
+ EXPECT_FALSE(RunUntilIdle());
+
+ dispatcher().Post(task);
+ EXPECT_TRUE(RunUntil(dispatcher().now()));
+ ASSERT_EQ(count, 2);
+ EXPECT_FALSE(RunUntilIdle());
+
+ dispatcher().Post(task);
+ EXPECT_TRUE(RunFor(std::chrono::seconds(1)));
+ ASSERT_EQ(count, 3);
+ EXPECT_FALSE(RunUntilIdle());
+
+ dispatcher().PostAfter(task, std::chrono::minutes(1));
+ dispatcher().RequestStop();
+ ASSERT_EQ(count, 3);
+ EXPECT_TRUE(RunUntilIdle());
+ ASSERT_EQ(count, 4);
}
} // namespace
diff --git a/pw_async_basic/heap_dispatcher_test.cc b/pw_async_basic/heap_dispatcher_test.cc
new file mode 100644
index 000000000..3c744971d
--- /dev/null
+++ b/pw_async_basic/heap_dispatcher_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_async/heap_dispatcher.h"
+
+#include "gtest/gtest.h"
+#include "pw_async/fake_dispatcher_fixture.h"
+
+using namespace std::chrono_literals;
+
+namespace pw::async {
+namespace {
+
+using HeapDispatcherTest = test::FakeDispatcherFixture;
+
+class DestructionChecker {
+ public:
+ DestructionChecker(bool* flag) : flag_(flag) {}
+ DestructionChecker(DestructionChecker&& other) {
+ flag_ = other.flag_;
+ other.flag_ = nullptr;
+ }
+ ~DestructionChecker() {
+ if (flag_) {
+ *flag_ = true;
+ }
+ }
+
+ private:
+ bool* flag_;
+};
+
+TEST_F(HeapDispatcherTest, RunUntilIdleRunsPostedTask) {
+ HeapDispatcher heap_dispatcher(dispatcher());
+
+ int count = 0;
+ Status status = heap_dispatcher.Post(
+ [&count](Context& /*ctx*/, Status /*status*/) { ++count; });
+ EXPECT_TRUE(status.ok());
+ ASSERT_EQ(count, 0);
+ RunUntilIdle();
+ ASSERT_EQ(count, 1);
+}
+
+TEST_F(HeapDispatcherTest, TaskFunctionIsDestroyedAfterBeingCalled) {
+ HeapDispatcher heap_dispatcher(dispatcher());
+
+ // Test that the lambda is destroyed after being called.
+ bool flag = false;
+ Status status =
+ heap_dispatcher.Post([checker = DestructionChecker(&flag)](
+ Context& /*ctx*/, Status /*status*/) {});
+ EXPECT_TRUE(status.ok());
+ EXPECT_FALSE(flag);
+ RunUntilIdle();
+ EXPECT_TRUE(flag);
+}
+
+} // namespace
+} // namespace pw::async
diff --git a/pw_async_basic/public/pw_async_basic/dispatcher.h b/pw_async_basic/public/pw_async_basic/dispatcher.h
index 7050d6622..eb712ed63 100644
--- a/pw_async_basic/public/pw_async_basic/dispatcher.h
+++ b/pw_async_basic/public/pw_async_basic/dispatcher.h
@@ -53,14 +53,7 @@ class BasicDispatcher final : public Dispatcher, public thread::ThreadCore {
void Run() override PW_LOCKS_EXCLUDED(lock_);
// Dispatcher overrides:
- void Post(Task& task) override;
- void PostAfter(Task& task, chrono::SystemClock::duration delay) override;
void PostAt(Task& task, chrono::SystemClock::time_point time) override;
- void PostPeriodic(Task& task,
- chrono::SystemClock::duration interval) override;
- void PostPeriodicAt(Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time) override;
bool Cancel(Task& task) override PW_LOCKS_EXCLUDED(lock_);
// VirtualSystemClock overrides:
@@ -73,7 +66,7 @@ class BasicDispatcher final : public Dispatcher, public thread::ThreadCore {
// |time_due|.
void PostTaskInternal(backend::NativeTask& task,
chrono::SystemClock::time_point time_due)
- PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+ PW_LOCKS_EXCLUDED(lock_);
// If no tasks are due, sleep until a notification is received, the next task
// comes due, or a timeout elapses; whichever occurs first.
diff --git a/pw_async_basic/public/pw_async_basic/fake_dispatcher.h b/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
index 96fd7aae8..43d35038a 100644
--- a/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
+++ b/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
@@ -32,18 +32,13 @@ class NativeFakeDispatcher final {
void PostAt(Task& task, chrono::SystemClock::time_point time);
- void PostPeriodic(Task& task, chrono::SystemClock::duration interval);
- void PostPeriodicAt(Task& task,
- chrono::SystemClock::duration interval,
- chrono::SystemClock::time_point start_time);
-
bool Cancel(Task& task);
- void RunUntilIdle();
+ bool RunUntilIdle();
- void RunUntil(chrono::SystemClock::time_point end_time);
+ bool RunUntil(chrono::SystemClock::time_point end_time);
- void RunFor(chrono::SystemClock::duration duration);
+ bool RunFor(chrono::SystemClock::duration duration);
chrono::SystemClock::time_point now() { return now_; }
@@ -54,11 +49,13 @@ class NativeFakeDispatcher final {
chrono::SystemClock::time_point time_due);
// Dequeue and run each task that is due.
- void ExecuteDueTasks();
+ // Returns true iff any tasks were invoked during the run.
+ bool ExecuteDueTasks();
// Dequeue each task and run each TaskFunction with a PW_STATUS_CANCELLED
// status.
- void DrainTaskQueue();
+ // Returns true iff any tasks were invoked during the run.
+ bool DrainTaskQueue();
Dispatcher& dispatcher_;
bool stop_requested_ = false;
diff --git a/pw_async_basic/public/pw_async_basic/task.h b/pw_async_basic/public/pw_async_basic/task.h
index d83ea34ab..e2be508c6 100644
--- a/pw_async_basic/public/pw_async_basic/task.h
+++ b/pw_async_basic/public/pw_async_basic/task.h
@@ -13,7 +13,8 @@
// the License.
#pragma once
-#include "pw_async/internal/types.h"
+#include "pw_async/context.h"
+#include "pw_async/task_function.h"
#include "pw_chrono/system_clock.h"
#include "pw_containers/intrusive_list.h"
@@ -43,19 +44,6 @@ class NativeTask final : public IntrusiveList<NativeTask>::Item {
void set_due_time(chrono::SystemClock::time_point due_time) {
due_time_ = due_time;
}
- std::optional<chrono::SystemClock::duration> interval() const {
- if (interval_ == chrono::SystemClock::duration::zero()) {
- return std::nullopt;
- }
- return interval_;
- }
- void set_interval(std::optional<chrono::SystemClock::duration> interval) {
- if (!interval.has_value()) {
- interval_ = chrono::SystemClock::duration::zero();
- return;
- }
- interval_ = *interval;
- }
TaskFunction func_ = nullptr;
// task_ is placed after func_ to take advantage of the padding that would
@@ -64,9 +52,6 @@ class NativeTask final : public IntrusiveList<NativeTask>::Item {
// padding would be added here, which is just enough for a pointer.
Task& task_;
pw::chrono::SystemClock::time_point due_time_;
- // A duration of 0 indicates that the task is not periodic.
- chrono::SystemClock::duration interval_ =
- chrono::SystemClock::duration::zero();
};
using NativeTaskHandle = NativeTask&;
diff --git a/pw_async_basic/size_report/BUILD.bazel b/pw_async_basic/size_report/BUILD.bazel
new file mode 100644
index 000000000..33c90988a
--- /dev/null
+++ b/pw_async_basic/size_report/BUILD.bazel
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+)
+
+licenses(["notice"])
+
+pw_cc_binary(
+ name = "post_1_task",
+ srcs = ["post_1_task.cc"],
+ deps = [
+ "//pw_async_basic:dispatcher",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
+
+pw_cc_binary(
+ name = "task",
+ srcs = ["task.cc"],
+ deps = [
+ "//pw_async:task",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
diff --git a/pw_async_basic/size_report/post_1_task.cc b/pw_async_basic/size_report/post_1_task.cc
index ef6065b1c..c074cf184 100644
--- a/pw_async_basic/size_report/post_1_task.cc
+++ b/pw_async_basic/size_report/post_1_task.cc
@@ -19,8 +19,9 @@ int main() {
pw::bloat::BloatThisBinary();
pw::async::BasicDispatcher dispatcher;
- pw::async::Task task(
- [](pw::async::Context& /*ctx*/) { printf("hello world\n"); });
+ pw::async::Task task([](pw::async::Context& /*ctx*/, pw::Status /*status*/) {
+ printf("hello world\n");
+ });
dispatcher.Post(task);
dispatcher.Run();
return 0;
diff --git a/pw_async_basic/size_report/task.cc b/pw_async_basic/size_report/task.cc
index d149346fe..d35c766e0 100644
--- a/pw_async_basic/size_report/task.cc
+++ b/pw_async_basic/size_report/task.cc
@@ -18,6 +18,7 @@
int main() {
pw::bloat::BloatThisBinary();
- pw::async::Task task([](pw::async::Context& /*ctx*/) {});
+ pw::async::Task task(
+ [](pw::async::Context& /*ctx*/, pw::Status /*status*/) {});
return 0;
}
diff --git a/pw_base64/Android.bp b/pw_base64/Android.bp
new file mode 100644
index 000000000..32d26a518
--- /dev/null
+++ b/pw_base64/Android.bp
@@ -0,0 +1,47 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_base64",
+ cpp_std: "c++20",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
+ defaults: [
+ "pw_assert_log_defaults",
+ ],
+ export_header_lib_headers: [
+ "pw_span_headers",
+ ],
+ host_supported: true,
+ srcs: [
+ "base64.cc",
+ ],
+ static_libs: [
+ "pw_string",
+ ],
+ export_static_lib_headers: [
+ "pw_string",
+ ],
+}
diff --git a/pw_base64/BUILD.gn b/pw_base64/BUILD.gn
index 6fa100756..2dea5e753 100644
--- a/pw_base64/BUILD.gn
+++ b/pw_base64/BUILD.gn
@@ -30,6 +30,9 @@ pw_source_set("pw_base64") {
dir_pw_span,
]
sources = [ "base64.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test_group("tests") {
diff --git a/pw_base64/Kconfig b/pw_base64/Kconfig
new file mode 100644
index 000000000..be59a6a08
--- /dev/null
+++ b/pw_base64/Kconfig
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Do not rely on the PW_ROOT environment variable being set through bootstrap.
+# Regardless of whether it's set or not the following include will ensure it is.
+
+menu "pw_base64"
+
+config PIGWEED_BASE64
+ bool "Link pw_base64 library"
+ help
+ See :ref:`module-pw_base64` for module details.
+
+endmenu
diff --git a/pw_base64/base64.cc b/pw_base64/base64.cc
index 97f9d1ec4..862aebbf4 100644
--- a/pw_base64/base64.cc
+++ b/pw_base64/base64.cc
@@ -74,13 +74,14 @@ constexpr uint8_t CharToBits(char ch) {
}
constexpr uint8_t Byte0(uint8_t bits0, uint8_t bits1) {
- return (bits0 << 2) | ((bits1 & 0b110000) >> 4);
+ return static_cast<uint8_t>(bits0 << 2) | ((bits1 & 0b110000) >> 4);
}
constexpr uint8_t Byte1(uint8_t bits1, uint8_t bits2) {
- return ((bits1 & 0b001111) << 4) | ((bits2 & 0b111100) >> 2);
+ return static_cast<uint8_t>((bits1 & 0b001111) << 4) |
+ ((bits2 & 0b111100) >> 2);
}
constexpr uint8_t Byte2(uint8_t bits2, uint8_t bits3) {
- return ((bits2 & 0b000011) << 6) | bits3;
+ return static_cast<uint8_t>((bits2 & 0b000011) << 6) | bits3;
}
} // namespace
@@ -141,7 +142,12 @@ extern "C" size_t pw_Base64Decode(const char* base64,
pad = 1;
}
- return binary - static_cast<uint8_t*>(output) - pad;
+ return static_cast<size_t>(binary - static_cast<uint8_t*>(output)) - pad;
+}
+
+extern "C" bool pw_Base64IsValidChar(char base64_char) {
+ return !(base64_char < kMinValidChar || base64_char > kMaxValidChar ||
+ CharToBits(base64_char) == kX /* invalid char */);
}
extern "C" bool pw_Base64IsValid(const char* base64_data, size_t base64_size) {
@@ -150,8 +156,7 @@ extern "C" bool pw_Base64IsValid(const char* base64_data, size_t base64_size) {
}
for (size_t i = 0; i < base64_size; ++i) {
- if (base64_data[i] < kMinValidChar || base64_data[i] > kMaxValidChar ||
- CharToBits(base64_data[i]) == kX /* invalid char */) {
+ if (!pw_Base64IsValidChar(base64_data[i])) {
return false;
}
}
diff --git a/pw_base64/docs.rst b/pw_base64/docs.rst
index 901334118..b86269c68 100644
--- a/pw_base64/docs.rst
+++ b/pw_base64/docs.rst
@@ -1,11 +1,14 @@
.. _module-pw_base64:
----------
+=========
pw_base64
----------
+=========
This module provides functions for encoding, decoding, and validating Base64
data as specified by `RFC 3548 <https://tools.ietf.org/html/rfc3548>`_ and
`RFC 4648 <https://tools.ietf.org/html/rfc4648>`_.
-.. note::
- The documentation for this module is currently incomplete.
+-------------
+API reference
+-------------
+.. doxygennamespace:: pw::base64
+ :members:
diff --git a/pw_base64/public/pw_base64/base64.h b/pw_base64/public/pw_base64/base64.h
index a0dcc82cb..dc5be2d3a 100644
--- a/pw_base64/public/pw_base64/base64.h
+++ b/pw_base64/public/pw_base64/base64.h
@@ -53,6 +53,9 @@ size_t pw_Base64Decode(const char* base64,
size_t base64_size_bytes,
void* output);
+// Returns true if provided char is a valid Base64 character.
+bool pw_Base64IsValidChar(char base64_char);
+
// Returns true if the provided string is valid Base64 encoded data. Accepts
// either the standard (+/) or URL-safe (-_) alphabets.
//
@@ -71,36 +74,63 @@ bool pw_Base64IsValid(const char* base64_data, size_t base64_size);
namespace pw::base64 {
-// Returns the size of the given number of bytes when encoded as Base64. Base64
-// encodes 3-byte groups into 4-character strings. The final group is padded to
-// be 3-bytes if it only has 1 or 2.
+/// @param[in] binary_size_bytes The size of the binary data in bytes, before
+/// encoding.
+///
+/// @returns The size of `binary_size_bytes` after Base64 encoding.
+///
+/// @note Base64 encodes 3-byte groups into 4-character strings. The final group
+/// is padded to be 3 bytes if it only has 1 or 2.
constexpr size_t EncodedSize(size_t binary_size_bytes) {
return PW_BASE64_ENCODED_SIZE(binary_size_bytes);
}
-// Encodes the provided data in Base64 and writes the result to the buffer.
-// Encodes to the standard alphabet with + and / for characters 62 and 63.
-// Exactly EncodedSize(binary_size_bytes) bytes will be written. The
-// output buffer *MUST* be large enough for the encoded output! The input and
-// output buffers MUST NOT be the same; encoding cannot occur in place.
-//
-// The resulting string in the output is NOT null-terminated!
+/// Encodes the provided data in Base64 and writes the result to the buffer.
+///
+/// @param[in] binary The binary data to encode.
+///
+/// @param[out] The output buffer where the encoded data is placed. Exactly
+/// `EncodedSize(binary_size_bytes)` bytes is written.
+///
+/// @note Encodes to the standard alphabet with `+` and `/` for characters `62`
+/// and `63`.
+///
+/// @pre
+/// * The output buffer **MUST** be large enough for the encoded output!
+/// * The input and output buffers **MUST NOT** be the same; encoding cannot
+/// occur in place.
+///
+/// @warning The resulting string in the output is **NOT** null-terminated!
inline void Encode(span<const std::byte> binary, char* output) {
pw_Base64Encode(binary.data(), binary.size_bytes(), output);
}
-// Encodes the provided data in Base64 if the result fits in the provided
-// buffer. Returns the number of bytes written, which will be 0 if the output
-// buffer is too small.
+/// Encodes the provided data in Base64 if the result fits in the provided
+/// buffer.
+///
+/// @param[in] binary The binary data to encode.
+///
+/// @param[out] output_buffer The output buffer where the encoded data is
+/// placed.
+///
+/// @warning The resulting string in the output is **NOT** null-terminated!
+///
+/// @returns The number of bytes written. Returns `0` if the output buffer
+/// is too small.
size_t Encode(span<const std::byte> binary, span<char> output_buffer);
-// Appends Base64 encoded binary data to the provided pw::InlineString. If the
-// data does not fit in the string, an assertion fails.
+/// Appends Base64 encoded binary data to the provided `pw::InlineString`.
+///
+/// @param[in] binary The binary data that has already been Base64-encoded.
+///
+/// @param[out] output The `pw::InlineString` that `binary` is appended to.
+///
+/// If the data does not fit in the string, an assertion fails.
void Encode(span<const std::byte> binary, InlineString<>& output);
-// Creates a pw::InlineString<> large enough to hold kMaxBinaryDataSizeBytes of
-// binary data when encoded as Base64 and encodes the provided span into it.
-// If the data is larger than kMaxBinaryDataSizeBytes, an assertion fails.
+/// Creates a `pw::InlineString<>` large enough to hold
+/// `kMaxBinaryDataSizeBytes` of binary data when encoded as Base64 and encodes
+/// the provided span into it.
template <size_t kMaxBinaryDataSizeBytes>
inline InlineString<EncodedSize(kMaxBinaryDataSizeBytes)> Encode(
span<const std::byte> binary) {
@@ -109,44 +139,69 @@ inline InlineString<EncodedSize(kMaxBinaryDataSizeBytes)> Encode(
return output;
}
-// Returns the maximum size of decoded Base64 data in bytes. base64_size_bytes
-// must be a multiple of 4, since Base64 encodes 3-byte groups into 4-character
-// strings. If the last 3-byte group has padding, the actual decoded size would
-// be 1 or 2 bytes less than MaxDecodedSize.
+/// Calculates the maximum size of Base64-encoded data after decoding.
+///
+/// @param[in] base64_size_bytes The size of the Base64-encoded data.
+///
+/// @pre `base64_size_bytes` must be a multiple of 4, since Base64 encodes
+/// 3-byte groups into 4-character strings.
+///
+/// @returns The maximum size of the Base64-encoded data represented by
+/// `base64_bytes_size` after decoding. If the last 3-byte group has padding,
+/// the actual decoded size will be 1 or 2 bytes less than the value returned
+/// by `MaxDecodedSize()`.
constexpr size_t MaxDecodedSize(size_t base64_size_bytes) {
return PW_BASE64_MAX_DECODED_SIZE(base64_size_bytes);
}
-// Decodes the provided Base64 data into raw binary. The output buffer *MUST* be
-// at least MaxDecodedSize bytes large. The output buffer may be the same as the
-// input buffer; decoding can occur in place. Returns the number of bytes that
-// were decoded.
-//
-// Decodes either standard (+/) or URL-safe (-_) alphabets. The data must be
-// padded to 4-character blocks with =. This function does NOT check that the
-// input is valid! Use IsValid or the four-argument overload to check the
-// input formatting.
+/// Decodes the provided Base64 data into raw binary.
+///
+/// @pre
+/// * The output buffer **MUST** be at least `MaxDecodedSize()` bytes large.
+/// * This function does NOT check that the input is valid! Use `IsValid()`
+/// or the four-argument overload to check the input formatting.
+///
+/// @param[in] base64 The Base64 data that should be decoded. Can be encoded
+/// with either the standard (`+/`) or URL-safe (`-_`) alphabet. The data must
+/// be padded to 4-character blocks with `=`.
+///
+/// @param[out] output The output buffer where the raw binary will be placed.
+/// The output buffer may be the same as the input buffer; decoding can occur
+/// in place.
+///
+/// @returns The number of bytes that were decoded.
inline size_t Decode(std::string_view base64, void* output) {
return pw_Base64Decode(base64.data(), base64.size(), output);
}
-// Decodes the provided Base64 data, if the data is valid and fits in the output
-// buffer. Returns the number of bytes written, which will be 0 if the data is
-// invalid or doesn't fit.
+/// Decodes the provided Base64 data, if the data is valid and fits in the
+/// output buffer.
+///
+/// @returns The number of bytes written, which will be `0` if the data is
+/// invalid or doesn't fit.
size_t Decode(std::string_view base64, span<std::byte> output_buffer);
+/// Decodes a `pw::InlineString<>` in place.
template <typename T>
inline void DecodeInPlace(InlineBasicString<T>& buffer) {
static_assert(sizeof(T) == sizeof(char));
buffer.resize(Decode(buffer, buffer.data()));
}
-// Returns true if the provided string is valid Base64 encoded data. Accepts
-// either the standard (+/) or URL-safe (-_) alphabets.
+/// @param[in] base64 The string to check. Can be encoded with either the
+/// standard (`+/`) or URL-safe (`-_`) alphabet.
+///
+/// @returns `true` if the provided string is valid Base64-encoded data.
inline bool IsValid(std::string_view base64) {
return pw_Base64IsValid(base64.data(), base64.size());
}
+/// @param[in] base64 The character to check. Can be encoded with either the
+/// standard (`+/`) or URL-safe (`-_`) alphabet.
+///
+/// @returns `true` if the provided character is a valid Base64 character.
+inline bool IsValidChar(char base64) { return pw_Base64IsValidChar(base64); }
+
} // namespace pw::base64
#endif // __cplusplus
diff --git a/pw_bloat/bloat.gni b/pw_bloat/bloat.gni
index 52a3a3f80..da0853028 100644
--- a/pw_bloat/bloat.gni
+++ b/pw_bloat/bloat.gni
@@ -49,12 +49,19 @@ declare_args() {
# output. Optional.
# github.com/google/bloaty/blob/a1bbc93f5f6f969242046dffd9deb379f6735020/doc/using.md
# source_filter: Regex to filter data source names in Bloaty. Optional.
+# json_key_prefix: Prefix for the key names in json size report. Defaults to
+# target name. Optional.
+# full_json_summary: If true, json report includes size breakdown per source
+# hierarchy. Otherwise, defaults to only include the top-level data source
+# type in size report. Optional.
#
# Example:
# pw_size_report("foo_bloat") {
# target = ":foo_static"
# datasources = "symbols,segment_names"
# source_filter = "foo"
+# json_key_prefix = "foo"
+# full_json_summary = true
# }
#
template("pw_size_report") {
@@ -117,6 +124,19 @@ template("pw_size_report") {
"--single-report",
]
+ if (defined(invoker.json_key_prefix)) {
+ _bloat_script_args += [
+ "--json-key-prefix",
+ invoker.json_key_prefix,
+ ]
+ }
+
+ if (defined(invoker.full_json_summary)) {
+ if (invoker.full_json_summary) {
+ _bloat_script_args += [ "--full-json-summary" ]
+ }
+ }
+
_doc_rst_output = "$target_gen_dir/${target_name}"
_binary_sizes_output = "$target_gen_dir/${target_name}.binary_sizes.json"
diff --git a/pw_bloat/docs.rst b/pw_bloat/docs.rst
index 85441e8f6..77bcda56b 100644
--- a/pw_bloat/docs.rst
+++ b/pw_bloat/docs.rst
@@ -121,37 +121,37 @@ base for the size diff can be specified either globally through the top-level
sources that override the global ones (if specified).
-.. code::
+.. code-block::
- import("$dir_pw_bloat/bloat.gni")
+ import("$dir_pw_bloat/bloat.gni")
- executable("empty_base") {
- sources = [ "empty_main.cc" ]
- }
+ executable("empty_base") {
+ sources = [ "empty_main.cc" ]
+ }
- executable("hello_world_printf") {
- sources = [ "hello_printf.cc" ]
- }
+ executable("hello_world_printf") {
+ sources = [ "hello_printf.cc" ]
+ }
- executable("hello_world_iostream") {
- sources = [ "hello_iostream.cc" ]
- }
+ executable("hello_world_iostream") {
+ sources = [ "hello_iostream.cc" ]
+ }
- pw_size_diff("my_size_report") {
- base = ":empty_base"
- data_sources = "symbols,segments"
- binaries = [
- {
- target = ":hello_world_printf"
- label = "Hello world using printf"
- },
- {
- target = ":hello_world_iostream"
- label = "Hello world using iostream"
- data_sources = "symbols"
- },
- ]
- }
+ pw_size_diff("my_size_report") {
+ base = ":empty_base"
+ data_sources = "symbols,segments"
+ binaries = [
+ {
+ target = ":hello_world_printf"
+ label = "Hello world using printf"
+ },
+ {
+ target = ":hello_world_iostream"
+ label = "Hello world using iostream"
+ data_sources = "symbols"
+ },
+ ]
+ }
A sample ``pw_size_diff`` ReST size report table can be found within module
docs. For example, see the :ref:`pw_checksum-size-report` section of the
@@ -168,61 +168,66 @@ a size report for a single binary. The template requires a target binary.
* ``target``: Binary target to run size report on.
* ``data_sources``: Optional list of data sources to organize outputs.
* ``source_filter``: Optional regex to filter labels in the output.
+* ``json_key_prefix``: Optional prefix for key names in json size report.
+* ``full_json_summary``: Optional boolean to print json size report by label
+* level hierarchy. Defaults to only use top-level label in size report.
-.. code::
+.. code-block::
- import("$dir_pw_bloat/bloat.gni")
+ import("$dir_pw_bloat/bloat.gni")
- executable("hello_world_iostream") {
- sources = [ "hello_iostream.cc" ]
- }
+ executable("hello_world_iostream") {
+ sources = [ "hello_iostream.cc" ]
+ }
pw_size_report("hello_world_iostream_size_report") {
target = ":hello_iostream"
data_sources = "segments,symbols"
source_filter = "pw::hello"
+ json_key_prefix = "hello_world_iostream"
+ full_json_summary = true
}
Sample Single Binary ASCII Table Generated
.. code-block::
- ┌─────────────┬──────────────────────────────────────────────────┬──────┐
- │segment_names│ symbols │ sizes│
- ├═════════════┼══════════════════════════════════════════════════┼══════┤
- │FLASH │ │12,072│
- │ │pw::kvs::KeyValueStore::InitializeMetadata() │ 684│
- │ │pw::kvs::KeyValueStore::Init() │ 456│
- │ │pw::kvs::internal::EntryCache::Find() │ 444│
- │ │pw::kvs::FakeFlashMemory::Write() │ 240│
- │ │pw::kvs::internal::Entry::VerifyChecksumInFlash() │ 228│
- │ │pw::kvs::KeyValueStore::GarbageCollectSector() │ 220│
- │ │pw::kvs::KeyValueStore::RemoveDeletedKeyEntries() │ 220│
- │ │pw::kvs::KeyValueStore::AppendEntry() │ 204│
- │ │pw::kvs::KeyValueStore::Get() │ 194│
- │ │pw::kvs::internal::Entry::Read() │ 188│
- │ │pw::kvs::ChecksumAlgorithm::Finish() │ 26│
- │ │pw::kvs::internal::Entry::ReadKey() │ 26│
- │ │pw::kvs::internal::Sectors::BaseAddress() │ 24│
- │ │pw::kvs::ChecksumAlgorithm::Update() │ 20│
- │ │pw::kvs::FlashTestPartition() │ 8│
- │ │pw::kvs::FakeFlashMemory::Disable() │ 6│
- │ │pw::kvs::FakeFlashMemory::Enable() │ 6│
- │ │pw::kvs::FlashMemory::SelfTest() │ 6│
- │ │pw::kvs::FlashPartition::Init() │ 6│
- │ │pw::kvs::FlashPartition::sector_size_bytes() │ 6│
- │ │pw::kvs::FakeFlashMemory::IsEnabled() │ 4│
- ├─────────────┼──────────────────────────────────────────────────┼──────┤
- │RAM │ │ 1,424│
- │ │test_kvs │ 992│
- │ │pw::kvs::(anonymous namespace)::test_flash │ 384│
- │ │pw::kvs::(anonymous namespace)::test_partition │ 24│
- │ │pw::kvs::FakeFlashMemory::no_errors_ │ 12│
- │ │borrowable_kvs │ 8│
- │ │kvs_entry_count │ 4│
- ├═════════════┼══════════════════════════════════════════════════┼══════┤
- │Total │ │13,496│
- └─────────────┴──────────────────────────────────────────────────┴──────┘
+ ┌─────────────┬──────────────────────────────────────────────────┬──────┐
+ │segment_names│ symbols │ sizes│
+ ├═════════════┼══════════════════════════════════════════════════┼══════┤
+ │FLASH │ │12,072│
+ │ │pw::kvs::KeyValueStore::InitializeMetadata() │ 684│
+ │ │pw::kvs::KeyValueStore::Init() │ 456│
+ │ │pw::kvs::internal::EntryCache::Find() │ 444│
+ │ │pw::kvs::FakeFlashMemory::Write() │ 240│
+ │ │pw::kvs::internal::Entry::VerifyChecksumInFlash() │ 228│
+ │ │pw::kvs::KeyValueStore::GarbageCollectSector() │ 220│
+ │ │pw::kvs::KeyValueStore::RemoveDeletedKeyEntries() │ 220│
+ │ │pw::kvs::KeyValueStore::AppendEntry() │ 204│
+ │ │pw::kvs::KeyValueStore::Get() │ 194│
+ │ │pw::kvs::internal::Entry::Read() │ 188│
+ │ │pw::kvs::ChecksumAlgorithm::Finish() │ 26│
+ │ │pw::kvs::internal::Entry::ReadKey() │ 26│
+ │ │pw::kvs::internal::Sectors::BaseAddress() │ 24│
+ │ │pw::kvs::ChecksumAlgorithm::Update() │ 20│
+ │ │pw::kvs::FlashTestPartition() │ 8│
+ │ │pw::kvs::FakeFlashMemory::Disable() │ 6│
+ │ │pw::kvs::FakeFlashMemory::Enable() │ 6│
+ │ │pw::kvs::FlashMemory::SelfTest() │ 6│
+ │ │pw::kvs::FlashPartition::Init() │ 6│
+ │ │pw::kvs::FlashPartition::sector_size_bytes() │ 6│
+ │ │pw::kvs::FakeFlashMemory::IsEnabled() │ 4│
+ ├─────────────┼──────────────────────────────────────────────────┼──────┤
+ │RAM │ │ 1,424│
+ │ │test_kvs │ 992│
+ │ │pw::kvs::(anonymous namespace)::test_flash │ 384│
+ │ │pw::kvs::(anonymous namespace)::test_partition │ 24│
+ │ │pw::kvs::FakeFlashMemory::no_errors_ │ 12│
+ │ │borrowable_kvs │ 8│
+ │ │kvs_entry_count │ 4│
+ ├═════════════┼══════════════════════════════════════════════════┼══════┤
+ │Total │ │13,496│
+ └─────────────┴──────────────────────────────────────────────────┴──────┘
Size reports are typically included in ReST documentation, as described in
@@ -235,10 +240,11 @@ Collecting size report data
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Each ``pw_size_report`` target outputs a JSON file containing the sizes of all
top-level labels in the binary. (By default, this represents "segments", i.e.
-ELF program headers.) If a build produces multiple images, it may be useful to
-collect all of their sizes into a single file to provide a snapshot of sizes at
-some point in time --- for example, to display per-commit size deltas through
-CI.
+ELF program headers.) If ``full_json_summary`` is set to true, sizes for all
+label levels are reported (i.e. Default labels would show size of each symbol
+per segment). If a build produces multiple images, it may be useful to collect
+all of their sizes into a single file to provide a snapshot of sizes at some
+point in time --- for example, to display per-commit size deltas through CI.
The ``pw_size_report_aggregation`` template is provided to collect multiple size
reports' data into a single JSON file.
@@ -248,17 +254,17 @@ reports' data into a single JSON file.
* ``deps``: List of ``pw_size_report`` targets whose data to collect.
* ``output``: Path to the output JSON file.
-.. code::
+.. code-block::
- import("$dir_pw_bloat/bloat.gni")
+ import("$dir_pw_bloat/bloat.gni")
- pw_size_report_aggregation("image_sizes") {
- deps = [
- ":app_image_size_report",
- ":bootloader_image_size_report",
- ]
- output = "$root_gen_dir/artifacts/image_sizes.json"
- }
+ pw_size_report_aggregation("image_sizes") {
+ deps = [
+ ":app_image_size_report",
+ ":bootloader_image_size_report",
+ ]
+ output = "$root_gen_dir/artifacts/image_sizes.json"
+ }
Documentation integration
=========================
@@ -270,7 +276,7 @@ This file can be imported directly into a ReST documentation file using the
For example, the ``simple_bloat_loop`` and ``simple_bloat_function`` size
reports under ``//pw_bloat/examples`` are imported into this file as follows:
-.. code:: rst
+.. code-block:: rst
Simple bloat loop example
^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -313,16 +319,16 @@ which may return something like:
.. code-block::
- 84.2% 1023Ki FLASH
- 94.2% 963Ki Free space
- 5.8% 59.6Ki Used space
- 15.8% 192Ki RAM
- 100.0% 192Ki Used space
- 0.0% 512 VECTOR_TABLE
- 96.9% 496 Free space
- 3.1% 16 Used space
- 0.0% 0 Not resident in memory
- NAN% 0 Used space
+ 84.2% 1023Ki FLASH
+ 94.2% 963Ki Free space
+ 5.8% 59.6Ki Used space
+ 15.8% 192Ki RAM
+ 100.0% 192Ki Used space
+ 0.0% 512 VECTOR_TABLE
+ 96.9% 496 Free space
+ 3.1% 16 Used space
+ 0.0% 0 Not resident in memory
+ NAN% 0 Used space
``utilization`` data source
@@ -345,125 +351,125 @@ For example imagine this partial example GNU LD linker script:
.. code-block::
- MEMORY
- {
- FLASH(rx) : \
- ORIGIN = PW_BOOT_FLASH_BEGIN, \
- LENGTH = PW_BOOT_FLASH_SIZE
- RAM(rwx) : \
- ORIGIN = PW_BOOT_RAM_BEGIN, \
- LENGTH = PW_BOOT_RAM_SIZE
- }
+ MEMORY
+ {
+ FLASH(rx) : \
+ ORIGIN = PW_BOOT_FLASH_BEGIN, \
+ LENGTH = PW_BOOT_FLASH_SIZE
+ RAM(rwx) : \
+ ORIGIN = PW_BOOT_RAM_BEGIN, \
+ LENGTH = PW_BOOT_RAM_SIZE
+ }
- SECTIONS
- {
- /* Main executable code. */
- .code : ALIGN(4)
- {
- /* Application code. */
- *(.text)
- *(.text*)
- KEEP(*(.init))
- KEEP(*(.fini))
-
- . = ALIGN(4);
- /* Constants.*/
- *(.rodata)
- *(.rodata*)
- } >FLASH
-
- /* Explicitly initialized global and static data. (.data)*/
- .static_init_ram : ALIGN(4)
- {
- *(.data)
- *(.data*)
- . = ALIGN(4);
- } >RAM AT> FLASH
-
- /* Zero initialized global/static data. (.bss) */
- .zero_init_ram (NOLOAD) : ALIGN(4)
- {
- *(.bss)
- *(.bss*)
- *(COMMON)
- . = ALIGN(4);
- } >RAM
- }
+ SECTIONS
+ {
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ /* Constants.*/
+ *(.rodata)
+ *(.rodata*)
+ } >FLASH
+
+ /* Explicitly initialized global and static data. (.data)*/
+ .static_init_ram : ALIGN(4)
+ {
+ *(.data)
+ *(.data*)
+ . = ALIGN(4);
+ } >RAM AT> FLASH
+
+ /* Zero initialized global/static data. (.bss) */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
+ {
+ *(.bss)
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } >RAM
+ }
Could be modified as follows enable ``Free Space`` reporting:
.. code-block::
- MEMORY
- {
- FLASH(rx) : ORIGIN = PW_BOOT_FLASH_BEGIN, LENGTH = PW_BOOT_FLASH_SIZE
- RAM(rwx) : ORIGIN = PW_BOOT_RAM_BEGIN, LENGTH = PW_BOOT_RAM_SIZE
-
- /* Each memory region above has an associated .*.unused_space section that
- * overlays the unused space at the end of the memory segment. These
- * segments are used by pw_bloat.bloaty_config to create the utilization
- * data source for bloaty size reports.
- *
- * These sections MUST be located immediately after the last section that is
- * placed in the respective memory region or lld will issue a warning like:
- *
- * warning: ignoring memory region assignment for non-allocatable section
- * '.VECTOR_TABLE.unused_space'
- *
- * If this warning occurs, it's also likely that LLD will have created quite
- * large padded regions in the ELF file due to bad cursor operations. This
- * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
- * megabytes.
- *
- * Attempting to add sections to the memory region AFTER the unused_space
- * section will cause the region to overflow.
- */
- }
+ MEMORY
+ {
+ FLASH(rx) : ORIGIN = PW_BOOT_FLASH_BEGIN, LENGTH = PW_BOOT_FLASH_SIZE
+ RAM(rwx) : ORIGIN = PW_BOOT_RAM_BEGIN, LENGTH = PW_BOOT_RAM_SIZE
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These
+ * segments are used by pw_bloat.bloaty_config to create the utilization
+ * data source for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
+ }
- SECTIONS
- {
- /* Main executable code. */
- .code : ALIGN(4)
- {
- /* Application code. */
- *(.text)
- *(.text*)
- KEEP(*(.init))
- KEEP(*(.fini))
-
- . = ALIGN(4);
- /* Constants.*/
- *(.rodata)
- *(.rodata*)
- } >FLASH
-
- /* Explicitly initialized global and static data. (.data)*/
- .static_init_ram : ALIGN(4)
- {
- *(.data)
- *(.data*)
- . = ALIGN(4);
- } >RAM AT> FLASH
-
- /* Defines a section representing the unused space in the FLASH segment.
- * This MUST be the last section assigned to the FLASH region.
- */
- PW_BLOAT_UNUSED_SPACE(FLASH)
-
- /* Zero initialized global/static data. (.bss). */
- .zero_init_ram (NOLOAD) : ALIGN(4)
- {
- *(.bss)
- *(.bss*)
- *(COMMON)
- . = ALIGN(4);
- } >RAM
-
- /* Defines a section representing the unused space in the RAM segment. This
- * MUST be the last section assigned to the RAM region.
- */
- PW_BLOAT_UNUSED_SPACE(RAM)
- }
+ SECTIONS
+ {
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ /* Constants.*/
+ *(.rodata)
+ *(.rodata*)
+ } >FLASH
+
+ /* Explicitly initialized global and static data. (.data)*/
+ .static_init_ram : ALIGN(4)
+ {
+ *(.data)
+ *(.data*)
+ . = ALIGN(4);
+ } >RAM AT> FLASH
+
+ /* Defines a section representing the unused space in the FLASH segment.
+ * This MUST be the last section assigned to the FLASH region.
+ */
+ PW_BLOAT_UNUSED_SPACE(FLASH)
+
+ /* Zero initialized global/static data. (.bss). */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
+ {
+ *(.bss)
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } >RAM
+
+ /* Defines a section representing the unused space in the RAM segment. This
+ * MUST be the last section assigned to the RAM region.
+ */
+ PW_BLOAT_UNUSED_SPACE(RAM)
+ }
The preprocessor macro ``PW_BLOAT_UNUSED_SPACE`` is defined in
``pw_bloat/bloat_macros.ld``. To use these macros include this file in your
@@ -518,7 +524,7 @@ generate the symbols needed for the that region:
.. code-block::
- PW_BLOAT_MEMORY_REGION(FLASH)
+ PW_BLOAT_MEMORY_REGION(FLASH)
As another example, if you have two aliased memory regions (``DCTM`` and
``ITCM``) into the same effective memory named you'd like to call ``RAM``, then
@@ -526,5 +532,6 @@ you should produce the following four symbols in your linker script:
.. code-block::
- PW_BLOAT_MEMORY_REGION_MAP(RAM, ITCM)
- PW_BLOAT_MEMORY_REGION_MAP(RAM, DTCM)
+ PW_BLOAT_MEMORY_REGION_MAP(RAM, ITCM)
+ PW_BLOAT_MEMORY_REGION_MAP(RAM, DTCM)
+
diff --git a/pw_bloat/py/BUILD.gn b/pw_bloat/py/BUILD.gn
index 2f75579bf..59327a6bf 100644
--- a/pw_bloat/py/BUILD.gn
+++ b/pw_bloat/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_bloat/__init__.py",
diff --git a/pw_bloat/py/label_test.py b/pw_bloat/py/label_test.py
index 2e2236839..07bfe83aa 100644
--- a/pw_bloat/py/label_test.py
+++ b/pw_bloat/py/label_test.py
@@ -51,24 +51,24 @@ class LabelStructTest(unittest.TestCase):
def test_data_source_single_insert_total_size(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
- ds_map.insert_label_hierachy(['FLASH', '.code', 'main()'], 30)
+ ds_map.insert_label_hierarchy(['FLASH', '.code', 'main()'], 30)
self.assertEqual(ds_map.get_total_size(), 30)
def test_data_source_multiple_insert_total_size(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
- ds_map.insert_label_hierachy(['FLASH', '.code', 'main()'], 30)
- ds_map.insert_label_hierachy(['RAM', '.code', 'foo()'], 100)
+ ds_map.insert_label_hierarchy(['FLASH', '.code', 'main()'], 30)
+ ds_map.insert_label_hierarchy(['RAM', '.code', 'foo()'], 100)
self.assertEqual(ds_map.get_total_size(), 130)
def test_parsing_generator_three_datasource_names(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
list_labels_three = [*LIST_LABELS, Label(name='total', size=350)]
- for label_hiearchy in ds_map.labels():
- self.assertIn(label_hiearchy, list_labels_three)
+ for label_hierarchy in ds_map.labels():
+ self.assertIn(label_hierarchy, list_labels_three)
self.assertEqual(ds_map.get_total_size(), 350)
def test_parsing_generator_two_datasource_names(self):
@@ -79,18 +79,18 @@ class LabelStructTest(unittest.TestCase):
Label(name='bar()', size=220, parents=tuple(['RAM'])),
]
for label in ds_label_list:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.name], label.size
)
list_labels_two = [*ds_label_list, Label(name='total', size=350)]
- for label_hiearchy in ds_map.labels():
- self.assertIn(label_hiearchy, list_labels_two)
+ for label_hierarchy in ds_map.labels():
+ self.assertIn(label_hierarchy, list_labels_two)
self.assertEqual(ds_map.get_total_size(), 350)
def test_parsing_generator_specified_datasource_1(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
list_labels_ds_b = [
@@ -98,14 +98,14 @@ class LabelStructTest(unittest.TestCase):
Label(name='.heap', size=320, parents=tuple(['RAM'])),
]
list_labels_ds_b += [Label(name='total', size=350)]
- for label_hiearchy in ds_map.labels(1):
- self.assertIn(label_hiearchy, list_labels_ds_b)
+ for label_hierarchy in ds_map.labels(1):
+ self.assertIn(label_hierarchy, list_labels_ds_b)
self.assertEqual(ds_map.get_total_size(), 350)
def test_parsing_generator_specified_datasource_str_2(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
list_labels_ds_a = [
@@ -113,14 +113,14 @@ class LabelStructTest(unittest.TestCase):
Label(name='RAM', size=320, parents=tuple([])),
]
list_labels_ds_a += [Label(name='total', size=350)]
- for label_hiearchy in ds_map.labels(0):
- self.assertIn(label_hiearchy, list_labels_ds_a)
+ for label_hierarchy in ds_map.labels(0):
+ self.assertIn(label_hierarchy, list_labels_ds_a)
self.assertEqual(ds_map.get_total_size(), 350)
def test_parsing_generator_specified_datasource_int(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
list_labels_ds_a = [
@@ -128,14 +128,14 @@ class LabelStructTest(unittest.TestCase):
Label(name='RAM', size=320, parents=tuple([])),
]
list_labels_ds_a += [Label(name='total', size=350)]
- for label_hiearchy in ds_map.labels(0):
- self.assertIn(label_hiearchy, list_labels_ds_a)
+ for label_hierarchy in ds_map.labels(0):
+ self.assertIn(label_hierarchy, list_labels_ds_a)
self.assertEqual(ds_map.get_total_size(), 350)
def test_parsing_generator_specified_datasource_int_2(self):
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
list_labels_ds_b = [
@@ -143,21 +143,21 @@ class LabelStructTest(unittest.TestCase):
Label(name='.heap', size=320, parents=tuple(['RAM'])),
]
list_labels_ds_b += [Label(name='total', size=350)]
- for label_hiearchy in ds_map.labels(1):
- self.assertIn(label_hiearchy, list_labels_ds_b)
+ for label_hierarchy in ds_map.labels(1):
+ self.assertIn(label_hierarchy, list_labels_ds_b)
self.assertEqual(ds_map.get_total_size(), 350)
def test_diff_same_ds_labels_diff_sizes(self):
"""Same map with different sizes."""
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
ds_map2 = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map2.insert_label_hierachy(
+ ds_map2.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name],
label.size + 10,
)
@@ -185,28 +185,30 @@ class LabelStructTest(unittest.TestCase):
ds_map_diff = ds_map.diff(ds_map2)
- for label_hiearchy in ds_map_diff.labels():
- self.assertIn(label_hiearchy, list_labels_ds_b)
+ for label_hierarchy in ds_map_diff.labels():
+ self.assertIn(label_hierarchy, list_labels_ds_b)
def test_diff_missing_ds_labels_diff_sizes(self):
"""Different map with different sizes."""
ds_map = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS:
- ds_map.insert_label_hierachy(
+ ds_map.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name], label.size
)
ds_map2 = DataSourceMap(['a', 'b', 'c'])
for label in LIST_LABELS[:-1]:
- ds_map2.insert_label_hierachy(
+ ds_map2.insert_label_hierarchy(
[label.parents[0], label.parents[1], label.name],
label.size + 20,
)
- ds_map2.insert_label_hierachy(
+ ds_map2.insert_label_hierarchy(
[label.parents[0], label.parents[1], 'foobar()'], label.size + 20
)
- ds_map2.insert_label_hierachy(["LOAD #5", 'random_load', 'func()'], 250)
+ ds_map2.insert_label_hierarchy(
+ ["LOAD #5", 'random_load', 'func()'], 250
+ )
list_labels_ds_b = [
Label(
@@ -234,8 +236,8 @@ class LabelStructTest(unittest.TestCase):
ds_map_diff = ds_map2.diff(ds_map)
- for label_hiearchy in ds_map_diff.labels(0):
- self.assertIn(label_hiearchy, list_labels_ds_b)
+ for label_hierarchy in ds_map_diff.labels(0):
+ self.assertIn(label_hierarchy, list_labels_ds_b)
if __name__ == '__main__':
diff --git a/pw_bloat/py/pw_bloat/bloat.py b/pw_bloat/py/pw_bloat/bloat.py
index d14c1c70b..4bf455dd8 100755
--- a/pw_bloat/py/pw_bloat/bloat.py
+++ b/pw_bloat/py/pw_bloat/bloat.py
@@ -28,7 +28,7 @@ from typing import Iterable, Optional
import pw_cli.log
from pw_bloat.bloaty_config import generate_bloaty_config
-from pw_bloat.label import DataSourceMap, Label
+from pw_bloat.label import DataSourceMap
from pw_bloat.label_output import (
BloatTableOutput,
LineCharset,
@@ -57,6 +57,17 @@ def parse_args() -> argparse.Namespace:
action="store_true",
help='Determine if calling single size report',
)
+ parser.add_argument(
+ '--json-key-prefix',
+ type=str,
+ help='Prefix for json keys in size report, default = target name',
+ default=None,
+ )
+ parser.add_argument(
+ '--full-json-summary',
+ action="store_true",
+ help='Include all levels of data sources in json binary report',
+ )
return parser.parse_args()
@@ -166,19 +177,35 @@ def write_file(filename: str, contents: str, out_dir_file: str) -> None:
_LOG.debug('Output written to %s', path)
-def create_binary_sizes_json(binary_name: str, labels: Iterable[Label]) -> str:
+def create_binary_sizes_json(
+ key_prefix: str,
+ data_source_map: DataSourceMap,
+ full_json: bool,
+) -> str:
"""Creates a binary_sizes.json file content from a list of labels.
Args:
- binary_name: the single binary name to attribute segment sizes to.
- labels: the label.Label content to include
+ key_prefix: Prefix for the json keys.
+ data_source_map: Hierarchical structure containing size of sources.
+ full_json: Report contains all sources, otherwise just top level.
Returns:
- a string of content to write to binary_sizes.json file.
+ A string of content to write to binary_sizes.json file.
"""
- json_content = {
- f'{binary_name} {label.name}': label.size for label in labels
- }
+ if full_json:
+ *ds_parents, last = data_source_map.get_ds_names()
+ json_content = {}
+ for label in data_source_map.labels():
+ key = f'{key_prefix}.'
+ for ds_parent, label_parent in zip(ds_parents, label.parents):
+ key += f'{ds_parent}.{label_parent}.'
+ key += f'{last}.{label.name}'
+ json_content[key] = label.size
+ else:
+ json_content = {
+ f'{key_prefix}.{label.name}': label.size
+ for label in data_source_map.labels(ds_index=0)
+ }
return json.dumps(json_content, sort_keys=True, indent=2)
@@ -189,8 +216,27 @@ def single_target_output(
out_dir: str,
data_sources: Iterable[str],
extra_args: Iterable[str],
+ json_key_prefix: str,
+ full_json: bool,
) -> int:
- """TODO(frolv) Add docstring."""
+ """Generates size report for a single target.
+
+ Args:
+ target: The ELF binary on which to run.
+ bloaty_config: Path to Bloaty config file.
+ target_out_file: Output file name for the generated reports.
+ out_dir: Path to write size reports to.
+ data_sources: Hierarchical data sources to display.
+ extra_args: Additional command-line arguments to pass to Bloaty.
+ json_key_prefix: Prefix for the json keys, uses target name by default.
+ full_json: Json report contains all hierarchical data source totals.
+
+ Returns:
+ Zero on success.
+
+ Raises:
+ subprocess.CalledProcessError: The Bloaty invocation failed.
+ """
try:
single_output = run_bloaty(
@@ -219,9 +265,9 @@ def single_target_output(
single_report_table = single_report.create_table()
- # Generates contents for top level summary for binary_sizes.json
+ # Generates contents for summary printed to binary_sizes.json
binary_json_content = create_binary_sizes_json(
- target, data_source_map.labels(ds_index=0)
+ json_key_prefix, data_source_map, full_json
)
print(single_report_table)
@@ -245,6 +291,7 @@ def main() -> int:
gn_arg_dict = {}
json_file = open(args.gn_arg_path)
gn_arg_dict = json.load(json_file)
+ json_key_prefix = args.json_key_prefix
if args.single_report:
single_binary_args = gn_arg_dict['binaries'][0]
@@ -255,6 +302,10 @@ def main() -> int:
if single_binary_args['data_sources']:
data_sources = single_binary_args['data_sources']
+ # Use target binary name as json key prefix if none given
+ if not json_key_prefix:
+ json_key_prefix = single_binary_args['target']
+
return single_target_output(
single_binary_args['target'],
single_binary_args['bloaty_config'],
@@ -262,6 +313,8 @@ def main() -> int:
gn_arg_dict['out_dir'],
data_sources,
extra_args,
+ json_key_prefix,
+ args.full_json_summary,
)
default_data_sources = ['segment_names', 'symbols']
diff --git a/pw_bloat/py/pw_bloat/label.py b/pw_bloat/py/pw_bloat/label.py
index 9a12610e7..2fc663f3f 100644
--- a/pw_bloat/py/pw_bloat/label.py
+++ b/pw_bloat/py/pw_bloat/label.py
@@ -17,6 +17,7 @@ The label module defines a class to store and manipulate size reports.
from collections import defaultdict
from dataclasses import dataclass
+from functools import lru_cache
from typing import Iterable, Dict, Sequence, Tuple, List, Optional
import csv
@@ -69,9 +70,8 @@ class _LabelMap:
def __contains__(self, parent_label: str) -> bool:
return parent_label in self._label_map
- def map_generator(self) -> Iterable[Tuple[str, Dict[str, LabelInfo]]]:
- for parent_label, label_dict in self._label_map.items():
- yield parent_label, label_dict
+ def map_items(self) -> Iterable[Tuple[str, Dict[str, LabelInfo]]]:
+ return self._label_map.items()
class _DataSource:
@@ -102,15 +102,14 @@ class _DataSource:
def __contains__(self, parent_label: str) -> bool:
return parent_label in self._ds_label_map
- def label_map_generator(self) -> Iterable[Tuple[str, Dict[str, LabelInfo]]]:
- for parent_label, label_dict in self._ds_label_map.map_generator():
- yield parent_label, label_dict
+ def label_map_items(self) -> Iterable[Tuple[str, Dict[str, LabelInfo]]]:
+ return self._ds_label_map.map_items()
class DataSourceMap:
"""Module to store an array of DataSources and capacities.
- An organize way to store a hierachy of labels and their sizes.
+ An organized way to store a hierarchy of labels and their sizes.
Includes a capacity array to hold regex patterns for applying
capacities to matching label names.
@@ -126,7 +125,7 @@ class DataSourceMap:
vmsize_index = top_row.index('vmsize')
ds_map_tsv = cls(top_row[:vmsize_index])
for row in reader:
- ds_map_tsv.insert_label_hierachy(
+ ds_map_tsv.insert_label_hierarchy(
row[:vmsize_index], int(row[vmsize_index])
)
return ds_map_tsv
@@ -144,25 +143,25 @@ class DataSourceMap:
child_label in self._data_sources[ds_index][parent_label]
)
- def insert_label_hierachy(
+ def insert_label_hierarchy(
self,
label_hierarchy: Iterable[str],
size: int,
diff_exist: Optional[bool] = None,
) -> None:
- """Insert a hierachy of labels with its size."""
+ """Insert a hierarchy of labels with its size."""
# Insert initial '__base__' data source that holds the
# running total size.
self._data_sources[0].add_label(
'__base__', self._BASE_TOTAL_LABEL, size
)
- complete_label_hierachy = [self._BASE_TOTAL_LABEL, *label_hierarchy]
- for index in range(len(complete_label_hierachy) - 1):
- if complete_label_hierachy[index]:
+ complete_label_hierarchy = [self._BASE_TOTAL_LABEL, *label_hierarchy]
+ for index in range(len(complete_label_hierarchy) - 1):
+ if complete_label_hierarchy[index]:
self._data_sources[index + 1].add_label(
- complete_label_hierachy[index],
- complete_label_hierachy[index + 1],
+ complete_label_hierarchy[index],
+ complete_label_hierarchy[index + 1],
size,
diff_exist,
)
@@ -182,7 +181,7 @@ class DataSourceMap:
for b_label in base.labels(last_data_source):
if last_data_source > 0:
curr_parent = b_label.parents[-1]
- lb_hierachy_names = [*b_label.parents, b_label.name]
+ lb_hierarchy_names = [*b_label.parents, b_label.name]
# Check if label exists in target binary DataSourceMap.
# Subtract base from target size and insert diff size
@@ -197,16 +196,16 @@ class DataSourceMap:
) - b_label.size
if diff_size:
- diff_dsm.insert_label_hierachy(
- lb_hierachy_names, diff_size, True
+ diff_dsm.insert_label_hierarchy(
+ lb_hierarchy_names, diff_size, True
)
else:
- diff_dsm.insert_label_hierachy(lb_hierachy_names, 0, True)
+ diff_dsm.insert_label_hierarchy(lb_hierarchy_names, 0, True)
# label is not present in target - insert with negative size
else:
- diff_dsm.insert_label_hierachy(
- lb_hierachy_names, -1 * b_label.size, False
+ diff_dsm.insert_label_hierarchy(
+ lb_hierarchy_names, -1 * b_label.size, False
)
# Iterate through all of target labels
@@ -219,7 +218,7 @@ class DataSourceMap:
if not base.label_exists(
parent_data_source_index, curr_parent, t_label.name
):
- diff_dsm.insert_label_hierachy(
+ diff_dsm.insert_label_hierarchy(
[*t_label.parents, f"{t_label.name}"], t_label.size, False
)
@@ -234,6 +233,7 @@ class DataSourceMap:
data_source.get_name() for data_source in self._data_sources[1:]
)
+ @lru_cache
def labels(self, ds_index: Optional[int] = None) -> Iterable[Label]:
"""Generator that yields a Label depending on specified data source.
@@ -244,36 +244,43 @@ class DataSourceMap:
Iterable Label objects.
"""
ds_index = len(self._data_sources) if ds_index is None else ds_index + 2
- yield from self._per_data_source_generator(
+ return self._per_data_source_labels(
tuple(), self._data_sources[1:ds_index]
)
- def _per_data_source_generator(
+ def _per_data_source_labels(
self,
parent_labels: Tuple[str, ...],
data_sources: Sequence[_DataSource],
) -> Iterable[Label]:
"""Recursive generator to return Label based off parent labels."""
+ if parent_labels:
+ current_parent = parent_labels[-1]
+ else:
+ current_parent = self._BASE_TOTAL_LABEL
+ labels = []
for ds_index, curr_ds in enumerate(data_sources):
- for parent_label, label_map in curr_ds.label_map_generator():
- if not parent_labels:
- curr_parent = self._BASE_TOTAL_LABEL
+ if not current_parent in curr_ds:
+ continue
+ label_map = curr_ds[current_parent]
+ for child_label, label_info in label_map.items():
+ if len(data_sources) == 1:
+ labels.append(
+ Label(
+ child_label,
+ label_info.size,
+ parents=parent_labels,
+ exists_both=label_info.exists_both,
+ )
+ )
else:
- curr_parent = parent_labels[-1]
- if parent_label == curr_parent:
- for child_label, label_info in label_map.items():
- if len(data_sources) == 1:
- yield Label(
- child_label,
- label_info.size,
- parents=parent_labels,
- exists_both=label_info.exists_both,
- )
- else:
- yield from self._per_data_source_generator(
- (*parent_labels, child_label),
- data_sources[ds_index + 1 :],
- )
+ labels.extend(
+ self._per_data_source_labels(
+ (*parent_labels, child_label),
+ data_sources[ds_index + 1 :],
+ )
+ )
+ return labels
class DiffDataSourceMap(DataSourceMap):
diff --git a/pw_bloat/py/pw_bloat/label_output.py b/pw_bloat/py/pw_bloat/label_output.py
index 938cc90e2..0ee44ab97 100644
--- a/pw_bloat/py/pw_bloat/label_output.py
+++ b/pw_bloat/py/pw_bloat/label_output.py
@@ -201,7 +201,7 @@ class BloatTableOutput:
def create_table(self) -> str:
"""Parse DataSourceMap to create ASCII table."""
- curr_lb_hierachy = None
+ curr_lb_hierarchy = None
last_diff_name = ''
if self._diff_mode:
self._ascii_table_rows.extend([*self._label_title_row()])
@@ -219,7 +219,7 @@ class BloatTableOutput:
has_entries = True
- new_lb_hierachy = tuple(
+ new_lb_hierarchy = tuple(
[
*self._get_ds_label_size(curr_label.parents),
self._LabelContent(
@@ -230,9 +230,9 @@ class BloatTableOutput:
]
)
diff_list = self._diff_label_names(
- curr_lb_hierachy, new_lb_hierachy
+ curr_lb_hierarchy, new_lb_hierarchy
)
- curr_lb_hierachy = new_lb_hierachy
+ curr_lb_hierarchy = new_lb_hierarchy
if curr_label.parents and curr_label.parents[0] == last_diff_name:
continue
diff --git a/pw_blob_store/blob_store.cc b/pw_blob_store/blob_store.cc
index 03336db94..38305d664 100644
--- a/pw_blob_store/blob_store.cc
+++ b/pw_blob_store/blob_store.cc
@@ -90,7 +90,7 @@ Status BlobStore::LoadMetadata() {
metadata.v1_metadata.checksum)
.ok()) {
PW_LOG_ERROR("BlobStore init - Invalidating blob with invalid checksum");
- Invalidate().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ Invalidate().IgnoreError(); // TODO: b/242598609 - Handle Status properly
return Status::DataLoss();
}
@@ -120,7 +120,7 @@ Status BlobStore::OpenWrite() {
writer_open_ = true;
// Clear any existing contents.
- Invalidate().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ Invalidate().IgnoreError(); // TODO: b/242598609 - Handle Status properly
return OkStatus();
}
@@ -453,7 +453,7 @@ Status BlobStore::Erase() {
// If any writes have been performed, reset the state.
if (flash_address_ != 0) {
- Invalidate().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ Invalidate().IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
PW_TRY(partition_.Erase());
diff --git a/pw_blob_store/docs.rst b/pw_blob_store/docs.rst
index a418f58af..e8810145b 100644
--- a/pw_blob_store/docs.rst
+++ b/pw_blob_store/docs.rst
@@ -47,15 +47,15 @@ appending to existing data.
.. code-block:: cpp
- BlobStore::BlobWriterWithBuffer writer(my_blob_store);
- writer.Open();
- writer.Write(my_data);
+ BlobStore::BlobWriterWithBuffer writer(my_blob_store);
+ writer.Open();
+ writer.Write(my_data);
- // ...
+ // ...
- // A close is implied when a BlobWriter is destroyed. Manually closing a
- // BlobWriter enables error handling on Close() failure.
- writer.Close();
+ // A close is implied when a BlobWriter is destroyed. Manually closing a
+ // BlobWriter enables error handling on Close() failure.
+ writer.Close();
Erasing a BlobStore
===================
@@ -83,13 +83,13 @@ for the ``std::string_view`` to be invalidated after the function returns.
.. code-block:: cpp
- constexpr size_t kMaxFileNameLength = 48;
- BlobStore::BlobWriterWithBuffer<kMaxFileNameLength> writer(my_blob_store);
- writer.Open();
- writer.SetFileName("stonks.jpg");
- writer.Write(my_data);
- // ...
- writer.Close();
+ constexpr size_t kMaxFileNameLength = 48;
+ BlobStore::BlobWriterWithBuffer<kMaxFileNameLength> writer(my_blob_store);
+ writer.Open();
+ writer.SetFileName("stonks.jpg");
+ writer.Write(my_data);
+ // ...
+ writer.Close();
Reading from a BlobStore
------------------------
@@ -97,12 +97,12 @@ A ``BlobStore`` may have multiple open ``BlobReader`` objects. No other
readers/writers may be open/active if a ``BlobWriter`` is opened on a blob
store.
- 0) Create BlobReader instance
- 1) BlobReader::Open().
- 2) Read data using BlobReader::Read() or
- BlobReader::GetMemoryMappedBlob(). BlobReader is seekable. Use
- BlobReader::Seek() to read from a desired offset.
- 3) BlobReader::Close().
+0) Create BlobReader instance
+1) BlobReader::Open()
+2) Read data using BlobReader::Read() or
+ BlobReader::GetMemoryMappedBlob(). BlobReader is seekable. Use
+ BlobReader::Seek() to read from a desired offset.
+3) BlobReader::Close()
--------------------------
FileSystem RPC integration
@@ -119,6 +119,5 @@ The following size report showcases the memory usage of the blob store.
.. include:: blob_size
-
.. note::
The documentation for this module is currently incomplete.
diff --git a/pw_blob_store/flat_file_system_entry.cc b/pw_blob_store/flat_file_system_entry.cc
index c1bc594db..d5f00b163 100644
--- a/pw_blob_store/flat_file_system_entry.cc
+++ b/pw_blob_store/flat_file_system_entry.cc
@@ -80,7 +80,7 @@ size_t FlatFileSystemBlobStoreEntry::SizeBytes() {
return reader.ConservativeReadLimit();
}
-// TODO(b/234888404): This file can be deleted even though it is read-only.
+// TODO: b/234888404 - This file can be deleted even though it is read-only.
// This type of behavior should be possible to express via the FileSystem RPC
// service.
Status FlatFileSystemBlobStoreEntry::Delete() {
diff --git a/pw_blob_store/public/pw_blob_store/blob_store.h b/pw_blob_store/public/pw_blob_store/blob_store.h
index 17a0335a0..d87621e6c 100644
--- a/pw_blob_store/public/pw_blob_store/blob_store.h
+++ b/pw_blob_store/public/pw_blob_store/blob_store.h
@@ -68,7 +68,7 @@ class BlobStore {
BlobWriter& operator=(const BlobWriter&) = delete;
~BlobWriter() override {
if (open_) {
- Close().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ Close().IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
@@ -349,7 +349,7 @@ class BlobStore {
BlobStore(std::string_view name,
kvs::FlashPartition& partition,
kvs::ChecksumAlgorithm* checksum_algo,
- sync::Borrowable<kvs::KeyValueStore>& kvs,
+ sync::Borrowable<kvs::KeyValueStore> kvs,
ByteSpan write_buffer,
size_t flash_write_size_bytes)
: name_(name),
@@ -548,7 +548,7 @@ class BlobStore {
kvs::FlashPartition& partition_;
// checksum_algo_ of nullptr indicates no checksum algorithm.
kvs::ChecksumAlgorithm* const checksum_algo_;
- sync::Borrowable<kvs::KeyValueStore>& kvs_;
+ sync::Borrowable<kvs::KeyValueStore> kvs_;
ByteSpan write_buffer_;
// Size in bytes of flash write operations. This should be chosen to balance
@@ -611,7 +611,7 @@ class BlobStoreBuffer : public BlobStore {
explicit BlobStoreBuffer(std::string_view name,
kvs::FlashPartition& partition,
kvs::ChecksumAlgorithm* checksum_algo,
- sync::Borrowable<kvs::KeyValueStore>& kvs,
+ sync::Borrowable<kvs::KeyValueStore> kvs,
size_t flash_write_size_bytes)
: BlobStore(name,
partition,
diff --git a/pw_bluetooth/BUILD.gn b/pw_bluetooth/BUILD.gn
index 7f7ea2c0d..9ef081ce0 100644
--- a/pw_bluetooth/BUILD.gn
+++ b/pw_bluetooth/BUILD.gn
@@ -80,21 +80,60 @@ if (dir_pw_third_party_emboss != "") {
visibility = [ ":*" ]
}
- emboss_cc_library("emboss_hci") {
+ emboss_cc_library("emboss_hci_common") {
public_configs = [ ":emboss_include_path" ]
- source = "public/pw_bluetooth/hci.emb"
+ source = "public/pw_bluetooth/hci_common.emb"
}
- emboss_cc_library("emboss_vendor") {
+ emboss_cc_library("emboss_hci_commands") {
public_configs = [ ":emboss_include_path" ]
- source = "public/pw_bluetooth/vendor.emb"
- imports = [ "public/pw_bluetooth/hci.emb" ]
- deps = [ ":emboss_hci" ]
+ source = "public/pw_bluetooth/hci_commands.emb"
+ imports = [ "public/pw_bluetooth/hci_common.emb" ]
+ deps = [ ":emboss_hci_common" ]
+ }
+
+ emboss_cc_library("emboss_hci_events") {
+ public_configs = [ ":emboss_include_path" ]
+ source = "public/pw_bluetooth/hci_events.emb"
+ imports = [ "public/pw_bluetooth/hci_common.emb" ]
+ deps = [ ":emboss_hci_common" ]
+ }
+
+ emboss_cc_library("emboss_hci_vendor") {
+ public_configs = [ ":emboss_include_path" ]
+ source = "public/pw_bluetooth/hci_vendor.emb"
+ imports = [ "public/pw_bluetooth/hci_common.emb" ]
+ deps = [ ":emboss_hci_common" ]
+ }
+
+ emboss_cc_library("emboss_hci_test") {
+ public_configs = [ ":emboss_include_path" ]
+ source = "public/pw_bluetooth/hci_test.emb"
+ imports = [ "public/pw_bluetooth/hci_common.emb" ]
+ deps = [ ":emboss_hci_common" ]
+ }
+
+ group("emboss_hci_group") {
+ public_configs = [ ":emboss_include_path" ]
+ public_deps = [
+ ":emboss_hci_commands",
+ ":emboss_hci_common",
+ ":emboss_hci_events",
+ ":emboss_hci_vendor",
+ ]
}
} else {
- group("emboss_hci") {
+ group("emboss_hci_common") {
+ }
+ group("emboss_hci_commands") {
+ }
+ group("emboss_hci_events") {
+ }
+ group("emboss_hci_vendor") {
+ }
+ group("emboss_hci_test") {
}
- group("emboss_vendor") {
+ group("emboss_hci_group") {
}
}
@@ -131,11 +170,12 @@ pw_test("uuid_test") {
pw_test("emboss_test") {
enable_if = dir_pw_third_party_emboss != ""
- configs = [ "$dir_pigweed/third_party/emboss:flags" ]
sources = [ "emboss_test.cc" ]
deps = [
- ":emboss_hci",
- ":emboss_vendor",
+ ":emboss_hci_commands",
+ ":emboss_hci_events",
+ ":emboss_hci_test",
+ ":emboss_hci_vendor",
]
}
diff --git a/pw_bluetooth/api_test.cc b/pw_bluetooth/api_test.cc
index 4fdd04b94..2e00c595d 100644
--- a/pw_bluetooth/api_test.cc
+++ b/pw_bluetooth/api_test.cc
@@ -22,4 +22,4 @@ namespace {
TEST(ApiTest, ApiCompiles) {}
} // namespace
-} // namespace pw::bluetooth \ No newline at end of file
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/emboss_test.cc b/pw_bluetooth/emboss_test.cc
index 9db81fb45..ba7706c6e 100644
--- a/pw_bluetooth/emboss_test.cc
+++ b/pw_bluetooth/emboss_test.cc
@@ -12,8 +12,10 @@
// License for the specific language governing permissions and limitations under
// the License.
#include "gtest/gtest.h"
-#include "pw_bluetooth/hci.emb.h"
-#include "pw_bluetooth/vendor.emb.h"
+#include "pw_bluetooth/hci_commands.emb.h"
+#include "pw_bluetooth/hci_events.emb.h"
+#include "pw_bluetooth/hci_test.emb.h"
+#include "pw_bluetooth/hci_vendor.emb.h"
namespace pw::bluetooth {
namespace {
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/types.h b/pw_bluetooth/public/pw_bluetooth/gatt/types.h
index 1bbe3c6d9..e136f769b 100644
--- a/pw_bluetooth/public/pw_bluetooth/gatt/types.h
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/types.h
@@ -137,4 +137,4 @@ struct Characteristic {
span<const Descriptor> descriptors;
};
-} // namespace pw::bluetooth::gatt \ No newline at end of file
+} // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/hci.emb b/pw_bluetooth/public/pw_bluetooth/hci.emb
deleted file mode 100644
index d781b3f18..000000000
--- a/pw_bluetooth/public/pw_bluetooth/hci.emb
+++ /dev/null
@@ -1,2570 +0,0 @@
-# Copyright 2023 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-# This file contains Emboss definitions for Host Controller Interface packets
-# and types found in the Bluetooth Core Specification. The Emboss compiler is
-# used to generate a C++ header from this file.
-
-[$default byte_order: "LittleEndian"]
-[(cpp) namespace: "pw::bluetooth::emboss"]
-# =========================== Constants =================================
-
-
-enum CodingFormat:
- -- Coding formats from assigned numbers.
- -- (https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface)
- [maximum_bits: 8]
- U_LAW = 0x00
- A_LAW = 0x01
- CVSD = 0x02
- TRANSPARENT = 0x03
- LINEAR_PCM = 0x04
- MSBC = 0x05
- LC3 = 0x06
- G729A = 0x07
- VENDOR_SPECIFIC = 0xFF
-
-
-enum GenericEnableParam:
- -- Binary values that can be generically passed to HCI commands that expect a
- -- 1-octet boolean "enable"/"disable" parameter.
- [maximum_bits: 8]
- DISABLE = 0x00
- ENABLE = 0x01
-
-
-enum InquiryAccessCode:
- -- General- and Device-specific Inquiry Access Codes (DIACs) for use in Inquiry
- -- command LAP fields.
- -- (https://www.bluetooth.com/specifications/assigned-numbers/baseband)
- [maximum_bits: 24]
- GIAC = 0x9E8B33
- -- General Inquiry Access Code
-
- LIAC = 0x9E8B00
- -- Limited Dedicated Inquiry Access Code
-
-
-enum PcmDataFormat:
- -- PCM data formats from assigned numbers.
- -- (https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface)
- [maximum_bits: 8]
- NOT_APPLICABLE = 0x00
- ONES_COMPLEMENT = 0x01
- TWOS_COMPLEMENT = 0x02
- SIGN_MAGNITUDE = 0x03
- UNSIGNED = 0x04
-
-
-enum ScoDataPath:
- [maximum_bits: 8]
- HCI = 0x00
- AUDIO_TEST_MODE = 0xFF
- -- 0x01 - 0xFE specify the logical channel number (vendor specific)
-
-
-enum ConnectionRole:
- [maximum_bits: 8]
- CENTRAL = 0x00
- PERIPHERAL = 0x01
-
-
-enum PageTimeout:
- [maximum_bits: 16]
- MIN = 0x0001
- MAX = 0xFFFF
- DEFAULT = 0x2000
-
-
-enum ScanInterval:
- -- The minimum and maximum range values for Page and Inquiry Scan Interval (in time slices)
- -- Page Scan Interval: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.19)
- -- Inquiry Scan Interval: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.21)
- [maximum_bits: 16]
- MIN = 0x0012
- MAX = 0x1000
-
-
-enum ScanWindow:
- -- The minimum and maximum range valeus for Page and Inquiry Scan Window (in time slices)
- -- Page Scan Window: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.19)
- -- Inquiry Scan Window: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.21)
- [maximum_bits: 16]
- MIN = 0x0011
- MAX = 0x1000
-
-
-enum StatusCode:
- -- HCI Error Codes. Refer to Core Spec v5.0, Vol 2, Part D for definitions and
- -- descriptions. All enum values are in increasing numerical order, however the
- -- values are listed below for clarity.
- [maximum_bits: 8]
- SUCCESS = 0x00
- UNKNOWN_COMMAND = 0x01
- UNKNOWN_CONNECTION_ID = 0x02
- HARDWARE_FAILURE = 0x03
- PAGE_TIMEOUT = 0x04
- AUTHENTICATION_FAILURE = 0x05
- PIN_OR_KEY_MISSING = 0x06
- MEMORY_CAPACITY_EXCEEDED = 0x07
- CONNECTION_TIMEOUT = 0x08
- CONNECTION_LIMIT_EXCEEDED = 0x09
- SYNCHRONOUS_CONNECTION_LIMIT_EXCEEDED = 0x0A
- CONNECTION_ALREADY_EXISTS = 0x0B
- COMMAND_DISALLOWED = 0x0C
- CONNECTION_REJECTED_LIMITED_RESOURCES = 0x0D
- CONNECTION_REJECTED_SECURITY = 0x0E
- CONNECTION_REJECTED_BAD_BD_ADDR = 0x0F
- CONNECTION_ACCEPT_TIMEOUT_EXCEEDED = 0x10
- UNSUPPORTED_FEATURE_OR_PARAMETER = 0x11
- INVALID_HCI_COMMAND_PARAMETERS = 0x12
- REMOTE_USER_TERMINATED_CONNECTION = 0x13
- REMOTE_DEVICE_TERMINATED_CONNECTION_LOW_RESOURCES = 0x14
- REMOTE_DEVICE_TERMINATED_CONNECTION_POWER_OFF = 0x15
- CONNECTION_TERMINATED_BY_LOCAL_HOST = 0x16
- REPEATED_ATTEMPTS = 0x17
- PAIRING_NOT_ALLOWED = 0x18
- UNKNOWN_LMP_PDU = 0x19
- UNSUPPORTED_REMOTE_FEATURE = 0x1A
- SCO_OFFSET_REJECTED = 0x1B
- SCO_INTERVAL_REJECTED = 0x1C
- SCO_AIRMODE_REJECTED = 0x1D
- INVALID_LMP_OR_LL_PARAMETERS = 0x1E
- UNSPECIFIED_ERROR = 0x1F
- UNSUPPORTED_LMP_OR_LL_PARAMETER_VALUE = 0x20
- ROLE_CHANGE_NOT_ALLOWED = 0x21
- LMP_OR_LL_RESPONSE_TIMEOUT = 0x22
- LMP_ERROR_TRANSACTION_COLLISION = 0x23
- LMP_PDU_NOT_ALLOWED = 0x24
- ENCRYPTION_MODE_NOT_ACCEPTABLE = 0x25
- LINK_KEY_CANNOT_BE_CHANGED = 0x26
- REQUESTED_QOS_NOT_SUPPORTED = 0x27
- INSTANT_PASSED = 0x28
- PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED = 0x29
- DIFFERENT_TRANSACTION_COLLISION = 0x2A
- RESERVED_0 = 0x2B
- QOS_UNACCEPTABLE_PARAMETER = 0x2C
- QOS_REJECTED = 0x2D
- CHANNEL_CLASSIFICATION_NOT_SUPPORTED = 0x2E
- INSUFFICIENT_SECURITY = 0x2F
- PARAMETER_OUT_OF_MANDATORY_RANGE = 0x30
- RESERVED_1 = 0x31
- ROLE_SWITCH_PENDING = 0x32
- RESERVED_2 = 0x33
- RESERVED_SLOT_VIOLATION = 0x34
- ROLE_SWITCH_FAILED = 0x35
- EXTENDED_INQUIRY_RESPONSE_TOO_LARGE = 0x36
- SECURE_SIMPLE_PAIRING_NOT_SUPPORTED_BY_HOST = 0x37
- HOST_BUSY_PAIRING = 0x38
- CONNECTION_REJECTED_NO_SUITABLE_CHANNEL_FOUND = 0x39
- CONTROLLER_BUSY = 0x3A
- UNACCEPTABLE_CONNECTION_PARAMETERS = 0x3B
- DIRECTED_ADVERTISING_TIMEOUT = 0x3C
- CONNECTION_TERMINATED_MIC_FAILURE = 0x3D
- CONNECTION_FAILED_TO_BE_ESTABLISHED = 0x3E
- MAC_CONNECTION_FAILED = 0x3F
- COARSE_CLOCK_ADJUSTMENT_REJECTED = 0x40
- # 5.0
- TYPE_0_SUBMAP_NOT_DEFINED = 0x41
- UNKNOWN_ADVERTISING_IDENTIFIER = 0x42
- LIMIT_REACHED = 0x43
- OPERATION_CANCELLED_BY_HOST = 0x44
-
-
-bits ScoPacketType:
- -- Bitmask of SCO packet types.
- # SCO packet types
- 0 [+1] Flag hv1
- $next [+1] Flag hv2
- $next [+1] Flag hv3
- # eSCO packet types
- $next [+1] Flag ev3
- $next [+1] Flag ev4
- $next [+1] Flag ev5
- $next [+1] Flag not_2_ev3
- $next [+1] Flag not_3_ev3
- $next [+1] Flag not_2_ev5
- $next [+1] Flag not_3_ev5
- $next [+6] UInt padding
-
-
-bits PacketType:
- -- Bitmask values for supported Packet Types
- -- Used for HCI_Create_Connection and HCI_Change_Connection_Packet_Type
- -- All other bits reserved for future use.
- 1 [+1] Flag disable_2_dh1
- 2 [+1] Flag disable_3_dh1
- 3 [+1] Flag enable_dm1 # Note: always on in >= v1.2
- 4 [+1] Flag enable_dh1
- 8 [+1] Flag disable_2_dh3
- 9 [+1] Flag disable_3_dh3
- 10 [+1] Flag enable_dm3
- 11 [+1] Flag enable_dh3
- 12 [+1] Flag disable_2_dh5
- 13 [+1] Flag disable_3_dh5
- 14 [+1] Flag enable_dm5
- 15 [+1] Flag enable_dh5
-
-
-enum PageScanRepetitionMode:
- -- The page scan repetition mode, representing a maximum time between Page Scans.
- -- (See Core Spec v5.0, Volume 2, Part B, Section 8.3.1)
- [maximum_bits: 8]
- R0_ = 0x00 # Continuous Scan
- R1_ = 0x01 # <= 1.28s
- R2_ = 0x02 # <= 2.56s
-
-
-bits ClockOffset:
- -- Clock Offset. The lower 15 bits are set to the clock offset as retrieved
- -- by an Inquiry. The highest bit is set to 1 if the rest of this parameter
- -- is valid.
- 15 [+1] Flag valid
- if valid:
- 0 [+15] UInt clock_offset
-
-
-struct BdAddr:
- -- Bluetooth Device Address
- 0 [+6] UInt bd_addr
-
-
-enum IoCapability:
- -- All other values reserved for future use.
- [maximum_bits: 8]
- DISPLAY_ONLY = 0x00
- DISPLAY_YES_NO = 0x01
- KEYBOARD_ONLY = 0x02
- NO_INPUT_NO_OUTPUT = 0x03
-
-
-enum OobDataPresent:
- -- Whether there is out-of-band data present, and what type.
- -- All other values reserved for future use.
- [maximum_bits: 8]
- NOT_PRESENT = 0x00
- P192_ = 0x01
- P256_ = 0x02
- P192_AND_P256 = 0x03
-
-# inclusive-language: disable
-
-
-enum AuthenticationRequirements:
- -- All options without MITM do not require MITM protection, and a numeric
- -- comparison with automatic accept is allowed.
- -- All options with MITM do require MITM protection, and IO capabilities should
- -- be used to determine the authentication procedure.
- [maximum_bits: 8]
- NO_BONDING = 0x00
- MITM_NO_BONDING = 0x01
- DEDICATED_BONDING = 0x02
- MITM_DEDICATED_BONDING = 0x03
- GENERAL_BONDING = 0x04
- MITM_GENERAL_BONDING = 0x05
-
-# inclusive-language: enable
-
-
-bits ScanEnableBits:
- -- Bitmask Values for the Scan_Enable parameter in a
- -- HCI_(Read,Write)_Scan_Enable command.
- 0 [+1] Flag inquiry
- -- Inquiry scan enabled
-
- $next [+1] Flag page
- -- Page scan enabled
-
- $next [+6] UInt padding
-
-
-enum InquiryScanType:
- [maximum_bits: 8]
- STANDARD = 0x00
- -- Standard scan (Default) (Mandatory)
-
- INTERLACED = 0x01
-
-
-struct LocalName:
- 0 [+248] UInt:8[248] local_name
-
-
-struct ExtendedInquiryResponse:
- 0 [+240] UInt:8[240] extended_inquiry_response
-
-
-enum LEExtendedDuplicateFilteringOption:
- -- Possible values that can be used for the |filter_duplicates| parameter in a
- -- HCI_LE_Set_Extended_Scan_Enable command.
- [maximum_bits: 8]
- DISABLED = 0x00
- ENABLED = 0x01
- ENABLED_RESET_FOR_EACH_SCAN_PERIOD = 0x02
- -- Duplicate advertisements in a single scan period should not be sent to the
- -- Host in advertising report events; this setting shall only be used if the
- -- Period parameter is non-zero.
-
-
-enum MajorDeviceClass:
- [maximum_bits: 5]
- MISCELLANEOUS = 0x00
- COMPUTER = 0x01
- PHONE = 0x02
- LAN = 0x03
- AUDIO_VIDEO = 0x04
- PERIPHERAL = 0x05
- IMAGING = 0x06
- WEARABLE = 0x07
- TOY = 0x08
- HEALTH = 0x09
- UNCATEGORIZED = 0x1F
-
-
-bits MajorServiceClasses:
- 0 [+1] Flag limited_discoverable_mode
- $next [+1] Flag le_audio
- $next [+1] Flag reserved
- $next [+1] Flag positioning
- $next [+1] Flag networking
- $next [+1] Flag rendering
- $next [+1] Flag capturing
- $next [+1] Flag object_transfer
- $next [+1] Flag audio
- $next [+1] Flag telephony
- $next [+1] Flag information
-
-
-enum ComputerMinorDeviceClass:
- [maximum_bits: 6]
- UNCATEGORIZED = 0x00
- DESKTOP_WORKSTATION = 0x01
- SERVER_CLASS = 0x02
- LAPTOP = 0x03
- HANDHELD_PC = 0x04
- PALM_SIZE_PC = 0x05
- WEARABLE = 0x06
- TABLET = 0x07
-
-
-enum PhoneMinorDeviceClass:
- [maximum_bits: 6]
- UNCATEGORIZED = 0x00
- CELLULAR = 0x01
- CORDLESS = 0x02
- SMARTPHONE = 0x03
- WIRED_MODEM_OR_VOID_GATEWAY = 0x04
- COMMON_ISDN_ACCESS = 0x05
-
-
-enum LANMinorDeviceClass:
- [maximum_bits: 6]
- FULLY_AVAILABLE = 0x00
- UTILIZED_1_TO_17 = 0x08
- UTILIZED_17_TO_33 = 0x10
- UTILIZED_33_TO_50 = 0x18
- UTILIZED_50_TO_67 = 0x20
- UTILIZED_67_TO_83 = 0x28
- UTILIZED_83_TO_99 = 0x30
- NO_SERVICE_AVAILABLE = 0x38
-
-
-enum AudioVideoMinorDeviceClass:
- [maximum_bits: 6]
- UNCATEGORIZED = 0x00
- WEARABLE_HEADSET_DEVICE = 0x01
- HANDS_FREE_DEVICE = 0x02
- RESERVED_0 = 0x03
- MICROPHONE = 0x04
- LOUDSPEAKER = 0x05
- HEADPHONES = 0x06
- PORTABLE_AUDIO = 0x07
- CAR_AUDIO = 0x08
- SET_TOP_BOX = 0x09
- HIFI_AUDIO_DEVICE = 0x0A
- VCR = 0x0B
- VIDEO_CAMERA = 0x0C
- CAMCORDER = 0x0D
- VIDEO_MONITOR = 0x0E
- VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x0F
- VIDEO_CONFERENCING = 0x10
- RESERVED_1 = 0x11
- GAMING_TOY = 0x12
-
-
-enum PeripheralMinorDeviceClass0:
- [maximum_bits: 4]
- UNCATEGORIZED = 0x00
- JOYSTICK = 0x01
- GAMEPAD = 0x02
- REMOTE_CONTROL = 0x03
- SENSING_DEVICE = 0x04
- DIGITIZER_TABLET = 0x05
- CARD_READER = 0x06
- DIGITAL_PEN = 0x07
- HANDHELD_SCANNER = 0x08
- HANDHELD_GESTURAL_INPUT_DEVICE = 0x09
-
-
-enum PeripheralMinorDeviceClass1:
- [maximum_bits: 2]
- UNCATEGORIZED = 0x00
- KEYBOARD = 0x01
- POINTING_DEVICE = 0x02
- COMBO_KEYBOARD_POINTING_DEVICE = 0x03
-
-
-bits PeripheralMinorDeviceClass:
- 0 [+4] PeripheralMinorDeviceClass0 device_class_0
- $next [+2] PeripheralMinorDeviceClass1 device_class_1
-
-
-enum ImagingMinorDeviceClass:
- [maximum_bits: 2]
- UNCATEGORIZED = 0x00
-
-
-bits ImagingMinorDeviceClassBits:
- 0 [+2] ImagingMinorDeviceClass device_class
- $next [+1] Flag display
- $next [+1] Flag camera
- $next [+1] Flag scanner
- $next [+1] Flag printer
-
-
-enum WearableMinorDeviceClass:
- [maximum_bits: 6]
- WRISTWATCH = 0x01
- PAGER = 0x02
- JACKET = 0x03
- HELMET = 0x04
- GLASSES = 0x05
-
-
-enum ToyMinorDeviceClass:
- [maximum_bits: 6]
- ROBOT = 0x01
- VEHICLE = 0x02
- DOLL = 0x03
- CONTROLLER = 0x04
- GAME = 0x05
-
-
-enum HealthMinorDeviceClass:
- [maximum_bits: 6]
- UNDEFINED = 0x00
- BLOOD_PRESSURE_MONITOR = 0x01
- THERMOMETER = 0x02
- WEIGHING_SCALE = 0x03
- GLUCOSE_METER = 0x04
- PULSE_OXIMETER = 0x05
- HEART_PULSE_RATE_MONITOR = 0x06
- HEALTH_DATA_DISPLAY = 0x07
- STEP_COUNTER = 0x08
- BODY_COMPOSITION_ANALYZER = 0x09
- PEAK_FLOW_MONITOR = 0x0A
- MEDICATION_MONITOR = 0x0B
- KNEE_PROSTHESIS = 0x0C
- ANKLE_PROSTHESIS = 0x0D
- GENERIC_HEALTH_MANAGER = 0x0E
- PERSONAL_MOBILITY_DEVICE = 0x0F
-
-
-bits ClassOfDevice:
- -- Defined in Assigned Numbers for the Baseband
- -- https://www.bluetooth.com/specifications/assigned-numbers/baseband
- 0 [+2] UInt zero
- [requires: this == 0]
-
- if major_device_class == MajorDeviceClass.COMPUTER:
- 2 [+6] ComputerMinorDeviceClass computer_minor_device_class
-
- if major_device_class == MajorDeviceClass.PHONE:
- 2 [+6] PhoneMinorDeviceClass phone_minor_device_class
-
- if major_device_class == MajorDeviceClass.LAN:
- 2 [+6] LANMinorDeviceClass lan_minor_device_class
-
- if major_device_class == MajorDeviceClass.AUDIO_VIDEO:
- 2 [+6] AudioVideoMinorDeviceClass audio_video_minor_device_class
-
- if major_device_class == MajorDeviceClass.PERIPHERAL:
- 2 [+6] PeripheralMinorDeviceClass peripheral_minor_device_class
-
- if major_device_class == MajorDeviceClass.IMAGING:
- 2 [+6] ImagingMinorDeviceClassBits imaging_minor_device_class
-
- if major_device_class == MajorDeviceClass.WEARABLE:
- 2 [+6] WearableMinorDeviceClass wearable_minor_device_class
-
- if major_device_class == MajorDeviceClass.TOY:
- 2 [+6] ToyMinorDeviceClass toy_minor_device_class
-
- if major_device_class == MajorDeviceClass.HEALTH:
- 2 [+6] HealthMinorDeviceClass health_minor_device_class
-
- 8 [+5] MajorDeviceClass major_device_class
- $next [+11] MajorServiceClasses major_service_classes
-
-
-enum LEPeriodicAdvertisingCreateSyncUseParams:
- [maximum_bits: 1]
-
- USE_PARAMS = 0x00
- -- Use the Advertising_SID, Advertiser_Address_Type, and Adertiser_Address parameters to
- -- determine which advertiser to listen to.
-
- USE_PERIODIC_ADVERTISER_LIST = 0x01
- -- Use the Periodic Advertiser List to determine which advertiser to listen to.
-
-
-bits LEPeriodicAdvertisingCreateSyncOptions:
- -- First parameter to the LE Periodic Advertising Create Sync command
-
- 0 [+1] LEPeriodicAdvertisingCreateSyncUseParams advertiser_source
-
- $next [+1] Flag enable_reporting
- -- 0: Reporting initially enabled
- -- 1: Reporting initially disabled
-
- $next [+1] Flag enable_duplicate_filtering
- -- 0: Duplicate filtering initially disabled
- -- 1: Duplicate filtering initially enabled
-
- $next [+5] UInt padding
- -- Reserved for future use
-
-
-enum LEPeriodicAdvertisingAddressType:
- -- Possible values that can be specified for the |advertiser_address_type| in an LE Periodic
- -- Advertising Create Sync command.
- [maximum_bits: 8]
- PUBLIC = 0x00
- -- Public Device Address or Public Identity Address
-
- RANDOM = 0x01
- -- Random Device Address or Random (static) Identity Address
-
-
-bits LEPeriodicAdvertisingSyncCTEType:
- -- Bit definitions for a |sync_cte_type| field in an LE Periodic Advertising Create Sync command
-
- 0 [+1] Flag dont_sync_aoa
- -- Do not sync to packets with an AoA Constant Tone Extension
-
- $next [+1] Flag dont_sync_aod_1us
- -- Do not sync to packets with an AoD Constant Tone Extension with 1 microsecond slots
-
- $next [+1] Flag dont_sync_aod_2us
- -- Do not sync to packets with an AoD Constant Tone Extension with 2 microsecond slots
-
- $next [+1] Flag dont_sync_type_3
- -- Do not sync to packets with a typoe 3 Constant Tone Extension (currently reserved for future
- -- use)
-
- $next [+1] Flag dont_sync_without_cte
- -- Do not sync to packets without a Constant Tone Extension
-
- $next [+3] UInt padding
- -- Reserved for future use
-
-
-enum LEAddressType:
- -- Possible values that can be reported for the |address_type| parameter in a LE
- -- Advertising Report event.
-
- [maximum_bits: 8]
-
- PUBLIC = 0x00
- -- Public device address (default)
-
- RANDOM = 0x01
- -- Random device address
-
- PUBLIC_IDENTITY = 0x02
- -- Public Identity Address (Corresponds to Resolved Private Address)
-
- RANDOM_IDENTITY = 0x03
- -- Random (static) Identity Address (Corresponds to Resolved Private Address)
-
- RANDOM_UNRESOLVED = 0xFE
- -- This is a special value used in LE Extended Advertising Report events to
- -- indicate a random address that the controller was unable to resolve.
-
- ANONYMOUS = 0xFF
- -- This is a special value that is only used in LE Directed Advertising Report
- -- events.
- -- Meaning: No address provided (anonymous advertisement)
-
-
-enum LEOwnAddressType:
- -- Possible values that can be used for the |own_address_type| parameter in various HCI commands
-
- [maximum_bits: 8]
-
- PUBLIC = 0x00
- -- Public Device Address
-
- RANDOM = 0x01
- -- Random Device Address
-
- PRIVATE_DEFAULT_TO_PUBLIC = 0x02
- -- Controller generates the Resolvable Private Address based on the local IRK from the resolving
- -- list. If the resolving list contains no matching entry, then use the public address.
-
- PRIVATE_DEFAULT_TO_RANDOM = 0x03
- -- Controller generates the Resolvable Private Address based on the local IRK from the resolving
- -- list. If the resolving list contains no matching entry, then use the random address from
- -- LE_Set_Random_Address.
-
-
-enum LEPeerAddressType:
- -- Possible values that can be used for the address_type parameters in various
- -- HCI commands
- [maximum_bits: 8]
- PUBLIC = 0x00
- RANDOM = 0x01
- ANONYMOUS = 0xFF
-
-
-enum LEScanType:
- -- Possible values that can be used for the |scan_type| parameter in various LE HCI commands.
- [maximum_bits: 8]
- PASSIVE = 0x00
- -- Passive Scanning. No scanning PDUs shall be sent (default)
-
- ACTIVE = 0x01
- -- Active scanning. Scanning PDUs may be sent.
-
-
-enum LEScanFilterPolicy:
- -- Possible values that can be used for the |filter_policy| parameter in various LE HCI commands
- [maximum_bits: 8]
- BASIC_UNFILTERED = 0x00
- BASIC_FILTERED = 0x01
- EXTENDED_UNFILTERED = 0x02
- EXTENDED_FILTERED = 0x03
-
-
-bits LEPHYBits:
- 0 [+1] Flag le_1m
- -- Scan advertisements on the LE 1M PHY
-
- $next [+1] Flag padding1
- -- Reserved for future use
-
- $next [+1] Flag le_coded
- -- Scan advertisements on the LE Coded PHY
-
- $next [+5] UInt padding2
- -- Reserved for future use
-
-
-enum LEPrivacyMode:
- -- Possible values for the |privacy_mode| parameter in an LE Set Privacy Mode
- -- command
- [maximum_bits: 8]
- NETWORK = 0x00
- -- Use Network Privacy Mode for this peer device (default).
-
- DEVICE = 0x01
- -- Use Device Privacy Mode for this peer device.
-
-
-enum InquiryMode:
- [maximum_bits: 8]
- STANDARD = 0x00
- -- Standard Inquiry Result format (default)
-
- RSSI = 0x01
- -- Inquiry Result format with RSSI
-
- EXTENDED = 0x02
- -- Inquiry Result format with RSSI or EIR format
-
-
-enum PageScanType:
- [maximum_bits: 8]
- STANDARD_SCAN = 0x00
- -- Standard scan (default) (mandatory)
-
- INTERLACED_SCAN = 0x01
- -- Interlaced scan (optional)
-
-
-bits LEEventMask:
- 0 [+1] Flag le_connection_complete
- $next [+1] Flag le_advertising_report
- $next [+1] Flag le_connection_update_complete
- $next [+1] Flag le_read_remote_features_complete
- $next [+1] Flag le_long_term_key_request
- $next [+1] Flag le_remote_connection_parameter_request
- $next [+1] Flag le_data_length_change
- $next [+1] Flag le_read_local_p256_public_key_complete
- $next [+1] Flag le_generate_dhkey_complete
- $next [+1] Flag le_enhanced_connection_complete
- $next [+1] Flag le_directed_advertising_report
- $next [+1] Flag le_phy_update_complete
- $next [+1] Flag le_extended_advertising_report
- $next [+1] Flag le_periodic_advertising_sync_established
- $next [+1] Flag le_periodic_advertising_report
- $next [+1] Flag le_periodic_advertising_sync_lost
- $next [+1] Flag le_extended_scan_timeout
- $next [+1] Flag le_extended_advertising_set_terminated
- $next [+1] Flag le_scan_request_received
- $next [+1] Flag le_channel_selection_algorithm
- $next [+1] Flag le_connectionless_iq_report
- $next [+1] Flag le_connection_iq_report
- $next [+1] Flag le_cte_request_failed
- $next [+1] Flag le_periodic_advertising_sync_transfer_received_event
- $next [+1] Flag le_cis_established_event
- $next [+1] Flag le_cis_request_event
- $next [+1] Flag le_create_big_complete_event
- $next [+1] Flag le_terminate_big_complete_event
- $next [+1] Flag le_big_sync_established_event
- $next [+1] Flag le_big_sync_lost_event
- $next [+1] Flag le_request_peer_sca_complete_event
- $next [+1] Flag le_path_loss_threshold_event
- $next [+1] Flag le_transmit_power_reporting_event
- $next [+1] Flag le_biginfo_advertising_report_event
- $next [+1] Flag le_subrate_change_event
-
-
-enum LEAdvertisingType:
- [maximum_bits: 8]
- CONNECTABLE_AND_SCANNABLE_UNDIRECTED = 0x00
- -- ADV_IND
-
- CONNECTABLE_HIGH_DUTY_CYCLE_DIRECTED = 0x01
- -- ADV_DIRECT_IND
-
- SCANNABLE_UNDIRECTED = 0x02
- -- ADV_SCAN_IND
-
- NOT_CONNECTABLE_UNDIRECTED = 0x03
- -- ADV_NONCONN_IND
-
- CONNECTABLE_LOW_DUTY_CYCLE_DIRECTED = 0x04
- -- ADV_DIRECT_IND
-
-
-bits LEAdvertisingChannels:
- 0 [+1] Flag channel_37
- $next [+1] Flag channel_38
- $next [+1] Flag channel_39
-
-
-enum LEAdvertisingFilterPolicy:
- [maximum_bits: 8]
-
- ALLOW_ALL = 0x00
- -- Process scan and connection requests from all devices (i.e., the Filter
- -- Accept List is not in use) (default).
-
- ALLOW_ALL_CONNECTIONS_AND_USE_FILTER_ACCEPT_LIST_FOR_SCANS = 0x01
- -- Process connection requests from all devices and scan requests only from
- -- devices that are in the Filter Accept List.
-
- ALLOW_ALL_SCANS_AND_USE_FILTER_ACCEPT_LIST_FOR_CONNECTIONS = 0x02
- -- Process scan requests from all devices and connection requests only from
- -- devices that are in the Filter Accept List.
-
- ALLOW_FILTER_ACCEPT_LIST_ONLY = 0x03
- -- Process scan and connection requests only from devices in the Filter
- -- Accept List.
-
-
-enum LESetExtendedAdvDataOp:
- -- Potential values for the Operation parameter in a HCI_LE_Set_Extended_Advertising_Data command.
- [maximum_bits: 8]
- INTERMEDIATE_FRAGMENT = 0x00
- -- Intermediate fragment of fragmented extended advertising data.
-
- FIRST_FRAGMENT = 0x01
- -- First fragment of fragmented extended advertising data.
-
- LAST_FRAGMENT = 0x02
- -- Last fragment of fragmented extended advertising data.
-
- COMPLETE = 0x03
- -- Complete extended advertising data.
-
- UNCHANGED_DATA = 0x04
- -- Unchanged data (just update the Advertising DID)
-
-
-enum LEExtendedAdvFragmentPreference:
- -- Potential values for the Fragment_Preference parameter in a
- -- HCI_LE_Set_Extended_Advertising_Data command.
- [maximum_bits: 8]
- MAY_FRAGMENT = 0x00
- -- The Controller may fragment all Host advertising data
-
- SHOULD_NOT_FRAGMENT = 0x01
- -- The Controller should not fragment or should minimize fragmentation of Host advertising data
-
-
-enum FlowControlMode:
- [maximum_bits: 8]
- PACKET_BASED = 0x00
- DATA_BLOCK_BASED = 0x01
-
-
-bits EventMaskPage2:
- 8 [+1] Flag number_of_completed_data_blocks_event
- 14 [+1] Flag triggered_clock_capture_event
- 15 [+1] Flag synchronization_train_complete_event
- 16 [+1] Flag synchronization_train_received_event
- 17 [+1] Flag connectionless_peripheral_broadcast_receive_event
- 18 [+1] Flag connectionless_peripheral_broadcast_timeout_event
- 19 [+1] Flag truncated_page_complete_event
- 20 [+1] Flag peripheral_page_response_timeout_event
- 21 [+1] Flag connectionless_peripheral_broadcast_channel_map_event
- 22 [+1] Flag inquiry_response_notification_event
- 23 [+1] Flag authenticated_payload_timeout_expired_event
- 24 [+1] Flag sam_status_change_event
- 25 [+1] Flag encryption_change_event_v2
-
-
-enum LinkType:
- [maximum_bits: 8]
- SCO = 0x00
- ACL = 0x01
- ESCO = 0x02
-
-
-enum EncryptionStatus:
- OFF = 0x00
- ON_WITH_E0_FOR_BREDR_OR_AES_FOR_LE = 0x01
- ON_WITH_AES_FOR_BREDR = 0x03
-
-
-bits LmpFeatures(page: UInt:8):
- -- Bit mask of Link Manager Protocol features.
- if page == 0:
- 0 [+1] Flag three_slot_packets
- 1 [+1] Flag five_slot_packets
- 2 [+1] Flag encryption
- 3 [+1] Flag slot_offset
- 4 [+1] Flag timing_accuracy
- 5 [+1] Flag role_switch
- 6 [+1] Flag hold_mode
- 7 [+1] Flag sniff_mode
- # 8: previously used
- 9 [+1] Flag power_control_requests
- 10 [+1] Flag channel_quality_driven_data_rate
- 11 [+1] Flag sco_link
- 12 [+1] Flag hv2_packets
- 13 [+1] Flag hv3_packets
- 14 [+1] Flag mu_law_log_synchronous_data
- 15 [+1] Flag a_law_log_synchronous_data
- 16 [+1] Flag cvsd_synchronous_data
- 17 [+1] Flag paging_parameter_negotiation
- 18 [+1] Flag power_control
- 19 [+1] Flag transparent_synchronous_data
- 20 [+3] UInt flow_control_lag
- 23 [+1] Flag broadcast_encryption
- # 24: reserved for future use
- 25 [+1] Flag enhanced_data_rate_acl_2_mbs_mode
- 26 [+1] Flag enhanced_data_rate_acl_3_mbs_mode
- 27 [+1] Flag enhanced_inquiry_scan
- 28 [+1] Flag interlaced_inquiry_scan
- 29 [+1] Flag interlaced_page_scan
- 30 [+1] Flag rssi_with_inquiry_results
- 31 [+1] Flag extended_sco_link_ev3_packets
- 32 [+1] Flag ev4_packets
- 33 [+1] Flag ev5_packets
- # 34: reserved for future use
- 35 [+1] Flag afh_capable_peripheral
- 36 [+1] Flag afh_classification_peripheral
- 37 [+1] Flag bredr_not_supported
- 38 [+1] Flag le_supported_controller
- 39 [+1] Flag three_slot_enhanced_data_rate_acl_packets
- 40 [+1] Flag five_slot_enhanced_data_rate_acl_packets
- 41 [+1] Flag sniff_subrating
- 42 [+1] Flag pause_encryption
- 43 [+1] Flag afh_capable_central
- 44 [+1] Flag afh_classification_central
- 45 [+1] Flag enhanced_data_rate_esco_2_mbs_mode
- 46 [+1] Flag enhanced_data_rate_esco_3_mbs_mode
- 47 [+1] Flag three_slot_enhanced_data_rate_esco_packets
- 48 [+1] Flag extended_inquiry_response
- 49 [+1] Flag simultaneous_le_and_bredr_to_same_device_capable_controller
- # 50: reserved for future use
- 51 [+1] Flag secure_simple_pairing_controller_support
- 52 [+1] Flag encapsulated_pdu
- 53 [+1] Flag erroneous_data_reporting
- 54 [+1] Flag non_flushable_packet_boundary_flag
- # 55: reserved for future use
- 56 [+1] Flag hci_link_supervision_timeout_changed_event
- 57 [+1] Flag variable_inquiry_tx_power_level
- 58 [+1] Flag enhanced_power_control
- # 59-62: reserved for future use
- 63 [+1] Flag extended_features
-
- if page == 1:
- 0 [+1] Flag secure_simple_pairing_host_support
- 1 [+1] Flag le_supported_host
- # 2: previously used
- 3 [+1] Flag secure_connection_host_support
-
- if page == 2:
- 0 [+1] Flag connectionless_peripheral_broadcast_transmitter_operation
- 1 [+1] Flag connectionless_peripheral_broadcast_receiver_operation
- 2 [+1] Flag synchronization_train
- 3 [+1] Flag synchronization_scan
- 4 [+1] Flag hci_inquiry_response_notification_event
- 5 [+1] Flag generalized_interlaced_scan
- 6 [+1] Flag coarse_clock_adjustment
- # 7: reserved for future use
- 8 [+1] Flag secure_connections_controller_support
- 9 [+1] Flag ping
- 10 [+1] Flag slot_availability_mask
- 11 [+1] Flag train_nudging
-
-
-enum LEClockAccuracy:
- -- Possible values that can be reported for the |central_clock_accuracy| and
- -- |advertiser_clock_accuracy| parameters.
- [maximum_bits: 8]
- PPM_500 = 0x00
- PPM_250 = 0x01
- PPM_150 = 0x02
- PPM_100 = 0x03
- PPM_75 = 0x04
- PPM_50 = 0x05
- PPM_30 = 0x06
- PPM_20 = 0x07
-
-# ========================= HCI packet headers ==========================
-
-
-bits OpCodeBits:
- # Emboss currently lacks support for default field values and cross-type integral equality.
- # (https://github.com/google/emboss/issues/21)
- # (https://github.com/google/emboss/issues/23)
- # Upon the addition of these features, we will transition OpCodeBits to be a parameterized
- # field which defaults for each HCI packet type to its corresponding OpCode.
- 0 [+10] UInt ocf
- $next [+6] UInt ogf
-
-
-struct CommandHeader:
- -- HCI Command packet header.
- 0 [+2] OpCodeBits opcode
- $next [+1] UInt parameter_total_size
-
-
-struct EventHeader:
- -- HCI Event packet header.
- 0 [+1] UInt event_code
- $next [+1] UInt parameter_total_size
-
-# ========================= HCI Command packets =========================
-# Core Spec v5.3 Vol 4, Part E, Section 7
-
-
-struct InquiryCommand:
- -- Inquiry Command (v1.1) (BR/EDR)
- --
- -- Note: NO Command Complete; Sends Inquiry Complete at the end of the
- -- inquiry to indicate it's completion. No Inquiry Complete event is sent if
- -- Inquiry is cancelled.
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+3] InquiryAccessCode lap
- -- LAP (Lower Address Part)
- -- In the range 0x9E8B00 - 0x9E8B3F, defined by the Bluetooth SIG in
- -- Baseband Assigned Numbers.
-
- $next [+1] UInt inquiry_length
- -- Time before the inquiry is halted. Defined in 1.28s units.
- -- Range: 0x01 to kInquiryLengthMax in hci_constants.h
-
- $next [+1] UInt num_responses
- -- Maximum number of responses before inquiry is halted.
- -- Set to 0x00 for unlimited.
-
-
-struct InquiryCancelCommand:
- -- Inquiry Cancel Command (v1.1) (BR/EDR)
- -- No command parameters
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct CreateConnectionCommand:
- -- Create Connection (v1.1) (BR/EDR)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Connection Complete event will indicate that this command has been
- -- completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- BD_ADDR of the device to be connected
-
- $next [+2] PacketType packet_type
- -- Mask of allowable packet types.
-
- $next [+1] PageScanRepetitionMode page_scan_repetition_mode
- -- The Page Scan Repetition Mode of the remote device as retrieved by Inquiry.
-
- $next [+1] UInt reserved
- [requires: this == 0]
-
- $next [+2] ClockOffset clock_offset
- -- Clock Offset. The lower 15 bits are set to the clock offset as retrieved
- -- by an Inquiry. The highest bit is set to 1 if the rest of this parameter
- -- is valid.
-
- $next [+1] GenericEnableParam allow_role_switch
- -- Allow Role Switch.
- -- Allowed values:
- -- 0x00 - No role switch allowed, this device will be the central
- -- 0x01 - Role switch allowed, this device may become peripheral during
- -- connection setup
-
-
-struct DisconnectCommand:
- -- Disconnect Command (v1.1) (BR/EDR & LE)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Disconnection Complete event will indicate that this command has been
- -- completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
-
- $next [+1] StatusCode reason
- -- Reason for the disconnect.
-
-
-struct CreateConnectionCancelCommand:
- -- Create Connection Cancel (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- BD_ADDR of the Create Connection Command request
-
-
-struct AcceptConnectionRequestCommand:
- -- Accept Connection Request (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The 48-bit BD_ADDR of the remote device requesting the connection.
-
- $next [+1] ConnectionRole role
-
-
-struct RejectConnectionRequestCommand:
- -- Reject Connection Request (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The 48-bit BD_ADDR of the remote device requesting the connection.
-
- $next [+1] StatusCode reason
- -- Must be one of CONNECTION_REJECTED* from StatusCode in this file
-
-
-struct LinkKey:
- 0 [+16] UInt:8[16] value
-
-
-struct LinkKeyRequestReplyCommand:
- -- Link Key Request Reply Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The 48-bit BD_ADDR of the remote device requesting the connection.
-
- let bredr_link_key_size = LinkKey.$size_in_bytes
- $next [+bredr_link_key_size] LinkKey link_key
- -- Link key to use for the connection with the peer device.
-
-
-struct LinkKeyRequestNegativeReplyCommand:
- -- Link Key Request Negative Reply Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- BD_ADDR of the peer device that the host does not have a link key for.
-
-
-struct AuthenticationRequestedCommand:
- -- Authentication Requested Command (v1.1) (BR/EDR)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Authentication Complete event will indicate that this command has been
- -- completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
- -- Must be the handle of a connected ACL-U logical link.
-
-
-struct SetConnectionEncryptionCommand:
- -- Set Connection Encryption Command (v1.1) (BR/EDR)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Encryption Change event will indicate that this command has been completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
- -- Must be the handle of a connected ACL-U logical link.
-
- $next [+1] GenericEnableParam encryption_enable
- -- Whether link level encryption should be turned on or off.
-
-
-struct RemoteNameRequestCommand:
- -- Remote Name Request Command (v1.1) (BR/EDR)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Remote Name Request Complete event will indicate that this command has been
- -- completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- Address of the device whose name is to be requested.
-
- $next [+1] PageScanRepetitionMode page_scan_repetition_mode
- -- Page Scan Repetition Mode of the device, obtained by Inquiry.
-
- $next [+1] UInt reserved
- [requires: this == 0]
-
- $next [+2] ClockOffset clock_offset
- -- Clock offset. The lower 15 bits of this represent bits 14-2
- -- of CLKNPeripheral-CLK, and the highest bit is set when the other
- -- bits are valid.
-
-
-struct ReadRemoteSupportedFeaturesCommand:
- -- Read Remote Supported Features Command (v1.1) (BR/EDR)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Read Remote Supported Features Complete event will indicate that this
- -- command has been completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
- -- Must be the handle of a connected ACL-U logical link.
-
-
-struct ReadRemoteExtendedFeaturesCommand:
- -- Read Remote Extended Features Command (v1.2) (BR/EDR)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Read Remote Extended Features Complete event will indicate that this
- -- command has been completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
- -- Must be the handle of a connected ACL-U logical link.
-
- $next [+1] UInt page_number
- -- Page of features to read.
- -- Values:
- -- - 0x00 standard features as if requested by Read Remote Supported Features
- -- - 0x01-0xFF the corresponding features page (see Vol 2, Part C, Sec 3.3).
-
-
-struct ReadRemoteVersionInfoCommand:
- -- Read Remote Version Information Command (v1.1) (BR/EDR & LE)
- --
- -- NOTE on ReturnParams: No Command Complete event will be sent by the
- -- Controller to indicate that this command has been completed. Instead, the
- -- Read Remote Version Information Complete event will indicate that this
- -- command has been completed.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
-
-
-struct RejectSynchronousConnectionRequestCommand:
- -- Reject Synchronous Connection Command (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- Address of the remote device that sent the request.
-
- $next [+1] StatusCode reason
- -- Reason the connection request was rejected.
-
-
-struct IoCapabilityRequestReplyCommand:
- -- IO Capability Request Reply Command (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The BD_ADDR of the remote device involved in simple pairing process
-
- $next [+1] IoCapability io_capability
- -- The IO capabilities of this device.
-
- $next [+1] OobDataPresent oob_data_present
- -- Whether there is out-of-band data present, and what type.
-
- $next [+1] AuthenticationRequirements authentication_requirements
- -- Authentication requirements of the host.
-
-
-struct UserConfirmationRequestReplyCommand:
- -- User Confirmation Request Reply Command (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The BD_ADDR of the remote device involved in simple pairing process
-
-
-struct UserConfirmationRequestNegativeReplyCommand:
- -- User Confirmation Request Negative Reply Command (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The BD_ADDR of the remote device involved in simple pairing process
-
-
-struct UserPasskeyRequestReplyCommand:
- -- User Passkey Request Reply Command (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The BD_ADDR of the remote device involved in simple pairing process
-
- $next [+4] UInt numeric_value
- -- Numeric value (passkey) entered by user.
- [requires: 0 <= this <= 999999]
-
-
-struct SynchronousConnectionParameters:
- -- Enhanced Setup Synchronous Connection Command (CSA2) (BR/EDR)
-
- struct VendorCodingFormat:
- 0 [+1] CodingFormat coding_format
- $next [+2] UInt company_id
- -- See assigned numbers.
-
- $next [+2] UInt vendor_codec_id
- -- Shall be ignored if |coding_format| is not VENDOR_SPECIFIC.
-
- enum ScoRetransmissionEffort:
- [maximum_bits: 8]
- NONE = 0x00
- -- SCO or eSCO
-
- POWER_OPTIMIZED = 0x01
- -- eSCO only
-
- QUALITY_OPTIMIZED = 0x02
- -- eSCO only
-
- DONT_CARE = 0xFF
- -- SCO or eSCO
-
- 0 [+4] UInt transmit_bandwidth
- -- Transmit bandwidth in octets per second.
-
- $next [+4] UInt receive_bandwidth
- -- Receive bandwidth in octets per second.
-
- let vcf_size = VendorCodingFormat.$size_in_bytes
-
- $next [+vcf_size] VendorCodingFormat transmit_coding_format
- -- Local Controller -> Remote Controller coding format.
-
- $next [+vcf_size] VendorCodingFormat receive_coding_format
- -- Remote Controller -> Local Controller coding format.
-
- $next [+2] UInt transmit_codec_frame_size_bytes
-
- $next [+2] UInt receive_codec_frame_size_bytes
-
- $next [+4] UInt input_bandwidth
- -- Host->Controller data rate in octets per second.
-
- $next [+4] UInt output_bandwidth
- -- Controller->Host data rate in octets per second.
-
- $next [+vcf_size] VendorCodingFormat input_coding_format
- -- Host->Controller coding format.
-
- $next [+vcf_size] VendorCodingFormat output_coding_format
- -- Controller->Host coding format.
-
- $next [+2] UInt input_coded_data_size_bits
- -- Size, in bits, of the sample or framed data.
-
- $next [+2] UInt output_coded_data_size_bits
- -- Size, in bits, of the sample or framed data.
-
- $next [+1] PcmDataFormat input_pcm_data_format
-
- $next [+1] PcmDataFormat output_pcm_data_format
-
- $next [+1] UInt input_pcm_sample_payload_msb_position
- -- The number of bit positions within an audio sample that the MSB of
- -- the sample is away from starting at the MSB of the data.
-
- $next [+1] UInt output_pcm_sample_payload_msb_position
- -- The number of bit positions within an audio sample that the MSB of
- -- the sample is away from starting at the MSB of the data.
-
- $next [+1] ScoDataPath input_data_path
-
- $next [+1] ScoDataPath output_data_path
-
- $next [+1] UInt input_transport_unit_size_bits
- -- The number of bits in each unit of data received from the Host over the audio data transport.
- -- 0 indicates "not applicable" (implied by the choice of audio data transport).
-
- $next [+1] UInt output_transport_unit_size_bits
- -- The number of bits in each unit of data sent to the Host over the audio data transport.
- -- 0 indicates "not applicable" (implied by the choice of audio data transport).
-
- $next [+2] UInt max_latency_ms
- -- The value in milliseconds representing the upper limit of the sum of
- -- the synchronous interval, and the size of the eSCO window, where the
- -- eSCO window is the reserved slots plus the retransmission window.
- -- Minimum: 0x0004
- -- Don't care: 0xFFFF
-
- $next [+2] ScoPacketType packet_types
- -- Bitmask of allowed packet types.
-
- $next [+1] ScoRetransmissionEffort retransmission_effort
-
-
-struct EnhancedSetupSynchronousConnectionCommand:
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- The connection handle of the associated ACL link if creating a new (e)SCO connection, or the
- -- handle of an existing eSCO link if updating connection parameters.
-
- let scp_size = SynchronousConnectionParameters.$size_in_bytes
- $next [+scp_size] SynchronousConnectionParameters connection_parameters
-
-
-struct EnhancedAcceptSynchronousConnectionRequestCommand:
- -- Enhanced Accept Synchronous Connection Request Command (CSA2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The 48-bit BD_ADDR of the remote device requesting the connection.
-
- let scp_size = SynchronousConnectionParameters.$size_in_bytes
- $next [+scp_size] SynchronousConnectionParameters connection_parameters
-
-
-struct SetEventMaskCommand:
- -- Set Event Mask Command (v1.1)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+8] UInt event_mask
- -- 64-bit Bit mask used to control which HCI events are generated by the HCI for the
- -- Host. See enum class EventMask in hci_constants.h
-
-
-struct WriteLocalNameCommand:
- -- Write Local Name Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- let local_name_size = LocalName.$size_in_bytes
- $next [+local_name_size] LocalName local_name
- -- A UTF-8 encoded User Friendly Descriptive Name for the device.
- -- If the name contained in the parameter is shorter than 248 octets, the end
- -- of the name is indicated by a NULL octet (0x00), and the following octets
- -- (to fill up 248 octets, which is the length of the parameter) do not have
- -- valid values.
-
-
-struct WritePageTimeoutCommand:
- -- Write Page Timeout Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt page_timeout
- -- Page_Timeout, in time slices (0.625 ms)
- -- Range: From MIN to MAX in PageTimeout in this file
- [requires: 0x0001 <= this <= 0xFFFF]
-
-
-struct WriteScanEnableCommand:
- -- Write Scan Enable Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] ScanEnableBits scan_enable
- -- Bit Mask of enabled scans. See enum class ScanEnableBits in this file
- -- for how to construct this bitfield.
-
-
-struct WritePageScanActivityCommand:
- -- Write Page Scan Activity Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt page_scan_interval
- -- Page_Scan_Interval, in time slices (0.625ms)
- -- Valid Range: MIN - MAX in ScanInterval in this file
- [requires: 0x0012 <= this <= 0x1000]
-
- $next [+2] UInt page_scan_window
- -- Page_Scan_Window, in time slices
- -- Valid Range: MIN - MAX in ScanWindow in this file
- [requires: 0x0011 <= this <= 0x1000]
-
-
-struct WriteInquiryScanActivityCommand:
- -- Write Inquiry Scan Activity Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt inquiry_scan_interval
- -- Inquiry_Scan_Interval, in time slices (0.625ms)
- -- Valid Range: MIN - MAX in ScanInterval in this file
- [requires: 0x0012 <= this <= 0x1000]
-
- $next [+2] UInt inquiry_scan_window
- -- Inquiry_Scan_Window, in time slices
- -- Valid Range: MIN - MAX in ScanWindow in this file
- [requires: 0x0011 <= this <= 0x1000]
-
-
-struct WriteAutomaticFlushTimeoutCommand:
- -- Write Automatic Flush Timeout Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Connection_Handle (only the lower 12-bits are meaningful).
- -- Range: 0x0000 to 0x0EFF
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+2] UInt flush_timeout
- -- The value for the Flush_Timeout configuration parameter (Core Spec v5.2, Vol 4, Part E, Sec 6.19).
- -- Range: 0x0000 to 0x07FF. 0x0000 indicates infinite flush timeout (no automatic flush).
- -- Time = flush_timeout * 0.625ms.
- -- Time Range: 0.625ms to 1279.375ms.
- [requires: 0x0000 <= this <= 0x07FF]
-
-
-struct WriteSynchronousFlowControlEnableCommand:
- -- Write Synchonous Flow Control Enable Command (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam synchronous_flow_control_enable
- -- If enabled, HCI_Number_Of_Completed_Packets events shall be sent from the controller
- -- for synchronous connection handles.
-
-
-struct WriteInquiryScanTypeCommand:
- -- Write Inquiry Scan Type (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] InquiryScanType inquiry_scan_type
- -- See enum class InquiryScanType in this file for possible values
-
-
-struct WriteExtendedInquiryResponseCommand:
- -- Write Extended Inquiry Response (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt fec_required
- -- If FEC Encoding is required. (v1.2) (7.3.56)
-
- let eir_size = ExtendedInquiryResponse.$size_in_bytes
- $next [+eir_size] ExtendedInquiryResponse extended_inquiry_response
- -- Extended inquiry response data as defined in Vol 3, Part C, Sec 8
-
-
-struct WriteSimplePairingModeCommand:
- -- Write Simple Pairing Mode (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam simple_pairing_mode
-
-
-struct LESetAdvertisingEnableCommand:
- -- LE Set Advertising Enable command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam advertising_enable
-
-
-struct LESetExtendedAdvertisingEnableData:
- -- Data fields for variable-length portion of an LE Set Extended Advertising Enable command
- 0 [+1] UInt advertising_handle
- $next [+2] UInt duration
- $next [+1] UInt max_extended_advertising_events
-
-
-struct LESetExtendedAdvertisingDataCommand:
- -- LE Set Extended Advertising Data Command (v5.0) (LE)
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+1] UInt advertising_handle
- -- Handle used to identify an advertising set.
-
- $next [+1] LESetExtendedAdvDataOp operation
-
- $next [+1] LEExtendedAdvFragmentPreference fragment_preference
- -- Provides a hint to the Controller as to whether advertising data should be fragmented.
-
- $next [+1] UInt advertising_data_length (sz)
- -- Length of the advertising data included in this command packet, up to
- -- kMaxLEExtendedAdvertisingDataLength bytes. If the advertising set uses legacy advertising
- -- PDUs that support advertising data then this shall not exceed kMaxLEAdvertisingDataLength
- -- bytes.
- [requires: 0 <= this <= 251]
-
- $next [+sz] UInt:8[sz] advertising_data
- -- Variable length advertising data.
-
-
-struct LESetExtendedScanResponseDataCommand:
- -- LE Set Extended Scan Response Data Command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt advertising_handle
- -- Used to identify an advertising set
- [requires: 0x00 <= this <= 0xEF]
-
- $next [+1] LESetExtendedAdvDataOp operation
- $next [+1] LEExtendedAdvFragmentPreference fragment_preference
- -- Provides a hint to the controller as to whether advertising data should be fragmented
-
- $next [+1] UInt scan_response_data_length (sz)
- -- The number of octets in the scan_response_data parameter
- [requires: 0 <= this <= 251]
-
- $next [+sz] UInt:8[sz] scan_response_data
- -- Scan response data formatted as defined in Core Spec v5.4, Vol 3, Part C, Section 11
-
-
-struct LESetExtendedAdvertisingEnableCommand:
- -- LE Set Extended Advertising Enable command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam enable
- $next [+1] UInt num_sets
- let single_data_size = LESetExtendedAdvertisingEnableData.$size_in_bytes
- $next [+single_data_size*num_sets] LESetExtendedAdvertisingEnableData[] data
-
-
-struct LEReadMaxAdvertisingDataLengthCommand:
- -- LE Read Maximum Advertising Data Length Command (v5.0) (LE)
- -- This command has no parameters
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEReadNumSupportedAdvertisingSetsCommand:
- -- LE Read Number of Supported Advertising Sets Command (v5.0) (LE)
- -- This command has no parameters
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LERemoveAdvertisingSetCommand:
- -- LE Remove Advertising Set command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt advertising_handle
-
-
-struct LEClearAdvertisingSetsCommand:
- -- LE Clear Advertising Sets Command (v5.0) (LE)
- -- This command has no parameters
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LESetExtendedScanParametersData:
- -- Data fields for variable-length portion of an LE Set Extneded Scan Parameters command
-
- 0 [+1] LEScanType scan_type
-
- $next [+2] UInt scan_interval
- -- Time interval from when the Controller started its last scan until it begins the subsequent
- -- scan on the primary advertising physical channel.
- -- Time = N × 0.625 ms
- -- Time Range: 2.5 ms to 40.959375 s
- [requires: 0x0004 <= this]
-
- $next [+2] UInt scan_window
- -- Duration of the scan on the primary advertising physical channel.
- -- Time = N × 0.625 ms
- -- Time Range: 2.5 ms to 40.959375 s
- [requires: 0x0004 <= this]
-
-
-struct LESetExtendedScanParametersCommand(num_entries: UInt:8):
- -- LE Set Extended Scan Parameters Command (v5.0) (LE)
- -- num_entries corresponds to the number of bits set in the |scanning_phys| field
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEOwnAddressType own_address_type
- $next [+1] LEScanFilterPolicy scanning_filter_policy
- $next [+1] LEPHYBits scanning_phys
- let single_entry_size = LESetExtendedScanParametersData.$size_in_bytes
- let total_entries_size = num_entries*single_entry_size
- $next [+total_entries_size] LESetExtendedScanParametersData[num_entries] data
- -- Indicates the type of address being used in the scan request packets (for active scanning).
-
-
-struct LESetExtendedScanEnableCommand:
- -- LE Set Extended Scan Enable Command (v5.0) (LE)
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+1] GenericEnableParam scanning_enabled
-
- $next [+1] LEExtendedDuplicateFilteringOption filter_duplicates
- -- See enum class LEExtendedDuplicateFilteringOption in this file for possible values
-
- $next [+2] UInt duration
- -- Possible values:
- -- 0x0000: Scan continuously until explicitly disabled
- -- 0x0001-0xFFFF: Scan duration, where:
- -- Time = N * 10 ms
- -- Time Range: 10 ms to 655.35 s
-
- $next [+2] UInt period
- -- Possible values:
- -- 0x0000: Periodic scanning disabled (scan continuously)
- -- 0x0001-0xFFFF: Time interval from when the Controller started its last
- -- Scan_Duration until it begins the subsequent Scan_Duration, where:
- -- Time = N * 1.28 sec
- -- Time Range: 1.28 s to 83,884.8 s
-
-
-struct UserPasskeyRequestNegativeReplyCommand:
- -- User Passkey Request Negative Reply Command (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The BD_ADDR of the remote device involved in the simple pairing process.
-
-
-struct IoCapabilityRequestNegativeReplyCommand:
- -- IO Capability Request Negative Reply Command (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The BD_ADDR of the remote device involved in the simple pairing process.
-
- $next [+1] StatusCode reason
- -- Reason that Simple Pairing was rejected. See 7.1.36 for valid error codes.
-
-
-struct ResetCommand:
- -- Reset Command (v1.1)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadLocalNameCommand:
- -- Read Local Name Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadScanEnableCommand:
- -- Read Scan Enable Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadPageScanActivityCommand:
- -- Read Page Scan Activity Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadInquiryScanActivityCommand:
- -- Read Inquiry Scan Activity Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadClassOfDeviceCommand:
- -- Read Class of Device Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct WriteClassOfDeviceCommand:
- -- Write Class Of Device Command (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+3] ClassOfDevice class_of_device
-
-
-struct LEPeriodicAdvertisingCreateSyncCommand:
- -- LE Periodic Advertising Create Sync Command (v5.0) (LE)
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+1] LEPeriodicAdvertisingCreateSyncOptions options
-
- $next [+1] UInt advertising_sid
- -- Advertising SID subfield in the ADI field used to identify the Periodic Advertising
- [requires: 0x00 <= this <= 0x0F]
-
- $next [+1] LEPeriodicAdvertisingAddressType advertiser_address_type
-
- $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
- -- Public Device Address, Random Device Address, Public Identity Address, or Random (static)
- -- Identity Address of the advertiser
-
- $next [+2] UInt skip
- -- The maximum number of periodic advertising events that can be skipped after a successful
- -- receive
- [requires: 0x0000 <= this <= 0x01F3]
-
- $next [+2] UInt sync_timeout
- -- Synchronization timeout for the periodic advertising.
- -- Time = N * 10 ms
- -- Time Range: 100 ms to 163.84 s
- [requires: 0x000A <= this <= 0x4000]
-
- $next [+1] LEPeriodicAdvertisingSyncCTEType sync_cte_type
- -- Constant Tone Extension sync options
-
-
-struct LEPeriodicAdvertisingCreateSyncCancel:
- -- LE Periodic Advertising Create Sync Cancel Command (v5.0) (LE)
- -- Note that this command has no arguments
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEPeriodicAdvertisingTerminateSyncCommand:
- -- LE Periodic Advertising Terminate Sync Command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt sync_handle
- -- Identifies the periodic advertising train
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct LEAddDeviceToPeriodicAdvertiserListCommand:
- -- LE Add Device To Periodic Advertiser List command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEAddressType advertiser_address_type
- -- Address type of the advertiser. The LEAddressType::kPublicIdentity and
- -- LEAddressType::kRandomIdentity values are excluded for this command.
-
- $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
- -- Public Device Address, Random Device Address, Public Identity Address, or
- -- Random (static) Identity Address of the advertiser.
-
- $next [+1] UInt advertising_sid
- -- Advertising SID subfield in the ADI field used to identify the Periodic
- -- Advertising.
-
-
-struct LERemoveDeviceFromPeriodicAdvertiserListCommand:
- -- LE Remove Device From Periodic Advertiser List command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt advertiser_address_type
- -- Address type of the advertiser. The LEAddressType::kPublicIdentity and
- -- LEAddressType::kRandomIdentity values are excluded for this command.
-
- $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
- -- Public Device Address, Random Device Address, Public Identity Address, or
- -- Random (static) Identity Address of the advertiser.
-
- $next [+1] UInt advertising_sid
- -- Advertising SID subfield in the ADI field used to identify the Periodic
- -- Advertising.
-
-
-struct LEClearPeriodicAdvertiserListCommand:
- -- LE Clear Periodic Advertiser List command (v5.0) (LE)
- -- Note that this command has no arguments
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEReadPeriodicAdvertiserListSizeCommand:
- -- LE Read Periodic Advertiser List Size command (v5.0) (LE)
- -- Note that this command has no arguments
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEReadTransmitPowerCommand:
- -- LE Read Transmit Power command (v5.0) (LE)
- -- Note that this command has no arguments
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEReadRFPathCompensationCommand:
- -- LE Read RF Path Compensation command (v5.0) (LE)
- -- Note that this command has no arguments
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEWriteRFPathCompensationCommand:
- -- LE Write RF Path Compensation command (v5.0) (LE)
- -- Values provided are used in the Tx Power Level and RSSI calculation.
- -- Range: -128.0 dB (0xFB00) ≤ N ≤ 128.0 dB (0x0500)
- -- Units: 0.1 dB
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] Int rf_tx_path_compensation_value
- [requires: -1280 <= this <= 1280]
-
- $next [+2] Int rf_rx_path_compensation_value
- [requires: -1280 <= this <= 1280]
-
-
-struct LESetPrivacyModeCommand:
- -- LE Set Privacy Mode command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEPeerAddressType peer_identity_address_type
- -- The peer identity address type (either Public Identity or Private
- -- Identity).
-
- $next [+BdAddr.$size_in_bytes] BdAddr peer_identity_address
- -- Public Identity Address or Random (static) Identity Address of the
- -- advertiser.
-
- $next [+1] LEPrivacyMode privacy_mode
- -- The privacy mode to be used for the given entry on the resolving list.
-
-
-struct ReadInquiryModeCommand:
- -- Read Inquiry Mode (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct WriteInquiryModeCommand:
- -- Write Inquiry Mode (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] InquiryMode inquiry_mode
-
-
-struct ReadPageScanTypeCommand:
- -- Read Page Scan Type (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct WritePageScanTypeCommand:
- -- Write Page Scan Type (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] PageScanType page_scan_type
-
-
-struct ReadSimplePairingModeCommand:
- -- Read Simple Pairing Mode (v2.1 + EDR) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct WriteLEHostSupportCommand:
- -- Write LE Host Support Command (v4.0) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam le_supported_host
- -- Sets the LE Supported (Host) Link Manager Protocol feature bit.
-
- $next [+1] UInt unused
- -- Core Spec v5.0, Vol 2, Part E, Section 6.35: This parameter was named
- -- "Simultaneous_LE_Host" and the value is set to "disabled(0x00)" and
- -- "shall be ignored".
- -- Core Spec v5.3, Vol 4, Part E, Section 7.3.79: This parameter was renamed
- -- to "Unused" and "shall be ignored by the controller".
-
-
-struct ReadLocalVersionInformationCommand:
- -- Read Local Version Information Command (v1.1)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadLocalSupportedCommandsCommand:
- -- Read Local Supported Commands Command (v1.2)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadBufferSizeCommand:
- -- Read Buffer Size Command (v1.1)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadBdAddrCommand:
- -- Read BD_ADDR Command (v1.1) (BR/EDR, LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadLocalSupportedFeaturesCommand:
- -- Read Local Supported Features Command (v1.1)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct ReadLocalExtendedFeaturesCommand:
- -- Read Local Extended Features Command (v1.2) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt page_number
- -- 0x00: Requests the normal LMP features as returned by
- -- Read_Local_Supported_Features.
- -- 0x01-0xFF: Return the corresponding page of features.
-
-
-struct ReadEncryptionKeySizeCommand:
- -- Read Encryption Key Size (v1.1) (BR/EDR)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- -- Identifies an active ACL link (only the lower 12 bits are meaningful).
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct LESetEventMaskCommand:
- -- LE Set Event Mask Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+8] bits:
- 0 [+35] LEEventMask le_event_mask
- -- Bitmask that indicates which LE events are generated by the HCI for the Host.
-
-
-struct LEReadBufferSizeCommandV1:
- -- LE Read Buffer Size Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEReadBufferSizeCommandV2:
- -- LE Read Buffer Size Command (v5.2) (LE)
- -- Version 2 of this command changed the opcode and added ISO return
- -- parameters.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEReadLocalSupportedFeaturesCommand:
- -- LE Read Local Supported Features Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LESetRandomAddressCommand:
- -- LE Set Random Address Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr random_address
-
-
-struct LESetAdvertisingParametersCommand:
- -- LE Set Advertising Parameters Command (v4.0) (LE)
-
- [requires: advertising_interval_min <= advertising_interval_max]
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+2] UInt advertising_interval_min
- -- Default: 0x0800 (1.28 s)
- -- Time: N * 0.625 ms
- -- Time Range: 20 ms to 10.24 s
- [requires: 0x0020 <= this <= 0x4000]
-
- $next [+2] UInt advertising_interval_max
- -- Default: 0x0800 (1.28 s)
- -- Time: N * 0.625 ms
- -- Time Range: 20 ms to 10.24 s
- [requires: 0x0020 <= this <= 0x4000]
-
- $next [+1] LEAdvertisingType adv_type
- -- Used to determine the packet type that is used for advertising when
- -- advertising is enabled.
-
- $next [+1] LEOwnAddressType own_address_type
-
- $next [+1] LEPeerAddressType peer_address_type
- -- ANONYMOUS address type not allowed.
-
- $next [+BdAddr.$size_in_bytes] BdAddr peer_address
- -- Public Device Address, Random Device Address, Public Identity Address, or
- -- Random (static) Identity Address of the device to be connected.
-
- $next [+1] bits:
-
- 0 [+3] LEAdvertisingChannels advertising_channel_map
- -- Indicates the advertising channels that shall be used when transmitting
- -- advertising packets. At least 1 channel must be enabled.
- -- Default: all channels enabled
-
- $next [+1] LEAdvertisingFilterPolicy advertising_filter_policy
- -- This parameter shall be ignored when directed advertising is enabled.
-
-
-struct LEReadAdvertisingChannelTxPowerCommand:
- -- LE Read Advertising Channel Tx Power Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LESetAdvertisingDataCommand:
- -- LE Set Advertising Data Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt advertising_data_length
- -- The number of significant octets in `advertising_data`.
- [requires: 0x00 <= this <= 0x1F]
-
- $next [+31] UInt:8[31] advertising_data
- -- 31 octets of advertising data formatted as defined in Core Spec
- -- v5.3, Vol 3, Part C, Section 11.
- -- Default: All octets zero
-
-
-struct LESetScanResponseDataCommand:
- -- LE Set Scan Response Data Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt scan_response_data_length
- -- The number of significant octets in `scan_response_data`.
- [requires: 0x00 <= this <= 0x1F]
-
- $next [+31] UInt:8[31] scan_response_data
- -- 31 octets of scan response data formatted as defined in Core Spec
- -- v5.3, Vol 3, Part C, Section 11.
- -- Default: All octets zero
-
-
-struct LESetScanParametersCommand:
- -- LE Set Scan Parameters Command (v4.0) (LE)
-
- [requires: le_scan_window <= le_scan_interval]
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+1] LEScanType le_scan_type
- -- Controls the type of scan to perform.
-
- $next [+2] UInt le_scan_interval
- -- Default: 0x0010 (10ms)
- -- Time: N * 0.625 ms
- -- Time Range: 2.5 ms to 10.24 s
- [requires: 0x0004 <= this <= 0x4000]
-
- $next [+2] UInt le_scan_window
- -- Default: 0x0010 (10ms)
- -- Time: N * 0.625 ms
- -- Time Range: 2.5ms to 10.24 s
- [requires: 0x0004 <= this <= 0x4000]
-
- $next [+1] LEOwnAddressType own_address_type
- -- The type of address being used in the scan request packets.
-
- $next [+1] LEScanFilterPolicy scanning_filter_policy
-
-
-struct LESetScanEnableCommand:
- -- LE Set Scan Enable Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam le_scan_enable
- $next [+1] GenericEnableParam filter_duplicates
- -- Controls whether the Link Layer should filter out duplicate advertising
- -- reports to the Host, or if the Link Layer should generate advertising
- -- reports for each packet received. Ignored if le_scan_enable is set to
- -- disabled.
- -- See Core Spec v5.3, Vol 6, Part B, Section 4.4.3.5
-
-
-struct LECreateConnectionCommand:
- -- LE Create Connection Command (v4.0) (LE)
-
- [requires: le_scan_window <= le_scan_interval && connection_interval_min <= connection_interval_max]
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+2] UInt le_scan_interval
- -- The time interval from when the Controller started the last LE scan until
- -- it begins the subsequent LE scan.
- -- Time: N * 0.625 ms
- -- Time Range: 2.5 ms to 10.24 s
- [requires: 0x0004 <= this <= 0x4000]
-
- $next [+2] UInt le_scan_window
- -- Amount of time for the duration of the LE scan.
- -- Time: N * 0.625 ms
- -- Time Range: 2.5 ms to 10.24 s
- [requires: 0x0004 <= this <= 0x4000]
-
- $next [+1] GenericEnableParam initiator_filter_policy
-
- $next [+1] LEAddressType peer_address_type
-
- $next [+BdAddr.$size_in_bytes] BdAddr peer_address
-
- $next [+1] LEOwnAddressType own_address_type
-
- $next [+2] UInt connection_interval_min
- -- Time: N * 1.25 ms
- -- Time Range: 7.5 ms to 4 s.
- [requires: 0x0006 <= this <= 0x0C80]
-
- $next [+2] UInt connection_interval_max
- -- Time: N * 1.25 ms
- -- Time Range: 7.5 ms to 4 s.
- [requires: 0x0006 <= this <= 0x0C80]
-
- $next [+2] UInt max_latency
- -- Maximum Peripheral latency for the connection in number of connection
- -- events.
- [requires: 0x0000 <= this <= 0x01F3]
-
- $next [+2] UInt supervision_timeout
- -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
- -- Time: N * 10 ms
- -- Time Range: 100 ms to 32 s
- [requires: 0x000A <= this <= 0x0C80]
-
- $next [+2] UInt min_connection_event_length
- -- Time: N * 0.625 ms
-
- $next [+2] UInt max_connection_event_length
- -- Time: N * 0.625 ms
-
-
-struct LECreateConnectionCancelCommand:
- -- LE Create Connection Cancel Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEClearFilterAcceptListCommand:
- -- LE Clear Filter Accept List Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEAddDeviceToFilterAcceptListCommand:
- -- LE Add Device To Filter Accept List Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEPeerAddressType address_type
- -- The address type of the peer.
-
- $next [+BdAddr.$size_in_bytes] BdAddr address
- -- Public Device Address or Random Device Address of the device to be added
- -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
-
-
-struct LERemoveDeviceFromFilterAcceptListCommand:
- -- LE Remove Device From Filter Accept List Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] LEPeerAddressType address_type
- -- The address type of the peer.
-
- $next [+BdAddr.$size_in_bytes] BdAddr address
- -- Public Device Address or Random Device Address of the device to be added
- -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
-
-
-struct LEConnectionUpdateCommand:
- -- LE Connection Update Command (v4.0) (LE)
-
- [requires: connection_interval_min <= connection_interval_max && min_connection_event_length <= max_connection_event_length]
-
- let hdr_size = CommandHeader.$size_in_bytes
-
- 0 [+hdr_size] CommandHeader header
-
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+2] UInt connection_interval_min
- -- Time: N * 1.25 ms
- -- Time Range: 7.5 ms to 4 s.
- [requires: 0x0006 <= this <= 0x0C80]
-
- $next [+2] UInt connection_interval_max
- -- Time: N * 1.25 ms
- -- Time Range: 7.5 ms to 4 s.
- [requires: 0x0006 <= this <= 0x0C80]
-
- $next [+2] UInt max_latency
- -- Maximum Peripheral latency for the connection in number of subrated
- -- connection events.
- [requires: 0x0000 <= this <= 0x01F3]
-
- $next [+2] UInt supervision_timeout
- -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
- -- Time: N * 10 ms
- -- Time Range: 100 ms to 32 s
- [requires: 0x000A <= this <= 0x0C80]
-
- $next [+2] UInt min_connection_event_length
- -- Time: N * 0.625 ms
-
- $next [+2] UInt max_connection_event_length
- -- Time: N * 0.625 ms
-
-
-struct LEReadRemoteFeaturesCommand:
- -- LE Read Remote Features Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct LEEnableEncryptionCommand:
- -- LE Enable Encryption Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+8] UInt random_number
- $next [+2] UInt encrypted_diversifier
- $next [+LinkKey.$size_in_bytes] LinkKey long_term_key
-
-
-struct LELongTermKeyRequestNegativeReplyCommand:
- -- LE Long Term Key Request Negative Reply Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct LEReadSupportedStatesCommand:
- -- LE Read Supported States Command (v4.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LEClearResolvingListCommand:
- -- LE Clear Resolving List Command (v4.2) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
-
-
-struct LESetAddressResolutionEnableCommand:
- -- LE Set Address Resolution Enable Command (v4.2) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] GenericEnableParam address_resolution_enable
-
-
-struct LESetAdvertisingSetRandomAddressCommand:
- -- LE Set Advertising Set Random Address Command (v5.0) (LE)
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt advertising_handle
- -- Handle used to identify an advertising set.
-
- $next [+BdAddr.$size_in_bytes] BdAddr random_address
- -- The random address to use in the advertising PDUs.
-
-
-struct WriteAuthenticatedPayloadTimeoutCommand:
- -- Write Authenticated Payload Timeout Command (v4.1) (BR/EDR & LE)
- 0 [+CommandHeader.$size_in_bytes] CommandHeader header
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+2] UInt authenticated_payload_timeout
- -- Default = 0x0BB8 (30 s)
- -- Time = N * 10 ms
- -- Time Range: 10 ms to 655,350 ms
- [requires: 0x0001 <= this <= 0xFFFF]
-
-
-struct ReadAuthenticatedPayloadTimeoutCommand:
- -- Read Authenticated Payload Timeout Command (v4.1) (BR/EDR & LE)
- 0 [+CommandHeader.$size_in_bytes] CommandHeader header
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct ReadLEHostSupportCommand:
- -- Read LE Host Support Command (v4.0) (BR/EDR)
- 0 [+CommandHeader.$size_in_bytes] CommandHeader header
-
-
-struct ReadFlowControlModeCommand:
- -- Read Flow Control Mode Command (v3.0 + HS) (BR/EDR)
- 0 [+CommandHeader.$size_in_bytes] CommandHeader header
-
-
-struct WriteFlowControlModeCommand:
- -- Write Flow Control Mode Command (v3.0 + HS) (BR/EDR)
- 0 [+CommandHeader.$size_in_bytes] CommandHeader header
- $next [+1] FlowControlMode flow_control_mode
-
-
-struct SetEventMaskPage2Command:
- -- Set Event Mask Page 2 Command (v3.0 + HS)
- 0 [+CommandHeader.$size_in_bytes] CommandHeader header
- $next [+8] bits:
- 0 [+26] EventMaskPage2 event_mask_page_2
- -- Bit mask used to control which HCI events are generated by the HCI for the Host.
-
-# ========================= HCI Event packets ===========================
-# Core Spec v5.3 Vol 4, Part E, Section 7.7
-
-
-struct VendorDebugEvent:
- -- This opcode is reserved for vendor-specific debugging events.
- -- See Core Spec v5.3 Vol 4, Part E, Section 5.4.4.
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] UInt subevent_code
- -- The event code for the vendor subevent.
-
-
-struct InquiryCompleteEvent:
- -- Inquiry Complete Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
-
-
-struct CommandCompleteEvent:
- -- Core Spec v5.3 Vol 4, Part E, Section 7.7.14
- -- EventHeader.opcode == 0xe
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] UInt num_hci_command_packets
- $next [+2] OpCodeBits command_opcode
- let event_fixed_size = $size_in_bytes-hdr_size
- let return_parameters_size = header.parameter_total_size-event_fixed_size
-
-
-struct ConnectionCompleteEvent:
- -- Connection Complete Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The address of the connected device
-
- $next [+1] LinkType link_type
- $next [+1] GenericEnableParam encryption_enabled
-
-
-struct ConnectionRequestEvent:
- -- Connection Request Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- -- The address of the device that's requesting the connection.
-
- $next [+3] ClassOfDevice class_of_device
- -- The Class of Device of the device which requests the connection.
-
- $next [+1] LinkType link_type
-
-
-struct DisconnectionCompleteEvent:
- -- Disconnection Complete Event (v1.1) (BR/EDR & LE)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+1] StatusCode reason
-
-
-struct AuthenticationCompleteEvent:
- -- Authentication Complete Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct RemoteNameRequestCompleteEvent:
- -- Remote Name Request Complete Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
- $next [+248] UInt:8[248] remote_name
- -- UTF-8 encoded friendly name. If the name is less than 248 characters, it
- -- is null terminated and the remaining bytes are not valid.
-
-
-struct EncryptionChangeEventV1:
- -- Encryption Change Event (v1.1) (BR/EDR & LE)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+1] EncryptionStatus encryption_enabled
-
-
-struct ChangeConnectionLinkKeyCompleteEvent:
- -- Change Connection Link Key Complete Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
-
-struct ReadRemoteSupportedFeaturesCompleteEvent:
- -- Read Remote Supported Features Complete Event (v1.1) (BR/EDR)
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] StatusCode status
- $next [+2] UInt connection_handle
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+8] LmpFeatures(0) lmp_features
- -- Page 0 of the LMP features.
-
-
-struct LEMetaEvent:
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] UInt subevent_code
- -- The event code for the LE subevent.
-
-
-struct LEConnectionCompleteSubevent:
- 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
-
- $next [+1] StatusCode status
-
- $next [+2] UInt connection_handle
- -- Only the lower 12-bits are meaningful.
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+1] ConnectionRole role
-
- $next [+1] LEPeerAddressType peer_address_type
-
- $next [+BdAddr.$size_in_bytes] BdAddr peer_address
- -- Public Device Address or Random Device Address of the peer device.
-
- $next [+2] UInt connection_interval
- -- Time: N * 1.25 ms
- -- Range: 7.5 ms to 4 s
- [requires: 0x0006 <= this <= 0x0C80]
-
- $next [+2] UInt peripheral_latency
- [requires: 0x0000 <= this <= 0x01F3]
-
- $next [+2] UInt supervision_timeout
- -- Time: N * 10 ms
- -- Range: 100 ms to 32 s
- [requires: 0x000A <= this <= 0x0C80]
-
- $next [+1] LEClockAccuracy central_clock_accuracy
- -- Only valid for a peripheral. On a central, this parameter shall be set to 0x00.
-
-
-struct LEConnectionUpdateCompleteSubevent:
- 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
-
- $next [+1] StatusCode status
-
- $next [+2] UInt connection_handle
- -- Only the lower 12-bits are meaningful.
- [requires: 0x0000 <= this <= 0x0EFF]
-
- $next [+2] UInt connection_interval
- -- Time: N * 1.25 ms
- -- Range: 7.5 ms to 4 s
- [requires: 0x0006 <= this <= 0x0C80]
-
- $next [+2] UInt peripheral_latency
- [requires: 0x0000 <= this <= 0x01F3]
-
- $next [+2] UInt supervision_timeout
- -- Time: N * 10 ms
- -- Range: 100 ms to 32 s
- [requires: 0x000A <= this <= 0x0C80]
-
-# ============================ Test packets =============================
-
-
-struct TestCommandPacket:
- -- Test HCI Command packet with single byte payload.
- let hdr_size = CommandHeader.$size_in_bytes
- 0 [+hdr_size] CommandHeader header
- $next [+1] UInt payload
-
-
-struct TestEventPacket:
- -- Test HCI Event packet with single byte payload.
- let hdr_size = EventHeader.$size_in_bytes
- 0 [+hdr_size] EventHeader header
- $next [+1] UInt payload
diff --git a/pw_bluetooth/public/pw_bluetooth/hci_commands.emb b/pw_bluetooth/public/pw_bluetooth/hci_commands.emb
new file mode 100644
index 000000000..fdabc7b62
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci_commands.emb
@@ -0,0 +1,2934 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This file contains Emboss definitions for Host Controller Interface packets
+# and types found in the Bluetooth Core Specification. The Emboss compiler is
+# used to generate a C++ header from this file.
+
+import "hci_common.emb" as hci
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::emboss"]
+# =========================== Constants =================================
+
+
+enum InquiryAccessCode:
+ -- General- and Device-specific Inquiry Access Codes (DIACs) for use in Inquiry
+ -- command LAP fields.
+ -- (https://www.bluetooth.com/specifications/assigned-numbers/baseband)
+ [maximum_bits: 24]
+ GIAC = 0x9E8B33
+ -- General Inquiry Access Code
+
+ LIAC = 0x9E8B00
+ -- Limited Dedicated Inquiry Access Code
+
+
+enum PcmDataFormat:
+ -- PCM data formats from assigned numbers.
+ -- (https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface)
+ [maximum_bits: 8]
+ NOT_APPLICABLE = 0x00
+ ONES_COMPLEMENT = 0x01
+ TWOS_COMPLEMENT = 0x02
+ SIGN_MAGNITUDE = 0x03
+ UNSIGNED = 0x04
+
+
+enum ScoDataPath:
+ [maximum_bits: 8]
+ HCI = 0x00
+ AUDIO_TEST_MODE = 0xFF
+ -- 0x01 - 0xFE specify the logical channel number (vendor specific)
+
+
+enum PageTimeout:
+ [maximum_bits: 16]
+ MIN = 0x0001
+ MAX = 0xFFFF
+ DEFAULT = 0x2000
+
+
+enum ScanInterval:
+ -- The minimum and maximum range values for Page and Inquiry Scan Interval (in time slices)
+ -- Page Scan Interval: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.19)
+ -- Inquiry Scan Interval: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.21)
+ [maximum_bits: 16]
+ MIN = 0x0012
+ MAX = 0x1000
+
+
+enum ScanWindow:
+ -- The minimum and maximum range valeus for Page and Inquiry Scan Window (in time slices)
+ -- Page Scan Window: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.19)
+ -- Inquiry Scan Window: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.21)
+ [maximum_bits: 16]
+ MIN = 0x0011
+ MAX = 0x1000
+
+
+bits ScoPacketType:
+ -- Bitmask of SCO packet types.
+ # SCO packet types
+ 0 [+1] Flag hv1
+ $next [+1] Flag hv2
+ $next [+1] Flag hv3
+ # eSCO packet types
+ $next [+1] Flag ev3
+ $next [+1] Flag ev4
+ $next [+1] Flag ev5
+ $next [+1] Flag not_2_ev3
+ $next [+1] Flag not_3_ev3
+ $next [+1] Flag not_2_ev5
+ $next [+1] Flag not_3_ev5
+
+
+bits PacketType:
+ -- Bitmask values for supported Packet Types
+ -- Used for HCI_Create_Connection and HCI_Change_Connection_Packet_Type
+ -- All other bits reserved for future use.
+ 1 [+1] Flag disable_2_dh1
+ 2 [+1] Flag disable_3_dh1
+ 3 [+1] Flag enable_dm1 # Note: always on in >= v1.2
+ 4 [+1] Flag enable_dh1
+ 8 [+1] Flag disable_2_dh3
+ 9 [+1] Flag disable_3_dh3
+ 10 [+1] Flag enable_dm3
+ 11 [+1] Flag enable_dh3
+ 12 [+1] Flag disable_2_dh5
+ 13 [+1] Flag disable_3_dh5
+ 14 [+1] Flag enable_dm5
+ 15 [+1] Flag enable_dh5
+
+
+enum OobDataPresent:
+ -- Whether there is out-of-band data present, and what type.
+ -- All other values reserved for future use.
+ [maximum_bits: 8]
+ NOT_PRESENT = 0x00
+ P192_ = 0x01
+ P256_ = 0x02
+ P192_AND_P256 = 0x03
+
+
+bits ScanEnableBits:
+ -- Bitmask Values for the Scan_Enable parameter in a HCI_(Read,Write)_Scan_Enable command.
+ 0 [+1] Flag inquiry
+ -- Inquiry scan enabled
+
+ 1 [+1] Flag page
+ -- Page scan enabled
+
+
+enum InquiryScanType:
+ [maximum_bits: 8]
+ STANDARD = 0x00
+ -- Standard scan (Default) (Mandatory)
+
+ INTERLACED = 0x01
+
+
+struct LocalName:
+ 0 [+248] UInt:8[248] local_name
+
+
+struct ExtendedInquiryResponse:
+ 0 [+240] UInt:8[240] extended_inquiry_response
+
+
+enum LEExtendedDuplicateFilteringOption:
+ -- Possible values that can be used for the |filter_duplicates| parameter in a
+ -- HCI_LE_Set_Extended_Scan_Enable command.
+ [maximum_bits: 8]
+ DISABLED = 0x00
+ ENABLED = 0x01
+ ENABLED_RESET_FOR_EACH_SCAN_PERIOD = 0x02
+ -- Duplicate advertisements in a single scan period should not be sent to the
+ -- Host in advertising report events; this setting shall only be used if the
+ -- Period parameter is non-zero.
+
+
+enum LEPeriodicAdvertisingCreateSyncUseParams:
+ [maximum_bits: 1]
+
+ USE_PARAMS = 0x00
+ -- Use the Advertising_SID, Advertiser_Address_Type, and Adertiser_Address parameters to
+ -- determine which advertiser to listen to.
+
+ USE_PERIODIC_ADVERTISER_LIST = 0x01
+ -- Use the Periodic Advertiser List to determine which advertiser to listen to.
+
+
+bits LEPeriodicAdvertisingCreateSyncOptions:
+ -- First parameter to the LE Periodic Advertising Create Sync command
+ 0 [+1] LEPeriodicAdvertisingCreateSyncUseParams advertiser_source
+ $next [+1] Flag enable_reporting
+ -- 0: Reporting initially enabled
+ -- 1: Reporting initially disabled
+
+ $next [+1] Flag enable_duplicate_filtering
+ -- 0: Duplicate filtering initially disabled
+ -- 1: Duplicate filtering initially enabled
+
+
+enum LEPeriodicAdvertisingAddressType:
+ -- Possible values that can be specified for the |advertiser_address_type| in an LE Periodic
+ -- Advertising Create Sync command.
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ -- Public Device Address or Public Identity Address
+
+ RANDOM = 0x01
+ -- Random Device Address or Random (static) Identity Address
+
+
+bits LEPeriodicAdvertisingSyncCTEType:
+ -- Bit definitions for a |sync_cte_type| field in an LE Periodic Advertising Create Sync command
+
+ 0 [+1] Flag dont_sync_aoa
+ -- Do not sync to packets with an AoA Constant Tone Extension
+
+ $next [+1] Flag dont_sync_aod_1us
+ -- Do not sync to packets with an AoD Constant Tone Extension with 1 microsecond slots
+
+ $next [+1] Flag dont_sync_aod_2us
+ -- Do not sync to packets with an AoD Constant Tone Extension with 2 microsecond slots
+
+ $next [+1] Flag dont_sync_type_3
+ -- Do not sync to packets with a typoe 3 Constant Tone Extension (currently reserved for future
+ -- use)
+
+ $next [+1] Flag dont_sync_without_cte
+ -- Do not sync to packets without a Constant Tone Extension
+
+
+enum LEOwnAddressType:
+ -- Possible values that can be used for the |own_address_type| parameter in various LE packets.
+
+ [maximum_bits: 8]
+
+ PUBLIC = 0x00
+ -- Public Device Address
+
+ RANDOM = 0x01
+ -- Random Device Address
+
+ PRIVATE_DEFAULT_TO_PUBLIC = 0x02
+ -- Controller generates the Resolvable Private Address based on the local IRK from the resolving
+ -- list. If the resolving list contains no matching entry, then use the public address.
+
+ PRIVATE_DEFAULT_TO_RANDOM = 0x03
+ -- Controller generates the Resolvable Private Address based on the local IRK from the resolving
+ -- list. If the resolving list contains no matching entry, then use the random address from
+ -- LE_Set_Random_Address.
+
+
+enum LEScanType:
+ -- Possible values that can be used for the |scan_type| parameter in various LE HCI commands.
+ [maximum_bits: 8]
+ PASSIVE = 0x00
+ -- Passive Scanning. No scanning PDUs shall be sent (default)
+
+ ACTIVE = 0x01
+ -- Active scanning. Scanning PDUs may be sent.
+
+
+enum LEScanFilterPolicy:
+ -- Possible values that can be used for the |filter_policy| parameter in various LE HCI commands
+ [maximum_bits: 8]
+ BASIC_UNFILTERED = 0x00
+ BASIC_FILTERED = 0x01
+ EXTENDED_UNFILTERED = 0x02
+ EXTENDED_FILTERED = 0x03
+
+
+bits LEScanPHYBits:
+ 0 [+1] Flag le_1m
+ -- Scan advertisements on the LE 1M PHY
+
+ 2 [+1] Flag le_coded
+ -- Scan advertisements on the LE Coded PHY
+
+
+bits LEInitiatingPHYBits:
+ 0 [+1] Flag le_1m
+ 1 [+1] Flag le_2m
+ 2 [+1] Flag le_coded
+
+
+enum LEPrivacyMode:
+ -- Possible values for the |privacy_mode| parameter in an LE Set Privacy Mode
+ -- command
+ [maximum_bits: 8]
+ NETWORK = 0x00
+ -- Use Network Privacy Mode for this peer device (default).
+
+ DEVICE = 0x01
+ -- Use Device Privacy Mode for this peer device.
+
+
+enum InquiryMode:
+ [maximum_bits: 8]
+ STANDARD = 0x00
+ -- Standard Inquiry Result format (default)
+
+ RSSI = 0x01
+ -- Inquiry Result format with RSSI
+
+ EXTENDED = 0x02
+ -- Inquiry Result format with RSSI or EIR format
+
+
+enum PageScanType:
+ [maximum_bits: 8]
+ STANDARD_SCAN = 0x00
+ -- Standard scan (default) (mandatory)
+
+ INTERLACED_SCAN = 0x01
+ -- Interlaced scan (optional)
+
+
+bits LEEventMask:
+ 0 [+1] Flag le_connection_complete
+ $next [+1] Flag le_advertising_report
+ $next [+1] Flag le_connection_update_complete
+ $next [+1] Flag le_read_remote_features_complete
+ $next [+1] Flag le_long_term_key_request
+ $next [+1] Flag le_remote_connection_parameter_request
+ $next [+1] Flag le_data_length_change
+ $next [+1] Flag le_read_local_p256_public_key_complete
+ $next [+1] Flag le_generate_dhkey_complete
+ $next [+1] Flag le_enhanced_connection_complete
+ $next [+1] Flag le_directed_advertising_report
+ $next [+1] Flag le_phy_update_complete
+ $next [+1] Flag le_extended_advertising_report
+ $next [+1] Flag le_periodic_advertising_sync_established
+ $next [+1] Flag le_periodic_advertising_report
+ $next [+1] Flag le_periodic_advertising_sync_lost
+ $next [+1] Flag le_extended_scan_timeout
+ $next [+1] Flag le_extended_advertising_set_terminated
+ $next [+1] Flag le_scan_request_received
+ $next [+1] Flag le_channel_selection_algorithm
+ $next [+1] Flag le_connectionless_iq_report
+ $next [+1] Flag le_connection_iq_report
+ $next [+1] Flag le_cte_request_failed
+ $next [+1] Flag le_periodic_advertising_sync_transfer_received_event
+ $next [+1] Flag le_cis_established_event
+ $next [+1] Flag le_cis_request_event
+ $next [+1] Flag le_create_big_complete_event
+ $next [+1] Flag le_terminate_big_complete_event
+ $next [+1] Flag le_big_sync_established_event
+ $next [+1] Flag le_big_sync_lost_event
+ $next [+1] Flag le_request_peer_sca_complete_event
+ $next [+1] Flag le_path_loss_threshold_event
+ $next [+1] Flag le_transmit_power_reporting_event
+ $next [+1] Flag le_biginfo_advertising_report_event
+ $next [+1] Flag le_subrate_change_event
+
+
+enum LEAdvertisingType:
+ [maximum_bits: 8]
+ CONNECTABLE_AND_SCANNABLE_UNDIRECTED = 0x00
+ -- ADV_IND
+
+ CONNECTABLE_HIGH_DUTY_CYCLE_DIRECTED = 0x01
+ -- ADV_DIRECT_IND
+
+ SCANNABLE_UNDIRECTED = 0x02
+ -- ADV_SCAN_IND
+
+ NOT_CONNECTABLE_UNDIRECTED = 0x03
+ -- ADV_NONCONN_IND
+
+ CONNECTABLE_LOW_DUTY_CYCLE_DIRECTED = 0x04
+ -- ADV_DIRECT_IND
+
+
+bits LEAdvertisingChannels:
+ 0 [+1] Flag channel_37
+ $next [+1] Flag channel_38
+ $next [+1] Flag channel_39
+
+
+enum LEAdvertisingFilterPolicy:
+ [maximum_bits: 8]
+
+ ALLOW_ALL = 0x00
+ -- Process scan and connection requests from all devices (i.e., the Filter
+ -- Accept List is not in use) (default).
+
+ ALLOW_ALL_CONNECTIONS_AND_USE_FILTER_ACCEPT_LIST_FOR_SCANS = 0x01
+ -- Process connection requests from all devices and scan requests only from
+ -- devices that are in the Filter Accept List.
+
+ ALLOW_ALL_SCANS_AND_USE_FILTER_ACCEPT_LIST_FOR_CONNECTIONS = 0x02
+ -- Process scan requests from all devices and connection requests only from
+ -- devices that are in the Filter Accept List.
+
+ ALLOW_FILTER_ACCEPT_LIST_ONLY = 0x03
+ -- Process scan and connection requests only from devices in the Filter
+ -- Accept List.
+
+
+enum ScanRequestNotifications:
+ [maximum_bits: 8]
+ DISABLED = 0x00
+ ENABLED = 0x01
+
+
+enum LESetExtendedAdvDataOp:
+ -- Potential values for the Operation parameter in a HCI_LE_Set_Extended_Advertising_Data command.
+ [maximum_bits: 8]
+ INTERMEDIATE_FRAGMENT = 0x00
+ -- Intermediate fragment of fragmented extended advertising data.
+
+ FIRST_FRAGMENT = 0x01
+ -- First fragment of fragmented extended advertising data.
+
+ LAST_FRAGMENT = 0x02
+ -- Last fragment of fragmented extended advertising data.
+
+ COMPLETE = 0x03
+ -- Complete extended advertising data.
+
+ UNCHANGED_DATA = 0x04
+ -- Unchanged data (just update the Advertising DID)
+
+
+enum LEExtendedAdvFragmentPreference:
+ -- Potential values for the Fragment_Preference parameter in a
+ -- HCI_LE_Set_Extended_Advertising_Data command.
+ [maximum_bits: 8]
+ MAY_FRAGMENT = 0x00
+ -- The Controller may fragment all Host advertising data
+
+ SHOULD_NOT_FRAGMENT = 0x01
+ -- The Controller should not fragment or should minimize fragmentation of Host advertising data
+
+
+bits LEAdvertisingEventProperties:
+ -- The Advertising_Event_Properties bitfield values used in a HCI LE Set Extended Advertising
+ -- Parameters command.
+ 0 [+1] Flag connectable
+ $next [+1] Flag scannable
+ $next [+1] Flag directed
+ $next [+1] Flag high_duty_cycle_directed_connectable
+ $next [+1] Flag use_legacy_pdus
+ $next [+1] Flag anonymous_advertising
+ $next [+1] Flag include_tx_power
+
+
+enum FlowControlMode:
+ [maximum_bits: 8]
+ PACKET_BASED = 0x00
+ DATA_BLOCK_BASED = 0x01
+
+
+bits EventMaskPage2:
+ 8 [+1] Flag number_of_completed_data_blocks_event
+ 14 [+1] Flag triggered_clock_capture_event
+ 15 [+1] Flag synchronization_train_complete_event
+ 16 [+1] Flag synchronization_train_received_event
+ 17 [+1] Flag connectionless_peripheral_broadcast_receive_event
+ 18 [+1] Flag connectionless_peripheral_broadcast_timeout_event
+ 19 [+1] Flag truncated_page_complete_event
+ 20 [+1] Flag peripheral_page_response_timeout_event
+ 21 [+1] Flag connectionless_peripheral_broadcast_channel_map_event
+ 22 [+1] Flag inquiry_response_notification_event
+ 23 [+1] Flag authenticated_payload_timeout_expired_event
+ 24 [+1] Flag sam_status_change_event
+ 25 [+1] Flag encryption_change_event_v2
+
+
+bits LECISPHYOptions:
+ -- Identifies PHYs that can be used for transmission
+ 0 [+1] Flag le_1m
+ $next [+1] Flag le_2m
+ $next [+1] Flag le_coded
+
+
+struct LESetCIGParametersCISOptions:
+ -- Parameters for the CISes defined in a LESetCIGParametersCommand
+
+ 0 [+1] UInt cis_id
+ -- Used to identify a CIS
+ [requires: 0x00 <= this <= 0xEF]
+
+ $next [+2] UInt max_sdu_c_to_p
+ -- Maximum size, in octets, of the payload from the Central's Host
+ [requires: 0x0000 <= this <= 0x0FFF]
+
+ $next [+2] UInt max_sdu_p_to_c
+ -- Maximum size, in octets, of the payload from the Peripheral's Host
+ [requires: 0x0000 <= this <= 0x0FFF]
+
+ $next [+1] bits:
+
+ 0 [+LECISPHYOptions.$size_in_bits] LECISPHYOptions phy_c_to_p
+ -- Identifies which PHY to use for transmission from the Central to the Peripheral
+
+ $next [+1] bits:
+
+ 0 [+LECISPHYOptions.$size_in_bits] LECISPHYOptions phy_p_to_c
+ -- Identifies which PHY to use for transmission from the Peripheral to the Central
+
+ $next [+1] UInt rtn_c_to_p
+ -- Number of times every CIS Data PDU should be retransmitted from the Central to the
+ -- Peripheral
+
+ $next [+1] UInt rtn_p_to_c
+ -- Number of times every CIS Data PDU should be retransmitted from the Peripheral to the
+ -- Central
+
+
+enum LESleepClockAccuracyRange:
+ -- Accuracy of the sleep clock, provided as a range
+ [maximum_bits: 8]
+ PPM_251_TO_500 = 0x00
+ PPM_151_TO_250 = 0x01
+ PPM_101_TO_150 = 0x02
+ PPM_76_TO_100 = 0x03
+ PPM_51_TO_75 = 0x04
+ PPM_31_TO_50 = 0x05
+ PPM_21_TO_30 = 0x06
+ PPM_0_TO_20 = 0x07
+
+
+enum LECISPacking:
+ -- Preferred method of arranging subevents of multiple CISes
+ [maximum_bits: 8]
+ SEQUENTIAL = 0x00
+ INTERLEAVED = 0x01
+
+
+enum LECISFraming:
+ -- Format of CIS Data PDUs
+ [maximum_bits: 8]
+ UNFRAMED = 0x00
+ FRAMED = 0x01
+
+
+enum DataPathDirection:
+ [maximum_bits: 8]
+ INPUT = 0x00
+ -- Host to Controller
+
+ OUTPUT = 0x01
+ -- Controller to Host
+
+# ========================= HCI Command packets =========================
+# Core Spec v5.3 Vol 4, Part E, Section 7
+
+# ========== 7.1 Link Control Commands ==========
+
+
+struct InquiryCommand:
+ -- 7.1.1 Inquiry command (v1.1) (BR/EDR)
+ -- HCI_Inquiry
+ --
+ -- Note: NO Command Complete; Sends Inquiry Complete at the end of the
+ -- inquiry to indicate it's completion. No Inquiry Complete event is sent if
+ -- Inquiry is cancelled.
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+3] InquiryAccessCode lap
+ -- LAP (Lower Address Part)
+ -- In the range 0x9E8B00 - 0x9E8B3F, defined by the Bluetooth SIG in
+ -- Baseband Assigned Numbers.
+
+ $next [+1] UInt inquiry_length
+ -- Time before the inquiry is halted. Defined in 1.28s units.
+ -- Range: 0x01 to kInquiryLengthMax in hci_constants.h
+
+ $next [+1] UInt num_responses
+ -- Maximum number of responses before inquiry is halted.
+ -- Set to 0x00 for unlimited.
+
+
+struct InquiryCancelCommand:
+ -- 7.1.2 Inquiry Cancel command (v1.1) (BR/EDR)
+ -- HCI_Inquiry_Cancel
+ --
+ -- No command parameters
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.1.3 Periodic Inquiry Mode command
+# HCI_Periodic_Inquiry_Mode
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.4 Exit Periodic Inquiry Mode command
+# HCI_Exit_Periodic_Inquiry_Mode
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct CreateConnectionCommand:
+ -- 7.1.5 Create Connection (v1.1) (BR/EDR)
+ -- HCI_Create_Connection
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Connection Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- BD_ADDR of the device to be connected
+
+ $next [+2] PacketType packet_type
+ -- Mask of allowable packet types.
+
+ $next [+1] hci.PageScanRepetitionMode page_scan_repetition_mode
+ -- The Page Scan Repetition Mode of the remote device as retrieved by Inquiry.
+
+ $next [+1] UInt reserved
+ [requires: this == 0]
+
+ $next [+2] hci.ClockOffset clock_offset
+ -- Clock Offset. The lower 15 bits are set to the clock offset as retrieved
+ -- by an Inquiry. The highest bit is set to 1 if the rest of this parameter
+ -- is valid.
+
+ $next [+1] hci.GenericEnableParam allow_role_switch
+ -- Allow Role Switch.
+ -- Allowed values:
+ -- 0x00 - No role switch allowed, this device will be the central
+ -- 0x01 - Role switch allowed, this device may become peripheral during
+ -- connection setup
+
+
+struct DisconnectCommand:
+ -- 7.1.6 Disconnect command (v1.1) (BR/EDR & LE)
+ -- HCI_Disconnect
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Disconnection Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+
+ $next [+1] hci.StatusCode reason
+ -- Reason for the disconnect.
+
+
+struct CreateConnectionCancelCommand:
+ -- 7.1.7 Create Connection Cancel command (v1.1) (BR/EDR)
+ -- HCI_Create_Connection_Cancel
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- BD_ADDR of the Create Connection Command request
+
+
+struct AcceptConnectionRequestCommand:
+ -- 7.1.8 Accept Connection Request command (v1.1) (BR/EDR)
+ -- HCI_Accept_Connection_Request
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ $next [+1] hci.ConnectionRole role
+
+
+struct RejectConnectionRequestCommand:
+ -- 7.1.9 Reject Connection Request command (v1.1) (BR/EDR)
+ -- HCI_Reject_Connection_Request
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ $next [+1] hci.StatusCode reason
+ -- Must be one of CONNECTION_REJECTED* from hci.StatusCode in this file
+
+
+struct LinkKeyRequestReplyCommand:
+ -- 7.1.10 Link Key Request Reply command (v1.1) (BR/EDR)
+ -- HCI_Link_Key_Request_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ let bredr_link_key_size = hci.LinkKey.$size_in_bytes
+ $next [+bredr_link_key_size] hci.LinkKey link_key
+ -- Link key to use for the connection with the peer device.
+
+
+struct LinkKeyRequestNegativeReplyCommand:
+ -- 7.1.11 Link Key Request Negative Reply command (v1.1) (BR/EDR)
+ -- HCI_Link_Key_Request_Negative_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- BD_ADDR of the peer device that the host does not have a link key for.
+
+# 7.1.12 PIN Code Request Reply command
+# HCI_PIN_Code_Request_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.13 PIN Code Request Negative Reply command
+# HCI_PIN_Code_Request_Negative_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.14 Change Connection Packet Type command
+# HCI_Change_Connection_Packet_Type
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct AuthenticationRequestedCommand:
+ -- 7.1.15 Authentication Requested command (v1.1) (BR/EDR)
+ -- HCI_Authentication_Requested
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Authentication Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+
+struct SetConnectionEncryptionCommand:
+ -- 7.1.16 Set Connection Encryption command (v1.1) (BR/EDR)
+ -- HCI_Set_Connection_Encryption
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Encryption Change event will indicate that this command has been completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+ $next [+1] hci.GenericEnableParam encryption_enable
+ -- Whether link level encryption should be turned on or off.
+
+# 7.1.17 Change Connection Link Key command
+# HCI_Change_Connection_Link_Key
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.18 Link Key Selection command
+# HCI_Link_Key_Selection
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct RemoteNameRequestCommand:
+ -- 7.1.19 Remote Name Request command (v1.1) (BR/EDR)
+ -- HCI_Remote_Name_Request
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Remote Name Request Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- Address of the device whose name is to be requested.
+
+ $next [+1] hci.PageScanRepetitionMode page_scan_repetition_mode
+ -- Page Scan Repetition Mode of the device, obtained by Inquiry.
+
+ $next [+1] UInt reserved
+ [requires: this == 0]
+
+ $next [+2] hci.ClockOffset clock_offset
+ -- Clock offset. The lower 15 bits of this represent bits 16-2
+ -- of CLKNPeripheral-CLK, and the highest bit is set when the other
+ -- bits are valid.
+
+# 7.1.20 Remote Name Request Cancel command
+# HCI_Remote_Name_Request_Cancel
+
+
+struct ReadRemoteSupportedFeaturesCommand:
+ -- 7.1.21 Read Remote Supported Features command (v1.1) (BR/EDR)
+ -- HCI_Read_Remote_Supported_Features
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Read Remote Supported Features Complete event will indicate that this
+ -- command has been completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+
+struct ReadRemoteExtendedFeaturesCommand:
+ -- 7.1.22 Read Remote Extended Features command (v1.2) (BR/EDR)
+ -- HCI_Read_Remote_Extended_Features
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Read Remote Extended Features Complete event will indicate that this
+ -- command has been completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+ $next [+1] UInt page_number
+ -- Page of features to read.
+ -- Values:
+ -- - 0x00 standard features as if requested by Read Remote Supported Features
+ -- - 0x01-0xFF the corresponding features page (see Vol 2, Part C, Sec 3.3).
+
+
+struct ReadRemoteVersionInfoCommand:
+ -- 7.1.23 Read Remote Version Information command (v1.1) (BR/EDR & LE)
+ -- HCI_Read_Remote_Version_Information
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Read Remote Version Information Complete event will indicate that this
+ -- command has been completed.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+
+# 7.1.24 Read Clock Offset command
+# HCI_Read_Clock_Offset
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.25 Read LMP Handle command
+# HCI_Read_LMP_Handle
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.26 Setup Synchronous Connection command
+# HCI_Setup_Synchronous_Connection
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.27 Accept Synchronous Connection Request command
+# HCI_Accept_Synchronous_Connection_Request
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct RejectSynchronousConnectionRequestCommand:
+ -- 7.1.28 Reject Synchronous Connection command (BR/EDR)
+ -- HCI_Reject_Synchronous_Connection_Request
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- Address of the remote device that sent the request.
+
+ $next [+1] hci.StatusCode reason
+ -- Reason the connection request was rejected.
+
+
+struct IoCapabilityRequestReplyCommand:
+ -- 7.1.29 IO Capability Request Reply command (v2.1 + EDR) (BR/EDR)
+ -- HCI_IO_Capability_Request_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+ $next [+1] hci.IoCapability io_capability
+ -- The IO capabilities of this device.
+
+ $next [+1] OobDataPresent oob_data_present
+ -- Whether there is out-of-band data present, and what type.
+
+ $next [+1] hci.AuthenticationRequirements authentication_requirements
+ -- Authentication requirements of the host.
+
+
+struct UserConfirmationRequestReplyCommand:
+ -- 7.1.30 User Confirmation Request Reply command (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Confirmation_Request_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+
+struct UserConfirmationRequestNegativeReplyCommand:
+ -- 7.1.31 User Confirmation Request Negative Reply command (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Confirmation_Request_Negative_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+
+struct UserPasskeyRequestReplyCommand:
+ -- 7.1.32 User Passkey Request Reply command (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Passkey_Request_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+ $next [+4] UInt numeric_value
+ -- Numeric value (passkey) entered by user.
+ [requires: 0 <= this <= 999999]
+
+
+struct UserPasskeyRequestNegativeReplyCommand:
+ -- 7.1.33 User Passkey Request Negative Reply command (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Passkey_Request_Negative_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in the simple pairing process.
+
+# 7.1.34 Remote OOB Data Request Reply command
+# HCI_Remote_OOB_Data_Request_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.35 Remote OOB Data Request Negative Reply command
+# HCI_Remote_OOB_Data_Request_Negative_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct IoCapabilityRequestNegativeReplyCommand:
+ -- 7.1.36 IO Capability Request Negative Reply command (v2.1 + EDR) (BR/EDR)
+ -- HCI_IO_Capability_Request_Negative_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in the simple pairing process.
+
+ $next [+1] hci.StatusCode reason
+ -- Reason that Simple Pairing was rejected. See 7.1.36 for valid error codes.
+
+
+struct CodecId:
+ 0 [+1] hci.CodingFormat coding_format
+ $next [+2] UInt company_id
+ -- See assigned numbers.
+
+ $next [+2] UInt vendor_codec_id
+ -- Shall be ignored if |coding_format| is not VENDOR_SPECIFIC.
+
+
+struct SynchronousConnectionParameters:
+ -- Enhanced Setup Synchronous Connection Command (CSA2) (BR/EDR)
+ -- TODO: b/308794058 - Use CodecId instead of VendorCodingFormat
+ struct VendorCodingFormat:
+ 0 [+1] hci.CodingFormat coding_format
+ $next [+2] UInt company_id
+ -- See assigned numbers.
+
+ $next [+2] UInt vendor_codec_id
+ -- Shall be ignored if |coding_format| is not VENDOR_SPECIFIC.
+
+ enum ScoRetransmissionEffort:
+ [maximum_bits: 8]
+ NONE = 0x00
+ -- SCO or eSCO
+
+ POWER_OPTIMIZED = 0x01
+ -- eSCO only
+
+ QUALITY_OPTIMIZED = 0x02
+ -- eSCO only
+
+ DONT_CARE = 0xFF
+ -- SCO or eSCO
+
+ 0 [+4] UInt transmit_bandwidth
+ -- Transmit bandwidth in octets per second.
+
+ $next [+4] UInt receive_bandwidth
+ -- Receive bandwidth in octets per second.
+
+ let vcf_size = VendorCodingFormat.$size_in_bytes
+ $next [+vcf_size] VendorCodingFormat transmit_coding_format
+ -- Local Controller -> Remote Controller coding format.
+
+ $next [+vcf_size] VendorCodingFormat receive_coding_format
+ -- Remote Controller -> Local Controller coding format.
+
+ $next [+2] UInt transmit_codec_frame_size_bytes
+ $next [+2] UInt receive_codec_frame_size_bytes
+ $next [+4] UInt input_bandwidth
+ -- Host->Controller data rate in octets per second.
+
+ $next [+4] UInt output_bandwidth
+ -- Controller->Host data rate in octets per second.
+
+ $next [+vcf_size] VendorCodingFormat input_coding_format
+ -- Host->Controller coding format.
+
+ $next [+vcf_size] VendorCodingFormat output_coding_format
+ -- Controller->Host coding format.
+
+ $next [+2] UInt input_coded_data_size_bits
+ -- Size, in bits, of the sample or framed data.
+
+ $next [+2] UInt output_coded_data_size_bits
+ -- Size, in bits, of the sample or framed data.
+
+ $next [+1] PcmDataFormat input_pcm_data_format
+ $next [+1] PcmDataFormat output_pcm_data_format
+ $next [+1] UInt input_pcm_sample_payload_msb_position
+ -- The number of bit positions within an audio sample that the MSB of
+ -- the sample is away from starting at the MSB of the data.
+
+ $next [+1] UInt output_pcm_sample_payload_msb_position
+ -- The number of bit positions within an audio sample that the MSB of
+ -- the sample is away from starting at the MSB of the data.
+
+ $next [+1] ScoDataPath input_data_path
+ $next [+1] ScoDataPath output_data_path
+ $next [+1] UInt input_transport_unit_size_bits
+ -- The number of bits in each unit of data received from the Host over the audio data transport.
+ -- 0 indicates "not applicable" (implied by the choice of audio data transport).
+
+ $next [+1] UInt output_transport_unit_size_bits
+ -- The number of bits in each unit of data sent to the Host over the audio data transport.
+ -- 0 indicates "not applicable" (implied by the choice of audio data transport).
+
+ $next [+2] UInt max_latency_ms
+ -- The value in milliseconds representing the upper limit of the sum of
+ -- the synchronous interval, and the size of the eSCO window, where the
+ -- eSCO window is the reserved slots plus the retransmission window.
+ -- Minimum: 0x0004
+ -- Don't care: 0xFFFF
+
+ $next [+2] bits:
+ 0 [+ScoPacketType.$size_in_bits] ScoPacketType packet_types
+ -- Bitmask of allowed packet types.
+
+ $next [+1] ScoRetransmissionEffort retransmission_effort
+
+
+struct EnhancedSetupSynchronousConnectionCommand:
+ -- 7.1.45 Enhanced Setup Synchronous Connection command
+ -- HCI_Enhanced_Setup_Synchronous_Connection
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- The connection handle of the associated ACL link if creating a new (e)SCO connection, or the
+ -- handle of an existing eSCO link if updating connection parameters.
+
+ let scp_size = SynchronousConnectionParameters.$size_in_bytes
+ $next [+scp_size] SynchronousConnectionParameters connection_parameters
+
+
+struct EnhancedAcceptSynchronousConnectionRequestCommand:
+ -- 7.1.46 Enhanced Accept Synchronous Connection Request command (CSA2) (BR/EDR)
+ -- HCI_Enhanced_Accept_Synchronous_Connection_Request
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ let scp_size = SynchronousConnectionParameters.$size_in_bytes
+ $next [+scp_size] SynchronousConnectionParameters connection_parameters
+
+# 7.1.47 Truncated Page command
+# HCI_Truncated_Page
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.48 Truncated Page Cancel command
+# HCI_Truncated_Page_Cancel
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.49 Set Connectionless Peripheral Broadcast command
+# HCI_Set_Connectionless_Peripheral_Broadcast
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.50 Set Connectionless Peripheral Broadcast Receive command
+# HCI_Set_Connectionless_Peripheral_Broadcast_Receive
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.51 Start Synchronization Train command
+# HCI_Start_Synchronization_Train
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.52 Receive Synchronization Train command
+# HCI_Receive_Synchronization_Train
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.1.53 Remote OOB Extended Data Request Reply command
+# HCI_Remote_OOB_Extended_Data_Request_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+# ========== 7.3 Controller & Baseband Commands ==========
+
+
+struct SetEventMaskCommand:
+ -- 7.3.1 Set Event Mask command (v1.1)
+ -- HCI_Set_Event_Mask
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+8] UInt event_mask
+ -- 64-bit Bit mask used to control which HCI events are generated by the HCI for the
+ -- Host. See enum class EventMask in hci_constants.h
+
+
+struct ResetCommand:
+ -- 7.3.2 Reset command (v1.1)
+ -- HCI_Reset
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.3.3 Set Event Filter command
+# HCI_Set_Event_Filter
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.4 Flush command
+# HCI_Flush
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.5 Read PIN Type command
+# HCI_Read_PIN_Type
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.6 Write PIN Type command
+# HCI_Write_PIN_Type
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.8 Read Stored Link Key command
+# HCI_Read_Stored_Link_Key
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.9 Write Stored Link Key command
+# HCI_Write_Stored_Link_Key
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.10 Delete Stored Link Key command
+# HCI_Delete_Stored_Link_Key
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WriteLocalNameCommand:
+ -- 7.3.11 Write Local Name command (v1.1) (BR/EDR)
+ -- HCI_Write_Local_Name
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ let local_name_size = LocalName.$size_in_bytes
+ $next [+local_name_size] LocalName local_name
+ -- A UTF-8 encoded User Friendly Descriptive Name for the device.
+ -- If the name contained in the parameter is shorter than 248 octets, the end
+ -- of the name is indicated by a NULL octet (0x00), and the following octets
+ -- (to fill up 248 octets, which is the length of the parameter) do not have
+ -- valid values.
+
+
+struct ReadLocalNameCommand:
+ -- 7.3.12 Read Local Name command (v1.1) (BR/EDR)
+ -- HCI_Read_Local_Name
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.3.13 Read Connection Accept Timeout command
+# HCI_Read_Connection_Accept_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.14 Write Connection Accept Timeout command
+# HCI_Write_Connection_Accept_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.15 Read Page Timeout command
+# HCI_Read_Page_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WritePageTimeoutCommand:
+ -- 7.3.16 Write Page Timeout command (v1.1) (BR/EDR)
+ -- HCI_Write_Page_Timeout
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt page_timeout
+ -- Page_Timeout, in time slices (0.625 ms)
+ -- Range: From MIN to MAX in PageTimeout in this file
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+
+struct ReadScanEnableCommand:
+ -- 7.3.17 Read Scan Enable command (v1.1) (BR/EDR)
+ -- HCI_Read_Scan_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WriteScanEnableCommand:
+ -- 7.3.18 Write Scan Enable command (v1.1) (BR/EDR)
+ -- HCI_Write_Scan_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] bits:
+ 0 [+ScanEnableBits.$size_in_bits] ScanEnableBits scan_enable
+
+
+struct ReadPageScanActivityCommand:
+ -- 7.3.19 Read Page Scan Activity command (v1.1) (BR/EDR)
+ -- HCI_Read_Page_Scan_Activity
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WritePageScanActivityCommand:
+ -- 7.3.20 Write Page Scan Activity command (v1.1) (BR/EDR)
+ -- HCI_Write_Page_Scan_Activity
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt page_scan_interval
+ -- Page_Scan_Interval, in time slices (0.625ms)
+ -- Valid Range: MIN - MAX in ScanInterval in this file
+ [requires: 0x0012 <= this <= 0x1000]
+
+ $next [+2] UInt page_scan_window
+ -- Page_Scan_Window, in time slices
+ -- Valid Range: MIN - MAX in ScanWindow in this file
+ [requires: 0x0011 <= this <= 0x1000]
+
+
+struct ReadInquiryScanActivityCommand:
+ -- 7.3.21 Read Inquiry Scan Activity command (v1.1) (BR/EDR)
+ -- HCI_Read_Inquiry_Scan_Activity
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WriteInquiryScanActivityCommand:
+ -- 7.3.22 Write Inquiry Scan Activity command (v1.1) (BR/EDR)
+ -- HCI_Write_Inquiry_Scan_Activity
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt inquiry_scan_interval
+ -- Inquiry_Scan_Interval, in time slices (0.625ms)
+ -- Valid Range: MIN - MAX in ScanInterval in this file
+ [requires: 0x0012 <= this <= 0x1000]
+
+ $next [+2] UInt inquiry_scan_window
+ -- Inquiry_Scan_Window, in time slices
+ -- Valid Range: MIN - MAX in ScanWindow in this file
+ [requires: 0x0011 <= this <= 0x1000]
+
+# 7.3.23 Read Authentication Enable command
+# HCI_Read_Authentication_Enable
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.24 Write Authentication Enable command
+# HCI_Write_Authentication_Enable
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct ReadClassOfDeviceCommand:
+ -- 7.3.25 Read Class of Device command (v1.1) (BR/EDR)
+ -- HCI_Read_Class_Of_Device
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WriteClassOfDeviceCommand:
+ -- 7.3.26 Write Class Of Device command (v1.1) (BR/EDR)
+ -- HCI_Write_Class_Of_Device
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+3] hci.ClassOfDevice class_of_device
+
+# 7.3.27 Read Voice Setting command
+# HCI_Read_Voice_Setting
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.28 Write Voice Setting command
+# HCI_Write_Voice_Setting
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.29 Read Automatic Flush Timeout command
+# HCI_Read_Automatic_Flush_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WriteAutomaticFlushTimeoutCommand:
+ -- 7.3.30 Write Automatic Flush Timeout command (v1.1) (BR/EDR)
+ -- HCI_Write_Automatic_Flush_Timeout
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt flush_timeout
+ -- The value for the Flush_Timeout configuration parameter (Core Spec v5.2, Vol 4, Part E, Sec 6.19).
+ -- Range: 0x0000 to 0x07FF. 0x0000 indicates infinite flush timeout (no automatic flush).
+ -- Time = flush_timeout * 0.625ms.
+ -- Time Range: 0.625ms to 1279.375ms.
+ [requires: 0x0000 <= this <= 0x07FF]
+
+# 7.3.31 Read Num Broadcast Retransmissions command
+# HCI_Read_Num_Broadcast_Retransmissions
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.32 Write Num Broadcast Retransmissions command
+# HCI_Write_Num_Broadcast_Retransmissions
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.33 Read Hold Mode Activity command
+# HCI_Read_Hold_Mode_Activity
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.34 Write Hold Mode Activity command
+# HCI_Write_Hold_Mode_Activity
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.35 Read Transmit Power Level command
+# HCI_Read_Transmit_Power_Level
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.36 Read Synchronous Flow Control Enable command
+# HCI_Read_Synchronous_Flow_Control_Enable
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WriteSynchronousFlowControlEnableCommand:
+ -- 7.3.37 Write Synchonous Flow Control Enable command (BR/EDR)
+ -- HCI_Write_Synchronous_Flow_Control_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam synchronous_flow_control_enable
+ -- If enabled, HCI_Number_Of_Completed_Packets events shall be sent from the controller
+ -- for synchronous connection handles.
+
+# 7.3.38 Set Controller To Host Flow Control command
+# HCI_Set_Controller_To_Host_Flow_Control
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.39 Host Buffer Size command
+# HCI_Host_Buffer_Size
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.40 Host Number Of Completed Packets command
+# HCI_Host_Number_Of_Completed_Packets
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.41 Read Link Supervision Timeout command
+# HCI_Read_Link_Supervision_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.42 Write Link Supervision Timeout command
+# HCI_Write_Link_Supervision_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.43 Read Number Of Supported IAC command
+# HCI_Read_Number_Of_Supported_IAC
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.44 Read Current IAC LAP command
+# HCI_Read_Current_IAC_LAP
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.45 Write Current IAC LAP command
+# HCI_Write_Current_IAC_LAP
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.46 Set AFH Host Channel Classification command
+# HCI_Set_AFH_Host_Channel_Classification
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.47 Read Inquiry Scan Type command
+# HCI_Read_Inquiry_Scan_Type
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WriteInquiryScanTypeCommand:
+ -- 7.3.48 Write Inquiry Scan Type (v1.2) (BR/EDR)
+ -- HCI_Write_Inquiry_Scan_Type
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] InquiryScanType inquiry_scan_type
+ -- See enum class InquiryScanType in this file for possible values
+
+
+struct ReadInquiryModeCommand:
+ -- 7.3.49 Read Inquiry Mode (v1.2) (BR/EDR)
+ -- HCI_Read_Inquiry_Mode
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WriteInquiryModeCommand:
+ -- 7.3.50 Write Inquiry Mode (v1.2) (BR/EDR)
+ -- HCI_Write_Inquiry_Mode
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] InquiryMode inquiry_mode
+
+
+struct ReadPageScanTypeCommand:
+ -- 7.3.51 Read Page Scan Type (v1.2) (BR/EDR)
+ -- HCI_Read_Page_Scan_Type
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WritePageScanTypeCommand:
+ -- 7.3.52 Write Page Scan Type (v1.2) (BR/EDR)
+ -- HCI_Write_Page_Scan_Type
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] PageScanType page_scan_type
+
+# 7.3.53 Read AFH Channel Assessment Mode command
+# HCI_Read_AFH_Channel_Assessment_Mode
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.54 Write AFH Channel Assessment Mode command
+# HCI_Write_AFH_Channel_Assessment_Mode
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.55 Read Extended Inquiry Response command
+# HCI_Read_Extended_Inquiry_Response
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WriteExtendedInquiryResponseCommand:
+ -- 7.3.56 Write Extended Inquiry Response (v1.2) (BR/EDR)
+ -- HCI_Write_Extended_Inquiry_Response
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt fec_required
+ -- If FEC Encoding is required. (v1.2) (7.3.56)
+
+ let eir_size = ExtendedInquiryResponse.$size_in_bytes
+ $next [+eir_size] ExtendedInquiryResponse extended_inquiry_response
+ -- Extended inquiry response data as defined in Vol 3, Part C, Sec 8
+
+# 7.3.57 Refresh Encryption Key command
+# HCI_Refresh_Encryption_Key
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct ReadSimplePairingModeCommand:
+ -- 7.3.58 Read Simple Pairing Mode (v2.1 + EDR) (BR/EDR)
+ -- HCI_Read_Simple_Pairing_Mode
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct WriteSimplePairingModeCommand:
+ -- 7.3.59 Write Simple Pairing Mode (v2.1 + EDR) (BR/EDR)
+ -- HCI_Write_Simple_Pairing_Mode
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam simple_pairing_mode
+
+# 7.3.60 Read Local OOB Data command
+# HCI_Read_Local_OOB_Data
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.61 Read Inquiry Response Transmit Power Level command
+# HCI_Read_Inquiry_Response_Transmit_Power_Level
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.62 Write Inquiry Transmit Power Level command
+# HCI_Write_Inquiry_Transmit_Power_Level
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.63 Send Keypress Notification command
+# HCI_Send_Keypress_Notification
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.64 Read Default Erroneous Data Reporting command
+# HCI_Read_Default_Erroneous_Data_Reporting
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.65 Write Default Erroneous Data Reporting command
+# HCI_Write_Default_Erroneous_Data_Reporting
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.66 Enhanced Flush command
+# HCI_Enhanced_Flush
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct SetEventMaskPage2Command:
+ -- 7.3.69 Set Event Mask Page 2 command (v3.0 + HS)
+ -- HCI_Set_Event_Mask_Page_2
+ 0 [+hci.CommandHeader.$size_in_bytes] hci.CommandHeader header
+ $next [+8] bits:
+ 0 [+26] EventMaskPage2 event_mask_page_2
+ -- Bit mask used to control which HCI events are generated by the HCI for the Host.
+
+
+struct ReadFlowControlModeCommand:
+ -- 7.3.72 Read Flow Control Mode command (v3.0 + HS) (BR/EDR)
+ -- HCI_Read_Flow_Control_Mode
+ 0 [+hci.CommandHeader.$size_in_bytes] hci.CommandHeader header
+
+
+struct WriteFlowControlModeCommand:
+ -- 7.3.73 Write Flow Control Mode command (v3.0 + HS) (BR/EDR)
+ -- HCI_Write_Flow_Control_Mode
+ 0 [+hci.CommandHeader.$size_in_bytes] hci.CommandHeader header
+ $next [+1] FlowControlMode flow_control_mode
+
+# 7.3.74 Read Enhanced Transmit Power Level command
+# HCI_Read_Enhanced_Transmit_Power_Level
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct ReadLEHostSupportCommand:
+ -- 7.3.78 Read LE Host Support command (v4.0) (BR/EDR)
+ -- HCI_Read_LE_Host_Support
+ 0 [+hci.CommandHeader.$size_in_bytes] hci.CommandHeader header
+
+
+struct WriteLEHostSupportCommand:
+ -- 7.3.79 Write LE Host Support command (v4.0) (BR/EDR)
+ -- HCI_Write_LE_Host_Support
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam le_supported_host
+ -- Sets the LE Supported (Host) Link Manager Protocol feature bit.
+
+ $next [+1] UInt unused
+ -- Core Spec v5.0, Vol 2, Part E, Section 6.35: This parameter was named
+ -- "Simultaneous_LE_Host" and the value is set to "disabled(0x00)" and
+ -- "shall be ignored".
+ -- Core Spec v5.3, Vol 4, Part E, Section 7.3.79: This parameter was renamed
+ -- to "Unused" and "shall be ignored by the controller".
+
+# 7.3.80 Set MWS Channel Parameters command
+# HCI_Set_MWS_Channel_Parameters
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.81 Set External Frame Configuration command
+# HCI_Set_External_Frame_Configuration
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.82 Set MWS Signaling command
+# HCI_Set_MWS_Signaling
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.83 Set MWS Transport Layer command
+# HCI_Set_MWS_Transport_Layer
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.84 Set MWS Scan Frequency Table command
+# HCI_Set_MWS_Scan_Frequency_Table
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.85 Set MWS_PATTERN Configuration command
+# HCI_Set_MWS_PATTERN_Configuration
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.86 Set Reserved LT_ADDR command
+# HCI_Set_Reserved_LT_ADDR
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.87 Delete Reserved LT_ADDR command
+# HCI_Delete_Reserved_LT_ADDR
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.88 Set Connectionless Peripheral Broadcast Data command
+# HCI_Set_Connectionless_Peripheral_Broadcast_Data
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.89 Read Synchronization Train Parameters command
+# HCI_Read_Synchronization_Train_Parameters
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.90 Write Synchronization Train Parameters command
+# HCI_Write_Synchronization_Train_Parameters
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.91 Read Secure Connections Host Support command
+# HCI_Read_Secure_Connections_Host_Support
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct WriteSecureConnectionsHostSupportCommand:
+ -- 7.3.92 Write Secure Connections Host Support command
+ -- HCI_Write_Secure_Connections_Host_Support
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam secure_connections_host_support
+
+
+struct ReadAuthenticatedPayloadTimeoutCommand:
+ -- 7.3.93 Read Authenticated Payload Timeout command (v4.1) (BR/EDR & LE)
+ -- HCI_Read_Authenticated_Payload_Timeout
+ 0 [+hci.CommandHeader.$size_in_bytes] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct WriteAuthenticatedPayloadTimeoutCommand:
+ -- 7.3.94 Write Authenticated Payload Timeout command (v4.1) (BR/EDR & LE)
+ -- HCI_Write_Authenticated_Payload_Timeout
+ 0 [+hci.CommandHeader.$size_in_bytes] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt authenticated_payload_timeout
+ -- Default = 0x0BB8 (30 s)
+ -- Time = N * 10 ms
+ -- Time Range: 10 ms to 655,350 ms
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+# 7.3.95 Read Local OOB Extended Data command
+# HCI_Read_Local_OOB_Extended_Data
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.96 Read Extended Page Timeout command
+# HCI_Read_Extended_Page_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.97 Write Extended Page Timeout command
+# HCI_Write_Extended_Page_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.98 Read Extended Inquiry Length command
+# HCI_Read_Extended_Inquiry_Length
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.99 Write Extended Inquiry Length command
+# HCI_Write_Extended_Inquiry_Length
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.100 Set Ecosystem Base Interval command
+# HCI_Set_Ecosystem_Base_Interval
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.101 Configure Data Path command
+# HCI_Configure_Data_Path
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.3.102 Set Min Encryption Key Size command
+# HCI_Set_Min_Encryption_Key_size
+# TODO: b/265052417 - Definition needs to be added
+
+
+# ========== 7.4 Informational Parameters ==========
+
+
+struct ReadLocalVersionInformationCommand:
+ -- 7.4.1 Read Local Version Information command (v1.1)
+ -- HCI_Read_Local_Version_Information
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct ReadLocalSupportedCommandsCommand:
+ -- 7.4.2 Read Local Supported Commands command (v1.2)
+ -- HCI_Read_Local_Supported_Commands
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct ReadLocalSupportedFeaturesCommand:
+ -- 7.4.3 Read Local Supported Features command (v1.1)
+ -- HCI_Read_Local_Supported_Features
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct ReadLocalExtendedFeaturesCommand:
+ -- 7.4.4 Read Local Extended Features command (v1.2) (BR/EDR)
+ -- HCI_Read_Local_Extended_Features
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt page_number
+ -- 0x00: Requests the normal LMP features as returned by
+ -- Read_Local_Supported_Features.
+ -- 0x01-0xFF: Return the corresponding page of features.
+
+
+struct ReadBufferSizeCommand:
+ -- 7.4.5 Read Buffer Size command (v1.1)
+ -- HCI_Read_Buffer_Size
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct ReadBdAddrCommand:
+ -- 7.4.6 Read BD_ADDR command (v1.1) (BR/EDR, LE)
+ -- HCI_Read_BD_ADDR
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.4.7 Read Data Block Size command
+# HCI_Read_Data_Block_Size
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.4.8 Read Local Supported Codecs command
+# HCI_Read_Local_Supported_Codecs [v1][v2]
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.4.9 Read Local Simple Pairing Options command
+# HCI_Read_Local_Simple_Pairing_Options
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.4.10 Read Local Supported Codec Capabilities command
+# HCI_Read_Local_Supported_Codec_Capabilities
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.4.11 Read Local Supported Controller Delay command
+# HCI_Read_Local_Supported_Controller_Delay
+# TODO: b/265052417 - Definition needs to be added
+
+
+# ========== 7.5 Status Parameters ==========
+
+
+struct ReadEncryptionKeySizeCommand:
+ -- 7.5.6 Read Encryption Key Size (v1.1) (BR/EDR)
+ -- HCI_Read_Encryption_Key_Size
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Identifies an active ACL link (only the lower 12 bits are meaningful).
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+# ========== 7.8 LE Controller Commands ==========
+
+
+struct LESetEventMaskCommand:
+ -- 7.8.1 LE Set Event Mask command (v4.0) (LE)
+ -- HCI_LE_Set_Event_Mask
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+8] bits:
+ 0 [+35] LEEventMask le_event_mask
+ -- Bitmask that indicates which LE events are generated by the HCI for the Host.
+
+
+struct LEReadBufferSizeCommandV1:
+ -- 7.8.2 LE Read Buffer Size command [v1] (v4.0) (LE)
+ -- HCI_LE_Read_Buffer_Size [v1]
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEReadBufferSizeCommandV2:
+ -- 7.8.2 LE Read Buffer Size command [v2] (v5.2) (LE)
+ -- HCI_LE_Read_Buffer_Size [v2]
+ -- Version 2 of this command changed the opcode and added ISO return
+ -- parameters.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEReadLocalSupportedFeaturesCommand:
+ -- 7.8.3 LE Read Local Supported Features command (v4.0) (LE)
+ -- HCI_LE_Read_Local_Supported_Features
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LESetRandomAddressCommand:
+ -- 7.8.4 LE Set Random Address command (v4.0) (LE)
+ -- HCI_LE_Set_Random_Address
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr random_address
+
+
+struct LESetAdvertisingParametersCommand:
+ -- 7.8.5 LE Set Advertising Parameters command (v4.0) (LE)
+ -- HCI_LE_Set_Advertising_Parameters
+
+ [requires: advertising_interval_min <= advertising_interval_max]
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+2] UInt advertising_interval_min
+ -- Default: 0x0800 (1.28 s)
+ -- Time: N * 0.625 ms
+ -- Time Range: 20 ms to 10.24 s
+ [requires: 0x0020 <= this <= 0x4000]
+
+ $next [+2] UInt advertising_interval_max
+ -- Default: 0x0800 (1.28 s)
+ -- Time: N * 0.625 ms
+ -- Time Range: 20 ms to 10.24 s
+ [requires: 0x0020 <= this <= 0x4000]
+
+ $next [+1] LEAdvertisingType adv_type
+ -- Used to determine the packet type that is used for advertising when
+ -- advertising is enabled.
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+1] hci.LEPeerAddressType peer_address_type
+ -- ANONYMOUS address type not allowed.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the device to be connected.
+
+ $next [+1] bits:
+
+ 0 [+3] LEAdvertisingChannels advertising_channel_map
+ -- Indicates the advertising channels that shall be used when transmitting
+ -- advertising packets. At least 1 channel must be enabled.
+ -- Default: all channels enabled
+
+ $next [+1] LEAdvertisingFilterPolicy advertising_filter_policy
+ -- This parameter shall be ignored when directed advertising is enabled.
+
+
+struct LEReadAdvertisingChannelTxPowerCommand:
+ -- 7.8.6 LE Read Advertising Channel Tx Power command (v4.0) (LE)
+ -- HCI_LE_Read_Advertising_Channel_Tx_Power
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LESetAdvertisingDataCommand:
+ -- 7.8.7 LE Set Advertising Data command (v4.0) (LE)
+ -- HCI_LE_Set_Advertising_Data
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt advertising_data_length
+ -- The number of significant octets in `advertising_data`.
+ [requires: 0x00 <= this <= 0x1F]
+
+ $next [+31] UInt:8[31] advertising_data
+ -- 31 octets of advertising data formatted as defined in Core Spec
+ -- v5.3, Vol 3, Part C, Section 11.
+ -- Default: All octets zero
+
+
+struct LESetScanResponseDataCommand:
+ -- 7.8.8 LE Set Scan Response Data command (v4.0) (LE)
+ -- HCI_LE_Set_Scan_Response_Data
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt scan_response_data_length
+ -- The number of significant octets in `scan_response_data`.
+ [requires: 0x00 <= this <= 0x1F]
+
+ $next [+31] UInt:8[31] scan_response_data
+ -- 31 octets of scan response data formatted as defined in Core Spec
+ -- v5.3, Vol 3, Part C, Section 11.
+ -- Default: All octets zero
+
+
+struct LESetAdvertisingEnableCommand:
+ -- 7.8.9 LE Set Advertising Enable command (v4.0) (LE)
+ -- HCI_LE_Set_Advertising_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam advertising_enable
+
+
+struct LESetScanParametersCommand:
+ -- 7.8.10 LE Set Scan Parameters command (v4.0) (LE)
+ -- HCI_LE_Set_Scan_Parameters
+
+ [requires: le_scan_window <= le_scan_interval]
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+1] LEScanType le_scan_type
+ -- Controls the type of scan to perform.
+
+ $next [+2] UInt le_scan_interval
+ -- Default: 0x0010 (10ms)
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+2] UInt le_scan_window
+ -- Default: 0x0010 (10ms)
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+1] LEOwnAddressType own_address_type
+ -- The type of address being used in the scan request packets.
+
+ $next [+1] LEScanFilterPolicy scanning_filter_policy
+
+
+struct LESetScanEnableCommand:
+ -- 7.8.11 LE Set Scan Enable command (v4.0) (LE)
+ -- HCI_LE_Set_Scan_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam le_scan_enable
+ $next [+1] hci.GenericEnableParam filter_duplicates
+ -- Controls whether the Link Layer should filter out duplicate advertising
+ -- reports to the Host, or if the Link Layer should generate advertising
+ -- reports for each packet received. Ignored if le_scan_enable is set to
+ -- disabled.
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.4.3.5
+
+
+struct LECreateConnectionCommand:
+ -- 7.8.12 LE Create Connection command (v4.0) (LE)
+ -- HCI_LE_Create_Connection
+
+ [requires: le_scan_window <= le_scan_interval && connection_interval_min <= connection_interval_max]
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+2] UInt le_scan_interval
+ -- The time interval from when the Controller started the last LE scan until
+ -- it begins the subsequent LE scan.
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+2] UInt le_scan_window
+ -- Amount of time for the duration of the LE scan.
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+1] hci.GenericEnableParam initiator_filter_policy
+
+ $next [+1] hci.LEAddressType peer_address_type
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_address
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+2] UInt connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt max_latency
+ -- Maximum Peripheral latency for the connection in number of connection
+ -- events.
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+2] UInt min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2] UInt max_connection_event_length
+ -- Time: N * 0.625 ms
+
+
+struct LECreateConnectionCancelCommand:
+ -- 7.8.13 LE Create Connection Cancel command (v4.0) (LE)
+ -- HCI_LE_Create_Connection_Cancel
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.8.14 LE Read Filter Accept List Size command
+# HCI_LE_Read_Filter_Accept_List_Size
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEClearFilterAcceptListCommand:
+ -- 7.8.15 LE Clear Filter Accept List command (v4.0) (LE)
+ -- HCI_LE_Clear_Filter_Accept_List
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEAddDeviceToFilterAcceptListCommand:
+ -- 7.8.16 LE Add Device To Filter Accept List command (v4.0) (LE)
+ -- HCI_LE_Add_Device_To_Filter_Accept_List
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.LEPeerAddressType address_type
+ -- The address type of the peer.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr address
+ -- Public Device Address or Random Device Address of the device to be added
+ -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
+
+
+struct LERemoveDeviceFromFilterAcceptListCommand:
+ -- 7.8.17 LE Remove Device From Filter Accept List command (v4.0) (LE)
+ -- HCI_LE_Remove_Device_From_Filter_Accept_List
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.LEPeerAddressType address_type
+ -- The address type of the peer.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr address
+ -- Public Device Address or Random Device Address of the device to be added
+ -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
+
+
+struct LEConnectionUpdateCommand:
+ -- 7.8.18 LE Connection Update command (v4.0) (LE)
+ -- HCI_LE_Connection_Update
+
+ [requires: connection_interval_min <= connection_interval_max && min_connection_event_length <= max_connection_event_length]
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt max_latency
+ -- Maximum Peripheral latency for the connection in number of subrated
+ -- connection events.
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+2] UInt min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2] UInt max_connection_event_length
+ -- Time: N * 0.625 ms
+
+# 7.8.19 LE Set Host Channel Classification command
+# HCI_LE_Set_Host_Channel_Classification
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.20 LE Read Channel Map command
+# HCI_LE_Read_Channel_Map
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEReadRemoteFeaturesCommand:
+ -- 7.8.21 LE Read Remote Features command (v4.0) (LE)
+ -- HCI_LE_Read_Remote_Features
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+# 7.8.22 LE Encrypt command
+# HCI_LE_Encrypt
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.23 LE Rand command
+# HCI_LE_Rand
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEEnableEncryptionCommand:
+ -- 7.8.24 LE Enable Encryption command (v4.0) (LE)
+ -- HCI_LE_Enable_Encryption
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] UInt random_number
+ $next [+2] UInt encrypted_diversifier
+ $next [+hci.LinkKey.$size_in_bytes] hci.LinkKey long_term_key
+
+
+struct LELongTermKeyRequestReplyCommand:
+ -- 7.8.25 LE Long Term Key Request Reply command (v4.0) (LE)
+ -- HCI_LE_Long_Term_Key_Request_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+hci.LinkKey.$size_in_bytes] hci.LinkKey long_term_key
+
+
+struct LELongTermKeyRequestNegativeReplyCommand:
+ -- 7.8.26 LE Long Term Key Request Negative Reply command (v4.0) (LE)
+ -- HCI_LE_Long_Term_Key_Request_Negative_Reply
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEReadSupportedStatesCommand:
+ -- 7.8.27 LE Read Supported States command (v4.0) (LE)
+ -- HCI_LE_Read_Supported_States
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.8.28 LE Receiver Test command
+# HCI_LE_Receiver_Test [v1] [v2] [v3]
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.29 LE Transmitter Test command
+# HCI_LE_Transmitter_Test [v1] [v2] [v3] [v4]
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.30 LE Test End command
+# HCI_LE_Test_End
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.31 LE Remote Connection Parameter Request Reply command
+# HCI_LE_Remote_Connection_Parameter_Request_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.32 LE Remote Connection Parameter Request Negative Reply command
+# HCI_LE_Remote_Connection_Parameter_Request_Negative_Reply
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.33 LE Set Data Length command
+# HCI_LE_Set_Data_Length
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.34 LE Read Suggested Default Data Length command
+# HCI_LE_Read_Suggested_Default_Data_Length
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.35 LE Write Suggested Default Data Length command
+# HCI_LE_Write_Suggested_Default_Data_Length
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.36 LE Read Local P-256 Public Key command
+# HCI_LE_Read_Local_P-256_Public_Key
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.37 LE Generate DHKey command
+# HCI_LE_Generate_DHKey [v1] [v2]
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.38 LE Add Device To Resolving List command
+# HCI_LE_Add_Device_To_Resolving_List
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.39 LE Remove Device From Resolving List command
+# HCI_LE_Remove_Device_From_Resolving_List
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEClearResolvingListCommand:
+ -- 7.8.40 LE Clear Resolving List command (v4.2) (LE)
+ -- HCI_LE_Clear_Resolving_List
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.8.41 LE Read Resolving List Size command
+# HCI_LE_Read_Resolving_List_Size
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.42 LE Read Peer Resolvable Address command
+# HCI_LE_Read_Peer_Resolvable_Address
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.43 LE Read Local Resolvable Address command
+# HCI_LE_Read_Local_Resolvable_Address
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LESetAddressResolutionEnableCommand:
+ -- 7.8.44 LE Set Address Resolution Enable command (v4.2) (LE)
+ -- HCI_LE_Set_Address_Resolution_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam address_resolution_enable
+
+# 7.8.45 LE Set Resolvable Private Address Timeout command
+# HCI_LE_Set_Resolvable_Private_Address_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.46 LE Read Maximum Data Length command
+# HCI_LE_Read_Maximum_Data_Length
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.47 LE Read PHY command
+# HCI_LE_Read_PHY
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.48 LE Set Default PHY command
+# HCI_LE_Set_Default_PHY
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.49 LE Set PHY command
+# HCI_LE_Set_PHY
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LESetAdvertisingSetRandomAddressCommand:
+ -- 7.8.52 LE Set Advertising Set Random Address command (v5.0) (LE)
+ -- HCI_LE_Set_Advertising_Set_Random_Address
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr random_address
+ -- The random address to use in the advertising PDUs.
+
+
+struct LESetExtendedAdvertisingParametersV1Command:
+ -- 7.8.53 LE Set Extended Advertising Parameters [v1] command (v5.0) (LE)
+ -- HCI_LE_Set_Extended_Advertising_Parameters [v1]
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+2] bits:
+
+ 0 [+7] LEAdvertisingEventProperties advertising_event_properties
+
+ $next [+3] UInt primary_advertising_interval_min
+ -- Time = N * 0.625 s
+ -- Time Range: 20 ms to 10,485.759375 s
+ [requires: 0x000020 <= this]
+
+ $next [+3] UInt primary_advertising_interval_max
+ -- Time = N * 0.625 s
+ -- Time Range: 20 ms to 10,485.759375 s
+ [requires: 0x000020 <= this]
+
+ $next [+1] bits:
+
+ 0 [+3] LEAdvertisingChannels primary_advertising_channel_map
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+1] hci.LEPeerAddressTypeNoAnon peer_address_type
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or Random (static)
+ -- Identity Address of the device to be connected.
+
+ $next [+1] LEAdvertisingFilterPolicy advertising_filter_policy
+
+ $next [+1] Int advertising_tx_power
+ -- Range: -127 <= N <= +126
+ -- Units: dBm
+ -- If N = 127: Host has no preference.
+ [requires: -127 <= this]
+
+ $next [+1] hci.LEPrimaryAdvertisingPHY primary_advertising_phy
+ -- LEPHY::kLE2M and LEPHY::kLECodedS2 are excluded.
+
+ $next [+1] UInt secondary_advertising_max_skip
+ -- Maximum advertising events the controller can skip before sending the AUX_ADV_IND packets on
+ -- the secondary advertising physical channel. If this value is zero, AUX_ADV_IND shall be sent
+ -- prior to the next advertising event.
+
+ $next [+1] hci.LESecondaryAdvertisingPHY secondary_advertising_phy
+
+ $next [+1] UInt advertising_sid
+ -- Value of the Advertising SID subfield in the ADI field of the PDU
+ [requires: 0x00 <= this <= 0x0F]
+
+ $next [+1] hci.GenericEnableParam scan_request_notification_enable
+
+# TODO: b/265052417 - LE Set Extended Advertising Parameters [v2] definition needs to be added
+
+
+struct LESetExtendedAdvertisingDataCommand:
+ -- 7.8.54 LE Set Extended Advertising Data command (v5.0) (LE)
+ -- HCI_LE_Set_Extended_Advertising_Data
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+1] LESetExtendedAdvDataOp operation
+
+ $next [+1] LEExtendedAdvFragmentPreference fragment_preference
+ -- Provides a hint to the Controller as to whether advertising data should be fragmented.
+
+ $next [+1] UInt advertising_data_length (sz)
+ -- Length of the advertising data included in this command packet, up to
+ -- kMaxLEExtendedAdvertisingDataLength bytes. If the advertising set uses legacy advertising
+ -- PDUs that support advertising data then this shall not exceed kMaxLEAdvertisingDataLength
+ -- bytes.
+ [requires: 0 <= this <= 251]
+
+ $next [+sz] UInt:8[sz] advertising_data
+ -- Variable length advertising data.
+
+
+struct LESetExtendedScanResponseDataCommand:
+ -- 7.8.55 LE Set Extended Scan Response Data command (v5.0) (LE)
+ -- HCI_LE_Set_Extended_Scan_Response_Data
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt advertising_handle
+ -- Used to identify an advertising set
+ [requires: 0x00 <= this <= 0xEF]
+
+ $next [+1] LESetExtendedAdvDataOp operation
+ $next [+1] LEExtendedAdvFragmentPreference fragment_preference
+ -- Provides a hint to the controller as to whether advertising data should be fragmented
+
+ $next [+1] UInt scan_response_data_length (sz)
+ -- The number of octets in the scan_response_data parameter
+ [requires: 0 <= this <= 251]
+
+ $next [+sz] UInt:8[sz] scan_response_data
+ -- Scan response data formatted as defined in Core Spec v5.4, Vol 3, Part C, Section 11
+
+
+struct LESetExtendedAdvertisingEnableData:
+ -- Data fields for variable-length portion of an LE Set Extended Advertising Enable command
+ 0 [+1] UInt advertising_handle
+ $next [+2] UInt duration
+ $next [+1] UInt max_extended_advertising_events
+
+
+struct LESetExtendedAdvertisingEnableCommand:
+ -- 7.8.56 LE Set Extended Advertising Enable command (v5.0) (LE)
+ -- HCI_LE_Set_Extended_Advertising_Enable
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam enable
+ $next [+1] UInt num_sets
+ let single_data_size = LESetExtendedAdvertisingEnableData.$size_in_bytes
+ $next [+single_data_size*num_sets] LESetExtendedAdvertisingEnableData[] data
+
+
+struct LEReadMaxAdvertisingDataLengthCommand:
+ -- 7.8.57 LE Read Maximum Advertising Data Length command (v5.0) (LE)
+ -- HCI_LE_Read_Maximum_Advertising_Data_Length
+ -- This command has no parameters
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEReadNumSupportedAdvertisingSetsCommand:
+ -- 7.8.58 LE Read Number of Supported Advertising Sets command (v5.0) (LE)
+ -- HCI_LE_Read_Number_of_Supported_Advertising_Sets
+ -- This command has no parameters
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LERemoveAdvertisingSetCommand:
+ -- 7.8.59 LE Remove Advertising Set command (v5.0) (LE)
+ -- HCI_LE_Remove_Advertising_Set
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt advertising_handle
+
+
+struct LEClearAdvertisingSetsCommand:
+ -- 7.8.60 LE Clear Advertising Sets command (v5.0) (LE)
+ -- HCI_LE_Clear_Advertising_Sets
+ -- This command has no parameters
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# 7.8.61 LE Set Periodic Advertising Parameters command
+# HCI_LE_Set_Periodic_Advertising_Parameters [v1] [v2]
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.62 LE Set Periodic Advertising Data command
+# HCI_LE_Set_Periodic_Advertising_Data
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.8.63 LE Set Periodic Advertising Enable command
+# HCI_LE_Set_Periodic_Advertising_Enable
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LESetExtendedScanParametersData:
+ -- Data fields for variable-length portion of an LE Set Extneded Scan Parameters command
+
+ 0 [+1] LEScanType scan_type
+
+ $next [+2] UInt scan_interval
+ -- Time interval from when the Controller started its last scan until it begins the subsequent
+ -- scan on the primary advertising physical channel.
+ -- Time = N × 0.625 ms
+ -- Time Range: 2.5 ms to 40.959375 s
+ [requires: 0x0004 <= this]
+
+ $next [+2] UInt scan_window
+ -- Duration of the scan on the primary advertising physical channel.
+ -- Time = N × 0.625 ms
+ -- Time Range: 2.5 ms to 40.959375 s
+ [requires: 0x0004 <= this]
+
+
+struct LESetExtendedScanParametersCommand:
+ -- 7.8.64 LE Set Extended Scan Parameters command (v5.0) (LE)
+ -- HCI_LE_Set_Extended_Scan_Parameters
+ -- num_entries corresponds to the number of bits set in the |scanning_phys| field
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] LEOwnAddressType own_address_type
+ $next [+1] LEScanFilterPolicy scanning_filter_policy
+ $next [+1] bits:
+ 0 [+LEScanPHYBits.$size_in_bits] LEScanPHYBits scanning_phys
+
+ let single_entry_size = LESetExtendedScanParametersData.$size_in_bytes
+ let num_entries = (scanning_phys.le_1m ? 1 : 0)+(scanning_phys.le_coded ? 1 : 0)
+ let total_entries_size = num_entries*single_entry_size
+ $next [+total_entries_size] LESetExtendedScanParametersData[num_entries] data
+
+
+struct LESetExtendedScanEnableCommand:
+ -- 7.8.65 LE Set Extended Scan Enable command (v5.0) (LE)
+ -- HCI_LE_Set_Extended_Scan_Enable
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+1] hci.GenericEnableParam scanning_enabled
+
+ $next [+1] LEExtendedDuplicateFilteringOption filter_duplicates
+ -- See enum class LEExtendedDuplicateFilteringOption in this file for possible values
+
+ $next [+2] UInt duration
+ -- Possible values:
+ -- 0x0000: Scan continuously until explicitly disabled
+ -- 0x0001-0xFFFF: Scan duration, where:
+ -- Time = N * 10 ms
+ -- Time Range: 10 ms to 655.35 s
+
+ $next [+2] UInt period
+ -- Possible values:
+ -- 0x0000: Periodic scanning disabled (scan continuously)
+ -- 0x0001-0xFFFF: Time interval from when the Controller started its last
+ -- Scan_Duration until it begins the subsequent Scan_Duration, where:
+ -- Time = N * 1.28 sec
+ -- Time Range: 1.28 s to 83,884.8 s
+
+
+struct LEExtendedCreateConnectionV1:
+ -- 7.8.66 LE Extended Create Connection command version 1
+ -- HCI_LE_Extended_Create_Connection v1
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.GenericEnableParam initiator_filter_policy
+ $next [+1] LEOwnAddressType own_address_type
+ $next [+1] hci.LEPeerAddressType peer_address_type
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_address
+ $next [+1] bits:
+ 0 [+LEInitiatingPHYBits.$size_in_bits] LEInitiatingPHYBits initiating_phys
+
+ let num_entries = (initiating_phys.le_1m ? 1 : 0)+(initiating_phys.le_2m ? 1 : 0)+(initiating_phys.le_coded ? 1 : 0)
+ $next [+2*num_entries] UInt:16[num_entries] scan_interval
+ -- Time interval from when the Controller started its last scan until it begins the subsequent
+ -- scan on the primary advertising physical channel.
+ -- Time = N × 0.625 ms
+ -- Time Range: 2.5 ms to 40.959375 s
+
+ $next [+2*num_entries] UInt:16[num_entries] scan_window
+ -- Duration of the scan on the primary advertising physical channel.
+ -- Time = N × 0.625 ms
+ -- Time Range: 2.5 ms to 40.959375 s
+
+ $next [+2*num_entries] UInt:16[num_entries] connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+
+ $next [+2*num_entries] UInt:16[num_entries] connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+
+ $next [+2*num_entries] UInt:16[num_entries] max_latency
+ -- Maximum Peripheral latency for the connection in number of connection events.
+
+ $next [+2*num_entries] UInt:16[num_entries] supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+
+ $next [+2*num_entries] UInt:16[num_entries] min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2*num_entries] UInt:16[num_entries] max_connection_event_length
+ -- Time: N * 0.625 ms
+
+# 7.8.66 LE Extended Create Connection command version 2
+# HCI_LE_Extended_Create_Connection v2
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEPeriodicAdvertisingCreateSyncCommand:
+ -- 7.8.67 LE Periodic Advertising Create Sync command (v5.0) (LE)
+ -- HCI_LE_Periodic_Advertising_Create_Sync
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+1] bits:
+
+ 0 [+LEPeriodicAdvertisingCreateSyncOptions.$size_in_bits] LEPeriodicAdvertisingCreateSyncOptions options
+
+ $next [+1] UInt advertising_sid
+ -- Advertising SID subfield in the ADI field used to identify the Periodic Advertising
+ [requires: 0x00 <= this <= 0x0F]
+
+ $next [+1] hci.LEPeerAddressTypeNoAnon advertiser_address_type
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr advertiser_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or Random (static)
+ -- Identity Address of the advertiser
+
+ $next [+2] UInt skip
+ -- The maximum number of periodic advertising events that can be skipped after a successful
+ -- receive
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt sync_timeout
+ -- Synchronization timeout for the periodic advertising.
+ -- Time = N * 10 ms
+ -- Time Range: 100 ms to 163.84 s
+ [requires: 0x000A <= this <= 0x4000]
+
+ $next [+1] bits:
+
+ 0 [+LEPeriodicAdvertisingSyncCTEType.$size_in_bits] LEPeriodicAdvertisingSyncCTEType sync_cte_type
+ -- Constant Tone Extension sync options
+
+
+struct LEPeriodicAdvertisingCreateSyncCancel:
+ -- 7.8.68 LE Periodic Advertising Create Sync Cancel command (v5.0) (LE)
+ -- HCI_LE_Periodic_Advertising_Create_Sync_Cancel
+ -- Note that this command has no arguments
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEPeriodicAdvertisingTerminateSyncCommand:
+ -- 7.8.69 LE Periodic Advertising Terminate Sync command (v5.0) (LE)
+ -- HCI_LE_Periodic_Advertising_Terminate_Sync
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt sync_handle
+ -- Identifies the periodic advertising train
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEAddDeviceToPeriodicAdvertiserListCommand:
+ -- 7.8.70 LE Add Device To Periodic Advertiser List command (v5.0) (LE)
+ -- HCI_LE_Add_Device_To_Periodic_Advertiser_List
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.LEAddressType advertiser_address_type
+ -- Address type of the advertiser. The LEAddressType::kPublicIdentity and
+ -- LEAddressType::kRandomIdentity values are excluded for this command.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr advertiser_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the advertiser.
+
+ $next [+1] UInt advertising_sid
+ -- Advertising SID subfield in the ADI field used to identify the Periodic
+ -- Advertising.
+
+
+struct LERemoveDeviceFromPeriodicAdvertiserListCommand:
+ -- 7.8.71 LE Remove Device From Periodic Advertiser List command (v5.0) (LE)
+ -- HCI_LE_Remove_Device_From_Periodic_Advertiser_List
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt advertiser_address_type
+ -- Address type of the advertiser. The LEAddressType::kPublicIdentity and
+ -- LEAddressType::kRandomIdentity values are excluded for this command.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr advertiser_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the advertiser.
+
+ $next [+1] UInt advertising_sid
+ -- Advertising SID subfield in the ADI field used to identify the Periodic
+ -- Advertising.
+
+
+struct LEClearPeriodicAdvertiserListCommand:
+ -- 7.8.72 LE Clear Periodic Advertiser List command (v5.0) (LE)
+ -- HCI_LE_Clear_Periodic_Advertiser_List
+ -- Note that this command has no arguments
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEReadPeriodicAdvertiserListSizeCommand:
+ -- 7.8.73 LE Read Periodic Advertiser List Size command (v5.0) (LE)
+ -- HCI_LE_Read_Periodic_Advertiser_List_Size
+ -- Note that this command has no arguments
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEReadTransmitPowerCommand:
+ -- 7.8.74 LE Read Transmit Power command (v5.0) (LE)
+ -- HCI_LE_Read_Transmit_Power
+ -- Note that this command has no arguments
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEReadRFPathCompensationCommand:
+ -- 7.8.75 LE Read RF Path Compensation command (v5.0) (LE)
+ -- HCI_LE_Read_RF_Path_Compensation
+ -- Note that this command has no arguments
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+struct LEWriteRFPathCompensationCommand:
+ -- 7.8.76 LE Write RF Path Compensation command (v5.0) (LE)
+ -- HCI_LE_Write_RF_Path_Compensation
+ -- Values provided are used in the Tx Power Level and RSSI calculation.
+ -- Range: -128.0 dB (0xFB00) ≤ N ≤ 128.0 dB (0x0500)
+ -- Units: 0.1 dB
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] Int rf_tx_path_compensation_value
+ [requires: -1280 <= this <= 1280]
+
+ $next [+2] Int rf_rx_path_compensation_value
+ [requires: -1280 <= this <= 1280]
+
+
+struct LESetPrivacyModeCommand:
+ -- 7.8.77 LE Set Privacy Mode command (v5.0) (LE)
+ -- HCI_LE_Set_Privacy_Mode
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] hci.LEPeerAddressTypeNoAnon peer_identity_address_type
+ -- The peer identity address type (either Public Identity or Private
+ -- Identity).
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_identity_address
+ -- Public Identity Address or Random (static) Identity Address of the
+ -- advertiser.
+
+ $next [+1] LEPrivacyMode privacy_mode
+ -- The privacy mode to be used for the given entry on the resolving list.
+
+# 7.8.93 [No longer used]
+# 7.8.94 LE Modify Sleep Clock Accuracy command
+# 7.8.95 [No longer used]
+
+
+struct LEReadISOTXSyncCommand:
+ -- 7.8.96 LE Read ISO TX Sync command (v5.2) (LE)
+ -- HCI_LE_Read_ISO_TX_Sync
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection handle of the CIS or BIS
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LESetCIGParametersCommand:
+ -- 7.8.97 LE Set CIG Parameters command (v5.2) (LE)
+ -- HCI_LE_Set_CIG_Parameters
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandHeader header
+
+ $next [+1] UInt cig_id
+ -- Used to identify the CIG
+ [requires: 0x00 <= this <= 0xEF]
+
+ $next [+3] UInt sdu_interval_c_to_p
+ -- The interval, in microseconds, of periodic SDUs (Central => Peripheral)
+ [requires: 0x0000FF <= this <= 0x0FFFFF]
+
+ $next [+3] UInt sdu_interval_p_to_c
+ -- The interval, in microseconds, of periodic SDUs (Peripheral => Central)
+ [requires: 0x0000FF <= this <= 0x0FFFFF]
+
+ $next [+1] LESleepClockAccuracyRange worst_case_sca
+ -- Worst-case sleep clock accuracy of all Peripherals that will participate in the CIG
+
+ $next [+1] LECISPacking packing
+ -- Preferred method of arranging subevents of multiple CISes
+
+ $next [+1] LECISFraming framing
+ -- Format of the CIS Data PDUs
+
+ $next [+2] UInt max_transport_latency_c_to_p
+ -- Maximum transport latency, in milliseconds, from the Central's Controller to the
+ -- Peripheral's Controller
+ [requires: 0x0005 <= this <= 0x0FA0]
+
+ $next [+2] UInt max_transport_latency_p_to_c
+ -- Maximum transport latency, in milliseconds, from the Peripheral's Controller to the
+ -- Central's Controller
+ [requires: 0x0005 <= this <= 0x0FA0]
+
+ $next [+1] UInt cis_count
+ -- Total number of CIS configurations in the CIG being added or modified
+ [requires: 0x00 <= this <= 0x1F]
+
+ let single_cis_options_size = LESetCIGParametersCISOptions.$size_in_bytes
+
+ let total_cis_options_size = cis_count*single_cis_options_size
+
+ $next [+total_cis_options_size] LESetCIGParametersCISOptions[cis_count] cis_options
+ -- Array of parameters, one for each of the CISes in this CIG
+
+# 7.8.98 LE Set CIG Parameters Test command
+
+
+struct LECreateCISCommand:
+ -- 7.8.99 LE Create CIS command (v5.2) (LE)
+ -- HCI_LE_Create_CIS
+ struct ConnectionInfo:
+ -- Handles for each stream being created
+
+ 0 [+2] UInt cis_connection_handle
+ -- Connection handle of a CIS
+ [requires: 0x0000 <= this <= 0xEFFF]
+
+ $next [+2] UInt acl_connection_handle
+ -- Connection handle of an ACL connection
+ [requires: 0x0000 <= this <= 0xEFFF]
+
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt cis_count
+ -- Total number of CISes to be created
+ [requires: 0x01 <= this <= 0x1F]
+
+ let single_cis_params_size = ConnectionInfo.$size_in_bytes
+ let total_cis_params_size = cis_count*single_cis_params_size
+ $next [+total_cis_params_size] ConnectionInfo[cis_count] cis_connection_info
+ -- Connection handle information for the CIS(es) being created
+
+
+struct LERemoveCIGCommand:
+ -- 7.8.100 LE Remove CIG command (v5.2) (LE)
+ -- HCI_LE_Remove_CIG
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt cig_id
+ -- Identifier of a CIG
+ [requires: 0x00 <= this <= 0xEF]
+
+
+struct LEAcceptCISRequestCommand:
+ -- 7.8.101 LE Accept CIS Request command (v5.2) (LE)
+ -- HCI_LE_Accept_CIS_Request
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection handle of the CIS
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LERejectCISRequestCommand:
+ -- 7.8.102 LE Reject CIS Request command (v5.2) (LE)
+ -- HCI_LE_Reject_CIS_Request
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection handle of the CIS
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] hci.StatusCode reason
+ -- Reason the CIS request was rejected
+
+# 7.8.103 LE Create BIG command
+# 7.8.104 LE Create BIG Test command
+# 7.8.105 LE Terminate BIG command
+# 7.8.106 LE BIG Create Sync command
+# 7.8.107 LE BIG Terminate Sync command
+
+
+struct LERequestPeerSCACommand:
+ -- 7.8.108 LE Request Peer SCA command
+ -- HCI_LE_Request_Peer_SCA
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection handle of the ACL
+ [requires: 0x0000 <= this <= 0xEFF]
+
+
+struct LESetupISODataPathCommand:
+ -- 7.8.109 LE Setup ISO Data Path command
+ -- HCI_LE_Setup_ISO_Data_Path
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection handle of the CIS or BIS
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] DataPathDirection data_path_direction
+ -- Specifies the direction for which the data path is being configured
+
+ $next [+1] UInt data_path_id
+ -- Data transport path used (0x00 for HCI).
+ [requires: 0x00 <= this <= 0xFE]
+
+ let vcf_size = CodecId.$size_in_bytes
+ $next [+vcf_size] CodecId codec_id
+ -- Codec to be used
+
+ $next [+3] UInt controller_delay
+ -- Controller delay in microseconds (0s to 4s)
+ [requires: 0x000000 <= this <= 0x3D0900]
+
+ $next [+1] UInt codec_configuration_length
+ -- Length of codec configuration
+
+ $next [+codec_configuration_length] UInt:8[codec_configuration_length] codec_configuration
+ -- Codec-specific configuration data
+
+
+struct LERemoveISODataPathCommand:
+ -- 7.8.110 LE Remove ISO Data Path command
+ -- HCI_LE_Remove_ISO_Data_Path
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection handle of the CIS or BIS
+ [requires: 0x0000 <= this <= 0x0EFFF]
+
+ $next [+1] bits:
+ 0 [+1] Flag remove_input_data_path
+ 1 [+1] Flag remove_output_data_path
+
+# 7.8.111 LE ISO Transmit Test command
+# 7.8.112 LE ISO Receive Test command
+# 7.8.113 LE ISO Read Test Counters command
+# 7.8.114 LE ISO Test End command
diff --git a/pw_bluetooth/public/pw_bluetooth/hci_common.emb b/pw_bluetooth/public/pw_bluetooth/hci_common.emb
new file mode 100644
index 000000000..58287161a
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci_common.emb
@@ -0,0 +1,503 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This file contains Emboss definitions for Host Controller Interface packets
+# and types found in the Bluetooth Core Specification. The Emboss compiler is
+# used to generate a C++ header from this file.
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::emboss"]
+# =========================== Common Definitions =================================
+
+
+enum StatusCode:
+ -- HCI Error Codes. Refer to Core Spec v5.0, Vol 2, Part D for definitions and
+ -- descriptions. All enum values are in increasing numerical order, however the
+ -- values are listed below for clarity.
+ [maximum_bits: 8]
+ SUCCESS = 0x00
+ UNKNOWN_COMMAND = 0x01
+ UNKNOWN_CONNECTION_ID = 0x02
+ HARDWARE_FAILURE = 0x03
+ PAGE_TIMEOUT = 0x04
+ AUTHENTICATION_FAILURE = 0x05
+ PIN_OR_KEY_MISSING = 0x06
+ MEMORY_CAPACITY_EXCEEDED = 0x07
+ CONNECTION_TIMEOUT = 0x08
+ CONNECTION_LIMIT_EXCEEDED = 0x09
+ SYNCHRONOUS_CONNECTION_LIMIT_EXCEEDED = 0x0A
+ CONNECTION_ALREADY_EXISTS = 0x0B
+ COMMAND_DISALLOWED = 0x0C
+ CONNECTION_REJECTED_LIMITED_RESOURCES = 0x0D
+ CONNECTION_REJECTED_SECURITY = 0x0E
+ CONNECTION_REJECTED_BAD_BD_ADDR = 0x0F
+ CONNECTION_ACCEPT_TIMEOUT_EXCEEDED = 0x10
+ UNSUPPORTED_FEATURE_OR_PARAMETER = 0x11
+ INVALID_HCI_COMMAND_PARAMETERS = 0x12
+ REMOTE_USER_TERMINATED_CONNECTION = 0x13
+ REMOTE_DEVICE_TERMINATED_CONNECTION_LOW_RESOURCES = 0x14
+ REMOTE_DEVICE_TERMINATED_CONNECTION_POWER_OFF = 0x15
+ CONNECTION_TERMINATED_BY_LOCAL_HOST = 0x16
+ REPEATED_ATTEMPTS = 0x17
+ PAIRING_NOT_ALLOWED = 0x18
+ UNKNOWN_LMP_PDU = 0x19
+ UNSUPPORTED_REMOTE_FEATURE = 0x1A
+ SCO_OFFSET_REJECTED = 0x1B
+ SCO_INTERVAL_REJECTED = 0x1C
+ SCO_AIRMODE_REJECTED = 0x1D
+ INVALID_LMP_OR_LL_PARAMETERS = 0x1E
+ UNSPECIFIED_ERROR = 0x1F
+ UNSUPPORTED_LMP_OR_LL_PARAMETER_VALUE = 0x20
+ ROLE_CHANGE_NOT_ALLOWED = 0x21
+ LMP_OR_LL_RESPONSE_TIMEOUT = 0x22
+ LMP_ERROR_TRANSACTION_COLLISION = 0x23
+ LMP_PDU_NOT_ALLOWED = 0x24
+ ENCRYPTION_MODE_NOT_ACCEPTABLE = 0x25
+ LINK_KEY_CANNOT_BE_CHANGED = 0x26
+ REQUESTED_QOS_NOT_SUPPORTED = 0x27
+ INSTANT_PASSED = 0x28
+ PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED = 0x29
+ DIFFERENT_TRANSACTION_COLLISION = 0x2A
+ RESERVED_0 = 0x2B
+ QOS_UNACCEPTABLE_PARAMETER = 0x2C
+ QOS_REJECTED = 0x2D
+ CHANNEL_CLASSIFICATION_NOT_SUPPORTED = 0x2E
+ INSUFFICIENT_SECURITY = 0x2F
+ PARAMETER_OUT_OF_MANDATORY_RANGE = 0x30
+ RESERVED_1 = 0x31
+ ROLE_SWITCH_PENDING = 0x32
+ RESERVED_2 = 0x33
+ RESERVED_SLOT_VIOLATION = 0x34
+ ROLE_SWITCH_FAILED = 0x35
+ EXTENDED_INQUIRY_RESPONSE_TOO_LARGE = 0x36
+ SECURE_SIMPLE_PAIRING_NOT_SUPPORTED_BY_HOST = 0x37
+ HOST_BUSY_PAIRING = 0x38
+ CONNECTION_REJECTED_NO_SUITABLE_CHANNEL_FOUND = 0x39
+ CONTROLLER_BUSY = 0x3A
+ UNACCEPTABLE_CONNECTION_PARAMETERS = 0x3B
+ DIRECTED_ADVERTISING_TIMEOUT = 0x3C
+ CONNECTION_TERMINATED_MIC_FAILURE = 0x3D
+ CONNECTION_FAILED_TO_BE_ESTABLISHED = 0x3E
+ MAC_CONNECTION_FAILED = 0x3F
+ COARSE_CLOCK_ADJUSTMENT_REJECTED = 0x40
+ # 5.0
+ TYPE_0_SUBMAP_NOT_DEFINED = 0x41
+ UNKNOWN_ADVERTISING_IDENTIFIER = 0x42
+ LIMIT_REACHED = 0x43
+ OPERATION_CANCELLED_BY_HOST = 0x44
+
+
+enum MajorDeviceClass:
+ [maximum_bits: 5]
+ MISCELLANEOUS = 0x00
+ COMPUTER = 0x01
+ PHONE = 0x02
+ LAN = 0x03
+ AUDIO_VIDEO = 0x04
+ PERIPHERAL = 0x05
+ IMAGING = 0x06
+ WEARABLE = 0x07
+ TOY = 0x08
+ HEALTH = 0x09
+ UNCATEGORIZED = 0x1F
+
+
+bits MajorServiceClasses:
+ 0 [+1] Flag limited_discoverable_mode
+ $next [+1] Flag le_audio
+ $next [+1] Flag reserved
+ $next [+1] Flag positioning
+ $next [+1] Flag networking
+ $next [+1] Flag rendering
+ $next [+1] Flag capturing
+ $next [+1] Flag object_transfer
+ $next [+1] Flag audio
+ $next [+1] Flag telephony
+ $next [+1] Flag information
+
+
+enum ComputerMinorDeviceClass:
+ [maximum_bits: 6]
+ UNCATEGORIZED = 0x00
+ DESKTOP_WORKSTATION = 0x01
+ SERVER_CLASS = 0x02
+ LAPTOP = 0x03
+ HANDHELD_PC = 0x04
+ PALM_SIZE_PC = 0x05
+ WEARABLE = 0x06
+ TABLET = 0x07
+
+
+enum PhoneMinorDeviceClass:
+ [maximum_bits: 6]
+ UNCATEGORIZED = 0x00
+ CELLULAR = 0x01
+ CORDLESS = 0x02
+ SMARTPHONE = 0x03
+ WIRED_MODEM_OR_VOID_GATEWAY = 0x04
+ COMMON_ISDN_ACCESS = 0x05
+
+
+enum LANMinorDeviceClass:
+ [maximum_bits: 6]
+ FULLY_AVAILABLE = 0x00
+ UTILIZED_1_TO_17 = 0x08
+ UTILIZED_17_TO_33 = 0x10
+ UTILIZED_33_TO_50 = 0x18
+ UTILIZED_50_TO_67 = 0x20
+ UTILIZED_67_TO_83 = 0x28
+ UTILIZED_83_TO_99 = 0x30
+ NO_SERVICE_AVAILABLE = 0x38
+
+
+enum AudioVideoMinorDeviceClass:
+ [maximum_bits: 6]
+ UNCATEGORIZED = 0x00
+ WEARABLE_HEADSET_DEVICE = 0x01
+ HANDS_FREE_DEVICE = 0x02
+ RESERVED_0 = 0x03
+ MICROPHONE = 0x04
+ LOUDSPEAKER = 0x05
+ HEADPHONES = 0x06
+ PORTABLE_AUDIO = 0x07
+ CAR_AUDIO = 0x08
+ SET_TOP_BOX = 0x09
+ HIFI_AUDIO_DEVICE = 0x0A
+ VCR = 0x0B
+ VIDEO_CAMERA = 0x0C
+ CAMCORDER = 0x0D
+ VIDEO_MONITOR = 0x0E
+ VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x0F
+ VIDEO_CONFERENCING = 0x10
+ RESERVED_1 = 0x11
+ GAMING_TOY = 0x12
+
+
+enum PeripheralMinorDeviceClass0:
+ [maximum_bits: 4]
+ UNCATEGORIZED = 0x00
+ JOYSTICK = 0x01
+ GAMEPAD = 0x02
+ REMOTE_CONTROL = 0x03
+ SENSING_DEVICE = 0x04
+ DIGITIZER_TABLET = 0x05
+ CARD_READER = 0x06
+ DIGITAL_PEN = 0x07
+ HANDHELD_SCANNER = 0x08
+ HANDHELD_GESTURAL_INPUT_DEVICE = 0x09
+
+
+enum PeripheralMinorDeviceClass1:
+ [maximum_bits: 2]
+ UNCATEGORIZED = 0x00
+ KEYBOARD = 0x01
+ POINTING_DEVICE = 0x02
+ COMBO_KEYBOARD_POINTING_DEVICE = 0x03
+
+
+bits PeripheralMinorDeviceClass:
+ 0 [+4] PeripheralMinorDeviceClass0 device_class_0
+ $next [+2] PeripheralMinorDeviceClass1 device_class_1
+
+
+enum ImagingMinorDeviceClass:
+ [maximum_bits: 2]
+ UNCATEGORIZED = 0x00
+
+
+bits ImagingMinorDeviceClassBits:
+ 0 [+2] ImagingMinorDeviceClass device_class
+ $next [+1] Flag display
+ $next [+1] Flag camera
+ $next [+1] Flag scanner
+ $next [+1] Flag printer
+
+
+enum WearableMinorDeviceClass:
+ [maximum_bits: 6]
+ WRISTWATCH = 0x01
+ PAGER = 0x02
+ JACKET = 0x03
+ HELMET = 0x04
+ GLASSES = 0x05
+
+
+enum ToyMinorDeviceClass:
+ [maximum_bits: 6]
+ ROBOT = 0x01
+ VEHICLE = 0x02
+ DOLL = 0x03
+ CONTROLLER = 0x04
+ GAME = 0x05
+
+
+enum HealthMinorDeviceClass:
+ [maximum_bits: 6]
+ UNDEFINED = 0x00
+ BLOOD_PRESSURE_MONITOR = 0x01
+ THERMOMETER = 0x02
+ WEIGHING_SCALE = 0x03
+ GLUCOSE_METER = 0x04
+ PULSE_OXIMETER = 0x05
+ HEART_PULSE_RATE_MONITOR = 0x06
+ HEALTH_DATA_DISPLAY = 0x07
+ STEP_COUNTER = 0x08
+ BODY_COMPOSITION_ANALYZER = 0x09
+ PEAK_FLOW_MONITOR = 0x0A
+ MEDICATION_MONITOR = 0x0B
+ KNEE_PROSTHESIS = 0x0C
+ ANKLE_PROSTHESIS = 0x0D
+ GENERIC_HEALTH_MANAGER = 0x0E
+ PERSONAL_MOBILITY_DEVICE = 0x0F
+
+
+enum GenericEnableParam:
+ -- Binary values that can be generically passed to HCI commands that expect a
+ -- 1-octet boolean "enable"/"disable" parameter.
+ [maximum_bits: 8]
+ DISABLE = 0x00
+ ENABLE = 0x01
+
+
+enum GenericPresenceParam:
+ [maximum_bits: 8]
+ NOT_PRESENT = 0x00
+ PRESENT = 0x01
+
+
+struct BdAddr:
+ -- Bluetooth Device Address
+ 0 [+6] UInt bd_addr
+
+
+bits ClassOfDevice:
+ -- Defined in Assigned Numbers for the Baseband
+ -- https://www.bluetooth.com/specifications/assigned-numbers/baseband
+ 0 [+2] UInt zero
+ [requires: this == 0]
+
+ if major_device_class == MajorDeviceClass.COMPUTER:
+ 2 [+6] ComputerMinorDeviceClass computer_minor_device_class
+
+ if major_device_class == MajorDeviceClass.PHONE:
+ 2 [+6] PhoneMinorDeviceClass phone_minor_device_class
+
+ if major_device_class == MajorDeviceClass.LAN:
+ 2 [+6] LANMinorDeviceClass lan_minor_device_class
+
+ if major_device_class == MajorDeviceClass.AUDIO_VIDEO:
+ 2 [+6] AudioVideoMinorDeviceClass audio_video_minor_device_class
+
+ if major_device_class == MajorDeviceClass.PERIPHERAL:
+ 2 [+6] PeripheralMinorDeviceClass peripheral_minor_device_class
+
+ if major_device_class == MajorDeviceClass.IMAGING:
+ 2 [+6] ImagingMinorDeviceClassBits imaging_minor_device_class
+
+ if major_device_class == MajorDeviceClass.WEARABLE:
+ 2 [+6] WearableMinorDeviceClass wearable_minor_device_class
+
+ if major_device_class == MajorDeviceClass.TOY:
+ 2 [+6] ToyMinorDeviceClass toy_minor_device_class
+
+ if major_device_class == MajorDeviceClass.HEALTH:
+ 2 [+6] HealthMinorDeviceClass health_minor_device_class
+
+ 8 [+5] MajorDeviceClass major_device_class
+ $next [+11] MajorServiceClasses major_service_classes
+
+
+enum ConnectionRole:
+ [maximum_bits: 8]
+ CENTRAL = 0x00
+ PERIPHERAL = 0x01
+
+
+enum LEPeerAddressType:
+ -- Possible values that can be used for the address_type parameters in various
+ -- HCI commands
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ RANDOM = 0x01
+ ANONYMOUS = 0xFF
+
+
+enum LEPeerAddressTypeNoAnon:
+ -- Possible values that can be used for the address_type parameters in various
+ -- HCI commands
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ RANDOM = 0x01
+
+
+bits ClockOffset:
+ -- Clock Offset. The lower 15 bits are set to the clock offset as retrieved
+ -- by an Inquiry. The highest bit is set to 1 if the rest of this parameter
+ -- is valid.
+ 15 [+1] Flag valid
+ if valid:
+ 0 [+15] UInt clock_offset
+
+
+enum LEPrimaryAdvertisingPHY:
+ [maximum_bits: 8]
+ LE_1M = 0x01
+ LE_CODED = 0x03
+ LE_CODED_S2 = 0x04
+
+
+enum LESecondaryAdvertisingPHY:
+ [maximum_bits: 8]
+ NONE = 0x00
+ LE_1M = 0x01
+ LE_2M = 0x02
+ LE_CODED = 0x03
+ LE_CODED_S2 = 0x04
+
+
+enum LEAddressType:
+ -- Possible values that can be reported for various |*_address_type| parameters in various LE
+ -- packets.
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ -- Public Device Address (default)
+
+ RANDOM = 0x01
+ -- Random Device Address
+
+ PUBLIC_IDENTITY = 0x02
+ -- Public Identity Address (corresponds to Resolved Private Address)
+
+ RANDOM_IDENTITY = 0x03
+ -- Random (static) Identity Address (corresponds to Resolved Private Address)
+
+
+enum LEExtendedAddressType:
+ -- Possible values that can be reported for various |*_address_type| parameters in various LE
+ -- packets.
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ -- Public Device Address (default)
+
+ RANDOM = 0x01
+ -- Random Device Address
+
+ PUBLIC_IDENTITY = 0x02
+ -- Public Identity Address (corresponds to Resolved Private Address)
+
+ RANDOM_IDENTITY = 0x03
+ -- Random (static) Identity Address (corresponds to Resolved Private Address)
+
+ ANONYMOUS = 0xFF
+ -- No address provided (anonymous advertisement)
+ -- This is a special value that is only used in LE Advertising Report events.
+
+
+enum PageScanRepetitionMode:
+ -- The page scan repetition mode, representing a maximum time between Page Scans.
+ -- (See Core Spec v5.0, Volume 2, Part B, Section 8.3.1)
+ [maximum_bits: 8]
+ R0_ = 0x00 # Continuous Scan
+ R1_ = 0x01 # <= 1.28s
+ R2_ = 0x02 # <= 2.56s
+
+
+enum CodingFormat:
+ -- Coding formats from assigned numbers.
+ -- (https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface)
+ [maximum_bits: 8]
+ U_LAW = 0x00
+ A_LAW = 0x01
+ CVSD = 0x02
+ TRANSPARENT = 0x03
+ LINEAR_PCM = 0x04
+ MSBC = 0x05
+ LC3 = 0x06
+ G729A = 0x07
+ VENDOR_SPECIFIC = 0xFF
+
+
+enum IoCapability:
+ -- All other values reserved for future use.
+ [maximum_bits: 8]
+ DISPLAY_ONLY = 0x00
+ DISPLAY_YES_NO = 0x01
+ KEYBOARD_ONLY = 0x02
+ NO_INPUT_NO_OUTPUT = 0x03
+
+# inclusive-language: disable
+
+
+enum AuthenticationRequirements:
+ -- All options without MITM do not require MITM protection, and a numeric
+ -- comparison with automatic accept is allowed.
+ -- All options with MITM do require MITM protection, and IO capabilities should
+ -- be used to determine the authentication procedure.
+ [maximum_bits: 8]
+ NO_BONDING = 0x00
+ MITM_NO_BONDING = 0x01
+ DEDICATED_BONDING = 0x02
+ MITM_DEDICATED_BONDING = 0x03
+ GENERAL_BONDING = 0x04
+ MITM_GENERAL_BONDING = 0x05
+
+# inclusive-language: enable
+
+
+struct LinkKey:
+ 0 [+16] UInt:8[16] value
+
+# ========================= HCI packet headers ==========================
+
+
+bits OpCodeBits:
+ # Emboss currently lacks support for default field values and cross-type integral equality.
+ # (https://github.com/google/emboss/issues/21)
+ # (https://github.com/google/emboss/issues/23)
+ # Upon the addition of these features, we will transition OpCodeBits to be a parameterized
+ # field which defaults for each HCI packet type to its corresponding OpCode.
+ 0 [+10] UInt ocf
+ $next [+6] UInt ogf
+
+
+struct CommandHeader:
+ -- HCI Command packet header.
+ 0 [+2] OpCodeBits opcode
+ $next [+1] UInt parameter_total_size
+
+
+struct EventHeader:
+ -- HCI Event packet header.
+ 0 [+1] UInt event_code
+ $next [+1] UInt parameter_total_size
+
+
+struct CommandCompleteEvent:
+ -- Core Spec v5.3 Vol 4, Part E, Section 7.7.14
+ -- EventHeader.opcode == 0xe
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt num_hci_command_packets
+ $next [+2] OpCodeBits command_opcode
+ let event_fixed_size = $size_in_bytes-hdr_size
+ let return_parameters_size = header.parameter_total_size-event_fixed_size
+
+
+struct VendorDebugEvent:
+ -- This opcode is reserved for vendor-specific debugging events.
+ -- See Core Spec v5.3 Vol 4, Part E, Section 5.4.4.
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt subevent_code
+ -- The event code for the vendor subevent.
diff --git a/pw_bluetooth/public/pw_bluetooth/hci_events.emb b/pw_bluetooth/public/pw_bluetooth/hci_events.emb
new file mode 100644
index 000000000..aa5f52931
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci_events.emb
@@ -0,0 +1,1751 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This file contains Emboss definitions for Host Controller Interface packets
+# and types found in the Bluetooth Core Specification. The Emboss compiler is
+# used to generate a C++ header from this file.
+
+import "hci_common.emb" as hci
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::emboss"]
+# =========================== Constants =================================
+
+
+enum CoreSpecificationVersion:
+ -- Bluetooth Core Specification version
+ [maximum_bits: 8]
+ V1_0B = 0x00 # v1.0b
+ V1_1 = 0x01 # v1.1
+ V1_2 = 0x02 # v1.2
+ V2_0_EDR = 0x03 # v2.0+EDR
+ V2_1_EDR = 0x04 # v2.0+EDR
+ V3_0_HS = 0x05 # v3.0+HS
+ V4_0 = 0x06 # v4.0
+ V4_1 = 0x07 # v4.1
+ V4_2 = 0x08 # v4.2
+ V5_0 = 0x09 # v5.0
+ V5_1 = 0x0A # v5.1
+ V5_2 = 0x0B # v5.2
+ V5_3 = 0x0C # v5.3
+ V5_4 = 0x0D # v5.4
+
+
+enum LinkType:
+ [maximum_bits: 8]
+ SCO = 0x00
+ ACL = 0x01
+ ESCO = 0x02
+
+
+enum EncryptionStatus:
+ OFF = 0x00
+ ON_WITH_E0_FOR_BREDR_OR_AES_FOR_LE = 0x01
+ ON_WITH_AES_FOR_BREDR = 0x02
+
+
+enum LEAdvertisingDataStatus:
+ [maximum_bits: 2]
+ COMPLETE = 0b00
+ INCOMPLETE = 0b01
+ INCOMPLETE_TRUNCATED = 0b10
+
+
+enum LEDirectAddressType:
+ -- Possible values that can be reported for the |direct_address_type| parameter in LE Advertising
+ -- Report events.
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ -- Public Device Address
+
+ PRIVATE = 0x01
+ -- Non-resolvable Private Address or Static Device Address
+
+ RESOLVABLE_PRIVATE_OWN_ADDRESS_PUBLIC = 0x02
+ -- Resolvable Private Address (resolved by Controller; Own_Address_Type was 0x00 or 0x02)
+
+ RESOLVABLE_PRIVATE_OWN_ADDRESS_RANDOM = 0x03
+ -- Resolvable Private Address (resolved by Controller; Own_Address_Type was 0x01 or 0x03)
+
+ RESOLVABLE_PRIVATE = 0xFE
+ -- Resolvable Private Address (Controller unable to resolve)
+
+
+enum LEClockAccuracy:
+ -- Possible values that can be reported for the |central_clock_accuracy| and
+ -- |advertiser_clock_accuracy| parameters.
+ [maximum_bits: 8]
+ PPM_500 = 0x00
+ PPM_250 = 0x01
+ PPM_150 = 0x02
+ PPM_100 = 0x03
+ PPM_75 = 0x04
+ PPM_50 = 0x05
+ PPM_30 = 0x06
+ PPM_20 = 0x07
+
+
+enum KeyType:
+ -- The key type used during pairing.
+ [maximum_bits: 8]
+ COMBINATION = 0x00
+ DEBUG_COMBINATION = 0x03
+ UNAUTHENTICATED_COMBINATION_FROM_P192 = 0x04
+ AUTHENTICATED_COMBINATION_FROM_P192 = 0x05
+ CHANGED_COMBINATION_KEY = 0x06
+ UNAUTHENTICATED_COMBINATION_FROM_P256 = 0x07
+ AUTHENTICATED_COMBINATION_FROM_P256 = 0x08
+
+# =========================== Field Types =================================
+
+
+bits LmpFeatures(page: UInt:8):
+ -- Bit mask of Link Manager Protocol features.
+ [requires: page <= 2]
+ if page == 0:
+ 0 [+1] Flag three_slot_packets
+ 1 [+1] Flag five_slot_packets
+ 2 [+1] Flag encryption
+ 3 [+1] Flag slot_offset
+ 4 [+1] Flag timing_accuracy
+ 5 [+1] Flag role_switch
+ 6 [+1] Flag hold_mode
+ 7 [+1] Flag sniff_mode
+ # 8: previously used
+ 9 [+1] Flag power_control_requests
+ 10 [+1] Flag channel_quality_driven_data_rate
+ 11 [+1] Flag sco_link
+ 12 [+1] Flag hv2_packets
+ 13 [+1] Flag hv3_packets
+ 14 [+1] Flag mu_law_log_synchronous_data
+ 15 [+1] Flag a_law_log_synchronous_data
+ 16 [+1] Flag cvsd_synchronous_data
+ 17 [+1] Flag paging_parameter_negotiation
+ 18 [+1] Flag power_control
+ 19 [+1] Flag transparent_synchronous_data
+ 20 [+3] UInt flow_control_lag
+ 23 [+1] Flag broadcast_encryption
+ # 24: reserved for future use
+ 25 [+1] Flag enhanced_data_rate_acl_2_mbs_mode
+ 26 [+1] Flag enhanced_data_rate_acl_3_mbs_mode
+ 27 [+1] Flag enhanced_inquiry_scan
+ 28 [+1] Flag interlaced_inquiry_scan
+ 29 [+1] Flag interlaced_page_scan
+ 30 [+1] Flag rssi_with_inquiry_results
+ 31 [+1] Flag extended_sco_link_ev3_packets
+ 32 [+1] Flag ev4_packets
+ 33 [+1] Flag ev5_packets
+ # 34: reserved for future use
+ 35 [+1] Flag afh_capable_peripheral
+ 36 [+1] Flag afh_classification_peripheral
+ 37 [+1] Flag bredr_not_supported
+ 38 [+1] Flag le_supported_controller
+ 39 [+1] Flag three_slot_enhanced_data_rate_acl_packets
+ 40 [+1] Flag five_slot_enhanced_data_rate_acl_packets
+ 41 [+1] Flag sniff_subrating
+ 42 [+1] Flag pause_encryption
+ 43 [+1] Flag afh_capable_central
+ 44 [+1] Flag afh_classification_central
+ 45 [+1] Flag enhanced_data_rate_esco_2_mbs_mode
+ 46 [+1] Flag enhanced_data_rate_esco_3_mbs_mode
+ 47 [+1] Flag three_slot_enhanced_data_rate_esco_packets
+ 48 [+1] Flag extended_inquiry_response
+ 49 [+1] Flag simultaneous_le_and_bredr_to_same_device_capable_controller
+ # 50: reserved for future use
+ 51 [+1] Flag secure_simple_pairing_controller_support
+ 52 [+1] Flag encapsulated_pdu
+ 53 [+1] Flag erroneous_data_reporting
+ 54 [+1] Flag non_flushable_packet_boundary_flag
+ # 55: reserved for future use
+ 56 [+1] Flag hci_link_supervision_timeout_changed_event
+ 57 [+1] Flag variable_inquiry_tx_power_level
+ 58 [+1] Flag enhanced_power_control
+ # 59-62: reserved for future use
+ 63 [+1] Flag extended_features
+
+ if page == 1:
+ 0 [+1] Flag secure_simple_pairing_host_support
+ 1 [+1] Flag le_supported_host
+ # 2: previously used
+ 3 [+1] Flag secure_connection_host_support
+
+ if page == 2:
+ 0 [+1] Flag connectionless_peripheral_broadcast_transmitter_operation
+ 1 [+1] Flag connectionless_peripheral_broadcast_receiver_operation
+ 2 [+1] Flag synchronization_train
+ 3 [+1] Flag synchronization_scan
+ 4 [+1] Flag hci_inquiry_response_notification_event
+ 5 [+1] Flag generalized_interlaced_scan
+ 6 [+1] Flag coarse_clock_adjustment
+ # 7: reserved for future use
+ 8 [+1] Flag secure_connections_controller_support
+ 9 [+1] Flag ping
+ 10 [+1] Flag slot_availability_mask
+ 11 [+1] Flag train_nudging
+
+
+bits LEFeatureSet:
+ 0 [+1] Flag le_encryption
+ $next [+1] Flag connection_parameters_request_procedure
+ $next [+1] Flag extended_reject_indication
+ $next [+1] Flag peripheral_initiated_features_exchange
+ $next [+1] Flag le_ping
+ $next [+1] Flag le_data_packet_length_extension
+ $next [+1] Flag ll_privacy
+ $next [+1] Flag extended_scanning_filter_policies
+ $next [+1] Flag le_2m_phy
+ $next [+1] Flag stable_modulation_index_transmitter
+ $next [+1] Flag stable_modulation_index_receiver
+ $next [+1] Flag le_coded_phy
+ $next [+1] Flag le_extended_advertising
+ $next [+1] Flag le_periodic_advertising
+ $next [+1] Flag channel_selection_algorithm_2
+ $next [+1] Flag le_power_class_1
+ $next [+1] Flag minimum_number_of_used_channels_procedure
+ $next [+1] Flag connection_cte_request
+ $next [+1] Flag connection_cte_response
+ $next [+1] Flag connectionless_cte_transmitter
+ $next [+1] Flag connectionless_cte_receiver
+ $next [+1] Flag antenna_switching_during_cte_transmission
+ $next [+1] Flag antenna_switching_during_cte_reception
+ $next [+1] Flag receiving_constant_tone_extensions
+ $next [+1] Flag periodic_advertising_sync_transfer_sender
+ $next [+1] Flag periodic_advertising_sync_transfer_recipient
+ $next [+1] Flag sleep_clock_accuracy_updates
+ $next [+1] Flag remote_public_key_validation
+ $next [+1] Flag connected_isochronous_stream_central
+ $next [+1] Flag connected_isochronous_stream_peripheral
+ $next [+1] Flag isochronous_broadcaster
+ $next [+1] Flag synchronized_receiver
+ $next [+1] Flag connected_isochronous_stream_host_support
+ $next [+1] Flag le_power_control_request_1
+ $next [+1] Flag le_power_control_request_2
+ -- Previous two bits shall always have the same value.
+
+ $next [+1] Flag le_path_loss_monitoring
+ $next [+1] Flag periodic_advertising_adi_support
+ $next [+1] Flag connection_subrating
+ $next [+1] Flag connection_subrating_host_support
+ $next [+1] Flag channel_classification
+ $next [+1] Flag advertising_coding_selection
+ $next [+1] Flag advertising_coding_selection_host_support
+ $next [+1] Flag reserved # Bit 42 is skipped
+ $next [+1] Flag periodic_advertising_with_responses_advertiser
+ $next [+1] Flag periodic_advertising_with_responses_scanner
+
+
+bits LEExtendedAdvertisingEventType:
+ 0 [+1] Flag connectable
+ $next [+1] Flag scannable
+ $next [+1] Flag directed
+ $next [+1] Flag scan_response
+ $next [+1] Flag legacy
+ $next [+2] LEAdvertisingDataStatus data_status
+
+
+bits SupportedCommands(octet: UInt:8):
+ [requires: octet <= 47]
+ if octet == 0:
+ 0 [+1] Flag inquiry
+ 1 [+1] Flag inquiry_cancel
+ 2 [+1] Flag periodic_inquiry_mode
+ 3 [+1] Flag exit_periodic_inquiry_mode
+ 4 [+1] Flag create_connection
+ 5 [+1] Flag disconnect
+ 7 [+1] Flag create_connection_cancel
+
+ if octet == 1:
+ 0 [+1] Flag accept_connection_request
+ 1 [+1] Flag reject_connection_request
+ 2 [+1] Flag link_key_request_reply
+ 3 [+1] Flag link_key_request_negative_reply
+ 4 [+1] Flag pin_code_request_reply
+ 5 [+1] Flag pin_code_request_negative_reply
+ 6 [+1] Flag change_connection_packet_type
+ 7 [+1] Flag authentication_requested
+
+ if octet == 2:
+ 0 [+1] Flag set_connection_encryption
+ 1 [+1] Flag change_connection_link_key
+ 2 [+1] Flag link_key_selection
+ 3 [+1] Flag remote_name_request
+ 4 [+1] Flag remote_name_request_cancel
+ 5 [+1] Flag read_remote_supported_features
+ 6 [+1] Flag read_remote_extended_features
+ 7 [+1] Flag read_remote_version_information
+
+ if octet == 3:
+ 0 [+1] Flag read_clock_offset
+ 1 [+1] Flag read_lmp_handle
+
+ if octet == 4:
+ 1 [+1] Flag hold_mode
+ 2 [+1] Flag sniff_mode
+ 3 [+1] Flag exit_sniff_mode
+ 6 [+1] Flag qos_setup
+ 7 [+1] Flag role_discovery
+
+ if octet == 5:
+ 0 [+1] Flag switch_role
+ 1 [+1] Flag read_link_policy_settings
+ 2 [+1] Flag write_link_policy_settings
+ 3 [+1] Flag read_default_link_policy_settings
+ 4 [+1] Flag write_default_link_policy_settings
+ 5 [+1] Flag flow_specification
+ 6 [+1] Flag set_event_mask
+ 7 [+1] Flag reset
+
+ if octet == 6:
+ 0 [+1] Flag set_event_filter
+ 1 [+1] Flag flush
+ 2 [+1] Flag read_pin_type
+ 3 [+1] Flag write_pin_type
+ 5 [+1] Flag read_stored_link_key
+ 6 [+1] Flag write_stored_link_key
+ 7 [+1] Flag deleted_stored_link_key
+
+ if octet == 7:
+ 0 [+1] Flag write_local_name
+ 1 [+1] Flag read_local_name
+ 2 [+1] Flag read_connection_attempt_timeout
+ 3 [+1] Flag write_connection_attempt_timeout
+ 4 [+1] Flag read_page_timeout
+ 5 [+1] Flag write_page_timeout
+ 6 [+1] Flag read_scan_enable
+ 7 [+1] Flag write_scan_enable
+
+ if octet == 8:
+ 0 [+1] Flag read_page_scan_activity
+ 1 [+1] Flag write_page_scan_activity
+ 2 [+1] Flag read_inquiry_scan_activity
+ 3 [+1] Flag write_inquiry_scan_activity
+ 4 [+1] Flag read_authentication_enable
+ 5 [+1] Flag write_authentication_enable
+
+ if octet == 9:
+ 0 [+1] Flag read_class_of_device
+ 1 [+1] Flag write_class_of_device
+ 2 [+1] Flag read_voice_setting
+ 3 [+1] Flag write_voice_setting
+ 4 [+1] Flag read_automatic_flush_timeout
+ 5 [+1] Flag write_automatic_flush_timeout
+ 6 [+1] Flag read_num_broadcast_retransmissions
+ 7 [+1] Flag write_num_broadcast_retransmissions
+
+ if octet == 10:
+ 0 [+1] Flag read_hold_mode_activity
+ 1 [+1] Flag write_hold_mode_activity
+ 2 [+1] Flag read_transmit_power_level
+ 3 [+1] Flag read_synchronous_flow_control_enable
+ 4 [+1] Flag write_synchronous_flow_control_enable
+ 5 [+1] Flag set_controller_to_host_flow_control
+ 6 [+1] Flag host_buffer_size
+ 7 [+1] Flag host_number_of_completed_packets
+
+ if octet == 11:
+ 0 [+1] Flag read_link_supervision_timeout
+ 1 [+1] Flag write_link_supervision_timeout
+ 2 [+1] Flag read_number_of_supported_iac
+ 3 [+1] Flag read_current_iaclap
+ 4 [+1] Flag write_current_iaclap
+
+ if octet == 12:
+ 1 [+1] Flag set_afh_host_channel_classification
+ 4 [+1] Flag read_inquiry_scan_type
+ 5 [+1] Flag write_inquiry_scan_type
+ 6 [+1] Flag read_inquiry_mode
+ 7 [+1] Flag write_inquiry_mode
+
+ if octet == 13:
+ 0 [+1] Flag read_page_scan_type
+ 1 [+1] Flag write_page_scan_type
+ 2 [+1] Flag read_afh_channel_assessment_mode
+ 3 [+1] Flag write_afh_channel_assessment_mode
+
+ if octet == 14:
+ 3 [+1] Flag read_local_version_information
+ 5 [+1] Flag read_local_supported_features
+ 6 [+1] Flag read_local_extended_features
+ 7 [+1] Flag read_buffer_size
+
+ if octet == 15:
+ 1 [+1] Flag read_bdaddr
+ 2 [+1] Flag read_failed_contact_counter
+ 3 [+1] Flag reset_failed_contact_c_ounter
+ 4 [+1] Flag read_link_quality
+ 5 [+1] Flag read_rssi
+ 6 [+1] Flag read_afh_channel_map
+ 7 [+1] Flag read_clock
+
+ if octet == 16:
+ 0 [+1] Flag read_loopback_mode
+ 1 [+1] Flag write_loopback_mode
+ 2 [+1] Flag enable_device_under_test_mode
+ 3 [+1] Flag setup_synchronous_connection_request
+ 4 [+1] Flag accept_synchronous_connection_request
+ 5 [+1] Flag reject_synchronous_connection_request
+
+ if octet == 17:
+ 0 [+1] Flag read_extended_inquiry_response
+ 1 [+1] Flag write_extended_inquiry_response
+ 2 [+1] Flag refresh_encryption_key
+ 4 [+1] Flag sniff_subrating
+ 5 [+1] Flag read_simple_pairing_mode
+ 6 [+1] Flag write_simple_pairing_mode
+ 7 [+1] Flag read_local_oob_data
+
+ if octet == 18:
+ 0 [+1] Flag read_inquiry_response_transmit_power_level
+ 1 [+1] Flag write_inquiry_transmit_power_level
+ 2 [+1] Flag read_default_erroneous_data_reporting
+ 3 [+1] Flag write_default_erroneous_data_reporting
+ 7 [+1] Flag io_capability_request_reply
+
+ if octet == 19:
+ 0 [+1] Flag user_confirmation_request_reply
+ 1 [+1] Flag user_confirmation_request_negative_reply
+ 2 [+1] Flag user_passkey_request_reply
+ 3 [+1] Flag user_passkey_request_negative_reply
+ 4 [+1] Flag remote_oob_data_request_reply
+ 5 [+1] Flag write_simple_pairing_debug_mode
+ 6 [+1] Flag enhanced_flush
+ 7 [+1] Flag remote_oob_data_request_negative_reply
+
+ if octet == 20:
+ 2 [+1] Flag send_keypress_notification
+ 3 [+1] Flag io_capability_request_negative_reply
+ 4 [+1] Flag read_encryption_key_size
+
+ if octet == 21:
+ 0 [+1] Flag create_physical_link
+ 1 [+1] Flag accept_physical_link
+ 2 [+1] Flag disconnect_physical_link
+ 3 [+1] Flag create_logical_link
+ 4 [+1] Flag accept_logical_link
+ 5 [+1] Flag disconnect_logical_link
+ 6 [+1] Flag logical_link_cancel
+ 7 [+1] Flag flow_spec_modify
+
+ if octet == 22:
+ 0 [+1] Flag read_logical_link_accept_timeout
+ 1 [+1] Flag write_logical_link_accept_timeout
+ 2 [+1] Flag set_event_mask_page_2
+ 3 [+1] Flag read_location_data
+ 4 [+1] Flag write_location_data
+ 5 [+1] Flag read_local_amp_info
+ 6 [+1] Flag read_local_ampassoc
+ 7 [+1] Flag write_remote_ampassoc
+
+ if octet == 23:
+ 0 [+1] Flag read_flow_control_mode
+ 1 [+1] Flag write_flow_control_mode
+ 2 [+1] Flag read_data_block_size
+
+ if octet == 24:
+ 0 [+1] Flag read_enhanced_transmit_power_level
+ 2 [+1] Flag read_best_effort_flush_timeout
+ 3 [+1] Flag write_best_effort_flush_timeout
+ 4 [+1] Flag short_range_mode
+ 5 [+1] Flag read_le_host_supported
+ 6 [+1] Flag write_le_host_support
+
+ if octet == 25:
+ 0 [+1] Flag le_set_event_mask
+ 1 [+1] Flag le_read_buffer_size_v1
+ 2 [+1] Flag le_read_local_supported_features
+ 4 [+1] Flag le_set_random_address
+ 5 [+1] Flag le_set_advertising_parameters
+ 6 [+1] Flag le_read_advertising_channel_tx_power
+ 7 [+1] Flag le_set_advertising_data
+
+ if octet == 26:
+ 0 [+1] Flag le_set_scan_response_data
+ 1 [+1] Flag le_set_advertising_enable
+ 2 [+1] Flag le_set_scan_parameters
+ 3 [+1] Flag le_set_scan_enable
+ 4 [+1] Flag le_create_connection
+ 5 [+1] Flag le_create_connection_cancel
+ 6 [+1] Flag le_read_filter_accept_list_size
+ 7 [+1] Flag le_clear_filter_accept_list
+
+ if octet == 27:
+ 0 [+1] Flag le_add_device_to_filter_accept_list
+ 1 [+1] Flag le_remove_device_from_filter_accept_list
+ 2 [+1] Flag le_connection_update
+ 3 [+1] Flag le_set_host_channel_classification
+ 4 [+1] Flag le_read_channel_map
+ 5 [+1] Flag le_read_remote_features
+ 6 [+1] Flag le_encrypt
+ 7 [+1] Flag le_rand
+
+ if octet == 28:
+ 0 [+1] Flag le_start_encryption
+ 1 [+1] Flag le_long_term_key_request_reply
+ 2 [+1] Flag le_long_term_key_request_negative_reply
+ 3 [+1] Flag le_read_supported_states
+ 4 [+1] Flag le_receiver_test_v1
+ 5 [+1] Flag le_transmitter_test_v1
+ 6 [+1] Flag le_test_end
+
+ if octet == 29:
+ 3 [+1] Flag enhanced_setup_synchronous_connection
+ 4 [+1] Flag enhanced_accept_synchronous_connection
+ 5 [+1] Flag read_local_supported_codecs
+ 6 [+1] Flag set_mws_channel_parameters
+ 7 [+1] Flag set_external_frame_configuration
+
+ if octet == 30:
+ 0 [+1] Flag set_mws_signaling
+ 1 [+1] Flag set_mws_transport_layer
+ 2 [+1] Flag set_mws_scan_frequency_table
+ 3 [+1] Flag get_mws_transport_layer_configuration
+ 4 [+1] Flag set_mws_pattern_configuration
+ 5 [+1] Flag set_triggered_clock_capture
+ 6 [+1] Flag truncated_page
+ 7 [+1] Flag truncated_page_cancel
+
+ if octet == 31:
+ 0 [+1] Flag set_connectionless_peripheral_broadcast
+ 1 [+1] Flag set_connectionless_peripheral_broadcast_receive
+ 2 [+1] Flag start_synchronization_train
+ 3 [+1] Flag receive_synchronization_train
+ 4 [+1] Flag set_reserved_ltaddr
+ 5 [+1] Flag delete_reserved_ltaddr
+ 6 [+1] Flag set_connectionless_peripheral_broadcast_data
+ 7 [+1] Flag read_synchronization_train_parameters
+
+ if octet == 32:
+ 0 [+1] Flag write_synchronization_train_parameters
+ 1 [+1] Flag remote_oob_extended_data_request_reply
+ 2 [+1] Flag read_secure_connections_host_support
+ 3 [+1] Flag write_secure_connections_host_support
+ 4 [+1] Flag read_authenticated_payload_timeout
+ 5 [+1] Flag write_authenticated_payload_timeout
+ 6 [+1] Flag read_local_oob_extended_data
+ 7 [+1] Flag write_secure_connections_test_mode
+
+ if octet == 33:
+ 0 [+1] Flag read_extended_page_timeout
+ 1 [+1] Flag write_extended_page_timeout
+ 2 [+1] Flag read_extended_inquiry_length
+ 3 [+1] Flag write_extended_inquiry_length
+ 4 [+1] Flag le_remote_connection_parameter_request_reply
+ 5 [+1] Flag le_remote_connection_parameter_request_negative_reply
+ 6 [+1] Flag le_set_data_length
+ 7 [+1] Flag le_read_suggested_default_data_length
+
+ if octet == 34:
+ 0 [+1] Flag le_write_suggested_default_data_length
+ 1 [+1] Flag le_read_local_p256_public_key
+ 2 [+1] Flag le_generate_dh_key_v1
+ 3 [+1] Flag le_add_device_to_resolving_list
+ 4 [+1] Flag le_remove_device_from_resolving_list
+ 5 [+1] Flag le_clear_resolving_list
+ 6 [+1] Flag le_read_resolving_list_size
+ 7 [+1] Flag le_read_peer_resolvable_address
+
+ if octet == 35:
+ 0 [+1] Flag le_read_local_resolvable_address
+ 1 [+1] Flag le_set_address_resolution_enable
+ 2 [+1] Flag le_set_resolvable_private_address_timeout
+ 3 [+1] Flag le_read_maximum_data_length
+ 4 [+1] Flag le_read_phy
+ 5 [+1] Flag le_set_default_phy
+ 6 [+1] Flag le_set_phy
+ 7 [+1] Flag le_enhanced_receiver_test_v2
+
+ if octet == 36:
+ 0 [+1] Flag le_enhanced_transmitter_test_v2
+ 1 [+1] Flag le_set_advertising_set_random_address
+ 2 [+1] Flag le_set_extended_advertising_parameters
+ 3 [+1] Flag le_set_extended_advertising_data
+ 4 [+1] Flag le_set_extended_scan_response_data
+ 5 [+1] Flag le_set_extended_advertising_enable
+ 6 [+1] Flag le_read_maximum_advertising_data_length
+ 7 [+1] Flag le_read_number_of_supported_advertising_sets
+
+ if octet == 37:
+ 0 [+1] Flag le_remove_advertising_set
+ 1 [+1] Flag le_clear_advertising_sets
+ 2 [+1] Flag le_set_periodic_advertising_parameters
+ 3 [+1] Flag le_set_periodic_advertising_data
+ 4 [+1] Flag le_set_periodic_advertising_enable
+ 5 [+1] Flag le_set_extended_scan_parameters
+ 6 [+1] Flag le_set_extended_scan_enable
+ 7 [+1] Flag le_extended_create_connection
+
+ if octet == 38:
+ 0 [+1] Flag le_periodic_advertising_create_sync
+ 1 [+1] Flag le_periodic_advertising_create_sync_cancel
+ 2 [+1] Flag le_periodic_advertising_terminate_sync
+ 3 [+1] Flag le_add_device_to_periodic_advertiser_list
+ 4 [+1] Flag le_remove_device_from_periodic_advertiser_list
+ 5 [+1] Flag le_clear_periodic_advertiser_list
+ 6 [+1] Flag le_read_periodic_advertiser_list_size
+ 7 [+1] Flag le_read_transmit_power
+
+ if octet == 39:
+ 0 [+1] Flag le_read_rf_path_compensation
+ 1 [+1] Flag le_write_rf_path_compensation
+ 2 [+1] Flag le_set_privacy_mode
+ 3 [+1] Flag le_receiver_test_v3
+ 4 [+1] Flag le_transmitter_test_v3
+ 5 [+1] Flag le_set_connectionless_cte_transmit_parameters
+ 6 [+1] Flag le_set_connectionless_cte_transmit_enable
+ 7 [+1] Flag le_set_connectionless_iq_sampling_enable
+
+ if octet == 40:
+ 0 [+1] Flag le_set_connection_cte_receive_parameters
+ 1 [+1] Flag le_set_connection_cte_transmit_parameters
+ 2 [+1] Flag le_connection_cte_request_enable
+ 3 [+1] Flag le_connection_cte_response_enable
+ 4 [+1] Flag le_read_antenna_information
+ 5 [+1] Flag le_set_periodic_advertising_receive_enable
+ 6 [+1] Flag le_periodic_advertising_sync_transfer
+ 7 [+1] Flag le_periodic_advertising_set_info_transfer
+
+ if octet == 41:
+ 0 [+1] Flag le_set_periodic_advertising_sync_transfer_parameters
+ 1 [+1] Flag le_set_default_periodic_advertising_sync_transfer_parameters
+ 2 [+1] Flag le_generate_dh_key_v3
+ 3 [+1] Flag read_local_simple_pairing_options
+ 4 [+1] Flag le_modify_sleep_clock_accuracy
+ 5 [+1] Flag le_read_buffer_size_v2
+ 6 [+1] Flag le_read_isotx_sync
+ 7 [+1] Flag le_set_cig_parameters
+
+ if octet == 42:
+ 0 [+1] Flag le_set_cig_parameters_test
+ 1 [+1] Flag le_create_cis
+ 2 [+1] Flag le_remove_cig
+ 3 [+1] Flag le_accept_cis_request
+ 4 [+1] Flag le_reject_cis_request
+ 5 [+1] Flag le_create_big
+ 6 [+1] Flag le_create_big_test
+ 7 [+1] Flag le_terminate_big
+
+ if octet == 43:
+ 0 [+1] Flag le_big_create_sync
+ 1 [+1] Flag le_big_terminate_sync
+ 2 [+1] Flag le_request_peer_sca
+ 3 [+1] Flag le_setup_iso_data_path
+ 4 [+1] Flag le_remove_iso_data_path
+ 5 [+1] Flag le_iso_transmit_test
+ 6 [+1] Flag le_iso_receive_test
+ 7 [+1] Flag le_iso_read_test_counters
+
+ if octet == 44:
+ 0 [+1] Flag le_iso_test_end
+ 1 [+1] Flag le_set_host_feature
+ 2 [+1] Flag le_read_iso_link_quality
+ 3 [+1] Flag le_enhanced_read_transmit_power_level
+ 4 [+1] Flag le_read_remote_transmit_power_level
+ 5 [+1] Flag le_set_path_loss_reporting_parameters
+ 6 [+1] Flag le_set_path_loss_reporting_enable
+ 7 [+1] Flag le_set_transmit_power_reporting_enable
+
+ if octet == 45:
+ 0 [+1] Flag le_transmitter_test_v4
+ 1 [+1] Flag set_ecosystem_base_interval
+ 2 [+1] Flag read_local_supported_codecs_v2
+ 3 [+1] Flag read_local_supported_codec_capabilities
+ 4 [+1] Flag read_local_supported_controller_delay
+ 5 [+1] Flag configure_data_path
+
+ if octet == 46:
+ 0 [+1] Flag le_set_default_subrate
+ 1 [+1] Flag le_subrate_request
+ 2 [+1] Flag le_set_extended_advertising_parameters_v2
+ 5 [+1] Flag le_set_periodic_advertising_subevent_data
+ 6 [+1] Flag le_set_periodic_advertising_response_data
+ 7 [+1] Flag le_set_periodic_sync_subevent
+
+ if octet == 47:
+ 0 [+1] Flag le_extended_create_connection_v2
+ 1 [+1] Flag le_set_periodic_advertising_parameters_v2
+
+
+enum LEChannelSelectionAlgorithm:
+ [maximum_bits: 8]
+ ALGORITHM1 = 0x00
+ ALGORITHM2 = 0x01
+
+# ========================= HCI Event packets ===========================
+# Core Spec v5.3 Vol 4, Part E, Section 7.7
+
+
+struct InquiryCompleteEvent:
+ -- 7.7.1 Inquiry Complete event (v1.1) (BR/EDR)
+ -- HCI_Inquiry_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+
+
+struct InquiryResult:
+ 0 [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- BD_ADDR for a device which responded.
+
+ $next [+1] hci.PageScanRepetitionMode page_scan_repetition_mode
+ $next [+2] UInt reserved
+ -- Reserved for future use.
+
+ $next [+3] hci.ClassOfDevice class_of_device
+ -- Class of Device for the device.
+
+ $next [+2] hci.ClockOffset clock_offset
+ -- The lower 15 bits represent bits 16-2 of CLKNPeripheral-CLK.
+
+
+struct InquiryResultEvent:
+ -- 7.7.2 Inquiry Result event (v1.1) (BR/EDR)
+ -- HCI_Inquiry_Result
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] UInt num_responses
+ -- Number of responses from the Inquiry.
+
+ let response_size = InquiryResult.$size_in_bytes
+ $next [+num_responses*response_size] InquiryResult[] responses
+
+
+struct ConnectionCompleteEvent:
+ -- 7.7.3 Connection Complete Event (v1.1) (BR/EDR)
+ -- HCI_Connection_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address of the connected device
+
+ $next [+1] LinkType link_type
+ $next [+1] hci.GenericEnableParam encryption_enabled
+
+
+struct ConnectionRequestEvent:
+ -- 7.7.4 Connection Request event (v1.1) (BR/EDR)
+ -- HCI_Connection_Request
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address of the device that's requesting the connection.
+
+ $next [+3] hci.ClassOfDevice class_of_device
+ -- The Class of Device of the device which requests the connection.
+
+ $next [+1] LinkType link_type
+
+
+struct DisconnectionCompleteEvent:
+ -- 7.7.5 Disconnection Complete event (v1.1) (BR/EDR & LE)
+ -- HCI_Disconnection_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] hci.StatusCode reason
+
+
+struct AuthenticationCompleteEvent:
+ -- 7.7.6 Authentication Complete event (v1.1) (BR/EDR)
+ -- HCI_Authentication_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct RemoteNameRequestCompleteEvent:
+ -- 7.7.7 Remote Name Request Complete event (v1.1) (BR/EDR)
+ -- HCI_Remote_Name_Request_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ $next [+248] UInt:8[248] remote_name
+ -- UTF-8 encoded friendly name. If the name is less than 248 characters, it
+ -- is null terminated and the remaining bytes are not valid.
+
+
+struct EncryptionChangeEventV1:
+ -- 7.7.8 Encryption Change event (v1.1) (BR/EDR & LE)
+ -- HCI_Encryption_Change [v1]
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] EncryptionStatus encryption_enabled
+
+
+struct ChangeConnectionLinkKeyCompleteEvent:
+ -- 7.7.9 Change Connection Link Key Complete event (v1.1) (BR/EDR)
+ -- HCI_Change_Connection_Link_Key_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+# 7.7.10 Link Key Type Changed event
+# HCI_Link_Key_Type_Changed
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct ReadRemoteSupportedFeaturesCompleteEvent:
+ -- 7.7.11 Read Remote Supported Features Complete event (v1.1) (BR/EDR)
+ -- HCI_Read_Remote_Supported_Features_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] LmpFeatures(0) lmp_features
+ -- Page 0 of the LMP features.
+
+
+struct ReadRemoteVersionInfoCompleteEvent:
+ -- 7.7.12 Read Remote Version Information Complete event (v1.1) (BR/EDR & LE)
+ -- HCI_Read_Remote_Version_Information_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] CoreSpecificationVersion version
+ -- Version of the Current LMP or Link Layer supported by the remote Controller.
+
+ $next [+2] UInt company_identifier
+ -- Company identifier for the manufacturer of the remote Controller. Assigned by Bluetooth SIG.
+
+ $next [+2] UInt subversion
+ -- Revision of the LMP or Link Layer implementation in the remote Controller. This value is vendor-specific.
+
+# 7.7.13 QoS Setup Complete event
+# HCI_QoS_Setup_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct SimpleCommandCompleteEvent:
+ -- 7.7.14 Command Complete event (status only)
+ -- HCI_Command_Complete
+ -- A Command Complete event where a StatusCode is the only return parameter.
+ -- Also useful for generically getting the status of a larger command complete
+ -- event.
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+
+
+struct ReadLocalVersionInfoCommandCompleteEvent:
+ -- 7.7.14 Command Complete event (7.4.1 Read Local Version Information command)
+
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+
+ $next [+1] hci.StatusCode status
+
+ $next [+1] CoreSpecificationVersion hci_version
+ -- Version of the HCI Specification supported by the Controller. See
+ -- Assigned Numbers
+
+ $next [+2] UInt hci_subversion
+ -- Revision of the HCI implementation in the Controller. This value is
+ -- vendor-specific.
+
+ $next [+1] UInt lmp_version
+ -- Version of the Current LMP supported by the Controller. See Assigned
+ -- Numbers
+
+ $next [+2] UInt company_identifier
+ -- Company identifier for the manufacturer of the Controller. See Assigned
+ -- Numbers
+
+ $next [+2] UInt lmp_subversion
+ -- Subversion of the Current LMP in the Controller. This value is
+ -- vendor-specific.
+
+
+struct ReadLocalSupportedCommandsCommandCompleteEvent:
+ -- 7.7.14 Command Complete event (7.4.2 Read Local Supported Commands command)
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+ $next [+1] SupportedCommands(0) supported_commands_0
+ $next [+1] SupportedCommands(1) supported_commands_1
+ $next [+1] SupportedCommands(2) supported_commands_2
+ $next [+1] SupportedCommands(3) supported_commands_3
+ $next [+1] SupportedCommands(4) supported_commands_4
+ $next [+1] SupportedCommands(5) supported_commands_5
+ $next [+1] SupportedCommands(6) supported_commands_6
+ $next [+1] SupportedCommands(7) supported_commands_7
+ $next [+1] SupportedCommands(8) supported_commands_8
+ $next [+1] SupportedCommands(9) supported_commands_9
+ $next [+1] SupportedCommands(10) supported_commands_10
+ $next [+1] SupportedCommands(11) supported_commands_11
+ $next [+1] SupportedCommands(12) supported_commands_12
+ $next [+1] SupportedCommands(13) supported_commands_13
+ $next [+1] SupportedCommands(14) supported_commands_14
+ $next [+1] SupportedCommands(15) supported_commands_15
+ $next [+1] SupportedCommands(16) supported_commands_16
+ $next [+1] SupportedCommands(17) supported_commands_17
+ $next [+1] SupportedCommands(18) supported_commands_18
+ $next [+1] SupportedCommands(19) supported_commands_19
+ $next [+1] SupportedCommands(20) supported_commands_20
+ $next [+1] SupportedCommands(21) supported_commands_21
+ $next [+1] SupportedCommands(22) supported_commands_22
+ $next [+1] SupportedCommands(23) supported_commands_23
+ $next [+1] SupportedCommands(24) supported_commands_24
+ $next [+1] SupportedCommands(25) supported_commands_25
+ $next [+1] SupportedCommands(26) supported_commands_26
+ $next [+1] SupportedCommands(27) supported_commands_27
+ $next [+1] SupportedCommands(28) supported_commands_28
+ $next [+1] SupportedCommands(29) supported_commands_29
+ $next [+1] SupportedCommands(30) supported_commands_30
+ $next [+1] SupportedCommands(31) supported_commands_31
+ $next [+1] SupportedCommands(32) supported_commands_32
+ $next [+1] SupportedCommands(33) supported_commands_33
+ $next [+1] SupportedCommands(34) supported_commands_34
+ $next [+1] SupportedCommands(35) supported_commands_35
+ $next [+1] SupportedCommands(36) supported_commands_36
+ $next [+1] SupportedCommands(37) supported_commands_37
+ $next [+1] SupportedCommands(38) supported_commands_38
+ $next [+1] SupportedCommands(39) supported_commands_39
+ $next [+1] SupportedCommands(40) supported_commands_40
+ $next [+1] SupportedCommands(41) supported_commands_41
+ $next [+1] SupportedCommands(42) supported_commands_42
+ $next [+1] SupportedCommands(43) supported_commands_43
+ $next [+1] SupportedCommands(44) supported_commands_44
+ $next [+1] SupportedCommands(45) supported_commands_45
+ $next [+1] SupportedCommands(46) supported_commands_46
+ $next [+1] SupportedCommands(47) supported_commands_47
+
+
+struct ReadBufferSizeCommandCompleteEvent:
+ -- 7.7.14 Command Complete event (7.4.5 Read Buffer Size command)
+
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+
+ $next [+1] hci.StatusCode status
+
+ $next [+2] UInt acl_data_packet_length
+ -- Maximum length (in octets) of the data portion of each HCI ACL Data
+ -- packet that the Controller is able to accept.
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+ $next [+1] UInt synchronous_data_packet_length
+ -- Maximum length (in octets) of the data portion of each HCI Synchronous
+ -- Data packet that the Controller is able to accept.
+ [requires: 0x01 <= this <= 0xFF]
+
+ $next [+2] UInt total_num_acl_data_packets
+ -- Total number of HCI ACL Data packets that can be stored in the data
+ -- buffers of the Controller.
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+ $next [+2] UInt total_num_synchronous_data_packets
+ -- Total number of HCI Synchronous Data packets that can be stored in the
+ -- data buffers of the Controller. A value of 0 indicates that the
+ -- Controller does not support SCO or eSCO over HCI.
+
+
+struct ReadBdAddrCommandCompleteEvent:
+ -- 7.7.14 Command Complete event (7.4.6 Read BD_ADDR command)
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+
+
+struct LEReadLocalSupportedFeaturesCommandCompleteEvent:
+ -- 7.7.14 Command Complete event (7.8.3 LE Read Local Supported Features command)
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+ $next [+8] bits:
+ 0 [+LEFeatureSet.$size_in_bits] LEFeatureSet le_features
+
+
+struct CommandStatusEvent:
+ -- 7.7.15 Command Status event
+ -- HCI_Command_Status
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+1] UInt num_hci_command_packets
+ $next [+2] hci.OpCodeBits command_opcode
+
+# 7.7.16 Hardware Error event
+# HCI_Hardware_Error
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.17 Flush Occurred event
+# HCI_Flush_Occurred
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct RoleChangeEvent:
+ -- 7.7.18 Role Change event (BR/EDR) (v1.1)
+ -- HCI_Role_Change
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address of the device for which a role change has completed.
+
+ $next [+1] hci.ConnectionRole role
+ -- The new role for the specified address.
+
+# 7.7.19 Number Of Completed Packets event
+# HCI_Number_Of_Completed_Packets
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.20 Mode Change event
+# HCI_Mode_Change
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.21 Return Link Keys event
+# HCI_Return_Link_Keys
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.22 PIN Code Request event
+# HCI_PIN_Code_Request
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LinkKeyRequestEvent:
+ -- 7.7.23 Link Key Request event (v1.1) (BR/EDR)
+ -- HCI_Link_Key_Request
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address for the device that a host-stored link key is being requested.
+
+
+struct LinkKeyNotificationEvent:
+ -- 7.7.24 Link Key Notification event (v1.1) (BR/EDR)
+ -- HCI_Link_Key_Notification
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address for the device for which a new link key has been generated.
+
+ $next [+hci.LinkKey.$size_in_bytes] hci.LinkKey link_key
+ -- Link key for the associated address.
+
+ $next [+1] KeyType key_type
+ -- Type of key used when pairing.
+
+# 7.7.25 Loopback Command event
+# HCI_Loopback_Command
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct DataBufferOverflowEvent:
+ -- 7.7.26 Data Buffer Overflow event (v1.1) (BR/EDR & LE)
+ -- HCI_Data_Buffer_Overflow
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] LinkType ll_type
+
+# 7.7.27 Max Slots Change event
+# HCI_Max_Slots_Change
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.28 Read Clock Offset Complete event
+# HCI_Read_Clock_Offset_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.29 Connection Packet Type Changed event
+# HCI_Connection_Packet_Type_Changed
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.30 QoS Violation event
+# HCI_QoS_Violation
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.31 Page Scan Repetition Mode Change event
+# HCI_Page_Scan_Repetition_Mode_Change
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.32 Flow Specification Complete event
+# HCI_Flow_Specification_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct InquiryResultWithRssi:
+ -- A single inquiry result (with RSSI).
+ 0 [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address for the device which responded.
+
+ $next [+1] hci.PageScanRepetitionMode page_scan_repetition_mode
+ -- The Page Scan Repetition Mode being used by the remote device.
+
+ $next [+1] UInt reserved
+ $next [+3] hci.ClassOfDevice class_of_device
+ $next [+2] hci.ClockOffset clock_offset
+ -- The lower 15 bits represent bits 16-2 of CLKNPeripheral-CLK. The most
+ -- significant bit is reserved.
+
+ $next [+1] Int rssi
+ -- Units: dBm
+ [requires: -127 <= this <= 20]
+
+
+struct InquiryResultWithRssiEvent:
+ -- 7.7.33 Inquiry Result with RSSI event (v1.2) (BR/EDR)
+ -- HCI_Inquiry_Result_with_RSSI
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] UInt num_responses
+ -- The number of responses included.
+
+ let response_size = InquiryResultWithRssi.$size_in_bytes
+ $next [+num_responses*response_size] InquiryResultWithRssi[] responses
+
+
+struct ReadRemoteExtendedFeaturesCompleteEvent:
+ -- 7.7.34 Read Remote Extended Features Complete event (v1.1) (BR/EDR)
+ -- HCI_Read_Remote_Extended_Features_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] UInt page_number
+ -- 0x00: The normal LMP features as returned by HCI_Read_Remote_Supported_Features command.
+ -- 0x01 to 0xFF: The page number of the features returned.
+
+ $next [+1] UInt max_page_number
+ -- The highest features page number which contains non-zero bits for the remote device.
+
+ $next [+8] LmpFeatures(page_number) lmp_features
+ -- Bit map of requested page of LMP features.
+
+
+struct SynchronousConnectionCompleteEvent:
+ -- 7.7.35 Synchronous Connection Complete event (BR/EDR)
+ -- HCI_Synchronous_Connection_Complete
+
+ let hdr_size = hci.EventHeader.$size_in_bytes
+
+ 0 [+hdr_size] hci.EventHeader header
+
+ $next [+1] hci.StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- A connection handle for the newly created SCO connection.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- BD_ADDR of the other connected device forming the connection.
+
+ $next [+1] LinkType link_type
+
+ $next [+1] UInt transmission_interval
+ -- Time between two consecutive eSCO instants measured in slots. Shall be
+ -- zero for SCO links.
+
+ $next [+1] UInt retransmission_window
+ -- The size of the retransmission window measured in slots. Shall be zero
+ -- for SCO links.
+
+ $next [+2] UInt rx_packet_length
+ -- Length in bytes of the eSCO payload in the receive direction. Shall be
+ -- zero for SCO links.
+
+ $next [+2] UInt tx_packet_length
+ -- Length in bytes of the eSCO payload in the transmit direction. Shall be
+ -- zero for SCO links.
+
+ $next [+1] hci.CodingFormat air_mode
+
+# 7.7.36 Synchronous Connection Changed event
+# HCI_Synchronous_Connection_Changed
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.37 Sniff Subrating event
+# HCI_Sniff_Subrating
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct ExtendedInquiryResultEvent:
+ -- 7.7.38 Extended Inquiry Result event (v1.2) (BR/EDR)
+ -- HCI_Extended_Inquiry_Result
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] UInt num_responses
+ -- Number of responses from the inquiry. The HCI_Extended_Inquiry_Result
+ -- event always contains a single response.
+ [requires: this == 0x01]
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- BD_ADDR of the device that responded.
+
+ $next [+1] hci.PageScanRepetitionMode page_scan_repetition_mode
+ -- The Page Scan Repetition Mode being used by the remote device.
+
+ $next [+1] UInt reserved
+ $next [+3] hci.ClassOfDevice class_of_device
+ $next [+2] hci.ClockOffset clock_offset
+ -- The lower 15 bits represent bits 16-2 of CLKNPeripheral-CLK.
+
+ $next [+1] Int rssi
+ -- Units: dBm
+ [requires: -127 <= this <= 20]
+
+ $next [+240] UInt:8[240] extended_inquiry_response
+ -- Extended inquiey response data as defined in Vol 3, Part C, Sec 8
+
+
+struct EncryptionKeyRefreshCompleteEvent:
+ -- 7.7.39 Encryption Key Refresh Complete event (v2.1 + EDR) (BR/EDR & LE)
+ -- HCI_Encryption_Key_Refresh_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ -- The connection_handle on which the encryption key was refreshed due to
+ -- encryption being started or resumed.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct IoCapabilityRequestEvent:
+ -- 7.7.40 IO Capability Request event (v2.1 + EDR) (BR/EDR)
+ -- HCI_IO_Capability_Request
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address of the remote device involved in the Secure Simple Pairing
+ -- process.
+
+
+struct IoCapabilityResponseEvent:
+ -- 7.7.41 IO Capability Response event (v2.1 + EDR) (BR/EDR)
+ -- HCI_IO_Capability_Response
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- The address of the remote device which the IO capabilities apply
+
+ $next [+1] hci.IoCapability io_capability
+ -- IO Capabilities of the device
+
+ $next [+1] hci.GenericPresenceParam oob_data_present
+ -- Whether out-of-band authentication data is present.
+
+ $next [+1] hci.AuthenticationRequirements authentication_requirements
+
+
+struct UserConfirmationRequestEvent:
+ -- 7.7.42 User Confirmation Request event (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Confirmation_Request
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- Address of the device involved in simple pairing process
+
+ $next [+4] UInt numeric_value
+ -- Numeric value to be displayed.
+ [requires: 0 <= this <= 999999]
+
+
+struct UserPasskeyRequestEvent:
+ -- 7.7.43 User Passkey Request event (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Passkey_Request
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- Address of the device involved in simple pairing process
+
+# 7.7.44 Remote OOB Data Request event
+# HCI_Remote_OOB_Data_Request
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct SimplePairingCompleteEvent:
+ -- 7.7.45 Simple Pairing Complete event (v2.1 + EDR) (BR/EDR)
+ -- HCI_Simple_Pairing_Complete
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] hci.StatusCode status
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- Address of the device involved in simple pairing process
+
+# 7.7.46 Link Supervision Timeout Changed event
+# HCI_Link_Supervision_Timeout_Changed
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.47 Enhanced Flush Complete event
+# HCI_Enhanced_Flush_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct UserPasskeyNotificationEvent:
+ -- 7.7.48 User Passkey Notification event (v2.1 + EDR) (BR/EDR)
+ -- HCI_User_Passkey_Notification
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr bd_addr
+ -- Address of the device involved in simple pairing process
+
+ $next [+4] UInt passkey
+ -- Numeric value (passkey) entered by user.
+ [requires: 0 <= this <= 999999]
+
+# 7.7.49 Keypress Notification event
+# HCI_Keypress_Notification
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.50 Remote Host Supported Features Notification event
+# HCI_Remote_Host_Supported_Features_Notification
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.59 Number Of Completed Data Blocks event
+# HCI_Number_Of_Completed_Data_Blocks
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEMetaEvent:
+ -- 7.7.65 LE Meta event
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] UInt subevent_code
+ -- The event code for the LE subevent.
+
+
+struct LEConnectionCompleteSubevent:
+ -- 7.7.65.1 LE Connection Complete event
+ -- HCI_LE_Connection_Complete
+
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+ $next [+1] hci.StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] hci.ConnectionRole role
+
+ $next [+1] hci.LEPeerAddressType peer_address_type
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_address
+ -- Public Device Address or Random Device Address of the peer device.
+
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+1] LEClockAccuracy central_clock_accuracy
+ -- Only valid for a peripheral. On a central, this parameter shall be set to 0x00.
+
+# 7.7.65.2 LE Advertising Report event
+# HCI_LE_Advertising_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEConnectionUpdateCompleteSubevent:
+ -- 7.7.65.3 LE Connection Update Complete event
+ -- HCI_LE_Connection_Update_Complete
+
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+ $next [+1] hci.StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+
+struct LEReadRemoteFeaturesCompleteSubevent:
+ -- 7.7.65.4 LE Read Remote Features Complete event
+ -- HCI_LE_Read_Remote_Features_Complete
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] bits:
+ 0 [+LEFeatureSet.$size_in_bits] LEFeatureSet le_features
+
+
+struct LELongTermKeyRequestSubevent:
+ -- 7.7.65.5 LE Long Term Key Request event (v4.0) (LE)
+ -- HCI_LE_Long_Term_Key_Request
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] UInt random_number
+ $next [+2] UInt encrypted_diversifier
+
+# 7.7.65.6 LE Remote Connection Parameter Request event
+# HCI_LE_Remote_Connection_Parameter_Request
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.7 LE Data Length Change event
+# HCI_LE_Data_Length_Change
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.8 LE Read Local P-256 Public Key Complete event
+# HCI_LE_Read_Local_P-256_Public_Key_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.9 LE Generate DHKey Complete event
+# HCI_LE_Generate_DHKey_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEEnhancedConnectionCompleteSubeventV1:
+ -- 7.7.65.10 LE Enhanced Connection Complete event
+ -- HCI_LE_Enhanced_Connection_Complete
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+ $next [+1] hci.StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] hci.ConnectionRole role
+ $next [+1] hci.LEAddressType peer_address_type
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_address
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr local_resolvable_private_address
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr peer_resolvable_private_address
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+1] LEClockAccuracy central_clock_accuracy
+ -- Only valid for a peripheral. On a central, this parameter shall be set to 0x00.
+
+# 7.7.65.10 LE Enhanced Connection Complete event
+# HCI_LE_Enhanced_Connection_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.11 LE Directed Advertising Report event
+# HCI_LE_Directed_Advertising_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.12 LE PHY Update Complete event
+# HCI_LE_PHY_Update_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEExtendedAdvertisingReportData:
+ 0 [+2] bits:
+
+ 0 [+LEExtendedAdvertisingEventType.$size_in_bits] LEExtendedAdvertisingEventType event_type
+
+ $next [+1] hci.LEExtendedAddressType address_type
+ -- Address type of the advertiser.
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr address
+ -- Public Device Address, Random Device Address, Public Identity Address or
+ -- Random (static) Identity Address of the advertising device.
+
+ $next [+1] hci.LEPrimaryAdvertisingPHY primary_phy
+ -- Indicates the PHY used to send the advertising PDU on the primary advertising
+ -- channel. Legacy PDUs always use LE_1M. NONE, LE_2M, and LE_CODED_S2 are excluded.
+
+ $next [+1] hci.LESecondaryAdvertisingPHY secondary_phy
+ -- Indicates the PHY used to send the advertising PDU(s), if any, on the secondary
+ -- advertising channel. A value of NONE means that no packets were received on the
+ -- secondary advertising channel.
+
+ $next [+1] UInt advertising_sid
+ -- Value of the Advertising SID subfield in the ADI field of the PDU. A value of
+ -- 0xFF means no ADI field provided.
+ [requires: 0x00 <= this <= 0x0F || this == 0xFF]
+
+ $next [+1] UInt tx_power
+ -- Units: dBm. A value of 0x7F means Tx Power information is not available.
+ [requires: -127 <= this <= 20 || this == 0x7F]
+
+ $next [+1] Int rssi
+ -- Units: dBm. A value of 0x7F means RSSI is not available.
+ [requires: -127 <= this <= 20 || this == 0x7F]
+
+ $next [+2] UInt periodic_advertising_interval
+ -- 0x0000: No periodic advertising.
+ -- 0xXXXX:
+ -- Time = N * 1.25 ms
+ -- Time Range: 7.5 ms to 81,918.75 s
+ [requires: 0x0006 <= this <= 0xFFFF || this == 0x0000]
+
+ $next [+1] LEDirectAddressType direct_address_type
+
+ $next [+hci.BdAddr.$size_in_bytes] hci.BdAddr direct_address
+ -- TargetA field in the advertisement or either Public Identity Address or Random (static)
+ -- Identity Address of the target device.
+
+ $next [+1] UInt data_length
+ -- Length of the |data| field.
+
+ $next [+data_length] UInt:8[data_length] data
+ -- |data_length| octets of advertising or scan response data formatted as defined in
+ -- [Vol 3] Part C, Section 11. Note: Each element of this array has a variable length.
+
+
+struct LEExtendedAdvertisingReportSubevent(reports_size: UInt:8):
+ -- 7.7.65.13 LE Extended Advertising Report event (v5.0) (LE)
+ -- HCI_LE_Extended_Advertising_Report
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+ $next [+1] UInt num_reports
+ -- Number of separate reports in the event.
+ [requires: 0x01 <= this <= 0x0A]
+
+ $next [+reports_size] UInt:8[reports_size] reports
+ -- Since each report has a variable length, they are stored in a UInt:8 array.
+
+# 7.7.65.14 LE Periodic Advertising Sync Established event
+# HCI_LE_Periodic_Advertising_Sync_Established
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.15 LE Periodic Advertising Report event
+# HCI_LE_Periodic_Advertising_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.16 LE Periodic Advertising Sync Lost event
+# HCI_LE_Periodic_Advertising_Sync_Lost
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEScanTimeoutSubevent:
+ -- 7.7.65.17 LE Scan Timeout event
+ -- HCI_LE_Scan_Timeout
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+# 7.7.65.18 LE Advertising Set Terminated event
+# HCI_LE_Advertising_Set_Terminated
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.19 LE Scan Request Received event
+# HCI_LE_Scan_Request_Received
+# TODO: b/265052417 - Definition needs to be added
+
+
+struct LEChannelSelectionAlgorithmSubevent:
+ -- 7.7.65.20 LE Channel Selection Algorithm event
+ -- HCI_LE_Channel_Selection_Algorithm
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] LEChannelSelectionAlgorithm channel_selection_algorithm
+
+# 7.7.65.21 LE Connectionless IQ Report event
+# HCI_LE_Connectionless_IQ_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.22 LE Connection IQ Report event
+# HCI_LE_Connection_IQ_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.23 LE CTE Request Failed event
+# HCI_LE_CTE_Request_Failed
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.24 LE Periodic Advertising Sync Transfer Received event
+# HCI_LE_Periodic_Advertising_Sync_Transfer_Received
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.25 LE CIS Established event
+# HCI_LE_CIS_Established
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.26 LE CIS Request event
+# HCI_LE_CIS_Request
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.27 LE Create BIG Complete event
+# HCI_LE_Create_BIG_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.28 LE Terminate BIG Complete event
+# HCI_LE_Terminate_BIG_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.29 LE BIG Sync Established event
+# HCI_LE_BIG_Sync_Established
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.30 LE BIG Sync Lost event
+# HCI_LE_BIG_Sync_Lost
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.31 LE Request Peer SCA Complete event
+# HCI_LE_Request_Peer_SCA_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.32 LE Path Loss Threshold event
+# HCI_LE_Path_Loss_Threshold
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.33 LE Transmit Power Reporting event
+# HCI_LE_Transmit_Power_Reporting
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.34 LE BIGInfo Advertising Report event
+# HCI_LE_BIGInfo_Advertising_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.35 LE Subrate Change event
+# HCI_LE_Subrate_Change
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.36 LE Periodic Advertising Subevent Data Request event
+# HCI_LE_Periodic_Advertising_Subevent_Data_Request
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.65.37 LE Periodic Advertising Response Report event
+# HCI_LE_Periodic_Advertising_Response_Report
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.66 Triggered Clock Capture event
+# HCI_Triggered_Clock_Capture
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.67 Synchronization Train Complete event
+# HCI_Synchronization_Train_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.68 Synchronization Train Received event
+# HCI_Synchronization_Train_Received
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.69 Connectionless Peripheral Broadcast Receive event
+# HCI_Connectionless_Peripheral_Broadcast_Receive
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.70 Connectionless Peripheral Broadcast Timeout event
+# HCI_Connectionless_Peripheral_Broadcast_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.71 Truncated Page Complete event
+# HCI_Truncated_Page_Complete
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.72 Peripheral Page Response Timeout event
+# HCI_Peripheral_Page_Response_Timeout
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.73 Connectionless Peripheral Broadcast Channel Map Change event
+# HCI_Connectionless_Peripheral_Broadcast_Channel_Map_Change
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.74 Inquiry Response Notification event
+# HCI_Inquiry_Response_Notification
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.75 Authenticated Payload Timeout Expired event
+# HCI_Authenticated_Payload_Timeout_Expired
+# TODO: b/265052417 - Definition needs to be added
+
+
+# 7.7.76 SAM Status Change event
+# HCI_SAM_Status_Change
+# TODO: b/265052417 - Definition needs to be added
diff --git a/pw_bluetooth/public/pw_bluetooth/hci_test.emb b/pw_bluetooth/public/pw_bluetooth/hci_test.emb
new file mode 100644
index 000000000..761991222
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci_test.emb
@@ -0,0 +1,37 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This file contains Emboss definitions for Host Controller Interface packets
+# and types found in the Bluetooth Core Specification. The Emboss compiler is
+# used to generate a C++ header from this file.
+
+import "hci_common.emb" as hci
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::emboss"]
+# ============================ Test packets =============================
+
+
+struct TestCommandPacket:
+ -- Test HCI Command packet with single byte payload.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt payload
+
+
+struct TestEventPacket:
+ -- Test HCI Event packet with single byte payload.
+ let hdr_size = hci.EventHeader.$size_in_bytes
+ 0 [+hdr_size] hci.EventHeader header
+ $next [+1] UInt payload
diff --git a/pw_bluetooth/public/pw_bluetooth/hci_vendor.emb b/pw_bluetooth/public/pw_bluetooth/hci_vendor.emb
new file mode 100644
index 000000000..7e3a0557c
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci_vendor.emb
@@ -0,0 +1,332 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This file contains Emboss packet definitions for extensions to the Bluetooth
+# Host-Controller interface. These extensions are not standardized through the
+# Bluetooth SIG.
+#
+# NOTE: The definitions below are incomplete. They get added as needed.
+# This list will grow as we support more vendor features.
+
+import "hci_common.emb" as hci
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::vendor::android_hci"]
+# ========================= HCI packet headers ==========================
+
+
+struct AndroidCommandHeader:
+ -- HCI Vendor Command packet header.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt sub_opcode
+
+# ======================= Android HCI extensions ========================
+# Documentation: https://source.android.com/devices/bluetooth/hci_requirements
+
+
+enum Capability:
+ [maximum_bits: 8]
+ NOT_CAPABLE = 0x00
+ CAPABLE = 0x01
+
+
+bits AudioCodecSupportMask:
+ 0 [+1] Flag sbc
+ 1 [+1] Flag aac
+ 2 [+1] Flag aptx
+ 3 [+1] Flag aptx_hd
+ 4 [+1] Flag ldac
+
+
+enum A2dpCodecType:
+ [maximum_bits: 8]
+ SBC = 0x01
+ AAC = 0x02
+ APTX = 0x04
+ APTX_HD = 0x08
+ LDAC = 0x10
+
+
+struct A2dpScmsTEnable:
+ 0 [+1] hci.GenericEnableParam enabled
+ $next [+1] UInt header
+
+
+enum A2dpSamplingFrequency:
+ [maximum_bits: 8]
+ HZ_44100 = 0x01
+ HZ_48000 = 0x02
+ HZ_88200 = 0x04
+ HZ_96000 = 0x08
+
+
+enum A2dpBitsPerSample:
+ [maximum_bits: 8]
+ BITS_PER_SAMPLE_16 = 0x01
+ BITS_PER_SAMPLE_24 = 0x02
+ BITS_PER_SAMPLE_32 = 0x04
+
+
+enum A2dpChannelMode:
+ [maximum_bits: 8]
+ MONO = 0x01
+ STEREO = 0x02
+
+
+enum SbcSamplingFrequency:
+ [maximum_bits: 4]
+ HZ_48000 = 0x01
+ HZ_44100 = 0x02
+ HZ_32000 = 0x04
+ HZ_16000 = 0x08
+
+
+enum SbcChannelMode:
+ [maximum_bits: 4]
+ JOINT_STEREO = 0x01
+ STEREO = 0x02
+ DUAL_CHANNEL = 0x04
+ MONO = 0x08
+
+
+enum SbcBlockLen:
+ [maximum_bits: 4]
+ BLOCK_LEN_16 = 0x01
+ BLOCK_LEN_12 = 0x02
+ BLOCK_LEN_8 = 0x04
+ BLOCK_LEN_4 = 0x08
+
+
+enum SbcSubBands:
+ [maximum_bits: 2]
+ SUBBANDS_8 = 0x01
+ SUBBANDS_4 = 0x02
+
+
+enum SbcAllocationMethod:
+ [maximum_bits: 2]
+ LOUNDNESS = 0x01
+ SNR = 0x02
+
+
+enum AacEnableVariableBitRate:
+ -- 1-octet boolean "enable"/"disable" parameter for AAC variable bitrate
+ [maximum_bits: 8]
+ DISABLE = 0x00
+ ENABLE = 0x80
+
+
+enum LdacBitrateIndex:
+ -- Values 0x03 - 0x7E are reserved
+ -- Values 0x80 - 0xFF are reserved
+ [maximum_bits: 8]
+ HIGH = 0x00
+ MID = 0x01
+ LOW = 0x02
+ ADAPTIVE_BITRATE = 0x7F
+
+
+bits LdacChannelMode:
+ -- Bitmask values for LDAC Channel Mode
+ 0 [+1] Flag stereo
+ 1 [+1] Flag dual
+ 2 [+1] Flag mono
+
+
+struct SbcCodecInformation:
+ 0 [+1] bits:
+ 0 [+2] SbcAllocationMethod allocation_method
+ $next [+2] SbcSubBands subbands
+ $next [+4] SbcBlockLen block_length
+
+ $next [+1] UInt min_bitpool_value
+ $next [+1] UInt max_bitpool_value
+ $next [+1] bits:
+ 0 [+4] SbcChannelMode channel_mode
+ $next [+4] SbcSamplingFrequency sampling_frequency
+
+ $next [+28] UInt:8[28] reserved
+
+
+struct AacCodecInformation:
+ 0 [+1] UInt object_type
+ $next [+1] AacEnableVariableBitRate variable_bit_rate
+ $next [+30] UInt:8[30] reserved
+
+
+struct LdacCodecInformation:
+ 0 [+4] UInt vendor_id
+ -- Must always be set to kLdacVendorId
+
+ $next [+2] UInt codec_id
+ -- Must always be set to kLdacCodecId
+ -- All other values are reserved
+
+ $next [+1] LdacBitrateIndex bitrate_index
+ -- See enum class LdacBitrateIndex in this file for possible values
+
+ $next [+1] bits:
+ 0 [+3] LdacChannelMode ldac_channel_mode
+ -- Bitmask: LDAC channel mode (see LdacChannelMode for bitmask values)
+
+ $next [+24] UInt:8[24] reserved
+
+# ============ Commands ============
+
+
+struct StartA2dpOffloadCommand:
+ let vendor_size = AndroidCommandHeader.$size_in_bytes
+
+ 0 [+vendor_size] AndroidCommandHeader vendor_command
+
+ $next [+4] bits:
+
+ 0 [+8] A2dpCodecType codec_type
+ -- See enum class A2dpCodecType in this file for possible values
+
+ $next [+2] UInt max_latency
+ -- Max latency allowed in ms. A value of zero disables flush.
+
+ $next [+2] A2dpScmsTEnable scms_t_enable
+
+ $next [+4] bits:
+
+ 0 [+8] A2dpSamplingFrequency sampling_frequency
+ -- See enum class A2dpSamplingFrequency in this file for possible values
+
+ $next [+1] A2dpBitsPerSample bits_per_sample
+ -- See enum class A2dpBitsPerSample in this file for possible values
+
+ $next [+1] A2dpChannelMode channel_mode
+ -- See enum class A2dpChannelMode in this file for possible values
+
+ $next [+4] UInt encoded_audio_bitrate
+ -- The encoded audio bitrate in bits per second
+ -- 0x00000000 - The audio bitrate is not specified / unused
+ -- 0x00000001 - 0x00FFFFFF - Encoded audio bitrate in bits per second
+ -- 0x01000000 - 0xFFFFFFFF - Reserved
+ [requires: 0x00000000 <= this <= 0x00FFFFFF]
+
+ $next [+2] UInt connection_handle
+ -- Connection handle of A2DP connection being configured (only the lower 12-bits are meaningful)
+ -- Range: 0x0000 to 0x0EFF
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt l2cap_channel_id
+ -- L2CAP channel ID to be used for this A2DP connection
+
+ $next [+2] UInt l2cap_mtu_size
+ -- Maximum size of L2CAP MTU containing encoded audio packets
+
+ if codec_type == A2dpCodecType.SBC:
+ 28 [+32] SbcCodecInformation sbc_codec_information
+
+ if codec_type == A2dpCodecType.AAC:
+ 28 [+32] AacCodecInformation aac_codec_information
+
+ if codec_type == A2dpCodecType.LDAC:
+ 28 [+32] LdacCodecInformation ldac_codec_information
+
+ if codec_type == A2dpCodecType.APTX || codec_type == A2dpCodecType.APTX_HD:
+ 28 [+32] UInt:8[32] reserved
+
+
+struct StopA2dpOffloadCommand:
+ let vendor_size = AndroidCommandHeader.$size_in_bytes
+ 0 [+vendor_size] AndroidCommandHeader vendor_command
+
+
+struct LEMultiAdvtEnableCommand:
+ -- LE multi-advertising enable command.
+ let vendor_size = AndroidCommandHeader.$size_in_bytes
+ 0 [+vendor_size] AndroidCommandHeader vendor_command
+ $next [+1] hci.GenericEnableParam enable
+ $next [+1] UInt advertising_handle
+
+
+struct LEGetVendorCapabilitiesCommand:
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+# ============ Events ============
+
+
+struct LEMultiAdvtStateChangeSubevent:
+ -- LE multi-advertising state change subevent.
+ 0 [+hci.VendorDebugEvent.$size_in_bytes] hci.VendorDebugEvent vendor_event
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+1] hci.StatusCode status
+ -- Reason for state change. Currently will always be 0x00.
+ -- 0x00: Connection received.
+
+ $next [+2] UInt connection_handle
+ -- Handle used to identify the connection that caused the state change (i.e.
+ -- advertising instance to be disabled). Value will be 0xFFFF if invalid.
+
+
+struct LEGetVendorCapabilitiesCommandCompleteEvent:
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+ $next [+1] UInt max_advt_instances
+ -- Number of advertisement instances supported
+ -- Deprecated in Google feature spec v0.98 and higher
+
+ $next [+1] Capability offloaded_resolution_of_private_address
+ -- BT chip capability of RPA
+ -- Deprecated in Google feature spec v0.98 and higher
+
+ $next [+2] UInt total_scan_results_storage
+ -- Storage for scan results in bytes
+
+ $next [+1] UInt max_irk_list_sz
+ -- Number of IRK entries supported in the firmware
+
+ $next [+1] Capability filtering_support
+ -- Support for filtering in the controller
+
+ $next [+1] UInt max_filter
+ -- Number of filters supported
+
+ $next [+1] Capability activity_energy_info_support
+ -- Supports reporting of activity and energy information
+
+ $next [+2] bits version_supported:
+ -- Specifies the version of the Google feature spec supported
+ 0 [+8] UInt major_number
+ $next [+8] UInt minor_number
+
+ $next [+2] UInt total_num_of_advt_tracked
+ -- Total number of advertisers tracked for OnLost/OnFound purposes
+
+ $next [+1] Capability extended_scan_support
+ -- Supports extended scan window and interval
+
+ $next [+1] Capability debug_logging_supported
+ -- Supports logging of binary debug information from controller
+
+ $next [+1] Capability le_address_generation_offloading_support
+ -- Deprecated in Google feature spec v0.98 and higher
+
+ $next [+4] bits:
+ 0 [+5] AudioCodecSupportMask a2dp_source_offload_capability_mask
+
+ $next [+1] Capability bluetooth_quality_report_support
+ -- Supports reporting of Bluetooth Quality events
+
+ $next [+4] bits:
+ 0 [+5] AudioCodecSupportMask dynamic_audio_buffer_support
diff --git a/pw_bluetooth/public/pw_bluetooth/vendor.emb b/pw_bluetooth/public/pw_bluetooth/vendor.emb
deleted file mode 100644
index 9ee77343a..000000000
--- a/pw_bluetooth/public/pw_bluetooth/vendor.emb
+++ /dev/null
@@ -1,112 +0,0 @@
-# Copyright 2023 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-# This file contains Emboss packet definitions for extensions to the Bluetooth
-# Host-Controller interface. These extensions are not standardized through the
-# Bluetooth SIG.
-#
-# NOTE: The definitions below are incomplete. They get added as needed.
-# This list will grow as we support more vendor features.
-
-import "hci.emb" as hci
-
-[$default byte_order: "LittleEndian"]
-[(cpp) namespace: "pw::bluetooth::emboss"]
-
-# ======================= Android HCI extensions ========================
-# Documentation: https://source.android.com/devices/bluetooth/hci_requirements
-
-enum Capability:
- [maximum_bits: 8]
- NOT_CAPABLE = 0x00
- CAPABLE = 0x01
-
-bits AudioCodecSupportMask:
- 0 [+1] Flag sbc
- 1 [+1] Flag aac
- 2 [+1] Flag aptx
- 3 [+1] Flag aptx_hd
- 4 [+1] Flag ldac
-
-# ============ Commands ============
-
-
-struct LEMultiAdvtEnableCommand:
- -- LE multi-advertising enable command.
- let hdr_size = hci.CommandHeader.$size_in_bytes
- 0 [+hdr_size] hci.CommandHeader header
- $next [+1] UInt opcode
- $next [+1] hci.GenericEnableParam enable
- $next [+1] UInt advertising_handle
-
-struct LEGetVendorCapabilitiesCommand:
- let hdr_size = hci.CommandHeader.$size_in_bytes
- 0 [+hdr_size] hci.CommandHeader header
-
-
-# ============ Events ============
-
-
-struct LEMultiAdvtStateChangeSubevent:
- -- LE multi-advertising state change subevent.
- 0 [+hci.VendorDebugEvent.$size_in_bytes] hci.VendorDebugEvent vendor_event
- $next [+1] UInt advertising_handle
- -- Handle used to identify an advertising set.
-
- $next [+1] hci.StatusCode status
- -- Reason for state change. Currently will always be 0x00.
- -- 0x00: Connection received.
-
- $next [+2] UInt connection_handle
- -- Handle used to identify the connection that caused the state change (i.e.
- -- advertising instance to be disabled). Value will be 0xFFFF if invalid.
-
-struct LEGetVendorCapabilitiesCommandCompleteEvent:
- let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
- 0 [+hdr_size] hci.CommandCompleteEvent command_complete
- $next [+1] hci.StatusCode status
- $next [+1] UInt max_advt_instances
- -- Number of advertisement instances supported
- -- Deprecated in Google feature spec v0.98 and higher
- $next [+1] Capability offloaded_resolution_of_private_address
- -- BT chip capability of RPA
- -- Deprecated in Google feature spec v0.98 and higher
- $next [+2] UInt total_scan_results_storage
- -- Storage for scan results in bytes
- $next [+1] UInt max_irk_list_sz
- -- Number of IRK entries supported in the firmware
- $next [+1] Capability filtering_support
- -- Support for filtering in the controller
- $next [+1] UInt max_filter
- -- Number of filters supported
- $next [+1] Capability activity_energy_info_support
- -- Supports reporting of activity and energy information
- $next [+2] bits version_supported:
- -- Specifies the version of the Google feature spec supported
- 0 [+8] UInt major_number
- $next [+8] UInt minor_number
- $next [+2] UInt total_num_of_advt_tracked
- -- Total number of advertisers tracked for OnLost/OnFound purposes
- $next [+1] Capability extended_scan_support
- -- Supports extended scan window and interval
- $next [+1] Capability debug_logging_supported
- -- Supports logging of binary debug information from controller
- $next [+1] Capability le_address_generation_offloading_support
- -- Deprecated in Google feature spec v0.98 and higher
- $next [+4] bits:
- 0 [+5] AudioCodecSupportMask a2dp_source_offload_capability_mask
- $next [+1] Capability bluetooth_quality_report_support
- -- Supports reporting of Bluetooth Quality events
- $next [+4] bits:
- 0 [+5] AudioCodecSupportMask dynamic_audio_buffer_support
diff --git a/pw_bluetooth/size_report/BUILD.gn b/pw_bluetooth/size_report/BUILD.gn
index 37ad18bd3..2878e2e85 100644
--- a/pw_bluetooth/size_report/BUILD.gn
+++ b/pw_bluetooth/size_report/BUILD.gn
@@ -22,7 +22,8 @@ if (dir_pw_third_party_emboss != "") {
sources = [ "make_view_and_write.cc" ]
deps = [
"$dir_pw_bloat:bloat_this_binary",
- "$dir_pw_bluetooth:emboss_hci",
+ "$dir_pw_bluetooth:emboss_hci_group",
+ "$dir_pw_bluetooth:emboss_hci_test",
]
}
@@ -30,7 +31,8 @@ if (dir_pw_third_party_emboss != "") {
sources = [ "make_2_views_and_write.cc" ]
deps = [
"$dir_pw_bloat:bloat_this_binary",
- "$dir_pw_bluetooth:emboss_hci",
+ "$dir_pw_bluetooth:emboss_hci_group",
+ "$dir_pw_bluetooth:emboss_hci_test",
]
}
}
diff --git a/pw_bluetooth/size_report/make_2_views_and_write.cc b/pw_bluetooth/size_report/make_2_views_and_write.cc
index f14752259..266ae5b58 100644
--- a/pw_bluetooth/size_report/make_2_views_and_write.cc
+++ b/pw_bluetooth/size_report/make_2_views_and_write.cc
@@ -13,7 +13,7 @@
// the License.
#include "pw_bloat/bloat_this_binary.h"
-#include "pw_bluetooth/hci.emb.h"
+#include "pw_bluetooth/hci_test.emb.h"
int main() {
pw::bloat::BloatThisBinary();
diff --git a/pw_bluetooth/size_report/make_view_and_write.cc b/pw_bluetooth/size_report/make_view_and_write.cc
index 7275d76ba..eb7e75277 100644
--- a/pw_bluetooth/size_report/make_view_and_write.cc
+++ b/pw_bluetooth/size_report/make_view_and_write.cc
@@ -13,7 +13,7 @@
// the License.
#include "pw_bloat/bloat_this_binary.h"
-#include "pw_bluetooth/hci.emb.h"
+#include "pw_bluetooth/hci_test.emb.h"
int main() {
pw::bloat::BloatThisBinary();
diff --git a/pw_bluetooth_hci/BUILD.bazel b/pw_bluetooth_hci/BUILD.bazel
index c5de7f694..33353c9a4 100644
--- a/pw_bluetooth_hci/BUILD.bazel
+++ b/pw_bluetooth_hci/BUILD.bazel
@@ -98,7 +98,6 @@ pw_cc_fuzz_test(
":packet",
":uart_transport",
"//pw_bytes",
- "//pw_fuzzer",
"//pw_status",
"//pw_stream",
],
diff --git a/pw_bluetooth_hci/BUILD.gn b/pw_bluetooth_hci/BUILD.gn
index 0f5107163..75ca8fe95 100644
--- a/pw_bluetooth_hci/BUILD.gn
+++ b/pw_bluetooth_hci/BUILD.gn
@@ -63,12 +63,12 @@ pw_test_group("tests") {
tests = [
":packet_test",
":uart_transport_test",
- ":uart_transport_fuzzer_test",
]
+ group_deps = [ ":fuzzers" ]
}
-group("fuzzers") {
- deps = [ ":uart_transport_fuzzer" ]
+pw_fuzzer_group("fuzzers") {
+ fuzzers = [ ":uart_transport_fuzzer" ]
}
pw_test("packet_test") {
diff --git a/pw_bluetooth_hci/docs.rst b/pw_bluetooth_hci/docs.rst
index 863be7c2b..2d8744aa1 100644
--- a/pw_bluetooth_hci/docs.rst
+++ b/pw_bluetooth_hci/docs.rst
@@ -31,24 +31,25 @@ Decoding
A decoder function is provided to parse HCI packets out of a HCI UART Transport
Layer buffer which may contain multiple packets.
- .. cpp:function:: StatusWithSize DecodeHciUartData(ConstByteSpan data, const DecodedPacketCallback& packet_callback);
+.. cpp:function:: StatusWithSize DecodeHciUartData(ConstByteSpan data, const DecodedPacketCallback& packet_callback);
- Parses the HCI Packets out of a HCI UART Transport Layer buffer.
+ Parses the HCI Packets out of a HCI UART Transport Layer buffer.
- Parses as many complete HCI packets out of the provided buffer based on the
- HCI UART Transport Layer as defined by Bluetooth Core Specification version
- 5.3 "Host Controller Interface Transport Layer" volume 4, part A.
+ Parses as many complete HCI packets out of the provided buffer based on the
+ HCI UART Transport Layer as defined by Bluetooth Core Specification version
+ 5.3 "Host Controller Interface Transport Layer" volume 4, part A.
- The HciPacketCallback is invoked for each full HCI packet.
+ The HciPacketCallback is invoked for each full HCI packet.
- Returns the number of bytes processed and a status based on:
+ Returns the number of bytes processed and a status based on:
- * OK - No invalid packet indicator found.
- * DATA_LOSS - An invalid packet indicator was detected between packets.
- Synchronization has been lost. The caller is responsible for
- regaining synchronization
+ * OK - No invalid packet indicator found.
+ * DATA_LOSS - An invalid packet indicator was detected between packets.
+ Synchronization has been lost. The caller is responsible for
+ regaining synchronization
- .. note:: The caller is responsible for detecting the lack of progress due
- to an undersized data buffer and/or an invalid length field in case a full
+ .. note::
+ The caller is responsible for detecting the lack of progress due to an
+ undersized data buffer and/or an invalid length field in case a full
buffer is passed and no bytes are processed.
diff --git a/pw_boot/BUILD.bazel b/pw_boot/BUILD.bazel
index ec4bac8e9..edce1884f 100644
--- a/pw_boot/BUILD.bazel
+++ b/pw_boot/BUILD.bazel
@@ -35,18 +35,22 @@ pw_cc_facade(
pw_cc_library(
name = "pw_boot",
+ hdrs = [
+ "public/pw_boot/boot.h",
+ ],
+ includes = ["public"],
deps = [
- ":facade",
- "@pigweed_config//:pw_boot_backend",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_boot_backend",
],
)
pw_cc_library(
name = "backend_multiplexer",
deps = select({
- "@platforms//cpu:armv7-m": ["//pw_boot_cortex_m:pw_boot_cortex_m"],
- "@platforms//cpu:armv7e-m": ["//pw_boot_cortex_m:pw_boot_cortex_m"],
- "@platforms//cpu:armv8-m": ["//pw_boot_cortex_m:pw_boot_cortex_m"],
+ "@platforms//cpu:armv7-m": ["//pw_boot_cortex_m"],
+ "@platforms//cpu:armv7e-m": ["//pw_boot_cortex_m"],
+ "@platforms//cpu:armv8-m": ["//pw_boot_cortex_m"],
# When building for the host, we don't need a pw_boot implementation.
"//conditions:default": [],
}),
diff --git a/pw_boot/docs.rst b/pw_boot/docs.rst
index 8be00c777..cec17446f 100644
--- a/pw_boot/docs.rst
+++ b/pw_boot/docs.rst
@@ -22,7 +22,7 @@ Sequence
The high level pw_boot boot sequence looks like the following pseudo-code
invocation of the user-implemented functions:
-.. code:: cpp
+.. code-block:: cpp
void pw_boot_Entry() { // Boot entry point provided by backend.
pw_boot_PreStaticMemoryInit(); // User-implemented function.
diff --git a/pw_boot_cortex_m/BUILD.gn b/pw_boot_cortex_m/BUILD.gn
index 864776671..a2b04f5b6 100644
--- a/pw_boot_cortex_m/BUILD.gn
+++ b/pw_boot_cortex_m/BUILD.gn
@@ -15,23 +15,19 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_boot/backend.gni")
+import("$dir_pw_boot_cortex_m/toolchain.gni")
import("$dir_pw_build/linker_script.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
-declare_args() {
- # This list should contain the necessary defines for setting pw_boot linker
- # script memory regions.
- pw_boot_cortex_m_LINK_CONFIG_DEFINES = []
-
- # The pw_linker_script that should be used for the target.
- pw_boot_cortex_m_LINKER_SCRIPT = ":cortex_m_linker_script"
+_boot_backend = ""
+if (pw_boot_BACKEND != "") {
+ _boot_backend = get_label_info(pw_boot_BACKEND, "label_no_toolchain")
}
-
-if (pw_boot_BACKEND != "$dir_pw_boot_cortex_m" &&
- pw_boot_BACKEND != "$dir_pw_boot_cortex_m:armv7m" &&
- pw_boot_BACKEND != "$dir_pw_boot_cortex_m:armv8m") {
+if (_boot_backend != "$dir_pw_boot_cortex_m:pw_boot_cortex_m" &&
+ _boot_backend != "$dir_pw_boot_cortex_m:armv7m" &&
+ _boot_backend != "$dir_pw_boot_cortex_m:armv8m") {
group("pw_boot_cortex_m") {
}
group("armv7m") {
diff --git a/pw_boot_cortex_m/basic_cortex_m.ld b/pw_boot_cortex_m/basic_cortex_m.ld
index ae84e3e6f..31d2d217e 100644
--- a/pw_boot_cortex_m/basic_cortex_m.ld
+++ b/pw_boot_cortex_m/basic_cortex_m.ld
@@ -65,7 +65,7 @@ ENTRY(pw_boot_Entry)
MEMORY
{
- /* TODO(b/234892223): Make it possible for projects to freely customize
+ /* TODO: b/234892223 - Make it possible for projects to freely customize
* memory regions.
*/
@@ -168,6 +168,14 @@ SECTIONS
PROVIDE_HIDDEN(__fini_array_end = .);
} >FLASH
+ /* GNU build ID section. Used by pw_build_info. */
+ .note.gnu.build-id :
+ {
+ . = ALIGN(4);
+ gnu_build_id_begin = .;
+ *(.note.gnu.build-id);
+ } >FLASH
+
/* Used by unwind-arm/ */
.ARM : ALIGN(4) {
__exidx_start = .;
diff --git a/pw_boot_cortex_m/core_init.c b/pw_boot_cortex_m/core_init.c
index d2d6db3e4..5052e88ba 100644
--- a/pw_boot_cortex_m/core_init.c
+++ b/pw_boot_cortex_m/core_init.c
@@ -122,6 +122,15 @@ void pw_boot_Entry(void) {
asm volatile("cpsid i");
#if _PW_ARCH_ARM_V8M_MAINLINE || _PW_ARCH_ARM_V8_1M_MAINLINE
+ // Set VTOR to the location of the vector table.
+ //
+ // Devices with a bootloader will often set VTOR after parsing the loaded
+ // binary and prior to launching it. When no bootloader is present, or if
+ // launched directly from memory after a reset, VTOR will be zero and must
+ // be assigned the correct value.
+ volatile uint32_t* vtor = (volatile uint32_t*)0xE000ED08u;
+ *vtor = (uintptr_t)&pw_boot_vector_table_addr;
+
// Configure MSP and MSPLIM.
asm volatile(
"msr msp, %0 \n"
diff --git a/pw_boot_cortex_m/docs.rst b/pw_boot_cortex_m/docs.rst
index bff056416..ef2b05df5 100644
--- a/pw_boot_cortex_m/docs.rst
+++ b/pw_boot_cortex_m/docs.rst
@@ -29,9 +29,10 @@ Sequence
The high level pw_boot_cortex_m boot sequence looks like the following
pseudo-code invocation of the user-implemented functions:
-.. code:: cpp
+.. code-block:: cpp
void pw_boot_Entry() { // Boot entry point.
+ // Set VTOR.
// Interrupts disabled.
pw_boot_PreStaticMemoryInit(); // User-implemented function.
// Static memory initialization.
@@ -131,9 +132,10 @@ section, and properly configuring ``PW_BOOT_VECTOR_TABLE_*`` preprocessor
defines to cover the address region your SoC expects the vector table to be
located at (often the beginning of the flash region). If using a bootloader,
ensure VTOR (Vector Table Offset Register) is configured to point to the vector
-table. Otherwise, refer to the hardware vendor's documentation to determine
-where the vector table should be located such that it resides where VTOR is
-initialized to by default.
+table, otherwise ensure that execution begins at ``pw_boot_Entry`` which will
+set VTOR as its first operation. If in doubt, refer to the hardware vendor's
+documentation to determine where the vector table and reset handler should be
+located.
Example vector table:
diff --git a/pw_tokenizer/backend.gni b/pw_boot_cortex_m/toolchain.gni
index ed0addf54..afb5437bc 100644
--- a/pw_tokenizer/backend.gni
+++ b/pw_boot_cortex_m/toolchain.gni
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -13,9 +13,10 @@
# the License.
declare_args() {
- # This variable is deprecated. It is only used for pw_log_tokenized, and is
- # provided for backwards compatibility. Set pw_log_tokenized_HANDLER_BACKEND
- # instead.
- # TODO(hepler): Remove this variable after migrating projects.
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND = ""
+ # This list should contain the necessary defines for setting pw_boot linker
+ # script memory regions.
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = []
+
+ # The pw_linker_script that should be used for the target.
+ pw_boot_cortex_m_LINKER_SCRIPT = ":cortex_m_linker_script"
}
diff --git a/pw_build/BUILD.bazel b/pw_build/BUILD.bazel
index 49e59cdbb..6ee0e3890 100644
--- a/pw_build/BUILD.bazel
+++ b/pw_build/BUILD.bazel
@@ -12,6 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_blob_info",
+ "pw_cc_blob_library",
+ "pw_cc_test",
+)
+
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
@@ -23,9 +30,65 @@ config_setting(
},
)
-# TODO(b/238339027): Run cc_blob_library_test when pw_cc_blob_library is
-# supported in Bazel.
-filegroup(
+pw_cc_blob_info(
+ name = "test_blob_aligned",
+ alignas = "512",
+ file_path = "test_blob_0123.bin",
+ symbol_name = "kFirstBlob0123",
+)
+
+pw_cc_blob_info(
+ name = "test_blob_unaligned",
+ file_path = "test_blob_0123.bin",
+ symbol_name = "kSecondBlob0123",
+)
+
+pw_cc_blob_library(
+ name = "test_blob",
+ blobs = [
+ ":test_blob_aligned",
+ ":test_blob_unaligned",
+ ],
+ namespace = "test::ns",
+ out_header = "pw_build/test_blob.h",
+)
+
+pw_cc_test(
name = "cc_blob_library_test",
srcs = ["cc_blob_library_test.cc"],
+ deps = [":test_blob"],
+)
+
+# Bazel produces root-relative file paths without the -ffile-prefix-map option.
+pw_cc_test(
+ name = "file_prefix_map_test",
+ srcs = [
+ "file_prefix_map_test.cc",
+ "pw_build_private/file_prefix_map_test.h",
+ ],
+ defines = [
+ "PW_BUILD_EXPECTED_HEADER_PATH=\\\"pw_build/pw_build_private/file_prefix_map_test.h\\\"",
+ "PW_BUILD_EXPECTED_SOURCE_PATH=\\\"pw_build/file_prefix_map_test.cc\\\"",
+ ],
+)
+
+# This empty library is used as a placeholder for label flags that need to
+# point to a library of some kind, but don't actually need the dependency to
+# amount to anything.
+cc_library(
+ name = "empty_cc_library",
+)
+
+# A special target used instead of a cc_library as the default condition in
+# backend multiplexer select statements to signal that a facade is in an
+# unconfigured state. This produces better error messages than e.g. using an
+# invalid label.
+#
+# If you're a user whose build errored out because a library transitively
+# depended on this target: the platform you're targeting did not specify which
+# backend to use for some facade. Look at the previous step in the dependency
+# chain (printed with the error) to figure out which one.
+cc_library(
+ name = "unspecified_backend",
+ target_compatible_with = ["@platforms//:incompatible"],
)
diff --git a/pw_build/BUILD.gn b/pw_build/BUILD.gn
index 25f53433a..55b0f8305 100644
--- a/pw_build/BUILD.gn
+++ b/pw_build/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/cc_blob_library.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/relative_source_file_names.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_toolchain/traits.gni")
import("$dir_pw_unit_test/test.gni")
import("target_types.gni")
@@ -139,9 +140,17 @@ config("extra_strict_warnings") {
# added in the future without breaking downstream.
config("internal_strict_warnings") {
cflags = [ "-Wswitch-enum" ]
- cflags_cc = [ "-Wextra-semi" ]
- # TODO(b/243069432): Enable pedantic warnings on Windows when they pass.
+ # TODO: b/301262374 - Provide a better way to detect the compiler type.
+ if (defined(pw_toolchain_SCOPE.cc) &&
+ get_path_info(pw_toolchain_SCOPE.cc, "file") == "clang") {
+ cflags += [ "-Wextra-semi" ]
+ } else {
+ # TODO: b/306734552 - On GCC, this only works as a C++ flag.
+ cflags_cc = [ "-Wextra-semi" ]
+ }
+
+ # TODO: b/243069432 - Enable pedantic warnings on Windows when they pass.
if (host_os != "win") {
configs = [ ":pedantic_warnings" ]
}
@@ -174,7 +183,7 @@ config("pedantic_warnings") {
#
# b/259746255 tracks converting everything to build with this warning.
config("conversion_warnings") {
- # TODO(b/260629756): Remove Windows restriction once fixed for Windows + GCC.
+ # TODO: b/260629756 - Remove Windows restriction once fixed for Windows + GCC.
if (host_os != "win") {
cflags = [ "-Wconversion" ]
}
@@ -217,25 +226,69 @@ config("toolchain_cpp_standard") {
}
# Removes system-dependent prefixes from macros like __FILE__ and debug symbols.
+#
+# All compilation is relative to root_build_dir. The root_build_dir path is
+# arbitrary, so it must be removed to make builds reproducible. This config
+# remaps paths as if compilation occurred from the repository root instead of
+# root_build_dir. Paths that include root_build_dir are updated to standardize
+# on out/ as the build directory.
+#
+# If an ELF is built from a directory other than "out/", debuggers will be able
+# to locate in-tree sources, but may have issues finding generated sources in
+# the output directory. This can be worked around in a few ways:
+#
+# - Use GDB's `set substitute-path <from> <to>` option to remap paths.
+# - Rebuild from out/.
+# - Symlink out/ to the build directory.
+# - Temporarily disable these transformations.
+#
+# If an ELF is built from "out/", debuggers will be able to locate all sources,
+# including generated sources, when run from the repo root.
config("relative_paths") {
+ # Apply a series of file-prefix-map transformations. Only the first matching
+ # option applies.
+ #
+ # GCC applies these in reverse order due to building iterating through a
+ # recursively constructed linked list:
+ # inclusive-language: disable
+ # https://github.com/gcc-mirror/gcc/blob/master/gcc/file-prefix-map.cc#L41.
+ # inclusive-language: enable
+ #
+ # Clang currently does not have a set forwards or backwards application order
+ # due to storing them in a std::map (that reorders lexicogrpahically by key):
+ # https://github.com/llvm/llvm-project/blob/main/clang/include/clang/Basic/CodeGenOptions.h#L209.
+ #
+ # TODO: b/278906020 - Tracks merging in an upstream change to LLVM to that will
+ # enforce clang applying these transformations in order. Until then, there
+ # will be issues with the debug and coverage information while using build
+ # directories that are more than a single directory from the root project
+ # directory. This is due to "=out/" always being applied first due to the
+ # left-hand side of the = being "", which is always ordered first in the
+ # std::map.
_transformations = [
- # Remap absolute paths to the build directory to "out", in case absolute
- # paths to files in the build directory are created.
- #
- # Clang and GCC apply these flags in opposite order. The build directory is
- # often nested under //. To ensure that both compilers removed it before
- # removing the absolute path to //, apply the option both first and last.
- rebase_path(root_build_dir) + "=out",
-
- # Remove absolute paths to the repo root.
+ # Remap absolute prefixes for files in the output directory to out/.
+ rebase_path(root_build_dir) + "/=out/",
+
+ # Remove the path to the out directory so the ELF is relative to the root.
+ rebase_path(root_build_dir) + "=",
+
+ # Remove any absolute paths to the repo root.
rebase_path("//") + "=",
- # Remove relative paths from the build directory to the source tree.
+ # Remove the relative path from the build directory to the root, which is
+ # the prefix of in-tree sources.
rebase_path("//", root_build_dir) + "=",
- # Repeat option to remap absolute paths to the build directory.
- rebase_path(root_build_dir) + "=out",
+ # Prepend out/ to any unmatched files, which are in the output directory.
+ "=out/",
+
+ # Repeat the replacements in reverse order since GCC applies them backwards.
+ rebase_path("//", root_build_dir) + "=",
+ rebase_path("//") + "=",
+ rebase_path(root_build_dir) + "=",
+ rebase_path(root_build_dir) + "/=out/",
]
+
cflags = []
foreach(transform, _transformations) {
@@ -269,7 +322,10 @@ group("empty") {
pw_doc_group("docs") {
sources = [
+ "bazel.rst",
+ "cmake.rst",
"docs.rst",
+ "gn.rst",
"python.rst",
]
}
@@ -301,7 +357,50 @@ if (current_toolchain != default_toolchain) {
visibility = [ ":*" ]
}
+ # file_prefix_map_test verifies that the -ffile-prefix-map option is applied
+ # correctly.
+
+ # File paths should be relative to the root of the GN build.
+ _test_header = rebase_path("pw_build_private/file_prefix_map_test.h", "//")
+ _path_test_define = [ "PW_BUILD_EXPECTED_HEADER_PATH=\"$_test_header\"" ]
+
+ pw_test("file_prefix_map_test") {
+ _source_path = rebase_path("file_prefix_map_test.cc", "//")
+
+ sources = [ "file_prefix_map_test.cc" ]
+
+ defines = _path_test_define +
+ [ "PW_BUILD_EXPECTED_SOURCE_PATH=\"$_source_path\"" ]
+
+ deps = [ ":file_prefix_map_generated_file" ]
+ }
+
+ # Generated file paths should be relative to the build directory.
+ copy("generate_file_prefix_map_test_source") {
+ sources = [ "file_prefix_map_test.cc" ]
+ outputs = [ get_label_info(":file_prefix_map_test", "target_gen_dir") +
+ "/file_prefix_map_test_generated.cc" ]
+ visibility = [ ":*" ]
+ }
+
+ pw_source_set("file_prefix_map_generated_file") {
+ public = [ "pw_build_private/file_prefix_map_test.h" ]
+ sources = get_target_outputs(":generate_file_prefix_map_test_source")
+ deps = [ ":generate_file_prefix_map_test_source" ]
+
+ _source_path = rebase_path(sources[0], root_build_dir)
+
+ # The source file is prefixed with out/ since it's generated.
+ defines = _path_test_define +
+ [ "PW_BUILD_EXPECTED_SOURCE_PATH=\"out/$_source_path\"" ]
+ include_dirs = [ "." ] # Allow accessing file_prefix_map_test.h
+ visibility = [ ":*" ]
+ }
+
pw_test_group("tests") {
- tests = [ ":cc_blob_library_test" ]
+ tests = [
+ ":cc_blob_library_test",
+ ":file_prefix_map_test",
+ ]
}
}
diff --git a/pw_build/CMakeLists.txt b/pw_build/CMakeLists.txt
index 62a310549..4d22be379 100644
--- a/pw_build/CMakeLists.txt
+++ b/pw_build/CMakeLists.txt
@@ -27,7 +27,6 @@ pw_add_library_generic(pw_build INTERFACE
$<$<CXX_COMPILER_ID:GNU>:-fdiagnostics-color=always>
PUBLIC_DEPS
pw_build.reduced_size
- pw_build.cpp17
)
if(ZEPHYR_PIGWEED_MODULE_DIR)
target_link_libraries(pw_build INTERFACE zephyr_interface)
diff --git a/pw_build/bazel.rst b/pw_build/bazel.rst
new file mode 100644
index 000000000..0263eeaa4
--- /dev/null
+++ b/pw_build/bazel.rst
@@ -0,0 +1,355 @@
+.. _module-pw_build-bazel:
+
+Bazel
+=====
+Bazel is currently very experimental, and only builds for host and ARM Cortex-M
+microcontrollers.
+
+.. _module-pw_build-bazel-wrapper-rules:
+
+Wrapper rules
+-------------
+The common configuration for Bazel for all modules is in the ``pigweed.bzl``
+file. The built-in Bazel rules ``cc_binary``, ``cc_library``, and ``cc_test``
+are wrapped with ``pw_cc_binary``, ``pw_cc_library``, and ``pw_cc_test``.
+
+.. _module-pw_build-bazel-pw_linker_script:
+
+pw_linker_script
+----------------
+In addition to wrapping the built-in rules, Pigweed also provides a custom
+rule for handling linker scripts with Bazel. e.g.
+
+.. code-block:: python
+
+ pw_linker_script(
+ name = "some_linker_script",
+ linker_script = ":some_configurable_linker_script.ld",
+ defines = [
+ "PW_BOOT_FLASH_BEGIN=0x08000200",
+ "PW_BOOT_FLASH_SIZE=1024K",
+ "PW_BOOT_HEAP_SIZE=112K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20000000",
+ "PW_BOOT_RAM_SIZE=192K",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ],
+ )
+
+ # You can include the linker script in the deps.
+ cc_binary(
+ name = "some_binary",
+ srcs = ["some_source.cc"],
+ deps = [":some_linker_script"],
+ )
+
+ # Alternatively, you can use additional_linker_inputs and linkopts. This
+ # allows you to explicitly specify the command line order of linker scripts,
+ # and may be useful if your project defines more than one.
+ cc_binary(
+ name = "some_binary",
+ srcs = ["some_source.cc"],
+ additional_linker_inputs = [":some_linker_script"],
+ linkopts = ["-T $(location :some_linker_script)"],
+ )
+
+.. _module-pw_build-bazel-pw_cc_facade:
+
+pw_cc_facade
+------------
+In Bazel, a :ref:`facade <docs-module-structure-facades>` module has a few
+components:
+
+#. The **facade target**, i.e. the interface to the module. This is what
+ *backend implementations* depend on to know what interface they're supposed
+ to implement. The facade is declared by creating a ``pw_cc_facade`` target,
+ which is just a thin wrapper for ``cc_library``. For example,
+
+ .. code-block:: python
+
+ pw_cc_facade(
+ name = "binary_semaphore_facade",
+ # The header that constitues the facade.
+ hdrs = [
+ "public/pw_sync/binary_semaphore.h",
+ ],
+ includes = ["public"],
+ # Dependencies of this header.
+ deps = [
+ "//pw_chrono:system_clock",
+ "//pw_preprocessor",
+ ],
+ )
+
+ .. note::
+ As pure interfaces, ``pw_cc_facade`` targets should not include any source
+ files. Backend-independent source files should be placed in the "library
+ target" instead.
+
+#. The **library target**, i.e. both the facade (interface) and backend
+ (implementation). This is what *users of the module* depend on. It's a
+ regular ``pw_cc_library`` that exposes the same headers as the facade, but
+ has a dependency on the "backend label flag" (discussed next). It may also
+ include some source files (if these are backend-independent). For example,
+
+ .. code-block:: python
+
+ pw_cc_library(
+ name = "binary_semaphore",
+ # A backend-independent source file.
+ srcs = [
+ "binary_semaphore.cc",
+ ],
+ # The same header as exposed by the facade.
+ hdrs = [
+ "public/pw_sync/binary_semaphore.h",
+ ],
+ deps = [
+ # Dependencies of this header
+ "//pw_chrono:system_clock",
+ "//pw_preprocessor",
+ # The backend, hidden behind a label_flag.
+ "@pigweed//targets:pw_sync_binary_semaphore_backend",
+ ],
+ )
+
+ .. note::
+ You may be tempted to reduce duplication in the BUILD.bazel files and
+ simply add the facade target to the ``deps`` of the library target,
+ instead of re-declaring the facade's ``hdrs`` and ``deps``. *Do not do
+ this!* It's a layering check violation: the facade headers provide the
+ module's interface, and should be directly exposed by the target the users
+ depend on.
+
+#. The **backend label flag**. This is a `label_flag
+ <https://bazel.build/extending/config#label-typed-build-settings>`_: a
+ dependency edge in the build graph that can be overridden by downstream projects.
+ For facades defined in upstream Pigweed, the ``label_flags`` are collected in
+ ``//targets/BUILD.bazel``.
+
+#. The **backend target** implements a particular backend for a facade. It's
+ just a plain ``pw_cc_library``, with a dependency on the facade target. For example,
+
+ .. code-block:: python
+
+ pw_cc_library(
+ name = "binary_semaphore",
+ srcs = [
+ "binary_semaphore.cc",
+ ],
+ hdrs = [
+ "public/pw_sync_stl/binary_semaphore_inline.h",
+ "public/pw_sync_stl/binary_semaphore_native.h",
+ "public_overrides/pw_sync_backend/binary_semaphore_inline.h",
+ "public_overrides/pw_sync_backend/binary_semaphore_native.h",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ deps = [
+ # Dependencies of the backend's headers and sources.
+ "//pw_assert",
+ "//pw_chrono:system_clock",
+ # A dependency on the facade target, which defines the interface
+ # this backend target implements.
+ "//pw_sync:binary_semaphore_facade",
+ ],
+ )
+
+ If a project uses only one backend for a given facade, the backend label
+ flag should point at that backend target.
+
+#. The **facade constraint setting** and **backend constraint values**. Every
+ facade has an associated `constraint setting
+ <https://bazel.build/concepts/platforms#api-review>`_ (enum used in platform
+ definition), and each backend for this facade has an associated
+ ``constraint_value`` (enum value). Example:
+
+ .. code-block:: python
+
+ # //pw_sync/BUILD.bazel
+ constraint_setting(
+ name = "binary_semaphore_backend_constraint_setting",
+ )
+
+ # //pw_sync_stl/BUILD.bazel
+ constraint_value(
+ name = "binary_semaphore_backend",
+ constraint_setting = "//pw_sync:binary_semaphore_backend_constraint_setting",
+ )
+
+ # //pw_sync_freertos/BUILD.bazel
+ constraint_value(
+ name = "binary_semaphore_backend",
+ constraint_setting = "//pw_sync:binary_semaphore_backend_constraint_setting",
+ )
+
+ `Target platforms <https://bazel.build/extending/platforms>`_ for Pigweed
+ projects should indicate which backend they select for each facade by
+ listing the corresponding ``constraint_value`` in their definition. This can
+ be used in a couple of ways:
+
+ #. It allows projects to switch between multiple backends based only on the
+ `target platform <https://bazel.build/extending/platforms>`_ using a
+ *backend multiplexer* (see below) instead of setting label flags in
+ their ``.bazelrc``.
+
+ #. It allows tests or libraries that only support a particular backend to
+ express this through the `target_compatible_with
+ <https://bazel.build/reference/be/common-definitions#common.target_compatible_with>`_
+ attribute. Bazel will use this to `automatically skip incompatible
+ targets in wildcard builds
+ <https://bazel.build/extending/platforms#skipping-incompatible-targets>`_.
+
+#. The **backend multiplexer**. If a project uses more than one backend for a
+ given facade (e.g., it uses different backends for host and embedded target
+ builds), the backend label flag will point to a target that resolves to the
+ correct backend based on the target platform. This will typically be an
+ `alias <https://bazel.build/reference/be/general#alias>`_ with a ``select``
+ statement mapping constraint values to the appropriate backend targets. For
+ example,
+
+ .. code-block:: python
+
+ alias(
+ name = "pw_sync_binary_semaphore_backend_multiplexer",
+ actual = select({
+ "//pw_sync_stl:binary_semaphore_backend": "@pigweed//pw_sync_stl:binary_semaphore",
+ "//pw_sync_freertos:binary_semaphore_backend": "@pigweed//pw_sync_freertos:binary_semaphore_backend",
+ # If we're building for a host OS, use the STL backend.
+ "@platforms//os:macos": "@pigweed//pw_sync_stl:binary_semaphore",
+ "@platforms//os:linux": "@pigweed//pw_sync_stl:binary_semaphore",
+ "@platforms//os:windows": "@pigweed//pw_sync_stl:binary_semaphore",
+ # Unless the target platform is the host platform, it must
+ # explicitly specify which backend to use. The unspecified_backend
+ # is not compatible with any platform; taking this branch will produce
+ # an informative error.
+ "//conditions:default": "@pigweed//pw_build:unspecified_backend",
+ }),
+ )
+
+pw_cc_blob_library
+------------------
+The ``pw_cc_blob_library`` rule is useful for embedding binary data into a
+program. The rule takes in a mapping of symbol names to file paths, and
+generates a set of C++ source and header files that embed the contents of the
+passed-in files as arrays of ``std::byte``.
+
+The blob byte arrays are constant initialized and are safe to access at any
+time, including before ``main()``.
+
+``pw_cc_blob_library`` is also available in the :ref:`GN <module-pw_build-cc_blob_library>`
+and CMake builds.
+
+Arguments
+^^^^^^^^^
+* ``blobs``: A list of ``pw_cc_blob_info`` targets, where each target
+ corresponds to a binary blob to be transformed from file to byte array. This
+ is a required field. ``pw_cc_blob_info`` attributes include:
+
+ * ``symbol_name``: The C++ symbol for the byte array.
+ * ``file_path``: The file path for the binary blob.
+ * ``linker_section``: If present, places the byte array in the specified
+ linker section.
+ * ``alignas``: If present, uses the specified string verbatim in
+ the ``alignas()`` specifier for the byte array.
+
+* ``out_header``: The header file to generate. Users will include this file
+ exactly as it is written here to reference the byte arrays.
+* ``namespace``: C++ namespace to place the generated blobs within.
+
+Example
+^^^^^^^
+**BUILD.bazel**
+
+.. code-block::
+
+ pw_cc_blob_info(
+ name = "foo_blob",
+ file_path = "foo.bin",
+ symbol_name = "kFooBlob",
+ )
+
+ pw_cc_blob_info(
+ name = "bar_blob",
+ file_path = "bar.bin",
+ symbol_name = "kBarBlob",
+ linker_section = ".bar_section",
+ )
+
+ pw_cc_blob_library(
+ name = "foo_bar_blobs",
+ blobs = [
+ ":foo_blob",
+ ":bar_blob",
+ ],
+ out_header = "my/stuff/foo_bar_blobs.h",
+ namespace = "my::stuff",
+ )
+
+.. note:: If the binary blobs are generated as part of the build, be sure to
+ list them as deps to the pw_cc_blob_library target.
+
+**Generated Header**
+
+.. code-block::
+
+ #pragma once
+
+ #include <array>
+ #include <cstddef>
+
+ namespace my::stuff {
+
+ extern const std::array<std::byte, 100> kFooBlob;
+
+ extern const std::array<std::byte, 50> kBarBlob;
+
+ } // namespace my::stuff
+
+**Generated Source**
+
+.. code-block::
+
+ #include "my/stuff/foo_bar_blobs.h"
+
+ #include <array>
+ #include <cstddef>
+
+ #include "pw_preprocessor/compiler.h"
+
+ namespace my::stuff {
+
+ const std::array<std::byte, 100> kFooBlob = { ... };
+
+ PW_PLACE_IN_SECTION(".bar_section")
+ const std::array<std::byte, 50> kBarBlob = { ... };
+
+ } // namespace my::stuff
+
+Miscellaneous utilities
+-----------------------
+
+.. _module-pw_build-bazel-empty_cc_library:
+
+empty_cc_library
+^^^^^^^^^^^^^^^^
+This empty library is used as a placeholder for label flags that need to point
+to a library of some kind, but don't actually need the dependency to amount to
+anything.
+
+unspecified_backend
+^^^^^^^^^^^^^^^^^^^
+A special target used instead of a cc_library as the default condition in
+backend multiplexer select statements to signal that a facade is in an
+unconfigured state. This produces better error messages than e.g. using an
+invalid label.
+
+Toolchains and platforms
+------------------------
+Pigweed provides clang-based host toolchains for Linux and Mac Arm gcc
+toolchain. The clang-based Linux and Arm gcc toolchains are entirely hermetic.
+We don't currently provide a host toolchain for Windows.
+
diff --git a/pw_build/bazel_internal/BUILD.bazel b/pw_build/bazel_internal/BUILD.bazel
index 8607c8340..822ee7da9 100644
--- a/pw_build/bazel_internal/BUILD.bazel
+++ b/pw_build/bazel_internal/BUILD.bazel
@@ -12,7 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
-load(":pigweed_internal.bzl", "pw_linker_script")
+load("//pw_build:pigweed.bzl", "pw_linker_script")
pw_linker_script(
name = "linker_script_test",
@@ -24,7 +24,7 @@ pw_linker_script(
"PW_BOOT_RAM_BEGIN=0x20000000",
"PW_BOOT_RAM_SIZE=192K",
"PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ "PW_BOOT_VECTOR_TABLE_SIZE=1M",
],
linker_script = "linker_script.ld",
)
@@ -34,6 +34,47 @@ pw_linker_script(
cc_binary(
name = "test_linker_script",
srcs = ["test.cc"],
+ copts = ["-Wno-unused-variable"],
+ # Only compatible with platforms that support linker scripts.
+ target_compatible_with = select({
+ "@platforms//os:macos": ["@platforms//:incompatible"],
+ "@platforms//os:windows": ["@platforms//:incompatible"],
+ "//conditions:default": [],
+ }),
+ deps = [":linker_script_test"],
+)
+
+# Use cc_library to depend on the linker script, and then use cc_binary to build
+# the test, verifying that linker scripts can be included via transitive deps.
+cc_library(
+ name = "lib_linker_script",
+ deps = [":linker_script_test"],
+)
+
+cc_binary(
+ name = "test_transitive_linker_script",
+ srcs = ["test.cc"],
+ copts = ["-Wno-unused-variable"],
+ # Only compatible with platforms that support linker scripts.
+ target_compatible_with = select({
+ "@platforms//os:macos": ["@platforms//:incompatible"],
+ "@platforms//os:windows": ["@platforms//:incompatible"],
+ "//conditions:default": [],
+ }),
+ deps = [":lib_linker_script"],
+)
+
+# Verify that the linker script can also be specified directly.
+cc_binary(
+ name = "test_direct_linker_script",
+ srcs = ["test.cc"],
additional_linker_inputs = [":linker_script_test"],
+ copts = ["-Wno-unused-variable"],
linkopts = ["-T $(location :linker_script_test)"],
+ # Only compatible with platforms that support linker scripts.
+ target_compatible_with = select({
+ "@platforms//os:macos": ["@platforms//:incompatible"],
+ "@platforms//os:windows": ["@platforms//:incompatible"],
+ "//conditions:default": [],
+ }),
)
diff --git a/pw_build/bazel_internal/linker_script.ld b/pw_build/bazel_internal/linker_script.ld
index bd6609931..126c64718 100644
--- a/pw_build/bazel_internal/linker_script.ld
+++ b/pw_build/bazel_internal/linker_script.ld
@@ -31,7 +31,7 @@ ENTRY(pw_boot_Entry)
MEMORY
{
- /* TODO(b/234892223): Make it possible for projects to freely customize
+ /* TODO: b/234892223 - Make it possible for projects to freely customize
* memory regions.
*/
@@ -49,3 +49,7 @@ MEMORY
LENGTH = PW_BOOT_RAM_SIZE
}
+/* Define a symbol used in the test source to verify that this linker script is
+ * actually used
+ */
+_linker_defined_symbol = 0xDEADBEEF;
diff --git a/pw_build/bazel_internal/pigweed_internal.bzl b/pw_build/bazel_internal/pigweed_internal.bzl
index ed452ffe8..b8b4afb67 100644
--- a/pw_build/bazel_internal/pigweed_internal.bzl
+++ b/pw_build/bazel_internal/pigweed_internal.bzl
@@ -15,130 +15,92 @@
# the License.
""" An internal set of tools for creating embedded CC targets. """
-load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain", "use_cpp_toolchain")
-load("@rules_cc//cc:action_names.bzl", "C_COMPILE_ACTION_NAME")
-
-DEBUGGING = [
- "-g",
-]
-
-# Standard compiler flags to reduce output binary size.
-REDUCED_SIZE_COPTS = [
- "-fno-common",
- "-fno-exceptions",
- "-ffunction-sections",
- "-fdata-sections",
-]
-
-STRICT_WARNINGS_COPTS = [
- "-Wall",
- "-Wextra",
- # Make all warnings errors, except for the exemptions below.
- "-Werror",
- "-Wno-error=cpp", # preprocessor #warning statement
- "-Wno-error=deprecated-declarations", # [[deprecated]] attribute
-]
-
-DISABLE_PENDING_WORKAROUND_COPTS = [
- "-Wno-private-header",
-]
-
-PW_DEFAULT_COPTS = (
- DEBUGGING +
- REDUCED_SIZE_COPTS +
- STRICT_WARNINGS_COPTS +
- DISABLE_PENDING_WORKAROUND_COPTS
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
+
+def _print_platform_impl(_, ctx):
+ if hasattr(ctx.rule.attr, "constraint_values"):
+ for cv in ctx.rule.attr.constraint_values:
+ # buildifier: disable=print
+ print(str(ctx.rule.attr.name) + " specifies " + str(cv))
+ return []
+
+print_platform = aspect(
+ implementation = _print_platform_impl,
+ attr_aspects = ["parents"],
+ doc = """
+ This is a little debug utility that traverses the platform inheritance
+ hierarchy and prints all the constraint values.
+
+ Example usage:
+
+ bazel build \
+ //pw_build/platforms:lm3s6965evb \
+ --aspects \
+ pw_build/bazel_internal/pigweed_internal.bzl%print_platform
+ """,
)
-KYTHE_COPTS = [
- "-Wno-unknown-warning-option",
-]
-
-def add_defaults(kwargs):
- """Adds default arguments suitable for both C and C++ code to kwargs.
+def compile_cc(
+ ctx,
+ srcs,
+ hdrs,
+ deps,
+ includes = [],
+ defines = [],
+ user_compile_flags = []):
+ """Compiles a list C++ source files.
Args:
- kwargs: cc_* arguments to be modified.
+ ctx: Rule context
+ srcs: List of C/C++ source files to compile
+ hdrs: List of C/C++ header files to compile with
+ deps: Dependencies to link with
+ includes: List of include paths
+ defines: List of preprocessor defines to use
+ user_compile_flags: Extra compiler flags to pass when compiling.
+
+ Returns:
+ A CcInfo provider.
"""
-
- copts = PW_DEFAULT_COPTS + kwargs.get("copts", [])
- kwargs["copts"] = select({
- "@pigweed//pw_build:kythe": copts + KYTHE_COPTS,
- "//conditions:default": copts,
- })
-
- # Set linkstatic to avoid building .so files.
- kwargs["linkstatic"] = True
-
- kwargs.setdefault("features", [])
-
- # Crosstool--adding this line to features disables header modules, which
- # don't work with -fno-rtti. Note: this is not a command-line argument,
- # it's "minus use_header_modules".
- kwargs["features"].append("-use_header_modules")
-
-def _preprocess_linker_script_impl(ctx):
cc_toolchain = find_cpp_toolchain(ctx)
- output_script = ctx.actions.declare_file(ctx.label.name + ".ld")
feature_configuration = cc_common.configure_features(
ctx = ctx,
cc_toolchain = cc_toolchain,
requested_features = ctx.features,
unsupported_features = ctx.disabled_features,
)
- cxx_compiler_path = cc_common.get_tool_for_action(
- feature_configuration = feature_configuration,
- action_name = C_COMPILE_ACTION_NAME,
- )
- c_compile_variables = cc_common.create_compile_variables(
+
+ compilation_contexts = [dep[CcInfo].compilation_context for dep in deps]
+ compilation_context, compilation_outputs = cc_common.compile(
+ name = ctx.label.name,
+ actions = ctx.actions,
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
- user_compile_flags = ctx.fragments.cpp.copts + ctx.fragments.cpp.conlyopts,
- )
- action_flags = cc_common.get_memory_inefficient_command_line(
- feature_configuration = feature_configuration,
- action_name = C_COMPILE_ACTION_NAME,
- variables = c_compile_variables,
+ srcs = srcs,
+ includes = includes,
+ defines = defines,
+ public_hdrs = hdrs,
+ user_compile_flags = user_compile_flags,
+ compilation_contexts = compilation_contexts,
)
- env = cc_common.get_environment_variables(
+
+ linking_contexts = [dep[CcInfo].linking_context for dep in deps]
+ linking_context, _ = cc_common.create_linking_context_from_compilation_outputs(
+ actions = ctx.actions,
feature_configuration = feature_configuration,
- action_name = C_COMPILE_ACTION_NAME,
- variables = c_compile_variables,
- )
- ctx.actions.run(
- outputs = [output_script],
- inputs = depset(
- [ctx.file.linker_script],
- transitive = [cc_toolchain.all_files],
- ),
- executable = cxx_compiler_path,
- arguments = [
- "-E",
- "-P",
- "-xc",
- ctx.file.linker_script.short_path,
- "-o",
- output_script.path,
- ] + [
- "-D" + d
- for d in ctx.attr.defines
- ] + action_flags + ctx.attr.copts,
- env = env,
+ cc_toolchain = cc_toolchain,
+ compilation_outputs = compilation_outputs,
+ linking_contexts = linking_contexts,
+ disallow_dynamic_library = True,
+ name = ctx.label.name,
)
- return [DefaultInfo(files = depset([output_script]))]
-pw_linker_script = rule(
- _preprocess_linker_script_impl,
- attrs = {
- "copts": attr.string_list(doc = "C compile options."),
- "defines": attr.string_list(doc = "C preprocessor defines."),
- "linker_script": attr.label(
- mandatory = True,
- allow_single_file = True,
- doc = "Linker script to preprocess.",
- ),
- "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
- },
- toolchains = use_cpp_toolchain(),
- fragments = ["cpp"],
-)
+ transitive_output_files = [dep[DefaultInfo].files for dep in deps]
+ output_files = depset(
+ compilation_outputs.pic_objects + compilation_outputs.objects,
+ transitive = transitive_output_files,
+ )
+ return [DefaultInfo(files = output_files), CcInfo(
+ compilation_context = compilation_context,
+ linking_context = linking_context,
+ )]
diff --git a/pw_build/bazel_internal/test.cc b/pw_build/bazel_internal/test.cc
index 6498ae9a9..fb7f63932 100644
--- a/pw_build/bazel_internal/test.cc
+++ b/pw_build/bazel_internal/test.cc
@@ -12,6 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
+// This symbol is defined by the linker script. If the linker script is not
+// correctly used, this will be missing.
+extern int _linker_defined_symbol;
+
// This file is intentionally very simple and is used only to test that the
// linker script generator works as expected.
-int main() { return 0; } \ No newline at end of file
+int main() {
+ volatile int linker_defined_pointer = _linker_defined_symbol;
+ return 0;
+}
diff --git a/pw_build/cc_library.gni b/pw_build/cc_library.gni
index 02fe625a9..936e2447d 100644
--- a/pw_build/cc_library.gni
+++ b/pw_build/cc_library.gni
@@ -40,7 +40,9 @@ import("$dir_pw_build/gn_internal/build_target.gni")
template("pw_source_set") {
pw_internal_build_target(target_name) {
forward_variables_from(invoker, "*")
- add_global_link_deps = false
+ if (!defined(add_global_link_deps)) {
+ add_global_link_deps = false
+ }
underlying_target_type = "source_set"
}
}
@@ -48,7 +50,9 @@ template("pw_source_set") {
template("pw_static_library") {
pw_internal_build_target(target_name) {
forward_variables_from(invoker, "*")
- add_global_link_deps = true
+ if (!defined(add_global_link_deps)) {
+ add_global_link_deps = false
+ }
underlying_target_type = "static_library"
}
}
@@ -56,7 +60,9 @@ template("pw_static_library") {
template("pw_shared_library") {
pw_internal_build_target(target_name) {
forward_variables_from(invoker, "*")
- add_global_link_deps = true
+ if (!defined(add_global_link_deps)) {
+ add_global_link_deps = false
+ }
underlying_target_type = "shared_library"
}
}
diff --git a/pw_build/cmake.rst b/pw_build/cmake.rst
new file mode 100644
index 000000000..11f3b7514
--- /dev/null
+++ b/pw_build/cmake.rst
@@ -0,0 +1,184 @@
+CMake
+=====
+Pigweed's `CMake`_ support is provided primarily for projects that have an
+existing CMake build and wish to integrate Pigweed without switching to a new
+build system.
+
+The following command generates Ninja build files for a host build in the
+``out/cmake_host`` directory:
+
+.. code-block:: sh
+
+ cmake -B out/cmake_host -S "$PW_ROOT" -G Ninja -DCMAKE_TOOLCHAIN_FILE=$PW_ROOT/pw_toolchain/host_clang/toolchain.cmake
+
+The ``PW_ROOT`` environment variable must point to the root of the Pigweed
+directory. This variable is set by Pigweed's environment setup.
+
+Tests can be executed with the ``pw_run_tests.GROUP`` targets. To run Pigweed
+module tests, execute ``pw_run_tests.modules``:
+
+.. code-block:: sh
+
+ ninja -C out/cmake_host pw_run_tests.modules
+
+:ref:`module-pw_watch` supports CMake, so you can also run
+
+.. code-block:: sh
+
+ pw watch -C out/cmake_host pw_run_tests.modules
+
+CMake functions
+---------------
+CMake convenience functions are defined in ``pw_build/pigweed.cmake``.
+
+* ``pw_add_library_generic`` -- The base helper used to instantiate CMake
+ libraries. This is meant for use in downstream projects as upstream Pigweed
+ modules are expected to use ``pw_add_library``.
+* ``pw_add_library`` -- Add an upstream Pigweed library.
+* ``pw_add_facade_generic`` -- The base helper used to instantiate facade
+ libraries. This is meant for use in downstream projects as upstream Pigweed
+ modules are expected to use ``pw_add_facade``.
+* ``pw_add_facade`` -- Declare an upstream Pigweed facade.
+* ``pw_set_backend`` -- Set the backend library to use for a facade.
+* ``pw_add_test_generic`` -- The base helper used to instantiate test targets.
+ This is meant for use in downstrema projects as upstream Pigweed modules are
+ expected to use ``pw_add_test``.
+* ``pw_add_test`` -- Declare an upstream Pigweed test target.
+* ``pw_add_test_group`` -- Declare a target to group and bundle test targets.
+* ``pw_target_link_targets`` -- Helper wrapper around ``target_link_libraries``
+ which only supports CMake targets and detects when the target does not exist.
+ Note that generator expressions are not supported.
+* ``pw_add_global_compile_options`` -- Applies compilation options to all
+ targets in the build. This should only be used to add essential compilation
+ options, such as those that affect the ABI. Use ``pw_add_library`` or
+ ``target_compile_options`` to apply other compile options.
+* ``pw_add_error_target`` -- Declares target which reports a message and causes
+ a build failure only when compiled. This is useful when ``FATAL_ERROR``
+ messages cannot be used to catch problems during the CMake configuration
+ phase.
+* ``pw_parse_arguments`` -- Helper to parse CMake function arguments.
+
+See ``pw_build/pigweed.cmake`` for the complete documentation of these
+functions.
+
+Special libraries that do not fit well with these functions are created with the
+standard CMake functions, such as ``add_library`` and ``target_link_libraries``.
+
+Facades and backends
+--------------------
+The CMake build uses CMake cache variables for configuring
+:ref:`facades<docs-module-structure-facades>` and backends. Cache variables are
+similar to GN's build args set with ``gn args``. Unlike GN, CMake does not
+support multi-toolchain builds, so these variables have a single global value
+per build directory.
+
+The ``pw_add_module_facade`` function declares a cache variable named
+``<module_name>_BACKEND`` for each facade. Cache variables can be awkward to
+work with, since their values only change when they're assigned, but then
+persist accross CMake invocations. These variables should be set in one of the
+following ways:
+
+* Prior to setting a backend, your application should include
+ ``$ENV{PW_ROOT}/backends.cmake``. This file will setup all the backend targets
+ such that any misspelling of a facade or backend will yield a warning.
+
+ .. note::
+ Zephyr developers do not need to do this, backends can be set automatically
+ by enabling the appropriate Kconfig options.
+
+* Call ``pw_set_backend`` to set backends appropriate for the target in the
+ target's toolchain file. The toolchain file is provided to ``cmake`` with
+ ``-DCMAKE_TOOLCHAIN_FILE=<toolchain file>``.
+* Call ``pw_set_backend`` in the top-level ``CMakeLists.txt`` before other
+ CMake code executes.
+* Set the backend variable at the command line with the ``-D`` option.
+
+ .. code-block:: sh
+
+ cmake -B out/cmake_host -S "$PW_ROOT" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=$PW_ROOT/pw_toolchain/host_clang/toolchain.cmake \
+ -Dpw_log_BACKEND=pw_log_basic
+
+* Temporarily override a backend by setting it interactively with ``ccmake`` or
+ ``cmake-gui``.
+
+If the backend is set to a build target that does not exist, there will be an
+error message like the following:
+
+.. code-block::
+
+ CMake Error at pw_build/pigweed.cmake:257 (message):
+ my_module.my_facade's INTERFACE dep "my_nonexistent_backend" is not
+ a target.
+ Call Stack (most recent call first):
+ pw_build/pigweed.cmake:238:EVAL:1 (_pw_target_link_targets_deferred_check)
+ CMakeLists.txt:DEFERRED
+
+
+Toolchain setup
+---------------
+In CMake, the toolchain is configured by setting CMake variables, as described
+in the `CMake documentation <https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html>`_.
+These variables are typically set in a toolchain CMake file passed to ``cmake``
+with the ``-D`` option (``-DCMAKE_TOOLCHAIN_FILE=path/to/file.cmake``).
+For Pigweed embedded builds, set ``CMAKE_SYSTEM_NAME`` to the empty string
+(``""``).
+
+Toolchains may set the ``pw_build_WARNINGS`` variable to a list of ``INTERFACE``
+libraries with compilation options for Pigweed's upstream libraries. This
+defaults to a strict set of warnings. Projects may need to use less strict
+compilation warnings to compile backends exposed to Pigweed code (such as
+``pw_log``) that cannot compile with Pigweed's flags. If desired, Projects can
+access these warnings by depending on ``pw_build.warnings``.
+
+Third party libraries
+---------------------
+The CMake build includes third-party libraries similarly to the GN build. A
+``dir_pw_third_party_<library>`` cache variable is defined for each third-party
+dependency. The variable must be set to the absolute path of the library in
+order to use it. If the variable is empty
+(``if("${dir_pw_third_party_<library>}" STREQUAL "")``), the dependency is not
+available.
+
+Third-party dependencies are not automatically added to the build. They can be
+manually added with ``add_subdirectory`` or by setting the
+``pw_third_party_<library>_ADD_SUBDIRECTORY`` option to ``ON``.
+
+Third party variables are set like any other cache global variable in CMake. It
+is recommended to set these in one of the following ways:
+
+* Set with the CMake ``set`` function in the toolchain file or a
+ ``CMakeLists.txt`` before other CMake code executes.
+
+ .. code-block:: cmake
+
+ set(dir_pw_third_party_nanopb ${CMAKE_CURRENT_SOURCE_DIR}/external/nanopb CACHE PATH "" FORCE)
+
+* Set the variable at the command line with the ``-D`` option.
+
+ .. code-block:: sh
+
+ cmake -B out/cmake_host -S "$PW_ROOT" -G Ninja \
+ -DCMAKE_TOOLCHAIN_FILE=$PW_ROOT/pw_toolchain/host_clang/toolchain.cmake \
+ -Ddir_pw_third_party_nanopb=/path/to/nanopb
+
+* Set the variable interactively with ``ccmake`` or ``cmake-gui``.
+
+Use Pigweed from an existing CMake project
+------------------------------------------
+To use Pigweed libraries form a CMake-based project, simply include the Pigweed
+repository from a ``CMakeLists.txt``.
+
+.. code-block:: cmake
+
+ add_subdirectory(path/to/pigweed pigweed)
+
+All module libraries will be available as ``module_name`` or
+``module_name.sublibrary``.
+
+If desired, modules can be included individually.
+
+.. code-block:: cmake
+
+ add_subdirectory(path/to/pigweed/pw_some_module pw_some_module)
+ add_subdirectory(path/to/pigweed/pw_another_module pw_another_module)
diff --git a/pw_build/constraints/chipset/BUILD.bazel b/pw_build/constraints/chipset/BUILD.bazel
index e292906e3..9f34c5c56 100644
--- a/pw_build/constraints/chipset/BUILD.bazel
+++ b/pw_build/constraints/chipset/BUILD.bazel
@@ -33,3 +33,8 @@ constraint_value(
name = "nrf52833",
constraint_setting = ":chipset",
)
+
+constraint_value(
+ name = "rp2040",
+ constraint_setting = ":chipset",
+)
diff --git a/pw_build/constraints/rtos/BUILD.bazel b/pw_build/constraints/rtos/BUILD.bazel
index 3a4c30e5d..4a6e39dff 100644
--- a/pw_build/constraints/rtos/BUILD.bazel
+++ b/pw_build/constraints/rtos/BUILD.bazel
@@ -29,7 +29,7 @@ constraint_value(
config_setting(
name = "none_setting",
flag_values = {
- "@pigweed_config//:target_rtos": ":none",
+ "@pigweed//targets:target_rtos": ":none",
},
)
@@ -41,7 +41,7 @@ constraint_value(
config_setting(
name = "embos_setting",
flag_values = {
- "@pigweed_config//:target_rtos": ":embos",
+ "@pigweed//targets:target_rtos": ":embos",
},
)
@@ -53,7 +53,7 @@ constraint_value(
config_setting(
name = "freertos_setting",
flag_values = {
- "@pigweed_config//:target_rtos": ":freertos",
+ "@pigweed//targets:target_rtos": ":freertos",
},
)
@@ -65,6 +65,6 @@ constraint_value(
config_setting(
name = "threadx_setting",
flag_values = {
- "@pigweed_config//:target_rtos": ":threadx",
+ "@pigweed//targets:target_rtos": ":threadx",
},
)
diff --git a/pw_build/copy_from_cipd.gni b/pw_build/copy_from_cipd.gni
index a961da10d..2f70c73d2 100644
--- a/pw_build/copy_from_cipd.gni
+++ b/pw_build/copy_from_cipd.gni
@@ -71,7 +71,7 @@ template("pw_cipd_static_library") {
# might work but is redundant, so serialize calls.
pool = "$dir_pw_build/pool:copy_from_cipd($default_toolchain)"
- # TODO(b/234884827): This should somehow track the actual .a for changes as
+ # TODO: b/234884827 - This should somehow track the actual .a for changes as
# well.
inputs = [ invoker.manifest ]
outputs = [ _out_file ]
diff --git a/pw_build/coverage_report.gni b/pw_build/coverage_report.gni
new file mode 100644
index 000000000..cb4aa5202
--- /dev/null
+++ b/pw_build/coverage_report.gni
@@ -0,0 +1,270 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+import("//build_overrides/pigweed_environment.gni")
+
+import("$dir_pw_build/python_action.gni")
+import("$dir_pw_toolchain/host_clang/toolchains.gni")
+
+# Expands to code coverage targets that can be used as dependencies to generate
+# coverage reports at build time.
+#
+# Arguments:
+# - enable_if (optional): Conditionally activates coverage report generation
+# when set to a boolean expression that evaluates to true.
+# - failure_mode (optional/unstable): Specify the failure mode for llvm-profdata
+# (used to merge inidividual profraw files from pw_test runs). Available
+# options are "any" (default) or "all". This should be considered an
+# unstable/deprecated argument that should only be used as a last resort to
+# get a build working again. Using failure_mode = "all" usually indicates that
+# there are underlying problems in the build or test infrastructure that
+# should be independently resolved. Please reach out to the Pigweed team for
+# assistance.
+# - Coverage Settings
+# - filter_paths (optional): List of file paths (using GN path helpers like
+# `//` is supported). These will be translated into absolute paths before
+# being used. These filter source files so that the coverage report *ONLY*
+# includes files that match one of these paths. These cannot be regular
+# expressions, but can be concrete file or folder paths. Folder paths will
+# allow all files in that directory or any recursive child directory.
+# - ignore_filename_patterns (optional): List of file path regular expressions
+# to ignore when generating the coverage report.
+# - pw_test Depedencies (required): These control which test binaries are used
+# to collect usage data for the coverage report. The following can basically
+# be used interchangeably with no actual difference in the template expansion.
+# Only one of these is required to be provided.
+# - tests: A list of pw_test targets.
+# - group_deps: A list of pw_test_group targets.
+#
+# Expands To:
+# pw_coverage_report follows the overall Pigweed pattern where targets exist
+# for all build configurations, but are only configured to do meaningful work
+# under the correct build configuration. In this vein, pw_coverage_report
+# ensures that a coverage-enabled toolchain is being used and the provided
+# enable_if evaluates to true (if provided).
+#
+# - If a coverage-enabled toolchain is being used and the provided enable_if
+# evaluates to true (if provided):
+# - <target_name>.text: Generates a text representation of the coverage
+# report. This is the output of
+# `llvm-cov show --format text`.
+# - <target_name>.html: Generates an HTML representation of the coverage
+# report. This is the output of
+# `llvm-cov show --format html`.
+# - <target_name>.lcov: Generates an LCOV representation of the coverage
+# report. This is the output of
+# `llvm-cov export --format lcov`.
+# - <target_name>.json: Generates a JSON representation of the coverage
+# report. This is the output of
+# `llvm-cov export --format text`.
+#
+# - <target_name>: A group that takes dependencies on <target_name>.text,
+# <target_name>.html, <target_name>.lcov, and
+# <target_name>.json. This can be used to force generation of
+# all coverage artifacts without manually depending on each
+# target.
+#
+# - The other targets this expands to should be considered private and not
+# used as dependencies.
+# - If a coverage-enabled toolchain is not being used or the provided enable_if
+# evaluates to false (if provided).
+# - All of the above target names, but they are empty groups.
+template("pw_coverage_report") {
+ assert(defined(invoker.tests) || defined(invoker.group_deps),
+ "One of `tests` or `group_deps` must be provided.")
+ assert(!defined(invoker.failure_mode) ||
+ (invoker.failure_mode == "any" || invoker.failure_mode == "all"),
+ "failure_mode only supports \"any\" or \"all\".")
+
+ _report_name = target_name
+ _format_types = [
+ "text",
+ "html",
+ "lcov",
+ "json",
+ ]
+ _should_enable = !defined(invoker.enable_if) || invoker.enable_if
+
+ # These two Pigweed build arguments are required to be in these states to
+ # ensure binaries are instrumented for coverage and profraw files are
+ # exported.
+ if (_should_enable && pw_toolchain_COVERAGE_ENABLED) {
+ _test_metadata = "$target_out_dir/$_report_name.test_metadata.json"
+ _profdata_file = "$target_out_dir/merged.profdata"
+ _arguments = {
+ filter_paths = []
+ if (defined(invoker.filter_paths)) {
+ filter_paths += invoker.filter_paths
+ }
+
+ ignore_filename_patterns = []
+ if (defined(invoker.ignore_filename_patterns)) {
+ ignore_filename_patterns += invoker.ignore_filename_patterns
+ }
+
+ # Merge any provided `tests` or `group_deps` to `deps` and `run_deps`.
+ #
+ # `deps` are used to generate the .test_metadata.json file.
+ # `run_deps` are used to block on the test execution to generate a profraw
+ # file.
+ deps = []
+ run_deps = []
+ test_or_group_deps = []
+ if (defined(invoker.tests)) {
+ test_or_group_deps += invoker.tests
+ }
+ if (defined(invoker.group_deps)) {
+ test_or_group_deps += invoker.group_deps
+ }
+ foreach(dep, test_or_group_deps) {
+ deps += [ dep ]
+
+ dep_target = get_label_info(dep, "label_no_toolchain")
+ dep_toolchain = get_label_info(dep, "toolchain")
+ run_deps += [ "$dep_target.run($dep_toolchain)" ]
+ }
+ }
+
+ # Generate a list of all test binaries and their associated profraw files
+ # after executing we can use to generate the coverage report.
+ generated_file("_$_report_name.test_metadata") {
+ outputs = [ _test_metadata ]
+ data_keys = [
+ "unit_tests",
+ "profraws",
+ ]
+ output_conversion = "json"
+ deps = _arguments.deps
+ }
+
+ # Merge the generated profraws from instrumented binaries into a single
+ # profdata.
+ pw_python_action("_$_report_name.merge_profraws") {
+ _depfile_path = "$target_out_dir/$_report_name.merged_profraws.d"
+
+ module = "pw_build.merge_profraws"
+ args = [
+ "--llvm-profdata-path",
+ pw_toolchain_clang_tools.llvm_profdata,
+ "--test-metadata-path",
+ rebase_path(_test_metadata, root_build_dir),
+ "--profdata-path",
+ rebase_path(_profdata_file, root_build_dir),
+ "--depfile-path",
+ rebase_path(_depfile_path, root_build_dir),
+ ]
+
+ # TODO: b/256651964 - We really want `--failure-mode any` always to guarantee
+ # we don't silently ignore any profraw report. However, there are downstream
+ # projects that currently break when using `--failure-mode any`.
+ #
+ # See the task for examples of what is currently going wrong.
+ #
+ # Invalid profraw files will be ignored so coverage reports might have a
+ # slight variance between runs depending on if something failed or not.
+ if (defined(invoker.failure_mode)) {
+ args += [
+ "--failure-mode",
+ invoker.failure_mode,
+ ]
+ }
+
+ inputs = [ _test_metadata ]
+ sources = []
+ depfile = _depfile_path
+
+ outputs = [ _profdata_file ]
+
+ python_deps = [ "$dir_pw_build/py" ]
+ deps = _arguments.run_deps
+ public_deps = [ ":_$_report_name.test_metadata" ]
+ }
+
+ foreach(format, _format_types) {
+ pw_python_action("$_report_name.$format") {
+ _depfile_path = "$target_out_dir/$_report_name.$format.d"
+ _output_dir = "$target_out_dir/$_report_name/$format/"
+
+ module = "pw_build.generate_report"
+ args = [
+ "--llvm-cov-path",
+ pw_toolchain_clang_tools.llvm_cov,
+ "--format",
+ format,
+ "--test-metadata-path",
+ rebase_path(_test_metadata, root_build_dir),
+ "--profdata-path",
+ rebase_path(_profdata_file, root_build_dir),
+ "--root-dir",
+ rebase_path("//", root_build_dir),
+ "--build-dir",
+ ".",
+ "--output-dir",
+ rebase_path(_output_dir, root_build_dir),
+ "--depfile-path",
+ rebase_path(_depfile_path, root_build_dir),
+ ]
+ foreach(filter_path, _arguments.filter_paths) {
+ args += [
+ # We rebase to absolute paths here to resolve any "//" used in the
+ # filter_paths.
+ "--filter-path",
+ rebase_path(filter_path),
+ ]
+ }
+ foreach(ignore_filename_pattern, _arguments.ignore_filename_patterns) {
+ args += [
+ "--ignore-filename-pattern",
+ ignore_filename_pattern,
+ ]
+ }
+
+ inputs = [
+ _test_metadata,
+ _profdata_file,
+ ]
+ sources = []
+ depfile = _depfile_path
+
+ outputs = []
+ if (format == "text") {
+ outputs += [ "$_output_dir/index.txt" ]
+ } else if (format == "html") {
+ outputs += [ "$_output_dir/index.html" ]
+ } else if (format == "lcov") {
+ outputs += [ "$_output_dir/report.lcov" ]
+ } else if (format == "json") {
+ outputs += [ "$_output_dir/report.json" ]
+ }
+
+ python_deps = [ "$dir_pw_build/py" ]
+ deps = [ ":_$_report_name.merge_profraws" ]
+ }
+ }
+ } else {
+ not_needed(invoker, "*")
+ foreach(format, _format_types) {
+ group("$_report_name.$format") {
+ }
+ }
+ }
+
+ group("$_report_name") {
+ deps = []
+ foreach(format, _format_types) {
+ deps += [ ":$_report_name.$format" ]
+ }
+ }
+}
diff --git a/pw_build/defaults.gni b/pw_build/defaults.gni
index 50f52db51..f1eb0aa71 100644
--- a/pw_build/defaults.gni
+++ b/pw_build/defaults.gni
@@ -14,18 +14,38 @@
import("//build_overrides/pigweed.gni")
-declare_args() {
- # Default configs and dependencies targets provided by the toolchain. These
- # are applied to all of the pw_* target types. They are set from a toolchain's
- # toolchain_args for cross-toolchain deps, e.g. for
- #
- # `deps = [ //pw_some_module(//pw_toolchain:not_default) ]`
- #
- # The default toolchain is never used.
- default_configs = []
- default_public_deps = []
- remove_default_configs = []
+# Default configs and dependencies provided by the toolchain. These
+# are applied to all of the pw_* target types. These are set from a toolchain's
+# toolchain_args scope with the following argument names:
+#
+# default_configs
+# default_public_deps
+# remove_default_configs
+# remove_default_public_deps
+#
+# Because of how GN handles default vs non-default toolchains, these only apply
+# for for non-default toolchains. A non-default toolchain invocation occurs
+# when a toolchain is specified in parenthesis after the name of a build target
+# in a dependency list. For example:
+#
+# group("foo_group") {
+# deps = [ //pw_some_module(//pw_toolchain:not_default) ]
+# }
+#
+# The default toolchain is never used by Pigweed to build C/C++ since
+# toolchain_args of that toolchain are ignored by GN.
+#
+# DO NOT RELY ON THIS: Use pw_build_defaults instead!
+# This is exposed as public because it is needed by both defaults.gni and
+# build_target.gni, and it helps surface the design of the GN build
+# architecture.
+pw_build_INTERNAL_DEFAULTS = {
+ # This is a little odd, but it's done for backwards compatibility. See the
+ # referenced .gni file if you want more information.
+ import("$dir_pw_build/gn_internal/defaults.gni")
+}
+declare_args() {
# Controls the default visibility of C/C++ libraries and executables
# (pw_source_set, pw_static_library, pw_shared_library pw_executable). This
# can be "*" or a list of paths.
@@ -35,36 +55,41 @@ declare_args() {
# at least include the Pigweed repository ("$dir_pigweed/*").
#
# Explicitly setting a target's visibility overrides this default.
- pw_build_DEFAULT_VISIBILITY = "*"
+ pw_build_DEFAULT_VISIBILITY = [ "*" ]
}
-# Combine target-specifc and target-agnostic default variables.
-_pw_build_defaults = {
- configs = default_configs
- public_deps = default_public_deps
-
- # The target-agnostic defaults.
- configs += [
- "$dir_pw_build:colorize_output",
- "$dir_pw_build:debugging",
- "$dir_pw_build:reduced_size",
- "$dir_pw_build:strict_warnings",
- "$dir_pw_build:toolchain_cpp_standard",
- "$dir_pw_build:relative_paths",
- ]
-
- if (pw_build_DEFAULT_VISIBILITY != "*") {
- visibility = pw_build_DEFAULT_VISIBILITY
- }
-}
+# These are the default configs automatically applied to every pw_* C/C++ build
+# step regardless of toolchain. If you want to omit one of these from your
+# toolchain, add the undesired config to pw_build_REMOVE_DEFAULT_CONFIGS.
+#
+# This is not the default value of pw_build_DEFAULT_CONFIGS because it would
+# be too easy to accidentally clobber.
+pigweed_default_configs = [
+ "$dir_pw_build:colorize_output",
+ "$dir_pw_build:debugging",
+ "$dir_pw_build:reduced_size",
+ "$dir_pw_build:strict_warnings",
+ "$dir_pw_build:toolchain_cpp_standard",
+ "$dir_pw_build:relative_paths",
+]
-# One more pass, to remove configs
+# Some Projects rely on this, so it is being retained for backwards
+# compatibilty. Using these is not recommended; prefer to use the pw_* target
+# types directly.
pw_build_defaults = {
- configs = []
- forward_variables_from(_pw_build_defaults, "*")
- if (remove_default_configs != []) {
+ configs = pw_build_INTERNAL_DEFAULTS.default_configs
+ public_deps = pw_build_INTERNAL_DEFAULTS.default_public_deps
+ if (pw_build_INTERNAL_DEFAULTS.remove_default_configs != []) {
+ # Add them first to ensure they are present to be removed.
+ configs += pw_build_INTERNAL_DEFAULTS.remove_default_configs
+ configs -= pw_build_INTERNAL_DEFAULTS.remove_default_configs
+ }
+ if (pw_build_INTERNAL_DEFAULTS.remove_default_public_deps != []) {
# Add them first to ensure they are present to be removed.
- configs += remove_default_configs
- configs -= remove_default_configs
+ public_deps += pw_build_INTERNAL_DEFAULTS.remove_default_public_deps
+ public_deps -= pw_build_INTERNAL_DEFAULTS.remove_default_public_deps
+ }
+ if (pw_build_DEFAULT_VISIBILITY != [ "*" ]) {
+ visibility = pw_build_DEFAULT_VISIBILITY
}
}
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index a1df9d929..16d2ad8c9 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -24,1190 +24,9 @@ Beyond just compiling code, Pigweed’s GN build system can also:
These are only supported in the GN build, so we recommend using it if possible.
-GN / Ninja
-==========
-The GN / Ninja build system is the primary build system used for upstream
-Pigweed development, and is the most tested and feature-rich build system
-Pigweed offers.
-
-This module's ``build.gn`` file contains a number of C/C++ ``config``
-declarations that are used by upstream Pigweed to set some architecture-agnostic
-compiler defaults. (See Pigweed's ``//BUILDCONFIG.gn``)
-
-``pw_build`` also provides several useful GN templates that are used throughout
-Pigweed.
-
-Build system philosophies
--------------------------
-While Pigweed's GN build is not hermetic, it strives to adhere to principles of
-`hermeticity <https://bazel.build/concepts/hermeticity>`_. Some guidelines to
-move towards the ideal of hermeticity include:
-
-* Only rely on pre-compiled tools provided by CIPD (or some other versioned,
- pre-compiled binary distribution mechanism). This eliminates build artifact
- differences caused by different tool versions or variations (e.g. same tool
- version built with slightly different compilation flags).
-* Do not use absolute paths in Ninja commands. Typically, these appear when
- using ``rebase_path("//path/to/my_script.py")``. Most of the time, Ninja
- steps should be passed paths rebased relative to the build directory (i.e.
- ``rebase_path("//path/to/my_script.py", root_build_dir)``). This ensures build
- commands are the same across different machines.
-* Prevent produced artifacts from relying on or referencing system state. This
- includes time stamps, writing absolute paths to generated artifacts, or
- producing artifacts that reference system state in a way that prevents them
- from working the same way on a different machine.
-* Isolate build actions to the build directory. In general, the build system
- should not add or modify files outside of the build directory. This can cause
- confusion to users, and makes the concept of a clean build more ambiguous.
-
-Target types
-------------
-.. code-block::
-
- import("$dir_pw_build/target_types.gni")
-
- pw_source_set("my_library") {
- sources = [ "lib.cc" ]
- }
-
-Pigweed defines wrappers around the four basic GN binary types ``source_set``,
-``executable``, ``static_library``, and ``shared_library``. These templates
-do several things:
-
-#. **Add default configs/deps**
-
- Rather than binding the majority of compiler flags related to C++ standard,
- cross-compilation, warning/error policy, etc. directly to toolchain
- invocations, these flags are applied as configs to all ``pw_*`` C/C++ target
- types. The primary motivations for this are to allow some targets to modify
- the default set of flags when needed by specifying ``remove_configs``, and to
- reduce the complexity of building novel toolchains.
-
- Pigweed's global default configs are set in ``pw_build/default.gni``, and
- individual platform-specific toolchains extend the list by appending to the
- ``default_configs`` build argument.
-
- Default deps were added to support polyfill, which has since been
- deprecated. Default dependency functionality continues to exist for
- backwards compatibility.
-
-#. **Optionally add link-time binding**
-
- Some libraries like pw_assert and pw_log are borderline impossible to
- implement well without introducing circular dependencies. One solution for
- addressing this is to break apart the libraries into an interface with
- minimal dependencies, and an implementation with the bulk of the
- dependencies that would typically create dependency cycles. In order for the
- implementation to be linked in, it must be added to the dependency tree of
- linked artifacts (e.g. ``pw_executable``, ``pw_static_library``). Since
- there's no way for the libraries themselves to just happily pull in the
- implementation if someone depends on the interface, the implementation is
- instead late-bound by adding it as a direct dependency of the final linked
- artifact. This is all managed through ``pw_build_LINK_DEPS``, which is global
- for each toolchain and applied to every ``pw_executable``,
- ``pw_static_library``, and ``pw_shared_library``.
-
-#. **Apply a default visibility policy**
-
- Projects can globally control the default visibility of pw_* target types by
- specifying ``pw_build_DEFAULT_VISIBILITY``. This template applies that as the
- default visibility for any pw_* targets that do not explicitly specify
- a visibility.
-
-#. **Add source file names as metadata**
-
- All source file names are collected as
- `GN metadata <https://gn.googlesource.com/gn/+/main/docs/reference.md#metadata_collection>`_.
- This list can be writen to a file at build time using ``generated_file``. The
- primary use case for this is to generate a token database containing all the
- source files. This allows PW_ASSERT to emit filename tokens even though it
- can't add them to the elf file because of the reasons described at
- :ref:`module-pw_assert-assert-api`.
-
- .. note::
- ``pw_source_files``, if not rebased will default to outputing module
- relative paths from a ``generated_file`` target. This is likely not
- useful. Adding a ``rebase`` argument to ``generated_file`` such as
- ``rebase = root_build_dir`` will result in usable paths. For an example,
- see ``//pw_tokenizer/database.gni``'s ``pw_tokenizer_filename_database``
- template.
-
-The ``pw_executable`` template provides additional functionality around building
-complete binaries. As Pigweed is a collection of libraries, it does not know how
-its final targets are built. ``pw_executable`` solves this by letting each user
-of Pigweed specify a global executable template for their target, and have
-Pigweed build against it. This is controlled by the build variable
-``pw_executable_config.target_type``, specifying the name of the executable
-template for a project.
-
-In some uncommon cases, a project's ``pw_executable`` template definition may
-need to stamp out some ``pw_source_set``\s. Since a pw_executable template can't
-import ``$dir_pw_build/target_types.gni`` due to circular imports, it should
-import ``$dir_pw_build/cc_library.gni`` instead.
-
-.. tip::
-
- Prefer to use ``pw_executable`` over plain ``executable`` targets to allow
- cleanly building the same code for multiple target configs.
-
-**Arguments**
-
-All of the ``pw_*`` target type overrides accept any arguments supported by
-the underlying native types, as they simply forward them through to the
-underlying target.
-
-Additionally, the following arguments are also supported:
-
-* **remove_configs**: (optional) A list of configs / config patterns to remove
- from the set of default configs specified by the current toolchain
- configuration.
-* **remove_public_deps**: (optional) A list of targets to remove from the set of
- default public_deps specified by the current toolchain configuration.
-
-.. _module-pw_build-link-deps:
-
-Link-only deps
---------------
-It may be necessary to specify additional link-time dependencies that may not be
-explicitly depended on elsewhere in the build. One example of this is a
-``pw_assert`` backend, which may need to leave out dependencies to avoid
-circular dependencies. Its dependencies need to be linked for executables and
-libraries, even if they aren't pulled in elsewhere.
-
-The ``pw_build_LINK_DEPS`` build arg is a list of dependencies to add to all
-``pw_executable``, ``pw_static_library``, and ``pw_shared_library`` targets.
-This should only be used as a last resort when dependencies cannot be properly
-expressed in the build.
-
-Python packages
----------------
-GN templates for :ref:`Python build automation <docs-python-build>` are
-described in :ref:`module-pw_build-python`.
-
.. toctree::
- :hidden:
-
- python
-
-
-.. _module-pw_build-cc_blob_library:
-
-pw_cc_blob_library
-------------------
-The ``pw_cc_blob_library`` template is useful for embedding binary data into a
-program. The template takes in a mapping of symbol names to file paths, and
-generates a set of C++ source and header files that embed the contents of the
-passed-in files as arrays of ``std::byte``.
-
-The blob byte arrays are constant initialized and are safe to access at any
-time, including before ``main()``.
-
-``pw_cc_blob_library`` is also available in the CMake build. It is provided by
-``pw_build/cc_blob_library.cmake``.
-
-**Arguments**
-
-* ``blobs``: A list of GN scopes, where each scope corresponds to a binary blob
- to be transformed from file to byte array. This is a required field. Blob
- fields include:
-
- * ``symbol_name``: The C++ symbol for the byte array.
- * ``file_path``: The file path for the binary blob.
- * ``linker_section``: If present, places the byte array in the specified
- linker section.
- * ``alignas``: If present, uses the specified string or integer verbatim in
- the ``alignas()`` specifier for the byte array.
-
-* ``out_header``: The header file to generate. Users will include this file
- exactly as it is written here to reference the byte arrays.
-* ``namespace``: An optional (but highly recommended!) C++ namespace to place
- the generated blobs within.
-
-Example
-^^^^^^^
-
-**BUILD.gn**
-
-.. code-block::
-
- pw_cc_blob_library("foo_bar_blobs") {
- blobs: [
- {
- symbol_name: "kFooBlob"
- file_path: "${target_out_dir}/stuff/bin/foo.bin"
- },
- {
- symbol_name: "kBarBlob"
- file_path: "//stuff/bin/bar.bin"
- linker_section: ".bar_section"
- },
- ]
- out_header: "my/stuff/foo_bar_blobs.h"
- namespace: "my::stuff"
- deps = [ ":generate_foo_bin" ]
- }
-
-.. note:: If the binary blobs are generated as part of the build, be sure to
- list them as deps to the pw_cc_blob_library target.
-
-**Generated Header**
-
-.. code-block::
-
- #pragma once
-
- #include <array>
- #include <cstddef>
-
- namespace my::stuff {
-
- extern const std::array<std::byte, 100> kFooBlob;
-
- extern const std::array<std::byte, 50> kBarBlob;
-
- } // namespace my::stuff
-
-**Generated Source**
-
-.. code-block::
-
- #include "my/stuff/foo_bar_blobs.h"
-
- #include <array>
- #include <cstddef>
-
- #include "pw_preprocessor/compiler.h"
-
- namespace my::stuff {
-
- const std::array<std::byte, 100> kFooBlob = { ... };
-
- PW_PLACE_IN_SECTION(".bar_section")
- const std::array<std::byte, 50> kBarBlob = { ... };
-
- } // namespace my::stuff
-
-.. _module-pw_build-facade:
-
-pw_facade
----------
-In their simplest form, a :ref:`facade<docs-module-structure-facades>` is a GN
-build arg used to change a dependency at compile time. Pigweed targets configure
-these facades as needed.
-
-The ``pw_facade`` template bundles a ``pw_source_set`` with a facade build arg.
-This allows the facade to provide header files, compilation options or anything
-else a GN ``source_set`` provides.
-
-The ``pw_facade`` template declares two targets:
-
-* ``$target_name``: the public-facing ``pw_source_set``, with a ``public_dep``
- on the backend
-* ``$target_name.facade``: target used by the backend to avoid circular
- dependencies
-
-.. code-block::
-
- # Declares ":foo" and ":foo.facade" GN targets
- pw_facade("foo") {
- backend = pw_log_BACKEND
- public_configs = [ ":public_include_path" ]
- public = [ "public/pw_foo/foo.h" ]
- }
-
-Low-level facades like ``pw_assert`` cannot express all of their dependencies
-due to the potential for dependency cycles. Facades with this issue may require
-backends to place their implementations in a separate build target to be listed
-in ``pw_build_LINK_DEPS`` (see :ref:`module-pw_build-link-deps`). The
-``require_link_deps`` variable in ``pw_facade`` asserts that all specified build
-targets are present in ``pw_build_LINK_DEPS`` if the facade's backend variable
-is set.
-
-.. _module-pw_build-python-action:
-
-pw_python_action
-----------------
-The ``pw_python_action`` template is a convenience wrapper around GN's `action
-function <https://gn.googlesource.com/gn/+/main/docs/reference.md#func_action>`_
-for running Python scripts. The main benefit it provides is resolution of GN
-target labels to compiled binary files. This allows Python scripts to be written
-independently of GN, taking only filesystem paths as arguments.
-
-Another convenience provided by the template is to allow running scripts without
-any outputs. Sometimes scripts run in a build do not directly produce output
-files, but GN requires that all actions have an output. ``pw_python_action``
-solves this by accepting a boolean ``stamp`` argument which tells it to create a
-placeholder output file for the action.
-
-**Arguments**
-
-``pw_python_action`` accepts all of the arguments of a regular ``action``
-target. Additionally, it has some of its own arguments:
-
-* ``module``: Run the specified Python module instead of a script. Either
- ``script`` or ``module`` must be specified, but not both.
-* ``capture_output``: Optional boolean. If true, script output is hidden unless
- the script fails with an error. Defaults to true.
-* ``stamp``: Optional variable indicating whether to automatically create a
- placeholder output file for the script. This allows running scripts without
- specifying ``outputs``. If ``stamp`` is true, a generic output file is
- used. If ``stamp`` is a file path, that file is used as a stamp file. Like any
- output file, ``stamp`` must be in the build directory. Defaults to false.
-* ``environment``: Optional list of strings. Environment variables to set,
- passed as NAME=VALUE strings.
-* ``working_directory``: Optional file path. When provided the current working
- directory will be set to this location before the Python module or script is
- run.
-* ``command_launcher``: Optional string. Arguments to prepend to the Python
- command, e.g. ``'/usr/bin/fakeroot --'`` will run the Python script within a
- fakeroot environment.
-* ``venv``: Optional gn target of the pw_python_venv that should be used to run
- this action.
-
-.. _module-pw_build-python-action-expressions:
-
-Expressions
-^^^^^^^^^^^
-
-``pw_python_action`` evaluates expressions in ``args``, the arguments passed to
-the script. These expressions function similarly to generator expressions in
-CMake. Expressions may be passed as a standalone argument or as part of another
-argument. A single argument may contain multiple expressions.
-
-Generally, these expressions are used within templates rather than directly in
-BUILD.gn files. This allows build code to use GN labels without having to worry
-about converting them to files.
-
-.. note::
-
- We intend to replace these expressions with native GN features when possible.
- See `b/234886742 <http://issuetracker.google.com/234886742>`_.
-
-The following expressions are supported:
-
-.. describe:: <TARGET_FILE(gn_target)>
-
- Evaluates to the output file of the provided GN target. For example, the
- expression
-
- .. code-block::
-
- "<TARGET_FILE(//foo/bar:static_lib)>"
-
- might expand to
-
- .. code-block::
-
- "/home/User/project_root/out/obj/foo/bar/static_lib.a"
-
- ``TARGET_FILE`` parses the ``.ninja`` file for the GN target, so it should
- always find the correct output file, regardless of the toolchain's or target's
- configuration. Some targets, such as ``source_set`` and ``group`` targets, do
- not have an output file, and attempting to use ``TARGET_FILE`` with them
- results in an error.
-
- ``TARGET_FILE`` only resolves GN target labels to their outputs. To resolve
- paths generally, use the standard GN approach of applying the
- ``rebase_path(path, root_build_dir)`` function. This function
- converts the provided GN path or list of paths to be relative to the build
- directory, from which all build commands and scripts are executed.
-
-.. describe:: <TARGET_FILE_IF_EXISTS(gn_target)>
-
- ``TARGET_FILE_IF_EXISTS`` evaluates to the output file of the provided GN
- target, if the output file exists. If the output file does not exist, the
- entire argument that includes this expression is omitted, even if there is
- other text or another expression.
-
- For example, consider this expression:
-
- .. code-block::
-
- "--database=<TARGET_FILE_IF_EXISTS(//alpha/bravo)>"
-
- If the ``//alpha/bravo`` target file exists, this might expand to the
- following:
-
- .. code-block::
-
- "--database=/home/User/project/out/obj/alpha/bravo/bravo.elf"
-
- If the ``//alpha/bravo`` target file does not exist, the entire
- ``--database=`` argument is omitted from the script arguments.
-
-.. describe:: <TARGET_OBJECTS(gn_target)>
-
- Evaluates to the object files of the provided GN target. Expands to a separate
- argument for each object file. If the target has no object files, the argument
- is omitted entirely. Because it does not expand to a single expression, the
- ``<TARGET_OBJECTS(...)>`` expression may not have leading or trailing text.
-
- For example, the expression
-
- .. code-block::
-
- "<TARGET_OBJECTS(//foo/bar:a_source_set)>"
-
- might expand to multiple separate arguments:
-
- .. code-block::
-
- "/home/User/project_root/out/obj/foo/bar/a_source_set.file_a.cc.o"
- "/home/User/project_root/out/obj/foo/bar/a_source_set.file_b.cc.o"
- "/home/User/project_root/out/obj/foo/bar/a_source_set.file_c.cc.o"
-
-**Example**
-
-.. code-block::
-
- import("$dir_pw_build/python_action.gni")
-
- pw_python_action("postprocess_main_image") {
- script = "py/postprocess_binary.py"
- args = [
- "--database",
- rebase_path("my/database.csv", root_build_dir),
- "--binary=<TARGET_FILE(//firmware/images:main)>",
- ]
- stamp = true
- }
-
-.. _module-pw_build-evaluate-path-expressions:
-
-pw_evaluate_path_expressions
-----------------------------
-It is not always feasible to pass information to a script through command line
-arguments. If a script requires a large amount of input data, writing to a file
-is often more convenient. However, doing so bypasses ``pw_python_action``'s GN
-target label resolution, preventing such scripts from working with build
-artifacts in a build system-agnostic manner.
-
-``pw_evaluate_path_expressions`` is designed to address this use case. It takes
-a list of input files and resolves target expressions within them, modifying the
-files in-place.
-
-Refer to ``pw_python_action``'s :ref:`module-pw_build-python-action-expressions`
-section for the list of supported expressions.
-
-.. note::
-
- ``pw_evaluate_path_expressions`` is typically used as an intermediate
- sub-target of a larger template, rather than a standalone build target.
-
-**Arguments**
-
-* ``files``: A list of scopes, each containing a ``source`` file to process and
- a ``dest`` file to which to write the result.
-
-**Example**
-
-The following template defines an executable target which additionally outputs
-the list of object files from which it was compiled, making use of
-``pw_evaluate_path_expressions`` to resolve their paths.
-
-.. code-block::
-
- import("$dir_pw_build/evaluate_path_expressions.gni")
-
- template("executable_with_artifacts") {
- executable("${target_name}.exe") {
- sources = invoker.sources
- if defined(invoker.deps) {
- deps = invoker.deps
- }
- }
-
- _artifacts_input = "$target_gen_dir/${target_name}_artifacts.json.in"
- _artifacts_output = "$target_gen_dir/${target_name}_artifacts.json"
- _artifacts = {
- binary = "<TARGET_FILE(:${target_name}.exe)>"
- objects = "<TARGET_OBJECTS(:${target_name}.exe)>"
- }
- write_file(_artifacts_input, _artifacts, "json")
-
- pw_evaluate_path_expressions("${target_name}.evaluate") {
- files = [
- {
- source = _artifacts_input
- dest = _artifacts_output
- },
- ]
- }
-
- group(target_name) {
- deps = [
- ":${target_name}.exe",
- ":${target_name}.evaluate",
- ]
- }
- }
-
-.. _module-pw_build-pw_exec:
-
-pw_exec
--------
-``pw_exec`` allows for execution of arbitrary programs. It is a wrapper around
-``pw_python_action`` but allows for specifying the program to execute.
-
-.. note:: Prefer to use ``pw_python_action`` instead of calling out to shell
- scripts, as the python will be more portable. ``pw_exec`` should generally
- only be used for interacting with legacy/existing scripts.
-
-**Arguments**
-
-* ``program``: The program to run. Can be a full path or just a name (in which
- case $PATH is searched).
-* ``args``: Optional list of arguments to the program.
-* ``deps``: Dependencies for this target.
-* ``public_deps``: Public dependencies for this target. In addition to outputs
- from this target, outputs generated by public dependencies can be used as
- inputs from targets that depend on this one. This is not the case for private
- deps.
-* ``inputs``: Optional list of build inputs to the program.
-* ``outputs``: Optional list of artifacts produced by the program's execution.
-* ``env``: Optional list of key-value pairs defining environment variables for
- the program.
-* ``env_file``: Optional path to a file containing a list of newline-separated
- key-value pairs defining environment variables for the program.
-* ``args_file``: Optional path to a file containing additional positional
- arguments to the program. Each line of the file is appended to the
- invocation. Useful for specifying arguments from GN metadata.
-* ``skip_empty_args``: If args_file is provided, boolean indicating whether to
- skip running the program if the file is empty. Used to avoid running
- commands which error when called without arguments.
-* ``capture_output``: If true, output from the program is hidden unless the
- program exits with an error. Defaults to true.
-* ``working_directory``: The working directory to execute the subprocess with.
- If not specified it will not be set and the subprocess will have whatever
- the parent current working directory is.
-* ``visibility``: GN visibility to apply to the underlying target.
-
-**Example**
-
-.. code-block::
-
- import("$dir_pw_build/exec.gni")
-
- pw_exec("hello_world") {
- program = "/bin/sh"
- args = [
- "-c",
- "echo hello \$WORLD",
- ]
- env = [
- "WORLD=world",
- ]
- }
-
-pw_input_group
---------------
-``pw_input_group`` defines a group of input files which are not directly
-processed by the build but are still important dependencies of later build
-steps. This is commonly used alongside metadata to propagate file dependencies
-through the build graph and force rebuilds on file modifications.
-
-For example ``pw_docgen`` defines a ``pw_doc_group`` template which outputs
-metadata from a list of input files. The metadata file is not actually part of
-the build, and so changes to any of the input files do not trigger a rebuild.
-This is problematic, as targets that depend on the metadata should rebuild when
-the inputs are modified but GN cannot express this dependency.
-
-``pw_input_group`` solves this problem by allowing a list of files to be listed
-in a target that does not output any build artifacts, causing all dependent
-targets to correctly rebuild.
-
-**Arguments**
-
-``pw_input_group`` accepts all arguments that can be passed to a ``group``
-target, as well as requiring one extra:
-
-* ``inputs``: List of input files.
-
-**Example**
-
-.. code-block::
-
- import("$dir_pw_build/input_group.gni")
-
- pw_input_group("foo_metadata") {
- metadata = {
- files = [
- "x.foo",
- "y.foo",
- "z.foo",
- ]
- }
- inputs = metadata.files
- }
-
-Targets depending on ``foo_metadata`` will rebuild when any of the ``.foo``
-files are modified.
-
-pw_zip
-------
-``pw_zip`` is a target that allows users to zip up a set of input files and
-directories into a single output ``.zip`` file—a simple automation of a
-potentially repetitive task.
-
-**Arguments**
-
-* ``inputs``: List of source files as well as the desired relative zip
- destination. See below for the input syntax.
-* ``dirs``: List of entire directories to be zipped as well as the desired
- relative zip destination. See below for the input syntax.
-* ``output``: Filename of output ``.zip`` file.
-* ``deps``: List of dependencies for the target.
-
-**Input Syntax**
-
-Inputs all need to follow the correct syntax:
-
-#. Path to source file or directory. Directories must end with a ``/``.
-#. The delimiter (defaults to ``>``).
-#. The desired destination of the contents within the ``.zip``. Must start
- with ``/`` to indicate the zip root. Any number of subdirectories are
- allowed. If the source is a file it can be put into any subdirectory of the
- root. If the source is a file, the zip copy can also be renamed by ending
- the zip destination with a filename (no trailing ``/``).
-
-Thus, it should look like the following: ``"[source file or dir] > /"``.
-
-**Example**
-
-Let's say we have the following structure for a ``//source/`` directory:
-
-.. code-block::
-
- source/
- ├── file1.txt
- ├── file2.txt
- ├── file3.txt
- └── some_dir/
- ├── file4.txt
- └── some_other_dir/
- └── file5.txt
-
-And we create the following build target:
-
-.. code-block::
-
- import("$dir_pw_build/zip.gni")
-
- pw_zip("target_name") {
- inputs = [
- "//source/file1.txt > /", # Copied to the zip root dir.
- "//source/file2.txt > /renamed.txt", # File renamed.
- "//source/file3.txt > /bar/", # File moved to the /bar/ dir.
- ]
-
- dirs = [
- "//source/some_dir/ > /bar/some_dir/", # All /some_dir/ contents copied
- # as /bar/some_dir/.
- ]
-
- # Note on output: if the specific output directory isn't defined
- # (such as output = "zoo.zip") then the .zip will output to the
- # same directory as the BUILD.gn file that called the target.
- output = "//$target_out_dir/foo.zip" # Where the foo.zip will end up
- }
-
-This will result in a ``.zip`` file called ``foo.zip`` stored in
-``//$target_out_dir`` with the following structure:
-
-.. code-block::
-
- foo.zip
- ├── bar/
- │ ├── file3.txt
- │ └── some_dir/
- │ ├── file4.txt
- │ └── some_other_dir/
- │ └── file5.txt
- ├── file1.txt
- └── renamed.txt
-
-.. _module-pw_build-relative-source-file-names:
-
-pw_relative_source_file_names
------------------------------
-This template recursively walks the listed dependencies and collects the names
-of all the headers and source files required by the targets, and then transforms
-them such that they reflect the ``__FILE__`` when pw_build's ``relative_paths``
-config is applied. This is primarily intended for side-band generation of
-pw_tokenizer tokens so file name tokens can be utilized in places where
-pw_tokenizer is unable to embed token information as part of C/C++ compilation.
-
-This template produces a JSON file containing an array of strings (file paths
-with ``-ffile-prefix-map``-like transformations applied) that can be used to
-:ref:`generate a token database <module-pw_tokenizer-database-creation>`.
-
-**Arguments**
-
-* ``deps``: A required list of targets to recursively extract file names from.
-* ``outputs``: A required array with a single element: the path to write the
- final JSON file to.
-
-**Example**
-
-Let's say we have the following project structure:
-
-.. code-block::
-
- project root
- ├── foo/
- │ ├── foo.h
- │ └── foo.cc
- ├── bar/
- │ ├── bar.h
- │ └── bar.cc
- ├── unused/
- │ ├── unused.h
- │ └── unused.cc
- └── main.cc
-
-And a BUILD.gn at the root:
-
-.. code-block::
-
- pw_source_set("bar") {
- public_configs = [ ":bar_headers" ]
- public = [ "bar/bar.h" ]
- sources = [ "bar/bar.cc" ]
- }
-
- pw_source_set("foo") {
- public_configs = [ ":foo_headers" ]
- public = [ "foo/foo.h" ]
- sources = [ "foo/foo.cc" ]
- deps = [ ":bar" ]
- }
-
-
- pw_source_set("unused") {
- public_configs = [ ":unused_headers" ]
- public = [ "unused/unused.h" ]
- sources = [ "unused/unused.cc" ]
- deps = [ ":bar" ]
- }
-
- pw_executable("main") {
- sources = [ "main.cc" ]
- deps = [ ":foo" ]
- }
-
- pw_relative_source_file_names("main_source_files") {
- deps = [ ":main" ]
- outputs = [ "$target_gen_dir/main_source_files.json" ]
- }
-
-The json file written to `out/gen/main_source_files.json` will contain:
-
-.. code-block::
-
- [
- "bar/bar.cc",
- "bar/bar.h",
- "foo/foo.cc",
- "foo/foo.h",
- "main.cc"
- ]
-
-Since ``unused`` isn't a transitive dependency of ``main``, its source files
-are not included. Similarly, even though ``bar`` is not a direct dependency of
-``main``, its source files *are* included because ``foo`` brings in ``bar`` as
-a transitive dependency.
-
-Note how the file paths in this example are relative to the project root rather
-than being absolute paths (e.g. ``/home/user/ralph/coding/my_proj/main.cc``).
-This is a result of transformations applied to strip absolute pathing prefixes,
-matching the behavior of pw_build's ``$dir_pw_build:relative_paths`` config.
-
-Build time errors: pw_error and pw_build_assert
------------------------------------------------
-In Pigweed's complex, multi-toolchain GN build it is not possible to build every
-target in every configuration. GN's ``assert`` statement is not ideal for
-enforcing the correct configuration because it may prevent the GN build files or
-targets from being referred to at all, even if they aren't used.
-
-The ``pw_error`` GN template results in an error if it is executed during the
-build. These error targets can exist in the build graph, but cannot be depended
-on without an error.
-
-``pw_build_assert`` evaluates to a ``pw_error`` if a condition fails or nothing
-(an empty group) if the condition passes. Targets can add a dependency on a
-``pw_build_assert`` to enforce a condition at build time.
-
-The templates for build time errors are defined in ``pw_build/error.gni``.
-
-Improved Ninja interface
-------------------------
-Ninja includes a basic progress display, showing in a single line the number of
-targets finished, the total number of targets, and the name of the most recent
-target it has either started or finished.
-
-For additional insight into the status of the build, Pigweed includes a Ninja
-wrapper, ``pw-wrap-ninja``, that displays additional real-time information about
-the progress of the build. The wrapper is invoked the same way you'd normally
-invoke Ninja:
-
-.. code-block:: sh
-
- pw-wrap-ninja -C out
-
-The script lists the progress of the build, as well as the list of targets that
-Ninja is currently building, along with a timer that measures how long each
-target has been building for:
-
-.. code-block::
-
- [51.3s] Building [8924/10690] ...
- [10.4s] c++ pw_strict_host_clang_debug/obj/pw_string/string_test.lib.string_test.cc.o
- [ 9.5s] ACTION //pw_console/py:py.lint.mypy(//pw_build/python_toolchain:python)
- [ 9.4s] ACTION //pw_console/py:py.lint.pylint(//pw_build/python_toolchain:python)
- [ 6.1s] clang-tidy ../pw_log_rpc/log_service.cc
- [ 6.1s] clang-tidy ../pw_log_rpc/log_service_test.cc
- [ 6.1s] clang-tidy ../pw_log_rpc/rpc_log_drain.cc
- [ 6.1s] clang-tidy ../pw_log_rpc/rpc_log_drain_test.cc
- [ 5.4s] c++ pw_strict_host_clang_debug/obj/BUILD_DIR/pw_strict_host_clang_debug/gen/pw...
- ... and 109 more
-
-This allows you to, at a glance, know what Ninja's currently building, which
-targets are bottlenecking the rest of the build, and which targets are taking
-an unusually long time to complete.
-
-``pw-wrap-ninja`` includes other useful functionality as well. The
-``--write-trace`` option writes a build trace to the specified path, which can
-be viewed in the `Perfetto UI <https://ui.perfetto.dev/>`_, or via Chrome's
-built-in ``chrome://tracing`` tool.
-
-CMake
-=====
-Pigweed's `CMake`_ support is provided primarily for projects that have an
-existing CMake build and wish to integrate Pigweed without switching to a new
-build system.
-
-The following command generates Ninja build files for a host build in the
-``out/cmake_host`` directory:
-
-.. code-block:: sh
-
- cmake -B out/cmake_host -S "$PW_ROOT" -G Ninja -DCMAKE_TOOLCHAIN_FILE=$PW_ROOT/pw_toolchain/host_clang/toolchain.cmake
-
-The ``PW_ROOT`` environment variable must point to the root of the Pigweed
-directory. This variable is set by Pigweed's environment setup.
-
-Tests can be executed with the ``pw_run_tests.GROUP`` targets. To run Pigweed
-module tests, execute ``pw_run_tests.modules``:
-
-.. code-block:: sh
-
- ninja -C out/cmake_host pw_run_tests.modules
-
-:ref:`module-pw_watch` supports CMake, so you can also run
-
-.. code-block:: sh
-
- pw watch -C out/cmake_host pw_run_tests.modules
-
-CMake functions
----------------
-CMake convenience functions are defined in ``pw_build/pigweed.cmake``.
-
-* ``pw_add_library_generic`` -- The base helper used to instantiate CMake
- libraries. This is meant for use in downstream projects as upstream Pigweed
- modules are expected to use ``pw_add_library``.
-* ``pw_add_library`` -- Add an upstream Pigweed library.
-* ``pw_add_facade_generic`` -- The base helper used to instantiate facade
- libraries. This is meant for use in downstream projects as upstream Pigweed
- modules are expected to use ``pw_add_facade``.
-* ``pw_add_facade`` -- Declare an upstream Pigweed facade.
-* ``pw_set_backend`` -- Set the backend library to use for a facade.
-* ``pw_add_test_generic`` -- The base helper used to instantiate test targets.
- This is meant for use in downstrema projects as upstream Pigweed modules are
- expected to use ``pw_add_test``.
-* ``pw_add_test`` -- Declare an upstream Pigweed test target.
-* ``pw_add_test_group`` -- Declare a target to group and bundle test targets.
-* ``pw_target_link_targets`` -- Helper wrapper around ``target_link_libraries``
- which only supports CMake targets and detects when the target does not exist.
- Note that generator expressions are not supported.
-* ``pw_add_global_compile_options`` -- Applies compilation options to all
- targets in the build. This should only be used to add essential compilation
- options, such as those that affect the ABI. Use ``pw_add_library`` or
- ``target_compile_options`` to apply other compile options.
-* ``pw_add_error_target`` -- Declares target which reports a message and causes
- a build failure only when compiled. This is useful when ``FATAL_ERROR``
- messages cannot be used to catch problems during the CMake configuration
- phase.
-* ``pw_parse_arguments`` -- Helper to parse CMake function arguments.
-
-See ``pw_build/pigweed.cmake`` for the complete documentation of these
-functions.
-
-Special libraries that do not fit well with these functions are created with the
-standard CMake functions, such as ``add_library`` and ``target_link_libraries``.
-
-Facades and backends
---------------------
-The CMake build uses CMake cache variables for configuring
-:ref:`facades<docs-module-structure-facades>` and backends. Cache variables are
-similar to GN's build args set with ``gn args``. Unlike GN, CMake does not
-support multi-toolchain builds, so these variables have a single global value
-per build directory.
-
-The ``pw_add_module_facade`` function declares a cache variable named
-``<module_name>_BACKEND`` for each facade. Cache variables can be awkward to
-work with, since their values only change when they're assigned, but then
-persist accross CMake invocations. These variables should be set in one of the
-following ways:
-
-* Prior to setting a backend, your application should include
- ``$ENV{PW_ROOT}/backends.cmake``. This file will setup all the backend targets
- such that any misspelling of a facade or backend will yield a warning.
-
- .. note::
- Zephyr developers do not need to do this, backends can be set automatically
- by enabling the appropriate Kconfig options.
-
-* Call ``pw_set_backend`` to set backends appropriate for the target in the
- target's toolchain file. The toolchain file is provided to ``cmake`` with
- ``-DCMAKE_TOOLCHAIN_FILE=<toolchain file>``.
-* Call ``pw_set_backend`` in the top-level ``CMakeLists.txt`` before other
- CMake code executes.
-* Set the backend variable at the command line with the ``-D`` option.
-
- .. code-block:: sh
-
- cmake -B out/cmake_host -S "$PW_ROOT" -G Ninja \
- -DCMAKE_TOOLCHAIN_FILE=$PW_ROOT/pw_toolchain/host_clang/toolchain.cmake \
- -Dpw_log_BACKEND=pw_log_basic
-
-* Temporarily override a backend by setting it interactively with ``ccmake`` or
- ``cmake-gui``.
-
-If the backend is set to a build target that does not exist, there will be an
-error message like the following:
-
-.. code-block::
-
- CMake Error at pw_build/pigweed.cmake:257 (message):
- my_module.my_facade's INTERFACE dep "my_nonexistent_backend" is not
- a target.
- Call Stack (most recent call first):
- pw_build/pigweed.cmake:238:EVAL:1 (_pw_target_link_targets_deferred_check)
- CMakeLists.txt:DEFERRED
-
-
-Toolchain setup
----------------
-In CMake, the toolchain is configured by setting CMake variables, as described
-in the `CMake documentation <https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html>`_.
-These variables are typically set in a toolchain CMake file passed to ``cmake``
-with the ``-D`` option (``-DCMAKE_TOOLCHAIN_FILE=path/to/file.cmake``).
-For Pigweed embedded builds, set ``CMAKE_SYSTEM_NAME`` to the empty string
-(``""``).
-
-Toolchains may set the ``pw_build_WARNINGS`` variable to a list of ``INTERFACE``
-libraries with compilation options for Pigweed's upstream libraries. This
-defaults to a strict set of warnings. Projects may need to use less strict
-compilation warnings to compile backends exposed to Pigweed code (such as
-``pw_log``) that cannot compile with Pigweed's flags. If desired, Projects can
-access these warnings by depending on ``pw_build.warnings``.
-
-Third party libraries
----------------------
-The CMake build includes third-party libraries similarly to the GN build. A
-``dir_pw_third_party_<library>`` cache variable is defined for each third-party
-dependency. The variable must be set to the absolute path of the library in
-order to use it. If the variable is empty
-(``if("${dir_pw_third_party_<library>}" STREQUAL "")``), the dependency is not
-available.
-
-Third-party dependencies are not automatically added to the build. They can be
-manually added with ``add_subdirectory`` or by setting the
-``pw_third_party_<library>_ADD_SUBDIRECTORY`` option to ``ON``.
-
-Third party variables are set like any other cache global variable in CMake. It
-is recommended to set these in one of the following ways:
-
-* Set with the CMake ``set`` function in the toolchain file or a
- ``CMakeLists.txt`` before other CMake code executes.
-
- .. code-block:: cmake
-
- set(dir_pw_third_party_nanopb ${CMAKE_CURRENT_SOURCE_DIR}/external/nanopb CACHE PATH "" FORCE)
-
-* Set the variable at the command line with the ``-D`` option.
-
- .. code-block:: sh
-
- cmake -B out/cmake_host -S "$PW_ROOT" -G Ninja \
- -DCMAKE_TOOLCHAIN_FILE=$PW_ROOT/pw_toolchain/host_clang/toolchain.cmake \
- -Ddir_pw_third_party_nanopb=/path/to/nanopb
-
-* Set the variable interactively with ``ccmake`` or ``cmake-gui``.
-
-Use Pigweed from an existing CMake project
-------------------------------------------
-To use Pigweed libraries form a CMake-based project, simply include the Pigweed
-repository from a ``CMakeLists.txt``.
-
-.. code-block:: cmake
-
- add_subdirectory(path/to/pigweed pigweed)
-
-All module libraries will be available as ``module_name`` or
-``module_name.sublibrary``.
-
-If desired, modules can be included individually.
-
-.. code-block:: cmake
-
- add_subdirectory(path/to/pigweed/pw_some_module pw_some_module)
- add_subdirectory(path/to/pigweed/pw_another_module pw_another_module)
-
-Bazel
-=====
-Bazel is currently very experimental, and only builds for host and ARM Cortex-M
-microcontrollers.
-
-The common configuration for Bazel for all modules is in the ``pigweed.bzl``
-file. The built-in Bazel rules ``cc_binary``, ``cc_library``, and ``cc_test``
-are wrapped with ``pw_cc_binary``, ``pw_cc_library``, and ``pw_cc_test``.
-These wrappers add parameters to calls to the compiler and linker.
-
-In addition to wrapping the built-in rules, Pigweed also provides a custom
-rule for handling linker scripts with Bazel. e.g.
-
-.. code-block:: python
-
- pw_linker_script(
- name = "some_linker_script",
- linker_script = ":some_configurable_linker_script.ld",
- defines = [
- "PW_BOOT_FLASH_BEGIN=0x08000200",
- "PW_BOOT_FLASH_SIZE=1024K",
- "PW_BOOT_HEAP_SIZE=112K",
- "PW_BOOT_MIN_STACK_SIZE=1K",
- "PW_BOOT_RAM_BEGIN=0x20000000",
- "PW_BOOT_RAM_SIZE=192K",
- "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
- ],
- )
-
- pw_cc_binary(
- name = "some_binary",
- srcs = ["some_source.c"],
- additional_linker_inputs = [":some_linker_script"],
- linkopts = ["-T $(location :some_linker_script)"],
- )
-
-Currently Pigweed is making use of a set of
-`open source <https://github.com/silvergasp/bazel-embedded>`_ toolchains. The
-host builds are only supported on Linux/Mac based systems. Additionally the
-host builds are not entirely hermetic, and will make use of system
-libraries and headers. This is close to the default configuration for Bazel,
-though slightly more hermetic. The host toolchain is based around clang-11 which
-has a system dependency on 'libtinfo.so.5' which is often included as part of
-the libncurses packages. On Debian based systems this can be installed using the
-command below:
-
-.. code-block:: sh
-
- sudo apt install libncurses5
-
-The host toolchain does not currently support native Windows, though using WSL
-is a viable alternative.
-
-The ARM Cortex-M Bazel toolchains are based around gcc-arm-non-eabi and are
-entirely hermetic. You can target Cortex-M, by using the platforms command line
-option. This set of toolchains is supported from hosts; Windows, Mac and Linux.
-The platforms that are currently supported are listed below:
-
-.. code-block:: sh
-
- bazel build //:your_target --platforms=@pigweed//pw_build/platforms:cortex_m0
- bazel build //:your_target --platforms=@pigweed//pw_build/platforms:cortex_m1
- bazel build //:your_target --platforms=@pigweed//pw_build/platforms:cortex_m3
- bazel build //:your_target --platforms=@pigweed//pw_build/platforms:cortex_m4
- bazel build //:your_target --platforms=@pigweed//pw_build/platforms:cortex_m7
- bazel build //:your_target \
- --platforms=@pigweed//pw_build/platforms:cortex_m4_fpu
- bazel build //:your_target \
- --platforms=@pigweed//pw_build/platforms:cortex_m7_fpu
-
-
-The above examples are cpu/fpu oriented platforms and can be used where
-applicable for your application. There some more specific platforms for the
-types of boards that are included as examples in Pigweed. It is strongly
-encouraged that you create your own set of platforms specific for your project,
-that implement the constraint_settings in this repository. e.g.
-
-New board constraint_value:
-
-.. code-block:: python
-
- #your_repo/build_settings/constraints/board/BUILD
- constraint_value(
- name = "nucleo_l432kc",
- constraint_setting = "@pigweed//pw_build/constraints/board",
- )
-
-New chipset constraint_value:
-
-.. code-block:: python
-
- # your_repo/build_settings/constraints/chipset/BUILD
- constraint_value(
- name = "stm32l432kc",
- constraint_setting = "@pigweed//pw_build/constraints/chipset",
- )
-
-New platforms for chipset and board:
-
-.. code-block:: python
-
- #your_repo/build_settings/platforms/BUILD
- # Works with all stm32l432kc
- platforms(
- name = "stm32l432kc",
- parents = ["@pigweed//pw_build/platforms:cortex_m4"],
- constraint_values =
- ["@your_repo//build_settings/constraints/chipset:stm32l432kc"],
- )
-
- # Works with only the nucleo_l432kc
- platforms(
- name = "nucleo_l432kc",
- parents = [":stm32l432kc"],
- constraint_values =
- ["@your_repo//build_settings/constraints/board:nucleo_l432kc"],
- )
-
-In the above example you can build your code with the command line:
-
-.. code-block:: python
-
- bazel build //:your_target_for_nucleo_l432kc \
- --platforms=@your_repo//build_settings:nucleo_l432kc
-
-
-You can also specify that a specific target is only compatible with one
-platform:
-
-.. code-block:: python
-
- cc_library(
- name = "compatible_with_all_stm32l432kc",
- srcs = ["tomato_src.c"],
- target_compatible_with =
- ["@your_repo//build_settings/constraints/chipset:stm32l432kc"],
- )
-
- cc_library(
- name = "compatible_with_only_nucleo_l432kc",
- srcs = ["bbq_src.c"],
- target_compatible_with =
- ["@your_repo//build_settings/constraints/board:nucleo_l432kc"],
- )
+ :maxdepth: 1
+ gn
+ cmake
+ bazel
diff --git a/pw_build/exec.gni b/pw_build/exec.gni
index eea4c01c1..6cf9bcf1e 100644
--- a/pw_build/exec.gni
+++ b/pw_build/exec.gni
@@ -57,6 +57,8 @@ import("python_action.gni")
# not specified it will not be set and the subprocess will have whatever the
# parent current working directory is.
#
+# venv: Python virtualenv to pass along to the underlying pw_python_action.
+#
# visibility: GN visibility to apply to the underlying target.
#
# Example:
@@ -140,6 +142,7 @@ template("pw_exec") {
"inputs",
"pool",
"public_deps",
+ "venv",
"visibility",
])
diff --git a/pw_build/facade.gni b/pw_build/facade.gni
index 0b188a10d..a82336089 100644
--- a/pw_build/facade.gni
+++ b/pw_build/facade.gni
@@ -132,6 +132,15 @@ template("pw_facade") {
"",
" $_varname = \"//path/to/the:backend\"",
"",
+ "Alternatively, if the target depending on this facade is a `pw_test`",
+ "which should only be built in toolchains with a provided backend,",
+ "consider adding an `enable_if` to the dependent target:",
+ "",
+ " pw_test(...) {",
+ " enable_if = $_varname != \"\"",
+ " ...",
+ " }",
+ "",
"If you are NOT using this facade, this error may have been triggered ",
"by trying to build all targets.",
]
diff --git a/pw_build/file_prefix_map_test.cc b/pw_build/file_prefix_map_test.cc
new file mode 100644
index 000000000..8206dc5d1
--- /dev/null
+++ b/pw_build/file_prefix_map_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_build_private/file_prefix_map_test.h"
+
+namespace pw::build::test {
+
+static_assert(StringsAreEqual("", ""));
+static_assert(StringsAreEqual("test", "test"));
+static_assert(!StringsAreEqual("1test", "test"));
+static_assert(!StringsAreEqual("test", "test1"));
+static_assert(!StringsAreEqual("test", "toast"));
+static_assert(!StringsAreEqual("", "test"));
+
+static_assert(StringsAreEqual(PW_BUILD_EXPECTED_SOURCE_PATH, __FILE__),
+ "The __FILE__ macro should be " PW_BUILD_EXPECTED_SOURCE_PATH
+ ", but it is " __FILE__);
+
+} // namespace pw::build::test
diff --git a/pw_build/generated_pigweed_modules_lists.gni b/pw_build/generated_pigweed_modules_lists.gni
index 77cec9182..e2ce68be8 100644
--- a/pw_build/generated_pigweed_modules_lists.gni
+++ b/pw_build/generated_pigweed_modules_lists.gni
@@ -54,9 +54,11 @@ declare_args() {
dir_pw_build_mcuxpresso = get_path_info("../pw_build_mcuxpresso", "abspath")
dir_pw_bytes = get_path_info("../pw_bytes", "abspath")
dir_pw_checksum = get_path_info("../pw_checksum", "abspath")
+ dir_pw_chre = get_path_info("../pw_chre", "abspath")
dir_pw_chrono = get_path_info("../pw_chrono", "abspath")
dir_pw_chrono_embos = get_path_info("../pw_chrono_embos", "abspath")
dir_pw_chrono_freertos = get_path_info("../pw_chrono_freertos", "abspath")
+ dir_pw_chrono_rp2040 = get_path_info("../pw_chrono_rp2040", "abspath")
dir_pw_chrono_stl = get_path_info("../pw_chrono_stl", "abspath")
dir_pw_chrono_threadx = get_path_info("../pw_chrono_threadx", "abspath")
dir_pw_chrono_zephyr = get_path_info("../pw_chrono_zephyr", "abspath")
@@ -70,24 +72,32 @@ declare_args() {
get_path_info("../pw_cpu_exception_cortex_m", "abspath")
dir_pw_crypto = get_path_info("../pw_crypto", "abspath")
dir_pw_digital_io = get_path_info("../pw_digital_io", "abspath")
+ dir_pw_digital_io_mcuxpresso =
+ get_path_info("../pw_digital_io_mcuxpresso", "abspath")
+ dir_pw_digital_io_rp2040 = get_path_info("../pw_digital_io_rp2040", "abspath")
dir_pw_docgen = get_path_info("../pw_docgen", "abspath")
dir_pw_doctor = get_path_info("../pw_doctor", "abspath")
+ dir_pw_emu = get_path_info("../pw_emu", "abspath")
dir_pw_env_setup = get_path_info("../pw_env_setup", "abspath")
dir_pw_file = get_path_info("../pw_file", "abspath")
+ dir_pw_format = get_path_info("../pw_format", "abspath")
dir_pw_function = get_path_info("../pw_function", "abspath")
dir_pw_fuzzer = get_path_info("../pw_fuzzer", "abspath")
dir_pw_hdlc = get_path_info("../pw_hdlc", "abspath")
dir_pw_hex_dump = get_path_info("../pw_hex_dump", "abspath")
dir_pw_i2c = get_path_info("../pw_i2c", "abspath")
+ dir_pw_i2c_linux = get_path_info("../pw_i2c_linux", "abspath")
dir_pw_i2c_mcuxpresso = get_path_info("../pw_i2c_mcuxpresso", "abspath")
dir_pw_ide = get_path_info("../pw_ide", "abspath")
dir_pw_interrupt = get_path_info("../pw_interrupt", "abspath")
dir_pw_interrupt_cortex_m =
get_path_info("../pw_interrupt_cortex_m", "abspath")
+ dir_pw_interrupt_xtensa = get_path_info("../pw_interrupt_xtensa", "abspath")
dir_pw_interrupt_zephyr = get_path_info("../pw_interrupt_zephyr", "abspath")
dir_pw_intrusive_ptr = get_path_info("../pw_intrusive_ptr", "abspath")
dir_pw_kvs = get_path_info("../pw_kvs", "abspath")
dir_pw_libc = get_path_info("../pw_libc", "abspath")
+ dir_pw_libcxx = get_path_info("../pw_libcxx", "abspath")
dir_pw_log = get_path_info("../pw_log", "abspath")
dir_pw_log_android = get_path_info("../pw_log_android", "abspath")
dir_pw_log_basic = get_path_info("../pw_log_basic", "abspath")
@@ -102,6 +112,7 @@ declare_args() {
dir_pw_minimal_cpp_stdlib =
get_path_info("../pw_minimal_cpp_stdlib", "abspath")
dir_pw_module = get_path_info("../pw_module", "abspath")
+ dir_pw_multibuf = get_path_info("../pw_multibuf", "abspath")
dir_pw_multisink = get_path_info("../pw_multisink", "abspath")
dir_pw_package = get_path_info("../pw_package", "abspath")
dir_pw_perf_test = get_path_info("../pw_perf_test", "abspath")
@@ -116,14 +127,21 @@ declare_args() {
dir_pw_ring_buffer = get_path_info("../pw_ring_buffer", "abspath")
dir_pw_router = get_path_info("../pw_router", "abspath")
dir_pw_rpc = get_path_info("../pw_rpc", "abspath")
+ dir_pw_rpc_transport = get_path_info("../pw_rpc_transport", "abspath")
dir_pw_rust = get_path_info("../pw_rust", "abspath")
dir_pw_snapshot = get_path_info("../pw_snapshot", "abspath")
dir_pw_software_update = get_path_info("../pw_software_update", "abspath")
dir_pw_span = get_path_info("../pw_span", "abspath")
dir_pw_spi = get_path_info("../pw_spi", "abspath")
+ dir_pw_spi_mcuxpresso = get_path_info("../pw_spi_mcuxpresso", "abspath")
dir_pw_status = get_path_info("../pw_status", "abspath")
dir_pw_stm32cube_build = get_path_info("../pw_stm32cube_build", "abspath")
dir_pw_stream = get_path_info("../pw_stream", "abspath")
+ dir_pw_stream_shmem_mcuxpresso =
+ get_path_info("../pw_stream_shmem_mcuxpresso", "abspath")
+ dir_pw_stream_uart_linux = get_path_info("../pw_stream_uart_linux", "abspath")
+ dir_pw_stream_uart_mcuxpresso =
+ get_path_info("../pw_stream_uart_mcuxpresso", "abspath")
dir_pw_string = get_path_info("../pw_string", "abspath")
dir_pw_symbolizer = get_path_info("../pw_symbolizer", "abspath")
dir_pw_sync = get_path_info("../pw_sync", "abspath")
@@ -134,6 +152,7 @@ declare_args() {
dir_pw_sync_threadx = get_path_info("../pw_sync_threadx", "abspath")
dir_pw_sync_zephyr = get_path_info("../pw_sync_zephyr", "abspath")
dir_pw_sys_io = get_path_info("../pw_sys_io", "abspath")
+ dir_pw_sys_io_ambiq_sdk = get_path_info("../pw_sys_io_ambiq_sdk", "abspath")
dir_pw_sys_io_arduino = get_path_info("../pw_sys_io_arduino", "abspath")
dir_pw_sys_io_baremetal_lm3s6965evb =
get_path_info("../pw_sys_io_baremetal_lm3s6965evb", "abspath")
@@ -142,7 +161,7 @@ declare_args() {
dir_pw_sys_io_emcraft_sf2 =
get_path_info("../pw_sys_io_emcraft_sf2", "abspath")
dir_pw_sys_io_mcuxpresso = get_path_info("../pw_sys_io_mcuxpresso", "abspath")
- dir_pw_sys_io_pico = get_path_info("../pw_sys_io_pico", "abspath")
+ dir_pw_sys_io_rp2040 = get_path_info("../pw_sys_io_rp2040", "abspath")
dir_pw_sys_io_stdio = get_path_info("../pw_sys_io_stdio", "abspath")
dir_pw_sys_io_stm32cube = get_path_info("../pw_sys_io_stm32cube", "abspath")
dir_pw_sys_io_zephyr = get_path_info("../pw_sys_io_zephyr", "abspath")
@@ -162,10 +181,12 @@ declare_args() {
dir_pw_tokenizer = get_path_info("../pw_tokenizer", "abspath")
dir_pw_tool = get_path_info("../pw_tool", "abspath")
dir_pw_toolchain = get_path_info("../pw_toolchain", "abspath")
+ dir_pw_toolchain_bazel = get_path_info("../pw_toolchain_bazel", "abspath")
dir_pw_trace = get_path_info("../pw_trace", "abspath")
dir_pw_trace_tokenized = get_path_info("../pw_trace_tokenized", "abspath")
dir_pw_transfer = get_path_info("../pw_transfer", "abspath")
dir_pw_unit_test = get_path_info("../pw_unit_test", "abspath")
+ dir_pw_unit_test_zephyr = get_path_info("../pw_unit_test_zephyr", "abspath")
dir_pw_varint = get_path_info("../pw_varint", "abspath")
dir_pw_watch = get_path_info("../pw_watch", "abspath")
dir_pw_web = get_path_info("../pw_web", "abspath")
@@ -203,9 +224,11 @@ declare_args() {
dir_pw_build_mcuxpresso,
dir_pw_bytes,
dir_pw_checksum,
+ dir_pw_chre,
dir_pw_chrono,
dir_pw_chrono_embos,
dir_pw_chrono_freertos,
+ dir_pw_chrono_rp2040,
dir_pw_chrono_stl,
dir_pw_chrono_threadx,
dir_pw_chrono_zephyr,
@@ -217,23 +240,30 @@ declare_args() {
dir_pw_cpu_exception_cortex_m,
dir_pw_crypto,
dir_pw_digital_io,
+ dir_pw_digital_io_mcuxpresso,
+ dir_pw_digital_io_rp2040,
dir_pw_docgen,
dir_pw_doctor,
+ dir_pw_emu,
dir_pw_env_setup,
dir_pw_file,
+ dir_pw_format,
dir_pw_function,
dir_pw_fuzzer,
dir_pw_hdlc,
dir_pw_hex_dump,
dir_pw_i2c,
+ dir_pw_i2c_linux,
dir_pw_i2c_mcuxpresso,
dir_pw_ide,
dir_pw_interrupt,
dir_pw_interrupt_cortex_m,
+ dir_pw_interrupt_xtensa,
dir_pw_interrupt_zephyr,
dir_pw_intrusive_ptr,
dir_pw_kvs,
dir_pw_libc,
+ dir_pw_libcxx,
dir_pw_log,
dir_pw_log_android,
dir_pw_log_basic,
@@ -247,6 +277,7 @@ declare_args() {
dir_pw_metric,
dir_pw_minimal_cpp_stdlib,
dir_pw_module,
+ dir_pw_multibuf,
dir_pw_multisink,
dir_pw_package,
dir_pw_perf_test,
@@ -261,14 +292,19 @@ declare_args() {
dir_pw_ring_buffer,
dir_pw_router,
dir_pw_rpc,
+ dir_pw_rpc_transport,
dir_pw_rust,
dir_pw_snapshot,
dir_pw_software_update,
dir_pw_span,
dir_pw_spi,
+ dir_pw_spi_mcuxpresso,
dir_pw_status,
dir_pw_stm32cube_build,
dir_pw_stream,
+ dir_pw_stream_shmem_mcuxpresso,
+ dir_pw_stream_uart_linux,
+ dir_pw_stream_uart_mcuxpresso,
dir_pw_string,
dir_pw_symbolizer,
dir_pw_sync,
@@ -279,12 +315,13 @@ declare_args() {
dir_pw_sync_threadx,
dir_pw_sync_zephyr,
dir_pw_sys_io,
+ dir_pw_sys_io_ambiq_sdk,
dir_pw_sys_io_arduino,
dir_pw_sys_io_baremetal_lm3s6965evb,
dir_pw_sys_io_baremetal_stm32f429,
dir_pw_sys_io_emcraft_sf2,
dir_pw_sys_io_mcuxpresso,
- dir_pw_sys_io_pico,
+ dir_pw_sys_io_rp2040,
dir_pw_sys_io_stdio,
dir_pw_sys_io_stm32cube,
dir_pw_sys_io_zephyr,
@@ -302,10 +339,12 @@ declare_args() {
dir_pw_tokenizer,
dir_pw_tool,
dir_pw_toolchain,
+ dir_pw_toolchain_bazel,
dir_pw_trace,
dir_pw_trace_tokenized,
dir_pw_transfer,
dir_pw_unit_test,
+ dir_pw_unit_test_zephyr,
dir_pw_varint,
dir_pw_watch,
dir_pw_web,
@@ -340,9 +379,11 @@ declare_args() {
"$dir_pw_build_mcuxpresso:tests",
"$dir_pw_bytes:tests",
"$dir_pw_checksum:tests",
+ "$dir_pw_chre:tests",
"$dir_pw_chrono:tests",
"$dir_pw_chrono_embos:tests",
"$dir_pw_chrono_freertos:tests",
+ "$dir_pw_chrono_rp2040:tests",
"$dir_pw_chrono_stl:tests",
"$dir_pw_chrono_threadx:tests",
"$dir_pw_chrono_zephyr:tests",
@@ -354,23 +395,30 @@ declare_args() {
"$dir_pw_cpu_exception_cortex_m:tests",
"$dir_pw_crypto:tests",
"$dir_pw_digital_io:tests",
+ "$dir_pw_digital_io_mcuxpresso:tests",
+ "$dir_pw_digital_io_rp2040:tests",
"$dir_pw_docgen:tests",
"$dir_pw_doctor:tests",
+ "$dir_pw_emu:tests",
"$dir_pw_env_setup:tests",
"$dir_pw_file:tests",
+ "$dir_pw_format:tests",
"$dir_pw_function:tests",
"$dir_pw_fuzzer:tests",
"$dir_pw_hdlc:tests",
"$dir_pw_hex_dump:tests",
"$dir_pw_i2c:tests",
+ "$dir_pw_i2c_linux:tests",
"$dir_pw_i2c_mcuxpresso:tests",
"$dir_pw_ide:tests",
"$dir_pw_interrupt:tests",
"$dir_pw_interrupt_cortex_m:tests",
+ "$dir_pw_interrupt_xtensa:tests",
"$dir_pw_interrupt_zephyr:tests",
"$dir_pw_intrusive_ptr:tests",
"$dir_pw_kvs:tests",
"$dir_pw_libc:tests",
+ "$dir_pw_libcxx:tests",
"$dir_pw_log:tests",
"$dir_pw_log_android:tests",
"$dir_pw_log_basic:tests",
@@ -384,6 +432,7 @@ declare_args() {
"$dir_pw_metric:tests",
"$dir_pw_minimal_cpp_stdlib:tests",
"$dir_pw_module:tests",
+ "$dir_pw_multibuf:tests",
"$dir_pw_multisink:tests",
"$dir_pw_package:tests",
"$dir_pw_perf_test:tests",
@@ -398,14 +447,19 @@ declare_args() {
"$dir_pw_ring_buffer:tests",
"$dir_pw_router:tests",
"$dir_pw_rpc:tests",
+ "$dir_pw_rpc_transport:tests",
"$dir_pw_rust:tests",
"$dir_pw_snapshot:tests",
"$dir_pw_software_update:tests",
"$dir_pw_span:tests",
"$dir_pw_spi:tests",
+ "$dir_pw_spi_mcuxpresso:tests",
"$dir_pw_status:tests",
"$dir_pw_stm32cube_build:tests",
"$dir_pw_stream:tests",
+ "$dir_pw_stream_shmem_mcuxpresso:tests",
+ "$dir_pw_stream_uart_linux:tests",
+ "$dir_pw_stream_uart_mcuxpresso:tests",
"$dir_pw_string:tests",
"$dir_pw_symbolizer:tests",
"$dir_pw_sync:tests",
@@ -416,12 +470,13 @@ declare_args() {
"$dir_pw_sync_threadx:tests",
"$dir_pw_sync_zephyr:tests",
"$dir_pw_sys_io:tests",
+ "$dir_pw_sys_io_ambiq_sdk:tests",
"$dir_pw_sys_io_arduino:tests",
"$dir_pw_sys_io_baremetal_lm3s6965evb:tests",
"$dir_pw_sys_io_baremetal_stm32f429:tests",
"$dir_pw_sys_io_emcraft_sf2:tests",
"$dir_pw_sys_io_mcuxpresso:tests",
- "$dir_pw_sys_io_pico:tests",
+ "$dir_pw_sys_io_rp2040:tests",
"$dir_pw_sys_io_stdio:tests",
"$dir_pw_sys_io_stm32cube:tests",
"$dir_pw_sys_io_zephyr:tests",
@@ -439,10 +494,12 @@ declare_args() {
"$dir_pw_tokenizer:tests",
"$dir_pw_tool:tests",
"$dir_pw_toolchain:tests",
+ "$dir_pw_toolchain_bazel:tests",
"$dir_pw_trace:tests",
"$dir_pw_trace_tokenized:tests",
"$dir_pw_transfer:tests",
"$dir_pw_unit_test:tests",
+ "$dir_pw_unit_test_zephyr:tests",
"$dir_pw_varint:tests",
"$dir_pw_watch:tests",
"$dir_pw_web:tests",
@@ -477,9 +534,11 @@ declare_args() {
"$dir_pw_build_mcuxpresso:docs",
"$dir_pw_bytes:docs",
"$dir_pw_checksum:docs",
+ "$dir_pw_chre:docs",
"$dir_pw_chrono:docs",
"$dir_pw_chrono_embos:docs",
"$dir_pw_chrono_freertos:docs",
+ "$dir_pw_chrono_rp2040:docs",
"$dir_pw_chrono_stl:docs",
"$dir_pw_chrono_threadx:docs",
"$dir_pw_chrono_zephyr:docs",
@@ -491,23 +550,30 @@ declare_args() {
"$dir_pw_cpu_exception_cortex_m:docs",
"$dir_pw_crypto:docs",
"$dir_pw_digital_io:docs",
+ "$dir_pw_digital_io_mcuxpresso:docs",
+ "$dir_pw_digital_io_rp2040:docs",
"$dir_pw_docgen:docs",
"$dir_pw_doctor:docs",
+ "$dir_pw_emu:docs",
"$dir_pw_env_setup:docs",
"$dir_pw_file:docs",
+ "$dir_pw_format:docs",
"$dir_pw_function:docs",
"$dir_pw_fuzzer:docs",
"$dir_pw_hdlc:docs",
"$dir_pw_hex_dump:docs",
"$dir_pw_i2c:docs",
+ "$dir_pw_i2c_linux:docs",
"$dir_pw_i2c_mcuxpresso:docs",
"$dir_pw_ide:docs",
"$dir_pw_interrupt:docs",
"$dir_pw_interrupt_cortex_m:docs",
+ "$dir_pw_interrupt_xtensa:docs",
"$dir_pw_interrupt_zephyr:docs",
"$dir_pw_intrusive_ptr:docs",
"$dir_pw_kvs:docs",
"$dir_pw_libc:docs",
+ "$dir_pw_libcxx:docs",
"$dir_pw_log:docs",
"$dir_pw_log_android:docs",
"$dir_pw_log_basic:docs",
@@ -521,6 +587,7 @@ declare_args() {
"$dir_pw_metric:docs",
"$dir_pw_minimal_cpp_stdlib:docs",
"$dir_pw_module:docs",
+ "$dir_pw_multibuf:docs",
"$dir_pw_multisink:docs",
"$dir_pw_package:docs",
"$dir_pw_perf_test:docs",
@@ -535,14 +602,19 @@ declare_args() {
"$dir_pw_ring_buffer:docs",
"$dir_pw_router:docs",
"$dir_pw_rpc:docs",
+ "$dir_pw_rpc_transport:docs",
"$dir_pw_rust:docs",
"$dir_pw_snapshot:docs",
"$dir_pw_software_update:docs",
"$dir_pw_span:docs",
"$dir_pw_spi:docs",
+ "$dir_pw_spi_mcuxpresso:docs",
"$dir_pw_status:docs",
"$dir_pw_stm32cube_build:docs",
"$dir_pw_stream:docs",
+ "$dir_pw_stream_shmem_mcuxpresso:docs",
+ "$dir_pw_stream_uart_linux:docs",
+ "$dir_pw_stream_uart_mcuxpresso:docs",
"$dir_pw_string:docs",
"$dir_pw_symbolizer:docs",
"$dir_pw_sync:docs",
@@ -553,12 +625,13 @@ declare_args() {
"$dir_pw_sync_threadx:docs",
"$dir_pw_sync_zephyr:docs",
"$dir_pw_sys_io:docs",
+ "$dir_pw_sys_io_ambiq_sdk:docs",
"$dir_pw_sys_io_arduino:docs",
"$dir_pw_sys_io_baremetal_lm3s6965evb:docs",
"$dir_pw_sys_io_baremetal_stm32f429:docs",
"$dir_pw_sys_io_emcraft_sf2:docs",
"$dir_pw_sys_io_mcuxpresso:docs",
- "$dir_pw_sys_io_pico:docs",
+ "$dir_pw_sys_io_rp2040:docs",
"$dir_pw_sys_io_stdio:docs",
"$dir_pw_sys_io_stm32cube:docs",
"$dir_pw_sys_io_zephyr:docs",
@@ -576,10 +649,12 @@ declare_args() {
"$dir_pw_tokenizer:docs",
"$dir_pw_tool:docs",
"$dir_pw_toolchain:docs",
+ "$dir_pw_toolchain_bazel:docs",
"$dir_pw_trace:docs",
"$dir_pw_trace_tokenized:docs",
"$dir_pw_transfer:docs",
"$dir_pw_unit_test:docs",
+ "$dir_pw_unit_test_zephyr:docs",
"$dir_pw_varint:docs",
"$dir_pw_watch:docs",
"$dir_pw_web:docs",
diff --git a/pw_build/gn.rst b/pw_build/gn.rst
new file mode 100644
index 000000000..d3ffc39bd
--- /dev/null
+++ b/pw_build/gn.rst
@@ -0,0 +1,1341 @@
+GN / Ninja
+==========
+The GN / Ninja build system is the primary build system used for upstream
+Pigweed development, and is the most tested and feature-rich build system
+Pigweed offers.
+
+This module's ``build.gn`` file contains a number of C/C++ ``config``
+declarations that are used by upstream Pigweed to set some architecture-agnostic
+compiler defaults. (See Pigweed's ``//BUILDCONFIG.gn``)
+
+``pw_build`` also provides several useful GN templates that are used throughout
+Pigweed.
+
+Building upstream Pigweed
+-------------------------
+See Pigweed's :ref:`docs-get-started-upstream` guide for a high-level introduction
+to using the GN build.
+
+Pigweed's root ``BUILD.gn`` file contains a variety of groups to help you
+control what parts of the project you'd like to build.
+
+* ``default``: Same as just calling ``ninja -C out``. Builds Pigweed's
+ documentation, recommended tests, and python linting, and static analysis.
+* ``extended_default``: Everything in ``default``, plus some other useful
+ configurations that are tested in CQ.
+* ``all``: Attempts to build everything in Pigweed. Note that ``pw package`` may
+ need to be used to enable some branches of the build.
+* ``docs``: Only build Pigweed's documentation.
+* ``stm32f429i``: Only build for the STMicroelectronics STM32F429I-DISC1 board.
+* ``host``: Only build for the host.
+
+There are a variety of other groups in the root ``BUILD.gn`` file that may be
+helpful for covering more areas of the build, or for reducing iteration time
+by only building a subset of the default build.
+
+Some currently broken groups are gated behind the ``pw_BUILD_BROKEN_GROUPS``
+build argument. You can set this to ``true`` using ``gn args out`` to try to
+build and debug known broken build configurations.
+
+Build system philosophies
+-------------------------
+While Pigweed's GN build is not hermetic, it strives to adhere to principles of
+`hermeticity <https://bazel.build/concepts/hermeticity>`_. Some guidelines to
+move towards the ideal of hermeticity include:
+
+* Only rely on pre-compiled tools provided by CIPD (or some other versioned,
+ pre-compiled binary distribution mechanism). This eliminates build artifact
+ differences caused by different tool versions or variations (e.g. same tool
+ version built with slightly different compilation flags).
+* Do not use absolute paths in Ninja commands. Typically, these appear when
+ using ``rebase_path("//path/to/my_script.py")``. Most of the time, Ninja
+ steps should be passed paths rebased relative to the build directory (i.e.
+ ``rebase_path("//path/to/my_script.py", root_build_dir)``). This ensures build
+ commands are the same across different machines.
+* Prevent produced artifacts from relying on or referencing system state. This
+ includes time stamps, writing absolute paths to generated artifacts, or
+ producing artifacts that reference system state in a way that prevents them
+ from working the same way on a different machine.
+* Isolate build actions to the build directory. In general, the build system
+ should not add or modify files outside of the build directory. This can cause
+ confusion to users, and makes the concept of a clean build more ambiguous.
+
+Target types
+------------
+.. code-block::
+
+ import("$dir_pw_build/target_types.gni")
+
+ pw_source_set("my_library") {
+ sources = [ "lib.cc" ]
+ }
+
+Pigweed defines wrappers around the four basic GN binary types ``source_set``,
+``executable``, ``static_library``, and ``shared_library``. These templates
+do several things:
+
+#. **Add default configs/deps**
+
+ Rather than binding the majority of compiler flags related to C++ standard,
+ cross-compilation, warning/error policy, etc. directly to toolchain
+ invocations, these flags are applied as configs to all ``pw_*`` C/C++ target
+ types. The primary motivations for this are to allow some targets to modify
+ the default set of flags when needed by specifying ``remove_configs``, and to
+ reduce the complexity of building novel toolchains.
+
+ Pigweed's global default configs are set in ``pw_build/default.gni``, and
+ individual platform-specific toolchains extend the list by appending to the
+ ``default_configs`` build argument.
+
+ Default deps were added to support polyfill, which has since been deprecated.
+ Default dependency functionality continues to exist for backwards
+ compatibility.
+
+#. **Optionally add link-time binding**
+
+ Some libraries like pw_assert and pw_log are borderline impossible to
+ implement well without introducing circular dependencies. One solution for
+ addressing this is to break apart the libraries into an interface with
+ minimal dependencies, and an implementation with the bulk of the
+ dependencies that would typically create dependency cycles. In order for the
+ implementation to be linked in, it must be added to the dependency tree of
+ linked artifacts (e.g. ``pw_executable``, ``pw_static_library``). Since
+ there's no way for the libraries themselves to just happily pull in the
+ implementation if someone depends on the interface, the implementation is
+ instead late-bound by adding it as a direct dependency of the final linked
+ artifact. This is all managed through ``pw_build_LINK_DEPS``, which is global
+ for each toolchain and applied to every ``pw_executable``,
+ ``pw_static_library``, and ``pw_shared_library``.
+
+#. **Apply a default visibility policy**
+
+ Projects can globally control the default visibility of pw_* target types by
+ specifying ``pw_build_DEFAULT_VISIBILITY``. This template applies that as the
+ default visibility for any pw_* targets that do not explicitly specify a
+ visibility.
+
+#. **Add source file names as metadata**
+
+ All source file names are collected as
+ `GN metadata <https://gn.googlesource.com/gn/+/main/docs/reference.md#metadata_collection>`_.
+ This list can be writen to a file at build time using ``generated_file``. The
+ primary use case for this is to generate a token database containing all the
+ source files. This allows :c:macro:`PW_ASSERT` to emit filename tokens even
+ though it can't add them to the elf file because of the reasons described at
+ :ref:`module-pw_assert-assert-api`.
+
+ .. note::
+ ``pw_source_files``, if not rebased will default to outputing module
+ relative paths from a ``generated_file`` target. This is likely not
+ useful. Adding a ``rebase`` argument to ``generated_file`` such as
+ ``rebase = root_build_dir`` will result in usable paths. For an example,
+ see ``//pw_tokenizer/database.gni``'s ``pw_tokenizer_filename_database``
+ template.
+
+The ``pw_executable`` template provides additional functionality around building
+complete binaries. As Pigweed is a collection of libraries, it does not know how
+its final targets are built. ``pw_executable`` solves this by letting each user
+of Pigweed specify a global executable template for their target, and have
+Pigweed build against it. This is controlled by the build variable
+``pw_executable_config.target_type``, specifying the name of the executable
+template for a project.
+
+In some uncommon cases, a project's ``pw_executable`` template definition may
+need to stamp out some ``pw_source_set``\s. Since a pw_executable template can't
+import ``$dir_pw_build/target_types.gni`` due to circular imports, it should
+import ``$dir_pw_build/cc_library.gni`` instead.
+
+.. tip::
+
+ Prefer to use ``pw_executable`` over plain ``executable`` targets to allow
+ cleanly building the same code for multiple target configs.
+
+Arguments
+^^^^^^^^^
+All of the ``pw_*`` target type overrides accept any arguments supported by
+the underlying native types, as they simply forward them through to the
+underlying target.
+
+Additionally, the following arguments are also supported:
+
+* **remove_configs**: (optional) A list of configs / config patterns to remove
+ from the set of default configs specified by the current toolchain
+ configuration.
+* **remove_public_deps**: (optional) A list of targets to remove from the set of
+ default public_deps specified by the current toolchain configuration.
+
+.. _module-pw_build-link-deps:
+
+Link-only deps
+--------------
+It may be necessary to specify additional link-time dependencies that may not be
+explicitly depended on elsewhere in the build. One example of this is a
+``pw_assert`` backend, which may need to leave out dependencies to avoid
+circular dependencies. Its dependencies need to be linked for executables and
+libraries, even if they aren't pulled in elsewhere.
+
+The ``pw_build_LINK_DEPS`` build arg is a list of dependencies to add to all
+``pw_executable``, ``pw_static_library``, and ``pw_shared_library`` targets.
+This should only be used as a last resort when dependencies cannot be properly
+expressed in the build.
+
+.. _module-pw_build-third-party:
+
+Third party libraries
+---------------------
+Pigweed includes build files for a selection of third-party libraries. For a
+given library, these include:
+
+* ``third_party/<library>/library.gni``: Declares build arguments like
+ ``dir_pw_third_party_<library>`` that default to ``""`` but can be set to the
+ absolute path of the library in order to use it.
+* ``third_party/<library>/BUILD.gn``: Describes how to build the library. This
+ should import ``third_party/<library>/library.gni`` and refer to source paths
+ relative to ``dir_pw_third_party_<library>``.
+
+To add or update GN build files for libraries that only offer Bazel build files,
+the Python script at ``pw_build/py/pw_build/generate_3p_gn.py`` may be used.
+
+.. note::
+ The ``generate_3p_gn.py`` script is experimental, and may not work on an
+ arbitrary Bazel library.
+
+To generate or update the GN offered by Pigweed from an Bazel upstream project,
+first create a ``third_party/<library>/repo.json`` file. This file should
+describe a single JSON object, with the following fields:
+
+* ``name``: String containg the project name.
+
+ .. code-block::
+
+ "name": "FuzzTest"
+
+* ``repos``: Object mapping Bazel repositories to library names.
+
+ .. code-block::
+
+ "repos": { "com_google_absl": "abseil-cpp" }
+
+* ``aliases``: Object mapping GN labels to other GN labels. In some cases, a
+ third party library may have a dependency on another library already supported
+ by Pigweed, but with a label that differs from what the script would generate.
+ This field allows those labels to be rewritten.
+
+ .. code-block::
+
+ "aliases": {
+ "$dir_pw_third_party/googletest:gtest": "$dir_pw_third_party/googletest"
+ }
+
+* ``add``: List of labels to existing GN configs. These will be added to every
+ target in the library.
+
+ .. code-block::
+
+ "add": [ "$dir_pw_third_party/re2/configs:disabled_warnings" ]
+
+* ``remove``: List of labels to default GN configs. These will be removed from
+ every target.
+
+ .. code-block::
+
+ "remove" = [ "$dir_pw_fuzzer:instrumentation" ]
+
+* ``allow_testonly``: Boolean indicating whether to generate GN for Bazel
+ targets marked test-only. Defaults to false.
+
+ .. code-block::
+
+ "allow_testonly": true
+
+* ``no_gn_check``: List of Bazel targets that violate ``gn check``'s
+ `rules`__. Third-party targets that do not conform can be excluded.
+
+ .. code-block::
+
+ "no_gn_check": [ "//fuzztest:regexp_dfa" ]
+
+* ``extra_files``: Object mapping additional files to create to Bazel targets
+ that create them. These targets will be passed to ``bazel run`` and their
+ output saved to the named file within ``third_party/<library>``. For example:
+
+ .. code-block::
+
+ "extra_files": {
+ "fuzztest.bazelrc": "@com_google_fuzztest//bazel:setup_configs"
+ }
+
+.. __: https://gn.googlesource.com/gn/+/main/docs/reference.md#cmd_check
+
+Python packages
+---------------
+GN templates for :ref:`Python build automation <docs-python-build>` are
+described in :ref:`module-pw_build-python`.
+
+.. toctree::
+ :hidden:
+
+ python
+
+
+.. _module-pw_build-cc_blob_library:
+
+pw_cc_blob_library
+------------------
+The ``pw_cc_blob_library`` template is useful for embedding binary data into a
+program. The template takes in a mapping of symbol names to file paths, and
+generates a set of C++ source and header files that embed the contents of the
+passed-in files as arrays of ``std::byte``.
+
+The blob byte arrays are constant initialized and are safe to access at any
+time, including before ``main()``.
+
+``pw_cc_blob_library`` is also available in the CMake build. It is provided by
+``pw_build/cc_blob_library.cmake``.
+
+Arguments
+^^^^^^^^^
+* ``blobs``: A list of GN scopes, where each scope corresponds to a binary blob
+ to be transformed from file to byte array. This is a required field. Blob
+ fields include:
+
+ * ``symbol_name``: The C++ symbol for the byte array.
+ * ``file_path``: The file path for the binary blob.
+ * ``linker_section``: If present, places the byte array in the specified
+ linker section.
+ * ``alignas``: If present, uses the specified string or integer verbatim in
+ the ``alignas()`` specifier for the byte array.
+
+* ``out_header``: The header file to generate. Users will include this file
+ exactly as it is written here to reference the byte arrays.
+* ``namespace``: An optional (but highly recommended!) C++ namespace to place
+ the generated blobs within.
+
+Example
+^^^^^^^
+**BUILD.gn**
+
+.. code-block::
+
+ pw_cc_blob_library("foo_bar_blobs") {
+ blobs: [
+ {
+ symbol_name: "kFooBlob"
+ file_path: "${target_out_dir}/stuff/bin/foo.bin"
+ },
+ {
+ symbol_name: "kBarBlob"
+ file_path: "//stuff/bin/bar.bin"
+ linker_section: ".bar_section"
+ },
+ ]
+ out_header: "my/stuff/foo_bar_blobs.h"
+ namespace: "my::stuff"
+ deps = [ ":generate_foo_bin" ]
+ }
+
+.. note:: If the binary blobs are generated as part of the build, be sure to
+ list them as deps to the pw_cc_blob_library target.
+
+**Generated Header**
+
+.. code-block::
+
+ #pragma once
+
+ #include <array>
+ #include <cstddef>
+
+ namespace my::stuff {
+
+ extern const std::array<std::byte, 100> kFooBlob;
+
+ extern const std::array<std::byte, 50> kBarBlob;
+
+ } // namespace my::stuff
+
+**Generated Source**
+
+.. code-block::
+
+ #include "my/stuff/foo_bar_blobs.h"
+
+ #include <array>
+ #include <cstddef>
+
+ #include "pw_preprocessor/compiler.h"
+
+ namespace my::stuff {
+
+ const std::array<std::byte, 100> kFooBlob = { ... };
+
+ PW_PLACE_IN_SECTION(".bar_section")
+ const std::array<std::byte, 50> kBarBlob = { ... };
+
+ } // namespace my::stuff
+
+.. _module-pw_build-facade:
+
+pw_facade
+---------
+In their simplest form, a :ref:`facade<docs-module-structure-facades>` is a GN
+build arg used to change a dependency at compile time. Pigweed targets configure
+these facades as needed.
+
+The ``pw_facade`` template bundles a ``pw_source_set`` with a facade build arg.
+This allows the facade to provide header files, compilation options or anything
+else a GN ``source_set`` provides.
+
+The ``pw_facade`` template declares two targets:
+
+* ``$target_name``: the public-facing ``pw_source_set``, with a ``public_dep``
+ on the backend
+* ``$target_name.facade``: target used by the backend to avoid circular
+ dependencies
+
+.. code-block::
+
+ # Declares ":foo" and ":foo.facade" GN targets
+ pw_facade("foo") {
+ backend = pw_log_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_foo/foo.h" ]
+ }
+
+Low-level facades like ``pw_assert`` cannot express all of their dependencies
+due to the potential for dependency cycles. Facades with this issue may require
+backends to place their implementations in a separate build target to be listed
+in ``pw_build_LINK_DEPS`` (see :ref:`module-pw_build-link-deps`). The
+``require_link_deps`` variable in ``pw_facade`` asserts that all specified build
+targets are present in ``pw_build_LINK_DEPS`` if the facade's backend variable
+is set.
+
+.. _module-pw_build-python-action:
+
+pw_python_action
+----------------
+.. seealso::
+ - :ref:`module-pw_build-python` for all of Pigweed's Python build GN templates.
+ - :ref:`docs-python-build` for details on how the GN Python build works.
+
+The ``pw_python_action`` template is a convenience wrapper around GN's `action
+function <https://gn.googlesource.com/gn/+/main/docs/reference.md#func_action>`_
+for running Python scripts. The main benefit it provides is resolution of GN
+target labels to compiled binary files. This allows Python scripts to be written
+independently of GN, taking only filesystem paths as arguments.
+
+Another convenience provided by the template is to allow running scripts without
+any outputs. Sometimes scripts run in a build do not directly produce output
+files, but GN requires that all actions have an output. ``pw_python_action``
+solves this by accepting a boolean ``stamp`` argument which tells it to create a
+placeholder output file for the action.
+
+Arguments
+^^^^^^^^^
+``pw_python_action`` accepts all of the arguments of a regular ``action``
+target. Additionally, it has some of its own arguments:
+
+* ``module``: Run the specified Python module instead of a script. Either
+ ``script`` or ``module`` must be specified, but not both.
+* ``capture_output``: Optional boolean. If true, script output is hidden unless
+ the script fails with an error. Defaults to true.
+* ``stamp``: Optional variable indicating whether to automatically create a
+ placeholder output file for the script. This allows running scripts without
+ specifying ``outputs``. If ``stamp`` is true, a generic output file is
+ used. If ``stamp`` is a file path, that file is used as a stamp file. Like any
+ output file, ``stamp`` must be in the build directory. Defaults to false.
+* ``environment``: Optional list of strings. Environment variables to set,
+ passed as NAME=VALUE strings.
+* ``working_directory``: Optional file path. When provided the current working
+ directory will be set to this location before the Python module or script is
+ run.
+* ``command_launcher``: Optional string. Arguments to prepend to the Python
+ command, e.g. ``'/usr/bin/fakeroot --'`` will run the Python script within a
+ fakeroot environment.
+* ``venv``: Optional gn target of the pw_python_venv that should be used to run
+ this action.
+* ``python_deps``: Extra dependencies that are required for running the Python
+ script for the ``action``. This must be used with ``module`` to specify the
+ build dependency of the ``module`` if it is user-defined code.
+* ``python_metadata_deps``: Extra dependencies that are ensured completed before
+ generating a Python package metadata manifest, not the overall Python script
+ ``action``. This should rarely be used by non-Pigweed code.
+
+.. _module-pw_build-python-action-test:
+
+pw_python_action_test
+---------------------
+The ``pw_python_action_test`` extends :ref:`module-pw_build-python-action` to
+create a test that is run by a Python script, and its associated test metadata.
+
+Include action tests in the :ref:`module-pw_unit_test-pw_test_group` to produce
+the JSON metadata that :ref:`module-pw_build-test-info` adds.
+
+This template derives several additional targets:
+
+* ``<target_name>.metadata`` produces the test metadata when included in a
+ ``pw_test_group``. This metadata includes the Ninja target that runs the test.
+* If``action`` is not provided as a label, ``<target_name>.script`` wraps a
+ ``pw_python_action`` to run the test as a standalone ``pw_python_package``.
+* ``<target_name>.group`` creates a ``pw_python_group`` in order to apply tools,
+ e.g. linters, to the standalone package.
+* ``<target_name>.lib`` is an empty group for compatibility with
+ ``pw_test_group``.
+* ``<target_name>.run`` invokes the test.
+
+Targets defined using this template will produce test metadata with a
+``test_type`` of "action_test" and a ``ninja_target`` value that will invoke the
+test when passed to Ninja, i.e. ``ninja -C out <ninja_target>``.
+
+Arguments
+^^^^^^^^^
+``pw_python_action_test`` accepts the following arguments:
+
+* All of the arguments of :ref:`module-pw_unit_test-pw_test`.
+* ``action``: An optional string or scope. If a string, this should be a label
+ to a ``pw_python_action`` target that performs the test. If a scope, this has
+ the same meaning as for ``pw_python_script``.
+* Optionally, the ``test_type`` and ``extra_metadata`` arguments of the
+ :ref:`module-pw_build-test-info` template.
+* Optionally, all of the arguments of the :ref:`module-pw_build-python-action`
+ template except ``module``, ``capture_output``, ``stamp``, and
+ ``python_metadata_deps``.
+* Optionally, all of the arguments of the ``pw_python_package`` template except
+ ``setup``, ``generate_setup``, ``tests``, ``python_test_deps``, and
+ ``proto_library``.
+
+.. _module-pw_build-test-info:
+
+pw_test_info
+------------
+``pw_test_info`` generates metadata describing tests. To produce a JSON file
+containing this metadata:
+
+#. For new modules, add a :ref:`module-pw_unit_test-pw_test_group` to the
+ BUILD.gn file. All modules are required to have a ``tests`` target.
+#. Include one or more tests or test groups via ``tests`` or ``group_deps``,
+ respectively, in the ``pw_test_group``.
+#. Set ``output_metadata`` to ``true`` in the ``pw_test_group`` definition.
+
+This template does not typically need to be used directly, unless adding new
+types of tests. It is typically used by other templates, such as the
+:ref:`module-pw_unit_test-pw_test` and the
+:ref:`module-pw_unit_test-pw_test_group`.
+
+Arguments
+^^^^^^^^^
+* ``test_type``: One of "test_group", "unit_test", "action_test", "perf_test",
+ or "fuzz_test".
+* ``test_name``: Name of the test. Defaults to the target name.
+* ``build_label``: GN label for the test. Defaults to the test name.
+* ``extra_metadata``: Additional variables to add to the metadata.
+
+Specific test templates add additional details using ``extra_metadata``. For
+example:
+
+* The :ref:`module-pw_unit_test-pw_test_group` includes its collected list of
+ tests and test groups as ``deps``.
+* The :ref:`module-pw_unit_test-pw_test` and the
+ :ref:`module-pw_perf_test-pw_perf_test` includes the ``test_directory``
+ that contains the test executable.
+* The :ref:`module-pw_build-python-action-test` includes the Ninja target that
+ can be used to invoke the Python action and run the test.
+
+Example
+^^^^^^^
+Let ``//my_module/BUILD.gn`` contain the following:
+
+.. code-block::
+
+ import("$dir_pw_build/python_action_test.gni")
+ import("$dir_pw_perf_test/perf_test.gni")
+ import("$dir_pw_unit_test/test.gni")
+
+ pw_test("my_unit_test") {
+ sources = [ ... ]
+ deps = [ ... ]
+ }
+
+ pw_python_action_test("my_action_test") {
+ script = [ ... ]
+ args = [ ... ]
+ deps = [ ... ]
+ }
+
+ pw_python_action_test("my_integration_test") {
+ script = [ ... ]
+ args = [ ... ]
+ deps = [ ... ]
+ tags = [ "integration" ]
+ }
+
+ pw_perf_test("my_perf_test") {
+ sources = [ ... ]
+ deps = [ ... ]
+ }
+
+ pw_test_group("tests") {
+ tests = [
+ ":my_unit_test",
+ ":my_action_test",
+ ":my_integration_test",
+ ]
+ }
+
+Let `//BUILD.gn`` contain the following:
+
+.. code-block::
+
+ import("$dir_pw_unit_test/test.gni")
+
+ group("run_tests") {
+ deps = [ ":my_module_tests(//targets/my_targets:my_toolchain)" ]
+ }
+
+ pw_test_group("my_module_tests") {
+ group_deps = [ "//my_module:tests" ]
+ output_metadata = true
+ }
+
+Then running ``gn gen out`` will produce the following JSON file at
+``out/my_toolchain/my_module_tests.testinfo.json``:
+
+.. code-block:: json
+
+ [
+ {
+ "build_label": "//my_module:my_unit_test",
+ "test_directory": "my_toolchain/obj/my_module/test",
+ "test_name": "my_unit_test",
+ "test_type": "unit_test"
+ },
+ {
+ "build_label": "//my_module:my_action_test",
+ "ninja_target": "my_toolchain/obj/my_module/my_action_test.run.stamp",
+ "test_name": "my_action_test",
+ "test_type": "action_test"
+ },
+ {
+ "build_label": "//my_module:my_integration_test",
+ "ninja_target": "my_toolchain/obj/my_module/my_integration_test.run.stamp",
+ "tags": [
+ "integration"
+ ],
+ "test_name": "my_integration_test",
+ "test_type": "action_test"
+ },
+ {
+ "build_label": "//my_module:my_perf_test",
+ "test_directory": "my_toolchain/obj/my_module/test",
+ "test_name": "my_perf_test",
+ "test_type": "perf_test"
+ },
+ {
+ "build_label": "//my_module:tests",
+ "deps": [
+ "//my_module:my_unit_test",
+ "//my_module:my_action_test",
+ "//my_module:my_integration_test",
+ ],
+ "test_name": "my_module/tests",
+ "test_type": "test_group"
+ },
+ {
+ "build_label": "//:my_module_tests",
+ "deps": [
+ "//my_module:tests",
+ ],
+ "test_name": "my_module_tests",
+ "test_type": "test_group"
+ }
+ ]
+
+.. _module-pw_build-python-action-expressions:
+
+Expressions
+^^^^^^^^^^^
+``pw_python_action`` evaluates expressions in ``args``, the arguments passed to
+the script. These expressions function similarly to generator expressions in
+CMake. Expressions may be passed as a standalone argument or as part of another
+argument. A single argument may contain multiple expressions.
+
+Generally, these expressions are used within templates rather than directly in
+BUILD.gn files. This allows build code to use GN labels without having to worry
+about converting them to files.
+
+.. note::
+
+ We intend to replace these expressions with native GN features when possible.
+ See `b/234886742 <http://issuetracker.google.com/234886742>`_.
+
+The following expressions are supported:
+
+.. describe:: <TARGET_FILE(gn_target)>
+
+ Evaluates to the output file of the provided GN target. For example, the
+ expression
+
+ .. code-block::
+
+ "<TARGET_FILE(//foo/bar:static_lib)>"
+
+ might expand to
+
+ .. code-block::
+
+ "/home/User/project_root/out/obj/foo/bar/static_lib.a"
+
+ ``TARGET_FILE`` parses the ``.ninja`` file for the GN target, so it should
+ always find the correct output file, regardless of the toolchain's or target's
+ configuration. Some targets, such as ``source_set`` and ``group`` targets, do
+ not have an output file, and attempting to use ``TARGET_FILE`` with them
+ results in an error.
+
+ ``TARGET_FILE`` only resolves GN target labels to their outputs. To resolve
+ paths generally, use the standard GN approach of applying the
+ ``rebase_path(path, root_build_dir)`` function. This function
+ converts the provided GN path or list of paths to be relative to the build
+ directory, from which all build commands and scripts are executed.
+
+.. describe:: <TARGET_FILE_IF_EXISTS(gn_target)>
+
+ ``TARGET_FILE_IF_EXISTS`` evaluates to the output file of the provided GN
+ target, if the output file exists. If the output file does not exist, the
+ entire argument that includes this expression is omitted, even if there is
+ other text or another expression.
+
+ For example, consider this expression:
+
+ .. code-block::
+
+ "--database=<TARGET_FILE_IF_EXISTS(//alpha/bravo)>"
+
+ If the ``//alpha/bravo`` target file exists, this might expand to the
+ following:
+
+ .. code-block::
+
+ "--database=/home/User/project/out/obj/alpha/bravo/bravo.elf"
+
+ If the ``//alpha/bravo`` target file does not exist, the entire
+ ``--database=`` argument is omitted from the script arguments.
+
+.. describe:: <TARGET_OBJECTS(gn_target)>
+
+ Evaluates to the object files of the provided GN target. Expands to a separate
+ argument for each object file. If the target has no object files, the argument
+ is omitted entirely. Because it does not expand to a single expression, the
+ ``<TARGET_OBJECTS(...)>`` expression may not have leading or trailing text.
+
+ For example, the expression
+
+ .. code-block::
+
+ "<TARGET_OBJECTS(//foo/bar:a_source_set)>"
+
+ might expand to multiple separate arguments:
+
+ .. code-block::
+
+ "/home/User/project_root/out/obj/foo/bar/a_source_set.file_a.cc.o"
+ "/home/User/project_root/out/obj/foo/bar/a_source_set.file_b.cc.o"
+ "/home/User/project_root/out/obj/foo/bar/a_source_set.file_c.cc.o"
+
+Example
+^^^^^^^
+.. code-block::
+
+ import("$dir_pw_build/python_action.gni")
+
+ pw_python_action("postprocess_main_image") {
+ script = "py/postprocess_binary.py"
+ args = [
+ "--database",
+ rebase_path("my/database.csv", root_build_dir),
+ "--binary=<TARGET_FILE(//firmware/images:main)>",
+ ]
+ stamp = true
+ }
+
+.. _module-pw_build-evaluate-path-expressions:
+
+pw_evaluate_path_expressions
+----------------------------
+It is not always feasible to pass information to a script through command line
+arguments. If a script requires a large amount of input data, writing to a file
+is often more convenient. However, doing so bypasses ``pw_python_action``'s GN
+target label resolution, preventing such scripts from working with build
+artifacts in a build system-agnostic manner.
+
+``pw_evaluate_path_expressions`` is designed to address this use case. It takes
+a list of input files and resolves target expressions within them, modifying the
+files in-place.
+
+Refer to ``pw_python_action``'s :ref:`module-pw_build-python-action-expressions`
+section for the list of supported expressions.
+
+.. note::
+
+ ``pw_evaluate_path_expressions`` is typically used as an intermediate
+ sub-target of a larger template, rather than a standalone build target.
+
+Arguments
+^^^^^^^^^
+* ``files``: A list of scopes, each containing a ``source`` file to process and
+ a ``dest`` file to which to write the result.
+
+Example
+^^^^^^^
+The following template defines an executable target which additionally outputs
+the list of object files from which it was compiled, making use of
+``pw_evaluate_path_expressions`` to resolve their paths.
+
+.. code-block::
+
+ import("$dir_pw_build/evaluate_path_expressions.gni")
+
+ template("executable_with_artifacts") {
+ executable("${target_name}.exe") {
+ sources = invoker.sources
+ if defined(invoker.deps) {
+ deps = invoker.deps
+ }
+ }
+
+ _artifacts_input = "$target_gen_dir/${target_name}_artifacts.json.in"
+ _artifacts_output = "$target_gen_dir/${target_name}_artifacts.json"
+ _artifacts = {
+ binary = "<TARGET_FILE(:${target_name}.exe)>"
+ objects = "<TARGET_OBJECTS(:${target_name}.exe)>"
+ }
+ write_file(_artifacts_input, _artifacts, "json")
+
+ pw_evaluate_path_expressions("${target_name}.evaluate") {
+ files = [
+ {
+ source = _artifacts_input
+ dest = _artifacts_output
+ },
+ ]
+ }
+
+ group(target_name) {
+ deps = [
+ ":${target_name}.exe",
+ ":${target_name}.evaluate",
+ ]
+ }
+ }
+
+.. _module-pw_build-pw_exec:
+
+pw_exec
+-------
+``pw_exec`` allows for execution of arbitrary programs. It is a wrapper around
+``pw_python_action`` but allows for specifying the program to execute.
+
+.. note::
+
+ Prefer to use ``pw_python_action`` instead of calling out to shell
+ scripts, as the Python will be more portable. ``pw_exec`` should generally
+ only be used for interacting with legacy/existing scripts.
+
+Arguments
+^^^^^^^^^
+* ``program``: The program to run. Can be a full path or just a name (in which
+ case $PATH is searched).
+* ``args``: Optional list of arguments to the program.
+* ``deps``: Dependencies for this target.
+* ``public_deps``: Public dependencies for this target. In addition to outputs
+ from this target, outputs generated by public dependencies can be used as
+ inputs from targets that depend on this one. This is not the case for private
+ deps.
+* ``inputs``: Optional list of build inputs to the program.
+* ``outputs``: Optional list of artifacts produced by the program's execution.
+* ``env``: Optional list of key-value pairs defining environment variables for
+ the program.
+* ``env_file``: Optional path to a file containing a list of newline-separated
+ key-value pairs defining environment variables for the program.
+* ``args_file``: Optional path to a file containing additional positional
+ arguments to the program. Each line of the file is appended to the
+ invocation. Useful for specifying arguments from GN metadata.
+* ``skip_empty_args``: If args_file is provided, boolean indicating whether to
+ skip running the program if the file is empty. Used to avoid running
+ commands which error when called without arguments.
+* ``capture_output``: If true, output from the program is hidden unless the
+ program exits with an error. Defaults to true.
+* ``working_directory``: The working directory to execute the subprocess with.
+ If not specified it will not be set and the subprocess will have whatever
+ the parent current working directory is.
+* ``venv``: Python virtualenv to pass along to the underlying
+ :ref:`module-pw_build-pw_python_action`.
+* ``visibility``: GN visibility to apply to the underlying target.
+
+Example
+^^^^^^^
+.. code-block::
+
+ import("$dir_pw_build/exec.gni")
+
+ pw_exec("hello_world") {
+ program = "/bin/sh"
+ args = [
+ "-c",
+ "echo hello \$WORLD",
+ ]
+ env = [
+ "WORLD=world",
+ ]
+ }
+
+pw_input_group
+--------------
+``pw_input_group`` defines a group of input files which are not directly
+processed by the build but are still important dependencies of later build
+steps. This is commonly used alongside metadata to propagate file dependencies
+through the build graph and force rebuilds on file modifications.
+
+For example ``pw_docgen`` defines a ``pw_doc_group`` template which outputs
+metadata from a list of input files. The metadata file is not actually part of
+the build, and so changes to any of the input files do not trigger a rebuild.
+This is problematic, as targets that depend on the metadata should rebuild when
+the inputs are modified but GN cannot express this dependency.
+
+``pw_input_group`` solves this problem by allowing a list of files to be listed
+in a target that does not output any build artifacts, causing all dependent
+targets to correctly rebuild.
+
+Arguments
+^^^^^^^^^
+``pw_input_group`` accepts all arguments that can be passed to a ``group``
+target, as well as requiring one extra:
+
+* ``inputs``: List of input files.
+
+Example
+^^^^^^^
+.. code-block::
+
+ import("$dir_pw_build/input_group.gni")
+
+ pw_input_group("foo_metadata") {
+ metadata = {
+ files = [
+ "x.foo",
+ "y.foo",
+ "z.foo",
+ ]
+ }
+ inputs = metadata.files
+ }
+
+Targets depending on ``foo_metadata`` will rebuild when any of the ``.foo``
+files are modified.
+
+pw_zip
+------
+``pw_zip`` is a target that allows users to zip up a set of input files and
+directories into a single output ``.zip`` file—a simple automation of a
+potentially repetitive task.
+
+Arguments
+^^^^^^^^^
+* ``inputs``: List of source files as well as the desired relative zip
+ destination. See below for the input syntax.
+* ``dirs``: List of entire directories to be zipped as well as the desired
+ relative zip destination. See below for the input syntax.
+* ``output``: Filename of output ``.zip`` file.
+* ``deps``: List of dependencies for the target.
+
+Input Syntax
+^^^^^^^^^^^^
+Inputs all need to follow the correct syntax:
+
+#. Path to source file or directory. Directories must end with a ``/``.
+#. The delimiter (defaults to ``>``).
+#. The desired destination of the contents within the ``.zip``. Must start
+ with ``/`` to indicate the zip root. Any number of subdirectories are
+ allowed. If the source is a file it can be put into any subdirectory of the
+ root. If the source is a file, the zip copy can also be renamed by ending
+ the zip destination with a filename (no trailing ``/``).
+
+Thus, it should look like the following: ``"[source file or dir] > /"``.
+
+Example
+^^^^^^^
+Let's say we have the following structure for a ``//source/`` directory:
+
+.. code-block::
+
+ source/
+ ├── file1.txt
+ ├── file2.txt
+ ├── file3.txt
+ └── some_dir/
+ ├── file4.txt
+ └── some_other_dir/
+ └── file5.txt
+
+And we create the following build target:
+
+.. code-block::
+
+ import("$dir_pw_build/zip.gni")
+
+ pw_zip("target_name") {
+ inputs = [
+ "//source/file1.txt > /", # Copied to the zip root dir.
+ "//source/file2.txt > /renamed.txt", # File renamed.
+ "//source/file3.txt > /bar/", # File moved to the /bar/ dir.
+ ]
+
+ dirs = [
+ "//source/some_dir/ > /bar/some_dir/", # All /some_dir/ contents copied
+ # as /bar/some_dir/.
+ ]
+
+ # Note on output: if the specific output directory isn't defined
+ # (such as output = "zoo.zip") then the .zip will output to the
+ # same directory as the BUILD.gn file that called the target.
+ output = "//$target_out_dir/foo.zip" # Where the foo.zip will end up
+ }
+
+This will result in a ``.zip`` file called ``foo.zip`` stored in
+``//$target_out_dir`` with the following structure:
+
+.. code-block::
+
+ foo.zip
+ ├── bar/
+ │ ├── file3.txt
+ │ └── some_dir/
+ │ ├── file4.txt
+ │ └── some_other_dir/
+ │ └── file5.txt
+ ├── file1.txt
+ └── renamed.txt
+
+.. _module-pw_build-relative-source-file-names:
+
+pw_relative_source_file_names
+-----------------------------
+This template recursively walks the listed dependencies and collects the names
+of all the headers and source files required by the targets, and then transforms
+them such that they reflect the ``__FILE__`` when pw_build's ``relative_paths``
+config is applied. This is primarily intended for side-band generation of
+pw_tokenizer tokens so file name tokens can be utilized in places where
+pw_tokenizer is unable to embed token information as part of C/C++ compilation.
+
+This template produces a JSON file containing an array of strings (file paths
+with ``-ffile-prefix-map``-like transformations applied) that can be used to
+:ref:`generate a token database <module-pw_tokenizer-database-creation>`.
+
+Arguments
+^^^^^^^^^
+* ``deps``: A required list of targets to recursively extract file names from.
+* ``outputs``: A required array with a single element: the path to write the
+ final JSON file to.
+
+Example
+^^^^^^^
+Let's say we have the following project structure:
+
+.. code-block::
+
+ project root
+ ├── foo/
+ │ ├── foo.h
+ │ └── foo.cc
+ ├── bar/
+ │ ├── bar.h
+ │ └── bar.cc
+ ├── unused/
+ │ ├── unused.h
+ │ └── unused.cc
+ └── main.cc
+
+And a BUILD.gn at the root:
+
+.. code-block::
+
+ pw_source_set("bar") {
+ public_configs = [ ":bar_headers" ]
+ public = [ "bar/bar.h" ]
+ sources = [ "bar/bar.cc" ]
+ }
+
+ pw_source_set("foo") {
+ public_configs = [ ":foo_headers" ]
+ public = [ "foo/foo.h" ]
+ sources = [ "foo/foo.cc" ]
+ deps = [ ":bar" ]
+ }
+
+
+ pw_source_set("unused") {
+ public_configs = [ ":unused_headers" ]
+ public = [ "unused/unused.h" ]
+ sources = [ "unused/unused.cc" ]
+ deps = [ ":bar" ]
+ }
+
+ pw_executable("main") {
+ sources = [ "main.cc" ]
+ deps = [ ":foo" ]
+ }
+
+ pw_relative_source_file_names("main_source_files") {
+ deps = [ ":main" ]
+ outputs = [ "$target_gen_dir/main_source_files.json" ]
+ }
+
+The json file written to `out/gen/main_source_files.json` will contain:
+
+.. code-block::
+
+ [
+ "bar/bar.cc",
+ "bar/bar.h",
+ "foo/foo.cc",
+ "foo/foo.h",
+ "main.cc"
+ ]
+
+Since ``unused`` isn't a transitive dependency of ``main``, its source files
+are not included. Similarly, even though ``bar`` is not a direct dependency of
+``main``, its source files *are* included because ``foo`` brings in ``bar`` as
+a transitive dependency.
+
+Note how the file paths in this example are relative to the project root rather
+than being absolute paths (e.g. ``/home/user/ralph/coding/my_proj/main.cc``).
+This is a result of transformations applied to strip absolute pathing prefixes,
+matching the behavior of pw_build's ``$dir_pw_build:relative_paths`` config.
+
+Build time errors: pw_error and pw_build_assert
+-----------------------------------------------
+In Pigweed's complex, multi-toolchain GN build it is not possible to build every
+target in every configuration. GN's ``assert`` statement is not ideal for
+enforcing the correct configuration because it may prevent the GN build files or
+targets from being referred to at all, even if they aren't used.
+
+The ``pw_error`` GN template results in an error if it is executed during the
+build. These error targets can exist in the build graph, but cannot be depended
+on without an error.
+
+``pw_build_assert`` evaluates to a ``pw_error`` if a condition fails or nothing
+(an empty group) if the condition passes. Targets can add a dependency on a
+``pw_build_assert`` to enforce a condition at build time.
+
+The templates for build time errors are defined in ``pw_build/error.gni``.
+
+Generate code coverage reports: ``pw_coverage_report``
+------------------------------------------------------
+Pigweed supports generating coverage reports, in a variety of formats, for C/C++
+code using the ``pw_coverage_report`` GN template.
+
+Coverage Caveats
+^^^^^^^^^^^^^^^^
+There are currently two code coverage caveats when enabled:
+
+#. Coverage reports are only populated based on host tests that use a ``clang``
+ toolchain.
+
+#. Coverage reports will only show coverage information for headers included in
+ a test binary.
+
+These two caveats mean that all device-specific code that cannot be compiled for
+and run on the host will not be able to have reports generated for them, and
+that the existence of these files will not appear in any coverage report.
+
+Try to ensure that your code can be written in a way that it can be compiled
+into a host test for the purpose of coverage reporting, although this is
+sometimes impossible due to requiring hardware-specific APIs to be available.
+
+Coverage Instrumentation
+^^^^^^^^^^^^^^^^^^^^^^^^
+For the ``pw_coverage_report`` to generate meaningful output, you must ensure
+that it is invoked by a toolchain that instruments tests for code coverage
+collection and output.
+
+Instrumentation is controlled by two GN build arguments:
+
+- ``pw_toolchain_COVERAGE_ENABLED`` being set to ``true``.
+- ``pw_toolchain_PROFILE_SOURCE_FILES`` is an optional argument that provides a
+ list of source files to selectively collect coverage.
+
+.. note::
+
+ It is possible to also instrument binaries for UBSAN, ASAN, or TSAN at the
+ same time as coverage. However, TSAN will find issues in the coverage
+ instrumentation code and fail to properly build.
+
+This can most easily be done by using the ``host_clang_coverage`` toolchain
+provided in :ref:`module-pw_toolchain`, but you can also create custom
+toolchains that manually set these GN build arguments as well.
+
+``pw_coverage_report``
+^^^^^^^^^^^^^^^^^^^^^^
+``pw_coverage_report`` is basically a GN frontend to the ``llvm-cov``
+`tool <https://llvm.org/docs/CommandGuide/llvm-cov.html>`_ that can be
+integrated into the normal build.
+
+It can be found at ``pw_build/coverage_report.gni`` and is available through
+``import("$dir_pw_build/coverage_report.gni")``.
+
+The supported report formats are:
+
+- ``text``: A text representation of the code coverage report. This
+ format is not suitable for further machine manipulation and is instead only
+ useful for cases where a human needs to interpret the report. The text format
+ provides a nice summary, but if you desire to drill down into the coverage
+ details more, please consider using ``html`` instead.
+
+ - This is equivalent to ``llvm-cov show --format text`` and similar to
+ ``llvm-cov report``.
+
+- ``html``: A static HTML site that provides an overall coverage summary and
+ per-file information. This format is not suitable for further machine
+ manipulation and is instead only useful for cases where a human needs to
+ interpret the report.
+
+ - This is equivalent to ``llvm-cov show --format html``.
+
+- ``lcov``: A machine-friendly coverage report format. This format is not human-
+ friendly. If that is necessary, use ``text`` or ``html`` instead.
+
+ - This is equivalent to ``llvm-cov export --format lcov``.
+
+- ``json``: A machine-friendly coverage report format. This format is not human-
+ friendly. If that is necessary, use ``text`` or ``html`` instead.
+
+ - This is equivalent to ``llvm-cov export --format json``.
+
+Arguments
+"""""""""
+There are three classes of ``template`` arguments: build, coverage, and test.
+
+**Build Arguments:**
+
+- ``enable_if`` (optional): Conditionally activates coverage report generation when set to
+ a boolean expression that evaluates to ``true``. This can be used to allow
+ project builds to conditionally enable or disable coverage reports to minimize
+ work needed for certain build configurations.
+
+- ``failure_mode`` (optional/unstable): Specify the failure mode for
+ ``llvm-profdata`` (used to merge inidividual profraw files from ``pw_test``
+ runs). Available options are ``"any"`` (default) or ``"all"``.
+
+ - This should be considered an unstable/deprecated argument that should only
+ be used as a last resort to get a build working again. Using
+ ``failure_mode = "all"`` usually indicates that there are underlying
+ problems in the build or test infrastructure that should be independently
+ resolved. Please reach out to the Pigweed team for assistance.
+
+**Coverage Arguments:**
+
+- ``filter_paths`` (optional): List of file paths to include when generating the
+ coverage report. These cannot be regular expressions, but can be concrete file
+ or folder paths. Folder paths will allow all files in that directory or any
+ recursive child directory.
+
+ - These are passed to ``llvm-cov`` by the optional trailing positional
+ ``[SOURCES]`` arguments.
+
+- ``ignore_filename_patterns`` (optional): List of file path regular expressions
+ to ignore when generating the coverage report.
+
+ - These are passed to ``llvm-cov`` via ``--ignore-filename-regex`` named
+ parameters.
+
+**Test Arguments (one of these is required to be provided):**
+
+- ``tests``: A list of ``pw_test`` :ref:`targets<module-pw_unit_test-pw_test>`.
+
+- ``group_deps``: A list of ``pw_test_group``
+ :ref:`targets<module-pw_unit_test-pw_test_group>`.
+
+.. note::
+
+ ``tests`` and ``group_deps`` are treated exactly the same by
+ ``pw_coverage_report``, so it is not that important to ensure they are used
+ properly.
+
+Target Expansion
+""""""""""""""""
+``pw_coverage_report(<target_name>)`` expands to one concrete target for each
+report format.
+
+- ``<target_name>.text``: Generates the ``text`` coverage report.
+
+- ``<target_name>.html``: Generates the ``html`` coverage report.
+
+- ``<target_name>.lcov``: Generates the ``lcov`` coverage report.
+
+- ``<target_name>.json``: Generates the ``json`` coverage report.
+
+To use any of these targets, you need only to add a dependency on the desired
+target somewhere in your build.
+
+There is also a ``<target_name>`` target generated that is a ``group`` that adds
+a dependency on all of the format-specific targets listed above.
+
+.. note::
+ These targets are always available, even when the toolchain executing the
+ target does not support coverage or coverage is not enabled. In these cases,
+ the targets are set to empty groups.
+
+Coverage Output
+^^^^^^^^^^^^^^^
+Coverage reports are currently generated and placed into the build output
+directory associated with the path to the GN file where the
+``pw_coverage_report`` is used in a subfolder named
+``<target_name>.<report_type>``.
+
+.. note::
+
+ Due to limitations with telling GN the entire output of coverage reports
+ (stemming from per-source-file generation for HTML and text representations),
+ it is not as simple as using GN's built-in ``copy`` to be able to move these
+ coverage reports to another output location. However, it seems possible to add
+ a target that can use Python to copy the entire output directory.
+
+Improved Ninja interface
+------------------------
+Ninja includes a basic progress display, showing in a single line the number of
+targets finished, the total number of targets, and the name of the most recent
+target it has either started or finished.
+
+For additional insight into the status of the build, Pigweed includes a Ninja
+wrapper, ``pw-wrap-ninja``, that displays additional real-time information about
+the progress of the build. The wrapper is invoked the same way you'd normally
+invoke Ninja:
+
+.. code-block:: sh
+
+ pw-wrap-ninja -C out
+
+The script lists the progress of the build, as well as the list of targets that
+Ninja is currently building, along with a timer that measures how long each
+target has been building for:
+
+.. code-block::
+
+ [51.3s] Building [8924/10690] ...
+ [10.4s] c++ pw_strict_host_clang_debug/obj/pw_string/string_test.lib.string_test.cc.o
+ [ 9.5s] ACTION //pw_console/py:py.lint.mypy(//pw_build/python_toolchain:python)
+ [ 9.4s] ACTION //pw_console/py:py.lint.pylint(//pw_build/python_toolchain:python)
+ [ 6.1s] clang-tidy ../pw_log_rpc/log_service.cc
+ [ 6.1s] clang-tidy ../pw_log_rpc/log_service_test.cc
+ [ 6.1s] clang-tidy ../pw_log_rpc/rpc_log_drain.cc
+ [ 6.1s] clang-tidy ../pw_log_rpc/rpc_log_drain_test.cc
+ [ 5.4s] c++ pw_strict_host_clang_debug/obj/BUILD_DIR/pw_strict_host_clang_debug/gen/pw...
+ ... and 109 more
+
+This allows you to, at a glance, know what Ninja's currently building, which
+targets are bottlenecking the rest of the build, and which targets are taking
+an unusually long time to complete.
+
+``pw-wrap-ninja`` includes other useful functionality as well. The
+``--write-trace`` option writes a build trace to the specified path, which can
+be viewed in the `Perfetto UI <https://ui.perfetto.dev/>`_, or via Chrome's
+built-in ``chrome://tracing`` tool.
diff --git a/pw_build/gn_internal/build_target.gni b/pw_build/gn_internal/build_target.gni
index 149c87eac..10b8ce25a 100644
--- a/pw_build/gn_internal/build_target.gni
+++ b/pw_build/gn_internal/build_target.gni
@@ -14,6 +14,8 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/defaults.gni")
+
declare_args() {
# Additional build targets to add as dependencies for pw_executable,
# pw_static_library, and pw_shared_library targets. The
@@ -85,6 +87,7 @@ template("pw_internal_build_target") {
_builtin_target_types = [
"executable",
"rust_library",
+ "rust_proc_macro",
"shared_library",
"source_set",
"static_library",
@@ -103,9 +106,6 @@ template("pw_internal_build_target") {
}
target(invoker.underlying_target_type, target_name) {
- # TODO(b/260111641): This import is terrible, and breaks typical Pigweed GN
- # build arg naming patterns.
- import("$dir_pw_build/defaults.gni")
forward_variables_from(
invoker,
"*",
@@ -120,36 +120,44 @@ template("pw_internal_build_target") {
}
}
+ _default_configs =
+ filter_exclude(pw_build_INTERNAL_DEFAULTS.default_configs,
+ pw_build_INTERNAL_DEFAULTS.remove_default_configs)
+ if (!defined(remove_configs)) {
+ remove_configs = []
+ }
+
+ # Check if configs is already defined.
+ # configs will already exist if set_defaults() is used.
if (!defined(configs)) {
configs = []
}
- if (defined(pw_build_defaults.configs)) {
- configs += pw_build_defaults.configs
- }
- if (defined(remove_configs)) {
- if (remove_configs != [] && remove_configs[0] == "*") {
- configs = []
- } else {
- configs -= filter_include(configs, remove_configs)
- }
- }
+
+ # `configs` was not forwarded initially to support flag ordering.
+ configs += filter_exclude(_default_configs, remove_configs)
+
+ # Add the public_deps from the actual target last so they are prioritized.
+ # This set of public_deps MUST be added last because it affects flag
+ # ordering. If this list is added later, flags from the default list will
+ # take precedence.
if (defined(invoker.configs)) {
configs += invoker.configs
}
- if (defined(pw_build_defaults.public_deps)) {
- public_deps = pw_build_defaults.public_deps
- } else {
- public_deps = []
- }
- if (defined(remove_public_deps)) {
- if (remove_public_deps != [] && remove_public_deps[0] == "*") {
- public_deps = []
- } else {
- public_deps += remove_public_deps
- public_deps -= remove_public_deps
- }
+ _default_public_deps =
+ filter_exclude(pw_build_INTERNAL_DEFAULTS.default_public_deps,
+ pw_build_INTERNAL_DEFAULTS.remove_default_public_deps)
+ if (!defined(remove_public_deps)) {
+ remove_public_deps = []
}
+
+ # `public_deps` was not forwarded initially to support flag ordering.
+ public_deps = filter_exclude(_default_public_deps, remove_public_deps)
+
+ # Add the public_deps from the actual target last so they are prioritized.
+ # This set of public_deps MUST be added last because it affects flag
+ # ordering. If this list is added later, flags from the default list will
+ # take precedence.
if (defined(invoker.public_deps)) {
public_deps += invoker.public_deps
}
@@ -163,8 +171,8 @@ template("pw_internal_build_target") {
deps += [ "$dir_pw_build:link_deps" ]
}
- if (defined(pw_build_defaults.visibility) && !defined(visibility)) {
- visibility = pw_build_defaults.visibility
+ if (!defined(visibility)) {
+ visibility = pw_build_DEFAULT_VISIBILITY
}
}
}
diff --git a/pw_build/gn_internal/defaults.gni b/pw_build/gn_internal/defaults.gni
new file mode 100644
index 000000000..10c7070d1
--- /dev/null
+++ b/pw_build/gn_internal/defaults.gni
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT IMPORT THIS .gni FILE DIRECTLY! Use "$dir_pw_build/defaults.gni"!
+#
+# When these variables were originally created, Pigweed did not have a widely
+# adopted naming pattern for build arguments declared by GN. For backwards
+# compatibility, these arguments will retain their naming.
+#
+# Unfortunately, there's two problems with this:
+#
+# 1. It's not clear these variables come from Pigweed since they don't follow
+# the established naming patterns.
+# 2. The names conflict with the attributes of pw_* wrapped targets, which
+# means you'll get confusing behavior if this declare_args() block gets
+# imported into the context of a regular build file. That is why this
+# .gni file is imported into a scope rather than handled like a regular
+# import. It truly is an amusing workaround, but it works.
+#
+# While it's possible to migrate away from these names, it's unlikely that will
+# ever happen.
+declare_args() {
+ default_configs = []
+ default_public_deps = []
+ remove_default_configs = []
+ remove_default_public_deps = []
+}
diff --git a/pw_build/pigweed.bzl b/pw_build/pigweed.bzl
index 57274d488..2192445c6 100644
--- a/pw_build/pigweed.bzl
+++ b/pw_build/pigweed.bzl
@@ -13,46 +13,47 @@
# the License.
"""Pigweed build environment for bazel."""
+load("@bazel_skylib//lib:selects.bzl", "selects")
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain", "use_cpp_toolchain")
+load("@rules_cc//cc:action_names.bzl", "C_COMPILE_ACTION_NAME")
load(
"//pw_build/bazel_internal:pigweed_internal.bzl",
- _add_defaults = "add_defaults",
+ _compile_cc = "compile_cc",
)
+# Used by `pw_cc_test`.
+FUZZTEST_OPTS = [
+ "-Wno-sign-compare",
+ "-Wno-unused-parameter",
+]
+
def pw_cc_binary(**kwargs):
"""Wrapper for cc_binary providing some defaults.
- Specifically, this wrapper,
-
- * Adds default copts.
- * Adds a dep on the pw_assert backend.
- * Sets "linkstatic" to True.
- * Disables header modules (via the feature -use_header_modules).
+ Specifically, this wrapper adds deps on backend_impl targets for pw_assert
+ and pw_log.
Args:
**kwargs: Passed to cc_binary.
"""
kwargs["deps"] = kwargs.get("deps", [])
- # TODO(b/234877642): Remove this implicit dependency once we have a better
+ # TODO: b/234877642 - Remove this implicit dependency once we have a better
# way to handle the facades without introducing a circular dependency into
# the build.
- kwargs["deps"] = kwargs["deps"] + ["@pigweed_config//:pw_assert_backend"]
- _add_defaults(kwargs)
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed//targets:pw_assert_backend_impl"]
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed//pw_log:backend_impl"]
native.cc_binary(**kwargs)
def pw_cc_library(**kwargs):
- """Wrapper for cc_library providing some defaults.
-
- Specifically, this wrapper,
+ """Wrapper for cc_library.
- * Adds default copts.
- * Sets "linkstatic" to True.
- * Disables header modules (via the feature -use_header_modules).
+ TODO: b/267498492 - This wrapper no longer does anything. Remove it once
+ all projects have been migrated off of it.
Args:
**kwargs: Passed to cc_library.
"""
- _add_defaults(kwargs)
native.cc_library(**kwargs)
def pw_cc_test(**kwargs):
@@ -60,43 +61,75 @@ def pw_cc_test(**kwargs):
Specifically, this wrapper,
- * Adds default copts.
* Adds a dep on the pw_assert backend.
* Adds a dep on //pw_unit_test:simple_printing_main
- * Sets "linkstatic" to True.
- * Disables header modules (via the feature -use_header_modules).
+
+ In addition, a .lib target is created that's a cc_library with the same
+ kwargs. Such library targets can be used as dependencies of firmware images
+ bundling multiple tests. The library target has alwayslink = 1, to support
+ dynamic registration (ensure the tests are baked into the image even though
+ there are no references to them, so that they can be found by RUN_ALL_TESTS
+ at runtime).
Args:
**kwargs: Passed to cc_test.
"""
kwargs["deps"] = kwargs.get("deps", []) + \
- ["@pigweed//pw_unit_test:simple_printing_main"]
+ ["@pigweed//targets:pw_unit_test_main"]
- # TODO(b/234877642): Remove this implicit dependency once we have a better
+ # TODO: b/234877642 - Remove this implicit dependency once we have a better
# way to handle the facades without introducing a circular dependency into
# the build.
- kwargs["deps"] = kwargs["deps"] + ["@pigweed_config//:pw_assert_backend"]
- _add_defaults(kwargs)
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed//targets:pw_assert_backend_impl"]
+
+ # Some tests may include FuzzTest, which includes headers that trigger
+ # warnings. This check must be done here and not in `add_defaults`, since
+ # the `use_fuzztest` config setting can refer by label to a library which
+ # itself calls `add_defaults`.
+ extra_copts = select({
+ "@pigweed//pw_fuzzer:use_fuzztest": FUZZTEST_OPTS,
+ "//conditions:default": [],
+ })
+ kwargs["copts"] = kwargs.get("copts", []) + extra_copts
+
native.cc_test(**kwargs)
+ kwargs["alwayslink"] = 1
+
+ # pw_cc_test deps may include testonly targets.
+ kwargs["testonly"] = True
+
+ # Remove any kwargs that cc_library would not recognize.
+ for arg in (
+ "additional_linker_inputs",
+ "args",
+ "env",
+ "env_inherit",
+ "flaky",
+ "local",
+ "malloc",
+ "shard_count",
+ "size",
+ "stamp",
+ "timeout",
+ ):
+ kwargs.pop(arg, "")
+ native.cc_library(name = kwargs.pop("name") + ".lib", **kwargs)
+
def pw_cc_perf_test(**kwargs):
"""A Pigweed performance test.
This macro produces a cc_binary and,
- * Adds default copts.
* Adds a dep on the pw_assert backend.
* Adds a dep on //pw_perf_test:logging_main
- * Sets "linkstatic" to True.
- * Disables header modules (via the feature -use_header_modules).
Args:
**kwargs: Passed to cc_binary.
"""
kwargs["deps"] = kwargs.get("deps", []) + \
["@pigweed//pw_perf_test:logging_main"]
- kwargs["deps"] = kwargs["deps"] + ["@pigweed_config//:pw_assert_backend"]
- _add_defaults(kwargs)
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed//targets:pw_assert_backend_impl"]
native.cc_binary(**kwargs)
def pw_cc_facade(**kwargs):
@@ -109,5 +142,232 @@ def pw_cc_facade(**kwargs):
if "srcs" in kwargs.keys():
fail("'srcs' attribute does not exist in pw_cc_facade, please use \
main implementing target.")
- _add_defaults(kwargs)
native.cc_library(**kwargs)
+
+def host_backend_alias(name, backend):
+ """An alias that resolves to the backend for host platforms."""
+ native.alias(
+ name = name,
+ actual = selects.with_or({
+ (
+ "@platforms//os:android",
+ "@platforms//os:chromiumos",
+ "@platforms//os:linux",
+ "@platforms//os:macos",
+ "@platforms//os:windows",
+ ): backend,
+ "//conditions:default": "@pigweed//pw_build:unspecified_backend",
+ }),
+ )
+
+CcBlobInfo = provider(
+ "Input to pw_cc_blob_library",
+ fields = {
+ "symbol_name": "The C++ symbol for the byte array.",
+ "file_path": "The file path for the binary blob.",
+ "linker_section": "If present, places the byte array in the specified " +
+ "linker section.",
+ "alignas": "If present, the byte array is aligned as specified. The " +
+ "value of this argument is used verbatim in an alignas() " +
+ "specifier for the blob byte array.",
+ },
+)
+
+def _pw_cc_blob_info_impl(ctx):
+ return [CcBlobInfo(
+ symbol_name = ctx.attr.symbol_name,
+ file_path = ctx.file.file_path,
+ linker_section = ctx.attr.linker_section,
+ alignas = ctx.attr.alignas,
+ )]
+
+pw_cc_blob_info = rule(
+ implementation = _pw_cc_blob_info_impl,
+ attrs = {
+ "symbol_name": attr.string(),
+ "file_path": attr.label(allow_single_file = True),
+ "linker_section": attr.string(default = ""),
+ "alignas": attr.string(default = ""),
+ },
+ provides = [CcBlobInfo],
+)
+
+def _pw_cc_blob_library_impl(ctx):
+ # Python tool takes a json file with info about blobs to generate.
+ blobs = []
+ blob_paths = []
+ for blob in ctx.attr.blobs:
+ blob_info = blob[CcBlobInfo]
+ blob_paths.append(blob_info.file_path)
+ blob_dict = {
+ "file_path": blob_info.file_path.path,
+ "symbol_name": blob_info.symbol_name,
+ "linker_section": blob_info.linker_section,
+ }
+ if (blob_info.alignas):
+ blob_dict["alignas"] = blob_info.alignas
+ blobs.append(blob_dict)
+ blob_json = ctx.actions.declare_file(ctx.label.name + "_blob.json")
+ ctx.actions.write(blob_json, json.encode(blobs))
+
+ hdr = ctx.actions.declare_file(ctx.attr.out_header)
+ src = ctx.actions.declare_file(ctx.attr.out_header.removesuffix(".h") + ".cc")
+
+ if (not ctx.attr.namespace):
+ fail("namespace required for pw_cc_blob_library")
+
+ args = ctx.actions.args()
+ args.add("--blob-file={}".format(blob_json.path))
+ args.add("--namespace={}".format(ctx.attr.namespace))
+ args.add("--header-include={}".format(ctx.attr.out_header))
+ args.add("--out-header={}".format(hdr.path))
+ args.add("--out-source={}".format(src.path))
+
+ ctx.actions.run(
+ inputs = depset(direct = blob_paths + [blob_json]),
+ progress_message = "Generating cc blob library for %s" % (ctx.label.name),
+ tools = [
+ ctx.executable._generate_cc_blob_library,
+ ctx.executable._python_runtime,
+ ],
+ outputs = [hdr, src],
+ executable = ctx.executable._generate_cc_blob_library,
+ arguments = [args],
+ )
+
+ return _compile_cc(
+ ctx,
+ [src],
+ [hdr],
+ deps = ctx.attr.deps,
+ includes = [ctx.bin_dir.path + "/" + ctx.label.package],
+ defines = [],
+ )
+
+pw_cc_blob_library = rule(
+ implementation = _pw_cc_blob_library_impl,
+ doc = """Turns binary blobs into a C++ library of hard-coded byte arrays.
+
+ The byte arrays are constant initialized and are safe to access at any time,
+ including before main().
+
+ Args:
+ ctx: Rule context.
+ blobs: A list of CcBlobInfo where each entry corresponds to a binary
+ blob to be transformed from file to byte array. This is a
+ required field. Blob fields include:
+
+ symbol_name [required]: The C++ symbol for the byte array.
+
+ file_path [required]: The file path for the binary blob.
+
+ linker_section [optional]: If present, places the byte array
+ in the specified linker section.
+
+ alignas [optional]: If present, the byte array is aligned as
+ specified. The value of this argument is used verbatim
+ in an alignas() specifier for the blob byte array.
+
+ out_header: The header file to generate. Users will include this file
+ exactly as it is written here to reference the byte arrays.
+
+ namespace: The C++ namespace in which to place the generated blobs.
+ """,
+ attrs = {
+ "blobs": attr.label_list(providers = [CcBlobInfo]),
+ "out_header": attr.string(),
+ "namespace": attr.string(),
+ "_python_runtime": attr.label(
+ default = Label("//:python3_interpreter"),
+ allow_single_file = True,
+ executable = True,
+ cfg = "exec",
+ ),
+ "_generate_cc_blob_library": attr.label(
+ default = Label("//pw_build/py:generate_cc_blob_library"),
+ executable = True,
+ cfg = "exec",
+ ),
+ "deps": attr.label_list(default = [Label("//pw_preprocessor")]),
+ },
+ provides = [CcInfo],
+ fragments = ["cpp"],
+ toolchains = use_cpp_toolchain(),
+)
+
+def _preprocess_linker_script_impl(ctx):
+ cc_toolchain = find_cpp_toolchain(ctx)
+ output_script = ctx.actions.declare_file(ctx.label.name + ".ld")
+ feature_configuration = cc_common.configure_features(
+ ctx = ctx,
+ cc_toolchain = cc_toolchain,
+ requested_features = ctx.features,
+ unsupported_features = ctx.disabled_features,
+ )
+ cxx_compiler_path = cc_common.get_tool_for_action(
+ feature_configuration = feature_configuration,
+ action_name = C_COMPILE_ACTION_NAME,
+ )
+ c_compile_variables = cc_common.create_compile_variables(
+ feature_configuration = feature_configuration,
+ cc_toolchain = cc_toolchain,
+ user_compile_flags = ctx.fragments.cpp.copts + ctx.fragments.cpp.conlyopts,
+ )
+ env = cc_common.get_environment_variables(
+ feature_configuration = feature_configuration,
+ action_name = C_COMPILE_ACTION_NAME,
+ variables = c_compile_variables,
+ )
+ ctx.actions.run(
+ outputs = [output_script],
+ inputs = depset(
+ [ctx.file.linker_script],
+ transitive = [cc_toolchain.all_files],
+ ),
+ executable = cxx_compiler_path,
+ arguments = [
+ "-E",
+ "-P",
+ # TODO: b/296928739 - This flag is needed so cc1 can be located
+ # despite the presence of symlinks. Normally this is provided
+ # through copts inherited from the toolchain, but since those are
+ # not pulled in here the flag must be explicitly added.
+ "-no-canonical-prefixes",
+ "-xc",
+ ctx.file.linker_script.path,
+ "-o",
+ output_script.path,
+ ] + [
+ "-D" + d
+ for d in ctx.attr.defines
+ ] + ctx.attr.copts,
+ env = env,
+ )
+ linker_input = cc_common.create_linker_input(
+ owner = ctx.label,
+ user_link_flags = ["-T", output_script.path],
+ additional_inputs = depset(direct = [output_script]),
+ )
+ linking_context = cc_common.create_linking_context(
+ linker_inputs = depset(direct = [linker_input]),
+ )
+ return [
+ DefaultInfo(files = depset([output_script])),
+ CcInfo(linking_context = linking_context),
+ ]
+
+pw_linker_script = rule(
+ _preprocess_linker_script_impl,
+ attrs = {
+ "copts": attr.string_list(doc = "C compile options."),
+ "defines": attr.string_list(doc = "C preprocessor defines."),
+ "linker_script": attr.label(
+ mandatory = True,
+ allow_single_file = True,
+ doc = "Linker script to preprocess.",
+ ),
+ "_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
+ },
+ toolchains = use_cpp_toolchain(),
+ fragments = ["cpp"],
+)
diff --git a/pw_build/pigweed.cmake b/pw_build/pigweed.cmake
index a1b2e871f..6d5823cd6 100644
--- a/pw_build/pigweed.cmake
+++ b/pw_build/pigweed.cmake
@@ -471,7 +471,7 @@ function(pw_add_library NAME TYPE)
HEADERS
${arg_HEADERS}
PUBLIC_DEPS
- # TODO(b/232141950): Apply compilation options that affect ABI
+ # TODO: b/232141950 - Apply compilation options that affect ABI
# globally in the CMake build instead of injecting them into libraries.
pw_build
${arg_PUBLIC_DEPS}
@@ -533,7 +533,7 @@ function(pw_add_facade NAME TYPE)
HEADERS
${arg_HEADERS}
PUBLIC_DEPS
- # TODO(b/232141950): Apply compilation options that affect ABI
+ # TODO: b/232141950 - Apply compilation options that affect ABI
# globally in the CMake build instead of injecting them into libraries.
pw_build
${arg_PUBLIC_DEPS}
@@ -737,6 +737,25 @@ function(pw_set_zephyr_backend_ifdef COND FACADE BACKEND BACKEND_DECL)
endif()
endfunction()
+# Zephyr specific wrapper to convert a pw library to a Zephyr library
+function(pw_zephyrize_libraries_ifdef COND)
+ if(DEFINED Zephyr_FOUND)
+ if(${${COND}})
+ zephyr_link_libraries(${ARGN})
+ foreach(lib ${ARGN})
+ target_link_libraries(${lib} INTERFACE zephyr_interface)
+ endforeach()
+ endif()
+ endif()
+endfunction()
+
+# Zephyr function allowing conversion of Kconfig values to Pigweed configs
+function(pw_set_config_from_zephyr ZEPHYR_CONFIG PW_CONFIG)
+ if(${ZEPHYR_CONFIG})
+ add_compile_definitions(${PW_CONFIG}=${${ZEPHYR_CONFIG}})
+ endif()
+endfunction()
+
# Set up the default pw_build_DEFAULT_MODULE_CONFIG.
set("pw_build_DEFAULT_MODULE_CONFIG" pw_build.empty CACHE STRING
"Default implementation for all Pigweed module configurations.")
diff --git a/pw_build/platforms/BUILD.bazel b/pw_build/platforms/BUILD.bazel
index 554957766..5822d84e4 100644
--- a/pw_build/platforms/BUILD.bazel
+++ b/pw_build/platforms/BUILD.bazel
@@ -43,78 +43,35 @@ platform(
constraint_values = ["@platforms//os:none"],
)
-# --- CPU's ---
-platform(
- name = "cortex_m0",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m0"],
-)
-
-platform(
- name = "cortex_m1",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m1"],
-)
-
-platform(
- name = "cortex_m3",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m3"],
-)
-
-platform(
- name = "cortex_m4",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m4"],
-)
-
-platform(
- name = "cortex_m4_fpu",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m4_fpu"],
-)
-
-platform(
- name = "cortex_m7",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m7"],
-)
-
-platform(
- name = "cortex_m7_fpu",
- constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m7_fpu"],
-)
-
-# --- Chipsets ---
-platform(
- name = "stm32f429",
- constraint_values = ["//pw_build/constraints/chipset:stm32f429"],
- parents = [":cortex_m4"],
-)
-
platform(
name = "lm3s6965evb",
- constraint_values = ["//pw_build/constraints/chipset:lm3s6965evb"],
- parents = [":cortex_m3"],
+ constraint_values = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m3",
+ "//pw_build/constraints/chipset:lm3s6965evb",
+ "//pw_sys_io_baremetal_lm3s6965evb:backend",
+ "@platforms//cpu:armv7-m",
+ "@rust_crates//:no_std",
+ ],
)
platform(
name = "nrf52833",
- constraint_values = ["//pw_build/constraints/chipset:nrf52833"],
- parents = [":cortex_m0"],
-)
-
-# --- Boards ---
-platform(
- name = "stm32f429i-disc1",
- constraint_values = ["//pw_build/constraints/board:stm32f429i-disc1"],
- parents = [":stm32f429"],
+ constraint_values = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m0",
+ "//pw_build/constraints/chipset:nrf52833",
+ "@platforms//cpu:armv6-m",
+ ],
)
+# Primarily a QEMU supported m0 target for rust development, based on the
+# nRF51822.
platform(
name = "microbit",
- constraint_values = ["//pw_build/constraints/board:microbit"],
+ constraint_values = [
+ "//pw_build/constraints/board:microbit",
+ "@rust_crates//:no_std",
+ # We have no pw_sys_io backend for this platform.
+ ],
parents = [":nrf52833"],
)
@@ -131,14 +88,19 @@ platform(
"//targets/stm32f429i_disc1_stm32cube:freertos_config_cv",
# Use the ARM_CM4F port of FreeRTOS.
"@freertos//:port_ARM_CM4F",
- # Specify this chipset to use the baremetal pw_sys_io backend (because
- # the default pw_sys_io_stdio backend is not compatible with FreeRTOS).
- "//pw_build/constraints/chipset:stm32f429",
+ # Use the baremetal pw_sys_io backend (because the default
+ # pw_sys_io_stdio backend is not compatible with FreeRTOS).
+ "//pw_sys_io_baremetal_stm32f429:backend",
+ # Select cortex-m backends
+ "//pw_cpu_exception:basic_handler_backend",
+ "//pw_cpu_exception_cortex_m:entry_backend",
+ "//pw_cpu_exception_cortex_m:support_backend",
+ "//pw_interrupt_cortex_m:backend",
# os:none means, we're not building for any host platform (Windows,
# Linux, or Mac). The pw_sys_io_baremetal_stm32f429 backend is only
# compatible with os:none.
"@platforms//os:none",
+ # For toolchain resolution.
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m4",
],
- # Inherit from cortex_m4_fpu to use the appropriate Arm toolchain.
- parents = [":cortex_m4_fpu"],
)
diff --git a/pw_build/pw_build_private/file_prefix_map_test.h b/pw_build/pw_build_private/file_prefix_map_test.h
new file mode 100644
index 000000000..de8820e74
--- /dev/null
+++ b/pw_build/pw_build_private/file_prefix_map_test.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#if !defined(PW_BUILD_EXPECTED_SOURCE_PATH) || \
+ !defined(PW_BUILD_EXPECTED_HEADER_PATH)
+#error "Expected source and header path macros must be defined!"
+#endif
+
+namespace pw::build::test {
+
+constexpr bool StringsAreEqual(const char* left, const char* right) {
+ while (*left == *right && *left != '\0') {
+ left += 1;
+ right += 1;
+ }
+
+ return *left == '\0' && *right == '\0';
+}
+
+static_assert(StringsAreEqual(PW_BUILD_EXPECTED_HEADER_PATH, __FILE__),
+ "The __FILE__ macro should be " PW_BUILD_EXPECTED_HEADER_PATH
+ ", but it is " __FILE__);
+
+} // namespace pw::build::test
diff --git a/pw_build/py/BUILD.bazel b/pw_build/py/BUILD.bazel
index 5abf4e769..64cd1a63c 100644
--- a/pw_build/py/BUILD.bazel
+++ b/pw_build/py/BUILD.bazel
@@ -28,3 +28,10 @@ py_library(
],
imports = ["."],
)
+
+py_binary(
+ name = "generate_cc_blob_library",
+ srcs = [
+ "pw_build/generate_cc_blob_library.py",
+ ],
+)
diff --git a/pw_build/py/BUILD.gn b/pw_build/py/BUILD.gn
index 16d6711fa..f9dbe41d0 100644
--- a/pw_build/py/BUILD.gn
+++ b/pw_build/py/BUILD.gn
@@ -20,10 +20,10 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_build/__init__.py",
+ "pw_build/bazel_query.py",
"pw_build/build_recipe.py",
"pw_build/collect_wheels.py",
"pw_build/copy_from_cipd.py",
@@ -32,32 +32,48 @@ pw_python_package("py") {
"pw_build/error.py",
"pw_build/exec.py",
"pw_build/file_prefix_map.py",
+ "pw_build/generate_3p_gn.py",
"pw_build/generate_cc_blob_library.py",
"pw_build/generate_modules_lists.py",
"pw_build/generate_python_package.py",
"pw_build/generate_python_package_gn.py",
"pw_build/generate_python_requirements.py",
+ "pw_build/generate_python_wheel.py",
+ "pw_build/generate_python_wheel_cache.py",
+ "pw_build/generate_report.py",
"pw_build/generated_tests.py",
+ "pw_build/gn_config.py",
"pw_build/gn_resolver.py",
+ "pw_build/gn_target.py",
+ "pw_build/gn_utils.py",
+ "pw_build/gn_writer.py",
"pw_build/host_tool.py",
+ "pw_build/merge_profraws.py",
"pw_build/mirror_tree.py",
"pw_build/nop.py",
+ "pw_build/pigweed_upstream_build.py",
"pw_build/pip_install_python_deps.py",
"pw_build/project_builder.py",
"pw_build/project_builder_argparse.py",
"pw_build/project_builder_context.py",
"pw_build/project_builder_prefs.py",
+ "pw_build/project_builder_presubmit_runner.py",
"pw_build/python_package.py",
"pw_build/python_runner.py",
- "pw_build/python_wheels.py",
"pw_build/wrap_ninja.py",
"pw_build/zip.py",
]
tests = [
+ "bazel_query_test.py",
"build_recipe_test.py",
"create_python_tree_test.py",
"file_prefix_map_test.py",
+ "generate_3p_gn_test.py",
"generate_cc_blob_library_test.py",
+ "gn_config_test.py",
+ "gn_target_test.py",
+ "gn_utils_test.py",
+ "gn_writer_test.py",
"project_builder_prefs_test.py",
"python_runner_test.py",
"zip_test.py",
@@ -67,6 +83,7 @@ pw_python_package("py") {
"$dir_pw_env_setup/py",
"$dir_pw_presubmit/py",
]
+ python_test_deps = [ "gn_tests:test_dist1" ]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_build/py/bazel_query_test.py b/pw_build/py/bazel_query_test.py
new file mode 100644
index 000000000..362fcf67d
--- /dev/null
+++ b/pw_build/py/bazel_query_test.py
@@ -0,0 +1,409 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the pw_build.bazel_query module."""
+
+import json
+import unittest
+
+from unittest import mock
+from tempfile import TemporaryDirectory
+from pw_build.bazel_query import ParseError, BazelRule, BazelWorkspace
+
+
+class TestBazelRule(unittest.TestCase):
+ """Tests for bazel_query.Rule."""
+
+ def test_rule_top_level(self):
+ """Tests a top-level rule with no package name."""
+ rule = BazelRule('//:no-package', 'custom-type')
+ self.assertEqual(rule.package(), '')
+
+ def test_rule_with_label(self):
+ """Tests a rule with a package and target name."""
+ rule = BazelRule('//foo:target', 'custom-type')
+ self.assertEqual(rule.package(), 'foo')
+ self.assertEqual(rule.label(), '//foo:target')
+
+ def test_rule_in_subdirectory(self):
+ """Tests a rule in a subdirectory."""
+ rule = BazelRule('//foo:bar/target', 'custom-type')
+ self.assertEqual(rule.package(), 'foo')
+ self.assertEqual(rule.label(), '//foo:bar/target')
+
+ def test_rule_in_subpackage(self):
+ """Tests a rule in a subpackage."""
+ rule = BazelRule('//foo/bar:target', 'custom-type')
+ self.assertEqual(rule.package(), 'foo/bar')
+ self.assertEqual(rule.label(), '//foo/bar:target')
+
+ def test_rule_no_target(self):
+ """Tests a rule with only a package name."""
+ rule = BazelRule('//foo/bar', 'custom-type')
+ self.assertEqual(rule.package(), 'foo/bar')
+ self.assertEqual(rule.label(), '//foo/bar:bar')
+
+ def test_rule_invalid_relative(self):
+ """Tests a rule with an invalid (non-absolute) package name."""
+ with self.assertRaises(ParseError):
+ BazelRule('../foo/bar:target', 'custom-type')
+
+ def test_rule_invalid_double_colon(self):
+ """Tests a rule with an invalid (non-absolute) package name."""
+ with self.assertRaises(ParseError):
+ BazelRule('//foo:bar:target', 'custom-type')
+
+ def test_rule_parse_invalid(self):
+ """Test for parsing invalid rule attributes."""
+ rule = BazelRule('//package:target', 'kind')
+ with self.assertRaises(ParseError):
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "invalid_attr",
+ "type": "ESOTERIC",
+ "intValue": 0,
+ "stringValue": "false",
+ "explicitlySpecified": true,
+ "booleanValue": false
+ }]'''
+ )
+ )
+
+ def test_rule_parse_boolean_unspecified(self):
+ """Test parsing an unset boolean rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "bool_attr",
+ "type": "BOOLEAN",
+ "intValue": 0,
+ "stringValue": "false",
+ "explicitlySpecified": false,
+ "booleanValue": false
+ }]'''
+ )
+ )
+ self.assertFalse(rule.has_attr('bool_attr'))
+
+ def test_rule_parse_boolean_false(self):
+ """Tests parsing boolean rule attribute set to false."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "bool_attr",
+ "type": "BOOLEAN",
+ "intValue": 0,
+ "stringValue": "false",
+ "explicitlySpecified": true,
+ "booleanValue": false
+ }]'''
+ )
+ )
+ self.assertTrue(rule.has_attr('bool_attr'))
+ self.assertFalse(rule.get_bool('bool_attr'))
+
+ def test_rule_parse_boolean_true(self):
+ """Tests parsing a boolean rule attribute set to true."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "bool_attr",
+ "type": "BOOLEAN",
+ "intValue": 1,
+ "stringValue": "true",
+ "explicitlySpecified": true,
+ "booleanValue": true
+ }]'''
+ )
+ )
+ self.assertTrue(rule.has_attr('bool_attr'))
+ self.assertTrue(rule.get_bool('bool_attr'))
+
+ def test_rule_parse_integer_unspecified(self):
+ """Tests parsing an unset integer rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "int_attr",
+ "type": "INTEGER",
+ "intValue": 0,
+ "explicitlySpecified": false
+ }]'''
+ )
+ )
+ self.assertFalse(rule.has_attr('int_attr'))
+
+ def test_rule_parse_integer(self):
+ """Tests parsing an integer rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "int_attr",
+ "type": "INTEGER",
+ "intValue": 100,
+ "explicitlySpecified": true
+ }]'''
+ )
+ )
+ self.assertTrue(rule.has_attr('int_attr'))
+ self.assertEqual(rule.get_int('int_attr'), 100)
+
+ def test_rule_parse_string_unspecified(self):
+ """Tests parsing an unset string rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "string_attr",
+ "type": "STRING",
+ "stringValue": "",
+ "explicitlySpecified": false
+ }]'''
+ )
+ )
+ self.assertFalse(rule.has_attr('string_attr'))
+
+ def test_rule_parse_string(self):
+ """Tests parsing a string rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "string_attr",
+ "type": "STRING",
+ "stringValue": "hello, world!",
+ "explicitlySpecified": true
+ }]'''
+ )
+ )
+ self.assertTrue(rule.has_attr('string_attr'))
+ self.assertEqual(rule.get_str('string_attr'), 'hello, world!')
+
+ def test_rule_parse_string_list_unspecified(self):
+ """Tests parsing an unset string list rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "string_list_attr",
+ "type": "STRING_LIST",
+ "stringListValue": [],
+ "explicitlySpecified": false
+ }]'''
+ )
+ )
+ self.assertFalse(rule.has_attr('string_list_attr'))
+
+ def test_rule_parse_string_list(self):
+ """Tests parsing a string list rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "string_list_attr",
+ "type": "STRING_LIST",
+ "stringListValue": [ "hello", "world!" ],
+ "explicitlySpecified": true
+ }]'''
+ )
+ )
+ self.assertTrue(rule.has_attr('string_list_attr'))
+ self.assertEqual(rule.get_list('string_list_attr'), ['hello', 'world!'])
+
+ def test_rule_parse_label_list_unspecified(self):
+ """Tests parsing an unset label list rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "label_list_attr",
+ "type": "LABEL_LIST",
+ "stringListValue": [],
+ "explicitlySpecified": false
+ }]'''
+ )
+ )
+ self.assertFalse(rule.has_attr('label_list_attr'))
+
+ def test_rule_parse_label_list(self):
+ """Tests parsing a label list rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "label_list_attr",
+ "type": "LABEL_LIST",
+ "stringListValue": [ "hello", "world!" ],
+ "explicitlySpecified": true
+ }]'''
+ )
+ )
+ self.assertTrue(rule.has_attr('label_list_attr'))
+ self.assertEqual(rule.get_list('label_list_attr'), ['hello', 'world!'])
+
+ def test_rule_parse_string_dict_unspecified(self):
+ """Tests parsing an unset string dict rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "string_dict_attr",
+ "type": "LABEL_LIST",
+ "stringDictValue": [],
+ "explicitlySpecified": false
+ }]'''
+ )
+ )
+ self.assertFalse(rule.has_attr('string_dict_attr'))
+
+ def test_rule_parse_string_dict(self):
+ """Tests parsing a string dict rule attribute."""
+ rule = BazelRule('//package:target', 'kind')
+ rule.parse(
+ json.loads(
+ '''[{
+ "name": "string_dict_attr",
+ "type": "STRING_DICT",
+ "stringDictValue": [
+ {
+ "key": "foo",
+ "value": "hello"
+ },
+ {
+ "key": "bar",
+ "value": "world"
+ }
+ ],
+ "explicitlySpecified": true
+ }]'''
+ )
+ )
+ string_dict_attr = rule.get_dict('string_dict_attr')
+ self.assertTrue(rule.has_attr('string_dict_attr'))
+ self.assertEqual(string_dict_attr['foo'], 'hello')
+ self.assertEqual(string_dict_attr['bar'], 'world')
+
+
+class TestWorkspace(unittest.TestCase):
+ """Test for bazel_query.Workspace."""
+
+ @mock.patch('subprocess.run')
+ def test_workspace_get_rules(self, mock_run):
+ """Tests querying a workspace for Bazel rules."""
+ attrs = []
+
+ # `bazel query //... --output=package
+ attrs.append(
+ {
+ 'stdout.decode.return_value': '''
+foo/pkg1
+bar/pkg2'''
+ }
+ )
+
+ # bazel query buildfiles(//foo:*) --output=xml
+ attrs.append(
+ {
+ 'stdout.decode.return_value': '''
+<query version="2">
+ <source-file name="//foo/pkg1:BUILD.bazel">
+ <visibility-label name="//visibility:public"/>
+ </source-file>
+</query>'''
+ }
+ )
+
+ # bazel query buildfiles(//bar:*) --output=xml
+ attrs.append(
+ {
+ 'stdout.decode.return_value': '''
+<query version="2">
+ <source-file name="//bar/pkg2:BUILD.bazel">
+ <visibility-label name="//visibility:private"/>
+ </source-file>
+</query>'''
+ }
+ )
+
+ # bazel cquery kind(some_kind, //...) --output=jsonproto
+ attrs.append(
+ {
+ 'stdout.decode.return_value': '''
+{
+ "results": [
+ {
+ "target": {
+ "type": "RULE",
+ "rule": {
+ "name": "//foo/pkg1:rule1",
+ "ruleClass": "some_kind",
+ "attribute": []
+ }
+ }
+ },
+ {
+ "target": {
+ "type": "RULE",
+ "rule": {
+ "name": "//bar/pkg2:rule2",
+ "ruleClass": "some_kind",
+ "attribute": []
+ }
+ }
+ }
+ ]
+}'''
+ }
+ )
+ mock_run.side_effect = [mock.MagicMock(**attr) for attr in attrs]
+ with TemporaryDirectory() as tmp:
+ workspace = BazelWorkspace(tmp)
+ rules = list(workspace.get_rules('some_kind'))
+ actual = [r.label() for r in rules]
+ self.assertEqual(actual, ['//foo/pkg1:rule1', '//bar/pkg2:rule2'])
+
+ @mock.patch('subprocess.run')
+ def test_revision(self, mock_run):
+ """Tests writing an OWNERS file."""
+ attrs = {'stdout.decode.return_value': 'fake-hash'}
+ mock_run.return_value = mock.MagicMock(**attrs)
+
+ with TemporaryDirectory() as tmp:
+ workspace = BazelWorkspace(tmp)
+ self.assertEqual(workspace.revision(), 'fake-hash')
+ args, kwargs = mock_run.call_args
+ self.assertEqual(*args, ['git', 'rev-parse', 'HEAD'])
+ self.assertEqual(kwargs['cwd'], tmp)
+
+ @mock.patch('subprocess.run')
+ def test_url(self, mock_run):
+ """Tests writing an OWNERS file."""
+ attrs = {'stdout.decode.return_value': 'https://repohub.com/repo.git'}
+ mock_run.return_value = mock.MagicMock(**attrs)
+
+ with TemporaryDirectory() as tmp:
+ workspace = BazelWorkspace(tmp)
+ self.assertEqual(workspace.url(), 'https://repohub.com/repo.git')
+ args, kwargs = mock_run.call_args
+ self.assertEqual(*args, ['git', 'remote', 'get-url', 'origin'])
+ self.assertEqual(kwargs['cwd'], tmp)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/build_recipe_test.py b/pw_build/py/build_recipe_test.py
index 6361da5b8..86ce9df21 100644
--- a/pw_build/py/build_recipe_test.py
+++ b/pw_build/py/build_recipe_test.py
@@ -47,20 +47,53 @@ class TestBuildRecipe(unittest.TestCase):
BuildCommand(
build_dir=Path('outbazel'),
build_system_command='bazel',
- build_system_extra_args=[],
- targets=['build', '//pw_analog/...', '//pw_assert/...'],
+ build_system_extra_args=['build'],
+ targets=['//pw_analog/...', '//pw_assert/...'],
),
# result
[
'bazel',
- '--output_base',
- 'outbazel',
'build',
+ '--symlink_prefix',
+ str(Path('outbazel') / 'bazel-'),
'//pw_analog/...',
'//pw_assert/...',
],
),
(
+ 'test command using bazel',
+ BuildCommand(
+ build_dir=Path('outbazel'),
+ build_system_command='bazel',
+ build_system_extra_args=['test'],
+ targets=['//...:all'],
+ ),
+ # result
+ [
+ 'bazel',
+ 'test',
+ '--symlink_prefix',
+ str(Path('outbazel') / 'bazel-'),
+ '//...:all',
+ ],
+ ),
+ (
+ 'clean command using bazel',
+ BuildCommand(
+ build_dir=Path('outbazel'),
+ build_system_command='bazel',
+ build_system_extra_args=['clean'],
+ targets=['//...:all'],
+ ),
+ # result
+ [
+ 'bazel',
+ 'clean',
+ '--symlink_prefix',
+ str(Path('outbazel') / 'bazel-'),
+ ],
+ ),
+ (
'cmake shell command',
BuildCommand(
build_dir=Path('outcmake'),
diff --git a/pw_build/py/create_python_tree_test.py b/pw_build/py/create_python_tree_test.py
index d545a0ffe..f4922046a 100644
--- a/pw_build/py/create_python_tree_test.py
+++ b/pw_build/py/create_python_tree_test.py
@@ -13,6 +13,7 @@
# the License.
"""Tests for pw_build.create_python_tree"""
+import importlib.resources
import io
import os
from pathlib import Path
@@ -31,6 +32,8 @@ from pw_build.create_python_tree import (
)
from pw_build.generate_python_package import PYPROJECT_FILE
+import test_dist1_data # type: ignore
+
def _setup_cfg(package_name: str, install_requires: str = '') -> str:
return f'''
@@ -494,6 +497,18 @@ saturn =
# Check expected files are in place.
self._check_result_paths_equal(install_dir, expected_file_list)
+ def test_importing_package_data(self) -> None:
+ self.assertIn(
+ 'EMPTY.CSV',
+ importlib.resources.read_text(test_dist1_data, 'empty.csv'),
+ )
+ self.assertIn(
+ 'EMPTY.CSV',
+ importlib.resources.read_text(
+ 'test_dist1_data.subdir', 'empty.csv'
+ ),
+ )
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_build/py/file_prefix_map_test.py b/pw_build/py/file_prefix_map_test.py
index 0cb258059..cffa60802 100644
--- a/pw_build/py/file_prefix_map_test.py
+++ b/pw_build/py/file_prefix_map_test.py
@@ -57,9 +57,10 @@ class FilePrefixMapTest(unittest.TestCase):
path_list = [
'/foo_root/root_subdir/source.cc',
'/foo_root/root_subdir/out/../gen.cc',
+ 'out/../gen.cc',
]
prefix_maps = [('/foo_root/root_subdir/', ''), ('out/../', 'out/')]
- expected_paths = ['source.cc', 'out/gen.cc']
+ expected_paths = ['source.cc', 'out/../gen.cc', 'out/gen.cc']
self.assertEqual(
list(file_prefix_map.remap_paths(path_list, prefix_maps)),
expected_paths,
diff --git a/pw_build/py/generate_3p_gn_test.py b/pw_build/py/generate_3p_gn_test.py
new file mode 100644
index 000000000..56aa8234c
--- /dev/null
+++ b/pw_build/py/generate_3p_gn_test.py
@@ -0,0 +1,744 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the pw_build.generate_3p_gn module."""
+
+import unittest
+
+from contextlib import AbstractContextManager
+from io import StringIO
+from pathlib import Path
+from tempfile import TemporaryDirectory
+from unittest import mock
+from types import TracebackType
+from typing import Iterator, Optional, Type
+
+from pw_build.generate_3p_gn import GnGenerator, write_owners
+from pw_build.gn_config import GnConfig
+from pw_build.gn_writer import GnWriter
+
+
+class GnGeneratorForTest(AbstractContextManager):
+ """Test fixture that creates a generator for a temporary directory."""
+
+ def __init__(self):
+ self._tmp = TemporaryDirectory()
+
+ def __enter__(self) -> GnGenerator:
+ """Creates a temporary directory and uses it to create a generator."""
+ tmp = self._tmp.__enter__()
+ generator = GnGenerator()
+ path = Path(tmp) / 'repo'
+ path.mkdir(parents=True)
+ generator.load_workspace(path)
+ return generator
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ """Removes the temporary directory."""
+ self._tmp.__exit__(exc_type, exc_val, exc_tb)
+
+
+def mock_return_values(mock_run, retvals: Iterator[str]) -> None:
+ """Mocks the return values of several calls to subprocess.run."""
+ side_effects = []
+ for retval in retvals:
+ attr = {'stdout.decode.return_value': retval}
+ side_effects.append(mock.MagicMock(**attr))
+ mock_run.side_effect = side_effects
+
+
+class TestGenerator(unittest.TestCase):
+ """Tests for generate_3p_gn.GnGenerator."""
+
+ def test_generate_configs(self):
+ """Tests finding the most common configs."""
+ generator = GnGenerator()
+ generator.set_repo('test')
+
+ generator.add_target(
+ json='''{
+ "target_name": "target0",
+ "cflags": ["common"]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_name": "target1",
+ "package": "foo",
+ "include_dirs": ["foo"],
+ "cflags": ["common", "foo-flag1"]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_name": "target2",
+ "package": "foo",
+ "include_dirs": ["foo"],
+ "cflags": ["common", "foo-flag1", "foo-flag2"]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_name": "target3",
+ "package": "foo",
+ "include_dirs": ["foo"],
+ "cflags": ["common", "foo-flag1"]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_name": "target4",
+ "package": "bar",
+ "include_dirs": ["bar"],
+ "cflags": ["common", "bar-flag"]
+ }''',
+ )
+
+ configs_to_add = ['//:added']
+ configs_to_remove = ['//remove:me']
+ generator.generate_configs(configs_to_add, configs_to_remove)
+
+ self.assertEqual(
+ generator.configs[''],
+ [
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test:test_config1",
+ "cflags": ["common"],
+ "usages": 5
+ }'''
+ )
+ ],
+ )
+
+ self.assertEqual(
+ generator.configs['foo'],
+ [
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test/foo:foo_config1",
+ "cflags": ["foo-flag1"],
+ "public": false,
+ "usages": 3
+ }'''
+ ),
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test/foo:foo_public_config1",
+ "include_dirs": ["foo"],
+ "public": true,
+ "usages": 3
+ }'''
+ ),
+ ],
+ )
+
+ self.assertEqual(
+ generator.configs['bar'],
+ [
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test/bar:bar_public_config1",
+ "include_dirs": ["bar"],
+ "public": true,
+ "usages": 1
+ }'''
+ )
+ ],
+ )
+
+ targets = [
+ target
+ for targets in generator.targets.values()
+ for target in targets
+ ]
+ targets.sort(key=lambda target: target.name())
+
+ target0 = targets[0]
+ self.assertFalse(target0.config)
+ self.assertEqual(
+ target0.configs,
+ {'//:added', '$dir_pw_third_party/test:test_config1'},
+ )
+ self.assertFalse(target0.public_configs)
+ self.assertEqual(target0.remove_configs, {'//remove:me'})
+
+ target1 = targets[1]
+ self.assertFalse(target1.config)
+ self.assertEqual(
+ target1.configs,
+ {
+ '//:added',
+ '$dir_pw_third_party/test:test_config1',
+ '$dir_pw_third_party/test/foo:foo_config1',
+ },
+ )
+ self.assertEqual(
+ target1.public_configs,
+ {'$dir_pw_third_party/test/foo:foo_public_config1'},
+ )
+ self.assertEqual(target1.remove_configs, {'//remove:me'})
+
+ target2 = targets[2]
+ self.assertEqual(
+ target2.config,
+ GnConfig(
+ json='''{
+ "cflags": ["foo-flag2"],
+ "public": false,
+ "usages": 0
+ }'''
+ ),
+ )
+ self.assertEqual(
+ target2.configs,
+ {
+ '//:added',
+ '$dir_pw_third_party/test:test_config1',
+ '$dir_pw_third_party/test/foo:foo_config1',
+ },
+ )
+ self.assertEqual(
+ target2.public_configs,
+ {'$dir_pw_third_party/test/foo:foo_public_config1'},
+ )
+ self.assertEqual(target2.remove_configs, {'//remove:me'})
+
+ target3 = targets[3]
+ self.assertFalse(target3.config)
+ self.assertEqual(
+ target3.configs,
+ {
+ '//:added',
+ '$dir_pw_third_party/test:test_config1',
+ '$dir_pw_third_party/test/foo:foo_config1',
+ },
+ )
+ self.assertEqual(
+ target3.public_configs,
+ {'$dir_pw_third_party/test/foo:foo_public_config1'},
+ )
+ self.assertEqual(target3.remove_configs, {'//remove:me'})
+
+ target4 = targets[4]
+ self.assertEqual(
+ target4.config,
+ GnConfig(
+ json='''{
+ "cflags": ["bar-flag"],
+ "public": false,
+ "usages": 0
+ }'''
+ ),
+ )
+ self.assertEqual(
+ target4.configs,
+ {'//:added', '$dir_pw_third_party/test:test_config1'},
+ )
+ self.assertEqual(
+ target4.public_configs,
+ {'$dir_pw_third_party/test/bar:bar_public_config1'},
+ )
+ self.assertEqual(target4.remove_configs, {'//remove:me'})
+
+ def test_write_build_gn(self):
+ """Tests writing a complete BUILD.gn file."""
+ generator = GnGenerator()
+ generator.set_repo('test')
+ generator.exclude_from_gn_check(bazel='//bar:target3')
+
+ generator.add_configs(
+ '',
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test:test_config1",
+ "cflags": ["common"],
+ "usages": 5
+ }'''
+ ),
+ )
+
+ generator.add_configs(
+ 'foo',
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test/foo:foo_config1",
+ "cflags": ["foo-flag1"],
+ "public": false,
+ "usages": 3
+ }'''
+ ),
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test/foo:foo_public_config1",
+ "include_dirs": ["foo"],
+ "public": true,
+ "usages": 3
+ }'''
+ ),
+ )
+
+ generator.add_configs(
+ 'bar',
+ GnConfig(
+ json='''{
+ "label": "$dir_pw_third_party/test/bar:bar_public_config1",
+ "include_dirs": ["bar"],
+ "public": true,
+ "usages": 1
+ }'''
+ ),
+ )
+
+ generator.add_target(
+ json='''{
+ "target_type": "pw_executable",
+ "target_name": "target0",
+ "configs": ["$dir_pw_third_party/test:test_config1"],
+ "sources": ["$dir_pw_third_party_test/target0.cc"],
+ "deps": [
+ "$dir_pw_third_party/test/foo:target1",
+ "$dir_pw_third_party/test/foo:target2"
+ ]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_type": "pw_source_set",
+ "target_name": "target1",
+ "package": "foo",
+ "public": ["$dir_pw_third_party_test/foo/target1.h"],
+ "sources": ["$dir_pw_third_party_test/foo/target1.cc"],
+ "public_configs": ["$dir_pw_third_party/test/foo:foo_public_config1"],
+ "configs": [
+ "$dir_pw_third_party/test:test_config1",
+ "$dir_pw_third_party/test/foo:foo_config1"
+ ]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_type": "pw_source_set",
+ "target_name": "target2",
+ "package": "foo",
+ "sources": ["$dir_pw_third_party_test/foo/target2.cc"],
+ "public_configs": ["$dir_pw_third_party/test/foo:foo_public_config1"],
+ "configs": [
+ "$dir_pw_third_party/test:test_config1",
+ "$dir_pw_third_party/test/foo:foo_config1"
+ ],
+ "cflags": ["foo-flag2"],
+ "public_deps": ["$dir_pw_third_party/test/bar:target3"]
+ }''',
+ )
+
+ generator.add_target(
+ json='''{
+ "target_type": "pw_source_set",
+ "target_name": "target3",
+ "package": "bar",
+ "include_dirs": ["bar"],
+ "public_configs": ["$dir_pw_third_party/test/bar:bar_public_config1"],
+ "configs": ["$dir_pw_third_party/test:test_config1"],
+ "cflags": ["bar-flag"]
+ }''',
+ )
+
+ output = StringIO()
+ build_gn = GnWriter(output)
+ generator.write_build_gn('', build_gn)
+ self.assertEqual(
+ output.getvalue(),
+ '''
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/test/test.gni")
+
+if (dir_pw_third_party_test != "") {
+ config("test_config1") {
+ cflags = [
+ "common",
+ ]
+ }
+
+ # Generated from //:target0
+ pw_executable("target0") {
+ sources = [
+ "$dir_pw_third_party_test/target0.cc",
+ ]
+ configs = [
+ ":test_config1",
+ ]
+ deps = [
+ "foo:target1",
+ "foo:target2",
+ ]
+ }
+}
+
+pw_doc_group("docs") {
+ sources = [
+ "docs.rst",
+ ]
+}
+'''.lstrip(),
+ )
+
+ output = StringIO()
+ build_gn = GnWriter(output)
+ generator.write_build_gn('foo', build_gn)
+ self.assertEqual(
+ output.getvalue(),
+ '''
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/test/test.gni")
+
+config("foo_public_config1") {
+ include_dirs = [
+ "foo",
+ ]
+}
+
+config("foo_config1") {
+ cflags = [
+ "foo-flag1",
+ ]
+}
+
+# Generated from //foo:target1
+pw_source_set("target1") {
+ public = [
+ "$dir_pw_third_party_test/foo/target1.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_test/foo/target1.cc",
+ ]
+ public_configs = [
+ ":foo_public_config1",
+ ]
+ configs = [
+ "..:test_config1",
+ ":foo_config1",
+ ]
+}
+
+# Generated from //foo:target2
+pw_source_set("target2") {
+ sources = [
+ "$dir_pw_third_party_test/foo/target2.cc",
+ ]
+ cflags = [
+ "foo-flag2",
+ ]
+ public_configs = [
+ ":foo_public_config1",
+ ]
+ configs = [
+ "..:test_config1",
+ ":foo_config1",
+ ]
+ public_deps = [
+ "../bar:target3",
+ ]
+}
+'''.lstrip(),
+ )
+
+ output = StringIO()
+ build_gn = GnWriter(output)
+ generator.write_build_gn('bar', build_gn)
+ self.assertEqual(
+ output.getvalue(),
+ '''
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/test/test.gni")
+
+config("bar_public_config1") {
+ include_dirs = [
+ "bar",
+ ]
+}
+
+# Generated from //bar:target3
+pw_source_set("target3") {
+ check_includes = false
+ cflags = [
+ "bar-flag",
+ ]
+ include_dirs = [
+ "bar",
+ ]
+ public_configs = [
+ ":bar_public_config1",
+ ]
+ configs = [
+ "..:test_config1",
+ ]
+}
+'''.lstrip(),
+ )
+
+ def test_write_repo_gni(self):
+ """Tests writing the GN import file for a repo."""
+ output = StringIO()
+ with GnGeneratorForTest() as generator:
+ generator.write_repo_gni(GnWriter(output), 'Repo')
+
+ self.assertEqual(
+ output.getvalue(),
+ '''
+declare_args() {
+ # If compiling tests with Repo, this variable is set to the path to the Repo
+ # installation. When set, a pw_source_set for the Repo library is created at
+ # "$dir_pw_third_party/repo".
+ dir_pw_third_party_repo = ""
+}
+'''.lstrip(),
+ )
+
+ @mock.patch('subprocess.run')
+ def test_write_docs_rst(self, mock_run):
+ """Tests writing the reStructuredText docs for a repo."""
+ mock_return_values(
+ mock_run,
+ [
+ 'https://host/repo.git',
+ 'https://host/repo.git',
+ 'deadbeeffeedface',
+ ],
+ )
+ output = StringIO()
+ with GnGeneratorForTest() as generator:
+ generator.write_docs_rst(output, 'Repo')
+
+ self.assertEqual(
+ output.getvalue(),
+ '''
+.. _module-pw_third_party_repo:
+
+====
+Repo
+====
+The ``$dir_pw_third_party/repo/`` module provides build files to allow
+optionally including upstream Repo.
+
+-------------------
+Using upstream Repo
+-------------------
+If you want to use Repo, you must do the following:
+
+Submodule
+=========
+Add Repo to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add https://host/repo.git \\
+ third_party/repo/src
+
+GN
+==
+* Set the GN var ``dir_pw_third_party_repo`` to the location of the
+ Repo source.
+
+ If you used the command above, this will be
+ ``//third_party/repo/src``
+
+ This can be set in your args.gn or .gn file like:
+ ``dir_pw_third_party_repo = "//third_party/repo/src"``
+
+Updating
+========
+The GN build files are generated from the third-party Bazel build files using
+$dir_pw_build/py/pw_build/generate_3p_gn.py.
+
+The script uses data taken from ``$dir_pw_third_party/repo/repo.json``.
+The schema of ``repo.json`` is described in :ref:`module-pw_build-third-party`.
+
+The script should be re-run whenever the submodule is updated or the JSON file
+is modified. Specify the location of the Bazel repository can be specified using
+the ``-w`` option, e.g.
+
+.. code-block:: sh
+
+ python pw_build/py/pw_build/generate_3p_gn.py \\
+ -w third_party/repo/src
+
+.. DO NOT EDIT BELOW THIS LINE. Generated section.
+
+Version
+=======
+The update script was last run for revision `deadbeef`_.
+
+.. _deadbeef: https://host/repo/tree/deadbeeffeedface
+'''.lstrip(),
+ )
+
+ @mock.patch('subprocess.run')
+ def test_update_docs_rst_same_rev(self, mock_run):
+ """Tests updating the docs with the same revision."""
+ mock_return_values(
+ mock_run,
+ [
+ 'https://host/repo.git',
+ 'https://host/repo.git',
+ 'deadbeeffeedface',
+ 'https://host/repo.git',
+ 'deadbeeffeedface',
+ ],
+ )
+ output = StringIO()
+ with GnGeneratorForTest() as generator:
+ generator.write_docs_rst(output, 'Repo')
+ original = output.getvalue().split('\n')
+ updated = list(generator.update_version(original))
+
+ self.assertEqual(original, updated)
+
+ @mock.patch('subprocess.run')
+ def test_update_docs_rst_new_rev(self, mock_run):
+ """Tests updating the docs with the different revision."""
+ mock_return_values(
+ mock_run,
+ [
+ 'https://host/repo.git',
+ 'https://host/repo.git',
+ 'deadbeeffeedface',
+ 'https://host/repo.git',
+ '0123456789abcdef',
+ ],
+ )
+ output = StringIO()
+ with GnGeneratorForTest() as generator:
+ generator.write_docs_rst(output, 'Repo')
+ contents = output.getvalue()
+ original = contents.split('\n')
+
+ # Convert the contents to a list of lines similar to those returned
+ # by iterating over an open file. In particular, include a newline
+ # at the end of each line.
+ with_newlines = [s + '\n' for s in original]
+ updated = list(generator.update_version(with_newlines))
+
+ self.assertEqual(original[:-6], updated[:-6])
+ self.assertEqual(
+ '\n'.join(updated[-6:]),
+ '''
+Version
+=======
+The update script was last run for revision `01234567`_.
+
+.. _01234567: https://host/repo/tree/0123456789abcdef
+'''.lstrip(),
+ )
+
+ @mock.patch('subprocess.run')
+ def test_update_docs_rst_no_rev(self, mock_run):
+ """Tests updating docs that do not have a revision."""
+ mock_return_values(
+ mock_run,
+ [
+ 'https://host/repo.git',
+ '0123456789abcdef',
+ ],
+ )
+ with GnGeneratorForTest() as generator:
+ updated = list(generator.update_version(['foo', 'bar', '']))
+
+ self.assertEqual(
+ '\n'.join(updated),
+ '''
+foo
+bar
+
+.. DO NOT EDIT BELOW THIS LINE. Generated section.
+
+Version
+=======
+The update script was last run for revision `01234567`_.
+
+.. _01234567: https://host/repo/tree/0123456789abcdef
+'''.lstrip(),
+ )
+
+ def test_update_third_party_docs(self):
+ """Tests adding docs to //docs::third_party_docs."""
+ with GnGeneratorForTest() as generator:
+ contents = generator.update_third_party_docs(
+ '''
+group("third_party_docs") {
+ deps = [
+ "$dir_pigweed/third_party/existing:docs",
+ ]
+}
+'''
+ )
+ # Formatting is performed separately.
+ self.assertEqual(
+ contents,
+ '''
+group("third_party_docs") {
+deps = ["$dir_pigweed/third_party/repo:docs",
+ "$dir_pigweed/third_party/existing:docs",
+ ]
+}
+''',
+ )
+
+ def test_update_third_party_docs_no_target(self):
+ """Tests adding docs to a file without a "third_party_docs" target."""
+ with GnGeneratorForTest() as generator:
+ with self.assertRaises(ValueError):
+ generator.update_third_party_docs('')
+
+ @mock.patch('subprocess.run')
+ def test_write_extra(self, mock_run):
+ """Tests extra files produced via `bazel run`."""
+ attr = {'stdout.decode.return_value': 'hello, world!'}
+ mock_run.return_value = mock.MagicMock(**attr)
+
+ output = StringIO()
+ with GnGeneratorForTest() as generator:
+ generator.write_extra(output, 'some_label')
+ self.assertEqual(output.getvalue(), 'hello, world!')
+
+ @mock.patch('subprocess.run')
+ def test_write_owners(self, mock_run):
+ """Tests writing an OWNERS file."""
+ attr = {'stdout.decode.return_value': 'someone@pigweed.dev'}
+ mock_run.return_value = mock.MagicMock(**attr)
+
+ output = StringIO()
+ write_owners(output)
+ self.assertEqual(output.getvalue(), 'someone@pigweed.dev')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/generate_cc_blob_library_test.py b/pw_build/py/generate_cc_blob_library_test.py
index 6bdf311fb..33709c9d7 100644
--- a/pw_build/py/generate_cc_blob_library_test.py
+++ b/pw_build/py/generate_cc_blob_library_test.py
@@ -261,8 +261,8 @@ class TestSourceFromBlobs(unittest.TestCase):
'\n'
f'alignas(64) {FOO_BLOB}'
'\n'
- 'PW_PLACE_IN_SECTION(".abc")\n'
- f'alignas(int) {BAR_BLOB}'
+ 'alignas(int) PW_PLACE_IN_SECTION(".abc")\n'
+ f'{BAR_BLOB}'
)
self.assertEqual(expected_source, source)
diff --git a/pw_build/py/gn_config_test.py b/pw_build/py/gn_config_test.py
new file mode 100644
index 000000000..3f1190060
--- /dev/null
+++ b/pw_build/py/gn_config_test.py
@@ -0,0 +1,210 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the pw_build.gn_config module."""
+
+import unittest
+
+from typing import Iterable
+
+from pw_build.gn_config import (
+ consolidate_configs,
+ GnConfig,
+ GN_CONFIG_FLAGS,
+)
+from pw_build.gn_utils import GnLabel, MalformedGnError
+
+
+class TestGnConfig(unittest.TestCase):
+ """Tests for gn_config.GnConfig.
+
+ Attributes:
+ config: A common config for testing.
+ """
+
+ def compare(self, actual: Iterable[str], *expected: str):
+ """Sorts lists and compares them."""
+ self.assertEqual(sorted(expected), sorted(list(actual)))
+
+ def setUp(self):
+ self.config = GnConfig()
+
+ def test_bool_empty(self):
+ """Identifies an empty config correctly."""
+ self.assertFalse(self.config)
+
+ def test_bool_nonempty(self):
+ """Identifies a non-empty config correctly."""
+ self.config.add('defines', 'KEY=VAL')
+ self.assertTrue(self.config)
+
+ def test_has_none(self):
+ """Indicates a config does not have a flag when it has no values."""
+ self.assertFalse(self.config.has('cflags'))
+
+ def test_has_single(self):
+ """Indicates a config has a flag when it has one value."""
+ self.config.add('cflags', '-frobinator')
+ self.assertTrue(self.config.has('cflags'))
+
+ def test_has_multiple(self):
+ """Indicates a config has a flag when it has multiple values."""
+ self.config.add('cflags', '-frobinator')
+ self.config.add('cflags', '-fizzbuzzer')
+ self.assertTrue(self.config.has('cflags'))
+
+ def test_has_two_flags(self):
+ """Indicates a config has a flag when it has multiple flags."""
+ self.config.add('cflags', '-frobinator')
+ self.config.add('cflags_c', '-foobarbaz')
+ self.assertTrue(self.config.has('cflags'))
+
+ def test_add_and_get(self):
+ """Tests ability to add valid flags to a config."""
+ for flag in GN_CONFIG_FLAGS:
+ self.config.add(flag, f'{flag}_value1')
+ self.config.add(flag, f'{flag}_value2', f'{flag}_value3')
+ for flag in GN_CONFIG_FLAGS:
+ self.compare(
+ self.config.get(flag),
+ f'{flag}_value1',
+ f'{flag}_value2',
+ f'{flag}_value3',
+ )
+
+ def test_add_and_get_invalid(self):
+ """Tests ability to add only valid flags to a config."""
+ with self.assertRaises(MalformedGnError):
+ self.config.add('invalid', 'value')
+
+ def test_take_missing(self):
+ """Tests ability to return an empty list when taking a missing flag."""
+ self.compare(self.config.take('cflags'))
+ self.assertFalse(self.config.has('defines'))
+ self.assertFalse(self.config.has('cflags'))
+
+ def test_take(self):
+ """Tests ability to remove and return values from a config."""
+ self.config.add('defines', 'KEY1=VAL1', 'KEY2=VAL2')
+ self.config.add('cflags', '-frobinator', '-fizzbuzzer')
+ self.assertTrue(self.config.has('defines'))
+ self.assertTrue(self.config.has('cflags'))
+
+ self.compare(self.config.take('cflags'), '-frobinator', '-fizzbuzzer')
+ self.assertTrue(self.config.has('defines'))
+ self.assertFalse(self.config.has('cflags'))
+
+ def test_deduplicate_no_label(self):
+ """Tests raising an error from deduplicating a labelless config."""
+ cfg1 = GnConfig()
+ cfg1.add('defines', 'KEY1=VAL1')
+ with self.assertRaises(ValueError):
+ list(self.config.deduplicate(cfg1))
+
+ def test_deduplicate_empty(self):
+ """Tests deduplicating an empty config."""
+ cfg2 = GnConfig()
+ cfg2.label = ':cfg2'
+ self.assertFalse(list(self.config.deduplicate(cfg2)))
+
+ def test_deduplicate_not_subset(self):
+ """Tests deduplicating a config whose values are not a subset."""
+ self.config.add('defines', 'KEY1=VAL1', 'KEY2=VAL2')
+ self.config.add('cflags', '-frobinator', '-fizzbuzzer', '-foobarbaz')
+
+ cfg3 = GnConfig()
+ cfg3.label = ':cfg3'
+ cfg3.add('defines', 'KEY1=VAL1', 'KEY3=VAL3')
+ self.assertFalse(list(self.config.deduplicate(cfg3)))
+
+ def test_deduplicate(self):
+ """Tests deduplicating multiple overlapping configs."""
+ self.config.add('defines', 'KEY1=VAL1', 'KEY2=VAL2')
+ self.config.add('cflags', '-frobinator', '-fizzbuzzer', '-foobarbaz')
+
+ cfg4 = GnConfig()
+ cfg4.label = ':cfg4'
+ cfg4.add('defines', 'KEY1=VAL1')
+ cfg4.add('cflags', '-frobinator')
+
+ cfg5 = GnConfig()
+ cfg5.label = ':cfg5'
+ cfg5.add('defines', 'KEY1=VAL1')
+ cfg5.add('cflags', '-foobarbaz')
+
+ cfg6 = GnConfig()
+ cfg6.label = ':skipped'
+ cfg6.add('cflags', '-foobarbaz')
+
+ cfgs = [
+ str(cfg.label) for cfg in self.config.deduplicate(cfg4, cfg5, cfg6)
+ ]
+ self.assertEqual(cfgs, [':cfg4', ':cfg5'])
+ self.compare(self.config.get('defines'), 'KEY2=VAL2')
+ self.compare(self.config.get('cflags'), '-fizzbuzzer')
+
+ def test_extract_public(self):
+ """Tests ability to removes and return `public_config` values."""
+ self.config.add('public_defines', 'KEY1=VAL1', 'KEY2=VAL2')
+ self.config.add('defines', 'KEY3=VAL3', 'KEY4=VAL4')
+ self.config.add('include_dirs', 'foo', 'bar', 'baz')
+ self.config.add('cflags', '-frobinator', '-fizzbuzzer')
+
+ config = self.config.extract_public()
+
+ self.assertFalse(self.config.has('public_defines'))
+ self.assertFalse(self.config.has('include_dirs'), 'KEY2=VAL2')
+ self.compare(self.config.get('defines'), 'KEY3=VAL3', 'KEY4=VAL4')
+ self.compare(self.config.get('cflags'), '-frobinator', '-fizzbuzzer')
+
+ self.assertFalse(config.has('public_defines'))
+ self.compare(config.get('defines'), 'KEY1=VAL1', 'KEY2=VAL2')
+ self.compare(config.get('include_dirs'), 'foo', 'bar', 'baz')
+ self.assertFalse(config.has('cflags'))
+
+ def test_consolidate_configs(self):
+ """Tests ability to merges configs into the smallest exact cover."""
+ cfg1 = GnConfig()
+ cfg1.add('cflags', 'one', 'a', 'b')
+ cfg1.add('defines', 'k=1')
+ cfg1.add('include_dirs', 'foo', 'bar')
+
+ cfg2 = GnConfig()
+ cfg2.add('cflags', 'one', 'a', 'b', 'c')
+ cfg2.add('defines', 'k=1')
+ cfg2.add('include_dirs', 'foo', 'baz')
+
+ cfg3 = GnConfig()
+ cfg3.add('cflags', 'one', 'two', 'a', 'b')
+ cfg3.add('defines', 'k=1')
+ cfg3.add('include_dirs', 'foo', 'bar')
+
+ cfg4 = GnConfig()
+ cfg4.add('cflags', 'one', 'b', 'c')
+ cfg4.add('defines', 'k=0')
+ cfg4.add('include_dirs', 'foo', 'baz')
+
+ label = GnLabel('//foo/bar')
+ common = list(consolidate_configs(label, cfg1, cfg2, cfg3, cfg4))
+ self.assertEqual(len(common), 3)
+ self.assertEqual(str(common[0].label), '//foo/bar:bar_public_config1')
+ self.assertEqual(str(common[1].label), '//foo/bar:bar_config1')
+ self.assertEqual(str(common[2].label), '//foo/bar:bar_config2')
+ self.compare(common[0].get('include_dirs'), 'foo')
+ self.compare(common[1].get('cflags'), 'one', 'b')
+ self.compare(common[2].get('cflags'), 'a')
+ self.compare(common[2].get('defines'), 'k=1')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/gn_target_test.py b/pw_build/py/gn_target_test.py
new file mode 100644
index 000000000..204defe97
--- /dev/null
+++ b/pw_build/py/gn_target_test.py
@@ -0,0 +1,227 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the pw_build.gn_target module."""
+
+import unittest
+
+from pw_build.bazel_query import BazelRule
+from pw_build.gn_target import GnTarget
+from pw_build.gn_utils import GnLabel, MalformedGnError
+
+
+class TestGnTarget(unittest.TestCase):
+ """Tests for gn_target.GnTarget."""
+
+ def setUp(self):
+ self.rule = BazelRule('//my-package:my-target', 'cc_library')
+
+ def test_from_bazel_rule_label(self):
+ """Tests the GN target name and package derived from a Bazel rule."""
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(target.name(), 'my-target')
+ self.assertEqual(target.package(), 'my-package')
+
+ def test_from_bazel_rule_type_source_set(self):
+ """Tests creating a `pw_source_set` from a Bazel rule."""
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(target.type(), 'pw_source_set')
+
+ def test_from_bazel_rule_type_static_lib(self):
+ """Tests creating a `pw_static_library` from a Bazel rule."""
+ self.rule.set_attr('linkstatic', True)
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(target.type(), 'pw_static_library')
+
+ def test_from_bazel_rule_type_invalid(self):
+ """Tests creating an invalid type from a Bazel rule."""
+ with self.assertRaises(MalformedGnError):
+ rule = BazelRule('//my-package:my-target', 'custom_type')
+ GnTarget('$build', '$src', bazel=rule)
+
+ def test_from_bazel_rule_visibility(self):
+ """Tests getting the GN visibility from a Bazel rule."""
+ self.rule.set_attr(
+ 'visibility',
+ [
+ '//foo:__subpackages__',
+ '//foo/bar:__pkg__',
+ '//baz:__pkg__',
+ ],
+ )
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ {str(scope) for scope in target.visibility},
+ {
+ '$build/my-package:*',
+ '$build/foo/*',
+ '$build/baz:*',
+ },
+ )
+
+ def test_from_bazel_rule_visibility_public(self):
+ """Tests that 'public' overrides any other visibility"""
+ self.rule.set_attr(
+ 'visibility',
+ [
+ '//visibility:private',
+ '//visibility:public',
+ ],
+ )
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual({str(scope) for scope in target.visibility}, {'//*'})
+
+ def test_from_bazel_rule_visibility_invalid(self):
+ """Tests that and invalid visibility raises an error."""
+ self.rule.set_attr('visibility', ['invalid'])
+ with self.assertRaises(MalformedGnError):
+ GnTarget('$build', '$src', bazel=self.rule)
+
+ def test_from_bazel_rule_testonly_unset(self):
+ """Tests that omitting `testonly` defaults it to False."""
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertFalse(target.testonly)
+
+ def test_from_bazel_rule_testonly_false(self):
+ """Tests setting `testonly` to False."""
+ self.rule.set_attr('testonly', False)
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertFalse(target.testonly)
+
+ def test_from_bazel_rule_testonly_true(self):
+ """Tests setting `testonly` to True."""
+ self.rule.set_attr('testonly', True)
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertTrue(target.testonly)
+
+ def test_from_bazel_rule_public(self):
+ """Tests getting the GN public headers from a Bazel rule."""
+ self.rule.set_attr('hdrs', ['//foo:bar.h', '//:baz.h'])
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ {str(path) for path in target.public},
+ {
+ '$src/foo/bar.h',
+ '$src/baz.h',
+ },
+ )
+
+ def test_from_bazel_rule_sources(self):
+ """Tests getting the GN source files from a Bazel rule."""
+ self.rule.set_attr('srcs', ['//foo:bar.cc', '//:baz.cc'])
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ {str(path) for path in target.sources},
+ {
+ '$src/foo/bar.cc',
+ '$src/baz.cc',
+ },
+ )
+
+ def test_from_bazel_rule_inputs(self):
+ """Tests getting the GN input files from a Bazel rule."""
+ self.rule.set_attr(
+ 'additional_linker_inputs', ['//foo:bar.data', '//:baz.data']
+ )
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ {str(path) for path in target.inputs},
+ {
+ '$src/foo/bar.data',
+ '$src/baz.data',
+ },
+ )
+
+ def test_from_bazel_rule_include_dirs(self):
+ """Tests getting the GN include directories from a Bazel rule."""
+ self.rule.set_attr('includes', ['//foo'])
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ set(target.config.get('include_dirs')),
+ {
+ '$src',
+ '$src/foo',
+ },
+ )
+
+ def test_from_bazel_rule_configs(self):
+ """Tests getting the GN config flags from a Bazel rule."""
+ self.rule.set_attr('defines', ['KEY1=VAL1'])
+ self.rule.set_attr('copts', ['-frobinator'])
+ self.rule.set_attr('linkopts', ['-fizzbuzzer'])
+ self.rule.set_attr('local_defines', ['KEY2=VAL2', 'KEY3=VAL3'])
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ set(target.config.get('public_defines')), {'KEY1=VAL1'}
+ )
+ self.assertEqual(set(target.config.get('cflags')), {'-frobinator'})
+ self.assertEqual(set(target.config.get('ldflags')), {'-fizzbuzzer'})
+ self.assertEqual(
+ set(target.config.get('defines')), {'KEY2=VAL2', 'KEY3=VAL3'}
+ )
+
+ def test_from_bazel_rule_deps(self):
+ """Tests getting the GN dependencies from a Bazel rule."""
+ self.rule.set_attr(
+ 'deps', ['//my-package:foo', '@com_corp_project//bar']
+ )
+ self.rule.set_attr(
+ 'implementation_deps',
+ [
+ '//other-pkg/subdir',
+ '@com_corp_project//:top-level',
+ ],
+ )
+ target = GnTarget('$build', '$src', bazel=self.rule)
+ self.assertEqual(
+ {str(label) for label in target.public_deps},
+ {
+ '$build/my-package:foo',
+ '$repo/bar',
+ },
+ )
+ self.assertEqual(
+ {str(label) for label in target.deps},
+ {
+ '$build/other-pkg/subdir',
+ '$repo:top-level',
+ },
+ )
+
+ def test_make_relative_adjacent(self):
+ """Tests rebasing labels for a target."""
+ target = GnTarget('$build/pkg', '$src')
+ label = target.make_relative(GnLabel('$build/pkg:adjacent-config'))
+ self.assertEqual(label, ":adjacent-config")
+
+ def test_make_relative_absolute(self):
+ """Tests rebasing labels for a target."""
+ target = GnTarget('$build/pkg', '$src')
+ label = target.make_relative(GnLabel('//absolute:config'))
+ self.assertEqual(label, "//absolute:config")
+
+ def test_make_relative_descend(self):
+ """Tests rebasing labels for a target."""
+ target = GnTarget('$build/pkg', '$src')
+ label = target.make_relative(GnLabel('$build/pkg/relative:dep'))
+ self.assertEqual(label, "relative:dep")
+
+ def test_make_relative_ascend(self):
+ """Tests rebasing labels for a target."""
+ target = GnTarget('$build/pkg', '$src')
+ label = target.make_relative(GnLabel('$build/dotdot/relative'))
+ self.assertEqual(label, "../dotdot/relative")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/gn_tests/BUILD.gn b/pw_build/py/gn_tests/BUILD.gn
new file mode 100644
index 000000000..08098fb91
--- /dev/null
+++ b/pw_build/py/gn_tests/BUILD.gn
@@ -0,0 +1,71 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action_test.gni")
+import("$dir_pw_build/python_dist.gni")
+import("$dir_pw_build/python_venv.gni")
+
+pw_python_distribution("test_dist1") {
+ packages = []
+ generate_setup_cfg = {
+ name = "test_dist1"
+ version = "0.0.1"
+ include_default_pyproject_file = true
+ include_extra_files_in_package_data = true
+ }
+ extra_files = [
+ "package_data/empty.csv > test_dist1_data/empty.csv",
+ "package_data/empty.csv > test_dist1_data/subdir/empty.csv",
+ ]
+}
+
+pw_python_action_test("python_package_integration_tests") {
+ action = ":test_dist1._build_wheel($pw_build_PYTHON_TOOLCHAIN)"
+ deps = [
+ ":downstream_project_bundle",
+ ":downstream_project_tools",
+ ]
+ tags = [ "integration" ]
+}
+
+# DOCSTAG: [downstream-project-venv]
+pw_python_distribution("downstream_project_tools") {
+ packages = [
+ "$dir_pw_env_setup/py",
+ "$dir_pw_console/py",
+ ]
+ generate_setup_cfg = {
+ name = "downstream_project_tools"
+ version = "0.0.1"
+ include_default_pyproject_file = true
+ }
+}
+
+pw_python_venv("downstream_tools_build_venv") {
+ path = "$root_build_dir/python-venv-downstream-tools-test"
+ requirements = []
+ constraints =
+ [ "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list" ]
+ source_packages = [ ":downstream_project_tools" ]
+}
+
+# DOCSTAG: [downstream-project-venv]
+
+pw_python_zip_with_setup("downstream_project_bundle") {
+ packages = [ ":downstream_project_tools" ]
+ venv = ":downstream_tools_build_venv"
+}
diff --git a/pw_build/py/gn_tests/package_data/empty.csv b/pw_build/py/gn_tests/package_data/empty.csv
new file mode 100644
index 000000000..7fb4d3d5e
--- /dev/null
+++ b/pw_build/py/gn_tests/package_data/empty.csv
@@ -0,0 +1 @@
+EMPTY.CSV \ No newline at end of file
diff --git a/pw_build/py/gn_utils_test.py b/pw_build/py/gn_utils_test.py
new file mode 100644
index 000000000..ee7f59979
--- /dev/null
+++ b/pw_build/py/gn_utils_test.py
@@ -0,0 +1,351 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the pw_build.gn_utils module."""
+
+import unittest
+
+from pw_build.gn_utils import GnLabel, GnPath, GnVisibility
+
+
+class TestGnPath(unittest.TestCase):
+ """Tests for gn_utils.GnPath."""
+
+ def test_from_bazel(self):
+ """Tests creating a GN path from a Bazel string."""
+ path = GnPath('$dir_3p_test', bazel='//foo:bar/baz.txt')
+ self.assertEqual(path.file(), 'baz.txt')
+ self.assertEqual(path.name(), 'baz')
+ self.assertEqual(path.extension(), 'txt')
+ self.assertEqual(path.dir(), '$dir_3p_test/foo/bar')
+
+ def test_from_gn(self):
+ """Tests creating a GN path from a GN string."""
+ path = GnPath('$dir_3p_test', gn='foo/bar/baz.txt')
+ self.assertEqual(path.file(), 'baz.txt')
+ self.assertEqual(path.name(), 'baz')
+ self.assertEqual(path.extension(), 'txt')
+ self.assertEqual(path.dir(), '$dir_3p_test/foo/bar')
+
+ def test_from_str(self):
+ """Tests creating a GN path from a raw string."""
+ path = GnPath('$dir_3p_test/foo/bar/baz.txt')
+ self.assertEqual(path.file(), 'baz.txt')
+ self.assertEqual(path.name(), 'baz')
+ self.assertEqual(path.extension(), 'txt')
+ self.assertEqual(path.dir(), '$dir_3p_test/foo/bar')
+
+
+class TestGnLabel(unittest.TestCase):
+ """Tests for gn_utils.GnLabel."""
+
+ def test_from_bazel_with_name(self):
+ """Tests creating a GN label from a Bazel string including a name."""
+ label = GnLabel('$dir_3p/test', bazel='//foo/bar:baz')
+ self.assertEqual(label.name(), 'baz')
+ self.assertEqual(label.dir(), '$dir_3p/test/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$dir_3p/test/foo/bar:baz')
+ self.assertEqual(
+ label.with_toolchain(),
+ '$dir_3p/test/foo/bar:baz(default_toolchain)',
+ )
+ self.assertFalse(label.repo())
+
+ def test_from_bazel_without_name(self):
+ """Tests creating a GN label from a Bazel string without a name."""
+ label = GnLabel('$dir_3p/test', bazel='//foo/bar')
+ self.assertEqual(label.name(), 'bar')
+ self.assertEqual(label.dir(), '$dir_3p/test/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$dir_3p/test/foo/bar')
+ self.assertEqual(
+ label.with_toolchain(), '$dir_3p/test/foo/bar(default_toolchain)'
+ )
+ self.assertFalse(label.repo())
+
+ def test_from_bazel_with_external_repo(self):
+ """Tests creating a GN label from a Bazel string with a repo."""
+ label = GnLabel('$dir_3p/test', bazel='@com_corp_project//foo/bar:baz')
+ self.assertEqual(label.name(), 'baz')
+ self.assertEqual(label.dir(), '$repo/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$repo/foo/bar:baz')
+ self.assertEqual(
+ label.with_toolchain(),
+ '$repo/foo/bar:baz(default_toolchain)',
+ )
+ self.assertEqual(label.repo(), 'com_corp_project')
+
+ def test_from_gn_absolute(self):
+ """Tests creating a GN label from an absolute GN label string."""
+ label = GnLabel('$dir_3p/test', gn='//foo/bar:baz')
+ self.assertEqual(label.name(), 'baz')
+ self.assertEqual(label.dir(), '//foo/bar')
+ self.assertEqual(label.no_toolchain(), '//foo/bar:baz')
+ self.assertEqual(
+ label.with_toolchain(), '//foo/bar:baz(default_toolchain)'
+ )
+
+ def test_from_gn_with_variable(self):
+ """Tests creating a GN label from a GN string with a variable."""
+ label = GnLabel('$dir_3p/test', gn='$dir_pw_build/foo/bar')
+ self.assertEqual(label.name(), 'bar')
+ self.assertEqual(label.dir(), '$dir_pw_build/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$dir_pw_build/foo/bar')
+ self.assertEqual(
+ label.with_toolchain(), '$dir_pw_build/foo/bar(default_toolchain)'
+ )
+
+ def test_from_gn_relative(self):
+ """Tests creating a GN label from a relative GN label string."""
+ label = GnLabel('$dir_3p/test', gn='foo/bar')
+ self.assertEqual(label.name(), 'bar')
+ self.assertEqual(label.dir(), '$dir_3p/test/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$dir_3p/test/foo/bar')
+ self.assertEqual(
+ label.with_toolchain(), '$dir_3p/test/foo/bar(default_toolchain)'
+ )
+
+ def test_from_gn_with_dotdot(self):
+ """Tests creating a GN label from a GN string that ascends the tree."""
+ label = GnLabel('$dir_3p/test', gn='../../../foo/../bar')
+ self.assertEqual(label.name(), 'bar')
+ self.assertEqual(label.dir(), '../bar')
+ self.assertEqual(label.no_toolchain(), '../bar')
+ self.assertEqual(label.with_toolchain(), '../bar(default_toolchain)')
+
+ def test_from_str_with_name(self):
+ """Tests creating a GN label from a raw string with a target name."""
+ label = GnLabel('$dir_3p/test/foo/bar:baz')
+ self.assertEqual(label.name(), 'baz')
+ self.assertEqual(label.dir(), '$dir_3p/test/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$dir_3p/test/foo/bar:baz')
+ self.assertEqual(
+ label.with_toolchain(),
+ '$dir_3p/test/foo/bar:baz(default_toolchain)',
+ )
+
+ def test_from_str_without_name(self):
+ """Tests creating a GN label from a raw string without a name."""
+ label = GnLabel('$dir_3p/test/foo/bar')
+ self.assertEqual(label.name(), 'bar')
+ self.assertEqual(label.dir(), '$dir_3p/test/foo/bar')
+ self.assertEqual(label.no_toolchain(), '$dir_3p/test/foo/bar')
+ self.assertEqual(
+ label.with_toolchain(), '$dir_3p/test/foo/bar(default_toolchain)'
+ )
+
+ def test_relative_to_with_name(self):
+ """Tests creating a relative label from a GN label with a name."""
+ label = GnLabel('$dir_3p/foo/bar:baz')
+ self.assertEqual(label.relative_to('$dir_3p'), 'foo/bar:baz')
+ self.assertEqual(label.relative_to('$dir_3p/foo'), 'bar:baz')
+ self.assertEqual(label.relative_to('$dir_3p/foo/bar'), ':baz')
+ self.assertEqual(label.relative_to('$dir_3p/bar'), '../foo/bar:baz')
+ self.assertEqual(label.relative_to('//'), '$dir_3p/foo/bar:baz')
+ self.assertEqual(label.relative_to('$other'), '$dir_3p/foo/bar:baz')
+
+ def test_relative_to_without_name(self):
+ """Tests creating a relative label from a GN label without a name."""
+ label = GnLabel('$dir_3p/foo/bar')
+ self.assertEqual(label.relative_to('$dir_3p/foo/bar'), ':bar')
+
+ def test_relative_to_absolute_with_name(self):
+ """Tests creating a relative label from a named absolute GN label."""
+ label = GnLabel('//foo/bar:baz')
+ self.assertEqual(label.relative_to('//'), 'foo/bar:baz')
+ self.assertEqual(label.relative_to('//bar/baz'), '../../foo/bar:baz')
+
+ def test_relative_to_absolute_without_name(self):
+ """Tests creating a relative label from an unnamed absolute GN label."""
+ label = GnLabel('//foo/bar')
+ self.assertEqual(label.relative_to('//foo/bar'), ':bar')
+
+ def test_resolve_repo_without_repo(self):
+ """Tests trying to set the repo placeholder for a local label."""
+ label = GnLabel('$dir_3p/test', bazel='//foo/bar:baz')
+ self.assertEqual(str(label), '$dir_3p/test/foo/bar:baz')
+ self.assertFalse(label.repo())
+ label.resolve_repo('my-external_repo')
+ self.assertEqual(str(label), '$dir_3p/test/foo/bar:baz')
+ self.assertFalse(label.repo())
+
+ def test_resolve_repowith_repo(self):
+ """Tests setting the repo placeholder."""
+ label = GnLabel('$dir_3p/test', bazel='@com_corp_project//foo/bar:baz')
+ self.assertEqual(str(label), '$repo/foo/bar:baz')
+ self.assertEqual(label.repo(), 'com_corp_project')
+ label.resolve_repo('my-external_repo')
+ self.assertEqual(
+ str(label), '$dir_pw_third_party/my-external_repo/foo/bar:baz'
+ )
+ self.assertEqual(label.repo(), 'com_corp_project')
+
+
+class TestGnVisibility(unittest.TestCase):
+ """Tests for gn_utils.GnVisibility."""
+
+ def test_from_bazel_public(self):
+ """Tests creating a public visibility scope from a Bazel string."""
+ scope = GnVisibility(
+ '$dir_3p/test', '$dir_3p/test/foo', bazel='//visibility:public'
+ )
+ self.assertEqual(str(scope), '//*')
+
+ def test_from_bazel_private(self):
+ """Tests creating a private visibility scope from a Bazel string."""
+ scope = GnVisibility(
+ '$dir_3p/test', '$dir_3p/test/foo', bazel='//visibility:private'
+ )
+ self.assertEqual(str(scope), '$dir_3p/test/foo:*')
+
+ def test_from_bazel_same_subpackage(self):
+ """Tests creating a visibility for the same subpackage."""
+ scope = GnVisibility(
+ '$dir_3p/test', '$dir_3p/test/foo', bazel='//foo:__subpackages__'
+ )
+ self.assertEqual(str(scope), '$dir_3p/test/foo/*')
+
+ def test_from_bazel_same_package(self):
+ """Tests creating a visibility for the same package."""
+ scope = GnVisibility(
+ '$dir_3p/test', '$dir_3p/test/foo', bazel='//foo:__pkg__'
+ )
+ self.assertEqual(str(scope), '$dir_3p/test/foo:*')
+
+ def test_from_bazel_other_subpackages(self):
+ """Tests creating a visibility for a different subpackage."""
+ scope = GnVisibility(
+ '$dir_3p/test', '$dir_3p/test/foo', bazel='//bar:__subpackages__'
+ )
+ self.assertEqual(str(scope), '$dir_3p/test/bar/*')
+
+ def test_from_bazel_other_package(self):
+ """Tests creating a visibility for a different package."""
+ scope = GnVisibility(
+ '$dir_3p/test', '$dir_3p/test/foo', bazel='//bar:__pkg__'
+ )
+ self.assertEqual(str(scope), '$dir_3p/test/bar:*')
+
+ def test_from_gn_relative(self):
+ """Tests creating a visibility from a relative GN string."""
+ scope = GnVisibility('$dir_3p/test', '$dir_3p/test/foo', gn=':*')
+ self.assertEqual(str(scope), '$dir_3p/test/foo:*')
+
+ def test_from_gn_with_dotdot(self):
+ """Tests creating a visibility from a string that ascends the tree."""
+ scope = GnVisibility('$dir_3p/test', '$dir_3p/test/foo', gn='../*')
+ self.assertEqual(str(scope), '$dir_3p/test/*')
+
+ def test_from_gn_absolute(self):
+ """Tests creating a visibility from an absolute GN string."""
+ scope = GnVisibility('$dir_3p/test', '$dir_3p/test/foo', gn='$dir_3p/*')
+ self.assertEqual(str(scope), '$dir_3p/*')
+
+ def test_from_str(self):
+ """Tests creating a visibility from a raw string."""
+ scope = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:*')
+ self.assertEqual(str(scope), '$dir_3p/test/foo/bar:*')
+
+ def test_within_equal_discrete(self):
+ """Tests that two equal, discrete visibilities are within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:baz')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:baz')
+ self.assertTrue(scope1.within(scope2))
+ self.assertTrue(scope2.within(scope1))
+
+ def test_within_equal_globbed(self):
+ """Tests that two equal, globbed visibilities are within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:*')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:*')
+ self.assertTrue(scope1.within(scope2))
+ self.assertTrue(scope2.within(scope1))
+
+ def test_within_equal_subtree(self):
+ """Tests that two equal, subtree visibilities are within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar/*')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar/*')
+ self.assertTrue(scope1.within(scope2))
+ self.assertTrue(scope2.within(scope1))
+
+ def test_within_not_equal_both_discrete(self):
+ """Tests that two unrelated visibilities are not within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:baz')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:qux')
+ self.assertFalse(scope1.within(scope2))
+ self.assertFalse(scope2.within(scope1))
+
+ def test_within_not_equal_both_globbed(self):
+ """Tests that two unrelated visibilities are not within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:*')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/baz:*')
+ self.assertFalse(scope1.within(scope2))
+ self.assertFalse(scope2.within(scope1))
+
+ def test_within_not_equal_both_subtree(self):
+ """Tests that two unrelated visibilities are not within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar/*')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/baz/*')
+ self.assertFalse(scope1.within(scope2))
+ self.assertFalse(scope2.within(scope1))
+
+ def test_within_subset_discrete_in_globbed(self):
+ """Tests a discrete visibility that is within a globbed one."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:baz')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:*')
+ self.assertTrue(scope1.within(scope2))
+ self.assertFalse(scope2.within(scope1))
+
+ def test_within_subset_discrete_in_subtree(self):
+ """Tests a discrete visibility that is within a subtree."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:baz')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/*')
+ public_scope = GnVisibility('$dir_3p/test', '//*')
+ self.assertTrue(scope1.within(scope2))
+ self.assertTrue(scope1.within(public_scope))
+ self.assertFalse(scope2.within(scope1))
+ self.assertFalse(public_scope.within(scope1))
+
+ def test_within_subset_globbed_in_subtree(self):
+ """Tests a globbed visibility that is within a subtree."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar:*')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/*')
+ public_scope = GnVisibility('$dir_3p/test', '//*')
+ self.assertTrue(scope1.within(scope2))
+ self.assertTrue(scope1.within(public_scope))
+ self.assertFalse(scope2.within(scope1))
+ self.assertFalse(public_scope.within(scope1))
+
+ def test_within_subset_subtree_in_subtree(self):
+ """Tests a subtree visibility that is within a subtree."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/bar/*')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/*')
+ public_scope = GnVisibility('$dir_3p/test', '//*')
+ self.assertTrue(scope1.within(scope2))
+ self.assertTrue(scope1.within(public_scope))
+ self.assertFalse(scope2.within(scope1))
+ self.assertFalse(public_scope.within(scope1))
+
+ def test_within_disjoint(self):
+ """Tests that disjoint visibilities are not within each other."""
+ scope1 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo:bar')
+ scope2 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/baz:*')
+ scope3 = GnVisibility('$dir_3p/test', '$dir_3p/test/foo/qux/*')
+ self.assertFalse(scope1.within(scope2))
+ self.assertFalse(scope1.within(scope3))
+ self.assertFalse(scope2.within(scope1))
+ self.assertFalse(scope2.within(scope3))
+ self.assertFalse(scope3.within(scope1))
+ self.assertFalse(scope3.within(scope2))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/gn_writer_test.py b/pw_build/py/gn_writer_test.py
new file mode 100644
index 000000000..88da2d040
--- /dev/null
+++ b/pw_build/py/gn_writer_test.py
@@ -0,0 +1,324 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the pw_build.gn_writer module."""
+
+import os
+import unittest
+
+from io import StringIO
+from pathlib import PurePath
+from tempfile import TemporaryDirectory
+
+from pw_build.gn_config import GnConfig
+from pw_build.gn_writer import (
+ COPYRIGHT_HEADER,
+ GnFile,
+ GnWriter,
+)
+from pw_build.gn_target import GnTarget
+from pw_build.gn_utils import MalformedGnError
+
+
+class TestGnWriter(unittest.TestCase):
+ """Tests for gn_writer.GnWriter."""
+
+ def setUp(self):
+ """Creates a GnWriter that writes to a StringIO."""
+ self.reset()
+
+ def reset(self):
+ """Resets the writer and output."""
+ self.output = StringIO()
+ self.writer = GnWriter(self.output)
+
+ def test_write_comment(self):
+ """Writes a GN comment."""
+ self.writer.write_comment('hello, world!')
+ self.assertEqual(
+ self.output.getvalue(),
+ '# hello, world!\n',
+ )
+
+ def test_write_comment_wrap(self):
+ """Writes a GN comment that is exactly 80 characters."""
+ extra_long = (
+ "This line is a " + ("really, " * 5) + "REALLY extra long comment"
+ )
+ self.writer.write_comment(extra_long)
+ self.assertEqual(
+ self.output.getvalue(),
+ '# This line is a really, really, really, really, really, REALLY '
+ 'extra long\n# comment\n',
+ )
+
+ def test_write_comment_nowrap(self):
+ """Writes a long GN comment without whitespace to wrap on."""
+ no_breaks = 'A' + ('a' * 76) + 'h!'
+ self.writer.write_comment(no_breaks)
+ self.assertEqual(
+ self.output.getvalue(),
+ '# Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
+ 'aaaaaaaaaaaaah!\n',
+ )
+
+ def test_write_imports(self):
+ """Writes GN import statements."""
+ self.writer.write_import('foo.gni')
+ self.writer.write_imports(['bar.gni', 'baz.gni'])
+ lines = [
+ 'import("foo.gni")',
+ 'import("bar.gni")',
+ 'import("baz.gni")',
+ ]
+ self.assertEqual('\n'.join(lines), self.output.getvalue().strip())
+
+ def test_write_config(self):
+ """Writes a GN config."""
+ config = GnConfig(
+ json='''{
+ "label": "$dir_3p/test:my-config",
+ "cflags": ["-frobinator", "-fizzbuzzer"],
+ "defines": ["KEY=VAL"],
+ "public": true,
+ "usages": 1
+ }'''
+ )
+ self.writer.write_config(config)
+ lines = [
+ 'config("my-config") {',
+ ' cflags = [',
+ ' "-fizzbuzzer",',
+ ' "-frobinator",',
+ ' ]',
+ ' defines = [',
+ ' "KEY=VAL",',
+ ' ]',
+ '}',
+ ]
+ self.assertEqual('\n'.join(lines), self.output.getvalue().strip())
+
+ def test_write_target(self):
+ """Tests writing the target using a GnWriter."""
+ target = GnTarget(
+ '$build',
+ '$src',
+ json='''{
+ "target_type": "custom_type",
+ "target_name": "my-target",
+ "package": "my-package"
+ }''',
+ )
+ target.add_visibility(bazel='//visibility:private')
+ target.add_visibility(bazel='//foo:__subpackages__')
+ target.add_path('public', bazel='//foo:my-header.h')
+ target.add_path('sources', bazel='//foo:my-source.cc')
+ target.add_path('inputs', bazel='//bar:my.data')
+ target.config.add('cflags', '-frobinator')
+ target.add_dep(public=True, bazel='//my-package:foo')
+ target.add_dep(public=True, bazel='@com_corp_repo//bar')
+ target.add_dep(bazel='//other-pkg/baz')
+ target.add_dep(bazel='@com_corp_repo//:top-level')
+
+ output = StringIO()
+ writer = GnWriter(output)
+ writer.repos = {'com_corp_repo': 'repo'}
+ writer.aliases = {'$build/other-pkg/baz': '$build/another-pkg/baz'}
+ writer.write_target(target)
+
+ self.assertEqual(
+ output.getvalue(),
+ '''
+# Generated from //my-package:my-target
+custom_type("my-target") {
+ visibility = [
+ "../foo/*",
+ ":*",
+ ]
+ public = [
+ "$src/foo/my-header.h",
+ ]
+ sources = [
+ "$src/foo/my-source.cc",
+ ]
+ inputs = [
+ "$src/bar/my.data",
+ ]
+ cflags = [
+ "-frobinator",
+ ]
+ public_deps = [
+ "$dir_pw_third_party/repo/bar",
+ ":foo",
+ ]
+ deps = [
+ "$dir_pw_third_party/repo:top-level",
+ "../another-pkg/baz",
+ ]
+}
+'''.lstrip(),
+ )
+
+ def test_write_target_public_visibility(self):
+ """Tests writing a globbaly visible target using a GnWriter."""
+ target = GnTarget(
+ '$build',
+ '$src',
+ json='''{
+ "target_type": "custom_type",
+ "target_name": "my-target",
+ "package": "my-package"
+ }''',
+ )
+ target.add_visibility(bazel='//visibility:private')
+ target.add_visibility(bazel='//visibility:public')
+
+ output = StringIO()
+ writer = GnWriter(output)
+ writer.repos = {'com_corp_repo': 'repo'}
+ writer.aliases = {'$build/other-pkg/baz': '$build/another-pkg/baz'}
+ writer.write_target(target)
+
+ self.assertEqual(
+ output.getvalue(),
+ '''
+# Generated from //my-package:my-target
+custom_type("my-target") {
+}
+'''.lstrip(),
+ )
+
+ def test_write_list(self):
+ """Writes a GN list assigned to a variable."""
+ self.writer.write_list('empty', [])
+ self.writer.write_list('items', ['foo', 'bar', 'baz'])
+ lines = [
+ 'items = [',
+ ' "bar",',
+ ' "baz",',
+ ' "foo",',
+ ']',
+ ]
+ self.assertEqual('\n'.join(lines), self.output.getvalue().strip())
+
+ def test_write_scope(self):
+ """Writes a GN scope assigned to a variable."""
+ self.writer.write_scope('outer')
+ self.writer.write('key1 = "val1"')
+ self.writer.write_scope('inner')
+ self.writer.write('key2 = "val2"')
+ self.writer.write_end()
+ self.writer.write('key3 = "val3"')
+ self.writer.write_end()
+ lines = [
+ 'outer = {',
+ ' key1 = "val1"',
+ ' inner = {',
+ ' key2 = "val2"',
+ ' }',
+ '',
+ ' key3 = "val3"',
+ '}',
+ ]
+ self.assertEqual('\n'.join(lines), self.output.getvalue().strip())
+
+ def test_write_if_else_end(self):
+ """Writes GN conditional statements."""
+ self.writer.write_if('current_os == "linux"')
+ self.writer.write('mascot = "penguin"')
+ self.writer.write_else_if('current_os == "mac"')
+ self.writer.write('mascot = "dogcow"')
+ self.writer.write_else_if('current_os == "win"')
+ self.writer.write('mascot = "clippy"')
+ self.writer.write_else()
+ self.writer.write('mascot = "dropbear"')
+ self.writer.write_end()
+ lines = [
+ 'if (current_os == "linux") {',
+ ' mascot = "penguin"',
+ '} else if (current_os == "mac") {',
+ ' mascot = "dogcow"',
+ '} else if (current_os == "win") {',
+ ' mascot = "clippy"',
+ '} else {',
+ ' mascot = "dropbear"',
+ '}',
+ ]
+ self.assertEqual('\n'.join(lines), self.output.getvalue().strip())
+
+ def test_write_unclosed_target(self):
+ """Triggers an error from an unclosed GN scope."""
+ self.writer.write_target_start('unclosed', 'target')
+ with self.assertRaises(MalformedGnError):
+ self.writer.seal()
+
+ def test_write_unclosed_scope(self):
+ """Triggers an error from an unclosed GN scope."""
+ self.writer.write_scope('unclosed_scope')
+ with self.assertRaises(MalformedGnError):
+ self.writer.seal()
+
+ def test_write_unclosed_if(self):
+ """Triggers an error from an unclosed GN condition."""
+ self.writer.write_if('var == "unclosed-if"')
+ with self.assertRaises(MalformedGnError):
+ self.writer.seal()
+
+ def test_write_unclosed_else_if(self):
+ """Triggers an error from an unclosed GN condition."""
+ self.writer.write_if('var == "closed-if"')
+ self.writer.write_else_if('var == "unclosed-else-if"')
+ with self.assertRaises(MalformedGnError):
+ self.writer.seal()
+
+ def test_write_unclosed_else(self):
+ """Triggers an error from an unclosed GN condition."""
+ self.writer.write_if('var == "closed-if"')
+ self.writer.write_else_if('var == "closed-else-if"')
+ self.writer.write_else()
+ with self.assertRaises(MalformedGnError):
+ self.writer.seal()
+
+
+class TestGnFile(unittest.TestCase):
+ """Tests for gn_writer.GnFile."""
+
+ def test_format_on_close(self):
+ """Verifies the GN file is formatted when the file is closed."""
+ with TemporaryDirectory() as tmpdirname:
+ with GnFile(PurePath(tmpdirname, 'BUILD.gn')) as build_gn:
+ build_gn.write(' correct = "indent"')
+ build_gn.write_comment('newline before comment')
+ build_gn.write_scope('no_newline_before_item')
+ build_gn.write_list('single_item', ['is.inlined'])
+ build_gn.write_end()
+
+ filename = PurePath('pw_build', 'gn_writer.py')
+ expected = (
+ COPYRIGHT_HEADER
+ + f'''
+# This file was automatically generated by {filename}
+
+correct = "indent"
+
+# newline before comment
+no_newline_before_item = {{
+ single_item = [ "is.inlined" ]
+}}'''
+ )
+ with open(os.path.join(tmpdirname, 'BUILD.gn'), 'r') as build_gn:
+ self.assertEqual(expected.strip(), build_gn.read().strip())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/pw_build/bazel_query.py b/pw_build/py/pw_build/bazel_query.py
new file mode 100644
index 000000000..536f2fc8e
--- /dev/null
+++ b/pw_build/py/pw_build/bazel_query.py
@@ -0,0 +1,302 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Parses Bazel rules from a local Bazel workspace."""
+
+import json
+import logging
+import os
+import subprocess
+
+from io import StringIO
+from pathlib import Path, PurePosixPath
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ IO,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Union,
+)
+from xml.etree import ElementTree
+
+_LOG = logging.getLogger(__name__)
+
+
+BazelValue = Union[bool, int, str, List[str], Dict[str, str]]
+
+
+class ParseError(Exception):
+ """Raised when a Bazel query returns data that can't be parsed."""
+
+
+def parse_invalid(attr: Dict[str, Any]) -> BazelValue:
+ """Raises an error that a type is unrecognized."""
+ attr_type = attr['type']
+ raise ParseError(f'unknown type: {attr_type}, expected one of {BazelValue}')
+
+
+class BazelRule:
+ """Represents a Bazel rule as parsed from the query results."""
+
+ def __init__(self, label: str, kind: str) -> None:
+ """Create a Bazel rule.
+
+ Args:
+ label: An absolute Bazel label corresponding to this rule.
+ kind: The type of Bazel rule, e.g. cc_library.
+ """
+ if not label.startswith('//'):
+ raise ParseError(f'invalid label: {label}')
+ if ':' in label:
+ parts = label.split(':')
+ if len(parts) != 2:
+ raise ParseError(f'invalid label: {label}')
+ self._package = parts[0][2:]
+ self._target = parts[1]
+ else:
+ self._package = str(label)[2:]
+ self._target = PurePosixPath(label).name
+ self._kind = kind
+
+ self._attrs: Dict[str, BazelValue] = {}
+
+ def package(self) -> str:
+ """Returns the package portion of this rule's name."""
+ return self._package
+
+ def label(self) -> str:
+ """Returns this rule's full target name."""
+ return f'//{self._package}:{self._target}'
+
+ def kind(self) -> str:
+ """Returns this rule's target type."""
+ return self._kind
+
+ def parse(self, attrs: Iterable[Dict[str, Any]]) -> None:
+ """Maps JSON data from a bazel query into this object.
+
+ Args:
+ attrs: A dictionary of attribute names and values for the Bazel
+ rule. These should match the output of
+ `bazel cquery ... --output=jsonproto`.
+ """
+ attr_parsers: Dict[str, Callable[[Dict[str, Any]], BazelValue]] = {
+ 'boolean': lambda attr: attr.get('booleanValue', False),
+ 'integer': lambda attr: int(attr.get('intValue', '0')),
+ 'string': lambda attr: attr.get('stringValue', ''),
+ 'string_list': lambda attr: attr.get('stringListValue', []),
+ 'label_list': lambda attr: attr.get('stringListValue', []),
+ 'string_dict': lambda attr: {
+ p['key']: p['value'] for p in attr.get('stringDictValue', [])
+ },
+ }
+ for attr in attrs:
+ if 'explicitlySpecified' not in attr:
+ continue
+ if not attr['explicitlySpecified']:
+ continue
+ try:
+ attr_name = attr['name']
+ except KeyError:
+ raise ParseError(
+ f'missing "name" in {json.dumps(attr, indent=2)}'
+ )
+ try:
+ attr_type = attr['type'].lower()
+ except KeyError:
+ raise ParseError(
+ f'missing "type" in {json.dumps(attr, indent=2)}'
+ )
+
+ attr_parser = attr_parsers.get(attr_type, parse_invalid)
+ self._attrs[attr_name] = attr_parser(attr)
+
+ def has_attr(self, attr_name: str) -> bool:
+ """Returns whether the rule has an attribute of the given name.
+
+ Args:
+ attr_name: The name of the attribute.
+ """
+ return attr_name in self._attrs
+
+ def get_bool(self, attr_name: str) -> bool:
+ """Gets the value of a boolean attribute.
+
+ Args:
+ attr_name: The name of the boolean attribute.
+ """
+ val = self._attrs.get(attr_name, False)
+ assert isinstance(val, bool)
+ return val
+
+ def get_int(self, attr_name: str) -> int:
+ """Gets the value of an integer attribute.
+
+ Args:
+ attr_name: The name of the integer attribute.
+ """
+ val = self._attrs.get(attr_name, 0)
+ assert isinstance(val, int)
+ return val
+
+ def get_str(self, attr_name: str) -> str:
+ """Gets the value of a string attribute.
+
+ Args:
+ attr_name: The name of the string attribute.
+ """
+ val = self._attrs.get(attr_name, '')
+ assert isinstance(val, str)
+ return val
+
+ def get_list(self, attr_name: str) -> List[str]:
+ """Gets the value of a string list attribute.
+
+ Args:
+ attr_name: The name of the string list attribute.
+ """
+ val = self._attrs.get(attr_name, [])
+ assert isinstance(val, list)
+ return val
+
+ def get_dict(self, attr_name: str) -> Dict[str, str]:
+ """Gets the value of a string list attribute.
+
+ Args:
+ attr_name: The name of the string list attribute.
+ """
+ val = self._attrs.get(attr_name, {})
+ assert isinstance(val, dict)
+ return val
+
+ def set_attr(self, attr_name: str, value: BazelValue) -> None:
+ """Sets the value of an attribute.
+
+ Args:
+ attr_name: The name of the attribute.
+ value: The value to set.
+ """
+ self._attrs[attr_name] = value
+
+
+class BazelWorkspace:
+ """Represents a local instance of a Bazel repository.
+
+ Attributes:
+ root: Path to the local instance of a Bazel workspace.
+ packages: Bazel packages mapped to their default visibility.
+ repo: The name of Bazel workspace.
+ """
+
+ def __init__(self, pathname: Path) -> None:
+ """Creates an object representing a Bazel workspace at the given path.
+
+ Args:
+ pathname: Path to the local instance of a Bazel workspace.
+ """
+ self.root: Path = pathname
+ self.packages: Dict[str, str] = {}
+ self.repo: str = os.path.basename(pathname)
+
+ def get_rules(self, kind: str) -> Iterator[BazelRule]:
+ """Returns rules matching the given kind, e.g. 'cc_library'."""
+ self._load_packages()
+ results = self._query(
+ 'cquery', f'kind({kind}, //...)', '--output=jsonproto'
+ )
+ json_data = json.loads(results)
+ for result in json_data.get('results', []):
+ rule_data = result['target']['rule']
+ target = rule_data['name']
+ rule = BazelRule(target, kind)
+ default_visibility = self.packages[rule.package()]
+ rule.set_attr('visibility', [default_visibility])
+ rule.parse(rule_data['attribute'])
+ yield rule
+
+ def _load_packages(self) -> None:
+ """Scans the workspace for packages and their default visibilities."""
+ if self.packages:
+ return
+ packages = self._query('query', '//...', '--output=package').split('\n')
+ for package in packages:
+ results = self._query(
+ 'query', f'buildfiles(//{package}:*)', '--output=xml'
+ )
+ xml_data = ElementTree.fromstring(results)
+ for pkg_elem in xml_data:
+ if not pkg_elem.attrib['name'].startswith(f'//{package}:'):
+ continue
+ for elem in pkg_elem:
+ if elem.tag == 'visibility-label':
+ self.packages[package] = elem.attrib['name']
+
+ def _query(self, *args: str) -> str:
+ """Invokes `bazel cquery` with the given selector."""
+ output = StringIO()
+ self._exec(*args, '--noshow_progress', output=output)
+ return output.getvalue()
+
+ def _exec(self, *args: str, output: Optional[IO] = None) -> None:
+ """Execute a Bazel command in the workspace."""
+ cmdline = ['bazel'] + list(args) + ['--noshow_progress']
+ result = subprocess.run(
+ cmdline,
+ cwd=self.root,
+ capture_output=True,
+ )
+ if not result.stdout:
+ _LOG.error(result.stderr.decode('utf-8'))
+ raise ParseError(f'Failed to query Bazel workspace: {self.root}')
+ if output:
+ output.write(result.stdout.decode('utf-8').strip())
+
+ def run(self, label: str, *args, output: Optional[IO] = None) -> None:
+ """Invokes `bazel run` on the given label.
+
+ Args:
+ label: A Bazel target, e.g. "@repo//package:target".
+ args: Additional options to pass to `bazel`.
+ output: Optional destination for the output of the command.
+ """
+ self._exec('run', label, *args, output=output)
+
+ def revision(self) -> str:
+ try:
+ result = subprocess.run(
+ ['git', 'rev-parse', 'HEAD'],
+ cwd=self.root,
+ check=True,
+ capture_output=True,
+ )
+ except subprocess.CalledProcessError as error:
+ print(error.stderr.decode('utf-8'))
+ raise
+ return result.stdout.decode('utf-8').strip()
+
+ def url(self) -> str:
+ try:
+ result = subprocess.run(
+ ['git', 'remote', 'get-url', 'origin'],
+ cwd=self.root,
+ check=True,
+ capture_output=True,
+ )
+ except subprocess.CalledProcessError as error:
+ print(error.stderr.decode('utf-8'))
+ raise
+ return result.stdout.decode('utf-8').strip()
diff --git a/pw_build/py/pw_build/build_recipe.py b/pw_build/py/pw_build/build_recipe.py
index ebd4c1b55..7df5e37f5 100644
--- a/pw_build/py/pw_build/build_recipe.py
+++ b/pw_build/py/pw_build/build_recipe.py
@@ -101,8 +101,6 @@ class BuildCommand:
"""Return flags that appear immediately after the build command."""
assert self.build_system_command
assert self.build_dir
- if self.build_system_command.endswith('bazel'):
- return ['--output_base', str(self.build_dir)]
return []
def _get_build_system_args(self) -> List[str]:
@@ -110,15 +108,18 @@ class BuildCommand:
assert self.build_dir
# Both make and ninja use -C for a build directory.
- if self.build_system_command.endswith(
- 'make'
- ) or self.build_system_command.endswith('ninja'):
+ if self.make_command() or self.ninja_command():
return ['-C', str(self.build_dir), *self.targets]
- # Bazel relies on --output_base which is handled by the
- # _get_starting_build_system_args() function.
- if self.build_system_command.endswith('bazel'):
- return [*self.targets]
+ if self.bazel_command():
+ # Bazel doesn't use -C for the out directory. Instead we use
+ # --symlink_prefix to save some outputs to the desired
+ # location. This is the same pattern used by pw_presubmit.
+ bazel_args = ['--symlink_prefix', str(self.build_dir / 'bazel-')]
+ if self.bazel_clean_command():
+ # Targets are unrecognized args for bazel clean
+ return bazel_args
+ return bazel_args + [*self.targets]
raise UnknownBuildSystem(
f'\n\nUnknown build system command "{self.build_system_command}" '
@@ -144,29 +145,32 @@ class BuildCommand:
resolved_args.append(arg)
return resolved_args
+ def make_command(self) -> bool:
+ return (
+ self.build_system_command is not None
+ and self.build_system_command.endswith('make')
+ )
+
def ninja_command(self) -> bool:
- if self.build_system_command and self.build_system_command.endswith(
- 'ninja'
- ):
- return True
- return False
+ return (
+ self.build_system_command is not None
+ and self.build_system_command.endswith('ninja')
+ )
def bazel_command(self) -> bool:
- if self.build_system_command and self.build_system_command.endswith(
- 'bazel'
- ):
- return True
- return False
+ return (
+ self.build_system_command is not None
+ and self.build_system_command.endswith('bazel')
+ )
def bazel_build_command(self) -> bool:
- if self.bazel_command() and 'build' in self.build_system_extra_args:
- return True
- return False
+ return self.bazel_command() and 'build' in self.build_system_extra_args
+
+ def bazel_test_command(self) -> bool:
+ return self.bazel_command() and 'test' in self.build_system_extra_args
def bazel_clean_command(self) -> bool:
- if self.bazel_command() and 'clean' in self.build_system_extra_args:
- return True
- return False
+ return self.bazel_command() and 'clean' in self.build_system_extra_args
def get_args(
self,
@@ -191,9 +195,7 @@ class BuildCommand:
if additional_bazel_args and self.bazel_command():
extra_args.extend(additional_bazel_args)
- build_system_target_args = []
- if not self.bazel_clean_command():
- build_system_target_args = self._get_build_system_args()
+ build_system_target_args = self._get_build_system_args()
# Construct the build system command args.
command = [
@@ -234,6 +236,29 @@ class BuildRecipeStatus:
lines.append(line)
self.error_lines[self.error_count] = lines
+ def has_empty_ninja_errors(self) -> bool:
+ for error_lines in self.error_lines.values():
+ # NOTE: There will be at least 2 lines for each ninja failure:
+ # - A starting 'FAILED: target' line
+ # - An ending line with this format:
+ # 'ninja: error: ... cannot make progress due to previous errors'
+
+ # If the total error line count is very short, assume it's an empty
+ # ninja error.
+ if len(error_lines) <= 3:
+ # If there is a failure in the regen step, there will be 3 error
+ # lines: The above two and one more with the regen command.
+ return True
+ # Otherwise, if the line starts with FAILED: build.ninja the failure
+ # is likely in the regen step and there will be extra cmake or gn
+ # error text that was not captured.
+ for line in error_lines:
+ if line.startswith(
+ '\033[31mFAILED: \033[0mbuild.ninja'
+ ) or line.startswith('FAILED: build.ninja'):
+ return True
+ return False
+
def increment_error_count(self, count: int = 1) -> None:
self.error_count += count
if self.error_count not in self.error_lines:
diff --git a/pw_build/py/pw_build/collect_wheels.py b/pw_build/py/pw_build/collect_wheels.py
index 5b4da5b55..92be86a0f 100644
--- a/pw_build/py/pw_build/collect_wheels.py
+++ b/pw_build/py/pw_build/collect_wheels.py
@@ -31,15 +31,15 @@ def _parse_args():
help='Root search path to use in conjunction with --wheels_file',
)
parser.add_argument(
- '--suffix_file',
- type=argparse.FileType('r'),
+ '--suffix-file',
+ type=Path,
help=(
'File that lists subdirs relative to --prefix, one per line,'
'to search for .whl files to copy into --out_dir'
),
)
parser.add_argument(
- '--out_dir',
+ '--out-dir',
type=Path,
help='Path where all the built and collected .whl files should be put',
)
@@ -47,17 +47,28 @@ def _parse_args():
return parser.parse_args()
-def copy_wheels(prefix, suffix_file, out_dir):
+def copy_wheels(prefix: Path, suffix_file: Path, out_dir: Path) -> None:
+ """Copy Python wheels or source archives to the out_dir.
+
+ Raises:
+ FileExistsError: If any separate wheel files are copied to the same
+ destination file path.
+ """
if not out_dir.exists():
out_dir.mkdir()
copied_files: Dict[str, Path] = dict()
- for suffix in suffix_file.readlines():
+ requirements_content: str = ''
+
+ for suffix in suffix_file.read_text().splitlines():
path = prefix / suffix.strip()
_LOG.debug('Searching for wheels in %s', path)
if path == out_dir:
continue
- for wheel in path.glob('**/*.whl'):
+ for wheel in path.iterdir():
+ if wheel.suffix not in ('.gz', '.whl'):
+ continue
+
if wheel.name in copied_files:
_LOG.error(
'Attempting to override %s with %s',
@@ -71,9 +82,20 @@ def copy_wheels(prefix, suffix_file, out_dir):
copied_files[wheel.name] = wheel
_LOG.debug('Copying %s to %s', wheel, out_dir)
shutil.copy(wheel, out_dir)
+ requirements_file = wheel.parent / 'requirements.txt'
+
+ if requirements_file.is_file():
+ requirements_content += '\n'
+ requirements_content += requirements_file.read_text()
+
+ if requirements_content:
+ (out_dir / 'requirements.txt').write_text(
+ '# Auto-generated requirements.txt\n' + requirements_content,
+ encoding='utf-8',
+ )
-def main():
+def main() -> None:
copy_wheels(**vars(_parse_args()))
diff --git a/pw_build/py/pw_build/create_gn_venv.py b/pw_build/py/pw_build/create_gn_venv.py
index e31e0ec2f..f03665d1f 100644
--- a/pw_build/py/pw_build/create_gn_venv.py
+++ b/pw_build/py/pw_build/create_gn_venv.py
@@ -14,24 +14,71 @@
"""Crate a venv."""
import argparse
+import os
+import pathlib
+import platform
+import shutil
+import stat
+import sys
import venv
-from pathlib import Path
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
+ '--depfile',
+ type=pathlib.Path,
+ required=True,
+ help='Path at which a depfile should be written.',
+ )
+ parser.add_argument(
'--destination-dir',
- type=Path,
+ type=pathlib.Path,
required=True,
help='Path to venv directory.',
)
+ parser.add_argument(
+ '--stampfile',
+ type=pathlib.Path,
+ required=True,
+ help="Path to this target's stamp file.",
+ )
return parser.parse_args()
-def main(destination_dir: Path) -> None:
- if not destination_dir.is_dir():
- venv.create(destination_dir, symlinks=True, with_pip=True)
+def _rm_dir(path_to_delete: pathlib.Path) -> None:
+ """Delete a directory recursively.
+
+ On Windows if a file can't be deleted, mark it as writable then delete.
+ """
+
+ def make_writable_and_delete(_func, path, _exc_info):
+ os.chmod(path, stat.S_IWRITE)
+ os.unlink(path)
+
+ on_rm_error = None
+ if platform.system() == 'Windows':
+ on_rm_error = make_writable_and_delete
+ shutil.rmtree(path_to_delete, onerror=on_rm_error)
+
+
+def main(
+ depfile: pathlib.Path,
+ destination_dir: pathlib.Path,
+ stampfile: pathlib.Path,
+) -> None:
+ # Create the virtualenv.
+ if destination_dir.exists():
+ _rm_dir(destination_dir)
+ venv.create(destination_dir, symlinks=True, with_pip=True)
+
+ # Write out the depfile, making sure the Python path is
+ # relative to the outdir so that this doesn't add user-specific
+ # info to the build.
+ out_dir_path = pathlib.Path(os.getcwd()).resolve()
+ python_path = pathlib.Path(sys.executable).resolve()
+ rel_python_path = os.path.relpath(python_path, start=out_dir_path)
+ depfile.write_text(f'{stampfile}: \\\n {rel_python_path}')
if __name__ == '__main__':
diff --git a/pw_build/py/pw_build/create_python_tree.py b/pw_build/py/pw_build/create_python_tree.py
index 981f12d5d..6a47ffcc9 100644
--- a/pw_build/py/pw_build/create_python_tree.py
+++ b/pw_build/py/pw_build/create_python_tree.py
@@ -22,7 +22,7 @@ import re
import shutil
import subprocess
import tempfile
-from typing import Iterable, Optional
+from typing import Iterable, List, Optional
import setuptools # type: ignore
@@ -96,6 +96,21 @@ def _parse_args():
)
parser.add_argument(
+ '--setupcfg-extra-files-in-package-data',
+ action='store_true',
+ help='List --extra-files in [options.package_data]',
+ )
+
+ parser.add_argument(
+ '--auto-create-package-data-init-py-files',
+ action='store_true',
+ help=(
+ 'Create __init__.py files as needed in subdirs of extra_files '
+ 'when including in [options.package_data].'
+ ),
+ )
+
+ parser.add_argument(
'--input-list-files',
nargs='+',
type=Path,
@@ -192,6 +207,16 @@ def load_common_config(
return config
+def _update_package_data_value(
+ config: configparser.ConfigParser, key: str, value: str
+) -> None:
+ existing_values = config['options.package_data'].get(key, '').splitlines()
+ new_value = '\n'.join(sorted(set(existing_values + value.splitlines())))
+ # Remove any empty lines
+ new_value = new_value.replace('\n\n', '\n')
+ config['options.package_data'][key] = new_value
+
+
def update_config_with_packages(
config: configparser.ConfigParser,
python_packages: Iterable[PythonPackage],
@@ -230,15 +255,7 @@ def update_config_with_packages(
# Collect package_data
if pkg.config.has_section('options.package_data'):
for key, value in pkg.config['options.package_data'].items():
- existing_values = (
- config['options.package_data'].get(key, '').splitlines()
- )
- new_value = '\n'.join(
- sorted(set(existing_values + value.splitlines()))
- )
- # Remove any empty lines
- new_value = new_value.replace('\n\n', '\n')
- config['options.package_data'][key] = new_value
+ _update_package_data_value(config, key, value)
# Collect entry_points
if pkg.config.has_section('options.entry_points'):
@@ -252,6 +269,36 @@ def update_config_with_packages(
config['options.entry_points'][key] = new_entry_points
+def update_config_with_package_data(
+ config: configparser.ConfigParser,
+ extra_files_list: List[Path],
+ auto_create_init_py_files: bool,
+ tree_destination_dir: Path,
+) -> None:
+ """Create options.package_data entries from a list of paths."""
+ for path in extra_files_list:
+ relative_file = path.relative_to(tree_destination_dir)
+
+ # Update options.package_data config section
+ root_package_dir = list(relative_file.parents)[-2]
+ _update_package_data_value(
+ config,
+ str(root_package_dir),
+ str(relative_file.relative_to(root_package_dir)),
+ )
+
+ # Add an __init__.py file to subdirectories
+ if (
+ auto_create_init_py_files
+ and relative_file.parent != tree_destination_dir
+ ):
+ init_py = (
+ tree_destination_dir / relative_file.parent / '__init__.py'
+ )
+ if not init_py.exists():
+ init_py.touch()
+
+
def write_config(
final_config: configparser.ConfigParser,
tree_destination_dir: Path,
@@ -345,10 +392,12 @@ def build_python_tree(
shutil.rmtree(lib_dir_path, ignore_errors=True)
-def copy_extra_files(extra_file_strings: Iterable[str]) -> None:
+def copy_extra_files(extra_file_strings: Iterable[str]) -> List[Path]:
"""Copy extra files to their destinations."""
+ output_files: List[Path] = []
+
if not extra_file_strings:
- return
+ return output_files
for extra_file_string in extra_file_strings:
# Convert 'source > destination' strings to Paths.
@@ -371,6 +420,9 @@ def copy_extra_files(extra_file_strings: Iterable[str]) -> None:
)
shutil.copy(source_file, dest_file)
+ output_files.append(dest_file)
+
+ return output_files
def _main():
@@ -387,7 +439,7 @@ def _main():
tree_destination_dir=args.tree_destination_dir,
include_tests=args.include_tests,
)
- copy_extra_files(args.extra_files)
+ extra_files_list = copy_extra_files(args.extra_files)
if args.create_default_pyproject_toml:
pyproject_path = args.tree_destination_dir / 'pyproject.toml'
@@ -407,6 +459,14 @@ def _main():
update_config_with_packages(config=config, python_packages=py_packages)
+ if args.setupcfg_extra_files_in_package_data:
+ update_config_with_package_data(
+ config,
+ extra_files_list,
+ args.auto_create_package_data_init_py_files,
+ args.tree_destination_dir,
+ )
+
write_config(
common_config=args.setupcfg_common_file,
final_config=config,
diff --git a/pw_build/py/pw_build/file_prefix_map.py b/pw_build/py/pw_build/file_prefix_map.py
index 5d6cfadb8..0497e998f 100644
--- a/pw_build/py/pw_build/file_prefix_map.py
+++ b/pw_build/py/pw_build/file_prefix_map.py
@@ -59,6 +59,7 @@ def remap_paths(paths: List[str], prefix_maps: PrefixMaps) -> Iterator[str]:
for from_prefix, to_prefix in prefix_maps:
if path.startswith(from_prefix):
path = path.replace(from_prefix, to_prefix, 1)
+ break # Only the first -ffile-prefix-map option applies.
yield path
diff --git a/pw_build/py/pw_build/generate_3p_gn.py b/pw_build/py/pw_build/generate_3p_gn.py
new file mode 100644
index 000000000..60bbea0db
--- /dev/null
+++ b/pw_build/py/pw_build/generate_3p_gn.py
@@ -0,0 +1,475 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Generates a BUILD.gn file from `cc_library` rules in Bazel workspace."""
+
+import argparse
+import json
+import os
+import subprocess
+from collections import defaultdict
+from pathlib import Path
+from string import Template
+from typing import Dict, List, IO, Iterable, Iterator, Set
+
+from pw_build.bazel_query import BazelWorkspace
+from pw_build.gn_config import consolidate_configs, GnConfig
+from pw_build.gn_target import GnTarget
+from pw_build.gn_utils import GnLabel, GnPath
+from pw_build.gn_writer import gn_format, GnFile, GnWriter, MalformedGnError
+
+_DOCS_RST_TEMPLATE = Template(
+ '''
+.. _module-pw_third_party_$repo:
+
+$name_section
+$name
+$name_section
+The ``$$dir_pw_third_party/$repo/`` module provides build files to allow
+optionally including upstream $name.
+
+---------------$name_subsection
+Using upstream $name
+---------------$name_subsection
+If you want to use $name, you must do the following:
+
+Submodule
+=========
+Add $name to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add $url \\
+ third_party/$repo/src
+
+GN
+==
+* Set the GN var ``dir_pw_third_party_$repo`` to the location of the
+ $name source.
+
+ If you used the command above, this will be
+ ``//third_party/$repo/src``
+
+ This can be set in your args.gn or .gn file like:
+ ``dir_pw_third_party_$repo_var = "//third_party/$repo/src"``
+
+Updating
+========
+The GN build files are generated from the third-party Bazel build files using
+$$dir_pw_build/py/pw_build/$script.
+
+The script uses data taken from ``$$dir_pw_third_party/$repo/repo.json``.
+The schema of ``repo.json`` is described in :ref:`module-pw_build-third-party`.
+
+The script should be re-run whenever the submodule is updated or the JSON file
+is modified. Specify the location of the Bazel repository can be specified using
+the ``-w`` option, e.g.
+
+.. code-block:: sh
+
+ python pw_build/py/pw_build/$script \\
+ -w third_party/$repo/src
+
+.. DO NOT EDIT BELOW THIS LINE. Generated section.
+'''.lstrip()
+)
+
+_GIT_SHORT_REV_LEN = 8
+
+
+class GnGenerator:
+ """Maintains state while GN files are generated from Bazel files.
+
+ Attributes:
+ packages: The set of packages/sub-directories, each of which will have a
+ BUILD.gn.
+ configs: A mapping of package names to common GN configs for that
+ package.
+ targets: A mapping of package names to that Gn targets in that package.
+ """
+
+ def __init__(self) -> None:
+ self.packages: Set[str] = set()
+ self._workspace: BazelWorkspace
+ self._base_label: GnLabel
+ self._base_path: GnPath
+ self._repo: str
+ self._repo_var: str
+ self._repos: Dict[str, Set[str]] = defaultdict(set)
+ self._no_gn_check: List[GnLabel] = []
+ self.configs: Dict[str, List[GnConfig]] = defaultdict(list)
+ self.targets: Dict[str, List[GnTarget]] = defaultdict(list)
+
+ self.packages.add('')
+ self.configs[''] = []
+ self.targets[''] = []
+
+ def set_repo(self, repo: str) -> None:
+ """Sets the repository related variables.
+
+ This does not need to be called unless `load_workspace` is not being
+ called (as in some unit tests).
+
+ Args:
+ repo: The repository name.
+ """
+ self._repo = repo
+ self._repo_var = repo.replace('-', '_')
+ self._base_label = GnLabel(f'$dir_pw_third_party/{repo}')
+ self._base_path = GnPath(f'$dir_pw_third_party_{self._repo_var}')
+
+ def exclude_from_gn_check(self, **kwargs) -> None:
+ """Mark a target as being excluding from `gn check`.
+
+ This should be called before loading or adding targets.
+
+ Args:
+ kwargs: Same as `GnLabel`.
+ """
+ self._no_gn_check.append(GnLabel(self._base_label, **kwargs))
+
+ def load_workspace(self, workspace_path: Path) -> str:
+ """Loads a Bazel workspace.
+
+ Args:
+ workspace_path: The path to the Bazel workspace.
+ """
+ self._workspace = BazelWorkspace(workspace_path)
+ self.set_repo(self._workspace.repo)
+ return self._repo
+
+ def load_targets(self, kind: str, allow_testonly: bool) -> None:
+ """Analyzes a Bazel workspace and loads target info from it.
+
+ Target info will only be added for `kind` rules. Additionally,
+ libraries marked "testonly" may be included if `allow_testonly` is
+ present and true in the repo.json file.
+
+ Args:
+ kind: The Bazel rule kind to parse.
+ allow_testonly: If true, include testonly targets as well.
+ """
+ for rule in self._workspace.get_rules(kind):
+ self.add_target(allow_testonly, bazel=rule)
+
+ def add_target(self, allow_testonly: bool = False, **kwargs) -> None:
+ """Adds a target using this object's base label and source root.
+
+ Args:
+ allow_testonly: If true, include testonly targets as well.
+
+ Keyword Args:
+ Same as `GnTarget`.
+ """
+ target = GnTarget(self._base_label.dir(), self._base_path, **kwargs)
+ target.check_includes = target.label() not in self._no_gn_check
+ if allow_testonly or not target.testonly:
+ package = target.package()
+ self.packages.add(package)
+ self._repos[package].update(target.repos())
+ self.targets[package].append(target)
+
+ def add_configs(self, package: str, *configs: GnConfig) -> None:
+ """Adds configs for the given package.
+
+ This is mostly used for testing and debugging.
+
+ Args:
+ package: The package to add configs for.
+
+ Variable Args:
+ configs: The configs to add.
+ """
+ self.configs[package].extend(configs)
+
+ def generate_configs(
+ self, configs_to_add: Iterable[str], configs_to_remove: Iterable[str]
+ ) -> bool:
+ """Extracts the most common flags into common configs.
+
+ Args:
+ configs_to_add: Additional configs to add to every target.
+ configs_to_remove: Default configs to remove from every target.
+ """
+ added = [GnLabel(label) for label in configs_to_add]
+ removed = [GnLabel(label) for label in configs_to_remove]
+ all_configs = []
+ for targets in self.targets.values():
+ for target in targets:
+ for label in added:
+ target.add_config(label)
+ for label in removed:
+ target.remove_config(label)
+ all_configs.append(target.config)
+
+ consolidated = list(consolidate_configs(self._base_label, *all_configs))
+
+ def count_packages(config: GnConfig) -> int:
+ return sum(
+ [
+ any(config.within(target.config) for target in targets)
+ for _package, targets in self.targets.items()
+ ]
+ )
+
+ common = list(
+ filter(lambda config: count_packages(config) > 1, consolidated)
+ )
+
+ self.configs[''] = sorted(common)
+ for targets in self.targets.values():
+ for target in targets:
+ for config in target.config.deduplicate(*common):
+ if not config.label:
+ raise MalformedGnError('config is missing label')
+ target.add_config(config.label, public=config.public())
+
+ for package, targets in self.targets.items():
+ configs = [target.config for target in targets]
+ common = list(
+ consolidate_configs(
+ self._base_label.joinlabel(package),
+ *configs,
+ extract_public=True,
+ )
+ )
+ self.configs[package].extend(common)
+ self.configs[package].sort()
+ for target in targets:
+ for config in target.config.deduplicate(*common):
+ if not config.label:
+ raise MalformedGnError('config is missing label')
+ target.add_config(config.label, public=config.public())
+ return True
+
+ def write_repo_gni(self, repo_gni: GnWriter, name: str) -> None:
+ """Write the top-level GN import that declares build arguments.
+
+ Args:
+ repo_gni: The output writer object.
+ name: The third party module_name.
+ """
+ repo_gni.write_target_start('declare_args')
+ repo_gni.write_comment(
+ f'If compiling tests with {name}, this variable is set to the path '
+ f'to the {name} installation. When set, a pw_source_set for the '
+ f'{name} library is created at "$dir_pw_third_party/{self._repo}".'
+ )
+ repo_gni.write(f'dir_pw_third_party_{self._repo_var} = ""')
+ repo_gni.write_end()
+
+ def write_build_gn(self, package: str, build_gn: GnWriter) -> None:
+ """Write the target info for a package to a BUILD.gn file.
+
+ Args:
+ package: The name of the package to write a BUILD.gn for.
+ build_gn: The output writer object.
+ no_gn_check: List of targets with `check_includes = false`.
+ """
+ build_gn.write_imports(['//build_overrides/pigweed.gni'])
+ build_gn.write_blank()
+ imports = set()
+ imports.add('$dir_pw_build/target_types.gni')
+
+ if not package:
+ imports.add('$dir_pw_docgen/docs.gni')
+ for repo in self._repos[package]:
+ repo = build_gn.repos[repo]
+ imports.add('$dir_pw_build/error.gni')
+ imports.add(f'$dir_pw_third_party/{repo}/{repo}.gni')
+ if self._repo:
+ imports.add(f'$dir_pw_third_party/{self._repo}/{self._repo}.gni')
+ build_gn.write_imports(sorted(list(imports)))
+
+ if not package:
+ build_gn.write_if(f'dir_pw_third_party_{self._repo_var} != ""')
+
+ for config in sorted(self.configs[package], reverse=True):
+ build_gn.write_config(config)
+
+ targets = self.targets[package]
+ targets.sort(key=lambda target: target.name())
+ for target in targets:
+ build_gn.write_target(target)
+
+ if not package:
+ build_gn.write_end()
+ build_gn.write_target_start('pw_doc_group', 'docs')
+ build_gn.write_list('sources', ['docs.rst'])
+ build_gn.write_end()
+
+ def write_docs_rst(self, docs_rst: IO, name: str) -> None:
+ """Writes new, top-level documentation for the repo.
+
+ Args:
+ docs_rst: The output object.
+ name: The third party module_name.
+ """
+ contents = _DOCS_RST_TEMPLATE.substitute(
+ script=os.path.basename(__file__),
+ name=name,
+ name_section='=' * len(name),
+ name_subsection='-' * len(name),
+ repo=self._repo,
+ repo_var=self._repo_var,
+ url=self._workspace.url(),
+ )
+ docs_rst.write('\n'.join(self.update_version(contents.split('\n'))))
+
+ def update_version(self, lines: Iterable[str]) -> Iterator[str]:
+ """Replaces the "Version" part of docs.rst with the latest revision.
+
+ This will truncate everything after the "generated section" comment and
+ add the comment and version information. If the file does not have the
+ comment, the comment and information will appended to the end of the
+ file.
+
+ Args:
+ lines: Iterator of lines.
+ """
+ comment = '.. DO NOT EDIT BELOW THIS LINE. Generated section.'
+ url = self._workspace.url().rstrip('.git')
+ revision = self._workspace.revision()
+ short = revision[:_GIT_SHORT_REV_LEN]
+ for line in lines:
+ line = line.rstrip()
+ if line == comment:
+ break
+ yield line
+ yield comment
+ yield ''
+ yield 'Version'
+ yield '======='
+ yield f'The update script was last run for revision `{short}`_.'
+ yield ''
+ yield f'.. _{short}: {url}/tree/{revision}'
+ yield ''
+
+ def update_third_party_docs(self, contents: str) -> str:
+ """Adds a dep on the generated docs to a "third_party_docs" group."""
+ lines = contents.split('\n')
+ new_deps = f'deps = ["$dir_pigweed/third_party/{self._repo}:docs",'
+ for i in range(len(lines) - 1):
+ if lines[i] == 'group("third_party_docs") {':
+ lines[i + 1] = new_deps
+ return '\n'.join(lines)
+ raise ValueError('"third_party_docs" target not found')
+
+ def write_extra(self, extra: IO, label: str) -> None:
+ """Runs a Bazel target to generate an extra file."""
+ self._workspace.run(label, output=extra)
+
+
+def write_owners(owners: IO) -> None:
+ """Write an OWNERS file, but only if it does not already exist.
+
+ Args:
+ owners: The output object.
+ """
+ try:
+ result = subprocess.run(
+ ['git', 'config', '--get', 'user.email'],
+ check=True,
+ capture_output=True,
+ )
+ owners.write(result.stdout.decode('utf-8'))
+ except subprocess.CalledProcessError:
+ # Couldn't get owner from git config.
+ pass
+
+
+def _parse_args() -> List[Path]:
+ """Parse arguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ 'workspace',
+ type=Path,
+ nargs='+',
+ help=('Path to Bazel workspace to be queried.'),
+ )
+ args = parser.parse_args()
+ return args.workspace
+
+
+def _generate_gn(workspace_path: Path) -> None:
+ """Creates GN, doc, and OWNERS files for a third-party repository.
+
+ Args:
+ workspace_path: Path to the Bazel workspace.
+ """
+ pw_root = os.getenv('PW_ROOT')
+ if not pw_root:
+ raise RuntimeError('PW_ROOT is not set')
+
+ generator = GnGenerator()
+ repo = generator.load_workspace(workspace_path)
+ output = Path(pw_root, 'third_party', repo)
+
+ with open(output.joinpath('repo.json')) as file:
+ repo_json = json.load(file)
+
+ for exclusion in repo_json.get('no_gn_check', []):
+ generator.exclude_from_gn_check(bazel=exclusion)
+ generator.load_targets('cc_library', repo_json.get('allow_testonly', False))
+ generator.generate_configs(
+ repo_json.get('add', []), repo_json.get('remove', [])
+ )
+
+ name = repo_json['name']
+ with GnFile(Path(output, f'{repo}.gni')) as repo_gni:
+ generator.write_repo_gni(repo_gni, name)
+
+ for package in generator.packages:
+ with GnFile(Path(output, package, 'BUILD.gn'), package) as build_gn:
+ build_gn.repos = repo_json.get('repos', {})
+ build_gn.aliases = repo_json.get('aliases', {})
+ generator.write_build_gn(package, build_gn)
+
+ created_docs_rst = False
+ try:
+ with open(Path(output, 'docs.rst'), 'x') as docs_rst:
+ generator.write_docs_rst(docs_rst, name)
+ created_docs_rst = True
+ except OSError:
+ pass # docs.rst file already exists.
+
+ if created_docs_rst:
+ # Add the doc group to //docs:third_party_docs
+ docs_build_gn_path = Path(pw_root, 'docs', 'BUILD.gn')
+ with open(docs_build_gn_path, 'r') as docs_build_gn:
+ contents = docs_build_gn.read()
+ with open(docs_build_gn_path, 'w') as docs_build_gn:
+ docs_build_gn.write(generator.update_third_party_docs(contents))
+ gn_format(docs_build_gn_path)
+
+ else:
+ # Replace the version section of the existing docs.rst.
+ with open(Path(output, 'docs.rst'), 'r') as docs_rst:
+ contents = '\n'.join(generator.update_version(docs_rst))
+ with open(Path(output, 'docs.rst'), 'w') as docs_rst:
+ docs_rst.write(contents)
+
+ try:
+ with open(Path(output, 'OWNERS'), 'x') as owners:
+ write_owners(owners)
+ except OSError:
+ pass # OWNERS file already exists.
+
+ for filename, label in repo_json.get('extra_files', {}).items():
+ with open(Path(output, filename), 'w') as extra:
+ generator.write_extra(extra, label)
+
+
+if __name__ == '__main__':
+ for workspace in _parse_args():
+ _generate_gn(workspace)
diff --git a/pw_build/py/pw_build/generate_cc_blob_library.py b/pw_build/py/pw_build/generate_cc_blob_library.py
index 65b799eaa..958c9da8a 100644
--- a/pw_build/py/pw_build/generate_cc_blob_library.py
+++ b/pw_build/py/pw_build/generate_cc_blob_library.py
@@ -78,8 +78,9 @@ BLOB_DECLARATION_TEMPLATE = Template(
LINKER_SECTION_TEMPLATE = Template('PW_PLACE_IN_SECTION("${linker_section}")\n')
BLOB_DEFINITION_MULTI_LINE = Template(
- '\n${section_attr}'
- '${alignas}constexpr std::array<std::byte, ${size_bytes}> ${symbol_name}'
+ '\n${alignas}'
+ '${section_attr}constexpr std::array<std::byte, ${size_bytes}>'
+ ' ${symbol_name}'
' = {\n${bytes_lines}\n};\n'
)
diff --git a/pw_build/py/pw_build/generate_python_package.py b/pw_build/py/pw_build/generate_python_package.py
index 76ac587b1..2fcfbe7b9 100644
--- a/pw_build/py/pw_build/generate_python_package.py
+++ b/pw_build/py/pw_build/generate_python_package.py
@@ -11,7 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""Generates a setup.cfg file for a Python package."""
+"""Generates a setup.cfg file for a pw_python_package target."""
import argparse
from collections import defaultdict
diff --git a/pw_build/py/pw_build/generate_python_requirements.py b/pw_build/py/pw_build/generate_python_requirements.py
index 3a7bc67fc..fc04394ca 100644
--- a/pw_build/py/pw_build/generate_python_requirements.py
+++ b/pw_build/py/pw_build/generate_python_requirements.py
@@ -11,12 +11,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""Check Python package install_requires are covered."""
+"""Generate a requirements.txt for a pw_python_venv."""
import argparse
import configparser
from pathlib import Path
import sys
+from typing import List
try:
from pw_build.python_package import load_packages
@@ -39,7 +40,13 @@ def _parse_args() -> argparse.Namespace:
),
)
parser.add_argument(
- '--requirement',
+ '--gn-root-build-dir',
+ type=Path,
+ required=True,
+ help='Path to the root gn build dir.',
+ )
+ parser.add_argument(
+ '--output-requirement-file',
type=Path,
required=True,
help='requirement file to generate',
@@ -57,6 +64,12 @@ def _parse_args() -> argparse.Namespace:
action='store_true',
help='Exclude checking transitive deps of the specified --gn-packages',
)
+ parser.add_argument(
+ '--constraint-files',
+ nargs='*',
+ type=Path,
+ help='Optional constraint files to include as requirements.',
+ )
return parser.parse_args()
@@ -66,7 +79,9 @@ class NoMatchingGnPythonDependency(Exception):
def main(
python_dep_list_files: Path,
- requirement: Path,
+ gn_root_build_dir: Path,
+ output_requirement_file: Path,
+ constraint_files: List[Path],
gn_packages: str,
exclude_transitive_deps: bool,
) -> int:
@@ -123,10 +138,25 @@ def main(
target_py_packages, key=lambda pkg: pkg.gn_target_name
)
)
+ output += '\n\n'
+ output += '# Constraint files:\n'
+
+ for constraint_file in constraint_files:
+ parent_count = len(
+ output_requirement_file.parent.absolute()
+ .relative_to(gn_root_build_dir.absolute())
+ .parents
+ )
+
+ relative_constraint_path = Path('../' * parent_count) / constraint_file
+
+ # NOTE: Must use as_posix() here or backslash paths will appear in the
+ # generated requirements.txt file on Windows.
+ output += f'-c {relative_constraint_path.as_posix()}\n'
output += config['options']['install_requires']
output += '\n'
- requirement.write_text(output)
+ output_requirement_file.write_text(output)
return 0
diff --git a/pw_build/py/pw_build/generate_python_wheel.py b/pw_build/py/pw_build/generate_python_wheel.py
new file mode 100644
index 000000000..65a0c24ee
--- /dev/null
+++ b/pw_build/py/pw_build/generate_python_wheel.py
@@ -0,0 +1,106 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Generate a Python wheel for a pw_python_package or pw_python_distribution."""
+
+import argparse
+import hashlib
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+
+try:
+ from pw_build.python_package import PythonPackage
+
+except ImportError:
+ # Load from python_package from this directory if pw_build is not available.
+ from python_package import PythonPackage # type: ignore
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--package-dir',
+ type=Path,
+ required=True,
+ help='Path to the root gn build dir.',
+ )
+ parser.add_argument(
+ '--out-dir',
+ type=Path,
+ required=True,
+ help='requirement file to generate',
+ )
+ parser.add_argument(
+ '--generate-hashes',
+ action='store_true',
+ help='Generate sha256 sums for the requirements.txt file.',
+ )
+ return parser.parse_args()
+
+
+def main(
+ package_dir: Path,
+ out_dir: Path,
+ generate_hashes: bool,
+) -> int:
+ """Build a Python wheel."""
+
+ # Delete existing wheels from the out dir, there may be stale versions.
+ shutil.rmtree(out_dir, ignore_errors=True)
+ out_dir.mkdir(parents=True, exist_ok=True)
+
+ # Find the target package name and version.
+ pkg = PythonPackage(
+ sources=[],
+ setup_sources=[package_dir / 'setup.cfg'],
+ tests=[],
+ inputs=[],
+ )
+ requirement_lines = f'{pkg.package_name}=={pkg.package_version}'
+
+ # Build the wheel.
+ subprocess.run(
+ [
+ sys.executable,
+ "-m",
+ "build",
+ str(package_dir),
+ "--wheel",
+ "--no-isolation",
+ "--outdir",
+ str(out_dir),
+ ],
+ check=True,
+ )
+
+ if generate_hashes:
+ # Cacluate the sha256
+ for wheel_path in out_dir.glob('**/*.whl'):
+ wheel_sha256 = hashlib.sha256()
+ wheel_sha256.update(wheel_path.read_bytes())
+ sha256 = wheel_sha256.hexdigest()
+ requirement_lines += ' \\\n '
+ requirement_lines += f'--hash=sha256:{sha256}'
+ break
+
+ # Save a requirements file for this wheel and hash value.
+ requirement_file = out_dir / 'requirements.txt'
+ requirement_file.write_text(requirement_lines, encoding='utf-8')
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(**vars(_parse_args())))
diff --git a/pw_build/py/pw_build/generate_python_wheel_cache.py b/pw_build/py/pw_build/generate_python_wheel_cache.py
new file mode 100644
index 000000000..17d32757a
--- /dev/null
+++ b/pw_build/py/pw_build/generate_python_wheel_cache.py
@@ -0,0 +1,181 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Download all Python packages required for a pw_python_venv."""
+
+import argparse
+import logging
+from pathlib import Path
+import shlex
+import subprocess
+import sys
+from typing import List
+
+
+_LOG = logging.getLogger('pw_build.generate_python_wheel_cache')
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--pip-download-log',
+ type=Path,
+ required=True,
+ help='Path to the root gn build dir.',
+ )
+ parser.add_argument(
+ '--wheel-dir',
+ type=Path,
+ required=True,
+ help='Path to save wheel files.',
+ )
+ parser.add_argument(
+ '--download-all-platforms',
+ action='store_true',
+ help='Download Python precompiled wheels for all platforms.',
+ )
+ parser.add_argument(
+ '-r',
+ '--requirement',
+ type=Path,
+ nargs='+',
+ required=True,
+ help='Requirements files',
+ )
+ return parser.parse_args()
+
+
+def main(
+ pip_download_log: Path,
+ wheel_dir: Path,
+ requirement: List[Path],
+ download_all_platforms: bool = False,
+) -> int:
+ """Download all Python packages required for a pw_python_venv."""
+
+ # Delete existing wheels from the out dir, there may be stale versions.
+ # shutil.rmtree(wheel_dir, ignore_errors=True)
+ wheel_dir.mkdir(parents=True, exist_ok=True)
+
+ pip_download_args = [
+ sys.executable,
+ "-m",
+ "pip",
+ "--log",
+ str(pip_download_log),
+ "download",
+ "--dest",
+ str(wheel_dir),
+ ]
+ for req in requirement:
+ pip_download_args.extend(["--requirement", str(req)])
+
+ if not download_all_platforms:
+ # Download for the current platform only.
+ quoted_pip_download_args = ' '.join(
+ shlex.quote(arg) for arg in pip_download_args
+ )
+ _LOG.info('Run ==> %s', quoted_pip_download_args)
+ # Download packages
+ subprocess.run(pip_download_args, check=True)
+ return 0
+
+ # DOCSTAG: [wheel-platform-args]
+ # These platform args are derived from the cffi pypi package:
+ # https://pypi.org/project/cffi/#files
+ # See also these pages on Python wheel filename format:
+ # https://peps.python.org/pep-0491/#file-name-convention
+ # and Platform compatibility tags:
+ # https://packaging.python.org/en/latest/specifications/
+ # platform-compatibility-tags/
+ platform_args = [
+ '--platform=any',
+ '--platform=macosx_10_9_universal2',
+ '--platform=macosx_10_9_x86_64',
+ '--platform=macosx_11_0_arm64',
+ '--platform=manylinux2010_x86_64',
+ '--platform=manylinux2014_aarch64',
+ '--platform=manylinux2014_x86_64',
+ '--platform=manylinux_2_17_aarch64',
+ '--platform=manylinux_2_17_x86_64',
+ '--platform=musllinux_1_1_x86_64',
+ '--platform=win_amd64',
+ # Note: These 32bit platforms are omitted
+ # '--platform=manylinux2010_i686',
+ # '--platform=manylinux2014_i686',
+ # '--platform=manylinux_2_12_i686'
+ # '--platform=musllinux_1_1_i686',
+ # '--platform=win32',
+ ]
+
+ # Pigweed supports Python 3.8 and up.
+ python_version_args = [
+ [
+ '--implementation=py3',
+ '--abi=none',
+ ],
+ [
+ '--implementation=cp',
+ '--python-version=3.8',
+ '--abi=cp38',
+ ],
+ [
+ '--implementation=cp',
+ '--python-version=3.9',
+ '--abi=cp39',
+ ],
+ [
+ '--implementation=cp',
+ '--python-version=3.10',
+ '--abi=cp310',
+ ],
+ [
+ '--implementation=cp',
+ '--python-version=3.11',
+ '--abi=cp311',
+ ],
+ ]
+ # DOCSTAG: [wheel-platform-args]
+
+ # --no-deps is required when downloading binary packages for different
+ # platforms other than the current one. The requirements.txt files already
+ # has the fully expanded deps list using pip-compile so this is not a
+ # problem.
+ pip_download_args.append('--no-deps')
+
+ # Run pip download once for each Python version. Multiple platform args can
+ # be added to the same command.
+ for py_version_args in python_version_args:
+ for platform_arg in platform_args:
+ final_pip_download_args = list(pip_download_args)
+ final_pip_download_args.append(platform_arg)
+ final_pip_download_args.extend(py_version_args)
+ quoted_pip_download_args = ' '.join(
+ shlex.quote(arg) for arg in final_pip_download_args
+ )
+ _LOG.info('')
+ _LOG.info('Fetching packages for:')
+ _LOG.info(
+ 'Python %s and Platforms: %s', py_version_args, platform_args
+ )
+ _LOG.info('Run ==> %s', quoted_pip_download_args)
+
+ # Download packages
+ subprocess.run(final_pip_download_args, check=True)
+
+ return 0
+
+
+if __name__ == '__main__':
+ logging.basicConfig(format='%(message)s', level=logging.DEBUG)
+ sys.exit(main(**vars(_parse_args())))
diff --git a/pw_build/py/pw_build/generate_report.py b/pw_build/py/pw_build/generate_report.py
new file mode 100644
index 000000000..8703adb65
--- /dev/null
+++ b/pw_build/py/pw_build/generate_report.py
@@ -0,0 +1,249 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Generate a coverage report using llvm-cov."""
+
+import argparse
+import json
+import logging
+import sys
+import subprocess
+from pathlib import Path
+from typing import List, Dict, Any
+
+_LOG = logging.getLogger(__name__)
+
+
+def _parser_args() -> Dict[str, Any]:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--llvm-cov-path',
+ type=Path,
+ required=True,
+ help='Path to the llvm-cov binary to use to generate coverage reports.',
+ )
+ parser.add_argument(
+ '--format',
+ dest='format_type',
+ type=str,
+ choices=['text', 'html', 'lcov', 'json'],
+ required=True,
+ help='Desired output format of the code coverage report.',
+ )
+ parser.add_argument(
+ '--test-metadata-path',
+ type=Path,
+ required=True,
+ help='Path to the *.test_metadata.json file that describes all of the '
+ 'tests being used to generate a coverage report.',
+ )
+ parser.add_argument(
+ '--profdata-path',
+ type=Path,
+ required=True,
+ help='Path for the output merged profdata file to use with generating a'
+ ' coverage report for the tests described in --test-metadata.',
+ )
+ parser.add_argument(
+ '--root-dir',
+ type=Path,
+ required=True,
+ help='Path to the project\'s root directory.',
+ )
+ parser.add_argument(
+ '--build-dir',
+ type=Path,
+ required=True,
+ help='Path to the ninja build directory.',
+ )
+ parser.add_argument(
+ '--output-dir',
+ type=Path,
+ required=True,
+ help='Path to where the output report should be placed. This must be a '
+ 'relative path (from the current working directory) to ensure the '
+ 'depfiles are generated correctly.',
+ )
+ parser.add_argument(
+ '--depfile-path',
+ type=Path,
+ required=True,
+ help='Path for the output depfile to convey the extra input '
+ 'requirements from parsing --test-metadata.',
+ )
+ parser.add_argument(
+ '--filter-path',
+ dest='filter_paths',
+ type=str,
+ action='append',
+ default=[],
+ help='Only these folder paths or files will be included in the output. '
+ 'To work properly, these must be aboslute paths or relative paths from '
+ 'the current working directory. No globs or regular expression features'
+ ' are supported.',
+ )
+ parser.add_argument(
+ '--ignore-filename-pattern',
+ dest='ignore_filename_patterns',
+ type=str,
+ action='append',
+ default=[],
+ help='Any file path that matches one of these regular expression '
+ 'patterns will be excluded from the output report (possibly even if '
+ 'that path was included in --filter-paths). The regular expression '
+ 'engine for these is somewhat primitive and does not support things '
+ 'like look-ahead or look-behind.',
+ )
+ return vars(parser.parse_args())
+
+
+def generate_report(
+ llvm_cov_path: Path,
+ format_type: str,
+ test_metadata_path: Path,
+ profdata_path: Path,
+ root_dir: Path,
+ build_dir: Path,
+ output_dir: Path,
+ depfile_path: Path,
+ filter_paths: List[str],
+ ignore_filename_patterns: List[str],
+) -> int:
+ """Generate a coverage report using llvm-cov."""
+
+ # Ensure directories that need to be absolute are.
+ root_dir = root_dir.resolve()
+ build_dir = build_dir.resolve()
+
+ # Open the test_metadata_path, parse it to JSON, and extract out the
+ # test binaries.
+ test_metadata = json.loads(test_metadata_path.read_text())
+ test_binaries = [
+ Path(obj['test_directory']) / obj['test_name']
+ for obj in test_metadata
+ if 'test_type' in obj and obj['test_type'] == 'unit_test'
+ ]
+
+ # llvm-cov export does not create an output file, so we mimic it by creating
+ # the directory structure and writing to file outself after we run the
+ # command.
+ if format_type in ['lcov', 'json']:
+ export_output_path = (
+ output_dir / 'report.lcov'
+ if format_type == 'lcov'
+ else output_dir / 'report.json'
+ )
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Build the command to the llvm-cov subtool based on provided arguments.
+ command = [str(llvm_cov_path)]
+ if format_type in ['html', 'text']:
+ command += [
+ 'show',
+ '--format',
+ format_type,
+ '--output-dir',
+ str(output_dir),
+ ]
+ else: # format_type in ['lcov', 'json']
+ command += [
+ 'export',
+ '--format',
+ # `text` is JSON format when using `llvm-cov`.
+ format_type if format_type == 'lcov' else 'text',
+ ]
+ # We really need two `--path-equivalence` options to be able to map both the
+ # root directory for coverage files to the absolute path of the project
+ # root_dir and to be able to map "out/" prefix to the provided build_dir.
+ #
+ # llvm-cov does not currently support two `--path-equivalence` options, so
+ # we use `--compilation-dir` and `--path-equivalence` together. This has the
+ # unfortunate consequence of showing file paths as absolute in the JSON,
+ # LCOV, and text reports.
+ #
+ # An unwritten assumption here is that root_dir must be an
+ # absolute path to enable file-path-based filtering.
+ #
+ # This is due to turning all file paths into absolute files here:
+ # https://github.com/llvm-mirror/llvm/blob/2c4ca6832fa6b306ee6a7010bfb80a3f2596f824/tools/llvm-cov/CodeCoverage.cpp#L188.
+ command += [
+ '--compilation-dir',
+ str(root_dir),
+ ]
+ # Pigweed maps any build directory to out, which causes generated files to
+ # be reported to exist under the out directory, which may not exist if the
+ # build directory is not exactly out. This maps out back to the build
+ # directory so generated files can be found.
+ command += [
+ '--path-equivalence',
+ f'{str(root_dir)}/out,{str(build_dir)}',
+ ]
+ command += [
+ '--instr-profile',
+ str(profdata_path),
+ ]
+ command += [
+ f'--ignore-filename-regex={path}' for path in ignore_filename_patterns
+ ]
+ # The test binary positional argument MUST appear before the filter path
+ # positional arguments. llvm-cov is a horrible interface.
+ command += [str(test_binaries[0])]
+ command += [f'--object={binary}' for binary in test_binaries[1:]]
+ command += [
+ str(Path(filter_path).resolve()) for filter_path in filter_paths
+ ]
+
+ _LOG.info('')
+ _LOG.info(' '.join(command))
+ _LOG.info('')
+
+ # Generate the coverage report by invoking the command.
+ if format_type in ['html', 'text']:
+ output = subprocess.run(command)
+ if output.returncode != 0:
+ return output.returncode
+ else: # format_type in ['lcov', 'json']
+ output = subprocess.run(command, capture_output=True)
+ if output.returncode != 0:
+ _LOG.error(output.stderr)
+ return output.returncode
+ export_output_path.write_bytes(output.stdout)
+
+ # Generate the depfile that describes the dependency on the test binaries
+ # used to create the report output.
+ depfile_target = Path('.')
+ if format_type in ['lcov', 'json']:
+ depfile_target = export_output_path
+ elif format_type == 'text':
+ depfile_target = output_dir / 'index.txt'
+ else: # format_type == 'html'
+ depfile_target = output_dir / 'index.html'
+ depfile_path.write_text(
+ ''.join(
+ [
+ str(depfile_target),
+ ': \\\n',
+ *[str(binary) + ' \\\n' for binary in test_binaries],
+ ]
+ )
+ )
+
+ return 0
+
+
+def main() -> int:
+ return generate_report(**_parser_args())
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/pw_build/py/pw_build/gn_config.py b/pw_build/py/pw_build/gn_config.py
new file mode 100644
index 000000000..9e89ca8f2
--- /dev/null
+++ b/pw_build/py/pw_build/gn_config.py
@@ -0,0 +1,386 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Utilities for manipulating GN configs."""
+
+from __future__ import annotations
+
+from collections import deque
+from json import loads as json_loads, dumps as json_dumps
+from typing import Any, Deque, Dict, Iterable, Iterator, Optional, Set
+
+from pw_build.gn_utils import GnLabel, MalformedGnError
+
+GN_CONFIG_FLAGS = [
+ 'asmflags',
+ 'cflags',
+ 'cflags_c',
+ 'cflags_cc',
+ 'cflags_objc',
+ 'cflags_objcc',
+ 'defines',
+ 'include_dirs',
+ 'inputs',
+ 'ldflags',
+ 'lib_dirs',
+ 'libs',
+ 'precompiled_header',
+ 'precompiled_source',
+ 'rustenv',
+ 'rustflags',
+ 'swiftflags',
+ 'testonly',
+]
+
+_INTERNAL_FLAGS = ['nested', 'public_defines']
+
+
+def _get_prefix(flag: str) -> str:
+ """Returns the prefix used to identify values for a particular flag.
+
+ Combining all values in a single set allows methods like `exact_cover` to
+ analyze configs for patterns, but the values also need to be able to be
+ separated out again. This prefix pattern is chosen as it is guaranteed not
+ to be a valid source-relative path or label in GN or Bazel. It is
+ encapsulated into its own function to encourage consistency and
+ maintainability.
+
+ Args:
+ flag: The flag to convert to a prefix.
+ """
+ return f'{flag}::'
+
+
+class GnConfig:
+ """Represents a GN config.
+
+ Attributes:
+ label: The GN label of this config.
+ values: A set of config values, prefixed by type. For example, a C
+ compiler flag might be 'cflag::-foo'.
+ """
+
+ def __init__(
+ self, public: bool = False, json: Optional[str] = None
+ ) -> None:
+ """Create a GN config object.
+
+ Args:
+ public: Indicates if this is a `public_config`.
+ json: If provided, populates this object from a JSON string.
+ """
+ self.label: Optional[GnLabel] = None
+ self.values: Set[str] = set()
+ self._public: bool = public
+ self._usages: int = 0
+ if json:
+ self._from_json(json)
+
+ def __lt__(self, other: GnConfig) -> bool:
+ """Compares two configs.
+
+ A config is said to be "less than" another if it comes before the other
+ when ordered according to the following rules, evaluated in order:
+ * Public configs come before regular configs.
+ * More frequently used configs come before those used less.
+ * Shorter configs (in terms of values) come before longer ones.
+ * Configs whose label comes before the other's comes first.
+ * If all else fails, the configs are converted to strings and
+ compared lexicographically.
+ """
+ if self._public != other.public():
+ return not self._public and other.public()
+ if self._usages != other._usages:
+ return self._usages < other._usages
+ if len(self.values) != len(other.values):
+ return len(self.values) < len(other.values)
+ if self.label != other.label:
+ return str(self.label) < str(other.label)
+ return str(self) < str(other)
+
+ def __eq__(self, other) -> bool:
+ return (
+ isinstance(other, GnConfig)
+ and self._public == other.public()
+ and self._usages == other._usages
+ and self.values == other.values
+ and self.label == other.label
+ )
+
+ def __hash__(self) -> int:
+ return hash((self._public, self._usages, str(self)))
+
+ def __str__(self) -> str:
+ return self.to_json()
+
+ def __bool__(self) -> bool:
+ return bool(self.values)
+
+ def _from_json(self, data: str) -> None:
+ """Populates this config from a JSON string.
+
+ Args:
+ data: A JSON representation of a config.
+ """
+ obj = json_loads(data)
+ if 'label' in obj:
+ self.label = GnLabel(obj['label'])
+ for flag in GN_CONFIG_FLAGS + _INTERNAL_FLAGS:
+ if flag in obj:
+ self.add(flag, *obj[flag])
+ if 'public' in obj:
+ self._public = bool(obj['public'])
+ if 'usages' in obj:
+ self._usages = int(obj['usages'])
+
+ def to_json(self) -> str:
+ """Returns a JSON representation of this config."""
+ obj: Dict[str, Any] = {}
+ if self.label:
+ obj['label'] = str(self.label)
+ for flag in GN_CONFIG_FLAGS + _INTERNAL_FLAGS:
+ if self.has(flag):
+ obj[flag] = list(self.get(flag))
+ if self._public:
+ obj['public'] = self._public
+ if self._usages:
+ obj['usages'] = self._usages
+ return json_dumps(obj)
+
+ def has(self, flag: str) -> bool:
+ """Returns whether this config has values for the given flag.
+
+ Args:
+ flag: The flag to check for.
+ """
+ return any(v.startswith(_get_prefix(flag)) for v in self.values)
+
+ def add(self, flag: str, *values: str) -> None:
+ """Adds a value to this config for the given flag.
+
+ Args:
+ flag: The flag to add values for.
+ Variable Args:
+ values: Strings to associate with the given flag.
+ """
+ if flag not in GN_CONFIG_FLAGS and flag not in _INTERNAL_FLAGS:
+ raise MalformedGnError(f'invalid flag: {flag}')
+ self.values |= {f'{_get_prefix(flag)}{v}' for v in values}
+
+ def get(self, flag: str) -> Iterator[str]:
+ """Iterates over the values for the given flag.
+
+ Args:
+ flag: the flag to look up.
+ """
+ prefix = _get_prefix(flag)
+ for value in self.values:
+ if value.startswith(prefix):
+ yield value[len(prefix) :]
+
+ def take(self, flag: str) -> Iterable[str]:
+ """Extracts and returns the set of values for the given flag.
+
+ Args:
+ flag: The flag to remove and return values for.
+ """
+ prefix = _get_prefix(flag)
+ taken = {v for v in self.values if v.startswith(prefix)}
+ self.values = self.values - taken
+ return [v[len(prefix) :] for v in taken]
+
+ def deduplicate(self, *configs: GnConfig) -> Iterator[GnConfig]:
+ """Removes values found in the given configs.
+
+ Values are only removed if all of the values in a config are present.
+ Returns the configs which resulte in values being removed.
+
+ Variable Args:
+ configs: The configs whose values should be removed from this config
+
+ Raises:
+ ValueError if any of the given configs do not have a label.
+ """
+ matching = []
+ for config in configs:
+ if not config.label:
+ raise ValueError('config has no label')
+ if not config:
+ continue
+ if config.values <= self.values:
+ matching.append(config)
+ matching.sort(key=lambda config: len(config.values), reverse=True)
+ for config in matching:
+ if config.values & self.values:
+ self.values = self.values - config.values
+ yield config
+
+ def within(self, config: GnConfig) -> bool:
+ """Returns whether the values of this config are a subset of another.
+
+ Args:
+ config: The config whose values are checked.
+ """
+ return self.values <= config.values
+
+ def count_usages(self, configs: Iterable[GnConfig]) -> int:
+ """Counts how many other configs this config is within.
+
+ Args:
+ configs: The set of configs which may contain this object's values.
+ """
+ self._usages = sum(map(self.within, configs))
+ return self._usages
+
+ def public(self) -> bool:
+ """Returns whether this object represents a public config."""
+ return self._public
+
+ def extract_public(self) -> GnConfig:
+ """Extracts and returns the set of values that need to be public.
+
+ 'include_dirs' and public 'defines' for a GN target need to be forwarded
+ to anything that depends on that target.
+ """
+ public = GnConfig(public=True)
+ public.add('include_dirs', *(self.take('include_dirs')))
+ public.add('defines', *(self.take('public_defines')))
+ return public
+
+ def generate_label(self, label: GnLabel, index: int) -> None:
+ """Creates a label for this config."""
+ name = label.name().replace('-', '_')
+ name = f'{name}_' or ''
+ public = 'public_' if self._public else ''
+ self.label = GnLabel(f'{label.dir()}:{name}{public}config{index}')
+
+
+def _exact_cover(*configs: GnConfig) -> Iterator[GnConfig]:
+ """Returns the exact covering set of configs for a given set of configs.
+
+ An exact cover of a sequence of sets is the smallest set of subsets such
+ that each element in the union of sets appears in exactly one subset. In
+ other words, the subsets are disjoint and every set in the original sequence
+ equals some union of subsets.
+
+ As a side effect, this also separates public and regular flags, as GN
+ targets have separate lists for `public_configs` and `configs`.
+
+ Variables Args:
+ configs: The set of configs to produce an exact cover for.
+ """
+ pending: Deque[Set[str]] = deque([config.values for config in configs])
+ intermediate: Deque[Set[str]] = deque()
+ disjoint: Deque[Set[str]] = deque()
+ while pending:
+ config_a = pending.popleft()
+ if not config_a:
+ continue
+ ok = True
+ while disjoint:
+ intermediate.append(disjoint.popleft())
+ while intermediate:
+ config_b = intermediate.popleft()
+ ok = False
+ if config_a == config_b:
+ disjoint.append(config_b)
+ break
+ if config_a < config_b:
+ pending.append(config_b - config_a)
+ disjoint.append(config_a)
+ break
+ if config_a > config_b:
+ pending.append(config_a - config_b)
+ disjoint.append(config_b)
+ break
+ config_c = config_a & config_b
+ if config_c:
+ pending.append(config_a - config_c)
+ pending.append(config_b - config_c)
+ pending.append(config_c)
+ break
+ ok = True
+ disjoint.append(config_b)
+ if ok:
+ disjoint.append(config_a)
+
+ for values in disjoint:
+ config = GnConfig()
+ config.values = values
+ public = config.extract_public()
+ if public:
+ yield public
+ if config:
+ yield config
+
+
+def _filter_by_usage(
+ covers: Iterator[GnConfig], extract_public: bool, *configs: GnConfig
+) -> Iterable[GnConfig]:
+ """Filters configs to only include public or those used at least 3 times.
+
+ Args:
+ covers: A set of configs that is an exact cover for `configs`.
+ extract_public: If true, all public configs are yielded.
+
+ Variable Args:
+ configs: A set of configs being conslidated.
+ """
+ for cover in covers:
+ if cover.count_usages(configs) > 2 or (
+ extract_public and cover.public()
+ ):
+ yield cover
+
+
+def consolidate_configs(
+ label: GnLabel, *configs: GnConfig, **kwargs
+) -> Iterator[GnConfig]:
+ """Extracts and returns the most common sub-configs across a set of configs.
+
+ See also `_exact_cover`. An exact cover of configs can be used to find the
+ most common sub-configs. These sub-configs are given labels, and then
+ replaced in the original configs.
+
+ Callers may optionally set the keyword argument of `extract_public` to
+ `True` if all public configs should be extracted, regardless of usage count.
+ Flags like `include_dirs` must be in a GN `public_config` to be forwarded,
+ so this is useful for the first consolidation of configs corresponding to a
+ Bazel package. Subsequent consolidations, i.e. for a group of Bazel
+ packages, may want to avoid pulling public configs out of packages and omit
+ this parameter.
+
+ Args:
+ label: The base label to use for generated configs.
+
+ Variable Args:
+ configs: Configs to examine for common, interesting shared sub-configs.
+
+ Keyword Args:
+ extract_public: If true, always considers public configs "interesting".
+ """
+ extract_public = kwargs.get('extract_public', False)
+ covers = list(
+ _filter_by_usage(_exact_cover(*configs), extract_public, *configs)
+ )
+ covers.sort(reverse=True)
+ public_i = 1
+ config_j = 1
+ for cover in covers:
+ if cover.public():
+ cover.generate_label(label, public_i)
+ public_i += 1
+ else:
+ cover.generate_label(label, config_j)
+ config_j += 1
+ yield cover
diff --git a/pw_build/py/pw_build/gn_resolver.py b/pw_build/py/pw_build/gn_resolver.py
index afa01cf76..50eb47e61 100644
--- a/pw_build/py/pw_build/gn_resolver.py
+++ b/pw_build/py/pw_build/gn_resolver.py
@@ -363,7 +363,7 @@ def _target_objects(paths: GnPaths, expr: _Expression) -> _Actions:
yield _ArgAction.EMIT_NEW, str(obj)
-# TODO(b/234886742): Replace expressions with native GN features when possible.
+# TODO: b/234886742 - Replace expressions with native GN features when possible.
_FUNCTIONS: Dict['str', Callable[[GnPaths, _Expression], _Actions]] = {
'TARGET_FILE': _target_file,
'TARGET_FILE_IF_EXISTS': _target_file_if_exists,
diff --git a/pw_build/py/pw_build/gn_target.py b/pw_build/py/pw_build/gn_target.py
new file mode 100644
index 000000000..942aa6b81
--- /dev/null
+++ b/pw_build/py/pw_build/gn_target.py
@@ -0,0 +1,324 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Utilities for manipulating GN targets."""
+
+from json import loads as json_loads, dumps as json_dumps
+from pathlib import PurePosixPath
+from typing import Dict, List, Optional, Set, Union
+
+from pw_build.bazel_query import BazelRule
+from pw_build.gn_config import GnConfig, GN_CONFIG_FLAGS
+from pw_build.gn_utils import (
+ GnLabel,
+ GnPath,
+ GnVisibility,
+ MalformedGnError,
+)
+
+
+class GnTarget: # pylint: disable=too-many-instance-attributes
+ """Represents a Pigweed Build target.
+
+ Attributes:
+ config: Variables on the target that can be added to a config, e.g.
+ those in `pw_build.gn_config.GN_CONFIG_FLAGS.
+
+ The following attributes match those of `pw_internal_build_target` in
+ //pw_build/gn_internal/build_target.gni
+ remove_configs
+
+ The following attributes are analogous to GN variables, see
+ https://gn.googlesource.com/gn/+/main/docs/reference.md#target_variables:
+ visibility
+ testonly
+ public
+ sources
+ inputs
+ public_configs
+ configs
+ public_deps
+ deps
+ """
+
+ def __init__(
+ self,
+ base_label: Union[str, PurePosixPath, GnLabel],
+ base_path: Union[str, PurePosixPath, GnPath],
+ bazel: Optional[BazelRule] = None,
+ json: Optional[str] = None,
+ check_includes: bool = True,
+ ) -> None:
+ """Creates a GN target
+
+ Args:
+
+ """
+ self._type: str
+ self._label: GnLabel
+ self._base_label: GnLabel = GnLabel(base_label)
+ self._base_path: GnPath = GnPath(base_path)
+ self._repos: Set[str] = set()
+ self.visibility: List[GnVisibility] = []
+ self.testonly: bool = False
+ self.check_includes = check_includes
+ self.public: List[GnPath] = []
+ self.sources: List[GnPath] = []
+ self.inputs: List[GnPath] = []
+ self.config: GnConfig = GnConfig()
+ self.public_configs: Set[GnLabel] = set()
+ self.configs: Set[GnLabel] = set()
+ self.remove_configs: Set[GnLabel] = set()
+ self.public_deps: Set[GnLabel] = set()
+ self.deps: Set[GnLabel] = set()
+ if bazel:
+ self._from_bazel(bazel)
+ elif json:
+ self._from_json(json)
+ else:
+ self._label = GnLabel(base_label)
+
+ def _from_bazel(self, rule: BazelRule) -> None:
+ """Populates target info from a Bazel Rule.
+
+ Filenames will be relative to the given path to the source directory.
+
+ Args:
+ rule: The Bazel rule to populate this object with.
+ """
+ kind = rule.kind()
+ linkstatic = rule.get_bool('linkstatic')
+ visibility = rule.get_list('visibility')
+
+ self.testonly = rule.get_bool('testonly')
+ public = rule.get_list('hdrs')
+ sources = rule.get_list('srcs')
+ inputs = rule.get_list('additional_linker_inputs')
+
+ include_dirs = rule.get_list('includes')
+ self.config.add('public_defines', *rule.get_list('defines'))
+ self.config.add('cflags', *rule.get_list('copts'))
+ self.config.add('ldflags', *rule.get_list('linkopts'))
+ self.config.add('defines', *rule.get_list('local_defines'))
+
+ public_deps = rule.get_list('deps')
+ deps = rule.get_list('implementation_deps')
+
+ rule_label = rule.label()
+ if rule_label.startswith('//'):
+ rule_label = rule_label[2:]
+ self._label = self._base_label.joinlabel(rule_label)
+
+ # Translate Bazel kind to GN target type.
+ if kind == 'cc_library':
+ if linkstatic:
+ self._type = 'pw_static_library'
+ else:
+ self._type = 'pw_source_set'
+ else:
+ raise MalformedGnError(f'unsupported Bazel kind: {kind}')
+
+ # Bazel always implicitly includes private visibility.
+ visibility.append('//visibility:private')
+ for scope in visibility:
+ self.add_visibility(bazel=scope)
+
+ # Add includer directories. Bazel implicitly includes the project root.
+ include_dirs.append('//')
+ gn_paths = [
+ GnPath(self._base_path, bazel=file) for file in include_dirs
+ ]
+ self.config.add('include_dirs', *[str(gn_path) for gn_path in gn_paths])
+
+ for path in public:
+ self.add_path('public', bazel=path)
+ for path in sources:
+ self.add_path('sources', bazel=path)
+ for path in inputs:
+ self.add_path('inputs', bazel=path)
+ for label in public_deps:
+ self.add_dep(public=True, bazel=label)
+ for label in deps:
+ self.add_dep(bazel=label)
+
+ def _from_json(self, data: str) -> None:
+ """Populates this target from a JSON string.
+
+ Args:
+ data: The JSON data to populate this object with.
+ """
+ obj = json_loads(data)
+ self._type = obj.get('target_type')
+ name = obj.get('target_name')
+ package = obj.get('package', '')
+ self._label = GnLabel(
+ self._base_label.joinlabel(package), gn=f':{name}'
+ )
+ self.visibility = [
+ GnVisibility(self._base_label, self._label, gn=scope)
+ for scope in obj.get('visibility', [])
+ ]
+ self.testonly = bool(obj.get('testonly', False))
+ self.testonly = bool(obj.get('check_includes', False))
+ self.public = [GnPath(path) for path in obj.get('public', [])]
+ self.sources = [GnPath(path) for path in obj.get('sources', [])]
+ self.inputs = [GnPath(path) for path in obj.get('inputs', [])]
+ for flag in GN_CONFIG_FLAGS:
+ if flag in obj:
+ self.config.add(flag, *obj[flag])
+ self.public_configs = {
+ GnLabel(label) for label in obj.get('public_configs', [])
+ }
+ self.configs = {GnLabel(label) for label in obj.get('configs', [])}
+ self.public_deps = {
+ GnLabel(label) for label in obj.get('public_deps', [])
+ }
+ self.deps = {GnLabel(label) for label in obj.get('deps', [])}
+
+ def to_json(self) -> str:
+ """Returns a JSON representation of this target."""
+ obj: Dict[str, Union[bool, str, List[str]]] = {}
+ if self._type:
+ obj['target_type'] = self._type
+ obj['target_name'] = self.name()
+ obj['package'] = self.package()
+ if self.visibility:
+ obj['visibility'] = [str(scope) for scope in self.visibility]
+ if self.testonly:
+ obj['testonly'] = self.testonly
+ if not self.check_includes:
+ obj['check_includes'] = self.check_includes
+ if self.public:
+ obj['public'] = [str(path) for path in self.public]
+ if self.sources:
+ obj['sources'] = [str(path) for path in self.sources]
+ if self.inputs:
+ obj['inputs'] = [str(path) for path in self.inputs]
+ for flag in GN_CONFIG_FLAGS:
+ if self.config.has(flag):
+ obj[flag] = list(self.config.get(flag))
+ if self.public_configs:
+ obj['public_configs'] = [
+ str(label) for label in self.public_configs
+ ]
+ if self.configs:
+ obj['configs'] = [str(label) for label in self.configs]
+ if self.remove_configs:
+ obj['remove_configs'] = [
+ str(label) for label in self.remove_configs
+ ]
+ if self.public_deps:
+ obj['public_deps'] = [str(label) for label in self.public_deps]
+ if self.deps:
+ obj['deps'] = [str(label) for label in self.deps]
+ return json_dumps(obj)
+
+ def name(self) -> str:
+ """Returns the target name."""
+ return self._label.name()
+
+ def label(self) -> GnLabel:
+ """Returns the target label."""
+ return self._label
+
+ def type(self) -> str:
+ """Returns the target type."""
+ return self._type
+
+ def repos(self) -> Set[str]:
+ """Returns any external repositories referenced from Bazel rules."""
+ return self._repos
+
+ def package(self) -> str:
+ """Returns the relative path from the base label."""
+ pkgname = self._label.relative_to(self._base_label).split(':')[0]
+ return '' if pkgname == '.' else pkgname
+
+ def add_path(self, dst: str, **kwargs) -> None:
+ """Adds a GN path using this target's source root.
+
+ Args:
+ dst: Variable to add the path to. Should be one of 'public',
+ 'sources', or 'inputs'.
+
+ Keyword Args:
+ Same as `GnPath.__init__`.
+ """
+ getattr(self, dst).append(GnPath(self._base_path, **kwargs))
+
+ def add_config(self, label: GnLabel, public: bool = False) -> None:
+ """Adds a GN config to this target.
+
+ Args:
+ label: The label of the config to add to this target.
+ public: If true, the config is added as a `public_config`.
+ """
+ self.remove_configs.discard(label)
+ if public:
+ self.public_configs.add(label)
+ self.configs.discard(label)
+ else:
+ self.public_configs.discard(label)
+ self.configs.add(label)
+
+ def remove_config(self, label: GnLabel) -> None:
+ """Adds the given config to the list of default configs to remove.
+
+ Args:
+ label: The config to add to `remove_configs`.
+ """
+ self.public_configs.discard(label)
+ self.configs.discard(label)
+ self.remove_configs.add(label)
+
+ def add_dep(self, **kwargs) -> None:
+ """Adds a GN dependency to the target using this target's base label.
+
+ Keyword Args:
+ Same as `GnLabel.__init__`.
+ """
+ dep = GnLabel(self._base_label, **kwargs)
+ repo = dep.repo()
+ if repo:
+ self._repos.add(repo)
+ if dep.public():
+ self.public_deps.add(dep)
+ else:
+ self.deps.add(dep)
+
+ def add_visibility(self, **kwargs) -> None:
+ """Adds a GN visibility scope using this target's base label.
+
+ This removes redundant scopes:
+ * If the scope to be added is already within an existing scope, it is
+ ignored.
+ * If existing scopes are within the scope being added, they are removed.
+
+ Keyword Args:
+ Same as `GnVisibility.__init__`.
+ """
+ new_scope = GnVisibility(self._base_label, self._label, **kwargs)
+ if any(new_scope.within(scope) for scope in self.visibility):
+ return
+ self.visibility = list(
+ filter(lambda scope: not scope.within(new_scope), self.visibility)
+ )
+ self.visibility.append(new_scope)
+
+ def make_relative(self, item: Union[GnLabel, GnVisibility]) -> str:
+ """Returns a label relative to this target.
+
+ Args:
+ item: The GN item to rebase relative to this target.
+ """
+ return item.relative_to(self._label)
diff --git a/pw_build/py/pw_build/gn_utils.py b/pw_build/py/pw_build/gn_utils.py
new file mode 100644
index 000000000..28d3b6894
--- /dev/null
+++ b/pw_build/py/pw_build/gn_utils.py
@@ -0,0 +1,361 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Utilities for manipulating GN labels and paths."""
+
+from __future__ import annotations
+
+import re
+
+from pathlib import PurePosixPath
+from typing import Optional, Union
+
+
+class MalformedGnError(Exception):
+ """Raised when creating a GN object fails."""
+
+
+class GnPath:
+ """Represents a GN source path to a file in the source tree."""
+
+ def __init__(
+ self,
+ base: Union[str, PurePosixPath, GnPath],
+ bazel: Optional[str] = None,
+ gn: Optional[str] = None, # pylint: disable=invalid-name
+ ) -> None:
+ """Creates a GN source path.
+
+ Args:
+ base: A base GN source path. Other parameters are used to define
+ this object relative to the base.
+ bazel: A Bazel path relative to `base`.
+ gn: A GN source path relative to `base`.
+ """
+ self._path: PurePosixPath
+ base_path = _as_path(base)
+ if bazel:
+ self._from_bazel(base_path, bazel)
+ elif gn:
+ self._from_gn(base_path, gn)
+ else:
+ self._path = base_path
+
+ def __str__(self) -> str:
+ return str(self._path)
+
+ def _from_bazel(self, base_path: PurePosixPath, label: str) -> None:
+ """Populates this object using a Bazel file label.
+
+ A Bazel label looks like:
+ //[{package-path}][:{relative-path}]
+ e.g.
+ "//foo" => "$base/foo"
+ "//:bar/baz.txt" => "$base/bar/baz.txt"
+ "//foo:bar/baz.txt" => "$base/foo/bar/baz.txt"
+ """
+ match = re.match(r'//([^():]*)(?::([^():]+))?', label)
+ if not match:
+ raise MalformedGnError(f'invalid path: {label}')
+ groups = filter(None, match.groups())
+ self._path = base_path.joinpath(*groups)
+
+ def _from_gn(self, base_path: PurePosixPath, path: str) -> None:
+ """Populates this object using a GN label.
+
+ Source-relative paths interpreted relative to `base`. Source-absolute
+ paths are used directly.
+ """
+ if path.startswith('//'):
+ self._path = PurePosixPath(path)
+ else:
+ self._path = base_path.joinpath(path)
+
+ def path(self) -> str:
+ """Returns the object's path."""
+ return str(self._path)
+
+ def file(self) -> str:
+ """Like GN's `get_path_info(..., "file")`."""
+ return self._path.name
+
+ def name(self) -> str:
+ """Like GN's `get_path_info(..., "name")`."""
+ return self._path.stem
+
+ def extension(self) -> str:
+ """Like GN's `get_path_info(..., "extension")`."""
+ suffix = self._path.suffix
+ return suffix[1:] if suffix.startswith('.') else suffix
+
+ def dir(self) -> str:
+ """Like GN's `get_path_info(..., "dir")`."""
+ return str(self._path.parent)
+
+
+class GnLabel:
+ """Represents a GN dependency.
+
+ See https://gn.googlesource.com/gn/+/main/docs/reference.md#labels.
+ """
+
+ def __init__(
+ self,
+ base: Union[str, PurePosixPath, GnLabel],
+ public: bool = False,
+ bazel: Optional[str] = None,
+ gn: Optional[str] = None, # pylint: disable=invalid-name
+ ) -> None:
+ """Creates a GN label.
+
+ Args:
+ base: A base GN label. Other parameters are used to define this
+ object relative to the base.
+ public: When this label is used to refer to a GN `dep`, this flag
+ indicates if it should be a `public_dep`.
+ bazel: A Bazel label relative to `base`.
+ gn: A GN label relative to `base`.
+ """
+ self._name: str
+ self._path: PurePosixPath
+ self._toolchain: Optional[str] = None
+ self._public: bool = public
+ self._repo: Optional[str] = None
+ base_path = _as_path(base)
+ if bazel:
+ self._from_bazel(base_path, bazel)
+ elif gn:
+ self._from_gn(base_path, gn)
+ elif ':' in str(base_path):
+ parts = str(base_path).split(':')
+ self._path = PurePosixPath(':'.join(parts[:-1]))
+ self._name = parts[-1]
+ elif isinstance(base, GnLabel):
+ self._path = base._path
+ self._name = base._name
+ else:
+ self._path = base_path
+ self._name = self._path.name
+
+ def __str__(self):
+ return self.with_toolchain() if self._toolchain else self.no_toolchain()
+
+ def __eq__(self, other):
+ return str(self) == str(other)
+
+ def __hash__(self):
+ return hash(str(self))
+
+ def _from_bazel(self, base: PurePosixPath, label: str):
+ """Populates this object using a Bazel label."""
+ match = re.match(r'(?:@([^():/]*))?//(.+)', label)
+ if not match:
+ raise MalformedGnError(f'invalid label: {label}')
+ self._repo = match[1]
+ if self._repo:
+ self._from_gn(PurePosixPath('$repo'), match[2])
+ else:
+ self._from_gn(base, match[2])
+
+ def _from_gn(self, base: PurePosixPath, label: str):
+ """Populates this object using a GN label."""
+ if label.startswith('//') or label.startswith('$'):
+ path = label
+ else:
+ path = str(base.joinpath(label))
+ if ':' in path:
+ parts = path.split(':')
+ self._path = PurePosixPath(':'.join(parts[:-1]))
+ self._name = parts[-1]
+ else:
+ self._path = PurePosixPath(path)
+ self._name = self._path.name
+ parts = []
+ for part in self._path.parts:
+ if part == '..' and parts and parts[-1] != '..':
+ parts.pop()
+ else:
+ parts.append(part)
+ self._path = PurePosixPath(*parts)
+
+ def name(self) -> str:
+ """Like GN's `get_label_info(..., "name"`)."""
+ return self._name
+
+ def dir(self) -> str:
+ """Like GN's `get_label_info(..., "dir"`)."""
+ return str(self._path)
+
+ def no_toolchain(self) -> str:
+ """Like GN's `get_label_info(..., "label_no_toolchain"`)."""
+ if self._path == PurePosixPath():
+ return f':{self._name}'
+ name = f':{self._name}' if self._name != self._path.name else ''
+ return f'{self._path}{name}'
+
+ def with_toolchain(self) -> str:
+ """Like GN's `get_label_info(..., "label_with_toolchain"`)."""
+ toolchain = self._toolchain if self._toolchain else 'default_toolchain'
+ if self._path == PurePosixPath():
+ return f':{self._name}({toolchain})'
+ name = f':{self._name}' if self._name != self._path.name else ''
+ return f'{self._path}{name}({toolchain})'
+
+ def public(self) -> bool:
+ """Returns whether this is a public dep."""
+ return self._public
+
+ def repo(self) -> str:
+ """Returns the label's repo, if any."""
+ return self._repo or ''
+
+ def resolve_repo(self, repo: str) -> None:
+ """Replaces the repo placeholder with the given value."""
+ if self._path and self._path.parts[0] == '$repo':
+ self._path = PurePosixPath(
+ '$dir_pw_third_party', repo, *self._path.parts[1:]
+ )
+
+ def relative_to(self, start: Union[str, PurePosixPath, GnLabel]) -> str:
+ """Returns a label string relative to the given starting label."""
+ start_path = _as_path(start)
+ if not start:
+ return self.no_toolchain()
+ if self._path == start_path:
+ return f':{self._name}'
+ path = _relative_to(self._path, start_path)
+ name = f':{self._name}' if self._name != self._path.name else ''
+ return f'{path}{name}'
+
+ def joinlabel(self, relative: str) -> GnLabel:
+ """Creates a new label by extending the current label."""
+ return GnLabel(self._path.joinpath(relative))
+
+
+class GnVisibility:
+ """Represents a GN visibility scope."""
+
+ def __init__(
+ self,
+ base: Union[str, PurePosixPath, GnLabel],
+ label: Union[str, PurePosixPath, GnLabel],
+ bazel: Optional[str] = None,
+ gn: Optional[str] = None, # pylint: disable=invalid-name
+ ) -> None:
+ """Creates a GN visibility scope.
+
+ Args:
+ base: A base GN label. Other parameters are used to define this
+ object relative to the base.
+ label: The label of the directory in which this scope is being
+ defined.
+ bazel: An absolute Bazel visibility label.
+ gn: A GN visibility label.
+ """
+ self._scope: GnLabel
+ label_path = _as_path(label)
+ if bazel:
+ self._from_bazel(_as_path(base), label_path, bazel)
+ elif gn:
+ self._from_gn(label_path, gn)
+ else:
+ self._scope = GnLabel(label)
+
+ def __str__(self):
+ return str(self._scope)
+
+ def _from_bazel(
+ self, base: PurePosixPath, label: PurePosixPath, scope: str
+ ):
+ """Populates this object using a Bazel visibility label."""
+ if scope == '//visibility:public':
+ self._scope = GnLabel('//*')
+ elif scope == '//visibility:private':
+ self._scope = GnLabel(label, gn=':*')
+ elif not (match := re.match(r'//([^():]*):([^():]+)', scope)):
+ raise MalformedGnError(f'invalid visibility scope: {scope}')
+ elif match[2] == '__subpackages__':
+ self._scope = GnLabel(base, gn=f'{match[1]}/*')
+ elif match[2] == '__pkg__':
+ self._scope = GnLabel(base, gn=f'{match[1]}:*')
+ else:
+ raise MalformedGnError(f'unsupported visibility scope: {scope}')
+
+ def _from_gn(self, label: PurePosixPath, scope: str):
+ """Populates this object using a GN visibility scope."""
+ self._scope = GnLabel(label, gn=scope)
+
+ def relative_to(self, start: Union[str, PurePosixPath, GnLabel]) -> str:
+ """Returns a label string relative to the given starting label."""
+ return self._scope.relative_to(start)
+
+ def within(self, other: GnVisibility) -> bool:
+ """Returns whether this scope is a subset of another."""
+ as_label = GnLabel(str(other))
+ if as_label.name() == '*':
+ _path = self._scope.dir()
+ other_path = as_label.dir()
+ if other_path == '//*':
+ return True
+ if other_path.endswith('*'):
+ parent = PurePosixPath(other_path).parent
+ return _is_relative_to(_path, parent)
+ return _path == other_path
+ return str(self) == str(other)
+
+
+def _as_path(item: Union[str, GnPath, GnLabel, PurePosixPath]) -> PurePosixPath:
+ """Converts an argument to be a PurePosixPath.
+
+ Args:
+ label: A string, path, or label to be converted to a PurePosixPath.
+ """
+ if isinstance(item, str):
+ return PurePosixPath(item)
+ if isinstance(item, GnPath):
+ return PurePosixPath(item.path())
+ if isinstance(item, GnLabel):
+ return PurePosixPath(item.dir())
+ return item
+
+
+def _is_relative_to(
+ path: Union[str, PurePosixPath], start: Union[str, PurePosixPath]
+) -> bool:
+ """Returns whether `path` is a subdirectory of `start`."""
+ _path = PurePosixPath(path)
+ _start = PurePosixPath(start)
+ try:
+ _path.relative_to(_start)
+ return True
+ except ValueError:
+ return False
+
+
+def _relative_to(
+ path: Union[str, PurePosixPath], start: Union[str, PurePosixPath]
+) -> PurePosixPath:
+ """Like `PosixPath._relative_to`, but can ascend directories as well."""
+ if not start:
+ return PurePosixPath(path)
+ _path = PurePosixPath(path)
+ _start = PurePosixPath(start)
+ if _path.parts[0] != _start.parts[0]:
+ return _path
+ ascend = PurePosixPath()
+ while not _is_relative_to(_path, _start):
+ if _start.parent == PurePosixPath():
+ break
+ _start = _start.parent
+ ascend = ascend.joinpath('..')
+ return ascend.joinpath(_path.relative_to(_start))
diff --git a/pw_build/py/pw_build/gn_writer.py b/pw_build/py/pw_build/gn_writer.py
new file mode 100644
index 000000000..354a5dec2
--- /dev/null
+++ b/pw_build/py/pw_build/gn_writer.py
@@ -0,0 +1,379 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Writes a formatted BUILD.gn _file."""
+
+import os
+import subprocess
+
+from datetime import datetime
+from pathlib import Path, PurePath, PurePosixPath
+from types import TracebackType
+from typing import Dict, IO, Iterable, Iterator, List, Optional, Type, Union
+
+from pw_build.gn_config import GnConfig, GN_CONFIG_FLAGS
+from pw_build.gn_target import GnTarget
+from pw_build.gn_utils import GnLabel, GnPath, MalformedGnError
+
+COPYRIGHT_HEADER = f'''
+# Copyright {datetime.now().year} The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!'''
+
+
+class GnWriter:
+ """Represents a partial BUILD.gn file being constructed.
+
+ Except for testing , callers should prefer using `GnFile`. That
+ object wraps this one, and ensures that the GN file produced includes the
+ relevant copyright header and is formatted correctly.
+
+ Attributes:
+ repos: A mapping of repository names to build args. These are used to
+ replace repository names when writing labels.
+ aliases: A mapping of label names to build args. These can be used to
+ rewrite labels with alternate names, e.g. "gtest" to "googletest".
+ """
+
+ def __init__(self, file: IO) -> None:
+ self._file: IO = file
+ self._scopes: List[str] = []
+ self._margin: str = ''
+ self._needs_blank: bool = False
+ self.repos: Dict[str, str] = {}
+ self.aliases: Dict[str, str] = {}
+
+ def write_comment(self, comment: Optional[str] = None) -> None:
+ """Adds a GN comment.
+
+ Args:
+ comment: The comment string to write.
+ """
+ if not comment:
+ self.write('#')
+ return
+ while len(comment) > 78:
+ index = comment.rfind(' ', 0, 78)
+ if index < 0:
+ break
+ self.write(f'# {comment[:index]}')
+ comment = comment[index + 1 :]
+ self.write(f'# {comment}')
+
+ def write_import(self, gni: Union[str, PurePosixPath, GnPath]) -> None:
+ """Adds a GN import.
+
+ Args:
+ gni: The source-relative path to a GN import file.
+ """
+ self._needs_blank = False
+ self.write(f'import("{str(gni)}")')
+ self._needs_blank = True
+
+ def write_imports(self, imports: Iterable[str]) -> None:
+ """Adds a list of GN imports.
+
+ Args:
+ imports: A list of GN import files.
+ """
+ for gni in imports:
+ self.write_import(gni)
+
+ def write_config(self, config: GnConfig) -> None:
+ """Adds a GN config.
+
+ Args:
+ config: The GN config data to write.
+ """
+ if not config:
+ return
+ if not config.label:
+ raise MalformedGnError('missing label for `config`')
+ self.write_target_start('config', config.label.name())
+ for flag in GN_CONFIG_FLAGS:
+ self.write_list(flag, config.get(flag))
+ self.write_end()
+
+ def write_target(self, target: GnTarget) -> None:
+ """Write a GN target.
+
+ Args:
+ target: The GN target data to write.
+ """
+ self.write_comment(
+ f'Generated from //{target.package()}:{target.name()}'
+ )
+ self.write_target_start(target.type(), target.name())
+
+ # GN use no `visibility` to indicate publicly visibile.
+ scopes = filter(lambda s: str(s) != '//*', target.visibility)
+ visibility = [target.make_relative(scope) for scope in scopes]
+ self.write_list('visibility', visibility)
+
+ if not target.check_includes:
+ self.write('check_includes = false')
+ self.write_list('public', [str(path) for path in target.public])
+ self.write_list('sources', [str(path) for path in target.sources])
+ self.write_list('inputs', [str(path) for path in target.inputs])
+
+ for flag in GN_CONFIG_FLAGS:
+ self.write_list(flag, target.config.get(flag))
+ self._write_relative('public_configs', target, target.public_configs)
+ self._write_relative('configs', target, target.configs)
+ self._write_relative('remove_configs', target, target.remove_configs)
+
+ self._write_relative('public_deps', target, target.public_deps)
+ self._write_relative('deps', target, target.deps)
+ self.write_end()
+
+ def _write_relative(
+ self, var_name: str, target: GnTarget, labels: Iterable[GnLabel]
+ ) -> None:
+ """Write a list of labels relative to a target.
+
+ Args:
+ var_name: The name of the GN list variable.
+ target: The GN target to rebase the labels to.
+ labels: The labels to write to the list.
+ """
+ self.write_list(var_name, self._resolve(target, labels))
+
+ def _resolve(
+ self, target: GnTarget, labels: Iterable[GnLabel]
+ ) -> Iterator[str]:
+ """Returns rewritten labels.
+
+ If this label has a repo, it must be a key in this object's `repos` and
+ will be replaced by the corresponding value. If this label is a key in
+ this object's `aliases`, it will be replaced by the corresponding value.
+
+ Args:
+ labels: The labels to resolve.
+ """
+ for label in labels:
+ repo = label.repo()
+ if repo:
+ label.resolve_repo(self.repos[repo])
+ label = GnLabel(self.aliases.get(str(label), str(label)))
+ yield target.make_relative(label)
+
+ def write_target_start(
+ self, target_type: str, target_name: Optional[str] = None
+ ) -> None:
+ """Begins a GN target of the given type.
+
+ Args:
+ target_type: The type of the GN target.
+ target_name: The name of the GN target.
+ """
+ if target_name:
+ self.write(f'{target_type}("{target_name}") {{')
+ self._indent(target_name)
+ else:
+ self.write(f'{target_type}() {{')
+ self._indent(target_type)
+
+ def write_list(
+ self, var_name: str, items: Iterable[str], reorder: bool = True
+ ) -> None:
+ """Adds a named GN list of the given items, if non-empty.
+
+ Args:
+ var_name: The name of the GN list variable.
+ items: The list items to write as strings.
+ reorder: If true, the list is sorted lexicographically.
+ """
+ items = list(items)
+ if not items:
+ return
+ self.write(f'{var_name} = [')
+ self._indent(var_name)
+ if reorder:
+ items = sorted(items)
+ for item in items:
+ self.write(f'"{str(item)}",')
+ self._outdent()
+ self.write(']')
+
+ def write_scope(self, var_name: str) -> None:
+ """Begins a named GN scope.
+
+ Args:
+ var_name: The name of the GN scope variable.
+ """
+ self.write(f'{var_name} = {{')
+ self._indent(var_name)
+
+ def write_if(self, cond: str) -> None:
+ """Begins a GN 'if' condition.
+
+ Args:
+ cond: The conditional expression.
+ """
+ self.write(f'if ({cond}) {{')
+ self._indent(cond)
+
+ def write_else_if(self, cond: str) -> None:
+ """Adds another GN 'if' condition to a previous 'if' condition.
+
+ Args:
+ cond: The conditional expression.
+ """
+ self._outdent()
+ self.write(f'}} else if ({cond}) {{')
+ self._indent(cond)
+
+ def write_else(self) -> None:
+ """Adds a GN 'else' clause to a previous 'if' condition."""
+ last = self._outdent()
+ self.write('} else {')
+ self._indent(f'!({last})')
+
+ def write_end(self) -> None:
+ """Ends a target, scope, or 'if' condition'."""
+ self._outdent()
+ self.write('}')
+ self._needs_blank = True
+
+ def write_blank(self) -> None:
+ """Adds a blank line."""
+ print('', file=self._file)
+ self._needs_blank = False
+
+ def write_preformatted(self, preformatted: str) -> None:
+ """Adds text with minimal formatting.
+
+ The only formatting applied to the given text is to strip any leading
+ whitespace. This allows calls to be more readable by allowing
+ preformatted text to start on a new line, e.g.
+
+ _write_preformatted('''
+ preformatted line 1
+ preformatted line 2
+ preformatted line 3''')
+
+ Args:
+ preformatted: The text to write.
+ """
+ print(preformatted.lstrip(), file=self._file)
+
+ def write(self, text: str) -> None:
+ """Writes to the file, appropriately indented.
+
+ Args:
+ text: The text to indent and write.
+ """
+ if self._needs_blank:
+ self.write_blank()
+ print(f'{self._margin}{text}', file=self._file)
+
+ def _indent(self, scope: str) -> None:
+ """Increases the current margin.
+
+ Saves the scope of indent to aid in debugging. For example, trying to
+ use incorrect code such as
+
+ ```
+ self.write_if('foo')
+ self.write_comment('bar')
+ self.write_else_if('baz')
+ ```
+
+ will throw an exception due to the missing `write_end`. The exception
+ will note that 'baz' was opened but not closed.
+
+ Args:
+ scope: The name of the scope (for debugging).
+ """
+ self._scopes.append(scope)
+ self._margin += ' '
+
+ def _outdent(self) -> str:
+ """Decreases the current margin."""
+ if not self._scopes:
+ raise MalformedGnError('scope closed unexpectedly')
+ last = self._scopes.pop()
+ self._margin = self._margin[2:]
+ self._needs_blank = False
+ return last
+
+ def seal(self) -> None:
+ """Instructs the object that no more writes will occur."""
+ if self._scopes:
+ raise MalformedGnError(f'unclosed scope(s): {self._scopes}')
+
+
+def gn_format(gn_file: Path) -> None:
+ """Calls `gn format` on a BUILD.gn or GN import file."""
+ subprocess.check_call(['gn', 'format', gn_file])
+
+
+class GnFile:
+ """Represents an open BUILD.gn file that is formatted on close.
+
+ Typical usage:
+
+ with GnFile('/path/to/BUILD.gn', 'my-package') as build_gn:
+ build_gn.write_...
+
+ where "write_..." refers to any of the "write" methods of `GnWriter`.
+ """
+
+ def __init__(
+ self, pathname: PurePath, package: Optional[str] = None
+ ) -> None:
+ if pathname.name != 'BUILD.gn' and pathname.suffix != '.gni':
+ raise MalformedGnError(f'invalid GN filename: {pathname}')
+ os.makedirs(pathname.parent, exist_ok=True)
+ self._pathname: PurePath = pathname
+ self._package: Optional[str] = package
+ self._file: IO
+ self._writer: GnWriter
+
+ def __enter__(self) -> GnWriter:
+ """Opens the GN file."""
+ self._file = open(self._pathname, 'w+')
+ self._writer = GnWriter(self._file)
+ self._writer.write_preformatted(COPYRIGHT_HEADER)
+ file = PurePath(*PurePath(__file__).parts[-2:])
+ self._writer.write_comment(
+ f'This file was automatically generated by {file}'
+ )
+ if self._package:
+ self._writer.write_comment(
+ f'It contains GN build targets for {self._package}.'
+ )
+ self._writer.write_blank()
+ return self._writer
+
+ def __exit__(
+ self,
+ exc_type: Optional[Type[BaseException]],
+ exc_val: Optional[BaseException],
+ exc_tb: Optional[TracebackType],
+ ) -> None:
+ """Closes the GN file and formats it."""
+ self._file.close()
+ gn_format(Path(self._pathname))
diff --git a/pw_build/py/pw_build/merge_profraws.py b/pw_build/py/pw_build/merge_profraws.py
new file mode 100755
index 000000000..17681e0c0
--- /dev/null
+++ b/pw_build/py/pw_build/merge_profraws.py
@@ -0,0 +1,133 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Merge multiple profraws together using llvm-profdata."""
+
+import argparse
+import enum
+import json
+import logging
+import subprocess
+import sys
+from pathlib import Path
+from typing import Dict, Any
+
+_LOG = logging.getLogger(__name__)
+
+
+class FailureMode(enum.Enum):
+ ANY = 'any'
+ ALL = 'all'
+
+ def __str__(self):
+ return self.value
+
+
+def _parse_args() -> Dict[str, Any]:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--llvm-profdata-path',
+ type=Path,
+ required=True,
+ help='Path to the llvm-profdata binary to use for merging.',
+ )
+ parser.add_argument(
+ '--test-metadata-path',
+ type=Path,
+ required=True,
+ help='Path to the *.test_metadata.json file that describes all of the '
+ 'tests being used to generate a coverage report.',
+ )
+ parser.add_argument(
+ '--profdata-path',
+ type=Path,
+ required=True,
+ help='Path for the output merged profdata file to use with generating a'
+ ' coverage report for the tests described in --test-metadata.',
+ )
+ parser.add_argument(
+ '--depfile-path',
+ type=Path,
+ required=True,
+ help='Path for the output depfile to convey the extra input '
+ 'requirements from parsing --test-metadata.',
+ )
+ parser.add_argument(
+ '--failure-mode',
+ type=FailureMode,
+ choices=list(FailureMode),
+ required=False,
+ default=FailureMode.ANY,
+ help='Sets the llvm-profdata --failure-mode option.',
+ )
+ return vars(parser.parse_args())
+
+
+def merge_profraws(
+ llvm_profdata_path: Path,
+ test_metadata_path: Path,
+ profdata_path: Path,
+ depfile_path: Path,
+ failure_mode: FailureMode,
+) -> int:
+ """Merge multiple profraws together using llvm-profdata."""
+
+ # Open the test_metadata_path, parse it to JSON, and extract out the
+ # profraws.
+ test_metadata = json.loads(test_metadata_path.read_text())
+ profraw_paths = [
+ str(obj['path'])
+ for obj in test_metadata
+ if 'type' in obj and obj['type'] == 'profraw'
+ ]
+
+ # Generate merged profdata.
+ command = [
+ str(llvm_profdata_path),
+ 'merge',
+ '--sparse',
+ '--failure-mode',
+ str(failure_mode),
+ '-o',
+ str(profdata_path),
+ ] + profraw_paths
+
+ _LOG.info('')
+ _LOG.info(' '.join(command))
+ _LOG.info('')
+
+ output = subprocess.run(command)
+ if output.returncode != 0:
+ return output.returncode
+
+ # Generate the depfile that describes the dependency on the profraws used to
+ # create profdata_path.
+ depfile_path.write_text(
+ ''.join(
+ [
+ str(profdata_path),
+ ': \\\n',
+ *[str(path) + ' \\\n' for path in profraw_paths],
+ ]
+ )
+ )
+
+ return 0
+
+
+def main() -> int:
+ return merge_profraws(**_parse_args())
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/pw_build/py/pw_build/pigweed_upstream_build.py b/pw_build/py/pw_build/pigweed_upstream_build.py
new file mode 100644
index 000000000..5c3ef8ec3
--- /dev/null
+++ b/pw_build/py/pw_build/pigweed_upstream_build.py
@@ -0,0 +1,118 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Default pw build script for upstream Pigweed."""
+
+import logging
+from pathlib import Path
+import shutil
+import sys
+
+import pw_presubmit.pigweed_presubmit
+from pw_presubmit.build import gn_args
+
+from pw_build.build_recipe import (
+ BuildCommand,
+ BuildRecipe,
+)
+from pw_build.project_builder_presubmit_runner import (
+ should_gn_gen,
+ main,
+)
+
+
+_LOG = logging.getLogger('pw_build')
+
+
+def pigweed_upstream_main() -> int:
+ """Entry point for Pigweed upstream ``pw build`` command.
+
+ Defines one or more BuildRecipes and passes that along with all of Pigweed
+ upstream presubmit programs to the project_builder_presubmit_runner.main to
+ start a pw build invocation.
+
+ Returns:
+ An int representing the success or failure status of the build; 0 if
+ successful, 1 if failed.
+
+ Command line usage examples:
+
+ .. code-block:: bash
+ pw build --list
+ pw build --step gn_combined_build_check --step gn_python_*
+ pw build --recipe default_ninja
+ pw build --recipe default_* --watch
+ pw build -C out --watch
+ """
+ default_bazel_targets = ['//...:all']
+ bazel_root = Path('out/bazel')
+ bazel_root.mkdir(parents=True, exist_ok=True)
+
+ default_gn_gen_command = [
+ 'gn',
+ 'gen',
+ '{build_dir}',
+ '--export-compile-commands',
+ ]
+ if shutil.which('ccache'):
+ default_gn_gen_command.append(gn_args(pw_command_launcher='ccache'))
+
+ build_recipes = [
+ BuildRecipe(
+ build_dir=Path('out/gn'),
+ title='default_ninja',
+ steps=[
+ BuildCommand(
+ run_if=should_gn_gen,
+ command=default_gn_gen_command,
+ ),
+ BuildCommand(
+ build_system_command='ninja',
+ targets=['default'],
+ ),
+ ],
+ ),
+ BuildRecipe(
+ build_dir=bazel_root,
+ title='default_bazel',
+ steps=[
+ BuildCommand(
+ build_system_command='bazel',
+ build_system_extra_args=[
+ 'build',
+ '--verbose_failures',
+ '--worker_verbose',
+ ],
+ targets=default_bazel_targets,
+ ),
+ BuildCommand(
+ build_system_command='bazel',
+ build_system_extra_args=[
+ 'test',
+ '--test_output=errors',
+ ],
+ targets=default_bazel_targets,
+ ),
+ ],
+ ),
+ ]
+
+ return main(
+ presubmit_programs=pw_presubmit.pigweed_presubmit.PROGRAMS,
+ build_recipes=build_recipes,
+ default_root_logfile=Path('out/build.txt'),
+ )
+
+
+if __name__ == '__main__':
+ sys.exit(pigweed_upstream_main())
diff --git a/pw_build/py/pw_build/pip_install_python_deps.py b/pw_build/py/pw_build/pip_install_python_deps.py
index 4768bed9c..1e4633e2b 100644
--- a/pw_build/py/pw_build/pip_install_python_deps.py
+++ b/pw_build/py/pw_build/pip_install_python_deps.py
@@ -14,8 +14,10 @@
"""Pip install Pigweed Python packages."""
import argparse
+import logging
from pathlib import Path
import subprocess
+import shlex
import sys
from typing import List, Tuple
@@ -25,6 +27,8 @@ except ImportError:
# Load from python_package from this directory if pw_build is not available.
from python_package import load_packages # type: ignore
+_LOG = logging.getLogger('pw_build.pip_install_python_deps')
+
def _parse_args() -> Tuple[argparse.Namespace, List[str]]:
parser = argparse.ArgumentParser(description=__doc__)
@@ -65,8 +69,14 @@ def main(
gn_targets: List[str],
pip_args: List[str],
) -> int:
- """Find matching python packages to pip install."""
- pip_target_dirs: List[str] = []
+ """Find matching python packages to pip install.
+
+ Raises:
+ NoMatchingGnPythonDependency: if a given gn_target is missing.
+ FileNotFoundError: if a Python wheel was not found when using pip install
+ with --require-hashes.
+ """
+ pip_target_dirs: List[Path] = []
py_packages = load_packages([python_dep_list_files], ignore_missing=True)
for pkg in py_packages:
@@ -74,7 +84,7 @@ def main(
if not any(valid_target):
continue
top_level_source_dir = pkg.package_dir
- pip_target_dirs.append(str(top_level_source_dir.parent.resolve()))
+ pip_target_dirs.append(top_level_source_dir.parent)
if not pip_target_dirs:
raise NoMatchingGnPythonDependency(
@@ -90,7 +100,34 @@ def main(
command_args += pip_args
if editable_pip_install:
command_args.append('--editable')
- command_args.append(target)
+
+ if '--require-hashes' in pip_args:
+ build_wheel_path = target.with_suffix('._build_wheel')
+ req_file = build_wheel_path / 'requirements.txt'
+ req_file_str = str(req_file.resolve())
+ if not req_file.exists():
+ raise FileNotFoundError(
+ 'Missing Python wheel requirement file: ' + req_file_str
+ )
+ # Install the wheel requirement file
+ command_args.extend(['--requirement', req_file_str])
+ # Add the wheel dir to --find-links
+ command_args.extend(
+ ['--find-links', str(build_wheel_path.resolve())]
+ )
+ # Switch any constraint files to requirements. Hashes seem to be
+ # ignored in constraint files.
+ command_args = list(
+ '--requirement' if arg == '--constraint' else arg
+ for arg in command_args
+ )
+
+ else:
+ # Pass the target along to pip with no modifications.
+ command_args.append(str(target.resolve()))
+
+ quoted_command_args = ' '.join(shlex.quote(arg) for arg in command_args)
+ _LOG.info('Run ==> %s', quoted_command_args)
process = subprocess.run(
command_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
@@ -103,6 +140,8 @@ def main(
if __name__ == '__main__':
+ logging.basicConfig(format='%(message)s', level=logging.DEBUG)
+
# Parse this script's args and pass any remaining args to pip.
argparse_args, remaining_args_for_pip = _parse_args()
diff --git a/pw_build/py/pw_build/project_builder.py b/pw_build/py/pw_build/project_builder.py
index cb8ab5941..d90777c21 100644
--- a/pw_build/py/pw_build/project_builder.py
+++ b/pw_build/py/pw_build/project_builder.py
@@ -126,10 +126,67 @@ def _exit_due_to_interrupt() -> None:
_NINJA_BUILD_STEP = re.compile(
- r'^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$'
+ # Start of line
+ r'^'
+ # Step count: [1234/5678]
+ r'\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\]'
+ # whitespace
+ r' *'
+ # Build step text
+ r'(?P<action>.*)$'
)
-_NINJA_FAILURE_TEXT = '\033[31mFAILED: '
+_BAZEL_BUILD_STEP = re.compile(
+ # Start of line
+ r'^'
+ # Optional starting green color
+ r'(?:\x1b\[32m)?'
+ # Step count: [1,234 / 5,678]
+ r'\[(?P<step>[0-9,]+) */ *(?P<total_steps>[0-9,]+)\]'
+ # Optional ending clear color and space
+ r'(?:\x1b\[0m)? *'
+ # Build step text
+ r'(?P<action>.*)$'
+)
+
+_NINJA_FAILURE = re.compile(
+ # Start of line
+ r'^'
+ # Optional red color
+ r'(?:\x1b\[31m)?'
+ r'FAILED:'
+ r' '
+ # Optional color reset
+ r'(?:\x1b\[0m)?'
+)
+
+_BAZEL_FAILURE = re.compile(
+ # Start of line
+ r'^'
+ # Optional red color
+ r'(?:\x1b\[31m)?'
+ # Optional bold color
+ r'(?:\x1b\[1m)?'
+ r'FAIL:'
+ # Optional color reset
+ r'(?:\x1b\[0m)?'
+ # whitespace
+ r' *'
+ r'.*bazel-out.*'
+)
+
+_BAZEL_ELAPSED_TIME = re.compile(
+ # Start of line
+ r'^'
+ # Optional green color
+ r'(?:\x1b\[32m)?'
+ r'INFO:'
+ # single space
+ r' '
+ # Optional color reset
+ r'(?:\x1b\[0m)?'
+ r'Elapsed time:'
+)
def execute_command_no_logging(
@@ -147,7 +204,11 @@ def execute_command_no_logging(
returncode = None
while returncode is None:
if BUILDER_CONTEXT.build_stopping():
- proc.terminate()
+ try:
+ proc.terminate()
+ except ProcessLookupError:
+ # Process already stopped.
+ pass
returncode = proc.poll()
time.sleep(0.05)
print()
@@ -167,6 +228,19 @@ def execute_command_with_logging(
current_stdout = ''
returncode = None
+ starting_failure_regex = _NINJA_FAILURE
+ ending_failure_regex = _NINJA_BUILD_STEP
+ build_step_regex = _NINJA_BUILD_STEP
+
+ if command[0].endswith('ninja'):
+ build_step_regex = _NINJA_BUILD_STEP
+ starting_failure_regex = _NINJA_FAILURE
+ ending_failure_regex = _NINJA_BUILD_STEP
+ elif command[0].endswith('bazel'):
+ build_step_regex = _BAZEL_BUILD_STEP
+ starting_failure_regex = _BAZEL_FAILURE
+ ending_failure_regex = _BAZEL_ELAPSED_TIME
+
with subprocess.Popen(
command,
env=env,
@@ -175,6 +249,7 @@ def execute_command_with_logging(
errors='replace',
) as proc:
BUILDER_CONTEXT.register_process(recipe, proc)
+ should_log_build_steps = BUILDER_CONTEXT.log_build_steps
# Empty line at the start.
logger.info('')
@@ -194,24 +269,31 @@ def execute_command_with_logging(
returncode = proc.poll()
continue
- line_match_result = _NINJA_BUILD_STEP.match(output)
+ line_match_result = build_step_regex.match(output)
if line_match_result:
if failure_line and not BUILDER_CONTEXT.build_stopping():
recipe.status.log_last_failure()
failure_line = False
matches = line_match_result.groupdict()
recipe.status.current_step = line_match_result.group(0)
- step = int(matches.get('step', 0))
- total_steps = int(matches.get('total_steps', 1))
+ # Remove commas from step and total_steps strings
+ step = int(matches.get('step', '0').replace(',', ''))
+ total_steps = int(
+ matches.get('total_steps', '1').replace(',', '')
+ )
recipe.status.percent = float(step / total_steps)
logger_method = logger.info
- if output.startswith(_NINJA_FAILURE_TEXT):
+ if starting_failure_regex.match(output):
logger_method = logger.error
if failure_line and not BUILDER_CONTEXT.build_stopping():
recipe.status.log_last_failure()
recipe.status.increment_error_count()
failure_line = True
+ elif ending_failure_regex.match(output):
+ if failure_line and not BUILDER_CONTEXT.build_stopping():
+ recipe.status.log_last_failure()
+ failure_line = False
# Mypy output mixes character encoding in color coded output
# and uses the 'sgr0' (or exit_attribute_mode) capability from the
@@ -226,7 +308,14 @@ def execute_command_with_logging(
# sequences.
stripped_output = output.replace('\x1b(B', '').strip()
- if not line_match_result:
+ # If this isn't a build step.
+ if not line_match_result or (
+ # Or if this is a build step and logging build steps is
+ # requested:
+ line_match_result
+ and should_log_build_steps
+ ):
+ # Log this line.
logger_method(stripped_output)
recipe.status.current_step = stripped_output
@@ -239,7 +328,11 @@ def execute_command_with_logging(
line_processed_callback(recipe)
if BUILDER_CONTEXT.build_stopping():
- proc.terminate()
+ try:
+ proc.terminate()
+ except ProcessLookupError:
+ # Process already stopped.
+ pass
recipe.status.return_code = returncode
@@ -331,7 +424,7 @@ def log_build_recipe_finish(
if (
not BUILDER_CONTEXT.build_stopping()
and cfg.status.failed()
- and cfg.status.error_count == 0
+ and (cfg.status.error_count == 0 or cfg.status.has_empty_ninja_errors())
):
cfg.status.log_entire_recipe_logfile()
@@ -401,6 +494,8 @@ class ProjectBuilder: # pylint: disable=too-many-instance-attributes
the ``-k`` option.
banners: Print the project banner at the start of each build.
allow_progress_bars: If False progress bar output will be disabled.
+ log_build_steps: If True all build step lines will be logged to the
+ screen and logfiles. Default: False.
colors: Print ANSI colors to stdout and logfiles
log_level: Optional log_level, defaults to logging.INFO.
root_logfile: Optional root logfile.
@@ -437,6 +532,7 @@ class ProjectBuilder: # pylint: disable=too-many-instance-attributes
log_level: int = logging.INFO,
allow_progress_bars: bool = True,
use_verbatim_error_log_formatting: bool = False,
+ log_build_steps: bool = False,
):
self.charset: ProjectBuilderCharset = charset
self.abort_callback = abort_callback
@@ -478,6 +574,7 @@ class ProjectBuilder: # pylint: disable=too-many-instance-attributes
# Progress bar enable/disable flag
self.allow_progress_bars = allow_progress_bars
+ self.log_build_steps = log_build_steps
self.stdout_proxy: Optional[StdoutProxy] = None
# Logger configuration
diff --git a/pw_build/py/pw_build/project_builder_argparse.py b/pw_build/py/pw_build/project_builder_argparse.py
index 18581eb66..7306c0116 100644
--- a/pw_build/py/pw_build/project_builder_argparse.py
+++ b/pw_build/py/pw_build/project_builder_argparse.py
@@ -70,8 +70,8 @@ def add_project_builder_arguments(
help=(
'Additional commands to run. These are run before any -C '
'arguments and may be repeated. For example: '
- "--run-command 'bazel build //pw_cli/...'"
- "--run-command 'bazel test //pw_cli/...'"
+ "--run-command 'bazel build //pw_cli/...' "
+ "--run-command 'bazel test //pw_cli/...' "
"-C out python.lint python.test"
),
)
@@ -84,7 +84,7 @@ def add_project_builder_arguments(
'--jobs',
type=int,
help=(
- 'Specify the number of cores to use for each build system.'
+ 'Specify the number of cores to use for each build system. '
'This is passed to ninja, bazel and make as "-j"'
),
)
@@ -133,7 +133,7 @@ def add_project_builder_arguments(
output_group = parser.add_argument_group(title='Display Output Options')
- # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is
+ # TODO: b/248257406 - Use argparse.BooleanOptionalAction when Python 3.8 is
# no longer supported.
output_group.add_argument(
'--banners',
@@ -148,7 +148,7 @@ def add_project_builder_arguments(
help='Hide pass/fail banners.',
)
- # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is
+ # TODO: b/248257406 - Use argparse.BooleanOptionalAction when Python 3.8 is
# no longer supported.
output_group.add_argument(
'--colors',
diff --git a/pw_build/py/pw_build/project_builder_context.py b/pw_build/py/pw_build/project_builder_context.py
index f45942351..d99362911 100644
--- a/pw_build/py/pw_build/project_builder_context.py
+++ b/pw_build/py/pw_build/project_builder_context.py
@@ -22,6 +22,7 @@ from enum import Enum
import logging
import os
import subprocess
+import time
from typing import Callable, Dict, List, Optional, NoReturn, TYPE_CHECKING
from prompt_toolkit.formatted_text import (
@@ -154,6 +155,9 @@ class ProjectBuilderContext: # pylint: disable=too-many-instance-attributes,too
formatters.Text(' '),
]
+ self._progress_bar_refresh_interval: float = 0.1 # 10 FPS
+ self._last_progress_bar_redraw_time: float = 0.0
+
self._enter_callback: Optional[Callable] = None
key_bindings = KeyBindings()
@@ -183,6 +187,12 @@ class ProjectBuilderContext: # pylint: disable=too-many-instance-attributes,too
def using_progress_bars(self) -> bool:
return bool(self.progress_bar) or self.using_fullscreen
+ @property
+ def log_build_steps(self) -> bool:
+ if self.project_builder:
+ return self.project_builder.log_build_steps
+ return False
+
def interrupted(self) -> bool:
return self.ctrl_c_pressed or self.restart_flag
@@ -221,15 +231,28 @@ class ProjectBuilderContext: # pylint: disable=too-many-instance-attributes,too
def clear_progress_scrollback(self) -> None:
if not self.progress_bar:
return
- self.progress_bar._app_loop.call_soon_threadsafe( # pylint: disable=protected-access
- self.progress_bar.app.renderer.clear
- )
+ if (
+ self.progress_bar.app.is_running
+ and self.progress_bar.app.loop is not None
+ ):
+ self.progress_bar.app.loop.call_soon_threadsafe(
+ self.progress_bar.app.renderer.clear
+ )
def redraw_progress(self) -> None:
if not self.progress_bar:
return
if hasattr(self.progress_bar, 'app'):
- self.progress_bar.invalidate()
+ redraw_time = time.time()
+ # Has enough time passed since last redraw?
+ if redraw_time > (
+ self._last_progress_bar_redraw_time
+ + self._progress_bar_refresh_interval
+ ):
+ # Update last redraw time
+ self._last_progress_bar_redraw_time = redraw_time
+ # Trigger Prompt Toolkit UI redraw.
+ self.progress_bar.invalidate()
def get_title_style(self) -> str:
if self.restart_flag:
diff --git a/pw_build/py/pw_build/project_builder_presubmit_runner.py b/pw_build/py/pw_build/project_builder_presubmit_runner.py
new file mode 100644
index 000000000..7abb6b717
--- /dev/null
+++ b/pw_build/py/pw_build/project_builder_presubmit_runner.py
@@ -0,0 +1,860 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""pw_build.project_builder_presubmit_runner"""
+
+import argparse
+import fnmatch
+import logging
+from pathlib import Path
+from typing import Callable, Dict, List, Optional, Union
+
+
+import pw_cli.log
+from pw_cli.arguments import (
+ print_completions_for_option,
+ add_tab_complete_arguments,
+)
+from pw_presubmit.presubmit import (
+ Program,
+ Programs,
+ Presubmit,
+ PresubmitContext,
+ Check,
+ fetch_file_lists,
+)
+import pw_presubmit.pigweed_presubmit
+from pw_presubmit.build import GnGenNinja, gn_args, write_gn_args_file
+from pw_presubmit.presubmit_context import get_check_traces, PresubmitCheckTrace
+from pw_presubmit.tools import file_summary
+
+# pw_watch is not required by pw_build, this is an optional feature.
+try:
+ from pw_watch.argparser import ( # type: ignore
+ add_parser_arguments as add_watch_arguments,
+ )
+ from pw_watch.watch import run_watch, watch_setup # type: ignore
+ from pw_watch.watch_app import WatchAppPrefs # type: ignore
+
+ PW_WATCH_AVAILABLE = True
+except ImportError:
+ PW_WATCH_AVAILABLE = False
+
+from pw_build.project_builder import (
+ ProjectBuilder,
+ run_builds,
+ ASCII_CHARSET,
+ EMOJI_CHARSET,
+)
+from pw_build.build_recipe import (
+ BuildCommand,
+ BuildRecipe,
+ create_build_recipes,
+ UnknownBuildSystem,
+)
+from pw_build.project_builder_argparse import add_project_builder_arguments
+from pw_build.project_builder_prefs import ProjectBuilderPrefs
+
+
+_COLOR = pw_cli.color.colors()
+_LOG = logging.getLogger('pw_build')
+
+
+class PresubmitTraceAnnotationError(Exception):
+ """Exception for malformed PresubmitCheckTrace annotations."""
+
+
+def should_gn_gen(out: Path) -> bool:
+ """Returns True if the gn gen command should be run."""
+ # gn gen only needs to run if build.ninja or args.gn files are missing.
+ expected_files = [
+ out / 'build.ninja',
+ out / 'args.gn',
+ ]
+ return any(not gen_file.is_file() for gen_file in expected_files)
+
+
+def should_gn_gen_with_args(gn_arg_dict: Dict[str, str]) -> Callable:
+ """Returns a callable which writes an args.gn file prior to checks.
+
+ Returns:
+ Callable which takes a single Path argument and returns a bool
+ for True if the gn gen command should be run.
+ """
+
+ def _write_args_and_check(out: Path) -> bool:
+ # Always re-write the args.gn file.
+ write_gn_args_file(out / 'args.gn', **gn_arg_dict)
+
+ return should_gn_gen(out)
+
+ return _write_args_and_check
+
+
+def _pw_package_install_command(package_name: str) -> BuildCommand:
+ return BuildCommand(
+ command=[
+ 'pw',
+ '--no-banner',
+ 'package',
+ 'install',
+ package_name,
+ ],
+ )
+
+
+def _pw_package_install_to_build_command(
+ trace: PresubmitCheckTrace,
+) -> BuildCommand:
+ """Returns a BuildCommand from a PresubmitCheckTrace."""
+ package_name = trace.call_annotation.get('pw_package_install', None)
+ if package_name is None:
+ raise PresubmitTraceAnnotationError(
+ 'Missing "pw_package_install" value.'
+ )
+
+ return _pw_package_install_command(package_name)
+
+
+def _bazel_command_args_to_build_commands(
+ trace: PresubmitCheckTrace,
+) -> List[BuildCommand]:
+ """Returns a list of BuildCommands based on a bazel PresubmitCheckTrace."""
+ build_steps: List[BuildCommand] = []
+
+ if not 'bazel' in trace.args:
+ return build_steps
+
+ bazel_command = list(arg for arg in trace.args if not arg.startswith('--'))
+ bazel_options = list(
+ arg for arg in trace.args if arg.startswith('--') and arg != '--'
+ )
+ # Check for `bazel build` or `bazel test`
+ if not (
+ bazel_command[0].endswith('bazel')
+ and bazel_command[1] in ['build', 'test']
+ ):
+ raise UnknownBuildSystem(
+ f'Unable to parse bazel command:\n {trace.args}'
+ )
+
+ bazel_subcommand = bazel_command[1]
+ bazel_targets = bazel_command[2:]
+ if bazel_subcommand == 'build':
+ build_steps.append(
+ BuildCommand(
+ build_system_command='bazel',
+ build_system_extra_args=['build'] + bazel_options,
+ targets=bazel_targets,
+ )
+ )
+ if bazel_subcommand == 'test':
+ build_steps.append(
+ BuildCommand(
+ build_system_command='bazel',
+ build_system_extra_args=['test'] + bazel_options,
+ targets=bazel_targets,
+ )
+ )
+ return build_steps
+
+
+def _presubmit_trace_to_build_commands(
+ ctx: PresubmitContext,
+ presubmit_step: Check,
+) -> List[BuildCommand]:
+ """Convert a presubmit step to a list of BuildCommands.
+
+ Specifically, this handles the following types of PresubmitCheckTraces:
+
+ - pw package installs
+ - gn gen followed by ninja
+ - bazel commands
+
+ If none of the specific scenarios listed above are found the command args
+ are passed along to BuildCommand as is.
+
+ Returns:
+ List of BuildCommands representing each command found in the
+ presubmit_step traces.
+ """
+ build_steps: List[BuildCommand] = []
+
+ presubmit_step(ctx)
+
+ step_traces = get_check_traces(ctx)
+
+ for trace in step_traces:
+ trace_args = list(trace.args)
+ # Check for ninja -t graph command and skip it
+ if trace_args[0].endswith('ninja'):
+ try:
+ dash_t_index = trace_args.index('-t')
+ graph_index = trace_args.index('graph')
+ if graph_index == dash_t_index + 1:
+ # This trace has -t graph, skip it.
+ continue
+ except ValueError:
+ # '-t graph' was not found
+ pass
+
+ if 'pw_package_install' in trace.call_annotation:
+ build_steps.append(_pw_package_install_to_build_command(trace))
+ continue
+
+ if 'bazel' in trace.args:
+ build_steps.extend(_bazel_command_args_to_build_commands(trace))
+ continue
+
+ # Check for gn gen or pw-wrap-ninja
+ transformed_args = []
+ pw_wrap_ninja_found = False
+ gn_found = False
+ gn_gen_found = False
+
+ for arg in trace.args:
+ # Check for a 'gn gen' command
+ if arg == 'gn':
+ gn_found = True
+ if arg == 'gen' and gn_found:
+ gn_gen_found = True
+
+ # Check for pw-wrap-ninja, pw build doesn't use this.
+ if arg == 'pw-wrap-ninja':
+ # Use ninja instead
+ transformed_args.append('ninja')
+ pw_wrap_ninja_found = True
+ continue
+ # Remove --log-actions if pw-wrap-ninja was found. This is a
+ # non-standard ninja arg.
+ if pw_wrap_ninja_found and arg == '--log-actions':
+ continue
+ transformed_args.append(str(arg))
+
+ if gn_gen_found:
+ # Run the command with run_if=should_gn_gen
+ build_steps.append(
+ BuildCommand(run_if=should_gn_gen, command=transformed_args)
+ )
+ else:
+ # Run the command as is.
+ build_steps.append(BuildCommand(command=transformed_args))
+
+ return build_steps
+
+
+def presubmit_build_recipe( # pylint: disable=too-many-locals
+ repo_root: Path,
+ presubmit_out_dir: Path,
+ package_root: Path,
+ presubmit_step: Check,
+ all_files: List[Path],
+ modified_files: List[Path],
+) -> Optional['BuildRecipe']:
+ """Construct a BuildRecipe from a pw_presubmit step."""
+ out_dir = presubmit_out_dir / presubmit_step.name
+
+ ctx = PresubmitContext(
+ root=repo_root,
+ repos=(repo_root,),
+ output_dir=out_dir,
+ failure_summary_log=out_dir / 'failure-summary.log',
+ paths=tuple(modified_files),
+ all_paths=tuple(all_files),
+ package_root=package_root,
+ luci=None,
+ override_gn_args={},
+ num_jobs=None,
+ continue_after_build_error=True,
+ _failed=False,
+ format_options=pw_presubmit.presubmit.FormatOptions.load(),
+ dry_run=True,
+ )
+
+ presubmit_instance = Presubmit(
+ root=repo_root,
+ repos=(repo_root,),
+ output_directory=out_dir,
+ paths=modified_files,
+ all_paths=all_files,
+ package_root=package_root,
+ override_gn_args={},
+ continue_after_build_error=True,
+ rng_seed=1,
+ full=False,
+ )
+
+ program = Program('', [presubmit_step])
+ checks = list(presubmit_instance.apply_filters(program))
+ if not checks:
+ _LOG.warning('')
+ _LOG.warning(
+ 'Step "%s" is not required for the current set of modified files.',
+ presubmit_step.name,
+ )
+ _LOG.warning('')
+ return None
+
+ try:
+ ctx.paths = tuple(checks[0].paths)
+ except IndexError:
+ raise PresubmitTraceAnnotationError(
+ 'Missing pw_presubmit.presubmit.Check for presubmit step:\n'
+ + repr(presubmit_step)
+ )
+
+ if isinstance(presubmit_step, GnGenNinja):
+ # GnGenNinja is directly translatable to a BuildRecipe.
+ selected_gn_args = {
+ name: value(ctx) if callable(value) else value
+ for name, value in presubmit_step.gn_args.items()
+ }
+
+ return BuildRecipe(
+ build_dir=out_dir,
+ title=presubmit_step.name,
+ steps=[
+ _pw_package_install_command(name)
+ for name in presubmit_step._packages # pylint: disable=protected-access
+ ]
+ + [
+ BuildCommand(
+ run_if=should_gn_gen,
+ command=[
+ 'gn',
+ 'gen',
+ str(out_dir),
+ gn_args(**selected_gn_args),
+ ],
+ ),
+ BuildCommand(
+ build_system_command='ninja',
+ targets=presubmit_step.ninja_targets,
+ ),
+ ],
+ )
+
+ # Unknown type of presubmit, use dry-run to capture subprocess traces.
+ build_steps = _presubmit_trace_to_build_commands(ctx, presubmit_step)
+
+ out_dir.mkdir(parents=True, exist_ok=True)
+
+ return BuildRecipe(
+ build_dir=out_dir,
+ title=presubmit_step.name,
+ steps=build_steps,
+ )
+
+
+def _get_parser(
+ presubmit_programs: Optional[Programs] = None,
+ build_recipes: Optional[List[BuildRecipe]] = None,
+) -> argparse.ArgumentParser:
+ """Setup argparse for pw_build.project_builder and optionally pw_watch."""
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ if PW_WATCH_AVAILABLE:
+ parser = add_watch_arguments(parser)
+ else:
+ parser = add_project_builder_arguments(parser)
+
+ if build_recipes is not None:
+
+ def build_recipe_argparse_type(arg: str) -> List[BuildRecipe]:
+ """Return a list of matching presubmit steps."""
+ assert build_recipes
+ all_recipe_names = list(
+ recipe.display_name for recipe in build_recipes
+ )
+ filtered_names = fnmatch.filter(all_recipe_names, arg)
+
+ if not filtered_names:
+ recipe_name_str = '\n'.join(sorted(all_recipe_names))
+ raise argparse.ArgumentTypeError(
+ f'"{arg}" does not match the name of a recipe.\n\n'
+ f'Valid Recipes:\n{recipe_name_str}'
+ )
+
+ return list(
+ recipe
+ for recipe in build_recipes
+ if recipe.display_name in filtered_names
+ )
+
+ parser.add_argument(
+ '-r',
+ '--recipe',
+ action='extend',
+ default=[],
+ help=(
+ 'Run a build recipe. Include an asterix to match more than one '
+ "name. For example: --recipe 'gn_*'"
+ ),
+ type=build_recipe_argparse_type,
+ )
+
+ if presubmit_programs is not None:
+ # Add presubmit step arguments.
+ all_steps = presubmit_programs.all_steps()
+
+ def presubmit_step_argparse_type(arg: str) -> List[Check]:
+ """Return a list of matching presubmit steps."""
+ filtered_step_names = fnmatch.filter(all_steps.keys(), arg)
+
+ if not filtered_step_names:
+ all_step_names = '\n'.join(sorted(all_steps.keys()))
+ raise argparse.ArgumentTypeError(
+ f'"{arg}" does not match the name of a presubmit step.\n\n'
+ f'Valid Steps:\n{all_step_names}'
+ )
+
+ return list(all_steps[name] for name in filtered_step_names)
+
+ parser.add_argument(
+ '-s',
+ '--step',
+ action='extend',
+ default=[],
+ help=(
+ 'Run presubmit step. Include an asterix to match more than one '
+ "step name. For example: --step '*_format'"
+ ),
+ type=presubmit_step_argparse_type,
+ )
+
+ if build_recipes or presubmit_programs:
+ parser.add_argument(
+ '-l',
+ '--list',
+ action='store_true',
+ default=False,
+ help=('List all known build recipes and presubmit steps.'),
+ )
+
+ if build_recipes:
+ parser.add_argument(
+ '--all',
+ action='store_true',
+ default=False,
+ help=('Run all known build recipes.'),
+ )
+
+ parser.add_argument(
+ '--progress-bars',
+ action='store_true',
+ default=True,
+ help='Show progress bars in the terminal.',
+ )
+
+ parser.add_argument(
+ '--no-progress-bars',
+ action='store_false',
+ dest='progress_bars',
+ help='Hide progress bars in terminal output.',
+ )
+
+ parser.add_argument(
+ '--log-build-steps',
+ action='store_true',
+ help='Show ninja build step log lines in output.',
+ )
+
+ parser.add_argument(
+ '--no-log-build-steps',
+ action='store_false',
+ dest='log_build_steps',
+ help='Hide ninja build steps log lines from log output.',
+ )
+
+ if PW_WATCH_AVAILABLE:
+ parser.add_argument(
+ '-w',
+ '--watch',
+ action='store_true',
+ help='Use pw_watch to monitor changes.',
+ default=False,
+ )
+
+ parser.add_argument(
+ '-b',
+ '--base',
+ help=(
+ 'Git revision to diff for changed files. This is used for '
+ 'presubmit steps.'
+ ),
+ )
+
+ parser = add_tab_complete_arguments(parser)
+
+ parser.add_argument(
+ '--tab-complete-recipe',
+ nargs='?',
+ help='Print tab completions for the supplied recipe name.',
+ )
+
+ parser.add_argument(
+ '--tab-complete-presubmit-step',
+ nargs='?',
+ help='Print tab completions for the supplied presubmit name.',
+ )
+
+ return parser
+
+
+def _get_prefs(
+ args: argparse.Namespace,
+) -> Union[ProjectBuilderPrefs, WatchAppPrefs]:
+ """Load either WatchAppPrefs or ProjectBuilderPrefs.
+
+ Applies the command line args to the correct prefs class.
+
+ Returns:
+ A WatchAppPrefs instance if pw_watch is importable, ProjectBuilderPrefs
+ otherwise.
+ """
+ prefs: Union[ProjectBuilderPrefs, WatchAppPrefs]
+ if PW_WATCH_AVAILABLE:
+ prefs = WatchAppPrefs(load_argparse_arguments=add_watch_arguments)
+ prefs.apply_command_line_args(args)
+ else:
+ prefs = ProjectBuilderPrefs(
+ load_argparse_arguments=add_project_builder_arguments
+ )
+ prefs.apply_command_line_args(args)
+ return prefs
+
+
+def load_presubmit_build_recipes(
+ presubmit_programs: Programs,
+ presubmit_steps: List[Check],
+ repo_root: Path,
+ presubmit_out_dir: Path,
+ package_root: Path,
+ all_files: List[Path],
+ modified_files: List[Path],
+ default_presubmit_step_names: Optional[List[str]] = None,
+) -> List[BuildRecipe]:
+ """Convert selected presubmit steps into a list of BuildRecipes."""
+ # Use the default presubmit if no other steps or command line out
+ # directories are provided.
+ if len(presubmit_steps) == 0 and default_presubmit_step_names:
+ default_steps = list(
+ check
+ for name, check in presubmit_programs.all_steps().items()
+ if name in default_presubmit_step_names
+ )
+ presubmit_steps = default_steps
+
+ presubmit_recipes: List[BuildRecipe] = []
+
+ for step in presubmit_steps:
+ build_recipe = presubmit_build_recipe(
+ repo_root,
+ presubmit_out_dir,
+ package_root,
+ step,
+ all_files,
+ modified_files,
+ )
+ if build_recipe:
+ presubmit_recipes.append(build_recipe)
+
+ return presubmit_recipes
+
+
+def _tab_complete_recipe(
+ build_recipes: List[BuildRecipe],
+ text: str = '',
+) -> None:
+ for name in sorted(recipe.display_name for recipe in build_recipes):
+ if name.startswith(text):
+ print(name)
+
+
+def _tab_complete_presubmit_step(
+ presubmit_programs: Programs,
+ text: str = '',
+) -> None:
+ for name in sorted(presubmit_programs.all_steps().keys()):
+ if name.startswith(text):
+ print(name)
+
+
+def _list_steps_and_recipes(
+ presubmit_programs: Optional[Programs] = None,
+ build_recipes: Optional[List[BuildRecipe]] = None,
+) -> None:
+ if presubmit_programs:
+ _LOG.info('Presubmit steps:')
+ print()
+ for name in sorted(presubmit_programs.all_steps().keys()):
+ print(name)
+ print()
+ if build_recipes:
+ _LOG.info('Build recipes:')
+ print()
+ for name in sorted(recipe.display_name for recipe in build_recipes):
+ print(name)
+ print()
+
+
+def _print_usage_help(
+ presubmit_programs: Optional[Programs] = None,
+ build_recipes: Optional[List[BuildRecipe]] = None,
+) -> None:
+ """Print usage examples with known presubmits and build recipes."""
+
+ def print_pw_build(
+ option: str, arg: Optional[str] = None, end: str = '\n'
+ ) -> None:
+ print(
+ ' '.join(
+ [
+ 'pw build',
+ _COLOR.cyan(option),
+ _COLOR.yellow(arg) if arg else '',
+ ]
+ ),
+ end=end,
+ )
+
+ if presubmit_programs:
+ print(_COLOR.green('All presubmit steps:'))
+ for name in sorted(presubmit_programs.all_steps().keys()):
+ print_pw_build('--step', name)
+ if build_recipes:
+ if presubmit_programs:
+ # Add a blank line separator
+ print()
+ print(_COLOR.green('All build recipes:'))
+ for name in sorted(recipe.display_name for recipe in build_recipes):
+ print_pw_build('--recipe', name)
+
+ print()
+ print(
+ _COLOR.green(
+ 'Recipe and step names may use wildcards and be repeated:'
+ )
+ )
+ print_pw_build('--recipe', '"default_*"', end=' ')
+ print(
+ _COLOR.cyan('--step'),
+ _COLOR.yellow('step1'),
+ _COLOR.cyan('--step'),
+ _COLOR.yellow('step2'),
+ )
+ print()
+ print(_COLOR.green('Run all build recipes:'))
+ print_pw_build('--all')
+ print()
+ print(_COLOR.green('For more help please run:'))
+ print_pw_build('--help')
+
+
+def main(
+ presubmit_programs: Optional[Programs] = None,
+ default_presubmit_step_names: Optional[List[str]] = None,
+ build_recipes: Optional[List[BuildRecipe]] = None,
+ default_build_recipe_names: Optional[List[str]] = None,
+ repo_root: Optional[Path] = None,
+ presubmit_out_dir: Optional[Path] = None,
+ package_root: Optional[Path] = None,
+ default_root_logfile: Path = Path('out/build.txt'),
+ force_pw_watch: bool = False,
+) -> int:
+ """Build upstream Pigweed presubmit steps."""
+ # pylint: disable=too-many-locals
+ parser = _get_parser(presubmit_programs, build_recipes)
+ args = parser.parse_args()
+
+ if args.tab_complete_option is not None:
+ print_completions_for_option(
+ parser,
+ text=args.tab_complete_option,
+ tab_completion_format=args.tab_complete_format,
+ )
+ return 0
+
+ log_level = logging.DEBUG if args.debug_logging else logging.INFO
+
+ pw_cli.log.install(
+ level=log_level,
+ use_color=args.colors,
+ # Hide the date from the timestamp
+ time_format='%H:%M:%S',
+ )
+
+ pw_env = pw_cli.env.pigweed_environment()
+ if pw_env.PW_EMOJI:
+ charset = EMOJI_CHARSET
+ else:
+ charset = ASCII_CHARSET
+
+ if build_recipes and args.tab_complete_recipe is not None:
+ _tab_complete_recipe(build_recipes, text=args.tab_complete_recipe)
+ return 0
+
+ if presubmit_programs and args.tab_complete_presubmit_step is not None:
+ _tab_complete_presubmit_step(
+ presubmit_programs, text=args.tab_complete_presubmit_step
+ )
+ return 0
+
+ # List valid steps + recipes.
+ if hasattr(args, 'list') and args.list:
+ _list_steps_and_recipes(presubmit_programs, build_recipes)
+ return 0
+
+ command_line_dash_c_recipes: List[BuildRecipe] = []
+ # If -C out directories are provided add them to the recipes list.
+ if args.build_directories:
+ prefs = _get_prefs(args)
+ command_line_dash_c_recipes = create_build_recipes(prefs)
+
+ if repo_root is None:
+ repo_root = pw_env.PW_PROJECT_ROOT
+ if presubmit_out_dir is None:
+ presubmit_out_dir = repo_root / 'out/presubmit'
+ if package_root is None:
+ package_root = pw_env.PW_PACKAGE_ROOT
+
+ all_files: List[Path]
+ modified_files: List[Path]
+
+ all_files, modified_files = fetch_file_lists(
+ root=repo_root,
+ repo=repo_root,
+ pathspecs=[],
+ base=args.base,
+ )
+
+ # Log modified file summary just like pw_presubmit if using --base.
+ if args.base:
+ _LOG.info(
+ 'Running steps that apply to modified files since "%s":', args.base
+ )
+ _LOG.info('')
+ for line in file_summary(
+ mf.relative_to(repo_root) for mf in modified_files
+ ):
+ _LOG.info(line)
+ _LOG.info('')
+
+ selected_build_recipes: List[BuildRecipe] = []
+ if build_recipes:
+ if hasattr(args, 'recipe'):
+ selected_build_recipes = args.recipe
+ if not selected_build_recipes and default_build_recipe_names:
+ selected_build_recipes = [
+ recipe
+ for recipe in build_recipes
+ if recipe.display_name in default_build_recipe_names
+ ]
+
+ selected_presubmit_recipes: List[BuildRecipe] = []
+ if presubmit_programs and hasattr(args, 'step'):
+ selected_presubmit_recipes = load_presubmit_build_recipes(
+ presubmit_programs,
+ args.step,
+ repo_root,
+ presubmit_out_dir,
+ package_root,
+ all_files,
+ modified_files,
+ default_presubmit_step_names=default_presubmit_step_names,
+ )
+
+ # If no builds specifed on the command line print a useful help message:
+ if (
+ not selected_build_recipes
+ and not command_line_dash_c_recipes
+ and not selected_presubmit_recipes
+ and not args.all
+ ):
+ _print_usage_help(presubmit_programs, build_recipes)
+ return 1
+
+ if build_recipes and args.all:
+ selected_build_recipes = build_recipes
+
+ # Run these builds in order:
+ recipes_to_build = (
+ # -C dirs
+ command_line_dash_c_recipes
+ # --step 'name'
+ + selected_presubmit_recipes
+ # --recipe 'name'
+ + selected_build_recipes
+ )
+
+ # Always set separate build file logging.
+ if not args.logfile:
+ args.logfile = default_root_logfile
+ if not args.separate_logfiles:
+ args.separate_logfiles = True
+
+ workers = 1
+ if args.parallel:
+ # If parallel is requested and parallel_workers is set to 0 run all
+ # recipes in parallel. That is, use the number of recipes as the worker
+ # count.
+ if args.parallel_workers == 0:
+ workers = len(recipes_to_build)
+ else:
+ workers = args.parallel_workers
+
+ project_builder = ProjectBuilder(
+ build_recipes=recipes_to_build,
+ jobs=args.jobs,
+ banners=args.banners,
+ keep_going=args.keep_going,
+ colors=args.colors,
+ charset=charset,
+ separate_build_file_logging=args.separate_logfiles,
+ # If running builds in serial, send all sub build logs to the root log
+ # window (or terminal).
+ send_recipe_logs_to_root=(workers == 1),
+ root_logfile=args.logfile,
+ root_logger=_LOG,
+ log_level=log_level,
+ allow_progress_bars=args.progress_bars,
+ log_build_steps=args.log_build_steps,
+ )
+
+ if project_builder.should_use_progress_bars():
+ project_builder.use_stdout_proxy()
+
+ if PW_WATCH_AVAILABLE and (
+ force_pw_watch or (args.watch or args.fullscreen)
+ ):
+ event_handler, exclude_list = watch_setup(
+ project_builder,
+ parallel=args.parallel,
+ parallel_workers=workers,
+ fullscreen=args.fullscreen,
+ logfile=args.logfile,
+ separate_logfiles=args.separate_logfiles,
+ )
+
+ run_watch(
+ event_handler,
+ exclude_list,
+ fullscreen=args.fullscreen,
+ )
+ return 0
+
+ # One off build
+ return run_builds(project_builder, workers)
diff --git a/pw_build/py/pw_build/python_package.py b/pw_build/py/pw_build/python_package.py
index d87de1faf..726028faa 100644
--- a/pw_build/py/pw_build/python_package.py
+++ b/pw_build/py/pw_build/python_package.py
@@ -60,6 +60,10 @@ class UnknownPythonPackageName(Exception):
"""Exception thrown when a Python package_name cannot be determined."""
+class UnknownPythonPackageVersion(Exception):
+ """Exception thrown when a Python package version cannot be determined."""
+
+
class MissingSetupSources(Exception):
"""Exception thrown when a Python package is missing setup source files.
@@ -175,13 +179,30 @@ class PythonPackage:
return actual_gn_target_name[-1]
@property
+ def package_version(self) -> str:
+ version = ''
+ if self.config:
+ try:
+ version = self.config['metadata']['version']
+ except KeyError:
+ raise UnknownPythonPackageVersion(
+ 'Unknown Python package version for: '
+ + _pretty_format(self.as_dict())
+ )
+ return version
+
+ @property
def package_dir(self) -> Path:
if self.setup_cfg and self.setup_cfg.is_file():
return self.setup_cfg.parent / self.package_name
root_source_dir = self.top_level_source_dir
if root_source_dir:
return root_source_dir
- return self.sources[0].parent
+ if self.sources:
+ return self.sources[0].parent
+ # If no sources available, assume the setup file root is the
+ # package_dir. This may be the case in a package with data files only.
+ return self.setup_sources[0].parent
@property
def top_level_source_dir(self) -> Optional[Path]:
diff --git a/pw_build/py/pw_build/python_runner.py b/pw_build/py/pw_build/python_runner.py
index 2bd8ac5c6..8fcdb2eaa 100755
--- a/pw_build/py/pw_build/python_runner.py
+++ b/pw_build/py/pw_build/python_runner.py
@@ -41,7 +41,7 @@ except (ImportError, ModuleNotFoundError):
if sys.platform != 'win32':
import fcntl # pylint: disable=import-error
- # TODO(b/227670947): Support Windows.
+ # TODO: b/227670947 - Support Windows.
_LOG = logging.getLogger(__name__)
_LOCK_ACQUISITION_TIMEOUT = 30 * 60 # 30 minutes in seconds
@@ -145,13 +145,15 @@ def acquire_lock(lockfile: Path, exclusive: bool):
"""
if sys.platform == 'win32':
# No-op on Windows, which doesn't have POSIX file locking.
- # TODO(b/227670947): Get this working on Windows, too.
+ # TODO: b/227670947 - Get this working on Windows, too.
return
start_time = time.monotonic()
if exclusive:
+ # pylint: disable-next=used-before-assignment
lock_type = fcntl.LOCK_EX # type: ignore[name-defined]
else:
+ # pylint: disable-next=used-before-assignment
lock_type = fcntl.LOCK_SH # type: ignore[name-defined]
fd = os.open(lockfile, os.O_RDWR | os.O_CREAT)
@@ -165,9 +167,11 @@ def acquire_lock(lockfile: Path, exclusive: bool):
backoff = 1
while time.monotonic() - start_time < _LOCK_ACQUISITION_TIMEOUT:
try:
+ # pylint: disable=used-before-assignment
fcntl.flock( # type: ignore[name-defined]
fd, lock_type | fcntl.LOCK_NB # type: ignore[name-defined]
)
+ # pylint: enable=used-before-assignment
return # Lock acquired!
except BlockingIOError:
pass # Keep waiting.
@@ -337,7 +341,7 @@ def main( # pylint: disable=too-many-arguments,too-many-branches,too-many-local
if working_directory:
run_args['cwd'] = working_directory
- # TODO(b/235239674): Deprecate the --lockfile option as part of the Python
+ # TODO: b/235239674 - Deprecate the --lockfile option as part of the Python
# GN template refactor.
if lockfile:
try:
diff --git a/pw_build/py/pw_build/python_wheels.py b/pw_build/py/pw_build/python_wheels.py
deleted file mode 100644
index 7f55b3eb3..000000000
--- a/pw_build/py/pw_build/python_wheels.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Copyright 2020 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Wrapper for the CLI commands for Python .whl building."""
-
-import argparse
-import logging
-import os
-import subprocess
-import sys
-
-_LOG = logging.getLogger(__name__)
-
-
-def _parse_args():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- 'setup_files',
- nargs='+',
- help='Path to a setup.py file to invoke to build wheels.',
- )
- parser.add_argument(
- '--out_dir', help='Path where the build artifacts should be put.'
- )
-
- return parser.parse_args()
-
-
-def build_wheels(setup_files, out_dir):
- """Build Python wheels by calling 'python setup.py bdist_wheel'."""
- dist_dir = os.path.abspath(out_dir)
-
- for filename in setup_files:
- if not (filename.endswith('setup.py') and os.path.isfile(filename)):
- raise RuntimeError(f'Unable to find setup.py file at {filename}.')
-
- working_dir = os.path.dirname(filename)
-
- cmd = [
- sys.executable,
- 'setup.py',
- 'bdist_wheel',
- '--dist-dir',
- dist_dir,
- ]
- _LOG.debug('Running command:\n %s', ' '.join(cmd))
- subprocess.check_call(cmd, cwd=working_dir)
-
-
-def main():
- build_wheels(**vars(_parse_args()))
-
-
-if __name__ == '__main__':
- logging.basicConfig()
- main()
- sys.exit(0)
diff --git a/pw_build/py/pw_build/wrap_ninja.py b/pw_build/py/pw_build/wrap_ninja.py
index e7a0f4911..56d37f740 100644
--- a/pw_build/py/pw_build/wrap_ninja.py
+++ b/pw_build/py/pw_build/wrap_ninja.py
@@ -249,7 +249,7 @@ class Ninja:
self.lock = threading.Lock()
# Launch ninja and configure pseudo-tty.
- # pylint: disable-next=no-member,undefined-variable
+ # pylint: disable-next=no-member,undefined-variable,used-before-assignment
ptty_parent, ptty_child = pty.openpty() # type: ignore
ptty_file = os.fdopen(ptty_parent, 'r')
env = dict(os.environ)
diff --git a/pw_build/py/setup.cfg b/pw_build/py/setup.cfg
index 37b8b733b..16fecaba2 100644
--- a/pw_build/py/setup.cfg
+++ b/pw_build/py/setup.cfg
@@ -30,6 +30,8 @@ install_requires =
types-setuptools
mypy>=0.971
pylint>=2.9.3
+ pip-tools>=6.12.3
+ parameterized
[options.entry_points]
console_scripts =
diff --git a/pw_build/python.gni b/pw_build/python.gni
index cf7448799..4d5f562e6 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -33,6 +33,9 @@ declare_args() {
# coverage data file for each test will be saved. To generate reports from
# this information run: pw presubmit --step gn_python_test_coverage
pw_build_PYTHON_TEST_COVERAGE = false
+
+ # Output format for pylint. Options include "text" and "colorized".
+ pw_build_PYLINT_OUTPUT_FORMAT = "colorized"
}
# Python packages provide the following targets as $target_name.$subtarget.
@@ -45,7 +48,6 @@ pw_python_package_subtargets = [
"wheel",
# Internal targets that directly depend on one another.
- "_run_pip_install",
"_build_wheel",
]
@@ -74,11 +76,6 @@ template("_pw_python_static_analysis_mypy") {
"--pretty",
"--show-error-codes",
- # TODO(b/265836842): Namespace packages are enabled by default starting in
- # mypy 0.991. This caused problems in some configurations, so return
- # to the prior behavior for now.
- "--no-namespace-packages",
-
# Use a mypy cache dir for this target only to avoid cache conflicts in
# parallel mypy invocations.
"--cache-dir",
@@ -125,7 +122,7 @@ template("_pw_python_static_analysis_pylint") {
args = [
rebase_path(".", root_build_dir) + "/{{source_target_relative}}",
"--jobs=1",
- "--output-format=colorized",
+ "--output-format=$pw_build_PYLINT_OUTPUT_FORMAT",
]
if (defined(invoker.pylintrc)) {
@@ -522,38 +519,39 @@ template("pw_python_package") {
}
if (_is_package) {
- # Stubs for non-package targets.
- group("$target_name._run_pip_install") {
- }
-
# Builds a Python wheel for this package. Records the output directory
# in the pw_python_package_wheels metadata key.
+
pw_python_action("$target_name._build_wheel") {
+ _wheel_out_dir = "$target_out_dir/$target_name"
+ _wheel_requirement = "$_wheel_out_dir/requirements.txt"
metadata = {
- pw_python_package_wheels = [ "$target_out_dir/$target_name" ]
+ pw_python_package_wheels = [ _wheel_out_dir ]
}
- module = "build"
+ script = "$dir_pw_build/py/pw_build/generate_python_wheel.py"
+
+ args = [
+ "--package-dir",
+ rebase_path(_setup_dir, root_build_dir),
+ "--out-dir",
+ rebase_path(_wheel_out_dir, root_build_dir),
+ ]
- args =
- [
- rebase_path(_setup_dir, root_build_dir),
- "--wheel",
- "--no-isolation",
- "--outdir",
- ] + rebase_path(metadata.pw_python_package_wheels, root_build_dir)
+ # Add hashes to the _wheel_requirement output.
+ if (pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES) {
+ args += [ "--generate-hashes" ]
+ }
deps = [ ":${invoker.target_name}" ]
foreach(dep, _python_deps) {
deps += [ string_replace(dep, "(", ".wheel(") ]
}
- stamp = true
+ outputs = [ _wheel_requirement ]
}
} else {
- # Stubs for non-package targets.
- group("$target_name._run_pip_install") {
- }
+ # Stub for non-package targets.
group("$target_name._build_wheel") {
}
}
@@ -567,10 +565,6 @@ template("pw_python_package") {
group("$target_name.install") {
public_deps = [ ":${invoker.target_name}" ]
- if (_is_package) {
- public_deps += [ ":${invoker.target_name}._run_pip_install" ]
- }
-
foreach(dep, _python_deps) {
public_deps += [ string_replace(dep, "(", ".install(") ]
}
@@ -831,6 +825,7 @@ template("pw_python_script") {
if (defined(invoker.action)) {
pw_python_action("$target_name.action") {
forward_variables_from(invoker.action, "*", [ "python_deps" ])
+ forward_variables_from(invoker, [ "testonly" ])
python_deps = [ ":${invoker.target_name}" ]
if (!defined(script) && !defined(module) && defined(invoker.sources)) {
@@ -874,7 +869,7 @@ template("pw_python_requirements") {
# Use the same subtargets as pw_python_package so these targets can be listed
# as python_deps of pw_python_packages.
group("$target_name.install") {
- # TODO(b/232800695): Remove reliance on this subtarget existing.
+ # TODO: b/232800695 - Remove reliance on this subtarget existing.
}
# Create stubs for the unused subtargets so that pw_python_requirements can be
diff --git a/pw_build/python.rst b/pw_build/python.rst
index 3a50a75b1..473d2ed5c 100644
--- a/pw_build/python.rst
+++ b/pw_build/python.rst
@@ -239,6 +239,7 @@ Example
"//tools:another_pw_python_package",
"//:my_product_packages",
]
+ pip_generate_hashes = true
}
Arguments
@@ -261,11 +262,15 @@ Arguments
``pw_build_PIP_REQUIREMENTS`` GN args see:
:ref:`docs-python-build-python-gn-requirements-files`
+- ``pip_generate_hashes``: (Default: false) Use ``--generate-hashes`` When
+ running `pip-compile <A list of requirements files to install into this
+ virtualenv>`_ to compute the final ``requirements.txt``
+
- ``source_packages``: A list of in-tree
:ref:`module-pw_build-pw_python_package` or targets that will be checked for
external third_party pip dependencies to install into this
virtualenv. Note this list of targets isn't actually installed into the
- virtualenv. Only packages defined inside the [options] install_requires
+ virtualenv. Only packages defined inside the ``[options] install_requires``
section of each pw_python_package's setup.cfg will be pip installed.
.. seealso::
@@ -274,6 +279,7 @@ Arguments
setup.cfg files
<https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`_
+- ``output_logs``: (Default: true) If this is true then the virtual environment will output to logs.
.. _module-pw_build-pw_python_pip_install:
@@ -430,10 +436,25 @@ Arguments
and ``version`` strings. The ``common_config_file`` should contain the
required fields in the ``metadata`` and ``options`` sections as shown in
`Configuring setup() using setup.cfg files <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`_.
- ``append_git_sha_to_version = true`` and ``append_date_to_version = true``
- will optionally append the current git SHA or date to the package version
- string after a ``+`` sign. You can also opt to include a generated
- ``pyproject.toml`` file by setting ``include_default_pyproject_file = true``.
+
+ This scope can optionally include:
+
+ - ``append_git_sha_to_version = true``: Append the current git SHA to the
+ package version string after a ``+`` sign.
+
+ - ``append_date_to_version = true``: Append the current date to the package
+ version string after a ``+`` sign.
+
+ - ``include_default_pyproject_file = true``: Include a standard
+ ``pyproject.toml`` file in the output.
+
+ - ``include_extra_files_in_package_data = true``: Add any ``extra_files``
+ entries to the generated ``setup.cfg`` file under the
+ ``[options.package_data]`` section.
+
+ - ``auto_create_package_data_init_py_files = true``: (Default: true) Create
+ ``__init__.py`` files as needed in all subdirs of ``extra_files`` when
+ including in ``[options.package_data]``.
.. code-block::
:caption: :octicon:`file;1em` Example using a common setup.cfg and
diff --git a/pw_build/python_action.gni b/pw_build/python_action.gni
index 0c7662774..7504bf2fb 100644
--- a/pw_build/python_action.gni
+++ b/pw_build/python_action.gni
@@ -50,6 +50,11 @@ import("$dir_pw_build/python_gn_args.gni")
#
# python_deps Dependencies on pw_python_package or related Python targets.
#
+# python_metadata_deps Python-related dependencies that are only used as deps
+# for generating Python package metadata list, not the
+# overall Python script action. This should rarely be
+# used by non-Pigweed code.
+#
# working_directory Switch to the provided working directory before running
# the Python script or action.
#
diff --git a/pw_build/python_action_test.gni b/pw_build/python_action_test.gni
new file mode 100644
index 000000000..2de0fee4f
--- /dev/null
+++ b/pw_build/python_action_test.gni
@@ -0,0 +1,187 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_build/test_info.gni")
+
+# Creates a Python action to use as a test and associated metadata.
+#
+# This template derives several additional targets:
+# - <target_name>.metadata produces the test metadata when included in a
+# `pw_test_group`. This metadata includes the Ninja target that runs the
+# test.
+# - <target_name>.script creates a `pw_python_action` to run the test and
+# wraps it as a standalone `pw_python_package`.
+# - <target_name>.group creates a `pw_python_group` in order to apply tools,
+# e.g. linters, to the standalone package.
+# - <target_name>.lib is an empty group for compatibility with
+# `pw_test_group`.
+# - <target_name>.run invokes the test.
+#
+# Targets defined using this template will produce test metadata with a
+# `test_type` of "action_test" and a `ninja_target` value that will invoke the
+# test when passed to Ninja, i.e. `ninja -C out <ninja_target>`.
+#
+# Args:
+# - The following args have the same meaning as for `pw_test`:
+# enable_if
+# envvars (may also use `environment`)
+# tags
+# extra_metadata
+# source_gen_deps
+#
+# - The following args have the same meaning as for `pw_python_action`:
+# script (may be omitted if `sources` has length=1)
+# args
+# deps
+# environment (may also use `envvars`)
+# working_directory
+# command_launcher
+# venv
+#
+# - The following args have the same meaning as for `pw_python_package`:
+# sources
+# python_deps
+# other_deps
+# inputs
+# static_analysis
+# pylintrc
+# mypy_ini
+#
+# - action: Optional string or scope. If a string, this should be a label to
+# a `pw_python_action` target that performs the test. If a scope, this
+# has the same meaning as for `pw_python_script`.
+template("pw_python_action_test") {
+ _test_target_name = target_name
+ _deps = []
+ _run_deps = []
+ _metadata = {
+ }
+
+ _test_is_enabled = !defined(invoker.enable_if) || invoker.enable_if
+ if (_test_is_enabled) {
+ # Metadata for this test when used as part of a pw_test_group target.
+ _test_metadata = "${target_name}.metadata"
+ _ninja_target = "$target_out_dir/$target_name.run.stamp"
+ _extra_metadata = {
+ forward_variables_from(invoker, [ "extra_metadata" ])
+ ninja_target = rebase_path(_ninja_target, root_build_dir)
+ }
+ pw_test_info(_test_metadata) {
+ test_type = "action_test"
+ test_name = _test_target_name
+ forward_variables_from(invoker, [ "tags" ])
+ extra_metadata = _extra_metadata
+ }
+ _deps += [ ":$_test_metadata" ]
+ _metadata = {
+ test_barrier = [ ":$_test_metadata" ]
+ }
+
+ if (defined(invoker.action) && invoker.action == "${invoker.action}") {
+ # `action` is a label to an existing Python action.
+ _run_deps = [ invoker.action ]
+ if (defined(invoker.deps)) {
+ _deps += invoker.deps
+ }
+ } else {
+ # Wrap the action in a Python script target for additional flexibility.
+ _test_script_name = _test_target_name + ".script"
+
+ _sources = []
+ if (defined(invoker.script)) {
+ _sources += [ invoker.script ]
+ }
+ if (defined(invoker.sources)) {
+ _sources += invoker.sources
+ }
+
+ pw_python_script(_test_script_name) {
+ other_deps = []
+ forward_variables_from(invoker,
+ [
+ "action",
+ "python_deps",
+ "other_deps",
+ "inputs",
+ "static_analysis",
+ "testonly",
+ "pylintrc",
+ "mypy_ini",
+ ])
+ sources = _sources
+ if (defined(invoker.source_gen_deps)) {
+ other_deps += invoker.source_gen_deps
+ }
+ if (!defined(pylintrc)) {
+ pylintrc = "$dir_pigweed/.pylintrc"
+ }
+ if (!defined(mypy_ini)) {
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ }
+ if (!defined(action)) {
+ action = {
+ environment = []
+ forward_variables_from(invoker,
+ [
+ "script",
+ "args",
+ "deps",
+ "environment",
+ "working_directory",
+ "command_launcher",
+ "venv",
+ ])
+ if (defined(invoker.envvars)) {
+ environment += invoker.envvars
+ }
+ stamp = true
+ }
+ }
+ }
+ _deps += [ ":$_test_script_name" ]
+ _run_deps = [ ":$_test_script_name.action" ]
+
+ # Create a Python group in order to ensure the package is linted.
+ _test_python_group = _test_target_name + ".group"
+ pw_python_group(_test_python_group) {
+ python_deps = [ ":$_test_script_name" ]
+ }
+ _deps += [ ":$_test_python_group" ]
+ }
+ } else {
+ if (defined(invoker.source_gen_deps)) {
+ _deps += invoker.source_gen_deps
+ }
+ }
+
+ # For compatibility with `pw_test_group`.
+ group(_test_target_name + ".lib") {
+ forward_variables_from(invoker, [ "testonly" ])
+ }
+
+ # For compatibility with `pw_test_group`.
+ group(_test_target_name + ".run") {
+ forward_variables_from(invoker, [ "testonly" ])
+ deps = _run_deps
+ }
+
+ group(_test_target_name) {
+ forward_variables_from(invoker, [ "testonly" ])
+ deps = _deps
+ metadata = _metadata
+ }
+}
diff --git a/pw_build/python_dist.gni b/pw_build/python_dist.gni
index 12f0afdbc..623d9da56 100644
--- a/pw_build/python_dist.gni
+++ b/pw_build/python_dist.gni
@@ -55,6 +55,15 @@ template("pw_python_wheels") {
_packages += [ "${_pkg_name}.wheel(${_pkg_toolchain})" ]
}
+ if (defined(invoker.venv)) {
+ _venv_target_label = pw_build_PYTHON_BUILD_VENV
+ _venv_target_label = invoker.venv
+ _venv_target_label =
+ get_label_info(_venv_target_label, "label_no_toolchain")
+ _packages +=
+ [ "${_venv_target_label}.vendor_wheels($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+
# Build a list of relative paths containing all the wheels we depend on.
generated_file("${target_name}._wheel_paths") {
data_keys = [ "pw_python_package_wheels" ]
@@ -74,7 +83,7 @@ template("pw_python_wheels") {
rebase_path(root_build_dir, root_build_dir),
"--suffix",
rebase_path(_wheel_paths_path, root_build_dir),
- "--out_dir",
+ "--out-dir",
rebase_path(_directory, root_build_dir),
]
@@ -120,7 +129,11 @@ template("pw_python_zip_with_setup") {
pw_python_wheels("$target_name.wheels") {
packages = invoker.packages
- forward_variables_from(invoker, [ "deps" ])
+ forward_variables_from(invoker,
+ [
+ "deps",
+ "venv",
+ ])
}
pw_zip(target_name) {
@@ -134,8 +147,22 @@ template("pw_python_zip_with_setup") {
output = _zip_path
- # TODO(b/235245034): Remove the plumbing-through of invoker's public_deps.
+ # TODO: b/235245034 - Remove the plumbing-through of invoker's public_deps.
public_deps = _public_deps + [ ":${_outer_name}.wheels" ]
+
+ if (defined(invoker.venv)) {
+ _venv_target_label = get_label_info(invoker.venv, "label_no_toolchain")
+ _requirements_target_name =
+ get_label_info("${_venv_target_label}($pw_build_PYTHON_TOOLCHAIN)",
+ "name")
+ _requirements_gen_dir =
+ get_label_info("${_venv_target_label}($pw_build_PYTHON_TOOLCHAIN)",
+ "target_gen_dir")
+
+ inputs += [ "$_requirements_gen_dir/$_requirements_target_name/compiled_requirements.txt > /${target_name}/requirements.txt" ]
+
+ public_deps += [ "${_venv_target_label}._compile_requirements($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
}
}
@@ -160,9 +187,23 @@ template("pw_python_zip_with_setup") {
# generate_setup_cfg: A scope containing either common_config_file or 'name'
# and 'version' If included this creates a merged setup.cfg for all python
# Packages using either a common_config_file as a base or name and version
-# strings. This scope can optionally include: 'append_git_sha_to_version' or
-# 'append_date_to_version' whic append the current git SHA or date to the
-# package version string after a + sign.
+# strings. This scope can optionally include:
+#
+# include_default_pyproject_file: Include a standard pyproject.toml file
+# that uses setuptools.
+#
+# append_git_sha_to_version: Append the current git SHA to the package
+# version string after a + sign.
+#
+# append_date_to_version: Append the current date to the package version
+# string after a + sign.
+#
+# include_extra_files_in_package_data: Add any extra_files to the setup.cfg
+# file under the [options.package_data] section.
+#
+# auto_create_package_data_init_py_files: Default: true
+# Create __init__.py files as needed in all subdirs of extra_files when
+# including in [options.package_data].
#
template("pw_python_distribution") {
_metadata_path_list_suffix = "_pw_python_distribution_metadata_path_list.txt"
@@ -262,6 +303,14 @@ template("pw_python_distribution") {
metadata = {
pw_python_package_metadata_json = [ _package_metadata_json_file ]
}
+
+ # Forward the package_metadata subtarget for all packages bundled in this
+ # distribution.
+ public_deps = []
+ foreach(dep, invoker.packages) {
+ public_deps += [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
}
_package_metadata_targets = []
@@ -335,6 +384,17 @@ template("pw_python_distribution") {
generate_setup_cfg.include_default_pyproject_file == true) {
args += [ "--create-default-pyproject-toml" ]
}
+ if (defined(generate_setup_cfg.include_extra_files_in_package_data)) {
+ args += [ "--setupcfg-extra-files-in-package-data" ]
+ }
+ _auto_create_package_data_init_py_files = true
+ if (defined(generate_setup_cfg.auto_create_package_data_init_py_files)) {
+ _auto_create_package_data_init_py_files =
+ generate_setup_cfg.auto_create_package_data_init_py_files
+ }
+ if (_auto_create_package_data_init_py_files) {
+ args += [ "--auto-create-package-data-init-py-files" ]
+ }
}
if (_extra_file_args == []) {
@@ -355,16 +415,25 @@ template("pw_python_distribution") {
# Template to build a bundled Python package wheel.
pw_python_action("$target_name._build_wheel") {
+ _wheel_out_dir = "$target_out_dir/$target_name"
+ _wheel_requirement = "$_wheel_out_dir/requirements.txt"
metadata = {
- pw_python_package_wheels = [ "$target_out_dir/$target_name" ]
+ pw_python_package_wheels = [ _wheel_out_dir ]
}
- module = "build"
+
+ script = "$dir_pw_build/py/pw_build/generate_python_wheel.py"
+
args = [
- rebase_path(_output_dir, root_build_dir),
- "--wheel",
- "--no-isolation",
- "--outdir",
- ] + rebase_path(metadata.pw_python_package_wheels, root_build_dir)
+ "--package-dir",
+ rebase_path(_output_dir, root_build_dir),
+ "--out-dir",
+ rebase_path(_wheel_out_dir, root_build_dir),
+ ]
+
+ # Add hashes to the _wheel_requirement output.
+ if (pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES) {
+ args += [ "--generate-hashes" ]
+ }
public_deps = []
if (defined(invoker.public_deps)) {
@@ -372,7 +441,7 @@ template("pw_python_distribution") {
}
public_deps += [ ":${invoker.target_name}" ]
- stamp = true
+ outputs = [ _wheel_requirement ]
}
group("$target_name.wheel") {
public_deps = [ ":${invoker.target_name}._build_wheel" ]
@@ -401,7 +470,7 @@ template("pw_python_distribution") {
}
}
-# TODO(b/232800695): Remove this template when all projects no longer use it.
+# TODO: b/232800695 - Remove this template when all projects no longer use it.
template("pw_create_python_source_tree") {
pw_python_distribution("$target_name") {
forward_variables_from(invoker, "*")
@@ -471,6 +540,8 @@ template("pw_python_pip_install") {
_pkg_gn_labels += [ get_label_info(pkg, "label_no_toolchain") ]
}
+ _pip_install_log_file = "$target_gen_dir/$target_name/pip_install_log.txt"
+
args = [
"--gn-packages",
string_join(",", _pkg_gn_labels),
@@ -481,10 +552,42 @@ template("pw_python_pip_install") {
}
args += [
+ "--log",
+ rebase_path(_pip_install_log_file, root_build_dir),
+ ]
+ args += pw_build_PYTHON_PIP_DEFAULT_OPTIONS
+ args += [
"install",
"--no-build-isolation",
]
+ if (!_editable_install) {
+ if (pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES) {
+ args += [ "--require-hashes" ]
+
+ # The --require-hashes option can only install wheels via
+ # requirement.txt files that contain hashes. Depend on this package's
+ # _build_wheel target.
+ foreach(pkg, _pkg_gn_labels) {
+ public_deps += [ "${pkg}._build_wheel" ]
+ }
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
+ args += [ "--no-index" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
+ args += [ "--no-cache-dir" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
+ foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
+ args += [
+ "--find-links",
+ rebase_path(link_dir, root_build_dir),
+ ]
+ }
+ }
+ }
+
_force_reinstall = false
if (defined(invoker.force_reinstall)) {
_force_reinstall = true
@@ -494,6 +597,7 @@ template("pw_python_pip_install") {
}
inputs = pw_build_PIP_CONSTRAINTS
+
foreach(_constraints_file, pw_build_PIP_CONSTRAINTS) {
args += [
"--constraint",
@@ -536,7 +640,7 @@ template("pw_python_pip_install") {
}
}
-# TODO(b/232800695): Remove this template when all projects no longer use it.
+# TODO: b/232800695 - Remove this template when all projects no longer use it.
template("pw_internal_pip_install") {
pw_python_pip_install("$target_name") {
forward_variables_from(invoker, "*")
diff --git a/pw_build/python_dist/setup.bat b/pw_build/python_dist/setup.bat
index 1ded5a7e0..b7d1ecbeb 100644
--- a/pw_build/python_dist/setup.bat
+++ b/pw_build/python_dist/setup.bat
@@ -13,15 +13,29 @@
:: the License.
@echo off
+set "ROOT_DIR=%~dp0"
+set "WHEEL_DIR=%ROOT_DIR%\python_wheels"
+
:: Generate python virtual environment using existing python.
-python3 -m venv %~dp0python-venv
+python -m venv "%ROOT_DIR%\python-venv"
+:: Use the venv python for pip installs
+set "python=%ROOT_DIR%\python-venv\Scripts\python.exe"
-:: Install pip inside the virtual environment.
-%~dp0python-venv\Scripts\python.exe -m pip install --upgrade pip
+set "CONSTRAINT_PATH=%ROOT_DIR%\constraints.txt"
+set "CONSTRAINT_ARG="
+if exist "%CONSTRAINT_PATH%" (
+ set "CONSTRAINT_ARG=--constraint=%CONSTRAINT_PATH%"
+)
-:: Install all wheel files, possibly with constraints.
-if exist %~dp0constraints.txt (
- for %%f in (%~dp0python_wheels\*) do %~dp0python-venv\Scripts\python.exe -m pip install -c %~dp0constraints.txt --find-links=%~dp0python_wheels %%f
-) else (
- for %%f in (%~dp0python_wheels\*) do %~dp0python-venv\Scripts\python.exe -m pip install --find-links=%~dp0python_wheels %%f
+set "EXTRA_REQUIREMENT_PATH=%WHEEL_DIR%\requirements.txt"
+set "EXTRA_REQUIREMENT_ARG="
+if exist "%EXTRA_REQUIREMENT_PATH%" (
+ set "EXTRA_REQUIREMENT_ARG=--requirement=%EXTRA_REQUIREMENT_PATH%"
)
+
+:: Run pip install in the venv.
+:: Note: pip install --require-hashes will be triggered if any hashes are present
+:: in the requirement.txt file.
+call "%python%" -m pip install ^
+ "--find-links=%ROOT_DIR%python_wheels" ^
+ "--requirement=requirements.txt" %EXTRA_REQUIREMENT_ARG% %CONSTRAINT_ARG%
diff --git a/pw_build/python_dist/setup.sh b/pw_build/python_dist/setup.sh
index 81ddb5d1a..56b136c6b 100755
--- a/pw_build/python_dist/setup.sh
+++ b/pw_build/python_dist/setup.sh
@@ -21,6 +21,7 @@ DIR="$(python3 -c "import os; print(os.path.dirname(os.path.abspath(os.path.real
VENV="${DIR}/python-venv"
PY_TO_TEST="python3"
CONSTRAINTS_PATH="${DIR}/constraints.txt"
+EXTRA_REQUIREMENT_PATH="${DIR}/python_wheels/requirements.txt"
if [ ! -z "${1-}" ]; then
VENV="${1-}"
@@ -29,15 +30,20 @@ fi
CONSTRAINTS_ARG=""
if [ -f "${CONSTRAINTS_PATH}" ]; then
- CONSTRAINTS_ARG="-c""${CONSTRAINTS_PATH}"
+ CONSTRAINTS_ARG="-c ""${CONSTRAINTS_PATH}"
+fi
+
+EXTRA_REQUIREMENT_ARG=""
+if [ -f "${EXTRA_REQUIREMENT_PATH}" ]; then
+ EXTRA_REQUIREMENT_ARG="-r ""${EXTRA_REQUIREMENT_PATH}"
fi
PY_MAJOR_VERSION=$(${PY_TO_TEST} -c "import sys; print(sys.version_info[0])")
PY_MINOR_VERSION=$(${PY_TO_TEST} -c "import sys; print(sys.version_info[1])")
-if [ ${PY_MAJOR_VERSION} -ne 3 ] || [ ${PY_MINOR_VERSION} -lt 7 ]
+if [ ${PY_MAJOR_VERSION} -ne 3 ] || [ ${PY_MINOR_VERSION} -lt 8 ]
then
- echo "ERROR: This Python distributable requires Python 3.7 or newer."
+ echo "ERROR: This Python distributable requires Python 3.8 or newer."
exit 1
fi
@@ -46,23 +52,10 @@ then
${PY_TO_TEST} -m venv "${VENV}"
fi
-"${VENV}/bin/python" -m pip install --upgrade pip
-
-# Uninstall wheels first, in case installing over an existing venv. This is a
-# faster and less destructive approach than --force-reinstall to ensure wheels
-# whose version numbers haven't incremented still get reinstalled.
-IFS_BACKUP="$IFS"
-IFS=$'\n'
-for wheel in $(ls "${DIR}/python_wheels/"*.whl)
-do
- "${VENV}/bin/python" -m pip uninstall --yes "$wheel"
-done
-
-for wheel in $(ls "${DIR}/python_wheels/"*.whl)
-do
- "${VENV}/bin/python" -m pip install \
- --upgrade --find-links="${DIR}/python_wheels" ${CONSTRAINTS_ARG} "$wheel"
-done
-IFS="$IFS_BACKUP"
+# Note: pip install --require-hashes will be triggered if any hashes are present
+# in the requirement.txt file.
+"${VENV}/bin/python" -m pip install \
+ --find-links="${DIR}/python_wheels" \
+ -r ${DIR}/requirements.txt ${EXTRA_REQUIREMENT_ARG} ${CONSTRAINTS_ARG}
exit 0
diff --git a/pw_build/python_gn_args.gni b/pw_build/python_gn_args.gni
index 11f3aadda..cc2bfc5fb 100644
--- a/pw_build/python_gn_args.gni
+++ b/pw_build/python_gn_args.gni
@@ -21,4 +21,34 @@ declare_args() {
# Default gn build virtualenv target.
pw_build_PYTHON_BUILD_VENV = "$dir_pw_env_setup:pigweed_build_venv"
+
+ # DOCSTAG: [default-pip-gn-args]
+ # Set pw_python_venv.vendor_wheel targets to download Python packages for all
+ # platform combinations. This takes a significant amount of time.
+ pw_build_PYTHON_PIP_DOWNLOAD_ALL_PLATFORMS = false
+
+ # Adds '--require-hashes'. This option enforces hash checking on Python
+ # package files.
+ pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES = false
+
+ # Adds --no-index forcing pip to not reach out to the internet (pypi.org) to
+ # download packages. Using this option requires setting
+ # pw_build_PYTHON_PIP_INSTALL_FIND_LINKS as well.
+ pw_build_PYTHON_PIP_INSTALL_OFFLINE = false
+
+ # Adds '--no-cache-dir' forcing pip to ignore any previously cached Python
+ # packages. On most systems this is located in ~/.cache/pip/
+ pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE = false
+
+ # List of paths to folders containing Python wheels (*.whl) or source tar
+ # files (*.tar.gz). Pip will check each of these directories when looking for
+ # potential install candidates. Each path will be passed to all 'pip install'
+ # commands as '--find-links PATH'.
+ pw_build_PYTHON_PIP_INSTALL_FIND_LINKS = []
+
+ # General options passed to pip commands
+ # https://pip.pypa.io/en/stable/cli/pip/#general-options
+ pw_build_PYTHON_PIP_DEFAULT_OPTIONS = [ "--disable-pip-version-check" ]
+
+ # DOCSTAG: [default-pip-gn-args]
}
diff --git a/pw_build/python_venv.gni b/pw_build/python_venv.gni
index c36b7f90f..6a5ae770c 100644
--- a/pw_build/python_venv.gni
+++ b/pw_build/python_venv.gni
@@ -46,6 +46,9 @@ import("$dir_pw_build/python_action.gni")
# requirements: A list of requirements files to install into this virtualenv
# on creation. By default this is set to pw_build_PIP_REQUIREMENTS
#
+# pip_generate_hashes: (Default: false) Use --generate-hashes When
+# running pip-compile to compute the final requirements.txt
+#
# source_packages: A list of in-tree pw_python_package targets that will be
# checked for external third_party pip dependencies to install into this
# virtualenv. Note this list of targets isn't actually installed into the
@@ -54,6 +57,8 @@ import("$dir_pw_build/python_action.gni")
# this page for a setup.cfg example:
# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
#
+# output_logs: (Default: true) Commands will output logs.
+#
template("pw_python_venv") {
assert(defined(invoker.path), "pw_python_venv requires a 'path'")
@@ -62,6 +67,9 @@ template("pw_python_venv") {
_generated_requirements_file =
"$target_gen_dir/$target_name/generated_requirements.txt"
+ _compiled_requirements_file =
+ "$target_gen_dir/$target_name/compiled_requirements.txt"
+
_source_packages = []
if (defined(invoker.source_packages)) {
_source_packages += invoker.source_packages
@@ -71,6 +79,14 @@ template("pw_python_venv") {
"_generated_requirements_file",
])
}
+ _output_logs = true
+ if (defined(invoker.output_logs)) {
+ _output_logs = invoker.output_logs
+ }
+ if (!defined(invoker.output_logs) ||
+ current_toolchain != pw_build_PYTHON_TOOLCHAIN) {
+ not_needed([ "_output_logs" ])
+ }
_source_package_labels = []
foreach(pkg, _source_packages) {
@@ -101,6 +117,8 @@ template("pw_python_venv") {
path = rebase_path(_path, root_build_dir)
generated_requirements =
rebase_path(_generated_requirements_file, root_build_dir)
+ compiled_requirements =
+ rebase_path(_compiled_requirements_file, root_build_dir)
requirements = rebase_path(_requirements, root_build_dir)
constraints = rebase_path(_constraints, root_build_dir)
interpreter = rebase_path(_python_interpreter, root_build_dir)
@@ -115,10 +133,17 @@ template("pw_python_venv") {
# outputs and must stamp instead.
stamp = true
+ # The virtualenv should depend on the version of Python currently in use.
+ stampfile = "$target_gen_dir/$target_name.pw_pystamp"
+ depfile = "$target_gen_dir/$target_name.d"
script = "$dir_pw_build/py/pw_build/create_gn_venv.py"
args = [
+ "--depfile",
+ rebase_path(depfile, root_build_dir),
"--destination-dir",
rebase_path(_path, root_build_dir),
+ "--stampfile",
+ rebase_path(stampfile, root_build_dir),
]
}
@@ -145,9 +170,19 @@ template("pw_python_venv") {
python_metadata_deps += _pkg_gn_labels
args = [
- "--requirement",
+ "--gn-root-build-dir",
+ rebase_path(root_build_dir, root_build_dir),
+ "--output-requirement-file",
rebase_path(_generated_requirements_file, root_build_dir),
]
+
+ if (_constraints != []) {
+ args += [ "--constraint-files" ]
+ }
+ foreach(_constraints_file, _constraints) {
+ args += [ rebase_path(_constraints_file, root_build_dir) ]
+ }
+
args += [
"--gn-packages",
string_join(",", _pkg_gn_labels),
@@ -161,27 +196,131 @@ template("pw_python_venv") {
}
}
+ _pip_generate_hashes = false
+ if (defined(invoker.pip_generate_hashes)) {
+ _pip_generate_hashes = invoker.pip_generate_hashes
+ } else {
+ not_needed([ "_pip_generate_hashes" ])
+ }
+
if (defined(invoker.source_packages) || defined(invoker.requirements)) {
if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
- # This target will run 'pip install wheel' in the venv. This is purposely
- # run before further pip installs so packages that run bdist_wheel as part
- # of their install process will succeed. Packages that run native compiles
- # typically do this.
+ # Compile requirements with hashes
+ pw_python_action("${target_name}._compile_requirements") {
+ module = "piptools"
+
+ # Set the venv to run this pip install in.
+ _pw_internal_run_in_venv = true
+ _skip_installing_external_python_deps = true
+ venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
+
+ _pip_args = []
+ if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
+ _pip_args += [ "--no-index" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
+ _pip_args += [ "--no-cache-dir" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
+ foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
+ _pip_args +=
+ [ "--find-links=" + rebase_path(link_dir, root_build_dir) ]
+ }
+ }
+
+ args = [ "compile" ]
+
+ if (_pip_generate_hashes) {
+ args += [
+ "--generate-hashes",
+ "--reuse-hashes",
+ ]
+ }
+
+ args += [
+ "--resolver=backtracking",
+
+ # --allow-unsafe will force pinning pip and setuptools.
+ "--allow-unsafe",
+ "--output-file",
+ rebase_path(_compiled_requirements_file, root_build_dir),
+
+ # Input requirements file:
+ rebase_path(_generated_requirements_file, root_build_dir),
+ ]
+
+ # Pass offline related pip args through the pip-compile command.
+ if (_pip_args != []) {
+ args += [
+ "--pip-args",
+ string_join(" ", _pip_args),
+ ]
+ }
+
+ # Extra requirements files
+ foreach(_requirements_file, _requirements) {
+ args += [ rebase_path(_requirements_file, root_build_dir) ]
+ }
+
+ inputs = []
+
+ # NOTE: constraint files are included in the content of the
+ # _generated_requirements_file. This occurs in the
+ # ._generate_3p_requirements target.
+ inputs += _constraints
+ inputs += _requirements
+ inputs += [ _generated_requirements_file ]
+
+ deps = [
+ ":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
+ ":${invoker.target_name}._install_base_3p_deps($pw_build_PYTHON_TOOLCHAIN)",
+ ]
+ outputs = [ _compiled_requirements_file ]
+ }
+
+ # This target will run 'pip install' in the venv to provide base
+ # dependencies needed to run piptools commands. That is required for the
+ # _compile_requirements sub target.
pw_python_action("${target_name}._install_base_3p_deps") {
module = "pip"
+
+ # Set the venv to run this pip install in.
_pw_internal_run_in_venv = true
_skip_installing_external_python_deps = true
+ venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
+
+ _base_requirement_file = "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/python_base_requirements.txt"
+
args = [
"install",
- "wheel",
+ "--requirement",
+ rebase_path(_base_requirement_file, root_build_dir),
]
- inputs = _constraints
-
- foreach(_constraints_file, _constraints) {
+ if (_output_logs) {
+ _pip_install_log_file =
+ "$target_gen_dir/$target_name/pip_install_log.txt"
args += [
- "--constraint",
- rebase_path(_constraints_file, root_build_dir),
+ "--log",
+ rebase_path(_pip_install_log_file, root_build_dir),
]
+ outputs = [ _pip_install_log_file ]
+ }
+
+ # NOTE: Constraints should be ignored for this step.
+
+ if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
+ args += [ "--no-index" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
+ args += [ "--no-cache-dir" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
+ foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
+ args += [
+ "--find-links",
+ rebase_path(link_dir, root_build_dir),
+ ]
+ }
}
deps = [ ":${invoker.target_name}._create_virtualenv($pw_build_PYTHON_TOOLCHAIN)" ]
@@ -192,55 +331,132 @@ template("pw_python_venv") {
# Install all 3rd party Python dependencies.
pw_python_action("${target_name}._install_3p_deps") {
module = "pip"
+
+ # Set the venv to run this pip install in.
_pw_internal_run_in_venv = true
_skip_installing_external_python_deps = true
- args = [ "install" ]
+ venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
- # Note: --no-build-isolation should be avoided for installing 3rd party
- # Python packages that use C/C++ extension modules.
- # https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
- inputs = _constraints + _requirements
+ args = pw_build_PYTHON_PIP_DEFAULT_OPTIONS
+ args += [
+ "install",
+ "--upgrade",
+ ]
- # Constraints
- foreach(_constraints_file, _constraints) {
+ if (_output_logs) {
+ _pip_install_log_file =
+ "$target_gen_dir/$target_name/pip_install_log.txt"
args += [
- "--constraint",
- rebase_path(_constraints_file, root_build_dir),
+ "--log",
+ rebase_path(_pip_install_log_file, root_build_dir),
]
}
- # Extra requirements files
- foreach(_requirements_file, _requirements) {
- args += [
- "--requirement",
- rebase_path(_requirements_file, root_build_dir),
- ]
+ if (_pip_generate_hashes) {
+ args += [ "--require-hashes" ]
+ }
+
+ if (pw_build_PYTHON_PIP_INSTALL_OFFLINE) {
+ args += [ "--no-index" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_DISABLE_CACHE) {
+ args += [ "--no-cache-dir" ]
+ }
+ if (pw_build_PYTHON_PIP_INSTALL_FIND_LINKS != []) {
+ foreach(link_dir, pw_build_PYTHON_PIP_INSTALL_FIND_LINKS) {
+ args += [
+ "--find-links",
+ rebase_path(link_dir, root_build_dir),
+ ]
+ }
}
- # Generated Python requirements file.
+ # Note: --no-build-isolation should be avoided for installing 3rd party
+ # Python packages that use C/C++ extension modules.
+ # https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
+ inputs = _constraints + _requirements + [ _compiled_requirements_file ]
+
+ # Use the pip-tools compiled requiremets file. This contains the fully
+ # expanded list of deps with constraints applied.
if (defined(invoker.source_packages)) {
- inputs += [ _generated_requirements_file ]
+ inputs += [ _compiled_requirements_file ]
args += [
"--requirement",
- rebase_path(_generated_requirements_file, root_build_dir),
+ rebase_path(_compiled_requirements_file, root_build_dir),
]
}
deps = [
+ ":${invoker.target_name}._compile_requirements($pw_build_PYTHON_TOOLCHAIN)",
":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
":${invoker.target_name}._install_base_3p_deps($pw_build_PYTHON_TOOLCHAIN)",
]
stamp = true
pool = "$dir_pw_build/pool:pip($default_toolchain)"
}
+
+ # Target to create a Python package cache for this pw_python_venv.
+ pw_python_action("${target_name}.vendor_wheels") {
+ _wheel_output_dir = "$target_gen_dir/$target_name/wheels"
+ _pip_download_logfile =
+ "$target_gen_dir/$target_name/pip_download_log.txt"
+ _pip_wheel_logfile = "$target_gen_dir/$target_name/pip_wheel_log.txt"
+ metadata = {
+ pw_python_package_wheels = [ _wheel_output_dir ]
+ }
+
+ script = "$dir_pw_build/py/pw_build/generate_python_wheel_cache.py"
+
+ # Set the venv to run this pip install in.
+ _pw_internal_run_in_venv = true
+ _skip_installing_external_python_deps = true
+ venv = get_label_info(":${invoker.target_name}", "label_no_toolchain")
+
+ args = [
+ "--pip-download-log",
+ rebase_path(_pip_download_logfile, root_build_dir),
+ "--wheel-dir",
+ rebase_path(_wheel_output_dir, root_build_dir),
+ "-r",
+ rebase_path(_compiled_requirements_file, root_build_dir),
+ ]
+
+ if (pw_build_PYTHON_PIP_DOWNLOAD_ALL_PLATFORMS) {
+ args += [ "--download-all-platforms" ]
+ }
+
+ deps = [
+ ":${invoker.target_name}._compile_requirements($pw_build_PYTHON_TOOLCHAIN)",
+ ":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
+ ]
+
+ outputs = [
+ _wheel_output_dir,
+ _pip_wheel_logfile,
+ _pip_download_logfile,
+ ]
+ pool = "$dir_pw_build/pool:pip($default_toolchain)"
+ }
+
+ # End pw_build_PYTHON_TOOLCHAIN check
} else {
+ group("${target_name}._compile_requirements") {
+ public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
group("${target_name}._install_3p_deps") {
public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
}
+ group("${target_name}.vendor_wheels") {
+ public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
}
} else {
+ group("${target_name}._compile_requirements") {
+ }
group("${target_name}._install_3p_deps") {
}
+ group("${target_name}.vendor_wheels") {
+ }
}
# Have this target directly depend on _install_3p_deps
diff --git a/pw_build/rust_executable.gni b/pw_build/rust_executable.gni
index cc564b65d..704119527 100644
--- a/pw_build/rust_executable.gni
+++ b/pw_build/rust_executable.gni
@@ -48,9 +48,13 @@ template("pw_rust_executable") {
}
configs += [ "$dir_pw_build:rust_edition_${_edition}" ]
+ output_dir = "${target_out_dir}/bin"
+ if (defined(invoker.output_dir)) {
+ output_dir = invoker.output_dir
+ }
+
underlying_target_type = pw_build_EXECUTABLE_TARGET_TYPE
target_type_file = pw_build_EXECUTABLE_TARGET_TYPE_FILE
- output_dir = "${target_out_dir}/bin"
add_global_link_deps = true
}
}
diff --git a/pw_build/rust_library.gni b/pw_build/rust_library.gni
index 370f47d53..90b0f0c87 100644
--- a/pw_build/rust_library.gni
+++ b/pw_build/rust_library.gni
@@ -56,7 +56,10 @@ template("pw_rust_library") {
}
}
- underlying_target_type = "rust_library"
+ if (!defined(underlying_target_type)) {
+ underlying_target_type = "rust_library"
+ }
+
crate_name = string_replace(crate_name, "-", "_")
output_name = crate_name
output_dir = "${target_out_dir}/lib"
diff --git a/pw_build/rust_proc_macro.gni b/pw_build/rust_proc_macro.gni
new file mode 100644
index 000000000..666456964
--- /dev/null
+++ b/pw_build/rust_proc_macro.gni
@@ -0,0 +1,32 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/rust_library.gni")
+
+# Note: In general, prefer to import target_types.gni rather than this file.
+#
+# A wrapper for GN's built-in rust_proc_macro target, this template wraps a
+# configurable target type specified by the current toolchain to be used for all
+# pw_rust_proc_macro targets.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_rust_proc_macro.
+template("pw_rust_proc_macro") {
+ pw_rust_library(target_name) {
+ forward_variables_from(invoker, "*")
+ underlying_target_type = "rust_proc_macro"
+ }
+}
diff --git a/pw_build/rust_test.gni b/pw_build/rust_test.gni
new file mode 100644
index 000000000..b4df9189f
--- /dev/null
+++ b/pw_build/rust_test.gni
@@ -0,0 +1,80 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/rust_executable.gni")
+
+# Note: In general, prefer to import target_types.gni rather than this file.
+#
+# This template sets up a configurable build of source files containing rust
+# unit tests. This wrapper is analogous to pw_test with additions to support
+# rust specific parameters such as rust edition and cargo config features.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_rust_test.
+template("pw_rust_test") {
+ output_dir = "${target_out_dir}/test"
+ if (defined(invoker.output_dir)) {
+ output_dir = invoker.output_dir
+ }
+
+ _actual_target_name = target_name
+ _is_enabled = !defined(invoker.enable_if) || invoker.enable_if
+ if (!_is_enabled) {
+ _actual_target_name = target_name + ".DISABLED"
+
+ # If the target is disabled, create an empty target in its place. Use an
+ # action with the original target's sources as inputs to ensure that the
+ # source files exist (even if they don't compile).
+ pw_python_action(target_name) {
+ script = "$dir_pw_build/py/pw_build/nop.py"
+ stamp = true
+
+ inputs = []
+ if (defined(invoker.sources)) {
+ inputs += invoker.sources
+ }
+ if (defined(invoker.public)) {
+ inputs += invoker.public
+ }
+ }
+ }
+
+ pw_rust_executable(_actual_target_name) {
+ forward_variables_from(invoker, "*")
+
+ if (!defined(rustflags)) {
+ rustflags = []
+ }
+ rustflags += [ "--test" ]
+
+ _extra_metadata = {
+ }
+ if (defined(invoker.extra_metadata)) {
+ _extra_metadata = invoker.extra_metadata
+ }
+
+ metadata = {
+ tests = [
+ {
+ type = "test"
+ test_name = _actual_target_name
+ test_directory = rebase_path(output_dir, root_build_dir)
+ extra_metadata = _extra_metadata
+ },
+ ]
+ }
+ }
+}
diff --git a/pw_build/target_config.bzl b/pw_build/target_config.bzl
deleted file mode 100644
index 2a1f2a56a..000000000
--- a/pw_build/target_config.bzl
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Configure Pigweed's backend implementations."""
-
-def _pigweed_config_impl(repository_ctx):
- if repository_ctx.attr.build_file_content and \
- repository_ctx.attr.build_file:
- fail("Attributes 'build_file_content' and 'build_file' cannot both be \
- defined at the same time.")
- if not repository_ctx.attr.build_file_content and \
- not repository_ctx.attr.build_file:
- fail("Either 'build_file_content' or 'build_file' must be defined.")
-
- if repository_ctx.name != "pigweed_config":
- fail("This repository should be name 'pigweed_config'")
-
- if repository_ctx.attr.build_file_content:
- repository_ctx.file("BUILD", repository_ctx.attr.build_file_content)
-
- if repository_ctx.attr.build_file:
- repository_ctx.template("BUILD", repository_ctx.attr.build_file, {})
-
-pigweed_config = repository_rule(
- _pigweed_config_impl,
- attrs = {
- "build_file_content": attr.string(
- doc = "The build file content to configure Pigweed.",
- mandatory = False,
- default = "",
- ),
- "build_file": attr.label(
- doc = "The label for the Pigweed config build file to use.",
- mandatory = False,
- default = "@pigweed//targets/host:host_config.BUILD",
- ),
- },
- doc = """
-Configure Pigweeds backend implementations.
-
-Example:
- # WORKSPACE
- pigweed_config(
- # This must use the exact name specified here otherwise this
- # remote repository will not function as expected.
- name = "pigweed_config",
- build_file = "//path/to/config.BUILD",
- )
-""",
-)
diff --git a/pw_build/target_types.gni b/pw_build/target_types.gni
index 3803c7194..b13bf052b 100644
--- a/pw_build/target_types.gni
+++ b/pw_build/target_types.gni
@@ -18,3 +18,5 @@ import("$dir_pw_build/cc_executable.gni")
import("$dir_pw_build/cc_library.gni")
import("$dir_pw_build/rust_executable.gni")
import("$dir_pw_build/rust_library.gni")
+import("$dir_pw_build/rust_proc_macro.gni")
+import("$dir_pw_build/rust_test.gni")
diff --git a/pw_build/test_info.gni b/pw_build/test_info.gni
new file mode 100644
index 000000000..a340d1859
--- /dev/null
+++ b/pw_build/test_info.gni
@@ -0,0 +1,101 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Generate metadata for tests.
+#
+# Various types of tests may use this template to create metadata used to
+# describe those tests. All entries will have the following form:
+#
+# {
+# "build_label": <GN source-absolute label>,
+# "test_name": <Name of test>,
+# "test_type": <Type of test>,
+# ...
+# }
+#
+# where "..." may represent additional, type-specific details.
+#
+# Args:
+# - test_type (required): String describing the type of test. One of:
+# "test_group"
+# "unit_test"
+# "action_test"
+# "perf_test"
+# "fuzz_test"
+# - test_name (optional): Name of the test as a string. Defaults to the
+# target name.
+# - build_label (optional): GN label for the test being described. Defaults to
+# the source-absolute label corresponding to `test_name`.
+# - extra_metadata (optional): Scope that defines additional metadata to
+# include for this test. Variables defined in the scope will be included
+# directly in the metadata, and thus should not be named "build_label",
+# "test_name", or "test_type".
+template("pw_test_info") {
+ assert(defined(invoker.test_type),
+ "`pw_test_info($target_name)` is missing `test_type`")
+ _type = invoker.test_type
+
+ if (defined(invoker.test_name)) {
+ _name = invoker.test_name
+ } else {
+ _name = get_label_info(target_name, "name")
+ }
+
+ if (defined(invoker.build_label)) {
+ _label = invoker.build_label
+ } else {
+ _label = _name
+ }
+
+ _metadata = {
+ # Include the extra metadata first, so any name collisions trigger errors.
+ forward_variables_from(invoker.extra_metadata, "*")
+ build_label = get_label_info(":$_label", "label_no_toolchain")
+ test_name = _name
+ test_type = _type
+ if (defined(invoker.tags) && invoker.tags != []) {
+ tags = invoker.tags
+ }
+ }
+
+ group(target_name) {
+ forward_variables_from(invoker, [ "testonly" ])
+ if (_type == "test_group") {
+ metadata = {
+ test_groups = [ _metadata ]
+ }
+ forward_variables_from(invoker, [ "deps" ])
+ } else if (_type == "unit_test") {
+ metadata = {
+ unit_tests = [ _metadata ]
+ }
+ } else if (_type == "action_test") {
+ metadata = {
+ action_tests = [ _metadata ]
+ }
+ } else if (_type == "perf_test") {
+ metadata = {
+ perf_tests = [ _metadata ]
+ }
+ } else if (_type == "fuzz_test") {
+ metadata = {
+ fuzz_tests = [ _metadata ]
+ }
+ } else {
+ assert(
+ false,
+ "pw_test_info($target_name) does not have a valid `test_type`: $_type")
+ }
+ }
+}
diff --git a/pw_build_info/BUILD.bazel b/pw_build_info/BUILD.bazel
index d7db7fe0b..09c6e4598 100644
--- a/pw_build_info/BUILD.bazel
+++ b/pw_build_info/BUILD.bazel
@@ -23,14 +23,21 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "build_id",
- srcs = [
- "build_id.cc",
- ],
+ name = "build_id_header",
hdrs = [
"public/pw_build_info/build_id.h",
],
includes = ["public"],
+ deps = [
+ "//pw_span",
+ ],
+)
+
+pw_cc_library(
+ name = "build_id",
+ srcs = [
+ "build_id.cc",
+ ],
linkopts = [
"-Lpw_build_info",
"-T$(location add_build_id_to_default_linker_script.ld)",
@@ -38,15 +45,43 @@ pw_cc_library(
],
deps = [
":add_build_id_to_default_linker_script.ld",
+ ":build_id_header",
":build_id_linker_snippet.ld",
"//pw_preprocessor",
"//pw_span",
],
)
+pw_cc_library(
+ name = "build_id_noop",
+ srcs = [
+ "build_id_noop.cc",
+ ],
+ deps = [
+ ":build_id_header",
+ "//pw_span",
+ ],
+)
+
+pw_cc_library(
+ name = "build_id_or_noop",
+ deps = select({
+ "@platforms//os:windows": [":build_id_noop"],
+ "@platforms//os:ios": [":build_id_noop"],
+ "@platforms//os:macos": [":build_id_noop"],
+ "//conditions:default": [":build_id"],
+ }),
+)
+
pw_cc_test(
name = "build_id_test",
srcs = ["build_id_test.cc"],
+ # Only compatible with platforms that support linker scripts.
+ target_compatible_with = select({
+ "@platforms//os:macos": ["@platforms//:incompatible"],
+ "@platforms//os:windows": ["@platforms//:incompatible"],
+ "//conditions:default": [],
+ }),
deps = [
":build_id",
"//pw_span",
diff --git a/pw_build_info/BUILD.gn b/pw_build_info/BUILD.gn
index 10560d84d..883837852 100644
--- a/pw_build_info/BUILD.gn
+++ b/pw_build_info/BUILD.gn
@@ -50,7 +50,18 @@ config("public_include_path") {
}
# GNU build IDs aren't supported by Windows and macOS.
-if (current_os != "mac" && current_os != "win") {
+if (current_os == "mac" || current_os == "win") {
+ # noop version of the package that allows build_id users to still be built
+ # for unsupported platforms and avoids amplifying downstream complexity. It
+ # provides a noop version of pw::build_info::BuildId() which just returns an
+ # empty span.
+ pw_source_set("build_id_or_noop") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_build_info/build_id.h" ]
+ sources = [ "build_id_noop.cc" ]
+ deps = [ dir_pw_span ]
+ }
+} else {
pw_source_set("build_id") {
all_dependent_configs = [
":gnu_build_id",
@@ -64,6 +75,10 @@ if (current_os != "mac" && current_os != "win") {
dir_pw_span,
]
}
+
+ pw_source_set("build_id_or_noop") {
+ public_deps = [ ":build_id" ]
+ }
}
pw_doc_group("docs") {
diff --git a/pw_build_info/build_id_noop.cc b/pw_build_info/build_id_noop.cc
new file mode 100644
index 000000000..2afef1edb
--- /dev/null
+++ b/pw_build_info/build_id_noop.cc
@@ -0,0 +1,22 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_build_info/build_id.h"
+#include "pw_span/span.h"
+
+namespace pw::build_info {
+
+span<const std::byte> BuildId() { return span<const std::byte>(); }
+
+} // namespace pw::build_info
diff --git a/pw_build_info/docs.rst b/pw_build_info/docs.rst
index 376bd4c38..e03a94337 100644
--- a/pw_build_info/docs.rst
+++ b/pw_build_info/docs.rst
@@ -28,6 +28,10 @@ Linux executables that depend on the ``build_id`` GN target will automatically
generate GNU build IDs. Windows and macOS binaries cannot use this target as
the implementation of GNU build IDs depends on the ELF file format.
+A separate GN target ``build_id_or_noop`` is available which provides an empty
+build ID on platforms where GNU build ID is not available while still providing
+a real GNU build ID where supported.
+
Getting started
===============
To generate GNU build IDs as part of your firmware image, you'll need to update
diff --git a/pw_build_info/py/BUILD.bazel b/pw_build_info/py/BUILD.bazel
index ddebfb238..24f8b1a17 100644
--- a/pw_build_info/py/BUILD.bazel
+++ b/pw_build_info/py/BUILD.bazel
@@ -24,7 +24,7 @@ py_library(
# This test attempts to run subprocesses directly in the source tree, which is
# incompatible with sandboxing.
-# TODO(b/241307309): Update this test to work with bazel.
+# TODO: b/241307309 - Update this test to work with bazel.
filegroup(
name = "build_id_test",
# size = "small",
diff --git a/pw_build_info/py/BUILD.gn b/pw_build_info/py/BUILD.gn
index c0e054f5e..f1dfe1e70 100644
--- a/pw_build_info/py/BUILD.gn
+++ b/pw_build_info/py/BUILD.gn
@@ -21,7 +21,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_build_info/__init__.py",
diff --git a/pw_build_info/py/build_id_test.py b/pw_build_info/py/build_id_test.py
index d6eb8e06c..ea0aae8fa 100644
--- a/pw_build_info/py/build_id_test.py
+++ b/pw_build_info/py/build_id_test.py
@@ -13,6 +13,7 @@
# the License.
"""Tests for pw_build_info's GNU build ID support."""
+import os
import subprocess
import tempfile
import unittest
@@ -32,6 +33,10 @@ class TestGnuBuildId(unittest.TestCase):
def test_build_id_correctness(self):
"""Tests to ensure GNU build IDs are read/written correctly."""
+ self.assertTrue('PW_PIGWEED_CIPD_INSTALL_DIR' in os.environ)
+ sysroot = Path(os.environ['PW_PIGWEED_CIPD_INSTALL_DIR']).joinpath(
+ "clang_sysroot"
+ )
with tempfile.TemporaryDirectory() as exe_dir:
exe_file = Path(exe_dir) / 'print_build_id.elf'
@@ -44,6 +49,7 @@ class TestGnuBuildId(unittest.TestCase):
'-I../pw_polyfill/public',
'-I../pw_preprocessor/public',
'-I../pw_span/public',
+ '--sysroot=%s' % sysroot,
'-std=c++17',
'-fuse-ld=lld',
'-Wl,-Tadd_build_id_to_default_linker_script.ld',
diff --git a/pw_build_info/py/setup.py b/pw_build_info/py/setup.py
deleted file mode 100644
index c1606beaa..000000000
--- a/pw_build_info/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_build_info"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_build_mcuxpresso/docs.rst b/pw_build_mcuxpresso/docs.rst
index 180587121..1302d5e0c 100644
--- a/pw_build_mcuxpresso/docs.rst
+++ b/pw_build_mcuxpresso/docs.rst
@@ -1,10 +1,10 @@
.. _module-pw_build_mcuxpresso:
--------------------
+===================
pw_build_mcuxpresso
--------------------
+===================
-The ``pw_build_mcuxpresso`` module provides helper utilizies for building a
+The ``pw_build_mcuxpresso`` module provides helper utilities for building a
target based on an NXP MCUXpresso SDK.
The GN build files live in ``third_party/mcuxpresso`` but are documented here.
@@ -12,21 +12,51 @@ The rationale for keeping the build files in ``third_party`` is that code
depending on an MCUXpresso SDK can clearly see that their dependency is on
third party, not pigweed code.
+-----------------------
Using an MCUXpresso SDK
-=======================
+-----------------------
An MCUXpresso SDK consists of a number of components, each of which has a set
-of sources, headers, pre-processor defines, and dependencies on other
+of sources, headers, preprocessor defines, and dependencies on other
components. These are all described in an XML "manifest" file included in the
SDK package.
To use the SDK within a Pigweed project, the set of components you need must be
-combined into a ``pw_source_set`` that you can depend on. This source set will
-include all of the sources and headers, along with necessary pre-processor
-defines, for those components and their dependencies.
+combined into a library that you can depend on. This library will include all of
+the sources and headers, along with necessary preprocessor defines, for those
+components and their dependencies.
-The source set is defined using the ``pw_mcuxpresso_sdk`` template, providing
-the path to the ``manifest`` XML, along with the names of the components you
-wish to ``include``.
+Optional components
+===================
+Including components will include all of their required dependencies. Where the
+components you include have optional dependencies, they must be satisfied by the
+set of components you include otherwise the library generation will fail with an
+error.
+
+Excluding components
+====================
+Components can be excluded from the generated source set, for example to
+suppress errors about optional dependencies your project does not need, or to
+prevent an unwanted component dependency from being introduced into your
+project.
+
+mcuxpresso_builder
+==================
+``mcuxpresso_builder`` is a utility installed into the environment that is used
+by the GN build scripts in ``third_party/mcuxpresso``, or directly by you to
+generate rules for the Bazel build.
+
+Usage is documented for each build system in the relevant section.
+
+------------
+The GN build
+------------
+Using an MCUxpresso SDK within a Pigweed project that uses the GN Build system
+involves the creation of one or more ``pw_source_set`` targets you can depend on
+in your executable targets.
+
+These source sets sets are defined using the ``pw_mcuxpresso_sdk`` template.
+Provide the path to the ``manifest`` XML, along with the names of the components
+you wish to ``include``.
.. code-block:: text
@@ -46,22 +76,9 @@ wish to ``include``.
deps = [ ":sample_project_sdk" ]
}
-Where the components you include have optional dependencies, they must be
-satisfied by the set of components you include otherwise the GN generation will
-fail with an error.
-
-Excluding components
---------------------
-Components can be excluded from the generated source set, for example to
-suppress errors about optional dependencies your project does not need, or to
-prevent an unwanted component dependency from being introduced into your
-project.
-
-This is accomplished by providing the list of components to ``exclude`` as an
-argument to the template.
-
-For example to replace the FreeRTOS kernel bundled with the MCUXpresso SDK with
-the Pigweed third-party target:
+To exclude components, provide the list to ``exclude`` as an argument to the
+template. For example to replace the FreeRTOS kernel bundled with the MCUXpresso
+SDK with the Pigweed third-party target:
.. code-block:: text
@@ -72,7 +89,7 @@ the Pigweed third-party target:
}
Introducing dependencies
-------------------------
+========================
As seen above, the generated source set can have dependencies added by passing
the ``public_deps`` (or ``deps``) arguments to the template.
@@ -85,7 +102,7 @@ SDK.
To resolve circular dependencies, in addition to the generated source set, two
configs named with the ``__defines`` and ``__includes`` suffixes on the template
-name are generated, to provide the pre-processor defines and include paths that
+name are generated, to provide the preprocessor defines and include paths that
the source set uses.
.. code-block:: text
@@ -111,22 +128,57 @@ the source set uses.
mcuxpresso_builder
==================
-``mcuxpresso_builder`` is a utility that contains the backend scripts used by
-the GN build scripts in ``third_party/mcuxpresso``. You should only need to
-interact with ``mcuxpresso_builder`` directly if you are doing something custom.
+For the GN build, this utility is invoked by the ``pw_mcuxpresso_sdk`` template.
+You should only need to interact with ``mcuxpresso_builder`` directly if you are
+doing something custom.
-project
--------
-This command outputs a GN scope describing the result of expanding the set of
-included and excluded components.
+The ``gn`` subcommand outputs a GN scope describing the result of expanding the
+set of included and excluded components.
The ``--prefix`` option specifies the GN location of the SDK files.
.. code-block:: bash
- mcuxpresso_builder project /path/to/manifest.xml \
+ mcuxpresso_builder gn /path/to/manifest.xml \
--include project_template.evkmimxrt595.MIMXRT595S \
--include utility.debug_console.MIMXRT595S \
--include component.serial_manager_uart.MIMXRT595S \
--exclude middleware.freertos-kernel.MIMXRT595S \
--prefix //path/to/sdk
+
+---------------
+The Bazel build
+---------------
+To use an MCUxpresso SDK within a Pigweed project that uses tha Bazel build
+system, you must use the ``mcuxpresso_builder`` tool directly and place its
+output in ``BUILD`` or ``BUILD.bazel`` files yourself.
+
+Provide the path to the manifest XML, the ``--name`` of the ``cc_library`` to
+create, along with the names of the components you wish to ``--include`` or
+``--exclude``.
+
+.. code-block:: bash
+
+ mcuxpresso_builder bazel /path/to/manifest.xml \
+ --name example_sdk \
+ --include project_template.evkmimxrt595.MIMXRT595S \
+ --include utility.debug_console.MIMXRT595S \
+ --include component.serial_manager_uart.MIMXRT595S \
+ --exclude middleware.freertos-kernel.MIMXRT595S
+
+
+Place the resulting output in a ``BUILD`` file, and then modify your
+``WORKSPACE`` to associate this build file with the path to the MCUxpresso SDK
+checkout.
+
+.. code-block:: python
+
+ new_local_repository(
+ name = "mcuxpresso_sdk",
+ build_file = "//third_party/mcuxpresso_sdk/BUILD",
+ path = "third_party/evkmimxrt595/sdk",
+ )
+
+To add other dependencies, compiler definitions, etc. it is recommended that
+you do so by creating a new target, and add a dependency to it, rather than
+modifying the generated targets.
diff --git a/pw_build_mcuxpresso/py/BUILD.gn b/pw_build_mcuxpresso/py/BUILD.gn
index e640bd372..274fbe4a9 100644
--- a/pw_build_mcuxpresso/py/BUILD.gn
+++ b/pw_build_mcuxpresso/py/BUILD.gn
@@ -20,12 +20,13 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_build_mcuxpresso/__init__.py",
"pw_build_mcuxpresso/__main__.py",
+ "pw_build_mcuxpresso/bazel.py",
"pw_build_mcuxpresso/components.py",
+ "pw_build_mcuxpresso/gn.py",
]
tests = [ "tests/components_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
index c537cf7e4..c2e816898 100644
--- a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
@@ -19,10 +19,12 @@ import pathlib
import sys
try:
- from pw_build_mcuxpresso import components
+ from pw_build_mcuxpresso import bazel, components, gn
except ImportError:
# Load from this directory if pw_build_mcuxpresso is not available.
import components # type: ignore
+ import bazel # type: ignore
+ import gn # type: ignore
def _parse_args() -> argparse.Namespace:
@@ -33,13 +35,22 @@ def _parse_args() -> argparse.Namespace:
dest='command', metavar='<command>', required=True
)
- project_parser = subparsers.add_parser(
- 'project', help='output components of an MCUXpresso project'
+ subparser = subparsers.add_parser(
+ 'gn', help='output components of an MCUXpresso project as GN scope'
)
- project_parser.add_argument('manifest_filename', type=pathlib.Path)
- project_parser.add_argument('--include', type=str, action='append')
- project_parser.add_argument('--exclude', type=str, action='append')
- project_parser.add_argument('--prefix', dest='path_prefix', type=str)
+ subparser.add_argument('manifest_filename', type=pathlib.Path)
+ subparser.add_argument('--include', type=str, action='append')
+ subparser.add_argument('--exclude', type=str, action='append')
+ subparser.add_argument('--prefix', dest='path_prefix', type=str)
+
+ subparser = subparsers.add_parser(
+ 'bazel', help='output an MCUXpresso project as a bazel target'
+ )
+ subparser.add_argument('manifest_filename', type=pathlib.Path)
+ subparser.add_argument('--name', dest='bazel_name', type=str, required=True)
+ subparser.add_argument('--include', type=str, action='append')
+ subparser.add_argument('--exclude', type=str, action='append')
+ subparser.add_argument('--prefix', dest='path_prefix', type=str)
return parser.parse_args()
@@ -48,12 +59,17 @@ def main():
"""Main command line function."""
args = _parse_args()
- if args.command == 'project':
- components.project(
- args.manifest_filename,
- include=args.include,
- exclude=args.exclude,
- path_prefix=args.path_prefix,
+ project = components.Project.from_file(
+ args.manifest_filename,
+ include=args.include,
+ exclude=args.exclude,
+ )
+
+ if args.command == 'gn':
+ gn.gn_output(project, path_prefix=args.path_prefix)
+ if args.command == 'bazel':
+ bazel.bazel_output(
+ project, name=args.bazel_name, path_prefix=args.path_prefix
)
sys.exit(0)
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/bazel.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/bazel.py
new file mode 100644
index 000000000..3d6d3a9b5
--- /dev/null
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/bazel.py
@@ -0,0 +1,89 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Bazel output support."""
+
+from typing import Any, List, Optional
+
+import pathlib
+
+try:
+ from pw_build_mcuxpresso.components import Project
+except ImportError:
+ # Load from this directory if pw_build_mcuxpresso is not available.
+ from components import Project # type: ignore
+
+
+def _bazel_str(val: Any) -> str:
+ """Returns string in Bazel format with correct escaping."""
+ return str(val).replace('"', r'\"').replace('$', r'\$')
+
+
+def _bazel_str_out(name: str, val: Any, indent: int = 0) -> None:
+ """Outputs string in Bazel format with correct escaping."""
+ print(' ' * indent + f'{name} = "{_bazel_str(val)}",')
+
+
+def _bazel_str_list_out(name: str, vals: List[Any], indent: int = 0) -> None:
+ """Outputs list of strings in Bazel format with correct escaping."""
+ if not vals:
+ return
+
+ print(' ' * indent + f'{name} = [')
+ for val in vals:
+ print(' ' * (indent + 1) + f'"{_bazel_str(val)}",')
+ print(' ' * indent + '],')
+
+
+def _bazel_path_list_out(
+ name: str,
+ vals: List[pathlib.Path],
+ path_prefix: Optional[str] = None,
+ indent: int = 0,
+) -> None:
+ """Outputs list of paths in Bazel format with common prefix."""
+ if path_prefix is not None:
+ str_vals = [f'{path_prefix}{str(val)}' for val in vals]
+ else:
+ str_vals = [str(f) for f in vals]
+
+ _bazel_str_list_out(name, sorted(set(str_vals)), indent=indent)
+
+
+def bazel_output(
+ project: Project, name: str, path_prefix: Optional[str] = None
+):
+ """Output Bazel target for a project with the specified components.
+
+ Args:
+ project: MCUXpresso project to output.
+ name: target name to output.
+ path_prefix: string prefix to prepend to all paths.
+ """
+ print('cc_library(')
+ _bazel_str_out('name', name, indent=1)
+ _bazel_path_list_out(
+ 'srcs',
+ project.sources + project.libs,
+ path_prefix=path_prefix,
+ indent=1,
+ )
+ _bazel_path_list_out(
+ 'hdrs', project.headers, path_prefix=path_prefix, indent=1
+ )
+ _bazel_str_list_out('defines', project.defines, indent=1)
+ _bazel_path_list_out(
+ 'includes', project.include_dirs, path_prefix=path_prefix, indent=1
+ )
+
+ print(')')
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
index a7223a434..c664d3f0e 100644
--- a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
@@ -13,37 +13,13 @@
# the License.
"""Finds components for a given manifest."""
-from typing import Any, List, Optional, Tuple
+from typing import List, Optional, Tuple
import pathlib
import sys
import xml.etree.ElementTree
-def _gn_str_out(name: str, val: Any):
- """Outputs scoped string in GN format."""
- print(f'{name} = "{val}"')
-
-
-def _gn_list_str_out(name: str, val: List[Any]):
- """Outputs list of strings in GN format with correct escaping."""
- list_str = ','.join(
- '"' + str(x).replace('"', r'\"').replace('$', r'\$') + '"' for x in val
- )
- print(f'{name} = [{list_str}]')
-
-
-def _gn_list_path_out(
- name: str, val: List[pathlib.Path], path_prefix: Optional[str] = None
-):
- """Outputs list of paths in GN format with common prefix."""
- if path_prefix is not None:
- str_val = list(f'{path_prefix}/{str(d)}' for d in val)
- else:
- str_val = list(str(d) for d in val)
- _gn_list_str_out(name, str_val)
-
-
def get_component(
root: xml.etree.ElementTree.Element, component_id: str
) -> Tuple[Optional[xml.etree.ElementTree.Element], Optional[pathlib.Path]]:
@@ -488,42 +464,68 @@ def create_project(
)
-def project(
- manifest_path: pathlib.Path,
- include: Optional[List[str]] = None,
- exclude: Optional[List[str]] = None,
- path_prefix: Optional[str] = None,
-):
- """Output GN scope for a project with the specified components.
+class Project:
+ """Self-contained MCUXpresso project.
- Args:
- manifest_path: path to SDK manifest XML.
- include: list of component ids included in the project.
- exclude: list of component ids excluded from the project.
- path_prefix: string prefix to prepend to all paths.
+ Properties:
+ component_ids: list of component ids compromising the project.
+ defines: list of compiler definitions to build the project.
+ include_dirs: list of include directory paths needed for the project.
+ headers: list of header paths exported by the project.
+ sources: list of source file paths built as part of the project.
+ libs: list of libraries linked to the project.
+ dependencies_satisfied: True if the project dependencies are satisfied.
"""
- assert include is not None, "Project must include at least one component."
-
- tree = xml.etree.ElementTree.parse(manifest_path)
- root = tree.getroot()
-
- (
- component_ids,
- defines,
- include_dirs,
- headers,
- sources,
- libs,
- ) = create_project(root, include, exclude=exclude)
-
- for component_id in component_ids:
- if not check_dependencies(
- root, component_id, component_ids, exclude=exclude
- ):
- return
-
- _gn_list_str_out('defines', defines)
- _gn_list_path_out('include_dirs', include_dirs, path_prefix=path_prefix)
- _gn_list_path_out('public', headers, path_prefix=path_prefix)
- _gn_list_path_out('sources', sources, path_prefix=path_prefix)
- _gn_list_path_out('libs', libs, path_prefix=path_prefix)
+
+ @classmethod
+ def from_file(
+ cls,
+ manifest_path: pathlib.Path,
+ include: Optional[List[str]] = None,
+ exclude: Optional[List[str]] = None,
+ ):
+ """Create a self-contained project with the specified components.
+
+ Args:
+ manifest_path: path to SDK manifest XML.
+ include: list of component ids included in the project.
+ exclude: list of component ids excluded from the project.
+ """
+ tree = xml.etree.ElementTree.parse(manifest_path)
+ root = tree.getroot()
+ return cls(root, include=include, exclude=exclude)
+
+ def __init__(
+ self,
+ manifest: xml.etree.ElementTree.Element,
+ include: Optional[List[str]] = None,
+ exclude: Optional[List[str]] = None,
+ ):
+ """Create a self-contained project with the specified components.
+
+ Args:
+ manifest: parsed manifest XML.
+ include: list of component ids included in the project.
+ exclude: list of component ids excluded from the project.
+ """
+ assert (
+ include is not None
+ ), "Project must include at least one component."
+
+ (
+ self.component_ids,
+ self.defines,
+ self.include_dirs,
+ self.headers,
+ self.sources,
+ self.libs,
+ ) = create_project(manifest, include, exclude=exclude)
+
+ for component_id in self.component_ids:
+ if not check_dependencies(
+ manifest, component_id, self.component_ids, exclude=exclude
+ ):
+ self.dependencies_satisfied = False
+ return
+
+ self.dependencies_satisfied = True
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/gn.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/gn.py
new file mode 100644
index 000000000..202c4f774
--- /dev/null
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/gn.py
@@ -0,0 +1,59 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""GN output support."""
+
+from typing import Any, List, Optional
+
+import pathlib
+
+try:
+ from pw_build_mcuxpresso.components import Project
+except ImportError:
+ # Load from this directory if pw_build_mcuxpresso is not available.
+ from components import Project # type: ignore
+
+
+def _gn_list_str_out(name: str, val: List[Any]):
+ """Outputs list of strings in GN format with correct escaping."""
+ list_str = ','.join(
+ '"' + str(x).replace('"', r'\"').replace('$', r'\$') + '"' for x in val
+ )
+ print(f'{name} = [{list_str}]')
+
+
+def _gn_list_path_out(
+ name: str, val: List[pathlib.Path], path_prefix: Optional[str] = None
+):
+ """Outputs list of paths in GN format with common prefix."""
+ if path_prefix is not None:
+ str_val = list(f'{path_prefix}/{str(d)}' for d in val)
+ else:
+ str_val = list(str(d) for d in val)
+ _gn_list_str_out(name, str_val)
+
+
+def gn_output(project: Project, path_prefix: Optional[str] = None):
+ """Output GN scope for a project with the specified components.
+
+ Args:
+ project: MCUXpresso project to output..
+ path_prefix: string prefix to prepend to all paths.
+ """
+ _gn_list_str_out('defines', project.defines)
+ _gn_list_path_out(
+ 'include_dirs', project.include_dirs, path_prefix=path_prefix
+ )
+ _gn_list_path_out('public', project.headers, path_prefix=path_prefix)
+ _gn_list_path_out('sources', project.sources, path_prefix=path_prefix)
+ _gn_list_path_out('libs', project.libs, path_prefix=path_prefix)
diff --git a/pw_build_mcuxpresso/py/setup.py b/pw_build_mcuxpresso/py/setup.py
deleted file mode 100644
index 694c28691..000000000
--- a/pw_build_mcuxpresso/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_build_mcuxpresso"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_build_mcuxpresso/py/tests/components_test.py b/pw_build_mcuxpresso/py/tests/components_test.py
index 94ca6828b..8b36a448f 100644
--- a/pw_build_mcuxpresso/py/tests/components_test.py
+++ b/pw_build_mcuxpresso/py/tests/components_test.py
@@ -805,8 +805,8 @@ class CheckDependenciesTest(unittest.TestCase):
self.assertEqual(satisfied, True)
-class CreateProjectTest(unittest.TestCase):
- """create_project tests."""
+class ProjectTest(unittest.TestCase):
+ """Project tests."""
def test_create_project(self):
"""test creating a project."""
@@ -904,21 +904,14 @@ class CreateProjectTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- (
- component_ids,
- defines,
- include_dirs,
- headers,
- sources,
- libs,
- ) = components.create_project(
- root, ['test', 'frodo'], exclude=['bilbo']
+ project = components.Project(
+ root, ['test', 'frodo'], exclude=['baz', 'bilbo']
)
- self.assertEqual(component_ids, ['test', 'frodo', 'foo', 'bar'])
- self.assertEqual(defines, ['FRODO', 'FOO', 'BAR'])
+ self.assertEqual(project.component_ids, ['test', 'frodo', 'foo', 'bar'])
+ self.assertEqual(project.defines, ['FRODO', 'FOO', 'BAR'])
self.assertEqual(
- include_dirs,
+ project.include_dirs,
[
pathlib.Path('frodo/include'),
pathlib.Path('foo/include'),
@@ -926,7 +919,7 @@ class CreateProjectTest(unittest.TestCase):
],
)
self.assertEqual(
- headers,
+ project.headers,
[
pathlib.Path('frodo/include/frodo.h'),
pathlib.Path('foo/include/foo.h'),
@@ -934,14 +927,15 @@ class CreateProjectTest(unittest.TestCase):
],
)
self.assertEqual(
- sources,
+ project.sources,
[
pathlib.Path('frodo/src/frodo.cc'),
pathlib.Path('foo/src/foo.cc'),
pathlib.Path('bar/src/bar.cc'),
],
)
- self.assertEqual(libs, [pathlib.Path('frodo/libonering.a')])
+ self.assertEqual(project.libs, [pathlib.Path('frodo/libonering.a')])
+ self.assertTrue(project.dependencies_satisfied)
if __name__ == '__main__':
diff --git a/pw_bytes/Android.bp b/pw_bytes/Android.bp
index d2950fd86..316debc7c 100644
--- a/pw_bytes/Android.bp
+++ b/pw_bytes/Android.bp
@@ -22,10 +22,14 @@ cc_library_static {
vendor_available: true,
export_include_dirs: ["public"],
header_libs: [
+ "fuchsia_sdk_lib_stdcompat",
"pw_polyfill_headers",
"pw_preprocessor_headers",
"pw_span_headers",
],
+ export_header_lib_headers: [
+ "fuchsia_sdk_lib_stdcompat",
+ ],
host_supported: true,
srcs: [
// Errors with
@@ -37,4 +41,4 @@ cc_library_static {
"pw_containers",
"pw_status",
],
-} \ No newline at end of file
+}
diff --git a/pw_bytes/BUILD.bazel b/pw_bytes/BUILD.bazel
index 7a6cbf6b4..9a8ded187 100644
--- a/pw_bytes/BUILD.bazel
+++ b/pw_bytes/BUILD.bazel
@@ -28,10 +28,12 @@ pw_cc_library(
"byte_builder.cc",
],
hdrs = [
+ "public/pw_bytes/alignment.h",
"public/pw_bytes/array.h",
"public/pw_bytes/byte_builder.h",
"public/pw_bytes/endian.h",
"public/pw_bytes/span.h",
+ "public/pw_bytes/suffix.h",
"public/pw_bytes/units.h",
],
includes = ["public"],
@@ -49,6 +51,16 @@ pw_cc_library(
name = "bit",
hdrs = ["public/pw_bytes/bit.h"],
includes = ["public"],
+ deps = ["//third_party/fuchsia:stdcompat"],
+)
+
+pw_cc_test(
+ name = "alignment_test",
+ srcs = ["alignment_test.cc"],
+ deps = [
+ ":pw_bytes",
+ "//pw_unit_test",
+ ],
)
pw_cc_test(
@@ -88,6 +100,14 @@ pw_cc_test(
)
pw_cc_test(
+ name = "suffix_test",
+ srcs = ["suffix_test.cc"],
+ deps = [
+ ":pw_bytes",
+ ],
+)
+
+pw_cc_test(
name = "units_test",
srcs = ["units_test.cc"],
deps = [
diff --git a/pw_bytes/BUILD.gn b/pw_bytes/BUILD.gn
index a486880ae..5aba51844 100644
--- a/pw_bytes/BUILD.gn
+++ b/pw_bytes/BUILD.gn
@@ -27,10 +27,12 @@ config("public_include_path") {
pw_source_set("pw_bytes") {
public_configs = [ ":public_include_path" ]
public = [
+ "public/pw_bytes/alignment.h",
"public/pw_bytes/array.h",
"public/pw_bytes/byte_builder.h",
"public/pw_bytes/endian.h",
"public/pw_bytes/span.h",
+ "public/pw_bytes/suffix.h",
"public/pw_bytes/units.h",
]
sources = [ "byte_builder.cc" ]
@@ -46,15 +48,17 @@ pw_source_set("pw_bytes") {
pw_source_set("bit") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_bytes/bit.h" ]
- remove_public_deps = [ "*" ]
+ public_deps = [ "$dir_pw_third_party/fuchsia:stdcompat" ]
}
pw_test_group("tests") {
tests = [
+ ":alignment_test",
":array_test",
":bit_test",
":byte_builder_test",
":endian_test",
+ ":suffix_test",
":units_test",
]
group_deps = [
@@ -63,6 +67,11 @@ pw_test_group("tests") {
]
}
+pw_test("alignment_test") {
+ deps = [ ":pw_bytes" ]
+ sources = [ "alignment_test.cc" ]
+}
+
pw_test("array_test") {
deps = [ ":pw_bytes" ]
sources = [ "array_test.cc" ]
@@ -83,6 +92,11 @@ pw_test("endian_test") {
sources = [ "endian_test.cc" ]
}
+pw_test("suffix_test") {
+ deps = [ ":pw_bytes" ]
+ sources = [ "suffix_test.cc" ]
+}
+
pw_test("units_test") {
deps = [ ":pw_bytes" ]
sources = [ "units_test.cc" ]
diff --git a/pw_bytes/CMakeLists.txt b/pw_bytes/CMakeLists.txt
index c6f2e677e..8abe481d5 100644
--- a/pw_bytes/CMakeLists.txt
+++ b/pw_bytes/CMakeLists.txt
@@ -16,10 +16,12 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_library(pw_bytes STATIC
HEADERS
+ public/pw_bytes/alignment.h
public/pw_bytes/array.h
public/pw_bytes/byte_builder.h
public/pw_bytes/endian.h
public/pw_bytes/span.h
+ public/pw_bytes/suffix.h
public/pw_bytes/units.h
PUBLIC_INCLUDES
public
@@ -33,15 +35,24 @@ pw_add_library(pw_bytes STATIC
SOURCES
byte_builder.cc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_BYTES)
- zephyr_link_libraries(pw_bytes)
-endif()
pw_add_library(pw_bytes.bit INTERFACE
HEADERS
public/pw_bytes/bit.h
PUBLIC_INCLUDES
public
+ PUBLIC_DEPS
+ pw_third_party.fuchsia.stdcompat
+)
+
+pw_add_test(pw_bytes.alignment_test
+ SOURCES
+ alignment_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ GROUPS
+ modules
+ pw_bytes
)
pw_add_test(pw_bytes.array_test
@@ -84,6 +95,16 @@ pw_add_test(pw_bytes.endian_test
pw_bytes
)
+pw_add_test(pw_bytes.suffix_test
+ SOURCES
+ suffix_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ GROUPS
+ modules
+ pw_bytes
+)
+
pw_add_test(pw_bytes.units_test
SOURCES
units_test.cc
diff --git a/pw_bytes/Kconfig b/pw_bytes/Kconfig
index 482ef21b2..f38dd7193 100644
--- a/pw_bytes/Kconfig
+++ b/pw_bytes/Kconfig
@@ -12,9 +12,15 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_bytes"
+
config PIGWEED_BYTES
- bool "Enable the Pigweed bytes library (pw_bytes)"
+ bool "Link pw_bytes library"
select PIGWEED_POLYFILL_OVERRIDES
select PIGWEED_PREPROCESSOR
select PIGWEED_SPAN
select PIGWEED_STATUS
+ help
+ See :ref:`module-pw_bytes` for module details.
+
+endmenu
diff --git a/pw_bytes/alignment_test.cc b/pw_bytes/alignment_test.cc
new file mode 100644
index 000000000..9a6e2616a
--- /dev/null
+++ b/pw_bytes/alignment_test.cc
@@ -0,0 +1,116 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_bytes/alignment.h"
+
+#include "gtest/gtest.h"
+
+namespace pw {
+namespace {
+
+TEST(AlignUp, Zero) {
+ EXPECT_EQ(0u, AlignUp(0, 1));
+ EXPECT_EQ(0u, AlignUp(0, 2));
+ EXPECT_EQ(0u, AlignUp(0, 15));
+}
+
+TEST(AlignUp, Aligned) {
+ for (size_t i = 1; i < 130; ++i) {
+ EXPECT_EQ(i, AlignUp(i, i));
+ EXPECT_EQ(2 * i, AlignUp(2 * i, i));
+ EXPECT_EQ(3 * i, AlignUp(3 * i, i));
+ }
+}
+
+TEST(AlignUp, NonAligned_PowerOf2) {
+ EXPECT_EQ(32u, AlignUp(1, 32));
+ EXPECT_EQ(32u, AlignUp(31, 32));
+ EXPECT_EQ(64u, AlignUp(33, 32));
+ EXPECT_EQ(64u, AlignUp(45, 32));
+ EXPECT_EQ(64u, AlignUp(63, 32));
+ EXPECT_EQ(128u, AlignUp(127, 32));
+}
+
+TEST(AlignUp, NonAligned_NonPowerOf2) {
+ EXPECT_EQ(2u, AlignUp(1, 2));
+
+ EXPECT_EQ(15u, AlignUp(1, 15));
+ EXPECT_EQ(15u, AlignUp(14, 15));
+ EXPECT_EQ(30u, AlignUp(16, 15));
+}
+
+TEST(AlignDown, Zero) {
+ EXPECT_EQ(0u, AlignDown(0, 1));
+ EXPECT_EQ(0u, AlignDown(0, 2));
+ EXPECT_EQ(0u, AlignDown(0, 15));
+}
+
+TEST(AlignDown, Aligned) {
+ for (size_t i = 1; i < 130; ++i) {
+ EXPECT_EQ(i, AlignDown(i, i));
+ EXPECT_EQ(2 * i, AlignDown(2 * i, i));
+ EXPECT_EQ(3 * i, AlignDown(3 * i, i));
+ }
+}
+
+TEST(AlignDown, NonAligned_PowerOf2) {
+ EXPECT_EQ(0u, AlignDown(1, 32));
+ EXPECT_EQ(0u, AlignDown(31, 32));
+ EXPECT_EQ(32u, AlignDown(33, 32));
+ EXPECT_EQ(32u, AlignDown(45, 32));
+ EXPECT_EQ(32u, AlignDown(63, 32));
+ EXPECT_EQ(96u, AlignDown(127, 32));
+}
+
+TEST(AlignDown, NonAligned_NonPowerOf2) {
+ EXPECT_EQ(0u, AlignDown(1, 2));
+
+ EXPECT_EQ(0u, AlignDown(1, 15));
+ EXPECT_EQ(0u, AlignDown(14, 15));
+ EXPECT_EQ(15u, AlignDown(16, 15));
+}
+
+TEST(Padding, Zero) {
+ EXPECT_EQ(0u, Padding(0, 1));
+ EXPECT_EQ(0u, Padding(0, 2));
+ EXPECT_EQ(0u, Padding(0, 15));
+}
+
+TEST(Padding, Aligned) {
+ for (size_t i = 1; i < 130; ++i) {
+ EXPECT_EQ(0u, Padding(i, i));
+ EXPECT_EQ(0u, Padding(2 * i, i));
+ EXPECT_EQ(0u, Padding(3 * i, i));
+ }
+}
+
+TEST(Padding, NonAligned_PowerOf2) {
+ EXPECT_EQ(31u, Padding(1, 32));
+ EXPECT_EQ(1u, Padding(31, 32));
+ EXPECT_EQ(31u, Padding(33, 32));
+ EXPECT_EQ(19u, Padding(45, 32));
+ EXPECT_EQ(1u, Padding(63, 32));
+ EXPECT_EQ(1u, Padding(127, 32));
+}
+
+TEST(Padding, NonAligned_NonPowerOf2) {
+ EXPECT_EQ(1u, Padding(1, 2));
+
+ EXPECT_EQ(14u, Padding(1, 15));
+ EXPECT_EQ(1u, Padding(14, 15));
+ EXPECT_EQ(14u, Padding(16, 15));
+}
+
+} // namespace
+} // namespace pw
diff --git a/pw_bytes/docs.rst b/pw_bytes/docs.rst
index a625fe04c..82c870bdd 100644
--- a/pw_bytes/docs.rst
+++ b/pw_bytes/docs.rst
@@ -21,6 +21,10 @@ Dependencies
Features
--------
+pw_bytes/alignment.h
+====================
+Functions for aligning sizes and addresses to memory alignment boundaries.
+
pw_bytes/array.h
================
Functions for working with byte arrays, primarily for building fixed-size byte
@@ -54,6 +58,13 @@ pw_bytes/endian.h
=================
Functions for converting the endianness of integral values.
+pw_bytes/suffix.h
+=================
+This module exports a single ``_b`` literal, making it easier to create
+``std::byte`` values for tests.
+
+.. cpp:function:: constexpr std::byte operator"" _b(unsigned long long value)
+
pw_bytes/units.h
================
Constants, functions and user-defined literals for specifying a number of bytes
diff --git a/pw_bytes/public/pw_bytes/alignment.h b/pw_bytes/public/pw_bytes/alignment.h
new file mode 100644
index 000000000..c9f02f748
--- /dev/null
+++ b/pw_bytes/public/pw_bytes/alignment.h
@@ -0,0 +1,35 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+
+namespace pw {
+
+// Returns the value rounded down to the nearest multiple of alignment.
+constexpr size_t AlignDown(size_t value, size_t alignment) {
+ return (value / alignment) * alignment;
+}
+
+// Returns the value rounded up to the nearest multiple of alignment.
+constexpr size_t AlignUp(size_t value, size_t alignment) {
+ return (value + alignment - 1) / alignment * alignment;
+}
+
+// Returns the number of padding bytes required to align the provided length.
+constexpr size_t Padding(size_t length, size_t alignment) {
+ return AlignUp(length, alignment) - length;
+}
+
+} // namespace pw
diff --git a/pw_bytes/public/pw_bytes/bit.h b/pw_bytes/public/pw_bytes/bit.h
index c712719e6..10b01d6ee 100644
--- a/pw_bytes/public/pw_bytes/bit.h
+++ b/pw_bytes/public/pw_bytes/bit.h
@@ -15,30 +15,10 @@
// Features from the <bit> header introduced in C++20.
#pragma once
-#if __has_include(<bit>)
-#include <bit>
-#endif // __has_include(<bit>)
+#include "lib/stdcompat/bit.h"
namespace pw {
-#ifdef __cpp_lib_endian
-
-using std::endian;
-
-#elif defined(__GNUC__)
-
-enum class endian {
- little = __ORDER_LITTLE_ENDIAN__,
- big = __ORDER_BIG_ENDIAN__,
- native = __BYTE_ORDER__,
-};
-
-#else
-
-static_assert(false,
- "The pw::endian enum is not defined for this compiler. Add a "
- "definition to pw_bytes/bit.h.");
-
-#endif // __cpp_lib_endian
+using ::cpp20::endian;
} // namespace pw
diff --git a/pw_bytes/public/pw_bytes/endian.h b/pw_bytes/public/pw_bytes/endian.h
index 215af47c6..1e4d877b0 100644
--- a/pw_bytes/public/pw_bytes/endian.h
+++ b/pw_bytes/public/pw_bytes/endian.h
@@ -15,6 +15,7 @@
#include <algorithm>
#include <array>
+#include <cstdint>
#include <cstring>
#include <type_traits>
diff --git a/pw_bytes/public/pw_bytes/suffix.h b/pw_bytes/public/pw_bytes/suffix.h
new file mode 100644
index 000000000..38c6d1002
--- /dev/null
+++ b/pw_bytes/public/pw_bytes/suffix.h
@@ -0,0 +1,33 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+
+namespace pw {
+
+/// Returns a ``std::byte`` when used as a ``_b`` suffix.
+///
+/// This is useful for writing byte literals, particularly in tests.
+/// To use, add ``using ::pw::operator""_b;`` and then use like ``5_b``
+/// in order to create a ``std::byte`` with the contents ``5``.
+///
+/// This should not be used in header files, as it requires a ``using``
+/// declaration that will be publicly exported at whatever level it is
+/// used.
+constexpr std::byte operator"" _b(unsigned long long value) {
+ return std::byte(value);
+}
+
+} // namespace pw
diff --git a/pw_bytes/suffix_test.cc b/pw_bytes/suffix_test.cc
new file mode 100644
index 000000000..5825c0326
--- /dev/null
+++ b/pw_bytes/suffix_test.cc
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_bytes/suffix.h"
+
+#include "gtest/gtest.h"
+
+namespace {
+
+using ::pw::operator""_b;
+
+TEST(Suffix, ReturnsByte) {
+ std::byte x = 5_b;
+ EXPECT_EQ(x, std::byte(5));
+}
+
+} // namespace
diff --git a/pw_checksum/Android.bp b/pw_checksum/Android.bp
new file mode 100644
index 000000000..09366fe3a
--- /dev/null
+++ b/pw_checksum/Android.bp
@@ -0,0 +1,35 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library {
+ name: "pw_checksum",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_span_headers",
+ ],
+ static_libs: [
+ "pw_bytes",
+ ],
+ srcs: [
+ "crc16_ccitt.cc",
+ "crc32.cc",
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
diff --git a/pw_checksum/BUILD.gn b/pw_checksum/BUILD.gn
index 450d70154..8bb4e47cf 100644
--- a/pw_checksum/BUILD.gn
+++ b/pw_checksum/BUILD.gn
@@ -56,7 +56,7 @@ pw_source_set("pw_checksum") {
dir_pw_span,
]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -77,7 +77,7 @@ pw_test("crc16_ccitt_test") {
"crc16_ccitt_test_c.c",
]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -91,7 +91,7 @@ pw_test("crc32_test") {
"crc32_test_c.c",
]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
diff --git a/pw_checksum/CMakeLists.txt b/pw_checksum/CMakeLists.txt
index c3fcb5582..a0e82657c 100644
--- a/pw_checksum/CMakeLists.txt
+++ b/pw_checksum/CMakeLists.txt
@@ -27,9 +27,20 @@ pw_add_library(pw_checksum STATIC
crc16_ccitt.cc
crc32.cc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_CHECKSUM)
- zephyr_link_libraries(pw_checksum)
-endif()
+
+# TODO: b/284002266 - Unresolved linker error when using pw_checksum above.
+# Created this crc32 library and linker error is resolved.
+pw_add_library(pw_checksum.crc32 STATIC
+ HEADERS
+ public/pw_checksum/crc32.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_span
+ SOURCES
+ crc32.cc
+)
pw_add_test(pw_checksum.crc16_ccitt_test
SOURCES
diff --git a/pw_checksum/Kconfig b/pw_checksum/Kconfig
index 91a44d495..a21eb2d81 100644
--- a/pw_checksum/Kconfig
+++ b/pw_checksum/Kconfig
@@ -12,7 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_checksum"
+
config PIGWEED_CHECKSUM
- bool "Enable Pigweed checksum library (pw_checksum)"
+ bool "Link pw_checksum library"
select PIGWEED_SPAN
select PIGWEED_BYTES
+ help
+ See :ref:`module-pw_checksum` for module details.
+
+endmenu
diff --git a/pw_checksum/docs.rst b/pw_checksum/docs.rst
index aa9a0a831..fcec3be09 100644
--- a/pw_checksum/docs.rst
+++ b/pw_checksum/docs.rst
@@ -124,7 +124,7 @@ Compatibility
Dependencies
============
-* ``pw_span``
+- :ref:`module-pw_span`
.. _Module Configuration Options:
diff --git a/pw_checksum/size_report/BUILD.gn b/pw_checksum/size_report/BUILD.gn
index 634cad4ac..199d60162 100644
--- a/pw_checksum/size_report/BUILD.gn
+++ b/pw_checksum/size_report/BUILD.gn
@@ -27,7 +27,7 @@ pw_executable("noop_checksum") {
]
defines = [ "USE_NOOP_CHECKSUM=1" ]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -42,7 +42,7 @@ pw_executable("crc32_8bit_checksum") {
]
defines = [ "USE_CRC32_8BIT_CHECKSUM=1" ]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -57,7 +57,7 @@ pw_executable("crc32_4bit_checksum") {
]
defines = [ "USE_CRC32_4BIT_CHECKSUM=1" ]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -72,7 +72,7 @@ pw_executable("crc32_1bit_checksum") {
]
defines = [ "USE_CRC32_1BIT_CHECKSUM=1" ]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -87,7 +87,7 @@ pw_executable("crc16_checksum") {
]
defines = [ "USE_CRC16_CHECKSUM=1" ]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
@@ -102,6 +102,6 @@ pw_executable("fletcher16_checksum") {
]
defines = [ "USE_FLETCHER16_CHECKSUM=1" ]
- # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
configs = [ "$dir_pw_build:conversion_warnings" ]
}
diff --git a/pw_checksum/size_report/run_checksum.cc b/pw_checksum/size_report/run_checksum.cc
index d37219081..abfb9633f 100644
--- a/pw_checksum/size_report/run_checksum.cc
+++ b/pw_checksum/size_report/run_checksum.cc
@@ -82,7 +82,9 @@ class Fletcher16 {
}
}
PW_NO_INLINE void Update(std::byte) {}
- PW_NO_INLINE uint32_t value() const { return (sum2_ << 8) | sum1_; };
+ PW_NO_INLINE uint32_t value() const {
+ return static_cast<uint32_t>(sum2_) << 8 | sum1_;
+ };
void clear() {}
private:
@@ -96,7 +98,7 @@ char buffer[128];
char* volatile get_buffer = buffer;
volatile unsigned get_size;
-unsigned RunChecksum() {
+int RunChecksum() {
// Trick the optimizer and also satisfy the type checker.
get_size = sizeof(buffer);
char* local_buffer = get_buffer;
diff --git a/pw_chre/BUILD.bazel b/pw_chre/BUILD.bazel
new file mode 100644
index 000000000..aefc5b975
--- /dev/null
+++ b/pw_chre/BUILD.bazel
@@ -0,0 +1,60 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# TODO: b/298660977 - Add bazel support for CHRE.
+filegroup(
+ name = "chre",
+ srcs = [
+ "chre.cc",
+ "chre_api_re.cc",
+ "chre_empty_host_link.cc",
+ "context.cc",
+ "docs.rst",
+ "example_init.cc",
+ "host_link.cc",
+ "include",
+ "include/chre/target_platform/atomic_base.h",
+ "include/chre/target_platform/atomic_base_impl.h",
+ "include/chre/target_platform/condition_variable_base.h",
+ "include/chre/target_platform/condition_variable_impl.h",
+ "include/chre/target_platform/fatal_error.h",
+ "include/chre/target_platform/host_link_base.h",
+ "include/chre/target_platform/log.h",
+ "include/chre/target_platform/mutex_base.h",
+ "include/chre/target_platform/mutex_base_impl.h",
+ "include/chre/target_platform/platform_nanoapp_base.h",
+ "include/chre/target_platform/platform_sensor_base.h",
+ "include/chre/target_platform/platform_sensor_manager_base.h",
+ "include/chre/target_platform/platform_sensor_type_helpers_base.h",
+ "include/chre/target_platform/power_control_manager_base.h",
+ "include/chre/target_platform/static_nanoapp_init.h",
+ "include/chre/target_platform/system_timer_base.h",
+ "memory.cc",
+ "memory_manager.cc",
+ "platform_debug_dump_manager.cc",
+ "platform_nanoapp.cc",
+ "platform_pal.cc",
+ "power_control_manager.cc",
+ "public",
+ "public/pw_chre/chre.h",
+ "public/pw_chre/host_link.h",
+ "static_nanoapps.cc",
+ "system_time.cc",
+ "system_timer.cc",
+ ],
+)
diff --git a/pw_chre/BUILD.gn b/pw_chre/BUILD.gn
new file mode 100644
index 000000000..76ac8a320
--- /dev/null
+++ b/pw_chre/BUILD.gn
@@ -0,0 +1,153 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/module_config.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/chre/chre.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("disable_warnings") {
+ cflags = [
+ "-Wno-nested-anon-types",
+ "-Wno-gnu-anonymous-struct",
+ "-Wno-thread-safety-analysis",
+ ]
+ visibility = [ ":*" ]
+}
+
+config("public_overrides") {
+ include_dirs = [ "include" ]
+}
+
+pw_source_set("chre_empty_host_link") {
+ sources = [ "chre_empty_host_link.cc" ]
+ deps = [ ":chre" ]
+}
+
+pw_source_set("chre_backend_headers") {
+ public = [
+ "include/chre/target_platform/atomic_base.h",
+ "include/chre/target_platform/atomic_base_impl.h",
+ "include/chre/target_platform/condition_variable_base.h",
+ "include/chre/target_platform/condition_variable_impl.h",
+ "include/chre/target_platform/fatal_error.h",
+ "include/chre/target_platform/host_link_base.h",
+ "include/chre/target_platform/log.h",
+ "include/chre/target_platform/mutex_base.h",
+ "include/chre/target_platform/mutex_base_impl.h",
+ "include/chre/target_platform/platform_nanoapp_base.h",
+ "include/chre/target_platform/platform_sensor_base.h",
+ "include/chre/target_platform/platform_sensor_manager_base.h",
+ "include/chre/target_platform/platform_sensor_type_helpers_base.h",
+ "include/chre/target_platform/power_control_manager_base.h",
+ "include/chre/target_platform/static_nanoapp_init.h",
+ "include/chre/target_platform/system_timer_base.h",
+ ]
+ public_configs = [ ":public_overrides" ]
+ public_deps = [
+ "$dir_pw_chrono:system_timer",
+ "$dir_pw_log",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_sync:timed_thread_notification",
+ ]
+}
+
+pw_source_set("pw_chre_backend") {
+ sources = [
+ "chre_api_re.cc",
+ "context.cc",
+ "host_link.cc",
+ "memory.cc",
+ "memory_manager.cc",
+ "platform_debug_dump_manager.cc",
+ "platform_nanoapp.cc",
+ "platform_pal.cc",
+ "power_control_manager.cc",
+ "system_time.cc",
+ "system_timer.cc",
+ ]
+ deps = [
+ "$dir_pw_string:format",
+ "$dir_pw_third_party/chre:chre_headers",
+ ]
+ public_deps = [
+ ":chre_backend_headers",
+ "$dir_pw_chrono:system_timer",
+ "$dir_pw_log",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_sync:timed_thread_notification",
+ ]
+
+ public_configs = [
+ ":public_include_path",
+ ":public_overrides",
+ ":disable_warnings",
+ ]
+ remove_configs = [ "$dir_pw_build:internal_strict_warnings" ]
+}
+
+pw_source_set("chre_backend") {
+ public_deps = [
+ ":pw_chre_backend",
+ "$dir_pw_third_party/chre:shared_platform",
+ ]
+}
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("chre") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_chre/chre.h",
+ "public/pw_chre/host_link.h",
+ ]
+ sources = [ "chre.cc" ]
+ deps = [ "$dir_pw_third_party/chre" ]
+}
+
+pw_executable("chre_example") {
+ sources = [
+ "example_init.cc",
+ "static_nanoapps.cc",
+ ]
+
+ deps = [
+ ":chre",
+ ":chre_empty_host_link",
+ "$dir_pw_system",
+ "$dir_pw_third_party/chre:example_apps",
+ ]
+}
+
+group("host_example") {
+ deps = [ ":chre_example($dir_pigweed/targets/host_device_simulator:host_device_simulator.speed_optimized)" ]
+}
+
+pw_test_group("tests") {
+ enable_if = dir_pw_third_party_chre != ""
+ tests = [
+ "$dir_pw_third_party/chre:unit_tests",
+ "$dir_pw_third_party/chre:integration_tests",
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_chre/chre.cc b/pw_chre/chre.cc
new file mode 100644
index 000000000..a5fda4947
--- /dev/null
+++ b/pw_chre/chre.cc
@@ -0,0 +1,62 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_chre/chre.h"
+
+#include "chre/core/event_loop.h"
+#include "chre/core/event_loop_manager.h"
+#include "chre/core/host_comms_manager.h"
+#include "chre/core/init.h"
+#include "chre/core/static_nanoapps.h"
+
+namespace pw::chre {
+
+void Init() {
+ ::chre::init();
+ ::chre::EventLoopManagerSingleton::get()->lateInit();
+ ::chre::loadStaticNanoapps();
+}
+
+void Deinit() { ::chre::deinit(); }
+
+void RunEventLoop() {
+ ::chre::EventLoopManagerSingleton::get()->getEventLoop().run();
+}
+
+void StopEventLoop() {
+ ::chre::EventLoopManagerSingleton::get()->getEventLoop().stop();
+}
+
+void SendMessageToNanoapp(pw::chre::NanoappMessage message) {
+ ::chre::HostCommsManager& manager =
+ ::chre::EventLoopManagerSingleton::get()->getHostCommsManager();
+ manager.sendMessageToNanoappFromHost(message.nano_app_id,
+ message.message_type,
+ message.host_endpoint,
+ message.data,
+ message.length);
+}
+
+void FreeMessageToAp(MessageToApContext context) {
+ auto& hostCommsManager =
+ ::chre::EventLoopManagerSingleton::get()->getHostCommsManager();
+ hostCommsManager.onMessageToHostComplete(
+ static_cast<const ::chre::MessageToHost*>(context));
+}
+
+void SetEstimatedHostTimeOffset(int64_t offset) {
+ ::chre::SystemTime::setEstimatedHostTimeOffset(offset);
+}
+
+} // namespace pw::chre
diff --git a/pw_chre/chre_api_re.cc b/pw_chre/chre_api_re.cc
new file mode 100644
index 000000000..5e1ff8410
--- /dev/null
+++ b/pw_chre/chre_api_re.cc
@@ -0,0 +1,48 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/log.h"
+#include "chre/util/macros.h"
+#include "chre_api/chre/re.h"
+#include "pw_log/log.h"
+#include "pw_string/format.h"
+
+namespace {
+int ToPigweedLogLevel(enum chreLogLevel level) {
+ switch (level) {
+ case CHRE_LOG_ERROR:
+ return PW_LOG_LEVEL_ERROR;
+ case CHRE_LOG_WARN:
+ return PW_LOG_LEVEL_WARN;
+ case CHRE_LOG_INFO:
+ return PW_LOG_LEVEL_INFO;
+ case CHRE_LOG_DEBUG:
+ return PW_LOG_LEVEL_DEBUG;
+ }
+}
+} // namespace
+
+DLL_EXPORT void chreLog(enum chreLogLevel level,
+ const char* format_string,
+ ...) {
+ char log[512];
+
+ va_list args;
+ va_start(args, format_string);
+ pw::StatusWithSize status = pw::string::Format(log, format_string, args);
+ PW_ASSERT(status.ok());
+ va_end(args);
+
+ PW_LOG(ToPigweedLogLevel(level), "CHRE", PW_LOG_FLAGS, "%s", log);
+}
diff --git a/pw_chre/chre_empty_host_link.cc b/pw_chre/chre_empty_host_link.cc
new file mode 100644
index 000000000..31a967626
--- /dev/null
+++ b/pw_chre/chre_empty_host_link.cc
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_chre/chre.h"
+#include "pw_chre/host_link.h"
+
+namespace pw::chre {
+
+bool SendMessageToAp(MessageToAp message) {
+ FreeMessageToAp(message.chre_context);
+ return true;
+}
+
+} // namespace pw::chre
diff --git a/pw_chre/context.cc b/pw_chre/context.cc
new file mode 100644
index 000000000..aeaf7b95a
--- /dev/null
+++ b/pw_chre/context.cc
@@ -0,0 +1,21 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/context.h"
+
+namespace chre {
+
+bool inEventLoopThread() { return true; }
+
+} // namespace chre
diff --git a/pw_chre/docs.rst b/pw_chre/docs.rst
new file mode 100644
index 000000000..0fe2af30f
--- /dev/null
+++ b/pw_chre/docs.rst
@@ -0,0 +1,138 @@
+.. _module-pw_chre:
+
+=======
+pw_chre
+=======
+
+.. warning::
+
+ This module is extremely experimental. Parts of this module might be broken,
+ and the module does not provide a stable API.
+
+The `Context Hub Runtime Environment <https://source.android.com/docs/core/interaction/contexthub>`_
+(CHRE) is Android's platform for developing always-on applications called
+nanoapps. These nanoapps run on a vendor-specific processor which is more power
+efficient. Nanoapps use the CHRE API, which is standardized across platforms,
+allowing them to be code-compatible across devices.
+
+This module implements a Pigweed backend to CHRE. In order to use this module,
+``dir_pw_third_party_chre`` must point to the directory to the CHRE library.
+
+-----------
+Get started
+-----------
+
+To integrate ``pw_chre`` with your project:
+
+- Call the initialization functions and start the event loop.
+- Handle messages from the application processor and connect them through to
+ the CHRE runtime.
+- Implement the functions in ``pw_chre/host_link``. This is how CHRE sends
+ messages to the application processor.
+
+
+``$pw_chre:chre_example`` runs the CHRE environment using ``pw_system``.
+This also loads several static example nanoapps from the CHRE codebase by
+compiling them into the executable. This can be a helpful reference.
+
+CHRE is implemented using the following Pigweed modules for functionality:
+
+- ``pw_chrono:system_timer``: implements getting monotonic time
+- ``pw_log``: implements logging to the application processor
+- ``pw_assert``: implements crash handling
+- ``pw_sync``: implements mutual exclusion primitives
+- ``malloc/free``: implements virtual memory allocation
+ (This may be eventually replaced with a pigweed module)
+
+----------------------
+Current implementation
+----------------------
+
+As mentioned at the top of this document, ``pw_chre`` is extremely experimental.
+Only a few parts of CHRE have been tested. There are likely to be bugs and
+unimplemented behavior. The lists below track the current state and will
+be updated as things change.
+
+Supported and tested behavior:
+
+- Loading static nanoapps.
+- The following sample nanoapps have been run:
+ - hello_world
+ - debug_dump_world
+ - timer_world
+ - unload_tester
+ - message_world
+ - spammer
+- Logging from a nanoapp.
+- Allocating memory (although it uses malloc/free).
+- Sending messages to/from the AP.
+
+Features not implemented, but likely to be implemented in the future:
+
+- Context Hub Qualification Test Suite (CHQTS).
+- Some simulated PALS for testing (based off of CHRE's linux simulated PALs).
+- Power Management APIs, e.g: waking the host AP and flushing messages.
+- Instructions around implementing a PAL.
+- Instructions around building and testing a nanoapp.
+- Dynamically loading nanoapps.
+
+Features that would be nice to have:
+
+- A plug-and-play implementation of AP <-> MCU flatbuffer message communication.
+- Pigweed defined facades for each PAL.
+- PAL implementations using Pigweed functionality (i.e: implementing bluetooth
+ via ``pw_bluetooth``).
+- Pigweed defined facades for core CHRE functionality, such as clock selection,
+ memory management, cache management.
+- A protobuf implementation of CHRE's flatbuffer API.
+- Cmake and Bazel build system integration.
+
+-------------
+API reference
+-------------
+.. doxygennamespace:: pw::chre
+ :members:
+
+-------------
+Porting Guide
+-------------
+The ``pw_chre`` module has completed the steps outlined for `creating a new CHRE platform`_ .
+
+.. _Creating a new CHRE platform: https://android.googlesource.com/platform/system/chre/+/refs/heads/main/doc/porting_guide.md#recommended-steps-for-porting-chre
+
+The ``pw_chre`` module still needs to be configured correctly on a new platform.
+You ``pw_chre`` user is responsible for:
+
+- Starting a thread for CHRE's event loop and calling the correct APIs.
+- Forwarding messages to/from the Application Processor (AP).
+
+-----------------------------
+Adding Optional Feature Areas
+-----------------------------
+However, ``pw_chre`` users will likely want to implement their own
+Platform Abstraction Layers (PALs). For more information see this
+`implementation guide <https://android.googlesource.com/platform/system/chre/+/refs/heads/main/doc/porting_guide.md#implementing-optional-feature-areas-e_g_pals>`_.
+
+.. list-table:: List of PALs
+ :widths: 1 1
+ :header-rows: 1
+
+ * - PAL Name
+ - Pigweed implementation available
+ * - Audio
+ - ❌
+ * - Bluetooth
+ - ❌
+ * - GNSS
+ - ❌
+ * - Sensor
+ - ❌
+ * - Wifi
+ - ❌
+ * - WWAN
+ - ❌
+
+
+For more information on a specific PAL see
+`the PAL headers <https://cs.android.com/android/platform/superproject/+/main:system/chre/pal/include/chre/pal/>`_
+or the `Linux reference PAL implementations <https://cs.android.com/android/platform/superproject/+/main:system/chre/platform/linux/>`_.
diff --git a/pw_chre/example_init.cc b/pw_chre/example_init.cc
new file mode 100644
index 000000000..5bd435cdb
--- /dev/null
+++ b/pw_chre/example_init.cc
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_chre/chre.h"
+#include "pw_system/target_hooks.h"
+#include "pw_thread/thread.h"
+
+namespace pw::system {
+namespace {
+
+pw::thread::Thread chre_thread;
+
+} // namespace
+
+// This will run once after pw::system::Init() completes. This callback must
+// return or it will block the work queue.
+void UserAppInit() {
+ // Start the thread that is running CHRE.
+ chre_thread = pw::thread::Thread(
+ pw::system::LogThreadOptions(),
+ [](void*) {
+ pw::chre::Init();
+ pw::chre::RunEventLoop();
+ pw::chre::Deinit();
+ },
+ nullptr);
+}
+
+} // namespace pw::system
diff --git a/pw_chre/host_link.cc b/pw_chre/host_link.cc
new file mode 100644
index 000000000..51d7749f8
--- /dev/null
+++ b/pw_chre/host_link.cc
@@ -0,0 +1,45 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/host_link.h"
+
+#include <algorithm>
+
+#include "chre/core/host_comms_manager.h"
+#include "public/pw_chre/host_link.h"
+#include "pw_chre/host_link.h"
+
+namespace chre {
+
+// TODO: b/301477662 - Implement these, possibly by adding a facade.
+void HostLink::flushMessagesSentByNanoapp(uint64_t) {}
+
+bool HostLink::sendMessage(const MessageToHost* message) {
+ pw::chre::MessageToAp pw_message{
+ .nanoapp_id = message->appId,
+ .message_type = message->toHostData.messageType,
+ .app_permissions = message->toHostData.appPermissions,
+ .message_permissions = message->toHostData.messagePermissions,
+ .host_endpoint = message->toHostData.hostEndpoint,
+ .woke_host = message->toHostData.wokeHost,
+ .data = message->message.data(),
+ .length = message->message.size(),
+ .chre_context = message,
+ };
+ return pw::chre::SendMessageToAp(std::move(pw_message));
+}
+
+void HostLinkBase::sendNanConfiguration(bool) {}
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/atomic_base.h b/pw_chre/include/chre/target_platform/atomic_base.h
new file mode 100644
index 000000000..3462671c0
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/atomic_base.h
@@ -0,0 +1,30 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <atomic>
+
+namespace chre {
+
+template <typename AtomicType>
+class AtomicBase {
+ protected:
+ std::atomic<AtomicType> atomic_;
+};
+
+typedef AtomicBase<bool> AtomicBoolBase;
+typedef AtomicBase<uint32_t> AtomicUint32Base;
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/atomic_base_impl.h b/pw_chre/include/chre/target_platform/atomic_base_impl.h
new file mode 100644
index 000000000..3979a52d6
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/atomic_base_impl.h
@@ -0,0 +1,63 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "chre/platform/atomic.h"
+
+namespace chre {
+
+inline AtomicBool::AtomicBool(bool starting_value) {
+ std::atomic_init(&atomic_, starting_value);
+}
+
+inline bool AtomicBool::operator=(bool desired) { return atomic_ = desired; }
+
+inline bool AtomicBool::load() const { return atomic_.load(); }
+
+inline void AtomicBool::store(bool desired) { atomic_.store(desired); }
+
+inline bool AtomicBool::exchange(bool desired) {
+ return atomic_.exchange(desired);
+}
+
+inline AtomicUint32::AtomicUint32(uint32_t startingValue) {
+ std::atomic_init(&atomic_, startingValue);
+}
+
+inline uint32_t AtomicUint32::operator=(uint32_t desired) {
+ return atomic_ = desired;
+}
+
+inline uint32_t AtomicUint32::load() const { return atomic_.load(); }
+
+inline void AtomicUint32::store(uint32_t desired) { atomic_.store(desired); }
+
+inline uint32_t AtomicUint32::exchange(uint32_t desired) {
+ return atomic_.exchange(desired);
+}
+
+inline uint32_t AtomicUint32::fetch_add(uint32_t arg) {
+ return atomic_.fetch_add(arg);
+}
+
+inline uint32_t AtomicUint32::fetch_increment() { return atomic_.fetch_add(1); }
+
+inline uint32_t AtomicUint32::fetch_sub(uint32_t arg) {
+ return atomic_.fetch_sub(arg);
+}
+
+inline uint32_t AtomicUint32::fetch_decrement() { return atomic_.fetch_sub(1); }
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/condition_variable_base.h b/pw_chre/include/chre/target_platform/condition_variable_base.h
new file mode 100644
index 000000000..f40ab3004
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/condition_variable_base.h
@@ -0,0 +1,26 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "pw_sync/timed_thread_notification.h"
+
+namespace chre {
+
+class ConditionVariableBase {
+ protected:
+ pw::sync::TimedThreadNotification notification_;
+};
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/condition_variable_impl.h b/pw_chre/include/chre/target_platform/condition_variable_impl.h
new file mode 100644
index 000000000..2d826ec06
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/condition_variable_impl.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+#include "chre/platform/condition_variable.h"
+#include "pw_chrono/system_clock.h"
+
+namespace chre {
+
+inline ConditionVariable::ConditionVariable() {}
+
+inline ConditionVariable::~ConditionVariable() {}
+
+inline void ConditionVariable::notify_one() { notification_.release(); }
+
+inline void ConditionVariable::wait(Mutex& mutex) {
+ mutex.unlock();
+ notification_.acquire();
+ mutex.lock();
+}
+
+inline bool ConditionVariable::wait_for(Mutex& mutex, Nanoseconds timeout) {
+ mutex.unlock();
+ auto pw_timeout = pw::chrono::SystemClock::for_at_least(
+ std::chrono::nanoseconds(timeout.toRawNanoseconds()));
+ bool did_acquire = notification_.try_acquire_for(pw_timeout);
+ mutex.lock();
+ return did_acquire;
+}
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/fatal_error.h b/pw_chre/include/chre/target_platform/fatal_error.h
new file mode 100644
index 000000000..5b236f613
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/fatal_error.h
@@ -0,0 +1,21 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+#include "pw_assert/assert.h"
+
+#define FATAL_ERROR_QUIT() \
+ do { \
+ PW_ASSERT(false); \
+ } while (0)
diff --git a/pw_chre/include/chre/target_platform/host_link_base.h b/pw_chre/include/chre/target_platform/host_link_base.h
new file mode 100644
index 000000000..6faea634b
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/host_link_base.h
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+namespace chre {
+
+// TODO: b/301477662 - This class will likely need a facade since this will be
+// implemented in downstream products.
+class HostLinkBase {
+ public:
+ void sendNanConfiguration(bool enable);
+};
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/log.h b/pw_chre/include/chre/target_platform/log.h
new file mode 100644
index 000000000..58aad2907
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/log.h
@@ -0,0 +1,24 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <cstddef>
+
+#include "pw_log/log.h"
+
+#define LOGW(...) PW_LOG_WARN(__VA_ARGS__)
+#define LOGE(...) PW_LOG_ERROR(__VA_ARGS__)
+#define LOGI(...) PW_LOG_INFO(__VA_ARGS__)
+#define LOGD(...) PW_LOG_DEBUG(__VA_ARGS__)
diff --git a/pw_chre/include/chre/target_platform/mutex_base.h b/pw_chre/include/chre/target_platform/mutex_base.h
new file mode 100644
index 000000000..b5e42b009
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/mutex_base.h
@@ -0,0 +1,20 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+#include "pw_sync/mutex.h"
+
+struct MutexBase {
+ pw::sync::Mutex mutex_;
+};
diff --git a/pw_chre/include/chre/target_platform/mutex_base_impl.h b/pw_chre/include/chre/target_platform/mutex_base_impl.h
new file mode 100644
index 000000000..215813861
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/mutex_base_impl.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "chre/platform/mutex.h"
+
+namespace chre {
+
+inline Mutex::Mutex() {}
+
+inline Mutex::~Mutex() {}
+
+inline void Mutex::lock() { mutex_.lock(); }
+
+inline bool Mutex::try_lock() { return mutex_.try_lock(); }
+
+inline void Mutex::unlock() { mutex_.unlock(); }
+
+} // namespace chre
diff --git a/pw_polyfill/cstddef_public_overrides/cstddef b/pw_chre/include/chre/target_platform/platform_nanoapp_base.h
index b0cff3b14..6f12d1e4a 100644
--- a/pw_polyfill/cstddef_public_overrides/cstddef
+++ b/pw_chre/include/chre/target_platform/platform_nanoapp_base.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -11,12 +11,19 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+
#pragma once
-static_assert(__cplusplus < 201703L,
- "The <cstddef> polyfill header is only intended for C++14. "
- "It cannot be used when building with C++17 or newer.");
+#include "chre/platform/shared/nanoapp_support_lib_dso.h"
+
+namespace chre {
+
+class PlatformNanoappBase {
+ public:
+ void loadStatic(const struct chreNslNanoappInfo* app_info);
-#include_next <cstddef>
+ protected:
+ const struct chreNslNanoappInfo* app_info_ = nullptr;
+};
-#include "pw_polyfill/standard_library/cstddef.h"
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/platform_sensor_base.h b/pw_chre/include/chre/target_platform/platform_sensor_base.h
new file mode 100644
index 000000000..d87095de7
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/platform_sensor_base.h
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+#include "chre_api/chre/sensor.h"
+
+namespace chre {
+
+class PlatformSensorBase {
+ public:
+ void initBase(const struct chreSensorInfo* sensor_info,
+ uint32_t sensor_handle) {
+ sensor_info_ = sensor_info;
+ sensor_handle_ = sensor_handle;
+ }
+
+ void setSensorInfo(const struct chreSensorInfo* sensor_info) {
+ sensor_info_ = sensor_info;
+ }
+
+ void setSensorHandle(uint32_t sensor_handle) {
+ sensor_handle_ = sensor_handle;
+ }
+
+ uint32_t getSensorHandle() const { return sensor_handle_; }
+
+ protected:
+ const struct chreSensorInfo* sensor_info_;
+ uint32_t sensor_handle_;
+};
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/platform_sensor_manager_base.h b/pw_chre/include/chre/target_platform/platform_sensor_manager_base.h
new file mode 100644
index 000000000..1c425bdef
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/platform_sensor_manager_base.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+#include <cstdint>
+
+#include "chre/pal/sensor.h"
+
+namespace chre {
+
+class PlatformSensorManagerBase {
+ public:
+ // Note: these are load bearing names
+ static const chrePalSensorCallbacks sSensorCallbacks;
+ const chrePalSensorApi* mSensorApi;
+
+ private:
+ static void samplingStatusUpdateCallback(
+ uint32_t sensor_handle, struct chreSensorSamplingStatus* status);
+ static void dataEventCallback(uint32_t sensor_handle, void* data);
+ static void biasEventCallback(uint32_t sensor_handle, void* bias_data);
+ static void flushCompleteCallback(uint32_t sensor_handle,
+ uint32_t flush_request_id,
+ uint8_t error_code);
+};
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/platform_sensor_type_helpers_base.h b/pw_chre/include/chre/target_platform/platform_sensor_type_helpers_base.h
new file mode 100644
index 000000000..072f88678
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/platform_sensor_type_helpers_base.h
@@ -0,0 +1,20 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+namespace chre {
+
+class PlatformSensorTypeHelpersBase {};
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/power_control_manager_base.h b/pw_chre/include/chre/target_platform/power_control_manager_base.h
new file mode 100644
index 000000000..c1d00d376
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/power_control_manager_base.h
@@ -0,0 +1,21 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+namespace chre {
+
+class PowerControlManagerBase {};
+
+} // namespace chre
diff --git a/pw_chre/include/chre/target_platform/static_nanoapp_init.h b/pw_chre/include/chre/target_platform/static_nanoapp_init.h
new file mode 100644
index 000000000..6f2c09119
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/static_nanoapp_init.h
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "chre/core/static_nanoapps.h"
+#include "chre/platform/fatal_error.h"
+#include "chre/platform/shared/nanoapp_support_lib_dso.h"
+
+#define CHRE_STATIC_NANOAPP_INIT(appName, appId_, appVersion_, appPerms) \
+ namespace chre { \
+ \
+ UniquePtr<Nanoapp> initializeStaticNanoapp##appName() { \
+ UniquePtr<Nanoapp> nanoapp = MakeUnique<Nanoapp>(); \
+ static struct chreNslNanoappInfo appInfo; \
+ appInfo.magic = CHRE_NSL_NANOAPP_INFO_MAGIC; \
+ appInfo.structMinorVersion = CHRE_NSL_NANOAPP_INFO_STRUCT_MINOR_VERSION; \
+ appInfo.targetApiVersion = CHRE_API_VERSION; \
+ appInfo.vendor = "Google"; \
+ appInfo.name = #appName; \
+ appInfo.isSystemNanoapp = true; \
+ appInfo.isTcmNanoapp = false; \
+ appInfo.appId = appId_; \
+ appInfo.appVersion = appVersion_; \
+ appInfo.entryPoints.start = nanoappStart; \
+ appInfo.entryPoints.handleEvent = nanoappHandleEvent; \
+ appInfo.entryPoints.end = nanoappEnd; \
+ appInfo.appVersionString = "<undefined>"; \
+ appInfo.appPermissions = appPerms; \
+ if (nanoapp.isNull()) { \
+ FATAL_ERROR("Failed to allocate nanoapp " #appName); \
+ } else { \
+ nanoapp->loadStatic(&appInfo); \
+ } \
+ \
+ return nanoapp; \
+ } \
+ \
+ } // namespace chre
diff --git a/pw_chre/include/chre/target_platform/system_timer_base.h b/pw_chre/include/chre/target_platform/system_timer_base.h
new file mode 100644
index 000000000..e8cb288c0
--- /dev/null
+++ b/pw_chre/include/chre/target_platform/system_timer_base.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <cinttypes>
+
+#include "pw_chrono/system_timer.h"
+
+namespace chre {
+
+class SystemTimerBase {
+ public:
+ SystemTimerBase()
+ : timer_([this](pw::chrono::SystemClock::time_point) {
+ this->OnExpired();
+ }) {}
+
+ protected:
+ void OnExpired();
+
+ bool is_active_ = false;
+ bool initialized_ = false;
+ pw::chrono::SystemTimer timer_;
+};
+
+} // namespace chre
diff --git a/pw_chre/memory.cc b/pw_chre/memory.cc
new file mode 100644
index 000000000..5aaddf90d
--- /dev/null
+++ b/pw_chre/memory.cc
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/memory.h"
+
+#include <cstdlib>
+
+namespace chre {
+
+// TODO: b/301477067 - Today these APIs call the system malloc and free, but
+// they should be updated to use pw_allocator.
+void* memoryAlloc(size_t size) { return malloc(size); }
+
+void* palSystemApiMemoryAlloc(size_t size) { return malloc(size); }
+
+void memoryFree(void* pointer) { free(pointer); }
+
+void palSystemApiMemoryFree(void* pointer) { free(pointer); }
+
+} // namespace chre
diff --git a/pw_chre/memory_manager.cc b/pw_chre/memory_manager.cc
new file mode 100644
index 000000000..0d3786cf9
--- /dev/null
+++ b/pw_chre/memory_manager.cc
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/memory_manager.h"
+
+#include "chre/util/memory.h"
+
+namespace chre {
+
+void* MemoryManager::doAlloc(Nanoapp*, uint32_t size) {
+ return chre::memoryAlloc(size);
+}
+
+void MemoryManager::doFree(Nanoapp*, void* ptr) { chre::memoryFree(ptr); }
+
+} // namespace chre
diff --git a/pw_chre/platform_debug_dump_manager.cc b/pw_chre/platform_debug_dump_manager.cc
new file mode 100644
index 000000000..af6b49778
--- /dev/null
+++ b/pw_chre/platform_debug_dump_manager.cc
@@ -0,0 +1,26 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "chre/platform/platform_debug_dump_manager.h"
+
+namespace chre {
+
+PlatformDebugDumpManagerBase::PlatformDebugDumpManagerBase() {}
+
+PlatformDebugDumpManagerBase::~PlatformDebugDumpManagerBase() {}
+
+// TODO: b/301477662 - Implement these.
+void PlatformDebugDumpManager::sendDebugDump(const char*, bool) {}
+void PlatformDebugDumpManager::logStateToBuffer(DebugDumpWrapper&) {}
+
+} // namespace chre
diff --git a/pw_chre/platform_nanoapp.cc b/pw_chre/platform_nanoapp.cc
new file mode 100644
index 000000000..5c9076901
--- /dev/null
+++ b/pw_chre/platform_nanoapp.cc
@@ -0,0 +1,77 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "chre/platform/platform_nanoapp.h"
+
+#include "chre/util/system/napp_permissions.h"
+#include "chre_api/chre/version.h"
+
+namespace chre {
+
+PlatformNanoapp::~PlatformNanoapp() {}
+
+bool PlatformNanoapp::start() { return app_info_->entryPoints.start(); }
+
+void PlatformNanoapp::handleEvent(uint32_t SenderInstanceId,
+ uint16_t eventType,
+ const void* eventData) {
+ app_info_->entryPoints.handleEvent(SenderInstanceId, eventType, eventData);
+}
+
+void PlatformNanoapp::end() { app_info_->entryPoints.end(); }
+
+uint64_t PlatformNanoapp::getAppId() const {
+ return (app_info_ == nullptr) ? 0 : app_info_->appId;
+}
+
+uint32_t PlatformNanoapp::getAppVersion() const {
+ return app_info_->appVersion;
+}
+
+uint32_t PlatformNanoapp::getTargetApiVersion() const {
+ return CHRE_API_VERSION;
+}
+
+const char* PlatformNanoapp::getAppName() const {
+ return (app_info_ != nullptr) ? app_info_->name : "Unknown";
+}
+
+bool PlatformNanoapp::supportsAppPermissions() const {
+ return (app_info_ != nullptr) ? (app_info_->structMinorVersion >=
+ CHRE_NSL_NANOAPP_INFO_STRUCT_MINOR_VERSION)
+ : false;
+}
+
+uint32_t PlatformNanoapp::getAppPermissions() const {
+ return (supportsAppPermissions())
+ ? app_info_->appPermissions
+ : static_cast<uint32_t>(chre::NanoappPermissions::CHRE_PERMS_NONE);
+}
+
+bool PlatformNanoapp::isSystemNanoapp() const {
+ return (app_info_ != nullptr && app_info_->isSystemNanoapp);
+}
+
+void PlatformNanoapp::logStateToBuffer(DebugDumpWrapper& debugDump) const {
+ if (!app_info_) {
+ return;
+ }
+ debugDump.print("%s: %s", app_info_->name, app_info_->vendor);
+}
+
+void PlatformNanoappBase::loadStatic(
+ const struct chreNslNanoappInfo* app_info) {
+ app_info_ = app_info;
+}
+
+} // namespace chre
diff --git a/pw_chre/platform_pal.cc b/pw_chre/platform_pal.cc
new file mode 100644
index 000000000..3003a2374
--- /dev/null
+++ b/pw_chre/platform_pal.cc
@@ -0,0 +1,23 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/shared/platform_pal.h"
+
+namespace chre {
+
+// TODO: b/301477951 - Implement this when we have PALs implemented. It is
+// unclear without more examples what this API would be used for.
+void PlatformPal::prePalApiCall(PalType) const {}
+
+} // namespace chre
diff --git a/pw_chre/power_control_manager.cc b/pw_chre/power_control_manager.cc
new file mode 100644
index 000000000..6d45ea058
--- /dev/null
+++ b/pw_chre/power_control_manager.cc
@@ -0,0 +1,24 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "chre/platform/power_control_manager.h"
+
+namespace chre {
+
+// TODO: b/301477662 - Implement these, possibly by adding a facade.
+void PowerControlManager::preEventLoopProcess(size_t) {}
+void PowerControlManager::postEventLoopProcess(size_t) {}
+bool PowerControlManager::hostIsAwake() { return true; }
+
+} // namespace chre
diff --git a/pw_chre/public/pw_chre/chre.h b/pw_chre/public/pw_chre/chre.h
new file mode 100644
index 000000000..69f6f81d2
--- /dev/null
+++ b/pw_chre/public/pw_chre/chre.h
@@ -0,0 +1,71 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "pw_chre/host_link.h"
+
+namespace pw::chre {
+
+/// A message to be sent to a CHRE nanoapp.
+/// This message originated from the Application Processor (AP).
+struct NanoappMessage {
+ /// The id of the nanoapp this message is sent to.
+ uint64_t nano_app_id;
+ /// The type of message this is.
+ uint32_t message_type;
+ /// The id of the host on the AP that sent this request.
+ uint16_t host_endpoint;
+ /// The actual message data.
+ const uint8_t* data;
+ /// The size in bytes of the message data.
+ size_t length;
+};
+
+/// Initialize the CHRE environment and load any static nanoapps that exist.
+/// This must be called before the event loop has been started.
+void Init();
+
+/// Teardown the CHRE environment.
+/// This must be called after Init and after the event loop has been stopped.
+void Deinit();
+
+/// Run the CHRE event loop.
+/// This function will not return until `StopEventLoop` is called.
+void RunEventLoop();
+
+/// Stop the CHRE event loop.
+/// This can be called from any thread.
+void StopEventLoop();
+
+/// Send a message to a nano app.
+/// This can be called from any thread.
+/// @param[in] message The message being send to the nano app.
+void SendMessageToNanoapp(NanoappMessage message);
+
+/// Free a message that CHRE created to send to the AP (via `SendMessageToAp`).
+/// This function must be called after the message is finishd being used.
+/// After this function is called, the message data must not be accessed.
+/// This can be called from any thread.
+/// @param[in] context The message being freed.
+void FreeMessageToAp(MessageToApContext context);
+
+/// Set the estimated offset between the AP time and CHRE's time.
+/// @param[in] offset The offset time in nanoseconds.
+void SetEstimatedHostTimeOffset(int64_t offset);
+
+} // namespace pw::chre
diff --git a/pw_chre/public/pw_chre/host_link.h b/pw_chre/public/pw_chre/host_link.h
new file mode 100644
index 000000000..ca4e2cc73
--- /dev/null
+++ b/pw_chre/public/pw_chre/host_link.h
@@ -0,0 +1,67 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+// These host link functions should be implemented by the system integrator.
+namespace pw::chre {
+
+/// This is a token representing a message that CHRE allocated.
+/// It must be passed to `FreeMessageToAp` when the message is finished.
+typedef const void* MessageToApContext;
+
+/// This is a message that should be sent to the AP.
+/// It was allocated by CHRE, so pw::chre::FreeMessageToAp should be called
+/// in order to free it.
+struct MessageToAp {
+ /// The id of the nanoapp sending the message.
+ uint64_t nanoapp_id;
+
+ /// The type of the message.
+ uint32_t message_type;
+
+ uint32_t app_permissions;
+ uint32_t message_permissions;
+
+ /// The id of the client that this message should be delivered to on the host.
+ uint16_t host_endpoint;
+
+ /// Whether CHRE is responsible for waking the AP.
+ /// If this is true, then the client must wake the AP in
+ /// `SendMessageToAp` before sending this message.
+ bool woke_host;
+
+ /// The underlying data of the message. This is owned by `chre_context` and
+ /// should not be accessed after the message has been freed.
+ const uint8_t* data;
+
+ /// The length of `data` in bytes.
+ size_t length;
+
+ /// The context of the message, used to free the message when the client is
+ /// finished sending it.
+ MessageToApContext chre_context;
+};
+
+/// CHRE calls this method to send a message to the Application Processor (AP).
+/// The client must implement this method, and the client is responsible for
+/// calling `FreeMessageToAp` once they are finished with the message.
+/// @param[in] message The message to be sent.
+/// @param[out] bool Whether this method was successful.
+bool SendMessageToAp(MessageToAp message);
+
+} // namespace pw::chre
diff --git a/pw_chre/static_nanoapps.cc b/pw_chre/static_nanoapps.cc
new file mode 100644
index 000000000..8612b7586
--- /dev/null
+++ b/pw_chre/static_nanoapps.cc
@@ -0,0 +1,34 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "chre/core/static_nanoapps.h"
+
+#include "chre/apps/apps.h"
+#include "chre/util/macros.h"
+
+namespace chre {
+
+const StaticNanoappInitFunction kStaticNanoappList[] = {
+ initializeStaticNanoappHelloWorld,
+ initializeStaticNanoappMessageWorld,
+#if defined(INCLUDE_SENSOR_APP)
+ initializeStaticNanoappSensorWorld,
+#endif
+ initializeStaticNanoappSpammer,
+ initializeStaticNanoappTimerWorld,
+ initializeStaticNanoappUnloadTester,
+};
+
+const size_t kStaticNanoappCount = ARRAY_SIZE(kStaticNanoappList);
+
+} // namespace chre
diff --git a/pw_chre/system_time.cc b/pw_chre/system_time.cc
new file mode 100644
index 000000000..9a9df9a61
--- /dev/null
+++ b/pw_chre/system_time.cc
@@ -0,0 +1,45 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "chre/platform/system_time.h"
+
+#include "chre/platform/assert.h"
+#include "chre/platform/log.h"
+#include "pw_chrono/system_clock.h"
+
+namespace chre {
+
+namespace {
+
+int64_t estimated_host_time_offset = 0;
+
+}
+
+Nanoseconds SystemTime::getMonotonicTime() {
+ const pw::chrono::SystemClock::time_point now =
+ pw::chrono::SystemClock::now();
+ auto nsecs = std::chrono::duration_cast<std::chrono::nanoseconds>(
+ now.time_since_epoch())
+ .count();
+ return Nanoseconds(static_cast<uint64_t>(nsecs));
+}
+
+int64_t SystemTime::getEstimatedHostTimeOffset() {
+ return estimated_host_time_offset;
+}
+
+void SystemTime::setEstimatedHostTimeOffset(int64_t offset) {
+ estimated_host_time_offset = offset;
+}
+
+} // namespace chre
diff --git a/pw_chre/system_timer.cc b/pw_chre/system_timer.cc
new file mode 100644
index 000000000..b178815be
--- /dev/null
+++ b/pw_chre/system_timer.cc
@@ -0,0 +1,67 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "chre/platform/system_timer.h"
+
+#include "chre/platform/log.h"
+#include "chre/util/time.h"
+
+namespace chre {
+
+void SystemTimerBase::OnExpired() {
+ SystemTimer* timer = static_cast<SystemTimer*>(this);
+ timer->mCallback(timer->mData);
+}
+
+SystemTimer::SystemTimer() {}
+
+SystemTimer::~SystemTimer() {
+ if (!initialized_) {
+ return;
+ }
+ cancel();
+ initialized_ = false;
+}
+
+bool SystemTimer::init() {
+ initialized_ = true;
+ return initialized_;
+}
+
+bool SystemTimer::set(SystemTimerCallback* callback,
+ void* data,
+ Nanoseconds delay) {
+ if (!initialized_) {
+ return false;
+ }
+ mCallback = callback;
+ mData = data;
+ pw::chrono::SystemClock::duration interval =
+ std::chrono::nanoseconds(delay.toRawNanoseconds());
+ const pw::chrono::SystemClock::time_point now =
+ pw::chrono::SystemClock::now();
+ timer_.InvokeAt(now + interval);
+ return true;
+}
+
+bool SystemTimer::cancel() {
+ if (!initialized_) {
+ return false;
+ }
+ timer_.Cancel();
+ return true;
+}
+
+bool SystemTimer::isActive() { return is_active_; }
+
+} // namespace chre
diff --git a/pw_chrono/Android.bp b/pw_chrono/Android.bp
new file mode 100644
index 000000000..9f61aa8be
--- /dev/null
+++ b/pw_chrono/Android.bp
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_chrono_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+cc_defaults {
+ name: "pw_chrono_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_chrono_include_dirs",
+ ],
+
+ export_header_lib_headers: [
+ "pw_chrono_include_dirs",
+ ],
+}
diff --git a/pw_chrono/BUILD.bazel b/pw_chrono/BUILD.bazel
index 172abf850..48285428f 100644
--- a/pw_chrono/BUILD.bazel
+++ b/pw_chrono/BUILD.bazel
@@ -12,14 +12,14 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_facade",
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
-load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -51,15 +51,21 @@ pw_cc_library(
srcs = [
"system_clock.cc",
],
+ hdrs = [
+ "public/pw_chrono/internal/system_clock_macros.h",
+ "public/pw_chrono/system_clock.h",
+ ],
+ includes = ["public"],
deps = [
- ":system_clock_facade",
- "@pigweed_config//:pw_chrono_system_clock_backend",
+ ":epoch",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_chrono_system_clock_backend",
],
)
pw_cc_library(
name = "system_clock_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:freertos": ["//pw_chrono_freertos:system_clock"],
"//pw_build/constraints/rtos:embos": ["//pw_chrono_embos:system_clock"],
@@ -84,13 +90,13 @@ pw_cc_library(
name = "system_timer",
deps = [
":system_timer_facade",
- "@pigweed_config//:pw_chrono_system_timer_backend",
+ "@pigweed//targets:pw_chrono_system_timer_backend",
],
)
pw_cc_library(
name = "system_timer_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:freertos": ["//pw_chrono_freertos:system_timer"],
"//pw_build/constraints/rtos:embos": ["//pw_chrono_embos:system_timer"],
@@ -107,9 +113,13 @@ proto_library(
strip_import_prefix = "/pw_chrono",
)
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "chrono_proto_pb2",
+ # Once b/241456982 is fixed srcs here should be replaced with:
+ # deps = [ ":chrono_proto" ]
srcs = ["chrono.proto"],
+ tags = ["manual"],
)
pw_proto_library(
diff --git a/pw_chrono/public/pw_chrono/system_clock.h b/pw_chrono/public/pw_chrono/system_clock.h
index 72035fb56..360241e0b 100644
--- a/pw_chrono/public/pw_chrono/system_clock.h
+++ b/pw_chrono/public/pw_chrono/system_clock.h
@@ -102,8 +102,9 @@ struct SystemClock {
/// The now() function can be invoked at any time.
static constexpr bool is_always_enabled = true;
- /// The now() function may work in non-masking interrupts, depending on the
- /// backend. This must be provided by the backend.
+ /// The now() function may work in non-maskable interrupt contexts (e.g.
+ /// exception/fault handlers), depending on the backend. This must be provided
+ /// by the backend.
static constexpr bool is_nmi_safe = backend::kSystemClockNmiSafe;
/// This is thread and IRQ safe. This must be provided by the backend.
diff --git a/pw_chrono/py/BUILD.bazel b/pw_chrono/py/BUILD.bazel
index 424250125..de8fab13a 100644
--- a/pw_chrono/py/BUILD.bazel
+++ b/pw_chrono/py/BUILD.bazel
@@ -20,8 +20,11 @@ py_library(
"pw_chrono/__init__.py",
"pw_chrono/timestamp_analyzer.py",
],
+ imports = ["."],
+ tags = ["manual"],
deps = [
- "//pw_chrono:chrono_proto_pb2",
+ # TODO: b/241456982 - Add this dep back in
+ # "//pw_chrono:chrono_proto_pb2",
],
)
@@ -30,8 +33,10 @@ py_test(
srcs = [
"timestamp_analyzer_test.py",
],
+ tags = ["manual"],
deps = [
":pw_chrono",
- "//pw_chrono:chrono_proto_pb2",
+ # TODO: b/241456982 - Add this dep back in
+ # "//pw_chrono:chrono_proto_pb2",
],
)
diff --git a/pw_chrono_embos/BUILD.bazel b/pw_chrono_embos/BUILD.bazel
index 7d176b21b..1a6549bf7 100644
--- a/pw_chrono_embos/BUILD.bazel
+++ b/pw_chrono_embos/BUILD.bazel
@@ -22,7 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "system_clock_headers",
+ name = "system_clock",
+ srcs = [
+ "system_clock.cc",
+ ],
hdrs = [
"public/pw_chrono_embos/config.h",
"public/pw_chrono_embos/system_clock_config.h",
@@ -36,27 +39,18 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
+ # TODO: b/290366523 - This should depend on embOS.
deps = [
"//pw_chrono:epoch",
+ "//pw_chrono:system_clock_facade",
],
)
pw_cc_library(
- name = "system_clock",
+ name = "system_timer",
srcs = [
- "system_clock.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:embos",
- ],
- deps = [
- ":system_clock_headers",
- "//pw_chrono:system_clock_facade",
+ "system_timer.cc",
],
-)
-
-pw_cc_library(
- name = "system_timer_headers",
hdrs = [
"public/pw_chrono_embos/system_timer_inline.h",
"public/pw_chrono_embos/system_timer_native.h",
@@ -67,27 +61,15 @@ pw_cc_library(
"public",
"public_overrides",
],
- deps = [
- "//pw_chrono:system_clock",
- "//pw_function",
- "//pw_chrono:system_timer_facade",
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
- ],
-)
-
-pw_cc_library(
- name = "system_timer",
- srcs = [
- "system_timer.cc",
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:embos",
],
+ # TODO: b/290366523 - This should depend on embOS.
deps = [
- ":system_timer_headers",
- "//pw_chrono:system_timer_facade",
- ":system_clock",
"//pw_assert",
+ "//pw_chrono:system_clock",
+ "//pw_chrono:system_timer_facade",
+ "//pw_function",
"//pw_interrupt:context",
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
],
)
diff --git a/pw_chrono_freertos/BUILD.bazel b/pw_chrono_freertos/BUILD.bazel
index 60c9eee0c..7bc74d958 100644
--- a/pw_chrono_freertos/BUILD.bazel
+++ b/pw_chrono_freertos/BUILD.bazel
@@ -22,7 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "system_clock_headers",
+ name = "system_clock",
+ srcs = [
+ "system_clock.cc",
+ ],
hdrs = [
"public/pw_chrono_freertos/config.h",
"public/pw_chrono_freertos/system_clock_config.h",
@@ -38,20 +41,6 @@ pw_cc_library(
],
deps = [
"//pw_chrono:epoch",
- "@freertos",
- ],
-)
-
-pw_cc_library(
- name = "system_clock",
- srcs = [
- "system_clock.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":system_clock_headers",
"//pw_chrono:system_clock_facade",
"//pw_interrupt:context",
"//pw_sync:interrupt_spin_lock",
@@ -60,7 +49,10 @@ pw_cc_library(
)
pw_cc_library(
- name = "system_timer_headers",
+ name = "system_timer",
+ srcs = [
+ "system_timer.cc",
+ ],
hdrs = [
"public/pw_chrono_freertos/system_timer_inline.h",
"public/pw_chrono_freertos/system_timer_native.h",
@@ -72,25 +64,10 @@ pw_cc_library(
"public_overrides",
],
deps = [
+ "//pw_assert",
"//pw_chrono:system_clock",
- "//pw_function",
- "//pw_chrono:system_timer_facade",
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
- ],
-)
-
-pw_cc_library(
- name = "system_timer",
- srcs = [
- "system_timer.cc",
- ],
- deps = [
- ":system_timer_headers",
"//pw_chrono:system_timer_facade",
- ":system_clock",
- "//pw_assert",
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ "//pw_function",
+ "@freertos",
],
)
diff --git a/pw_chrono_freertos/system_timer.cc b/pw_chrono_freertos/system_timer.cc
index 3e9a17172..cd9643f07 100644
--- a/pw_chrono_freertos/system_timer.cc
+++ b/pw_chrono_freertos/system_timer.cc
@@ -48,12 +48,16 @@ void HandleTimerCallback(TimerHandle_t timer_handle) {
backend::NativeSystemTimer& native_type =
*reinterpret_cast<backend::NativeSystemTimer*>(timer_handle);
- PW_CHECK_UINT_EQ(xTimerIsTimerActive(timer_handle),
- pdFALSE,
- "The timer is still active while being executed");
-
if (native_type.state == State::kCancelled) {
// Do nothing, we were invoked while the stop command was in the queue.
+ //
+ // Note that xTimerIsTimerActive cannot be used here. If a timer is started
+ // after it expired, it is executed immediately from the command queue.
+ // Older versions of FreeRTOS failed to mark expired timers as inactive
+ // before executing them in this way. So, if the timer is executed in the
+ // command queue before the stop command is processed, this callback will be
+ // invoked while xTimerIsTimerActive returns true. This was fixed in
+ // https://github.com/FreeRTOS/FreeRTOS-Kernel/pull/305.
return;
}
@@ -138,9 +142,12 @@ SystemTimer::~SystemTimer() {
"Timer command queue overflowed");
// In case the timer is still active as warned above, busy yield loop until it
- // has been removed. Note that this is safe before the scheduler has been
- // started because the timer cannot have been added to the queue yet and ergo
- // it shouldn't attempt to yield.
+ // has been removed. The active flag is cleared in the StaticTimer_t when the
+ // delete command is processed.
+ //
+ // Note that this is safe before the scheduler has been started because the
+ // timer cannot have been added to the queue yet and ergo it shouldn't attempt
+ // to yield.
while (
xTimerIsTimerActive(reinterpret_cast<TimerHandle_t>(&native_type_.tcb))) {
taskYIELD();
diff --git a/pw_chrono_rp2040/BUILD.bazel b/pw_chrono_rp2040/BUILD.bazel
new file mode 100644
index 000000000..fbbac24b5
--- /dev/null
+++ b/pw_chrono_rp2040/BUILD.bazel
@@ -0,0 +1,61 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "system_clock",
+ hdrs = [
+ "public/pw_chrono_rp2040/system_clock_config.h",
+ "public/pw_chrono_rp2040/system_clock_inline.h",
+ "public_overrides/pw_chrono_backend/system_clock_config.h",
+ "public_overrides/pw_chrono_backend/system_clock_inline.h",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/chipset:rp2040",
+ ],
+ # TODO: b/261603269 - This should depend on the Pi Pico SDK build rules.
+ deps = [
+ "//pw_chrono:epoch",
+ "//pw_chrono:system_clock_facade",
+ ],
+)
+
+pw_cc_test(
+ name = "clock_properties_test",
+ srcs = ["clock_properties_test.cc"],
+ target_compatible_with = [
+ "//pw_build/constraints/chipset:rp2040",
+ ],
+ # TODO: b/261603269 - This should depend on the Pi Pico SDK build rules.
+ deps = [":system_clock"],
+)
+
+# Bazel does not yet support building docs.
+filegroup(
+ name = "docs",
+ srcs = ["docs.rst"],
+)
diff --git a/pw_chrono_rp2040/BUILD.gn b/pw_chrono_rp2040/BUILD.gn
new file mode 100644
index 000000000..65b50d407
--- /dev/null
+++ b/pw_chrono_rp2040/BUILD.gn
@@ -0,0 +1,68 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+config("backend_config") {
+ include_dirs = [ "public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+# This target provides the backend for pw::chrono::SystemClock.
+pw_source_set("system_clock") {
+ public_configs = [
+ ":public_include_path",
+ ":backend_config",
+ ]
+ public = [
+ "public/pw_chrono_rp2040/system_clock_config.h",
+ "public/pw_chrono_rp2040/system_clock_inline.h",
+ "public_overrides/pw_chrono_backend/system_clock_config.h",
+ "public_overrides/pw_chrono_backend/system_clock_inline.h",
+ ]
+ public_deps = [
+ "$PICO_ROOT/src/common/pico_time",
+ "$dir_pw_chrono:epoch",
+ "$dir_pw_chrono:system_clock.facade",
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":clock_properties_test" ]
+}
+
+pw_test("clock_properties_test") {
+ deps = [
+ "$PICO_ROOT/src/common/pico_sync",
+ "$dir_pw_chrono:system_clock",
+ ]
+ sources = [ "clock_properties_test.cc" ]
+ enable_if =
+ pw_chrono_SYSTEM_CLOCK_BACKEND == "$dir_pw_chrono_rp2040:system_clock"
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_chrono_rp2040/CMakeLists.txt b/pw_chrono_rp2040/CMakeLists.txt
new file mode 100644
index 000000000..c3663c9e7
--- /dev/null
+++ b/pw_chrono_rp2040/CMakeLists.txt
@@ -0,0 +1,42 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# This target provides the backend for pw::chrono::SystemClock.
+pw_add_library(pw_chrono_rp2040.system_clock INTERFACE
+ HEADERS
+ public/pw_chrono_rp2040/system_clock_config.h
+ public/pw_chrono_rp2040/system_clock_inline.h
+ public_overrides/pw_chrono_backend/system_clock_config.h
+ public_overrides/pw_chrono_backend/system_clock_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_chrono.epoch
+ pw_chrono.system_clock.facade
+ pw_third_party.rp2040
+)
+
+pw_add_test(pw_chrono_rp2040.clock_properties_test
+ SOURCES
+ clock_properties_test.cc
+ PRIVATE_DEPS
+ pico_sync
+ pw_chrono.system_clock
+ GROUPS
+ modules
+ pw_chrono_rp2040
+)
diff --git a/pw_chrono_rp2040/clock_properties_test.cc b/pw_chrono_rp2040/clock_properties_test.cc
new file mode 100644
index 000000000..3ea06216f
--- /dev/null
+++ b/pw_chrono_rp2040/clock_properties_test.cc
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <cmath>
+
+#include "gtest/gtest.h"
+#include "pico/critical_section.h"
+#include "pw_chrono/system_clock.h"
+
+namespace pw::chrono::rp2040 {
+namespace {
+
+static_assert(pw::chrono::SystemClock::is_free_running == true);
+
+TEST(ClockProperties, IsFreeRunning) {
+ // Enter a critical section.
+ critical_section state;
+ critical_section_init(&state);
+ critical_section_enter_blocking(&state);
+
+ // Check initial clock value.
+ auto start = pw::chrono::SystemClock::now();
+
+ // Do some work.
+ volatile float num = 4;
+ for (int i = 0; i < 100; i++) {
+ float tmp = std::pow(num, 4);
+ num = tmp;
+ }
+
+ // Check final clock value.
+ auto end = pw::chrono::SystemClock::now();
+
+ // Exit critical section.
+ critical_section_exit(&state);
+
+ EXPECT_GT(num, 4);
+ EXPECT_GT(end, start);
+}
+
+} // namespace
+} // namespace pw::chrono::rp2040
diff --git a/pw_chrono_rp2040/docs.rst b/pw_chrono_rp2040/docs.rst
new file mode 100644
index 000000000..a9b82967e
--- /dev/null
+++ b/pw_chrono_rp2040/docs.rst
@@ -0,0 +1,17 @@
+.. _module-pw_chrono_rp2040:
+
+================
+pw_chrono_rp2040
+================
+This module provides backend implementations for pw_chrono that are suited for
+use with an RP2040-based board (e.g. Raspberry Pi Pico).
+
+-------------------
+SystemClock backend
+-------------------
+The ``pw_chrono_rp2040:system_clock`` backend target implements the
+``pw_chrono:system_clock`` facade by using the Pico SDK's time API. This is
+backed by a 64-bit hardware timer that reports time-since-boot as microseconds.
+
+See the :ref:`module-pw_chrono` documentation for ``pw_chrono`` for more
+information on what functionality this provides.
diff --git a/pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_config.h b/pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_config.h
new file mode 100644
index 000000000..5c1810938
--- /dev/null
+++ b/pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_config.h
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This override header includes the main tokenized logging header and defines
+// the PW_LOG macro as the tokenized logging macro.
+#pragma once
+
+// This clock relies on the RP2040's native time API, which provides clock
+// granularity to 1us.
+#define PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMERATOR 1
+#define PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR 1000000
+
+#ifdef __cplusplus
+
+#include "pw_chrono/epoch.h"
+
+namespace pw::chrono::backend {
+
+// The Pico SDK states that the system clock is strictly time-since-boot.
+constexpr inline Epoch kSystemClockEpoch = pw::chrono::Epoch::kTimeSinceBoot;
+
+// The Pico's system clock is tied to the watchdog, which is a hardware
+// block not tied to a maskable interrupt.
+constexpr inline bool kSystemClockNmiSafe = true;
+
+// The Pico's system clock is backed by a hardware block, which means it will
+// continue happily even if interrupts are disabled.
+constexpr inline bool kSystemClockFreeRunning = true;
+
+} // namespace pw::chrono::backend
+
+#endif // __cplusplus
diff --git a/pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_inline.h b/pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_inline.h
new file mode 100644
index 000000000..f023934e5
--- /dev/null
+++ b/pw_chrono_rp2040/public/pw_chrono_rp2040/system_clock_inline.h
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pico/time.h"
+#include "pw_chrono/system_clock.h"
+
+namespace pw::chrono::backend {
+
+inline int64_t GetSystemClockTickCount() {
+ // While it's probably possible to get a more granular clock, the 64-bit
+ // microsecond hardware timer is sufficient.
+ return time_us_64();
+}
+
+} // namespace pw::chrono::backend
diff --git a/pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_config.h b/pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_config.h
new file mode 100644
index 000000000..26097243d
--- /dev/null
+++ b/pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_config.h
@@ -0,0 +1,19 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This override header includes the main tokenized logging header and defines
+// the PW_LOG macro as the tokenized logging macro.
+#pragma once
+
+#include "pw_chrono_rp2040/system_clock_config.h"
diff --git a/pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_inline.h b/pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_inline.h
new file mode 100644
index 000000000..1bd55bef0
--- /dev/null
+++ b/pw_chrono_rp2040/public_overrides/pw_chrono_backend/system_clock_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_chrono_rp2040/system_clock_inline.h"
diff --git a/pw_chrono_stl/Android.bp b/pw_chrono_stl/Android.bp
new file mode 100644
index 000000000..5753d3260
--- /dev/null
+++ b/pw_chrono_stl/Android.bp
@@ -0,0 +1,40 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_chrono_stl_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ "public_overrides",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+cc_defaults {
+ name: "pw_chrono_stl_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_chrono_stl_include_dirs",
+ ],
+
+ export_header_lib_headers: [
+ "pw_chrono_stl_include_dirs",
+ ],
+}
diff --git a/pw_chrono_stl/BUILD.bazel b/pw_chrono_stl/BUILD.bazel
index 1fa008f5b..c4a6d7c21 100644
--- a/pw_chrono_stl/BUILD.bazel
+++ b/pw_chrono_stl/BUILD.bazel
@@ -26,7 +26,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "system_clock_headers",
+ name = "system_clock",
hdrs = [
"public/pw_chrono_stl/system_clock_config.h",
"public/pw_chrono_stl/system_clock_inline.h",
@@ -37,22 +37,18 @@ pw_cc_library(
"public",
"public_overrides",
],
- deps = [
- "//pw_chrono:epoch",
- ],
-)
-
-pw_cc_library(
- name = "system_clock",
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":system_clock_headers",
+ "//pw_chrono:epoch",
"//pw_chrono:system_clock_facade",
],
)
pw_cc_library(
- name = "system_timer_headers",
+ name = "system_timer",
+ srcs = [
+ "system_timer.cc",
+ ],
hdrs = [
"public/pw_chrono_stl/system_timer_inline.h",
"public/pw_chrono_stl/system_timer_native.h",
@@ -63,21 +59,10 @@ pw_cc_library(
"public",
"public_overrides",
],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
"//pw_chrono:system_clock",
"//pw_chrono:system_timer_facade",
"//pw_function",
],
)
-
-pw_cc_library(
- name = "system_timer",
- srcs = [
- "system_timer.cc",
- ],
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
- ":system_timer_headers",
- "//pw_chrono:system_timer_facade",
- ],
-)
diff --git a/pw_chrono_threadx/BUILD.bazel b/pw_chrono_threadx/BUILD.bazel
index 94b0e6ac5..cf1f164ba 100644
--- a/pw_chrono_threadx/BUILD.bazel
+++ b/pw_chrono_threadx/BUILD.bazel
@@ -22,7 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "system_clock_headers",
+ name = "system_clock",
+ srcs = [
+ "system_clock.cc",
+ ],
hdrs = [
"public/pw_chrono_threadx/config.h",
"public/pw_chrono_threadx/system_clock_config.h",
@@ -36,23 +39,9 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
],
+ # TODO: b/290362542 - This should depend on ThreadX.
deps = [
"//pw_chrono:epoch",
- ],
-)
-
-pw_cc_library(
- name = "system_clock",
- srcs = [
- "system_clock.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:threadx",
- ],
- deps = [
- ":system_clock_headers",
"//pw_chrono:system_clock_facade",
- # TODO(ewout): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
],
)
diff --git a/pw_chrono_zephyr/CMakeLists.txt b/pw_chrono_zephyr/CMakeLists.txt
index bea4f6de2..ecee072a7 100644
--- a/pw_chrono_zephyr/CMakeLists.txt
+++ b/pw_chrono_zephyr/CMakeLists.txt
@@ -30,7 +30,4 @@ pw_add_library(pw_chrono_zephyr.system_clock INTERFACE
pw_chrono.system_clock.facade
)
-if(CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK)
- zephyr_link_interface(pw_chrono_zephyr.system_clock)
- zephyr_link_libraries(pw_chrono_zephyr.system_clock)
-endif()
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK pw_chrono_zephyr.system_clock)
diff --git a/pw_chrono_zephyr/Kconfig b/pw_chrono_zephyr/Kconfig
index 4996cb02b..f5b7849b4 100644
--- a/pw_chrono_zephyr/Kconfig
+++ b/pw_chrono_zephyr/Kconfig
@@ -12,7 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_chrono"
+
config PIGWEED_CHRONO_SYSTEM_CLOCK
- bool "Enabled the Pigweed chrono system clock library (pw_chrono.system_clock)"
+ bool "Link pw_chrono.system_clock library"
select PIGWEED_PREPROCESSOR
select PIGWEED_FUNCTION
+ help
+ See :ref:`module-pw_checksum` for module details.
+
+endmenu
diff --git a/pw_chrono_zephyr/docs.rst b/pw_chrono_zephyr/docs.rst
index 06760fd66..9d3cafd92 100644
--- a/pw_chrono_zephyr/docs.rst
+++ b/pw_chrono_zephyr/docs.rst
@@ -13,4 +13,4 @@ This chrono backend implements the ``pw_chrono`` facades. To enable, set
pw_chrono.system_clock
----------------------
To enable the ``system_clock`` facade, it is also required to add
-``CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK=y``. \ No newline at end of file
+``CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK=y``.
diff --git a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_inline.h b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_inline.h
index f65246769..1863de66a 100644
--- a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_inline.h
+++ b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_inline.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
#include "pw_chrono/system_timer.h"
diff --git a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_native.h b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_native.h
index 9aa1a6583..dcf8372bb 100644
--- a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_native.h
+++ b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_timer_native.h
@@ -13,8 +13,8 @@
// the License.
#pragma once
-#include <kernel.h>
-#include <sys/mutex.h>
+#include <zephyr/kernel.h>
+#include <zephyr/sys/mutex.h>
#include "pw_chrono/system_clock.h"
#include "pw_function/function.h"
diff --git a/pw_cli/docs.rst b/pw_cli/docs.rst
index b6dc32e3e..c4aa8ac0e 100644
--- a/pw_cli/docs.rst
+++ b/pw_cli/docs.rst
@@ -49,43 +49,56 @@ Here are some example invocations of ``pw``:
Registering ``pw`` plugins
==========================
Projects can register their own Python scripts as ``pw`` commands. ``pw``
-plugins are registered by providing the command name, module, and function in a
-``PW_PLUGINS`` file. ``PW_PLUGINS`` files can add new commands or override
-built-in commands. Since they are accessed by module name, plugins must be
-defined in Python packages that are installed in the Pigweed virtual
+plugins are registered by providing the command name, module, and function in
+the ``pigweed.json`` file. ``pigweed.json`` files can add new commands or
+override built-in commands. Since they are accessed by module name, plugins must
+be defined in Python packages that are installed in the Pigweed virtual
environment.
-Plugin registrations in a ``PW_PLUGINS`` file apply to the their directory and
-all subdirectories, similarly to configuration files like ``.clang-format``.
-Registered plugins appear as commands in the ``pw`` tool when ``pw`` is run from
-those directories.
-
-Projects that wish to register commands might place a ``PW_PLUGINS`` file in the
-root of their repo. Multiple ``PW_PLUGINS`` files may be applied, but the ``pw``
-tool gives precedence to a ``PW_PLUGINS`` file in the current working directory
-or the nearest parent directory.
-
-PW_PLUGINS file format
-----------------------
-``PW_PLUGINS`` contains one plugin entry per line in the following format:
-
-.. code-block:: python
-
- # Lines that start with a # are ignored.
- <command name> <Python module> <function>
+pigweed.json file format
+---------------------------
+``pigweed.json`` contains plugin entries in the following format:
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_cli": {
+ "plugins": {
+ "<plugin name>": {
+ "module": "<module containing plugin>",
+ "function": "<entry point for plugin>"
+ },
+ ...
+ }
+ }
+ }
+ }
The following example registers three commands:
-.. code-block:: python
-
- # Register the presubmit script as pw presubmit
- presubmit my_cool_project.tools run_presubmit
-
- # Override the pw test command with a custom version
- test my_cool_project.testing run_test
-
- # Add a custom command
- flash my_cool_project.flash main
+.. code-block::
+
+ {
+ "pw": {
+ "pw_cli": {
+ "plugins": {
+ "presubmit": {
+ "module": "my_cool_project.tools",
+ "function": "run_presubmit"
+ },
+ "test": {
+ "module": "my_cool_project.testing",
+ "function": "run_test"
+ },
+ "flash": {
+ "module": "my_cool_project.flash",
+ "function": "main"
+ }
+ }
+ }
+ }
+ }
Defining a plugin function
--------------------------
@@ -280,50 +293,50 @@ registered (see :py:meth:`pw_cli.plugins.Registry.__init__`).
Plugins may be registered in a few different ways.
- * **Direct function call.** Register plugins by calling
- :py:meth:`pw_cli.plugins.Registry.register` or
- :py:meth:`pw_cli.plugins.Registry.register_by_name`.
+* **Direct function call.** Register plugins by calling
+ :py:meth:`pw_cli.plugins.Registry.register` or
+ :py:meth:`pw_cli.plugins.Registry.register_by_name`.
- .. code-block:: python
+ .. code-block:: python
- registry = pw_cli.plugins.Registry()
+ registry = pw_cli.plugins.Registry()
- registry.register('plugin_name', my_plugin)
- registry.register_by_name('plugin_name', 'module_name', 'function_name')
+ registry.register('plugin_name', my_plugin)
+ registry.register_by_name('plugin_name', 'module_name', 'function_name')
- * **Decorator.** Register using the :py:meth:`pw_cli.plugins.Registry.plugin`
- decorator.
+* **Decorator.** Register using the :py:meth:`pw_cli.plugins.Registry.plugin`
+ decorator.
- .. code-block:: python
+ .. code-block:: python
- _REGISTRY = pw_cli.plugins.Registry()
+ _REGISTRY = pw_cli.plugins.Registry()
- # This function is registered as the "my_plugin" plugin.
- @_REGISTRY.plugin
- def my_plugin():
- pass
+ # This function is registered as the "my_plugin" plugin.
+ @_REGISTRY.plugin
+ def my_plugin():
+ pass
- # This function is registered as the "input" plugin.
- @_REGISTRY.plugin(name='input')
- def read_something():
- pass
+ # This function is registered as the "input" plugin.
+ @_REGISTRY.plugin(name='input')
+ def read_something():
+ pass
- The decorator may be aliased to give a cleaner syntax (e.g. ``register =
- my_registry.plugin``).
+ The decorator may be aliased to give a cleaner syntax (e.g. ``register =
+ my_registry.plugin``).
- * **Plugins files.** Plugins files use a simple format:
+* **Plugins files.** Plugins files use a simple format:
- .. code-block::
+ .. code-block::
# Comments start with "#". Blank lines are ignored.
name_of_the_plugin module.name module_member
another_plugin some_module some_function
- These files are placed in the file system and apply similarly to Git's
- ``.gitignore`` files. From Python, these files are registered using
- :py:meth:`pw_cli.plugins.Registry.register_file` and
- :py:meth:`pw_cli.plugins.Registry.register_directory`.
+ These files are placed in the file system and apply similarly to Git's
+ ``.gitignore`` files. From Python, these files are registered using
+ :py:meth:`pw_cli.plugins.Registry.register_file` and
+ :py:meth:`pw_cli.plugins.Registry.register_directory`.
pw_cli.plugins module reference
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/pw_cli/py/BUILD.gn b/pw_cli/py/BUILD.gn
index 4bb4cd2e3..6b7ff68c4 100644
--- a/pw_cli/py/BUILD.gn
+++ b/pw_cli/py/BUILD.gn
@@ -15,12 +15,12 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action_test.gni")
pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_cli/__init__.py",
@@ -36,6 +36,10 @@ pw_python_package("py") {
"pw_cli/process.py",
"pw_cli/pw_command_plugins.py",
"pw_cli/requires.py",
+ "pw_cli/shell_completion/__init__.py",
+ "pw_cli/shell_completion/zsh/__init__.py",
+ "pw_cli/shell_completion/zsh/pw/__init__.py",
+ "pw_cli/shell_completion/zsh/pw_build/__init__.py",
"pw_cli/toml_config_loader_mixin.py",
"pw_cli/yaml_config_loader_mixin.py",
]
@@ -43,18 +47,22 @@ pw_python_package("py") {
"envparse_test.py",
"plugins_test.py",
]
+ python_deps = [ "$dir_pw_env_setup/py" ]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
+ inputs = [
+ "pw_cli/shell_completion/common.bash",
+ "pw_cli/shell_completion/pw.bash",
+ "pw_cli/shell_completion/pw.zsh",
+ "pw_cli/shell_completion/pw_build.bash",
+ "pw_cli/shell_completion/pw_build.zsh",
+ "pw_cli/shell_completion/zsh/pw/_pw",
+ "pw_cli/shell_completion/zsh/pw_build/_pw_build",
+ ]
}
-pw_python_script("process_integration_test") {
+pw_python_action_test("process_integration_test") {
sources = [ "process_integration_test.py" ]
python_deps = [ ":py" ]
-
- pylintrc = "$dir_pigweed/.pylintrc"
- mypy_ini = "$dir_pigweed/.mypy.ini"
-
- action = {
- stamp = true
- }
+ tags = [ "integration" ]
}
diff --git a/pw_cli/py/plugins_test.py b/pw_cli/py/plugins_test.py
index a3f35132d..aadee9022 100644
--- a/pw_cli/py/plugins_test.py
+++ b/pw_cli/py/plugins_test.py
@@ -134,29 +134,6 @@ class TestPluginRegistry(unittest.TestCase):
with self.assertRaises(plugins.Error):
self._registry.register('bar', lambda: None)
- def test_register_directory_innermost_takes_priority(self) -> None:
- with tempfile.TemporaryDirectory() as tempdir:
- paths = list(_create_files(tempdir, _TEST_PLUGINS))
- self._registry.register_directory(paths[1].parent, 'TEST_PLUGINS')
-
- self.assertEqual(self._registry.run_with_argv('test_plugin', []), 123)
-
- def test_register_directory_only_searches_up(self) -> None:
- with tempfile.TemporaryDirectory() as tempdir:
- paths = list(_create_files(tempdir, _TEST_PLUGINS))
- self._registry.register_directory(paths[0].parent, 'TEST_PLUGINS')
-
- self.assertEqual(self._registry.run_with_argv('test_plugin', []), 456)
-
- def test_register_directory_with_restriction(self) -> None:
- with tempfile.TemporaryDirectory() as tempdir:
- paths = list(_create_files(tempdir, _TEST_PLUGINS))
- self._registry.register_directory(
- paths[0].parent, 'TEST_PLUGINS', Path(tempdir, 'nested', 'in')
- )
-
- self.assertNotIn('other_plugin', self._registry)
-
def test_register_same_file_multiple_times_no_error(self) -> None:
with tempfile.TemporaryDirectory() as tempdir:
paths = list(_create_files(tempdir, _TEST_PLUGINS))
diff --git a/pw_cli/py/process_integration_test.py b/pw_cli/py/process_integration_test.py
index 3d05b1a1b..fa5873d30 100644
--- a/pw_cli/py/process_integration_test.py
+++ b/pw_cli/py/process_integration_test.py
@@ -21,8 +21,9 @@ from pw_cli import process
import psutil # type: ignore
-
-FAST_TIMEOUT_SECONDS = 0.1
+# This timeout must be long enough to wait for the subprocess output, but
+# fast enough that the test doesn't take terribly long in the success case.
+FAST_TIMEOUT_SECONDS = 0.5
KILL_SIGNALS = set({-9, 137})
PYTHON = sys.executable
@@ -44,8 +45,8 @@ class RunTest(unittest.TestCase):
self.assertIn(result.returncode, KILL_SIGNALS)
def test_timeout_kills_subprocess(self) -> None:
- # Spawn a subprocess which waits for 100 seconds, print its pid,
- # then wait for 100 seconds.
+ # Spawn a subprocess which prints its pid and then waits for 100
+ # seconds.
sleep_in_subprocess = textwrap.dedent(
f"""
import subprocess
@@ -62,7 +63,7 @@ class RunTest(unittest.TestCase):
PYTHON, '-c', sleep_in_subprocess, timeout=FAST_TIMEOUT_SECONDS
)
self.assertIn(result.returncode, KILL_SIGNALS)
- # THe first line of the output is the PID of the child sleep process.
+ # The first line of the output is the PID of the child sleep process.
child_pid_str, sep, remainder = result.output.partition(b'\n')
del sep, remainder
child_pid = int(child_pid_str)
diff --git a/pw_cli/py/pw_cli/__main__.py b/pw_cli/py/pw_cli/__main__.py
index 3320a4556..363bc80a5 100644
--- a/pw_cli/py/pw_cli/__main__.py
+++ b/pw_cli/py/pw_cli/__main__.py
@@ -31,14 +31,33 @@ def main() -> NoReturn:
pw_cli.log.install(level=args.loglevel, debug_log=args.debug_log)
- # Start with the most critical part of the Pigweed command line tool.
- if not args.no_banner:
+ # Print the banner unless --no-banner or --tab-complete-command is provided.
+ # Note: args.tab_complete_command may be the empty string '' so check for
+ # None instead.
+ if not args.no_banner and args.tab_complete_command is None:
arguments.print_banner()
_LOG.debug('Executing the pw command from %s', args.directory)
os.chdir(args.directory)
- pw_command_plugins.register(args.directory)
+ pw_command_plugins.register()
+
+ if args.tab_complete_option is not None:
+ arguments.print_completions_for_option(
+ arguments.arg_parser(),
+ text=args.tab_complete_option,
+ tab_completion_format=args.tab_complete_format,
+ )
+ sys.exit(0)
+
+ if args.tab_complete_command is not None:
+ for name, plugin in sorted(pw_command_plugins.plugin_registry.items()):
+ if name.startswith(args.tab_complete_command):
+ if args.tab_complete_format == 'zsh':
+ print(':'.join([name, plugin.help()]))
+ else:
+ print(name)
+ sys.exit(0)
if args.help or args.command is None:
print(pw_command_plugins.format_help(), file=sys.stderr)
diff --git a/pw_cli/py/pw_cli/arguments.py b/pw_cli/py/pw_cli/arguments.py
index f7fe37b4e..a5b8e4b27 100644
--- a/pw_cli/py/pw_cli/arguments.py
+++ b/pw_cli/py/pw_cli/arguments.py
@@ -14,10 +14,12 @@
"""Defines arguments for the pw command."""
import argparse
+from dataclasses import dataclass, field
+from enum import Enum
import logging
from pathlib import Path
import sys
-from typing import NoReturn
+from typing import List, NoReturn, Optional
from pw_cli import argument_types, plugins
from pw_cli.branding import banner
@@ -31,7 +33,77 @@ Example uses:
def parse_args() -> argparse.Namespace:
- return _parser().parse_args()
+ return arg_parser().parse_args()
+
+
+class ShellCompletionFormat(Enum):
+ """Supported shell tab completion modes."""
+
+ BASH = 'bash'
+ ZSH = 'zsh'
+
+
+@dataclass(frozen=True)
+class ShellCompletion:
+ option_strings: List[str] = field(default_factory=list)
+ help: Optional[str] = None
+ choices: Optional[List[str]] = None
+ flag: bool = True
+
+ def bash_completion(self, text: str) -> List[str]:
+ result: List[str] = []
+ for option_str in self.option_strings:
+ if option_str.startswith(text):
+ result.append(option_str)
+ return result
+
+ def zsh_completion(self, text: str) -> List[str]:
+ result: List[str] = []
+ for option_str in self.option_strings:
+ if option_str.startswith(text):
+ short_and_long_opts = ' '.join(self.option_strings)
+ # '(-h --help)-h[Display help message and exit]'
+ # '(-h --help)--help[Display help message and exit]'
+ help_text = self.help if self.help else ''
+ state_str = ''
+ if not self.flag:
+ state_str = ': :->' + option_str
+
+ result.append(
+ f'({short_and_long_opts}){option_str}[{help_text}]'
+ f'{state_str}'
+ )
+ return result
+
+
+def get_options_and_help(
+ parser: argparse.ArgumentParser,
+) -> List[ShellCompletion]:
+ return list(
+ ShellCompletion(
+ option_strings=list(action.option_strings),
+ help=action.help,
+ choices=list(action.choices) if action.choices else [],
+ flag=action.nargs == 0,
+ )
+ for action in parser._actions # pylint: disable=protected-access
+ )
+
+
+def print_completions_for_option(
+ parser: argparse.ArgumentParser,
+ text: str = '',
+ tab_completion_format: str = ShellCompletionFormat.BASH.value,
+) -> None:
+ matched_lines: List[str] = []
+ for completion in get_options_and_help(parser):
+ if tab_completion_format == ShellCompletionFormat.ZSH.value:
+ matched_lines.extend(completion.zsh_completion(text))
+ else:
+ matched_lines.extend(completion.bash_completion(text))
+
+ for line in matched_lines:
+ print(line)
def print_banner() -> None:
@@ -41,7 +113,7 @@ def print_banner() -> None:
def format_help(registry: plugins.Registry) -> str:
"""Returns the pw help information as a string."""
- return f'{_parser().format_help()}\n{registry.short_help()}'
+ return f'{arg_parser().format_help()}\n{registry.short_help()}'
class _ArgumentParserWithBanner(argparse.ArgumentParser):
@@ -53,7 +125,24 @@ class _ArgumentParserWithBanner(argparse.ArgumentParser):
self.exit(2, f'{self.prog}: error: {message}\n')
-def _parser() -> argparse.ArgumentParser:
+def add_tab_complete_arguments(
+ parser: argparse.ArgumentParser,
+) -> argparse.ArgumentParser:
+ parser.add_argument(
+ '--tab-complete-option',
+ nargs='?',
+ help='Print tab completions for the supplied option text.',
+ )
+ parser.add_argument(
+ '--tab-complete-format',
+ choices=list(shell.value for shell in ShellCompletionFormat),
+ default='bash',
+ help='Output format for tab completion results.',
+ )
+ return parser
+
+
+def arg_parser() -> argparse.ArgumentParser:
"""Creates an argument parser for the pw command."""
argparser = _ArgumentParserWithBanner(
prog='pw',
@@ -97,6 +186,11 @@ def _parser() -> argparse.ArgumentParser:
help='Do not print the Pigweed banner',
)
argparser.add_argument(
+ '--tab-complete-command',
+ nargs='?',
+ help='Print tab completions for the supplied command text.',
+ )
+ argparser.add_argument(
'command',
nargs='?',
help='Which command to run; see supported commands below',
@@ -107,5 +201,4 @@ def _parser() -> argparse.ArgumentParser:
nargs=argparse.REMAINDER,
help='Remaining arguments are forwarded to the command',
)
-
- return argparser
+ return add_tab_complete_arguments(argparser)
diff --git a/pw_cli/py/pw_cli/branding.py b/pw_cli/py/pw_cli/branding.py
index 358e4019b..0d9dccd7f 100644
--- a/pw_cli/py/pw_cli/branding.py
+++ b/pw_cli/py/pw_cli/branding.py
@@ -42,7 +42,7 @@ def banner() -> str:
# Take the banner from the file PW_BRANDING_BANNER; or use the default.
banner_filename = parsed_env.PW_BRANDING_BANNER
_memoized_banner = (
- Path(banner_filename).read_text()
+ Path(banner_filename).read_text(encoding='utf-8', errors='replace')
if banner_filename
else _PIGWEED_BANNER
)
diff --git a/pw_cli/py/pw_cli/color.py b/pw_cli/py/pw_cli/color.py
index 855a8d11c..6b9074bf5 100644
--- a/pw_cli/py/pw_cli/color.py
+++ b/pw_cli/py/pw_cli/color.py
@@ -68,13 +68,21 @@ def colors(enabled: Optional[bool] = None) -> Union[_Color, _NoColor]:
"""
if enabled is None:
env = pw_cli.env.pigweed_environment()
- enabled = env.PW_USE_COLOR or (
- sys.stdout.isatty() and sys.stderr.isatty()
- )
+ if 'PW_USE_COLOR' in os.environ:
+ enabled = env.PW_USE_COLOR
+ else:
+ enabled = sys.stdout.isatty() and sys.stderr.isatty()
if enabled and os.name == 'nt':
# Enable ANSI color codes in Windows cmd.exe.
kernel32 = ctypes.windll.kernel32 # type: ignore
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
+ # These are semi-standard ways to turn colors off or on for many projects.
+ # See https://bixense.com/clicolors/ and https://no-color.org/ for more.
+ if 'NO_COLOR' in os.environ:
+ enabled = False
+ elif 'CLICOLOR_FORCE' in os.environ:
+ enabled = True
+
return _Color() if enabled else _NoColor()
diff --git a/pw_cli/py/pw_cli/env.py b/pw_cli/py/pw_cli/env.py
index 74deff8ea..afe3d8dfe 100644
--- a/pw_cli/py/pw_cli/env.py
+++ b/pw_cli/py/pw_cli/env.py
@@ -45,7 +45,7 @@ def pigweed_environment_parser() -> envparse.EnvironmentParser:
)
parser.add_var('PW_SKIP_BOOTSTRAP')
parser.add_var('PW_SUBPROCESS', type=envparse.strict_bool, default=False)
- parser.add_var('PW_USE_COLOR', type=envparse.strict_bool, default=False)
+ parser.add_var('PW_USE_COLOR', type=envparse.strict_bool, default=True)
parser.add_var('PW_USE_GCS_ENVSETUP', type=envparse.strict_bool)
parser.add_allowed_suffix('_CIPD_INSTALL_DIR')
@@ -69,6 +69,7 @@ def pigweed_environment_parser() -> envparse.EnvironmentParser:
parser.add_var('PW_CONSOLE_CONFIG_FILE')
parser.add_var('PW_ENVIRONMENT_NO_ERROR_ON_UNRECOGNIZED')
+ parser.add_var('PW_NO_CIPD_CACHE_DIR')
parser.add_var('PW_CIPD_SERVICE_ACCOUNT_JSON')
# RBE environment variables
diff --git a/pw_cli/py/pw_cli/envparse.py b/pw_cli/py/pw_cli/envparse.py
index d9ed9de47..182936f2d 100644
--- a/pw_cli/py/pw_cli/envparse.py
+++ b/pw_cli/py/pw_cli/envparse.py
@@ -22,6 +22,7 @@ from typing import (
Generic,
IO,
List,
+ Literal,
Mapping,
Optional,
TypeVar,
@@ -173,7 +174,10 @@ class EnvironmentParser:
and var not in self._variables
and not var.endswith(allowed_suffixes)
):
- raise ValueError(f'Unrecognized environment variable {var}')
+ raise ValueError(
+ f'Unrecognized environment variable {var}, please '
+ 'remove it from your environment'
+ )
return namespace
@@ -202,9 +206,7 @@ def strict_bool(value: str) -> bool:
)
-# TODO(mohrr) Switch to Literal when no longer supporting Python 3.7.
-# OpenMode = Literal['r', 'rb', 'w', 'wb']
-OpenMode = str
+OpenMode = Literal['r', 'rb', 'w', 'wb']
class FileType:
diff --git a/pw_cli/py/pw_cli/log.py b/pw_cli/py/pw_cli/log.py
index c8414d1f5..e1ef13fde 100644
--- a/pw_cli/py/pw_cli/log.py
+++ b/pw_cli/py/pw_cli/log.py
@@ -165,6 +165,12 @@ def install(
9,
):
formatter.default_msec_format = ''
+ # For 3.8 set datefmt to time_format
+ elif sys.version_info >= (
+ 3,
+ 8,
+ ):
+ formatter.datefmt = time_format
# Set the log level on the root logger to NOTSET, so that all logs
# propagated from child loggers are handled.
diff --git a/pw_cli/py/pw_cli/plugins.py b/pw_cli/py/pw_cli/plugins.py
index b82b7f675..15a5dcd2d 100644
--- a/pw_cli/py/pw_cli/plugins.py
+++ b/pw_cli/py/pw_cli/plugins.py
@@ -318,6 +318,33 @@ class Registry(collections.abc.Mapping):
return plugin
+ def register_config(
+ self,
+ config: Dict,
+ path: Optional[Path] = None,
+ ) -> None:
+ """Registers plugins from a Pigweed config.
+
+ Any exceptions raised from parsing the file are caught and logged.
+ """
+ plugins = config.get('pw', {}).get('pw_cli', {}).get('plugins', {})
+ for name, location in plugins.items():
+ module = location.pop('module')
+ function = location.pop('function')
+ if location:
+ raise ValueError(f'unrecognized plugin options: {location}')
+
+ try:
+ self.register_by_name(name, module, function, path)
+ except Error as err:
+ self._errors[name].append(err)
+ _LOG.error(
+ '%s Failed to register plugin "%s": %s',
+ path,
+ name,
+ err,
+ )
+
def register_file(self, path: Path) -> None:
"""Registers plugins from a plugins file.
diff --git a/pw_cli/py/pw_cli/process.py b/pw_cli/py/pw_cli/process.py
index 98852e35f..c77627bd5 100644
--- a/pw_cli/py/pw_cli/process.py
+++ b/pw_cli/py/pw_cli/process.py
@@ -200,7 +200,8 @@ async def run_async(
if process.returncode:
_LOG.error('%s exited with status %d', program, process.returncode)
else:
- _LOG.error('%s exited successfully', program)
+ # process.returncode is 0
+ _LOG.debug('%s exited successfully', program)
return CompletedProcess(process, output)
diff --git a/pw_cli/py/pw_cli/pw_command_plugins.py b/pw_cli/py/pw_cli/pw_command_plugins.py
index 99b86fdd8..cfef10a42 100644
--- a/pw_cli/py/pw_cli/pw_command_plugins.py
+++ b/pw_cli/py/pw_cli/pw_command_plugins.py
@@ -14,14 +14,14 @@
"""This module manages the global plugin registry for pw_cli."""
import argparse
-import os
from pathlib import Path
import sys
from typing import Iterable
-from pw_cli import arguments, plugins
+from pw_cli import arguments, env, plugins
+import pw_env_setup.config_file
-_plugin_registry = plugins.Registry(validator=plugins.callable_with_no_args)
+plugin_registry = plugins.Registry(validator=plugins.callable_with_no_args)
REGISTRY_FILE = 'PW_PLUGINS'
@@ -31,6 +31,7 @@ def _register_builtin_plugins(registry: plugins.Registry) -> None:
# Register these by name to avoid circular dependencies.
registry.register_by_name('bloat', 'pw_bloat.__main__', 'main')
registry.register_by_name('doctor', 'pw_doctor.doctor', 'main')
+ registry.register_by_name('emu', 'pw_emu.__main__', 'main')
registry.register_by_name('format', 'pw_presubmit.format_code', 'main')
registry.register_by_name('keep-sorted', 'pw_presubmit.keep_sorted', 'main')
registry.register_by_name('logdemo', 'pw_cli.log', 'main')
@@ -54,26 +55,33 @@ def _help_command():
help='command for which to display detailed info',
)
- print(arguments.format_help(_plugin_registry), file=sys.stderr)
+ print(arguments.format_help(plugin_registry), file=sys.stderr)
- for line in _plugin_registry.detailed_help(**vars(parser.parse_args())):
+ for line in plugin_registry.detailed_help(**vars(parser.parse_args())):
print(line, file=sys.stderr)
-def register(directory: Path) -> None:
- _register_builtin_plugins(_plugin_registry)
- _plugin_registry.register_directory(
- directory, REGISTRY_FILE, Path(os.environ.get('PW_PROJECT_ROOT', ''))
- )
+def register() -> None:
+ _register_builtin_plugins(plugin_registry)
+ parsed_env = env.pigweed_environment()
+ pw_plugins_file: Path = parsed_env.PW_PROJECT_ROOT / REGISTRY_FILE
+
+ if pw_plugins_file.is_file():
+ plugin_registry.register_file(pw_plugins_file)
+ else:
+ plugin_registry.register_config(
+ config=pw_env_setup.config_file.load(),
+ path=pw_env_setup.config_file.path(),
+ )
def errors() -> dict:
- return _plugin_registry.errors()
+ return plugin_registry.errors()
def format_help() -> str:
- return arguments.format_help(_plugin_registry)
+ return arguments.format_help(plugin_registry)
def run(name: str, args: Iterable[str]) -> int:
- return _plugin_registry.run_with_argv(name, args)
+ return plugin_registry.run_with_argv(name, args)
diff --git a/pw_cli/py/pw_cli/requires.py b/pw_cli/py/pw_cli/requires.py
index 79a2b61de..d9a996b20 100755
--- a/pw_cli/py/pw_cli/requires.py
+++ b/pw_cli/py/pw_cli/requires.py
@@ -27,13 +27,16 @@ For more see http://go/pigweed-ci-cq-intro.
"""
import argparse
+import dataclasses
import json
import logging
+import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
+from typing import Callable, Dict, IO, List, Sequence
import uuid
HELPER_GERRIT = 'pigweed-internal'
@@ -59,6 +62,42 @@ remote:
_LOG = logging.getLogger(__name__)
+@dataclasses.dataclass
+class Change:
+ gerrit_name: str
+ number: int
+
+
+class EnhancedJSONEncoder(json.JSONEncoder):
+ def default(self, o):
+ if dataclasses.is_dataclass(o):
+ return dataclasses.asdict(o)
+ return super().default(o)
+
+
+def dump_json_patches(obj: Sequence[Change], outs: IO):
+ json.dump(obj, outs, indent=2, cls=EnhancedJSONEncoder)
+
+
+def log_entry_exit(func: Callable) -> Callable:
+ def wrapper(*args, **kwargs):
+ _LOG.debug('entering %s()', func.__name__)
+ _LOG.debug('args %r', args)
+ _LOG.debug('kwargs %r', kwargs)
+ try:
+ res = func(*args, **kwargs)
+ _LOG.debug('return value %r', res)
+ return res
+ except Exception as exc:
+ _LOG.debug('exception %r', exc)
+ raise
+ finally:
+ _LOG.debug('exiting %s()', func.__name__)
+
+ return wrapper
+
+
+@log_entry_exit
def parse_args() -> argparse.Namespace:
"""Creates an argument parser and parses arguments."""
@@ -78,7 +117,8 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()
-def _run_command(*args, **kwargs):
+@log_entry_exit
+def _run_command(*args, **kwargs) -> subprocess.CompletedProcess:
kwargs.setdefault('capture_output', True)
_LOG.debug('%s', args)
_LOG.debug('%s', kwargs)
@@ -89,6 +129,7 @@ def _run_command(*args, **kwargs):
return res
+@log_entry_exit
def check_status() -> bool:
res = subprocess.run(['git', 'status'], capture_output=True)
if res.returncode:
@@ -97,34 +138,40 @@ def check_status() -> bool:
return True
+@log_entry_exit
def clone(requires_dir: Path) -> None:
_LOG.info('cloning helper repository into %s', requires_dir)
_run_command(['git', 'clone', HELPER_REPO, '.'], cwd=requires_dir)
-def create_commit(requires_dir: Path, requirements) -> None:
+@log_entry_exit
+def create_commit(
+ requires_dir: Path, requirement_strings: Sequence[str]
+) -> None:
"""Create a commit in the local tree with the given requirements."""
change_id = str(uuid.uuid4()).replace('-', '00')
_LOG.debug('change_id %s', change_id)
- reqs = []
- for req in requirements:
+ requirement_objects: List[Change] = []
+ for req in requirement_strings:
gerrit_name, number = req.split(':', 1)
- reqs.append({'gerrit_name': gerrit_name, 'number': number})
+ requirement_objects.append(Change(gerrit_name, int(number)))
path = requires_dir / 'patches.json'
_LOG.debug('path %s', path)
with open(path, 'w') as outs:
- json.dump(reqs, outs)
+ dump_json_patches(requirement_objects, outs)
+ outs.write('\n')
_run_command(['git', 'add', path], cwd=requires_dir)
+ # TODO: b/232234662 - Don't add 'Requires:' lines to commit messages.
commit_message = [
f'{_DNS} {change_id[0:10]}\n\n',
'',
f'Change-Id: I{change_id}',
]
- for req in requirements:
+ for req in requirement_strings:
commit_message.append(f'Requires: {req}')
_LOG.debug('message %s', commit_message)
@@ -137,8 +184,18 @@ def create_commit(requires_dir: Path, requirements) -> None:
_run_command(['git', 'show'], cwd=requires_dir)
-def push_commit(requires_dir: Path, push=True) -> str:
- output = DEFAULT_OUTPUT
+@log_entry_exit
+def push_commit(requires_dir: Path, push=True) -> Change:
+ """Push a commit to the helper repository.
+
+ Args:
+ requires_dir: Local checkout of the helper repository.
+ push: Whether to actually push or if this is a local-only test.
+
+ Returns a Change object referencing the pushed commit.
+ """
+
+ output: str = DEFAULT_OUTPUT
if push:
res = _run_command(
['git', 'push', HELPER_REPO, '+HEAD:refs/for/main'],
@@ -157,22 +214,46 @@ def push_commit(requires_dir: Path, push=True) -> str:
match = regex.search(output)
if not match:
raise ValueError(f"invalid output from 'git push': {output}")
- change_num = match.group('num')
+ change_num = int(match.group('num'))
_LOG.info('created %s change %s', HELPER_PROJECT, change_num)
- return f'{HELPER_GERRIT}:{change_num}'
+ return Change(HELPER_GERRIT, change_num)
+
+
+@log_entry_exit
+def amend_existing_change(dependency: Dict[str, str]) -> None:
+ """Amend the current change to depend on the dependency
+
+ Args:
+ dependency: The change on which the top of the current checkout now
+ depends.
+ """
+ git_root = Path(
+ subprocess.run(
+ ['git', 'rev-parse', '--show-toplevel'],
+ capture_output=True,
+ )
+ .stdout.decode()
+ .rstrip('\n')
+ )
+ patches_json = git_root / 'patches.json'
+ _LOG.info('%s %d', patches_json, os.path.isfile(patches_json))
+ patches = []
+ if os.path.isfile(patches_json):
+ with open(patches_json, 'r') as ins:
+ patches = json.load(ins)
-def amend_existing_change(change: str) -> None:
- res = _run_command(['git', 'log', '-1', '--pretty=%B'])
- original = res.stdout.rstrip().decode()
+ patches.append(dependency)
+ with open(patches_json, 'w') as outs:
+ dump_json_patches(patches, outs)
+ outs.write('\n')
+ _LOG.info('%s %d', patches_json, os.path.isfile(patches_json))
- addition = f'Requires: {change}'
- _LOG.info('adding "%s" to current commit message', addition)
- message = '\n'.join((original, addition))
- _run_command(['git', 'commit', '--amend', '--message', message])
+ _run_command(['git', 'add', patches_json])
+ _run_command(['git', 'commit', '--amend', '--no-edit'])
-def run(requirements, push=True) -> int:
+def run(requirements: Sequence[str], push: bool = True) -> int:
"""Entry point for requires."""
if not check_status():
diff --git a/pw_cli/py/pw_cli/shell_completion/__init__.py b/pw_cli/py/pw_cli/shell_completion/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/__init__.py
diff --git a/pw_cli/py/pw_cli/shell_completion/common.bash b/pw_cli/py/pw_cli/shell_completion/common.bash
new file mode 100644
index 000000000..95ed56171
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/common.bash
@@ -0,0 +1,65 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+if [[ -n ${ZSH_VERSION-} ]]; then
+ autoload -U +X bashcompinit && bashcompinit
+fi
+
+__pwcomp_words_include ()
+{
+ local i=1
+ while [[ $i -lt $COMP_CWORD ]]; do
+ if [[ "${COMP_WORDS[i]}" = "$1" ]]; then
+ return 0
+ fi
+ i=$((++i))
+ done
+ return 1
+}
+
+# Find the previous non-switch word
+__pwcomp_prev ()
+{
+ local idx=$((COMP_CWORD - 1))
+ local prv="${COMP_WORDS[idx]}"
+ while [[ $prv == -* ]]; do
+ idx=$((--idx))
+ prv="${COMP_WORDS[idx]}"
+ done
+}
+
+
+__pwcomp ()
+{
+ # break $1 on space, tab, and newline characters,
+ # and turn it into a newline separated list of words
+ local list s sep=$'\n' IFS=$' '$'\t'$'\n'
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+
+ for s in $1; do
+ __pwcomp_words_include "$s" && continue
+ list="$list$s$sep"
+ done
+
+ case "$cur" in
+ --*=)
+ COMPREPLY=()
+ ;;
+ *)
+ IFS=$sep
+ COMPREPLY=( $(compgen -W "$list" -- "$cur" | sed -e 's/[^=]$/& /g') )
+ ;;
+ esac
+}
+
diff --git a/pw_cli/py/pw_cli/shell_completion/pw.bash b/pw_cli/py/pw_cli/shell_completion/pw.bash
new file mode 100644
index 000000000..a32085395
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/pw.bash
@@ -0,0 +1,75 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Source common bash completion functions
+# Path to this directory, works in bash and zsh
+COMPLETION_DIR=$(dirname "${BASH_SOURCE[0]-$0}")
+. "${COMPLETION_DIR}/common.bash"
+
+_pw () {
+ local i=1 cmd
+
+ if [[ -n ${ZSH_VERSION-} ]]; then
+ emulate -L bash
+ setopt KSH_TYPESET
+
+ # Workaround zsh's bug that leaves 'words' as a special
+ # variable in versions < 4.3.12
+ typeset -h words
+ fi
+
+ # find the subcommand
+ while [[ $i -lt $COMP_CWORD ]]; do
+ local s="${COMP_WORDS[i]}"
+ case "$s" in
+ --*) ;;
+ -*) ;;
+ *) cmd="$s"
+ break
+ ;;
+ esac
+ i=$((++i))
+ done
+
+ if [[ $i -eq $COMP_CWORD ]]; then
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ case "$cur" in
+ -*)
+ local all_options=$(pw --no-banner --tab-complete-option "")
+ __pwcomp "${all_options}"
+ return
+ ;;
+ *)
+ local all_commands=$(pw --no-banner --tab-complete-command "")
+ __pwcomp "${all_commands}"
+ return
+ ;;
+ esac
+ return
+ fi
+
+ # subcommands have their own completion functions
+ case "$cmd" in
+ help)
+ local all_commands=$(pw --no-banner --tab-complete-command "")
+ __pwcomp "${all_commands}"
+ ;;
+ *)
+ # If the command is 'build' and a function named _pw_build exists, then run it.
+ [[ $(type -t _pw_$cmd) == function ]] && _pw_$cmd
+ ;;
+ esac
+}
+
+complete -o bashdefault -o default -o nospace -F _pw pw
diff --git a/pw_hdlc/py/setup.py b/pw_cli/py/pw_cli/shell_completion/pw.zsh
index bf8cfe69b..ff70ab797 100644
--- a/pw_hdlc/py/setup.py
+++ b/pw_cli/py/pw_cli/shell_completion/pw.zsh
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,8 +11,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_hdlc"""
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
+fpath=("$PW_ROOT/pw_cli/py/pw_cli/shell_completion/zsh/pw" $fpath)
+autoload -Uz compinit && compinit -i
diff --git a/pw_cli/py/pw_cli/shell_completion/pw_build.bash b/pw_cli/py/pw_cli/shell_completion/pw_build.bash
new file mode 100644
index 000000000..3a0c3ded5
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/pw_build.bash
@@ -0,0 +1,62 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Source common bash completion functions
+# Path to this directory, works in bash and zsh
+
+__pw_complete_recipes () {
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ local all_recipes=$(pw --no-banner build --tab-complete-recipe "${cur}")
+ COMPREPLY=($(compgen -W "$all_recipes" -- "$cur"))
+}
+
+__pw_complete_presubmit_steps () {
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ local all_recipes=$(pw --no-banner build --tab-complete-presubmit-step "${cur}")
+ COMPREPLY=($(compgen -W "$all_recipes" -- "$cur"))
+}
+
+_pw_build () {
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ local prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+ case "$prev" in
+ -r|--recipe)
+ __pw_complete_recipes
+ return
+ ;;
+ -s|--step)
+ __pw_complete_presubmit_steps
+ return
+ ;;
+ --logfile)
+ # Complete a file
+ COMPREPLY=($(compgen -f "$cur"))
+ return
+ ;;
+ *)
+ ;;
+ esac
+
+ case "$cur" in
+ -*)
+ __pwcomp
+ local all_options=$(pw --no-banner build --tab-complete-option "")
+ __pwcomp "${all_options}"
+ return
+ ;;
+ esac
+ # Non-option args go here
+ COMPREPLY=()
+}
diff --git a/pw_allocator/py/setup.py b/pw_cli/py/pw_cli/shell_completion/pw_build.zsh
index 7a82dcf7e..d2e4e75a6 100644
--- a/pw_allocator/py/setup.py
+++ b/pw_cli/py/pw_cli/shell_completion/pw_build.zsh
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,8 +11,6 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_allocator"""
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
+fpath=("$PW_ROOT/pw_cli/py/pw_cli/shell_completion/zsh/pw_build" $fpath)
+autoload -Uz compinit && compinit -i
diff --git a/pw_cli/py/pw_cli/shell_completion/zsh/__init__.py b/pw_cli/py/pw_cli/shell_completion/zsh/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/zsh/__init__.py
diff --git a/pw_cli/py/pw_cli/shell_completion/zsh/pw/__init__.py b/pw_cli/py/pw_cli/shell_completion/zsh/pw/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/zsh/pw/__init__.py
diff --git a/pw_cli/py/pw_cli/shell_completion/zsh/pw/_pw b/pw_cli/py/pw_cli/shell_completion/zsh/pw/_pw
new file mode 100755
index 000000000..1b077763f
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/zsh/pw/_pw
@@ -0,0 +1,49 @@
+#compdef pw
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+_complete_pw_subcommands() {
+ local -a _1st_arguments=("${(@f)$(pw --no-banner --tab-complete-format=zsh --tab-complete-command '')}")
+ _describe -t commands "pw subcommand" _1st_arguments
+}
+
+_pw() {
+ # Complete pw options
+ local -a _pw_options=("${(@f)$(pw --no-banner --tab-complete-format=zsh --tab-complete-option '')}")
+ _pw_options+=('*:: :->subcmds')
+ _arguments $_pw_options && return 0
+
+ # Match Sub-command
+ if (( CURRENT == 1 )); then
+ _complete_pw_subcommands
+ return
+ fi
+
+ # Completion for each sub command
+ case "$words[1]" in
+ help)
+ # Help takes a subcommand name to display help for
+ _complete_pw_subcommands
+ ;;
+ *)
+ # If the command is 'build' and a function named _pw_build exists, then run it.
+ (( $+functions[_pw_$words[1]] )) && _pw_$words[1] && return 0
+ # Otherwise, complete files.
+ _files
+ ;;
+
+ esac
+}
+
+_pw
diff --git a/pw_cli/py/pw_cli/shell_completion/zsh/pw_build/__init__.py b/pw_cli/py/pw_cli/shell_completion/zsh/pw_build/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/zsh/pw_build/__init__.py
diff --git a/pw_cli/py/pw_cli/shell_completion/zsh/pw_build/_pw_build b/pw_cli/py/pw_cli/shell_completion/zsh/pw_build/_pw_build
new file mode 100644
index 000000000..ecaf8124e
--- /dev/null
+++ b/pw_cli/py/pw_cli/shell_completion/zsh/pw_build/_pw_build
@@ -0,0 +1,40 @@
+#autoload
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+_pw_build() {
+ local -a _pw_build_options=("${(@f)$(pw --no-banner build --tab-complete-format=zsh --tab-complete-option '')}")
+ _arguments $_pw_build_options && return 0
+
+ case "$state" in
+ -r|--recipe)
+ _pw_build_recipe
+ ;;
+ -s|--step)
+ _pw_build_presubmit_step
+ ;;
+ *)
+ ;;
+ esac
+}
+
+_pw_build_recipe() {
+ local -a all_recipes=("${(@f)$(pw --no-banner build --tab-complete-recipe '')}")
+ _values recipe $all_recipes
+}
+
+_pw_build_presubmit_step() {
+ local -a all_steps=("${(@f)$(pw --no-banner build --tab-complete-presubmit-step '')}")
+ _values 'presubmit step' $all_steps
+}
diff --git a/pw_cli/py/setup.cfg b/pw_cli/py/setup.cfg
index 20af8de6f..290b0a364 100644
--- a/pw_cli/py/setup.cfg
+++ b/pw_cli/py/setup.cfg
@@ -30,4 +30,12 @@ install_requires =
console_scripts = pw = pw_cli.__main__:main
[options.package_data]
-pw_cli = py.typed
+pw_cli =
+ py.typed
+ shell_completion/common.bash
+ shell_completion/pw.bash
+ shell_completion/pw.zsh
+ shell_completion/pw_build.bash
+ shell_completion/pw_build.zsh
+ shell_completion/zsh/pw/_pw
+ shell_completion/zsh/pw_build/_pw_build
diff --git a/pw_compilation_testing/negative_compilation_test.gni b/pw_compilation_testing/negative_compilation_test.gni
index 5569cef4c..3f0cf7241 100644
--- a/pw_compilation_testing/negative_compilation_test.gni
+++ b/pw_compilation_testing/negative_compilation_test.gni
@@ -63,6 +63,7 @@ template("pw_cc_negative_compilation_test") {
# Create a group of the generated NC test targets.
group(target_name) {
+ forward_variables_from(invoker, [ "testonly" ])
deps = []
foreach(test, _tests) {
deps += [ "$_out_dir:$target_name.$test.negative_compilation_test" ]
@@ -73,6 +74,7 @@ template("pw_cc_negative_compilation_test") {
# negative compilation test targets depend on. Use an action for this target
# so that depending on it will not link in any source files.
pw_python_action(target_name) {
+ forward_variables_from(invoker, [ "testonly" ])
script = "$dir_pw_build/py/pw_build/nop.py"
stamp = true
deps = [ ":$target_name._base" ]
diff --git a/pw_compilation_testing/py/BUILD.gn b/pw_compilation_testing/py/BUILD.gn
index 7a8bbed73..ecd80713a 100644
--- a/pw_compilation_testing/py/BUILD.gn
+++ b/pw_compilation_testing/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_compilation_testing/__init__.py",
diff --git a/pw_compilation_testing/py/setup.py b/pw_compilation_testing/py/setup.py
deleted file mode 100644
index 299dea40a..000000000
--- a/pw_compilation_testing/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2022 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""The pw_compilation_testing package supports negative compilation testing."""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_console/docs.rst b/pw_console/docs.rst
index 0c7ffc3e0..b4f2a19d5 100644
--- a/pw_console/docs.rst
+++ b/pw_console/docs.rst
@@ -1,12 +1,14 @@
.. _module-pw_console:
-----------
+==========
pw_console
-----------
-
-:bdg-primary:`host`
-:bdg-secondary:`Python`
-:bdg-success:`stable`
+==========
+.. pigweed-module::
+ :name: pw_console
+ :tagline: Multi-purpose pluggable interactive console for dev & manufacturing
+ :status: stable
+ :languages: Python
+ :code-size-impact: N/A (host) but works best with pw_rpc on device
The Pigweed Console provides a Python repl (read eval print loop) using
`ptpython`_ and a log message viewer in a single-window terminal based
@@ -15,8 +17,9 @@ interface. It is designed to be a replacement for `IPython's embed()`_ function.
.. figure:: images/pw_system_boot.png
:alt: Pigweed Console screenshot with serial debug log messages.
+--------
Features
-========
+--------
``pw_console`` aims to be a complete solution for interacting with hardware
devices using :ref:`module-pw_rpc` over a :ref:`module-pw_hdlc` transport.
@@ -33,18 +36,18 @@ devices using :ref:`module-pw_rpc` over a :ref:`module-pw_hdlc` transport.
- Log viewer with searching and filtering.
+------------
Contributing
-============
-
+------------
- All code submissions to ``pw_console`` require running the
:ref:`module-pw_console-testing`.
- Commit messages should include a ``Testing:`` line with the steps that were
manually run.
+------
Guides
-======
-
+------
.. toctree::
:maxdepth: 1
@@ -54,7 +57,6 @@ Guides
testing
internals
-
.. _IPython's embed(): https://ipython.readthedocs.io/en/stable/interactive/reference.html#embedding
.. _IPython: https://ipython.readthedocs.io/
.. _prompt_toolkit: https://python-prompt-toolkit.readthedocs.io/
diff --git a/pw_console/embedding.rst b/pw_console/embedding.rst
index 07a944324..65724e9fc 100644
--- a/pw_console/embedding.rst
+++ b/pw_console/embedding.rst
@@ -3,6 +3,9 @@
===============
Embedding Guide
===============
+.. pigweed-module-subpage::
+ :name: pw_console
+ :tagline: pw_console: Multi-purpose pluggable interactive console for dev & manufacturing
-------------
Using embed()
@@ -89,16 +92,17 @@ following code will create a log message with two custom columns titled
}
)
-
---------------------
Debugging Serial Data
---------------------
``pw_console`` is often used to communicate with devices using `pySerial
-<https://pythonhosted.org/pyserial/>`_ and it may be necessary to monitor the
-raw data flowing over the wire to help with debugging. ``pw_console`` provides a
-simple wrapper for a pySerial instances that log data for each read and write
-call.
+<https://pythonhosted.org/pyserial/>`_ or
+``pw_console.socket_client.SocketClient``. To monitor the raw data flowing over
+the wire, ``pw_console`` provides simple wrappers for pySerial and socket client
+instances that log data for each read and write call.
+Logging data with PySerial
+==========================
.. code-block:: python
# Instead of 'import serial' use this import:
@@ -106,7 +110,28 @@ call.
serial_device = SerialWithLogging('/dev/ttyUSB0', 115200, timeout=1)
-With the above example each ``serial_device.read`` and ``write`` call will
+Logging data with sockets
+=========================
+.. code-block:: python
+
+ from pw_console.socket_client import SocketClientWithLogging
+
+ # Name resolution with explicit port
+ serial_device = SocketClientWithLogging('localhost:1234')
+ # Name resolution with default port.
+ serial_device = SocketClientWithLogging('pigweed.dev')
+ # Link-local IPv6 address with explicit port.
+ serial_device = SocketClientWithLogging('[fe80::100%enp1s0]:1234')
+ # Link-local IPv6 address with default port.
+ serial_device = SocketClientWithLogging('[fe80::100%enp1s0]')
+ # IPv4 address with port.
+ serial_device = SocketClientWithLogging('1.2.3.4:5678')
+
+.. tip::
+ The ``SocketClient`` takes an optional callback called when a disconnect is
+ detected. The ``pw_system`` console provides an example reconnect routine.
+
+With the above examples each ``serial_device.read`` and ``write`` call will
create a log message to the ``pw_console.serial_debug_logger`` Python
logger. This logger can then be included as a log window pane in the
``PwConsoleEmbed()`` call.
diff --git a/pw_console/internals.rst b/pw_console/internals.rst
index c204a1e3f..8ce8efcf7 100644
--- a/pw_console/internals.rst
+++ b/pw_console/internals.rst
@@ -2,6 +2,9 @@
Internal Design
===============
+.. pigweed-module-subpage::
+ :name: pw_console
+ :tagline: pw_console: Multi-purpose pluggable interactive console for dev & manufacturing
Threads and Event Loops
-----------------------
diff --git a/pw_console/plugins.rst b/pw_console/plugins.rst
index e4e5cbaae..acd5d48af 100644
--- a/pw_console/plugins.rst
+++ b/pw_console/plugins.rst
@@ -3,6 +3,10 @@
============
Plugin Guide
============
+.. pigweed-module-subpage::
+ :name: pw_console
+ :tagline: pw_console: Multi-purpose pluggable interactive console for dev & manufacturing
+
Pigweed Console supports extending the user interface with custom widgets. For
example: Toolbars that display device information and provide buttons for
interacting with the device.
@@ -78,6 +82,13 @@ Pigweed Console will provide a few sample plugins to serve as templates for
creating your own plugins. These are a work in progress at the moment and not
available at this time.
+Bandwidth Toolbar
+=================
+Tracks and logs the data sent and received over a serial transport like a socket
+or PySerial device. To use in a custom transport interface instantiate the
+``SerialBandwidthTracker`` and call ``track_read_data`` on incoming data bytes
+and ``track_write_data`` on outoing data bytes.
+
Calculator
==========
This plugin is similar to the full-screen `calculator.py example`_ provided in
diff --git a/pw_console/py/BUILD.bazel b/pw_console/py/BUILD.bazel
index a10a1c437..9fdeebd91 100644
--- a/pw_console/py/BUILD.bazel
+++ b/pw_console/py/BUILD.bazel
@@ -71,6 +71,7 @@ py_library(
"pw_console/quit_dialog.py",
"pw_console/repl_pane.py",
"pw_console/search_toolbar.py",
+ "pw_console/socket_client.py",
"pw_console/style.py",
"pw_console/templates/__init__.py",
"pw_console/test_mode.py",
@@ -100,6 +101,12 @@ py_library(
":pyserial_wrapper",
"//pw_cli/py:pw_cli",
"//pw_log_tokenized/py:pw_log_tokenized",
+ "@python_packages_jinja2//:pkg",
+ "@python_packages_prompt_toolkit//:pkg",
+ "@python_packages_ptpython//:pkg",
+ "@python_packages_pyperclip//:pkg",
+ "@python_packages_pyyaml//:pkg",
+ "@python_packages_websockets//:pkg",
],
)
@@ -166,6 +173,7 @@ py_test(
],
deps = [
":pw_console",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -188,6 +196,18 @@ py_test(
],
deps = [
":pw_console",
+ "@python_packages_parameterized//:pkg",
+ ],
+)
+
+py_test(
+ name = "socket_client_test",
+ size = "small",
+ srcs = [
+ "socket_client_test.py",
+ ],
+ deps = [
+ ":pw_console",
],
)
@@ -210,6 +230,7 @@ py_test(
],
deps = [
":pw_console",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -221,6 +242,7 @@ py_test(
],
deps = [
":pw_console",
+ "@python_packages_parameterized//:pkg",
],
)
diff --git a/pw_console/py/BUILD.gn b/pw_console/py/BUILD.gn
index 9e053e2e7..3683004d5 100644
--- a/pw_console/py/BUILD.gn
+++ b/pw_console/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_console/__init__.py",
@@ -63,6 +62,7 @@ pw_python_package("py") {
"pw_console/quit_dialog.py",
"pw_console/repl_pane.py",
"pw_console/search_toolbar.py",
+ "pw_console/socket_client.py",
"pw_console/style.py",
"pw_console/templates/__init__.py",
"pw_console/test_mode.py",
@@ -87,6 +87,7 @@ pw_python_package("py") {
"log_store_test.py",
"log_view_test.py",
"repl_pane_test.py",
+ "socket_client_test.py",
"table_test.py",
"text_formatting_test.py",
"window_manager_test.py",
diff --git a/pw_console/py/command_runner_test.py b/pw_console/py/command_runner_test.py
index 822b46488..03b7beaaa 100644
--- a/pw_console/py/command_runner_test.py
+++ b/pw_console/py/command_runner_test.py
@@ -16,7 +16,7 @@
import logging
import re
import unittest
-from typing import Callable, List, Tuple
+from typing import List
from unittest.mock import MagicMock
@@ -26,6 +26,7 @@ from prompt_toolkit.output import ColorDepth
# inclusive-language: ignore
from prompt_toolkit.output import DummyOutput as FakeOutput
+from pw_console.command_runner import CommandRunnerItem
from pw_console.console_app import ConsoleApp
from pw_console.console_prefs import ConsolePrefs
from pw_console.text_formatting import (
@@ -89,10 +90,8 @@ class TestCommandRunner(unittest.TestCase):
with create_app_session(output=FakeOutput()):
console_app = _create_console_app(log_pane_count=2)
flattened_menu_items = [
- text
- # pylint: disable=line-too-long
- for text, handler in console_app.command_runner.load_menu_items()
- # pylint: enable=line-too-long
+ item.title
+ for item in console_app.command_runner.load_menu_items()
]
# Check some common menu items exist.
@@ -126,12 +125,16 @@ class TestCommandRunner(unittest.TestCase):
def empty_handler() -> None:
return None
- def get_completions() -> List[Tuple[str, Callable]]:
+ def get_completions() -> List[CommandRunnerItem]:
return [
- ('[File] > Open Logger', empty_handler),
- ('[Windows] > 1: Host Logs > Show/Hide', empty_handler),
- ('[Windows] > 2: Device Logs > Show/Hide', empty_handler),
- ('[Help] > User Guide', empty_handler),
+ CommandRunnerItem('[File] > Open Logger', empty_handler),
+ CommandRunnerItem(
+ '[Windows] > 1: Host Logs > Show/Hide', empty_handler
+ ),
+ CommandRunnerItem(
+ '[Windows] > 2: Device Logs > Show/Hide', empty_handler
+ ),
+ CommandRunnerItem('[Help] > User Guide', empty_handler),
]
command_runner.filter_completions.assert_not_called()
@@ -301,7 +304,7 @@ class TestCommandRunner(unittest.TestCase):
command_runner._make_regexes.assert_called_once() # pylint: disable=protected-access
self.assertIn(
- '[View] > Move Window Right', command_runner.selected_item_text
+ '[View] > Move Window Right', command_runner.selected_item_title
)
# Run the Move Window Right action
command_runner._run_selected_item() # pylint: disable=protected-access
diff --git a/pw_console/py/pw_console/__main__.py b/pw_console/py/pw_console/__main__.py
index 2881b0a5c..867292570 100644
--- a/pw_console/py/pw_console/__main__.py
+++ b/pw_console/py/pw_console/__main__.py
@@ -69,11 +69,13 @@ def _build_argument_parser() -> argparse.ArgumentParser:
return parser
-def main() -> int:
+def main(args: Optional[argparse.Namespace] = None) -> int:
"""Pigweed Console."""
parser = _build_argument_parser()
- args = parser.parse_args()
+
+ if args is None:
+ args = parser.parse_args()
if not args.logfile:
# Create a temp logfile to prevent logs from appearing over stdout. This
diff --git a/pw_console/py/pw_console/command_runner.py b/pw_console/py/pw_console/command_runner.py
index e2caf1212..63003cb6d 100644
--- a/pw_console/py/pw_console/command_runner.py
+++ b/pw_console/py/pw_console/command_runner.py
@@ -14,6 +14,7 @@
"""CommandRunner dialog classes."""
from __future__ import annotations
+import dataclasses
import functools
import logging
import re
@@ -24,10 +25,10 @@ from typing import (
List,
Optional,
TYPE_CHECKING,
- Tuple,
)
from prompt_toolkit.buffer import Buffer
+from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
@@ -48,8 +49,10 @@ from prompt_toolkit.layout import (
Window,
WindowAlign,
)
-from prompt_toolkit.widgets import MenuItem
-from prompt_toolkit.widgets import TextArea
+from prompt_toolkit.lexers import PygmentsLexer
+from prompt_toolkit.widgets import MenuItem, TextArea
+
+from pygments.lexers.markup import MarkdownLexer # type: ignore
from pw_console.widgets import (
create_border,
@@ -63,9 +66,16 @@ if TYPE_CHECKING:
_LOG = logging.getLogger(__package__)
+@dataclasses.dataclass
+class CommandRunnerItem:
+ title: str
+ handler: Callable
+ description: Optional[str] = None
+
+
def flatten_menu_items(
items: List[MenuItem], prefix: str = ''
-) -> Iterator[Tuple[str, Callable]]:
+) -> Iterator[CommandRunnerItem]:
"""Flatten nested prompt_toolkit MenuItems into text and callable tuples."""
for item in items:
new_text = []
@@ -80,7 +90,7 @@ def flatten_menu_items(
# Skip this item if it's a separator or disabled.
if item.text == '-' or item.disabled:
continue
- yield (new_prefix, item.handler)
+ yield CommandRunnerItem(title=new_prefix, handler=item.handler)
def highlight_matches(
@@ -122,7 +132,7 @@ class CommandRunner:
application: ConsoleApp,
window_title: Optional[str] = None,
load_completions: Optional[
- Callable[[], List[Tuple[str, Callable]]]
+ Callable[[], List[CommandRunnerItem]]
] = None,
width: int = 80,
height: int = 10,
@@ -136,14 +146,15 @@ class CommandRunner:
self.last_focused_pane = None
# List of all possible completion items
- self.completions: List[Tuple[str, Callable]] = []
+ self.completions: List[CommandRunnerItem] = []
# Formatted text fragments of matched items
self.completion_fragments: List[StyleAndTextTuples] = []
# Current selected item tracking variables
self.selected_item: int = 0
- self.selected_item_text: str = ''
+ self.selected_item_title: str = ''
self.selected_item_handler: Optional[Callable] = None
+ self.selected_item_description: Optional[str] = None
# Previous input text
self.last_input_field_text: str = 'EMPTY'
# Previous selected item
@@ -155,7 +166,7 @@ class CommandRunner:
self.window_title: str
# Callable to fetch completion items
- self.load_completions: Callable[[], List[Tuple[str, Callable]]]
+ self.load_completions: Callable[[], List[CommandRunnerItem]]
# Command runner text input field
self.input_field = TextArea(
@@ -209,6 +220,16 @@ class CommandRunner:
height=self.height,
)
+ self.selected_item_description_text_area = TextArea(
+ focusable=False,
+ focus_on_click=False,
+ scrollbar=False,
+ style='class:help_window_content',
+ wrap_lines=False,
+ lexer=PygmentsLexer(MarkdownLexer),
+ text='empty',
+ )
+
# Main content HSplit
self.command_runner_content = HSplit(
[
@@ -221,6 +242,22 @@ class CommandRunner:
),
# Completion items below
command_items_window,
+ # Selected item description / help text.
+ ConditionalContainer(
+ create_border(
+ self.selected_item_description_text_area,
+ title=self._snippet_description_pane_title,
+ border_style='class:command-runner-border',
+ left_margin_columns=0,
+ right_margin_columns=0,
+ bottom=False,
+ left=False,
+ right=False,
+ ),
+ filter=Condition(
+ lambda: bool(self.selected_item_description)
+ ),
+ ),
],
style='class:command-runner class:theme-fg-default',
)
@@ -236,6 +273,16 @@ class CommandRunner:
filter=Condition(lambda: self.show_dialog),
)
+ def _snippet_description_pane_title(self) -> StyleAndTextTuples:
+ return [
+ # Left padding
+ ('', '━━ '),
+ # Snippet title in yellow
+ ('class:theme-fg-yellow', self.selected_item_title),
+ # right padding
+ ('', ' '),
+ ]
+
def _create_bordered_content(self) -> None:
"""Wrap self.command_runner_content in a border."""
# This should be called whenever the window_title changes.
@@ -311,7 +358,7 @@ class CommandRunner:
self,
window_title: Optional[str] = None,
load_completions: Optional[
- Callable[[], List[Tuple[str, Callable]]]
+ Callable[[], List[CommandRunnerItem]]
] = None,
) -> None:
"""Set window title and callable to fetch possible completions.
@@ -336,7 +383,7 @@ class CommandRunner:
def reload_completions(self) -> None:
self.completions = self.load_completions()
- def load_menu_items(self) -> List[Tuple[str, Callable]]:
+ def load_menu_items(self) -> List[CommandRunnerItem]:
# pylint: disable=no-self-use
return list(flatten_menu_items(self.application.menu_items))
@@ -377,17 +424,27 @@ class CommandRunner:
check_match = self._matches_orderless
i = 0
- for text, handler in self.completions:
- if not (input_text == '' or check_match(regexes, text)):
+ for item in self.completions:
+ title = item.title
+ if not (input_text == '' or check_match(regexes, item.title)):
continue
style = ''
if i == self.selected_item:
style = 'class:command-runner-selected-item'
- self.selected_item_text = text
- self.selected_item_handler = handler
- text = text.ljust(self.content_width())
+ self.selected_item_title = title
+ self.selected_item_handler = item.handler
+ self.selected_item_description = item.description
+ if self.selected_item_description:
+ self.selected_item_description_text_area.buffer.document = (
+ Document(
+ text=self.selected_item_description,
+ cursor_position=0,
+ )
+ )
+
+ title = item.title.ljust(self.content_width())
fragments: StyleAndTextTuples = highlight_matches(
- regexes, [(style, text + '\n')]
+ regexes, [(style, title + '\n')]
)
self.completion_fragments.append(fragments)
i += 1
@@ -470,8 +527,9 @@ class CommandRunner:
def _reset_selected_item(self) -> None:
self.selected_item = 0
self.last_selected_item = 0
- self.selected_item_text = ''
+ self.selected_item_title = ''
self.selected_item_handler = None
+ self.selected_item_description = None
self.last_input_field_text = 'EMPTY'
self.input_field.buffer.reset()
@@ -493,7 +551,7 @@ class CommandRunner:
'[File] > Insert Repl History',
'[File] > Open Logger',
]:
- if command_text in self.selected_item_text:
+ if command_text in self.selected_item_title:
close_dialog = False
break
@@ -509,7 +567,7 @@ class CommandRunner:
'Save/Export a copy',
'[Windows] > Floating ',
]:
- if command_text in self.selected_item_text:
+ if command_text in self.selected_item_title:
close_dialog_first = True
break
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index 38f0bfa38..d27ea7308 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -14,6 +14,7 @@
"""ConsoleApp control class."""
import asyncio
+import base64
import builtins
import functools
import socketserver
@@ -21,13 +22,16 @@ import importlib.resources
import logging
import os
from pathlib import Path
+import subprocess
import sys
+import tempfile
import time
from threading import Thread
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
from jinja2 import Environment, DictLoader, make_logging_undefined
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
+from prompt_toolkit.clipboard import ClipboardData
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.output import ColorDepth
from prompt_toolkit.application import Application
@@ -57,8 +61,9 @@ from ptpython.key_bindings import ( # type: ignore
load_python_bindings,
load_sidebar_bindings,
)
+from pyperclip import PyperclipException # type: ignore
-from pw_console.command_runner import CommandRunner
+from pw_console.command_runner import CommandRunner, CommandRunnerItem
from pw_console.console_log_server import (
ConsoleLogHTTPRequestHandler,
pw_console_http_server,
@@ -504,6 +509,58 @@ class ConsoleApp:
)
return call_function
+ def set_system_clipboard_data(self, data: ClipboardData) -> str:
+ return self.set_system_clipboard(data.text)
+
+ def set_system_clipboard(self, text: str) -> str:
+ """Set the host system clipboard.
+
+ The following methods are attempted in order:
+
+ - The pyperclip package which uses various cross platform methods.
+ - Teminal OSC 52 escape sequence which works on some terminal emulators
+ such as: iTerm2 (MacOS), Alacritty, xterm.
+ - Tmux paste buffer via the load-buffer command. This only happens if
+ pw-console is running inside tmux. You can paste in tmux by pressing:
+ ctrl-b =
+ """
+ copied = False
+ copy_methods = []
+ try:
+ self.application.clipboard.set_text(text)
+
+ copied = True
+ copy_methods.append('system clipboard')
+ except PyperclipException:
+ pass
+
+ # Set the clipboard via terminal escape sequence.
+ b64_data = base64.b64encode(text.encode('utf-8'))
+ sys.stdout.write(f"\x1B]52;c;{b64_data.decode('utf-8')}\x07")
+ _LOG.debug('Clipboard set via teminal escape sequence')
+ copy_methods.append('teminal')
+ copied = True
+
+ if os.environ.get('TMUX'):
+ with tempfile.NamedTemporaryFile(
+ prefix='pw_console_clipboard_',
+ delete=True,
+ ) as clipboard_file:
+ clipboard_file.write(text.encode('utf-8'))
+ clipboard_file.flush()
+ subprocess.run(
+ ['tmux', 'load-buffer', '-w', clipboard_file.name]
+ )
+ _LOG.debug('Clipboard set via tmux load-buffer')
+ copy_methods.append('tmux')
+ copied = True
+
+ message = ''
+ if copied:
+ message = 'Copied to: '
+ message += ', '.join(copy_methods)
+ return message
+
def update_menu_items(self):
self.menu_items = self._create_menu_items()
self.root_container.menu_items = self.menu_items
@@ -521,11 +578,11 @@ class ConsoleApp:
if not self.command_runner_is_open():
self.command_runner.open_dialog()
- def _create_logger_completions(self) -> List[Tuple[str, Callable]]:
- completions: List[Tuple[str, Callable]] = [
- (
- 'root',
- functools.partial(
+ def _create_logger_completions(self) -> List[CommandRunnerItem]:
+ completions: List[CommandRunnerItem] = [
+ CommandRunnerItem(
+ title='root',
+ handler=functools.partial(
self.open_new_log_pane_for_logger, '', window_title='root'
),
),
@@ -535,9 +592,9 @@ class ConsoleApp:
for logger_name in all_logger_names:
completions.append(
- (
- logger_name,
- functools.partial(
+ CommandRunnerItem(
+ title=logger_name,
+ handler=functools.partial(
self.open_new_log_pane_for_logger, logger_name
),
)
@@ -552,15 +609,15 @@ class ConsoleApp:
if not self.command_runner_is_open():
self.command_runner.open_dialog()
- def _create_history_completions(self) -> List[Tuple[str, Callable]]:
+ def _create_history_completions(self) -> List[CommandRunnerItem]:
return [
- (
- description,
- functools.partial(
+ CommandRunnerItem(
+ title=title,
+ handler=functools.partial(
self.repl_pane.insert_text_into_input_buffer, text
),
)
- for description, text in self.repl_pane.history_completions()
+ for title, text in self.repl_pane.history_completions()
]
def open_command_runner_snippets(self) -> None:
@@ -593,16 +650,24 @@ class ConsoleApp:
)
server_thread.start()
- def _create_snippet_completions(self) -> List[Tuple[str, Callable]]:
- completions: List[Tuple[str, Callable]] = [
- (
- description,
- functools.partial(
- self.repl_pane.insert_text_into_input_buffer, text
- ),
+ def _create_snippet_completions(self) -> List[CommandRunnerItem]:
+ completions: List[CommandRunnerItem] = []
+
+ for snippet in self.prefs.snippet_completions():
+ fenced_code = f'```python\n{snippet.code.strip()}\n```'
+ description = '\n' + fenced_code + '\n'
+ if snippet.description:
+ description += '\n' + snippet.description.strip() + '\n'
+ completions.append(
+ CommandRunnerItem(
+ title=snippet.title,
+ handler=functools.partial(
+ self.repl_pane.insert_text_into_input_buffer,
+ snippet.code,
+ ),
+ description=description,
+ )
)
- for description, text in self.prefs.snippet_completions()
- ]
return completions
@@ -1007,6 +1072,15 @@ class ConsoleApp:
self.update_menu_items()
self._update_help_window()
+ def all_log_stores(self) -> List[LogStore]:
+ log_stores: List[LogStore] = []
+ for pane in self.window_manager.active_panes():
+ if not isinstance(pane, LogPane):
+ continue
+ if pane.log_view.log_store not in log_stores:
+ log_stores.append(pane.log_view.log_store)
+ return log_stores
+
def add_log_handler(
self,
window_title: str,
diff --git a/pw_console/py/pw_console/console_prefs.py b/pw_console/py/pw_console/console_prefs.py
index 94219cf7b..966d6ba21 100644
--- a/pw_console/py/pw_console/console_prefs.py
+++ b/pw_console/py/pw_console/console_prefs.py
@@ -13,9 +13,10 @@
# the License.
"""pw_console preferences"""
+import dataclasses
import os
from pathlib import Path
-from typing import Dict, Callable, List, Tuple, Union
+from typing import Dict, Callable, List, Optional, Union
from prompt_toolkit.key_binding import KeyBindings
import yaml
@@ -70,6 +71,50 @@ class EmptyWindowList(Exception):
"""Exception for window lists with no content."""
+class EmptyPreviousPreviousDescription(Exception):
+ """Previous snippet description is empty for 'description: USE_PREVIOUS'."""
+
+
+@dataclasses.dataclass
+class CodeSnippet:
+ """Stores a single code snippet for inserting into the Python Repl.
+
+ Attributes:
+
+ title: The displayed title in the command runner window.
+ code: Python code text to be inserted.
+ description: Optional help text to be displayed below the snippet
+ selection window.
+ """
+
+ title: str
+ code: str
+ description: Optional[str] = None
+
+ @staticmethod
+ def from_yaml(
+ title: str,
+ value: Union[str, Dict],
+ previous_description: Optional[str] = None,
+ ) -> 'CodeSnippet':
+ if isinstance(value, str):
+ return CodeSnippet(title=title, code=value)
+
+ assert isinstance(value, dict)
+
+ code = value.get('code', None)
+ description = value.get('description', None)
+ if description == 'USE_PREVIOUS':
+ if not previous_description:
+ raise EmptyPreviousPreviousDescription(
+ f'\nERROR: pw_console.yaml snippet "{title}" has '
+ '"description: USE_PREVIOUS" but the previous snippet '
+ 'description is empty.'
+ )
+ description = previous_description
+ return CodeSnippet(title=title, code=code, description=description)
+
+
def error_unknown_window(
window_title: str, existing_pane_titles: List[str]
) -> None:
@@ -127,7 +172,7 @@ class ConsolePrefs(YamlConfigLoaderMixin):
environment_var='PW_CONSOLE_CONFIG_FILE',
)
- self._snippet_completions: List[Tuple[str, str]] = []
+ self._snippet_completions: List[CodeSnippet] = []
self.registered_commands = DEFAULT_KEY_BINDINGS
self.registered_commands.update(self.user_key_bindings)
@@ -329,32 +374,26 @@ class ConsolePrefs(YamlConfigLoaderMixin):
def user_snippets(self) -> Dict:
return self._config.get('user_snippets', {})
- def snippet_completions(self) -> List[Tuple[str, str]]:
+ def snippet_completions(self) -> List[CodeSnippet]:
if self._snippet_completions:
return self._snippet_completions
- all_descriptions: List[str] = []
- all_descriptions.extend(self.user_snippets.keys())
- all_descriptions.extend(self.snippets.keys())
- if not all_descriptions:
- return []
- max_description_width = max(
- len(description) for description in all_descriptions
- )
+ all_snippets: List[CodeSnippet] = []
- all_snippets: List[Tuple[str, str]] = []
- all_snippets.extend(self.user_snippets.items())
- all_snippets.extend(self.snippets.items())
-
- self._snippet_completions = [
- (
- description.ljust(max_description_width) + ' : ' +
- # Flatten linebreaks in the text.
- ' '.join([line.lstrip() for line in text.splitlines()]),
- # Pass original text as the completion result.
- text,
+ def previous_description() -> Optional[str]:
+ if not all_snippets:
+ return None
+ return all_snippets[-1].description
+
+ for title, value in self.user_snippets.items():
+ all_snippets.append(
+ CodeSnippet.from_yaml(title, value, previous_description())
+ )
+ for title, value in self.snippets.items():
+ all_snippets.append(
+ CodeSnippet.from_yaml(title, value, previous_description())
)
- for description, text in all_snippets
- ]
+
+ self._snippet_completions = all_snippets
return self._snippet_completions
diff --git a/pw_console/py/pw_console/docs/user_guide.rst b/pw_console/py/pw_console/docs/user_guide.rst
index 43da6d0d4..45cf7cc41 100644
--- a/pw_console/py/pw_console/docs/user_guide.rst
+++ b/pw_console/py/pw_console/docs/user_guide.rst
@@ -2,6 +2,9 @@
User Guide
==========
+.. pigweed-module-subpage::
+ :name: pw_console
+ :tagline: pw_console: Multi-purpose pluggable interactive console for dev & manufacturing
.. tip::
@@ -785,21 +788,140 @@ Example Config
log-pane.shift-line-to-center:
- z z
+ # Project and User defined Python snippets
+ # Press Ctrl-t the Python Repl to select which snippet to insert.
+
# Python Repl Snippets (Project owned)
snippets:
- Count Ten Times: |
- for i in range(10):
- print(i)
- Local Variables: |
- locals()
+ Count Ten Times:
+ code: |
+ for i in range(10):
+ print(i)
+ description: |
+ Print out 1 through 10 each on a separate line
+
+ Local Variables:
+ code: |
+ locals()
+ description: |
+ Return all local Python variables.
# Python Repl Snippets (User owned)
user_snippets:
- Pretty print format function: |
- import pprint
- _pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
- Global variables: |
- globals()
+ Pretty print format function:
+ code: |
+ import pprint
+ _pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+ description: |
+ Create a function named `_pretty_format` which returns a pretty
+ formatted string for a Python object.
+
+ Example:
+
+ ```python
+ from dataclasses import dataclass
+
+ @dataclass
+ class CodeSnippet:
+ title: str
+ code: str
+ description: str = ''
+
+ _pretty_format(CodeSnippet('one', '1'))
+ ```
+
+ The last line will return the string:
+
+ ```
+ "CodeSnippet(title='one', code='1', description='')"
+ ```
+
+ Global variables:
+ code: |
+ globals()
+ description: |
+ Return all global Python variables.
+
+
+Python Repl Snippets
+--------------------
+Python code snippets can be defined under the ``snippets:`` or
+``user_snippets:`` section. We suggest reserving ``user_snippets:`` for the user
+based config files (``$HOME/.pw_console.yaml`` or
+``$PW_PROJECT_ROOT/.pw_console.user.yaml``). ``snippets:`` is best suited for a
+project specific config file shared by all team members:
+``$PW_PROJECT_ROOT/.pw_console.yaml``
+
+Snippets consist of a title followed by ``code: |`` and optionally
+``description: |``. The YAML operator ``|`` will concatenate the following lines
+into a string and strip leading whitespace.
+
+
+.. code-block:: yaml
+
+ snippets:
+ Count Ten Times:
+ code: |
+ for i in range(10):
+ print(i)
+ description: |
+ Print out 1 through 10 each on a separate line
+
+Inserting this snippet will paste the for loop above into the Python Repl input
+window.
+
+Descriptions are markdown formatted and displayed below the selected snippet
+window. Fenced code blocks will have the correct syntax highlighting
+applied. For example the following will apply Python syntax highlighting to the
+code block below ``Example:``.
+
+.. code-block:: yaml
+
+ snippets:
+ Pretty print format function:
+ code: |
+ import pprint
+ _pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+ description: |
+ Create a function named `_pretty_format` which returns a pretty
+ formatted string for a Python object.
+
+ Example:
+
+ ```python
+ from dataclasses import dataclass
+
+ @dataclass
+ class CodeSnippet:
+ title: str
+ code: str
+ description: str = ''
+
+ _pretty_format(CodeSnippet('one', '1'))
+ ```
+
+If identical description text needs to be repeated for multiple snippets in a
+row you can set ``description: USE_PREVIOUS``. For example these two snippets
+will have the same description text:
+``Helper functions for pretty printing Python objects.``
+
+
+.. code-block:: yaml
+
+ snippets:
+ Pretty print format function:
+ code: |
+ import pprint
+ _pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+ description: |
+ Helper functions for pretty printing Python objects.
+
+ Pretty print function:
+ code: |
+ import pprint
+ _pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
+ description: USE_PREVIOUS
+
Changing Keyboard Shortcuts
diff --git a/pw_console/py/pw_console/help_window.py b/pw_console/py/pw_console/help_window.py
index d90a234db..9380b9aea 100644
--- a/pw_console/py/pw_console/help_window.py
+++ b/pw_console/py/pw_console/help_window.py
@@ -227,9 +227,7 @@ class HelpWindow(ConditionalContainer):
def copy_all_text(self):
"""Copy all text in the Python input to the system clipboard."""
- self.application.application.clipboard.set_text(
- self.help_text_area.buffer.text
- )
+ self.application.set_system_clipboard(self.help_text_area.buffer.text)
def toggle_display(self):
"""Toggle visibility of this help window."""
diff --git a/pw_console/py/pw_console/html/index.html b/pw_console/py/pw_console/html/index.html
index 5d7a21b09..f50d0cce0 100644
--- a/pw_console/py/pw_console/html/index.html
+++ b/pw_console/py/pw_console/html/index.html
@@ -18,20 +18,13 @@ the License.
</head>
<body>
- <div class="table-container">
- <div class="log-header">
- <div class="log-entry">
- <span class="timestamp">Time</span>
- <span class="level">Level</span>
- <span class="module">Module</span>
- <span class="time">Timestamp</span>
- <span class="keys">Keys</span>
- <span class="msg">Message</span>
- </div>
- </div>
- <div class="log-container"></div>
- </div>
+ <main id="log-viewer-container"></main>
- <script src="https://unpkg.com/virtualized-list@2.2.0/umd/virtualized-list.min.js"></script>
+ <!-- CDN pigweedjs -->
+ <script src="https://unpkg.com/pigweedjs/dist/logging.umd.js"></script>
+
+ <!-- Locally built pigweedjs -->
+ <!-- <script src="./index.umd.js"></script> -->
+ <!-- <script src="./logging.umd.js"></script> -->
<script src="./main.js"></script>
</body>
diff --git a/pw_console/py/pw_console/html/main.js b/pw_console/py/pw_console/html/main.js
index d08d019ed..79e239eca 100644
--- a/pw_console/py/pw_console/html/main.js
+++ b/pw_console/py/pw_console/html/main.js
@@ -12,136 +12,146 @@
// License for the specific language governing permissions and limitations under
// the License.
-var VirtualizedList = window.VirtualizedList.default;
-const rowHeight = 30;
+// eslint-disable-next-line no-undef
+const { createLogViewer, LogSource, LogEntry, Severity } = PigweedLogging;
-function formatDate(dt) {
- function pad2(n) {
- return (n < 10 ? '0' : '') + n;
- }
+let currentTheme = {};
+let defaultLogStyleRule = 'color: #ffffff;';
+let columnStyleRules = {};
+let defaultColumnStyles = [];
+let logLevelStyles = {};
- return dt.getFullYear() + pad2(dt.getMonth() + 1) + pad2(dt.getDate()) + ' ' +
- pad2(dt.getHours()) + ':' + pad2(dt.getMinutes()) + ':' +
- pad2(dt.getSeconds());
-}
+const logLevelToString = {
+ 10: 'DBG',
+ 20: 'INF',
+ 21: 'OUT',
+ 30: 'WRN',
+ 40: 'ERR',
+ 50: 'CRT',
+ 70: 'FTL',
+};
-let data = [];
-function clearLogs() {
- data = [{
- 'message': 'Logs started',
- 'levelno': 20,
- time: formatDate(new Date()),
- 'levelname': '\u001b[35m\u001b[1mINF\u001b[0m',
- 'args': [],
- 'fields': {'module': '', 'file': '', 'timestamp': '', 'keys': ''}
- }];
-}
-clearLogs();
+const logLevelToSeverity = {
+ 10: Severity.DEBUG,
+ 20: Severity.INFO,
+ 21: Severity.INFO,
+ 30: Severity.WARNING,
+ 40: Severity.ERROR,
+ 50: Severity.CRITICAL,
+ 70: Severity.CRITICAL,
+};
-let nonAdditionalDataFields =
- ['_hosttime', 'levelname', 'levelno', 'args', 'fields', 'message', 'time'];
+let nonAdditionalDataFields = [
+ '_hosttime',
+ 'levelname',
+ 'levelno',
+ 'args',
+ 'fields',
+ 'message',
+ 'time',
+];
let additionalHeaders = [];
-function updateHeadersFromData(data) {
- let dirty = false;
- Object.keys(data).forEach((columnName) => {
- if (nonAdditionalDataFields.indexOf(columnName) === -1 &&
- additionalHeaders.indexOf(columnName) === -1) {
- additionalHeaders.push(columnName);
- dirty = true;
- }
- });
- Object.keys(data.fields || {}).forEach((columnName) => {
- if (nonAdditionalDataFields.indexOf(columnName) === -1 &&
- additionalHeaders.indexOf(columnName) === -1) {
- additionalHeaders.push(columnName);
- dirty = true;
- }
- });
- const headerDOM = document.querySelector('.log-header');
- if (dirty) {
- headerDOM.innerHTML = `
- <span class="_hosttime">Time</span>
- <span class="level">Level</span>
- ${
- additionalHeaders
- .map((key) => `
- <span class="${key}">${key}</span>
- `).join('\n')}
- <span class="msg">Message</span>`
+// New LogSource to consume pw-console log json messages
+class PwConsoleLogSource extends LogSource {
+ constructor() {
+ super();
+ }
+ append_log(data) {
+ var fields = [
+ { key: 'severity', value: logLevelToSeverity[data.levelno] },
+ { key: 'time', value: data.time },
+ ];
+ Object.keys(data.fields).forEach((columnName) => {
+ if (
+ nonAdditionalDataFields.indexOf(columnName) === -1 &&
+ additionalHeaders.indexOf(columnName) === -1
+ ) {
+ fields.push({ key: columnName, value: data.fields[columnName] });
+ }
+ });
+ fields.push({ key: 'message', value: data.message });
+ fields.push({ key: 'py_file', value: data.py_file });
+ fields.push({ key: 'py_logger', value: data.py_logger });
+ this.emitEvent('logEntry', {
+ severity: logLevelToSeverity[data.levelno],
+ timestamp: new Date(),
+ fields: fields,
+ });
}
+}
- // Also update column widths to match actual row.
- const headerChildren = Array.from(headerDOM.children);
+// Setup the pigweedjs log-viewer
+const logSource = new PwConsoleLogSource();
+const containerEl = document.querySelector('#log-viewer-container');
+let unsubscribe = createLogViewer(logSource, containerEl);
- const firstRow = document.querySelector('.log-container .log-entry');
- const firstRowChildren = Array.from(firstRow.children);
- headerChildren.forEach((col, index) => {
- if (firstRowChildren[index]) {
- col.setAttribute(
- 'style',
- `width:${firstRowChildren[index].getBoundingClientRect().width}`);
- col.setAttribute('title', col.innerText);
- }
- })
+// Format a date in the standard pw_cli style YYYY-mm-dd HH:MM:SS
+function formatDate(dt) {
+ function pad2(n) {
+ return (n < 10 ? '0' : '') + n;
+ }
+
+ return (
+ dt.getFullYear() +
+ pad2(dt.getMonth() + 1) +
+ pad2(dt.getDate()) +
+ ' ' +
+ pad2(dt.getHours()) +
+ ':' +
+ pad2(dt.getMinutes()) +
+ ':' +
+ pad2(dt.getSeconds())
+ );
}
+// Return the value for the given # parameter name.
function getUrlHashParameter(param) {
var params = getUrlHashParameters();
return params[param];
}
+// Capture all # parameters from the current URL.
function getUrlHashParameters() {
var sPageURL = window.location.hash;
- if (sPageURL)
- sPageURL = sPageURL.split('#')[1];
+ if (sPageURL) sPageURL = sPageURL.split('#')[1];
var pairs = sPageURL.split('&');
var object = {};
- pairs.forEach(function(pair, i) {
+ pairs.forEach(function (pair, i) {
pair = pair.split('=');
- if (pair[0] !== '')
- object[pair[0]] = pair[1];
+ if (pair[0] !== '') object[pair[0]] = pair[1];
});
return object;
}
-let currentTheme = {};
-let defaultLogStyleRule = 'color: #ffffff;';
-let columnStyleRules = {};
-let defaultColumnStyles = [];
-let logLevelStyles = {};
-const logLevelToString = {
- 10: 'DBG',
- 20: 'INF',
- 21: 'OUT',
- 30: 'WRN',
- 40: 'ERR',
- 50: 'CRT',
- 70: 'FTL'
-};
+// Update web page CSS styles based on a pw-console color json log message.
function setCurrentTheme(newTheme) {
currentTheme = newTheme;
- defaultLogStyleRule = parseStyle(newTheme.default);
- document.querySelector('body').setAttribute('style', defaultLogStyleRule);
+ defaultLogStyleRule = parsePromptToolkitStyle(newTheme.default);
+ // Set body background color
+ // document.querySelector('body').setAttribute('style', defaultLogStyleRule);
+
// Apply default font styles to columns
let styles = [];
- Object.keys(newTheme).forEach(key => {
+ Object.keys(newTheme).forEach((key) => {
if (key.startsWith('log-table-column-')) {
styles.push(newTheme[key]);
}
if (key.startsWith('log-level-')) {
logLevelStyles[parseInt(key.replace('log-level-', ''))] =
- parseStyle(newTheme[key]);
+ parsePromptToolkitStyle(newTheme[key]);
}
});
defaultColumnStyles = styles;
}
-function parseStyle(rule) {
+// Convert prompt_toolkit color format strings to CSS.
+// 'bg:#BG-HEX #FG-HEX STYLE' where STYLE is either 'bold' or 'underline'
+function parsePromptToolkitStyle(rule) {
const ruleList = rule.split(' ');
- let outputStyle = ruleList.map(fragment => {
+ let outputStyle = ruleList.map((fragment) => {
if (fragment.startsWith('bg:')) {
- return `background-color: ${fragment.replace('bg:', '')}`
+ return `background-color: ${fragment.replace('bg:', '')}`;
} else if (fragment === 'bold') {
return `font-weight: bold`;
} else if (fragment === 'underline') {
@@ -150,32 +160,35 @@ function parseStyle(rule) {
return `color: ${fragment}`;
}
});
- return outputStyle.join(';')
+ return outputStyle.join(';');
}
+// Inject styled spans into the log message column values.
function applyStyling(data, applyColors = false) {
let colIndex = 0;
- Object.keys(data).forEach(key => {
+ Object.keys(data).forEach((key) => {
if (columnStyleRules[key] && typeof data[key] === 'string') {
- Object.keys(columnStyleRules[key]).forEach(token => {
+ Object.keys(columnStyleRules[key]).forEach((token) => {
data[key] = data[key].replaceAll(
- token,
- `<span
+ token,
+ `<span
style="${defaultLogStyleRule};${
- applyColors ? (defaultColumnStyles
- [colIndex % defaultColumnStyles.length]) :
- ''};${parseStyle(columnStyleRules[key][token])};">
+ applyColors
+ ? defaultColumnStyles[colIndex % defaultColumnStyles.length]
+ : ''
+ };${parsePromptToolkitStyle(columnStyleRules[key][token])};">
${token}
- </span>`);
+ </span>`,
+ );
});
} else if (key === 'fields') {
data[key] = applyStyling(data.fields, true);
}
if (applyColors) {
data[key] = `<span
- style="${
- parseStyle(
- defaultColumnStyles[colIndex % defaultColumnStyles.length])}">
+ style="${parsePromptToolkitStyle(
+ defaultColumnStyles[colIndex % defaultColumnStyles.length],
+ )}">
${data[key]}
</span>`;
}
@@ -184,78 +197,35 @@ function applyStyling(data, applyColors = false) {
return data;
}
-(function() {
-const container = document.querySelector('.log-container');
-const height = window.innerHeight - 50
-let follow = true;
-// Initialize our VirtualizedList
-var virtualizedList = new VirtualizedList(container, {
- height,
- rowCount: data.length,
- rowHeight: rowHeight,
- estimatedRowHeight: rowHeight,
- renderRow: (index) => {
- const element = document.createElement('div');
- element.classList.add('log-entry');
- element.setAttribute('style', `height: ${rowHeight}px;`);
- const logData = data[index];
- element.innerHTML = `
- <span class="time">${logData.time}</span>
- <span class="level" style="${logLevelStyles[logData.levelno] || ''}">${
- logLevelToString[logData.levelno]}</span>
- ${
- additionalHeaders
- .map(
- (key) => `
- <span class="${key}">${
- logData[key] || logData.fields[key] || ''}</span>
- `).join('\n')}
- <span class="msg">${logData.message}</span>
- `;
- return element;
- },
- initialIndex: 0,
- onScroll: (scrollTop, event) => {
- const offset =
- virtualizedList._sizeAndPositionManager.getUpdatedOffsetForIndex({
- containerSize: height,
- targetIndex: data.length - 1,
- });
-
- if (scrollTop < offset) {
- follow = false;
- } else {
- follow = true;
- }
- }
-});
-
-const port = getUrlHashParameter('ws')
-const hostname = location.hostname || '127.0.0.1';
-var ws = new WebSocket(`ws://${hostname}:${port}/`);
-ws.onmessage = function(event) {
- let dataObj;
- try {
- dataObj = JSON.parse(event.data);
- } catch (e) {
- }
- if (!dataObj)
- return;
-
- if (dataObj.__pw_console_colors) {
- const colors = dataObj.__pw_console_colors;
- setCurrentTheme(colors.classes);
- if (colors.column_values) {
- columnStyleRules = {...colors.column_values};
+// Connect to the pw-console websocket and start emitting logs.
+(function () {
+ const container = document.querySelector('.log-container');
+ const height = window.innerHeight - 50;
+ let follow = true;
+
+ const port = getUrlHashParameter('ws');
+ const hostname = location.hostname || '127.0.0.1';
+ var ws = new WebSocket(`ws://${hostname}:${port}/`);
+ ws.onmessage = function (event) {
+ let dataObj;
+ try {
+ dataObj = JSON.parse(event.data);
+ } catch (e) {
+ // empty
}
- } else {
- const currentData = {...dataObj, time: formatDate(new Date())};
- updateHeadersFromData(currentData);
- data.push(applyStyling(currentData));
- virtualizedList.setRowCount(data.length);
- if (follow) {
- virtualizedList.scrollToIndex(data.length - 1);
+ if (!dataObj) return;
+
+ if (dataObj.__pw_console_colors) {
+ // If this is a color theme message, update themes.
+ const colors = dataObj.__pw_console_colors;
+ setCurrentTheme(colors.classes);
+ if (colors.column_values) {
+ columnStyleRules = { ...colors.column_values };
+ }
+ } else {
+ // Normal log message.
+ const currentData = { ...dataObj, time: formatDate(new Date()) };
+ logSource.append_log(currentData);
}
- }
-};
+ };
})();
diff --git a/pw_console/py/pw_console/html/style.css b/pw_console/py/pw_console/html/style.css
index 2c92a48d0..0cd5b6a76 100644
--- a/pw_console/py/pw_console/html/style.css
+++ b/pw_console/py/pw_console/html/style.css
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Pigweed Authors
+ * Copyright 2023 The Pigweed Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
@@ -13,62 +13,205 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-body {
- background-color: rgb(46, 46, 46);
- color: #ffffff;
- overflow: hidden;
- margin: 0;
-}
-.table-container {
- display: table;
- width: 100%;
- border-spacing: 30px 0px;
+ @import url('https://fonts.googleapis.com/css2?family=Google+Sans&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block');
+
+:root {
+ background-color: var(--sys-log-viewer-color-bg);
+ color-scheme: light dark;
+ font-family: "Google Sans", Arial, sans-serif;
+ font-synthesis: none;
+ font-weight: 400;
+ line-height: 1.5;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
}
-.log-header {
- font-size: 18px;
- font-family: monospace;
+
+:root {
+ /* Material component properties */
+ --md-icon-font: 'Material Symbols Rounded';
+ --md-icon-size: 1.25rem;
+ --md-filled-button-label-text-type: "Google Sans", Arial, sans-serif;
+ --md-outlined-button-label-text-type: "Google Sans", Arial, sans-serif;
+ --md-icon-button-unselected-icon-color: var(--md-sys-color-on-surface-variant);
+ --md-icon-button-unselected-hover-icon-color: var(--md-sys-color-on-primary-container);
+
+ /* Log View */
+ --sys-log-viewer-view-outline-width: 1px;
+ --sys-log-viewer-view-corner-radius: 0.5rem;
}
-.log-container {
- width: 100%;
- height: calc(100vh - 50px);
- overflow-y: auto;
- border-top: 1px solid #DDD;
- font-size: 18px;
- font-family: monospace;
+* {
+ box-sizing: border-box;
}
-.log-header {
- width: 100%;
- font-weight: bold;
- display: table-row;
+button {
+ font-family: "Google Sans";
}
-.log-container .row>span {
- display: table-cell;
- padding: 20px 18px;
+main {
+ height: 100vh;
+ padding: 2rem;
+}
+a {
+ color: var(--md-sys-color-primary);
+ font-weight: 500;
+ text-decoration: inherit;
}
-.log-header>span {
- text-transform: capitalize;
- overflow: hidden;
- display: inline-block;
- margin-left: 30px;
+a:hover {
+ color: var(--md-sys-color-secondary);
}
-.log-entry {
- display: table-row;
+body {
+ display: grid;
+ place-content: start;
+ margin: 0;
}
-.log-entry>span {
- display: table-cell;
- overflow: hidden;
- text-overflow: ellipsis;
+@media (prefers-color-scheme: dark) {
+ :root {
+ --md-sys-color-primary: #A8C7FA;
+ --md-sys-color-primary-60: #4C8DF6;
+ --md-sys-color-primary-container: #0842A0;
+ --md-sys-color-on-primary: #062E6F;
+ --md-sys-color-on-primary-container: #D3E3FD;
+ --md-sys-color-inverse-primary: #0B57D0;
+ --md-sys-color-secondary: #7FCFFF;
+ --md-sys-color-secondary-container: #004A77;
+ --md-sys-color-on-secondary: #003355;
+ --md-sys-color-on-secondary-container: #C2E7FF;
+ --md-sys-color-tertiary: #6DD58C;
+ --md-sys-color-tertiary-container: #0F5223;
+ --md-sys-color-on-tertiary: #0A3818;
+ --md-sys-color-on-tertiary-container: #C4EED0;
+ --md-sys-color-surface: #131314;
+ --md-sys-color-surface-dim: #131314;
+ --md-sys-color-surface-bright: #37393B;
+ --md-sys-color-surface-container-lowest: #0E0E0E;
+ --md-sys-color-surface-container-low: #1B1B1B;
+ --md-sys-color-surface-container: #1E1F20;
+ --md-sys-color-surface-container-high: #282A2C;
+ --md-sys-color-surface-container-highest: #333537;
+ --md-sys-color-on-surface: #E3E3E3;
+ --md-sys-color-on-surface-variant: #C4C7C5;
+ --md-sys-color-inverse-surface: #E3E3E3;
+ --md-sys-color-inverse-on-surface: #303030;
+ --md-sys-color-outline: #8E918F;
+ --md-sys-color-outline-variant: #444746;
+ --md-sys-color-shadow: #000000;
+ --md-sys-color-scrim: #000000;
+
+ --md-sys-inverse-surface-rgb: 230, 225, 229;
+
+ /* Log Viewer */
+ --sys-log-viewer-color-bg: var(--md-sys-color-surface);
+
+ /* Log View */
+ --sys-log-viewer-color-view-outline: var(--md-sys-color-outline-variant);
+
+ /* Log View Controls */
+ --sys-log-viewer-color-controls-bg: var(--md-sys-color-surface-container-high);
+ --sys-log-viewer-color-controls-text: var(--md-sys-color-on-surface-variant);
+ --sys-log-viewer-color-controls-input-outline: transparent;
+ --sys-log-viewer-color-controls-input-bg: var(--md-sys-color-surface);
+ --sys-log-viewer-color-controls-button-enabled: var(--md-sys-color-primary-container);
+
+ /* Log List */
+ --sys-log-viewer-color-table-header-bg: var(--md-sys-color-surface-container);
+ --sys-log-viewer-color-table-header-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-bg: var(--md-sys-color-surface-container-lowest);
+ --sys-log-viewer-color-table-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-cell-outline: var(--md-sys-color-outline-variant);
+ --sys-log-viewer-color-overflow-indicator: var(--md-sys-color-surface-container-lowest);
+ --sys-log-viewer-color-table-mark: var(--md-sys-color-primary-container);
+ --sys-log-viewer-color-table-mark-text: var(--md-sys-color-on-primary-container);
+ --sys-log-viewer-color-table-mark-outline: var(--md-sys-color-outline-variant);
+
+ /* Severity */
+ --sys-log-viewer-color-error-bright: #E46962;
+ --sys-log-viewer-color-surface-error: #601410;
+ --sys-log-viewer-color-on-surface-error: #F9DEDC;
+ --sys-log-viewer-color-orange-bright: #EE9836;
+ --sys-log-viewer-color-surface-yellow: #402D00;
+ --sys-log-viewer-color-on-surface-yellow: #FFDFA0;
+ --sys-log-viewer-color-debug: var(--md-sys-color-primary-60);
+ }
}
-.log-entry .msg {
- flex: 1;
+@media (prefers-color-scheme: light) {
+ :root {
+ --md-sys-color-primary: #0B57D0;
+ --md-sys-color-primary-70: #7CACF8;
+ --md-sys-color-primary-90: #D3E3FD;
+ --md-sys-color-primary-95: #ECF3FE;
+ --md-sys-color-primary-99: #FAFBFF;
+ --md-sys-color-primary-container: #D3E3FD;
+ --md-sys-color-on-primary: #FFFFFF;
+ --md-sys-color-on-primary-container: #041E49;
+ --md-sys-color-inverse-primary: #A8C7FA;
+ --md-sys-color-secondary: #00639B;
+ --md-sys-color-secondary-container: #C2E7FF;
+ --md-sys-color-on-secondary: #FFFFFF;
+ --md-sys-color-on-secondary-container: #001D35;
+ --md-sys-color-tertiary: #146C2E;
+ --md-sys-color-tertiary-container: #C4EED0;
+ --md-sys-color-on-tertiary: #FFFFFF;
+ --md-sys-color-on-tertiary-container: #072711;
+ --md-sys-color-surface: #FFFFFF;
+ --md-sys-color-surface-dim: #D3DBE5;
+ --md-sys-color-surface-bright: #FFFFFF;
+ --md-sys-color-surface-container-lowest: #FFFFFF;
+ --md-sys-color-surface-container-low: #F8FAFD;
+ --md-sys-color-surface-container: #F0F4F9;
+ --md-sys-color-surface-container-high: #E9EEF6;
+ --md-sys-color-surface-container-highest: #DDE3EA;
+ --md-sys-color-on-surface: #1F1F1F;
+ --md-sys-color-on-surface-variant: #444746;
+ --md-sys-color-inverse-surface: #303030;
+ --md-sys-color-inverse-on-surface: #F2F2F2;
+ --md-sys-color-outline: #747775;
+ --md-sys-color-outline-variant: #C4C7C5;
+ --md-sys-color-shadow: #000000;
+ --md-sys-color-scrim: #000000;
+
+ --md-sys-inverse-surface-rgb: 49, 48, 51;
+
+ /* Log Viewer */
+ --sys-log-viewer-color-bg: var(--md-sys-color-surface);
+
+ /* Log View */
+ --sys-log-viewer-color-view-outline: var(--md-sys-color-outline);
+
+ /* Log View Controls */
+ --sys-log-viewer-color-controls-bg: var(--md-sys-color-primary-90);
+ --sys-log-viewer-color-controls-text: var(--md-sys-color-on-primary-container);
+ --sys-log-viewer-color-controls-input-outline: transparent;
+ --sys-log-viewer-color-controls-input-bg: var(--md-sys-color-surface-container-lowest);
+ --sys-log-viewer-color-controls-button-enabled: var(--md-sys-color-primary-70);
+
+ /* Log List */
+ --sys-log-viewer-color-table-header-bg: var(--md-sys-color-primary-95);
+ --sys-log-viewer-color-table-header-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-bg: var(--md-sys-color-surface-container-lowest);
+ --sys-log-viewer-color-table-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-cell-outline: var(--md-sys-color-outline-variant);
+ --sys-log-viewer-color-overflow-indicator: var(--md-sys-color-surface-container);
+ --sys-log-viewer-color-table-mark: var(--md-sys-color-primary-container);
+ --sys-log-viewer-color-table-mark-text: var(--md-sys-color-on-primary-container);
+ --sys-log-viewer-color-table-mark-outline: var(--md-sys-color-outline-variant);
+
+ /* Severity */
+ --sys-log-viewer-color-error-bright: #DC362E;
+ --sys-log-viewer-color-surface-error: #FCEFEE;
+ --sys-log-viewer-color-on-surface-error: #8C1D18;
+ --sys-log-viewer-color-orange-bright: #F49F2A;
+ --sys-log-viewer-color-surface-yellow: #FEF9EB;
+ --sys-log-viewer-color-on-surface-yellow: #783616;
+ --sys-log-viewer-color-debug: var(--md-sys-color-primary);
+ }
}
diff --git a/pw_console/py/pw_console/key_bindings.py b/pw_console/py/pw_console/key_bindings.py
index 3db259217..33f55ded9 100644
--- a/pw_console/py/pw_console/key_bindings.py
+++ b/pw_console/py/pw_console/key_bindings.py
@@ -50,7 +50,7 @@ DEFAULT_KEY_BINDINGS: Dict[str, List[str]] = {
'log-pane.move-cursor-down': ['down', 'j'],
'log-pane.visual-select-up': ['s-up'],
'log-pane.visual-select-down': ['s-down'],
- 'log-pane.visual-select-all': ['N', 'c-r'],
+ 'log-pane.visual-select-all': ['c-r'],
'log-pane.deselect-cancel-search': ['c-c'],
'log-pane.scroll-page-up': ['pageup'],
'log-pane.scroll-page-down': ['pagedown'],
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index c9bfa8f84..500b978ea 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -183,6 +183,11 @@ class LogContentControl(UIControl):
"""Select next log line."""
self.log_view.visual_select_down()
+ @register('log-pane.visual-select-all', key_bindings)
+ def _select_all_logs(_event: KeyPressEvent) -> None:
+ """Select all log lines."""
+ self.log_pane.log_view.visual_select_all()
+
@register('log-pane.scroll-page-up', key_bindings)
def _pageup(_event: KeyPressEvent) -> None:
"""Scroll the logs up by one page."""
@@ -213,11 +218,6 @@ class LogContentControl(UIControl):
"""Previous search match."""
self.log_view.search_backwards()
- @register('log-pane.visual-select-all', key_bindings)
- def _select_all_logs(_event: KeyPressEvent) -> None:
- """Clear search."""
- self.log_pane.log_view.visual_select_all()
-
@register('log-pane.deselect-cancel-search', key_bindings)
def _clear_search_and_selection(_event: KeyPressEvent) -> None:
"""Clear selection or search."""
@@ -424,10 +424,11 @@ class LogPaneWebsocketDialog(ConditionalContainer):
self._last_action_message = text
def copy_url_to_clipboard(self) -> None:
- self.log_pane.application.application.clipboard.set_text(
+ result_message = self.log_pane.application.set_system_clipboard(
self.log_pane.log_view.get_web_socket_url()
)
- self._set_action_message('Copied!')
+ if result_message:
+ self._set_action_message(result_message)
def get_message_fragments(self):
"""Return FormattedText with the last action message."""
diff --git a/pw_console/py/pw_console/log_store.py b/pw_console/py/pw_console/log_store.py
index 8a8c4e83c..f56247674 100644
--- a/pw_console/py/pw_console/log_store.py
+++ b/pw_console/py/pw_console/log_store.py
@@ -147,6 +147,9 @@ class LogStore(logging.Handler):
self.channel_formatted_prefix_widths = {}
self.line_index = 0
+ def get_channel_names(self) -> List[str]:
+ return list(sorted(self.channel_counts.keys()))
+
def get_channel_counts(self):
"""Return the seen channel log counts for this conatiner."""
return ', '.join(
@@ -223,7 +226,7 @@ class LogStore(logging.Handler):
self.channel_counts.get(record.name, 0) + 1
)
- # TODO(b/235271486): Revisit calculating prefix widths automatically
+ # TODO: b/235271486 - Revisit calculating prefix widths automatically
# when line wrapping indentation is supported.
# Set the prefix width to 0
self.channel_formatted_prefix_widths[record.name] = 0
diff --git a/pw_console/py/pw_console/log_view.py b/pw_console/py/pw_console/log_view.py
index 74ebd3d30..da2c19516 100644
--- a/pw_console/py/pw_console/log_view.py
+++ b/pw_console/py/pw_console/log_view.py
@@ -166,9 +166,6 @@ class LogView:
self.websocket_port = self.websocket_server.ws_server.sockets[
0
].getsockname()[1]
- self.log_pane.application.application.clipboard.set_text(
- self.get_web_socket_url()
- )
self.websocket_running = True
self.websocket_loop.run_forever()
@@ -964,9 +961,7 @@ class LogView:
elif to_clipboard:
if add_markdown_fence:
text_output = '```\n' + text_output + '```\n'
- self.log_pane.application.application.clipboard.set_text(
- text_output
- )
+ self.log_pane.application.set_system_clipboard(text_output)
_LOG.debug('Copied logs to clipboard.')
return True
diff --git a/pw_console/py/pw_console/plugins/bandwidth_toolbar.py b/pw_console/py/pw_console/plugins/bandwidth_toolbar.py
index e0135a6c5..2f4ecfdc9 100644
--- a/pw_console/py/pw_console/plugins/bandwidth_toolbar.py
+++ b/pw_console/py/pw_console/plugins/bandwidth_toolbar.py
@@ -11,13 +11,161 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""Bandwidth Monitor Toolbar"""
+"""Bandwidth Monitor Toolbar and Tracker."""
+from __future__ import annotations
+
+from contextvars import ContextVar
+import logging
+import textwrap
+from typing import TYPE_CHECKING
from prompt_toolkit.layout import WindowAlign
from pw_console.plugin_mixin import PluginMixin
from pw_console.widgets import ToolbarButton, WindowPaneToolbar
-from pw_console.pyserial_wrapper import BANDWIDTH_HISTORY_CONTEXTVAR
+from pw_console.widgets.event_count_history import EventCountHistory
+
+if TYPE_CHECKING:
+ from _typeshed import ReadableBuffer
+
+_LOG = logging.getLogger('pw_console.serial_debug_logger')
+
+
+def _log_hex_strings(data: bytes, prefix=''):
+ """Create alinged hex number and character view log messages."""
+ # Make a list of 2 character hex number strings.
+ hex_numbers = textwrap.wrap(data.hex(), 2)
+
+ hex_chars = [
+ ('<' + str(b.to_bytes(1, byteorder='big')) + '>')
+ .replace("<b'\\x", '', 1) # Remove b'\x from the beginning
+ .replace("<b'", '', 1) # Remove b' from the beginning
+ .replace("'>", '', 1) # Remove ' from the end
+ .rjust(2)
+ for b in data
+ ]
+
+ # Replace non-printable bytes with dots.
+ for i, num in enumerate(hex_numbers):
+ if num == hex_chars[i]:
+ hex_chars[i] = '..'
+
+ hex_numbers_msg = ' '.join(hex_numbers)
+ hex_chars_msg = ' '.join(hex_chars)
+
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ hex_numbers_msg,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': hex_numbers_msg,
+ 'view': 'hex',
+ }
+ ),
+ )
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ hex_chars_msg,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': hex_chars_msg,
+ 'view': 'chars',
+ }
+ ),
+ )
+
+
+BANDWIDTH_HISTORY_CONTEXTVAR = ContextVar(
+ 'pw_console_bandwidth_history',
+ default={
+ 'total': EventCountHistory(interval=3),
+ 'read': EventCountHistory(interval=3),
+ 'write': EventCountHistory(interval=3),
+ },
+)
+
+
+class SerialBandwidthTracker:
+ """Tracks and logs the data read and written by a serial tranport."""
+
+ def __init__(self):
+ self.pw_bps_history = BANDWIDTH_HISTORY_CONTEXTVAR.get()
+
+ def track_read_data(self, data: bytes) -> None:
+ """Tracks and logs data read."""
+ self.pw_bps_history['read'].log(len(data))
+ self.pw_bps_history['total'].log(len(data))
+
+ if len(data) > 0:
+ prefix = 'Read %2d B: ' % len(data)
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ data,
+ extra=dict(
+ extra_metadata_fields={
+ 'mode': 'Read',
+ 'bytes': len(data),
+ 'view': 'bytes',
+ 'msg': str(data),
+ }
+ ),
+ )
+ _log_hex_strings(data, prefix=prefix)
+
+ # Print individual lines
+ for line in data.decode(
+ encoding='utf-8', errors='ignore'
+ ).splitlines():
+ _LOG.debug(
+ '%s',
+ line,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': line,
+ 'view': 'lines',
+ }
+ ),
+ )
+
+ def track_write_data(self, data: ReadableBuffer) -> None:
+ """Tracks and logs data to be written."""
+ if isinstance(data, bytes) and len(data) > 0:
+ self.pw_bps_history['write'].log(len(data))
+ self.pw_bps_history['total'].log(len(data))
+
+ prefix = 'Write %2d B: ' % len(data)
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ data,
+ extra=dict(
+ extra_metadata_fields={
+ 'mode': 'Write',
+ 'bytes': len(data),
+ 'view': 'bytes',
+ 'msg': str(data),
+ }
+ ),
+ )
+ _log_hex_strings(data, prefix=prefix)
+
+ # Print individual lines
+ for line in data.decode(
+ encoding='utf-8', errors='ignore'
+ ).splitlines():
+ _LOG.debug(
+ '%s',
+ line,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': line,
+ 'view': 'lines',
+ }
+ ),
+ )
class BandwidthToolbar(WindowPaneToolbar, PluginMixin):
diff --git a/pw_console/py/pw_console/plugins/calc_pane.py b/pw_console/py/pw_console/plugins/calc_pane.py
index b316e0cbe..38757a379 100644
--- a/pw_console/py/pw_console/plugins/calc_pane.py
+++ b/pw_console/py/pw_console/plugins/calc_pane.py
@@ -196,10 +196,8 @@ class CalcPane(WindowPane):
def copy_selected_output(self):
"""Copy highlighted text in the output_field to the system clipboard."""
clipboard_data = self.output_field.buffer.copy_selection()
- self.application.application.clipboard.set_data(clipboard_data)
+ self.application.set_system_clipboard_data(clipboard_data)
def copy_all_output(self):
"""Copy all text in the output_field to the system clipboard."""
- self.application.application.clipboard.set_text(
- self.output_field.buffer.text
- )
+ self.application.set_system_clipboard(self.output_field.buffer.text)
diff --git a/pw_console/py/pw_console/pyserial_wrapper.py b/pw_console/py/pw_console/pyserial_wrapper.py
index 57c297a52..fb15b97ca 100644
--- a/pw_console/py/pw_console/pyserial_wrapper.py
+++ b/pw_console/py/pw_console/pyserial_wrapper.py
@@ -14,157 +14,28 @@
"""Wrapers for pyserial classes to log read and write data."""
from __future__ import annotations
-from contextvars import ContextVar
-import logging
-import textwrap
from typing import TYPE_CHECKING
import serial
-from pw_console.widgets.event_count_history import EventCountHistory
+from pw_console.plugins.bandwidth_toolbar import SerialBandwidthTracker
if TYPE_CHECKING:
from _typeshed import ReadableBuffer
-_LOG = logging.getLogger('pw_console.serial_debug_logger')
-
-
-def _log_hex_strings(data: bytes, prefix=''):
- """Create alinged hex number and character view log messages."""
- # Make a list of 2 character hex number strings.
- hex_numbers = textwrap.wrap(data.hex(), 2)
-
- hex_chars = [
- ('<' + str(b.to_bytes(1, byteorder='big')) + '>')
- .replace("<b'\\x", '', 1) # Remove b'\x from the beginning
- .replace("<b'", '', 1) # Remove b' from the beginning
- .replace("'>", '', 1) # Remove ' from the end
- .rjust(2)
- for b in data
- ]
-
- # Replace non-printable bytes with dots.
- for i, num in enumerate(hex_numbers):
- if num == hex_chars[i]:
- hex_chars[i] = '..'
-
- hex_numbers_msg = ' '.join(hex_numbers)
- hex_chars_msg = ' '.join(hex_chars)
-
- _LOG.debug(
- '%s%s',
- prefix,
- hex_numbers_msg,
- extra=dict(
- extra_metadata_fields={
- 'msg': hex_numbers_msg,
- 'view': 'hex',
- }
- ),
- )
- _LOG.debug(
- '%s%s',
- prefix,
- hex_chars_msg,
- extra=dict(
- extra_metadata_fields={
- 'msg': hex_chars_msg,
- 'view': 'chars',
- }
- ),
- )
-
-
-BANDWIDTH_HISTORY_CONTEXTVAR = ContextVar(
- 'pw_console_bandwidth_history',
- default={
- 'total': EventCountHistory(interval=3),
- 'read': EventCountHistory(interval=3),
- 'write': EventCountHistory(interval=3),
- },
-)
-
class SerialWithLogging(serial.Serial): # pylint: disable=too-many-ancestors
"""pyserial with read and write wrappers for logging."""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.pw_bps_history = BANDWIDTH_HISTORY_CONTEXTVAR.get()
+ self._bandwidth_tracker = SerialBandwidthTracker()
def read(self, size: int = 1) -> bytes:
data = super().read(size)
- self.pw_bps_history['read'].log(len(data))
- self.pw_bps_history['total'].log(len(data))
-
- if len(data) > 0:
- prefix = 'Read %2d B: ' % len(data)
- _LOG.debug(
- '%s%s',
- prefix,
- data,
- extra=dict(
- extra_metadata_fields={
- 'mode': 'Read',
- 'bytes': len(data),
- 'view': 'bytes',
- 'msg': str(data),
- }
- ),
- )
- _log_hex_strings(data, prefix=prefix)
-
- # Print individual lines
- for line in data.decode(
- encoding='utf-8', errors='ignore'
- ).splitlines():
- _LOG.debug(
- '%s',
- line,
- extra=dict(
- extra_metadata_fields={
- 'msg': line,
- 'view': 'lines',
- }
- ),
- )
-
+ self._bandwidth_tracker.track_read_data(data)
return data
def write(self, data: ReadableBuffer) -> None:
- if isinstance(data, bytes) and len(data) > 0:
- self.pw_bps_history['write'].log(len(data))
- self.pw_bps_history['total'].log(len(data))
-
- prefix = 'Write %2d B: ' % len(data)
- _LOG.debug(
- '%s%s',
- prefix,
- data,
- extra=dict(
- extra_metadata_fields={
- 'mode': 'Write',
- 'bytes': len(data),
- 'view': 'bytes',
- 'msg': str(data),
- }
- ),
- )
- _log_hex_strings(data, prefix=prefix)
-
- # Print individual lines
- for line in data.decode(
- encoding='utf-8', errors='ignore'
- ).splitlines():
- _LOG.debug(
- '%s',
- line,
- extra=dict(
- extra_metadata_fields={
- 'msg': line,
- 'view': 'lines',
- }
- ),
- )
-
+ self._bandwidth_tracker.track_write_data(data)
super().write(data)
diff --git a/pw_console/py/pw_console/python_logging.py b/pw_console/py/pw_console/python_logging.py
index 362158909..026ee8476 100644
--- a/pw_console/py/pw_console/python_logging.py
+++ b/pw_console/py/pw_console/python_logging.py
@@ -96,6 +96,7 @@ def setup_python_logging(
# Prevent these loggers from propagating to the root logger.
hidden_host_loggers = [
+ "blib2to3.pgen2.driver", # spammy and unhelpful
"pw_console",
"pw_console.plugins",
# prompt_toolkit triggered debug log messages
@@ -104,6 +105,7 @@ def setup_python_logging(
"parso.python.diff",
"parso.cache",
"pw_console.serial_debug_logger",
+ "websockets.server",
]
for logger_name in hidden_host_loggers:
logging.getLogger(logger_name).propagate = False
@@ -121,6 +123,15 @@ def log_record_to_json(record: logging.LogRecord) -> str:
log_dict["levelno"] = record.levelno
log_dict["levelname"] = record.levelname
log_dict["args"] = record.args
+ log_dict["time"] = str(record.created)
+ log_dict["time_string"] = datetime.fromtimestamp(record.created).isoformat(
+ timespec="seconds"
+ )
+
+ lineno = record.lineno
+ file_name = str(record.filename)
+ log_dict['py_file'] = f'{file_name}:{lineno}'
+ log_dict['py_logger'] = str(record.name)
if hasattr(record, "extra_metadata_fields") and (
record.extra_metadata_fields # type: ignore
@@ -157,11 +168,15 @@ class JsonLogFormatter(logging.Formatter):
"pw_system ",
"System init"
],
+ "time": "1692302986.4729185",
+ "time_string": "2023-08-17T13:09:46",
"fields": {
"module": "pw_system",
"file": "pw_system/init.cc",
"timestamp": "0:00"
- }
+ },
+ "py_file": "script.py:1234",
+ "py_logger": "root"
}
Example usage:
diff --git a/pw_console/py/pw_console/repl_pane.py b/pw_console/py/pw_console/repl_pane.py
index 20a3b9504..de40816c3 100644
--- a/pw_console/py/pw_console/repl_pane.py
+++ b/pw_console/py/pw_console/repl_pane.py
@@ -321,22 +321,20 @@ class ReplPane(WindowPane):
def copy_output_selection(self):
"""Copy highlighted output text to the system clipboard."""
clipboard_data = self.output_field.buffer.copy_selection()
- self.application.application.clipboard.set_data(clipboard_data)
+ self.application.set_system_clipboard_data(clipboard_data)
def copy_input_selection(self):
"""Copy highlighted input text to the system clipboard."""
clipboard_data = self.pw_ptpython_repl.default_buffer.copy_selection()
- self.application.application.clipboard.set_data(clipboard_data)
+ self.application.set_system_clipboard_data(clipboard_data)
def copy_all_output_text(self):
"""Copy all text in the Python output to the system clipboard."""
- self.application.application.clipboard.set_text(
- self.output_field.buffer.text
- )
+ self.application.set_system_clipboard(self.output_field.buffer.text)
def copy_all_input_text(self):
"""Copy all text in the Python input to the system clipboard."""
- self.application.application.clipboard.set_text(
+ self.application.set_system_clipboard(
self.pw_ptpython_repl.default_buffer.text
)
diff --git a/pw_console/py/pw_console/socket_client.py b/pw_console/py/pw_console/socket_client.py
new file mode 100644
index 000000000..5344c3199
--- /dev/null
+++ b/pw_console/py/pw_console/socket_client.py
@@ -0,0 +1,218 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Wrapers for socket clients to log read and write data."""
+from __future__ import annotations
+
+from typing import Callable, Optional, TYPE_CHECKING, Tuple, Union
+
+import errno
+import re
+import socket
+
+from pw_console.plugins.bandwidth_toolbar import SerialBandwidthTracker
+
+if TYPE_CHECKING:
+ from _typeshed import ReadableBuffer
+
+
+class SocketClient:
+ """Socket transport implementation."""
+
+ FILE_SOCKET_SERVER = 'file'
+ DEFAULT_SOCKET_SERVER = 'localhost'
+ DEFAULT_SOCKET_PORT = 33000
+ PW_RPC_MAX_PACKET_SIZE = 256
+
+ _InitArgsType = Tuple[
+ socket.AddressFamily, int # pylint: disable=no-member
+ ]
+ # Can be a string, (address, port) for AF_INET or (address, port, flowinfo,
+ # scope_id) AF_INET6.
+ _AddressType = Union[str, Tuple[str, int], Tuple[str, int, int, int]]
+
+ def __init__(
+ self,
+ config: str,
+ on_disconnect: Optional[Callable[[SocketClient], None]] = None,
+ ):
+ """Creates a socket connection.
+
+ Args:
+ config: The socket configuration. Accepted values and formats are:
+ 'default' - uses the default configuration (localhost:33000)
+ 'address:port' - An IPv4 address and port.
+ 'address' - An IPv4 address. Uses default port 33000.
+ '[address]:port' - An IPv6 address and port.
+ '[address]' - An IPv6 address. Uses default port 33000.
+ 'file:path_to_file' - A Unix socket at ``path_to_file``.
+ In the formats above,``address`` can be an actual address or a name
+ that resolves to an address through name-resolution.
+ on_disconnect: An optional callback called when the socket
+ disconnects.
+
+ Raises:
+ TypeError: The type of socket is not supported.
+ ValueError: The socket configuration is invalid.
+ """
+ self.socket: socket.socket
+ (
+ self._socket_init_args,
+ self._address,
+ ) = SocketClient._parse_socket_config(config)
+ self._on_disconnect = on_disconnect
+ self._connected = False
+ self.connect()
+
+ @staticmethod
+ def _parse_socket_config(
+ config: str,
+ ) -> Tuple[SocketClient._InitArgsType, SocketClient._AddressType]:
+ """Sets the variables used to create a socket given a config string.
+
+ Raises:
+ TypeError: The type of socket is not supported.
+ ValueError: The socket configuration is invalid.
+ """
+ init_args: SocketClient._InitArgsType
+ address: SocketClient._AddressType
+
+ # Check if this is using the default settings.
+ if config == 'default':
+ init_args = socket.AF_INET6, socket.SOCK_STREAM
+ address = (
+ SocketClient.DEFAULT_SOCKET_SERVER,
+ SocketClient.DEFAULT_SOCKET_PORT,
+ )
+ return init_args, address
+
+ # Check if this is a UNIX socket.
+ unix_socket_file_setting = f'{SocketClient.FILE_SOCKET_SERVER}:'
+ if config.startswith(unix_socket_file_setting):
+ # Unix socket support is available on Windows 10 since April
+ # 2018. However, there is no Python support on Windows yet.
+ # See https://bugs.python.org/issue33408 for more information.
+ if not hasattr(socket, 'AF_UNIX'):
+ raise TypeError(
+ 'Unix sockets are not supported in this environment.'
+ )
+ init_args = (
+ socket.AF_UNIX, # pylint: disable=no-member
+ socket.SOCK_STREAM,
+ )
+ address = config[len(unix_socket_file_setting) :]
+ return init_args, address
+
+ # Search for IPv4 or IPv6 address or name and port.
+ # First, try to capture an IPv6 address as anything inside []. If there
+ # are no [] capture the IPv4 address. Lastly, capture the port as the
+ # numbers after :, if any.
+ match = re.match(
+ r'(\[(?P<ipv6_addr>.+)\]:?|(?P<ipv4_addr>[a-zA-Z0-9\._\/]+):?)'
+ r'(?P<port>[0-9]+)?',
+ config,
+ )
+ invalid_config_message = (
+ f'Invalid socket configuration "{config}"'
+ 'Accepted values are "default", "file:<file_path>", '
+ '"<name_or_ipv4_address>" with optional ":<port>", and '
+ '"[<name_or_ipv6_address>]" with optional ":<port>".'
+ )
+ if match is None:
+ raise ValueError(invalid_config_message)
+
+ info = match.groupdict()
+ if info['port']:
+ port = int(info['port'])
+ else:
+ port = SocketClient.DEFAULT_SOCKET_PORT
+
+ if info['ipv4_addr']:
+ ip_addr = info['ipv4_addr']
+ elif info['ipv6_addr']:
+ ip_addr = info['ipv6_addr']
+ else:
+ raise ValueError(invalid_config_message)
+
+ sock_family, sock_type, _, _, address = socket.getaddrinfo(
+ ip_addr, port, type=socket.SOCK_STREAM
+ )[0]
+ init_args = sock_family, sock_type
+ return init_args, address
+
+ def __del__(self):
+ if self._connected:
+ self.socket.close()
+
+ def write(self, data: ReadableBuffer) -> None:
+ """Writes data and detects disconnects."""
+ if not self._connected:
+ raise Exception('Socket is not connected.')
+ try:
+ self.socket.sendall(data)
+ except socket.error as exc:
+ if isinstance(exc.args, tuple) and exc.args[0] == errno.EPIPE:
+ self._handle_disconnect()
+ else:
+ raise exc
+
+ def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE) -> bytes:
+ """Blocks until data is ready and reads up to num_bytes."""
+ if not self._connected:
+ raise Exception('Socket is not connected.')
+ data = self.socket.recv(num_bytes)
+ # Since this is a blocking read, no data returned means the socket is
+ # closed.
+ if not data:
+ self._handle_disconnect()
+ return data
+
+ def connect(self) -> None:
+ """Connects to socket."""
+ self.socket = socket.socket(*self._socket_init_args)
+
+ # Enable reusing address and port for reconnections.
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if hasattr(socket, 'SO_REUSEPORT'):
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+ self.socket.connect(self._address)
+ self._connected = True
+
+ def _handle_disconnect(self):
+ """Escalates a socket disconnect to the user."""
+ self.socket.close()
+ self._connected = False
+ if self._on_disconnect:
+ self._on_disconnect(self)
+
+ def fileno(self) -> int:
+ return self.socket.fileno()
+
+
+class SocketClientWithLogging(SocketClient):
+ """Socket with read and write wrappers for logging."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._bandwidth_tracker = SerialBandwidthTracker()
+
+ def read(
+ self, num_bytes: int = SocketClient.PW_RPC_MAX_PACKET_SIZE
+ ) -> bytes:
+ data = super().read(num_bytes)
+ self._bandwidth_tracker.track_read_data(data)
+ return data
+
+ def write(self, data: ReadableBuffer) -> None:
+ self._bandwidth_tracker.track_write_data(data)
+ super().write(data)
diff --git a/pw_console/py/pw_console/test_mode.py b/pw_console/py/pw_console/test_mode.py
index 815792c1d..3a2f518d2 100644
--- a/pw_console/py/pw_console/test_mode.py
+++ b/pw_console/py/pw_console/test_mode.py
@@ -54,7 +54,7 @@ def prepare_fake_logs(lines) -> List[Tuple[str, Dict]]:
if search:
keyboard_key = search.group(1)
- fake_logs.append((line, {'keys': keyboard_key}))
+ fake_logs.append((line.lstrip(), {'keys': keyboard_key}))
return fake_logs
@@ -64,15 +64,25 @@ async def log_forever(fake_log_messages: List[Tuple[str, Dict]]):
start_time = time.time()
message_count = 0
+ log_methods = [
+ _FAKE_DEVICE_LOG.info,
+ _FAKE_DEVICE_LOG.debug,
+ _FAKE_DEVICE_LOG.warning,
+ _FAKE_DEVICE_LOG.error,
+ _FAKE_DEVICE_LOG.critical,
+ ]
+ log_method_rand_weights = [50, 20, 10, 10, 10]
+
# Fake module column names.
module_names = ['APP', 'RADIO', 'BAT', 'USB', 'CPU']
while True:
- if message_count > 32 or message_count < 2:
- await asyncio.sleep(0.1)
+ if message_count > 32:
+ await asyncio.sleep(1)
fake_log = random.choice(fake_log_messages)
+ log_func = random.choices(log_methods, weights=log_method_rand_weights)
module_name = module_names[message_count % len(module_names)]
- _FAKE_DEVICE_LOG.info(
+ log_func[0](
fake_log[0],
extra=dict(
extra_metadata_fields=dict(
@@ -84,3 +94,5 @@ async def log_forever(fake_log_messages: List[Tuple[str, Dict]]):
),
)
message_count += 1
+ if message_count % 10 == 0:
+ _ROOT_LOG.info('Device message count: %d', message_count)
diff --git a/pw_console/py/pw_console/widgets/border.py b/pw_console/py/pw_console/widgets/border.py
index 33096560d..c69be3a88 100644
--- a/pw_console/py/pw_console/widgets/border.py
+++ b/pw_console/py/pw_console/widgets/border.py
@@ -23,12 +23,14 @@ from prompt_toolkit.layout import (
Window,
)
+from prompt_toolkit.formatted_text import StyleAndTextTuples
+
def create_border(
# pylint: disable=too-many-arguments
content: AnyContainer,
content_height: Optional[int] = None,
- title: Union[Callable[[], str], str] = '',
+ title: Union[Callable[[], Union[str, StyleAndTextTuples]], str] = '',
border_style: Union[Callable[[], str], str] = '',
base_style: Union[Callable[[], str], str] = '',
top: bool = True,
diff --git a/pw_console/py/pw_console/widgets/event_count_history.py b/pw_console/py/pw_console/widgets/event_count_history.py
index 242f615e4..100703e1b 100644
--- a/pw_console/py/pw_console/widgets/event_count_history.py
+++ b/pw_console/py/pw_console/widgets/event_count_history.py
@@ -15,7 +15,7 @@
import collections
import logging
-from dataclasses import dataclass
+from dataclasses import dataclass, field
import time
from typing import Optional
@@ -57,11 +57,11 @@ class EventCountHistory:
interval: float = 1.0 # Number of seconds per sum of events.
history_limit: int = 20
scale_characters = ' ▁▂▃▄▅▆▇█'
- history: collections.deque = collections.deque()
+ history: collections.deque = field(default_factory=collections.deque)
show_sparkline: bool = False
_this_count: int = 0
_last_count: int = 0
- _last_update_time: float = time.time()
+ _last_update_time: float = field(default_factory=time.time)
def log(self, count: int) -> None:
self._this_count += count
diff --git a/pw_console/py/setup.py b/pw_console/py/setup.py
deleted file mode 100644
index 7308b8c47..000000000
--- a/pw_console/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_console"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_console/py/socket_client_test.py b/pw_console/py/socket_client_test.py
new file mode 100644
index 000000000..f4e5a9f5d
--- /dev/null
+++ b/pw_console/py/socket_client_test.py
@@ -0,0 +1,181 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for pw_console.socket_client"""
+
+import socket
+import unittest
+
+
+from pw_console import socket_client
+
+
+class TestSocketClient(unittest.TestCase):
+ """Tests for SocketClient."""
+
+ def test_parse_config_default(self) -> None:
+ config = "default"
+ with unittest.mock.patch.object(
+ socket_client.SocketClient, 'connect', return_value=None
+ ):
+ client = socket_client.SocketClient(config)
+ self.assertEqual(
+ client._socket_init_args, # pylint: disable=protected-access
+ (socket.AF_INET6, socket.SOCK_STREAM),
+ )
+ self.assertEqual(
+ client._address, # pylint: disable=protected-access
+ (
+ socket_client.SocketClient.DEFAULT_SOCKET_SERVER,
+ socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ ),
+ )
+
+ def test_parse_config_unix_file(self) -> None:
+ # Skip test if UNIX sockets are not supported.
+ if not hasattr(socket, 'AF_UNIX'):
+ return
+
+ config = 'file:fake_file_path'
+ with unittest.mock.patch.object(
+ socket_client.SocketClient, 'connect', return_value=None
+ ):
+ client = socket_client.SocketClient(config)
+ self.assertEqual(
+ client._socket_init_args, # pylint: disable=protected-access
+ (
+ socket.AF_UNIX, # pylint: disable=no-member
+ socket.SOCK_STREAM,
+ ),
+ )
+ self.assertEqual(
+ client._address, # pylint: disable=protected-access
+ 'fake_file_path',
+ )
+
+ def _check_config_parsing(
+ self, config: str, expected_address: str, expected_port: int
+ ) -> None:
+ with unittest.mock.patch.object(
+ socket_client.SocketClient, 'connect', return_value=None
+ ):
+ fake_getaddrinfo_return_value = [
+ (socket.AF_INET6, socket.SOCK_STREAM, 0, None, None)
+ ]
+ with unittest.mock.patch.object(
+ socket,
+ 'getaddrinfo',
+ return_value=fake_getaddrinfo_return_value,
+ ) as mock_getaddrinfo:
+ client = socket_client.SocketClient(config)
+ mock_getaddrinfo.assert_called_with(
+ expected_address, expected_port, type=socket.SOCK_STREAM
+ )
+ # Assert the init args are what is returned by ``getaddrinfo``
+ # not necessarily the correct ones, since this test should not
+ # perform any network action.
+ self.assertEqual(
+ client._socket_init_args, # pylint: disable=protected-access
+ (
+ socket.AF_INET6,
+ socket.SOCK_STREAM,
+ ),
+ )
+
+ def test_parse_config_ipv4_domain(self) -> None:
+ self._check_config_parsing(
+ config='file.com/some_long/path:80',
+ expected_address='file.com/some_long/path',
+ expected_port=80,
+ )
+
+ def test_parse_config_ipv4_domain_no_port(self) -> None:
+ self._check_config_parsing(
+ config='file.com/some/path',
+ expected_address='file.com/some/path',
+ expected_port=socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ )
+
+ def test_parse_config_ipv4_address(self) -> None:
+ self._check_config_parsing(
+ config='8.8.8.8:8080',
+ expected_address='8.8.8.8',
+ expected_port=8080,
+ )
+
+ def test_parse_config_ipv4_address_no_port(self) -> None:
+ self._check_config_parsing(
+ config='8.8.8.8',
+ expected_address='8.8.8.8',
+ expected_port=socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ )
+
+ def test_parse_config_ipv6_domain(self) -> None:
+ self._check_config_parsing(
+ config='[file.com/some_long/path]:80',
+ expected_address='file.com/some_long/path',
+ expected_port=80,
+ )
+
+ def test_parse_config_ipv6_domain_no_port(self) -> None:
+ self._check_config_parsing(
+ config='[file.com/some/path]',
+ expected_address='file.com/some/path',
+ expected_port=socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ )
+
+ def test_parse_config_ipv6_address(self) -> None:
+ self._check_config_parsing(
+ config='[2001:4860:4860::8888:8080]:666',
+ expected_address='2001:4860:4860::8888:8080',
+ expected_port=666,
+ )
+
+ def test_parse_config_ipv6_address_no_port(self) -> None:
+ self._check_config_parsing(
+ config='[2001:4860:4860::8844]',
+ expected_address='2001:4860:4860::8844',
+ expected_port=socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ )
+
+ def test_parse_config_ipv6_local(self) -> None:
+ self._check_config_parsing(
+ config='[fe80::100%eth0]:80',
+ expected_address='fe80::100%eth0',
+ expected_port=80,
+ )
+
+ def test_parse_config_ipv6_local_no_port(self) -> None:
+ self._check_config_parsing(
+ config='[fe80::100%eth0]',
+ expected_address='fe80::100%eth0',
+ expected_port=socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ )
+
+ def test_parse_config_ipv6_local_windows(self) -> None:
+ self._check_config_parsing(
+ config='[fe80::100%4]:80',
+ expected_address='fe80::100%4',
+ expected_port=80,
+ )
+
+ def test_parse_config_ipv6_local_no_port_windows(self) -> None:
+ self._check_config_parsing(
+ config='[fe80::100%4]',
+ expected_address='fe80::100%4',
+ expected_port=socket_client.SocketClient.DEFAULT_SOCKET_PORT,
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index af91318fb..7cc428fb7 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -3,6 +3,9 @@
=====================
Manual Test Procedure
=====================
+.. pigweed-module-subpage::
+ :name: pw_console
+ :tagline: pw_console: Multi-purpose pluggable interactive console for dev & manufacturing
``pw_console`` is a Terminal based user interface which is difficult to
completely test in an automated fashion. Unit tests that don't depend on the
diff --git a/pw_containers/BUILD.bazel b/pw_containers/BUILD.bazel
index 91f996226..9907950eb 100644
--- a/pw_containers/BUILD.bazel
+++ b/pw_containers/BUILD.bazel
@@ -27,6 +27,8 @@ pw_cc_library(
deps = [
":algorithm",
":flat_map",
+ ":inline_deque",
+ ":inline_queue",
":intrusive_list",
":vector",
],
@@ -64,14 +66,51 @@ pw_cc_library(
)
pw_cc_library(
+ name = "inline_deque",
+ hdrs = [
+ "public/pw_containers/inline_deque.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":raw_storage",
+ "//pw_assert",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
+
+pw_cc_library(
+ name = "inline_queue",
+ hdrs = [
+ "public/pw_containers/inline_queue.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":inline_deque",
+ ],
+)
+
+pw_cc_library(
+ name = "variable_length_entry_queue",
+ srcs = ["variable_length_entry_queue.c"],
+ hdrs = ["public/pw_containers/variable_length_entry_queue.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_varint",
+ ],
+)
+
+pw_cc_library(
name = "vector",
hdrs = [
"public/pw_containers/vector.h",
],
includes = ["public"],
deps = [
- "//pw_assert:facade",
+ "//pw_assert",
"//pw_polyfill",
+ "//pw_preprocessor",
],
)
@@ -89,6 +128,23 @@ pw_cc_library(
name = "flat_map",
hdrs = ["public/pw_containers/flat_map.h"],
includes = ["public"],
+ deps = ["//pw_assert"],
+)
+
+pw_cc_library(
+ name = "raw_storage",
+ hdrs = [
+ "public/pw_containers/internal/raw_storage.h",
+ ],
+ includes = ["public"],
+ visibility = [":__subpackages__"],
+)
+
+pw_cc_library(
+ name = "test_helpers",
+ srcs = ["test_helpers.cc"],
+ hdrs = ["pw_containers_private/test_helpers.h"],
+ visibility = [":__subpackages__"],
)
pw_cc_library(
@@ -108,6 +164,7 @@ pw_cc_test(
srcs = ["algorithm_test.cc"],
deps = [
":algorithm",
+ ":flat_map",
":intrusive_list",
":vector",
"//pw_span",
@@ -118,7 +175,9 @@ pw_cc_test(
name = "filtered_view_test",
srcs = ["filtered_view_test.cc"],
deps = [
+ ":algorithm",
":filtered_view",
+ ":flat_map",
":intrusive_list",
],
)
@@ -130,17 +189,71 @@ pw_cc_test(
],
deps = [
":pw_containers",
+ "//pw_polyfill",
"//pw_unit_test",
],
)
pw_cc_test(
+ name = "variable_length_entry_queue_test",
+ srcs = [
+ "pw_containers_private/variable_length_entry_queue_test_oracle.h",
+ "variable_length_entry_queue_test.cc",
+ ],
+ deps = [
+ ":variable_length_entry_queue",
+ "//pw_bytes",
+ ],
+)
+
+pw_cc_test(
name = "vector_test",
srcs = [
"vector_test.cc",
],
deps = [
":pw_containers",
+ ":test_helpers",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "inline_deque_test",
+ srcs = [
+ "inline_deque_test.cc",
+ ],
+ deps = [
+ ":algorithm",
+ ":inline_deque",
+ ":test_helpers",
+ "//pw_compilation_testing:negative_compilation_testing",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "inline_queue_test",
+ srcs = [
+ "inline_queue_test.cc",
+ ],
+ deps = [
+ ":algorithm",
+ ":inline_queue",
+ ":test_helpers",
+ "//pw_compilation_testing:negative_compilation_testing",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "raw_storage_test",
+ srcs = [
+ "raw_storage_test.cc",
+ ],
+ deps = [
+ ":raw_storage",
+ ":test_helpers",
"//pw_unit_test",
],
)
@@ -164,6 +277,7 @@ pw_cc_test(
],
deps = [
":intrusive_list",
+ ":vector",
"//pw_unit_test",
],
)
diff --git a/pw_containers/BUILD.gn b/pw_containers/BUILD.gn
index e1632ada8..1bc4fd29b 100644
--- a/pw_containers/BUILD.gn
+++ b/pw_containers/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/traits.gni")
import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
@@ -27,6 +28,8 @@ group("pw_containers") {
public_deps = [
":algorithm",
":flat_map",
+ ":inline_deque",
+ ":inline_queue",
":intrusive_list",
":vector",
]
@@ -52,6 +55,24 @@ pw_source_set("filtered_view") {
pw_source_set("flat_map") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_containers/flat_map.h" ]
+ public_deps = [ "$dir_pw_assert:assert" ]
+}
+
+pw_source_set("inline_deque") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":raw_storage",
+ dir_pw_assert,
+ dir_pw_preprocessor,
+ dir_pw_span,
+ ]
+ public = [ "public/pw_containers/inline_deque.h" ]
+}
+
+pw_source_set("inline_queue") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ ":inline_deque" ]
+ public = [ "public/pw_containers/inline_queue.h" ]
}
pw_source_set("iterator") {
@@ -60,16 +81,40 @@ pw_source_set("iterator") {
public = [ "public/pw_containers/iterator.h" ]
}
+pw_source_set("raw_storage") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_containers/internal/raw_storage.h" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("test_helpers") {
+ public = [ "pw_containers_private/test_helpers.h" ]
+ sources = [ "test_helpers.cc" ]
+ visibility = [ ":*" ]
+}
+
pw_source_set("to_array") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_containers/to_array.h" ]
}
+pw_source_set("variable_length_entry_queue") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ dir_pw_varint ]
+ deps = [ dir_pw_assert ]
+ public = [ "public/pw_containers/variable_length_entry_queue.h" ]
+ sources = [ "variable_length_entry_queue.c" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
pw_source_set("vector") {
public_configs = [ ":public_include_path" ]
public_deps = [
dir_pw_assert,
dir_pw_polyfill,
+ dir_pw_preprocessor,
]
public = [ "public/pw_containers/vector.h" ]
}
@@ -94,8 +139,12 @@ pw_test_group("tests") {
":algorithm_test",
":filtered_view_test",
":flat_map_test",
+ ":inline_deque_test",
+ ":inline_queue_test",
":intrusive_list_test",
+ ":raw_storage_test",
":to_array_test",
+ ":variable_length_entry_queue_test",
":vector_test",
":wrapped_iterator_test",
]
@@ -105,48 +154,130 @@ pw_test("algorithm_test") {
sources = [ "algorithm_test.cc" ]
deps = [
":algorithm",
+ ":flat_map",
":intrusive_list",
":vector",
dir_pw_span,
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("filtered_view_test") {
sources = [ "filtered_view_test.cc" ]
deps = [
+ ":algorithm",
":filtered_view",
+ ":flat_map",
":intrusive_list",
dir_pw_span,
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("flat_map_test") {
sources = [ "flat_map_test.cc" ]
- deps = [ ":flat_map" ]
+ deps = [
+ ":flat_map",
+ dir_pw_polyfill,
+ ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_test("inline_deque_test") {
+ sources = [ "inline_deque_test.cc" ]
+ deps = [
+ ":algorithm",
+ ":inline_deque",
+ ":test_helpers",
+ ]
+ negative_compilation_tests = true
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_test("inline_queue_test") {
+ sources = [ "inline_queue_test.cc" ]
+ deps = [
+ ":algorithm",
+ ":inline_queue",
+ ":test_helpers",
+ ]
+ negative_compilation_tests = true
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_test("raw_storage_test") {
+ sources = [ "raw_storage_test.cc" ]
+ deps = [
+ ":raw_storage",
+ ":test_helpers",
+ ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("to_array_test") {
sources = [ "to_array_test.cc" ]
deps = [ ":to_array" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_test("variable_length_entry_queue_test") {
+ sources = [
+ "pw_containers_private/variable_length_entry_queue_test_oracle.h",
+ "variable_length_entry_queue_test.cc",
+ ]
+ deps = [
+ ":variable_length_entry_queue",
+ dir_pw_bytes,
+ ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("vector_test") {
sources = [ "vector_test.cc" ]
- deps = [ ":vector" ]
+ deps = [
+ ":test_helpers",
+ ":vector",
+ ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("wrapped_iterator_test") {
sources = [ "wrapped_iterator_test.cc" ]
deps = [ ":wrapped_iterator" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("intrusive_list_test") {
sources = [ "intrusive_list_test.cc" ]
deps = [
":intrusive_list",
+ ":vector",
"$dir_pw_preprocessor",
]
negative_compilation_tests = true
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_doc_group("docs") {
diff --git a/pw_containers/CMakeLists.txt b/pw_containers/CMakeLists.txt
index 4b12aa2ce..bc05298e0 100644
--- a/pw_containers/CMakeLists.txt
+++ b/pw_containers/CMakeLists.txt
@@ -18,12 +18,11 @@ pw_add_library(pw_containers INTERFACE
PUBLIC_DEPS
pw_containers.algorithm
pw_containers.flat_map
+ pw_containers.inline_deque
+ pw_containers.inline_queue
pw_containers.intrusive_list
pw_containers.vector
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_CONTAINERS)
- zephyr_link_libraries(pw_containers)
-endif()
pw_add_library(pw_containers.algorithm INTERFACE
HEADERS
@@ -48,6 +47,29 @@ pw_add_library(pw_containers.flat_map INTERFACE
public/pw_containers/flat_map.h
PUBLIC_INCLUDES
public
+ PUBLIC_DEPS
+ pw_assert.assert
+)
+
+pw_add_library(pw_containers.inline_deque INTERFACE
+ HEADERS
+ public/pw_containers/inline_deque.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert.assert
+ pw_containers._raw_storage
+ pw_preprocessor
+ pw_span
+)
+
+pw_add_library(pw_containers.inline_queue INTERFACE
+ HEADERS
+ public/pw_containers/inline_queue.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_containers.inline_deque
)
pw_add_library(pw_containers.iterator INTERFACE
@@ -57,6 +79,20 @@ pw_add_library(pw_containers.iterator INTERFACE
public
)
+pw_add_library(pw_containers._raw_storage INTERFACE
+ HEADERS
+ public/pw_containers/internal/raw_storage.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_library(pw_containers._test_helpers STATIC
+ HEADERS
+ pw_containers_private/test_helpers.h
+ SOURCES
+ test_helpers.cc
+)
+
pw_add_library(pw_containers.to_array INTERFACE
HEADERS
public/pw_containers/to_array.h
@@ -64,6 +100,19 @@ pw_add_library(pw_containers.to_array INTERFACE
public
)
+pw_add_library(pw_containers.variable_length_entry_queue STATIC
+ HEADERS
+ public/pw_containers/variable_length_entry_queue.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_varint
+ PRIVATE_DEPS
+ pw_assert
+ SOURCES
+ variable_length_entry_queue.c
+)
+
pw_add_library(pw_containers.vector INTERFACE
HEADERS
public/pw_containers/vector.h
@@ -72,6 +121,7 @@ pw_add_library(pw_containers.vector INTERFACE
PUBLIC_DEPS
pw_assert
pw_polyfill
+ pw_preprocessor
)
pw_add_library(pw_containers.wrapped_iterator INTERFACE
@@ -98,6 +148,7 @@ pw_add_test(pw_containers.algorithm_test
algorithm_test.cc
PRIVATE_DEPS
pw_containers.algorithm
+ pw_containers.flat_map
pw_containers.intrusive_list
pw_containers.vector
pw_span
@@ -110,7 +161,9 @@ pw_add_test(pw_containers.filtered_view_test
SOURCES
filtered_view_test.cc
PRIVATE_DEPS
+ pw_containers.algorithm
pw_containers.filtered_view
+ pw_containers.flat_map
pw_containers.intrusive_list
pw_span
GROUPS
@@ -123,6 +176,45 @@ pw_add_test(pw_containers.flat_map_test
flat_map_test.cc
PRIVATE_DEPS
pw_containers.flat_map
+ pw_polyfill
+ GROUPS
+ modules
+ pw_containers
+ pw_polyfill
+)
+
+pw_add_test(pw_containers.inline_deque_test
+ SOURCES
+ inline_deque_test.cc
+ PRIVATE_DEPS
+ pw_compilation_testing._pigweed_only_negative_compilation
+ pw_containers.algorithm
+ pw_containers.inline_deque
+ pw_containers._test_helpers
+ GROUPS
+ modules
+ pw_containers
+)
+
+pw_add_test(pw_containers.inline_queue_test
+ SOURCES
+ inline_queue_test.cc
+ PRIVATE_DEPS
+ pw_compilation_testing._pigweed_only_negative_compilation
+ pw_containers.algorithm
+ pw_containers.inline_queue
+ pw_containers._test_helpers
+ GROUPS
+ modules
+ pw_containers
+)
+
+pw_add_test(pw_containers.raw_storage_test
+ SOURCES
+ raw_storage_test.cc
+ PRIVATE_DEPS
+ pw_containers._raw_storage
+ pw_containers._test_helpers
GROUPS
modules
pw_containers
@@ -138,10 +230,20 @@ pw_add_test(pw_containers.to_array_test
pw_containers
)
+pw_add_test(pw_containers.variable_length_entry_queue_test
+ SOURCES
+ pw_containers_private/variable_length_entry_queue_test_oracle.h
+ variable_length_entry_queue_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ pw_containers.variable_length_entry_queue
+)
+
pw_add_test(pw_containers.vector_test
SOURCES
vector_test.cc
PRIVATE_DEPS
+ pw_containers._test_helpers
pw_containers.vector
GROUPS
modules
diff --git a/pw_containers/Kconfig b/pw_containers/Kconfig
index 4b83c360f..41ea32535 100644
--- a/pw_containers/Kconfig
+++ b/pw_containers/Kconfig
@@ -12,7 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_containers"
+
config PIGWEED_CONTAINERS
- bool "Enable the Pigweed containers library (pw_containers)"
+ bool "Link pw_containers library"
select PIGWEED_ASSERT
select PIGWEED_STATUS
+ help
+ See :ref:`module-pw_containers` for module details.
+
+endmenu
diff --git a/pw_containers/algorithm_test.cc b/pw_containers/algorithm_test.cc
index b4d1c0c5d..beb3e63a3 100644
--- a/pw_containers/algorithm_test.cc
+++ b/pw_containers/algorithm_test.cc
@@ -22,6 +22,7 @@
#include <iterator>
#include "gtest/gtest.h"
+#include "pw_containers/flat_map.h"
#include "pw_containers/intrusive_list.h"
#include "pw_containers/vector.h"
#include "pw_span/span.h"
@@ -228,6 +229,15 @@ TEST_F(NonMutatingTest, Equal) {
EXPECT_TRUE(pw::containers::Equal(span_, array_));
EXPECT_TRUE(pw::containers::Equal(array_, vector_));
+ pw::containers::FlatMap<char, int, 3> map1({{{'A', 1}, {'B', 2}, {'C', 3}}});
+ pw::containers::FlatMap<char, int, 3> map2({{{'B', 2}, {'A', 1}, {'C', 3}}});
+ pw::containers::FlatMap<char, int, 3> map3({{{'A', 1}, {'B', 2}, {'C', 4}}});
+ pw::containers::FlatMap<char, int, 3> map4({{{'A', 1}, {'B', 2}, {'D', 3}}});
+
+ EXPECT_TRUE(pw::containers::Equal(map1, map2));
+ EXPECT_FALSE(pw::containers::Equal(map1, map3));
+ EXPECT_FALSE(pw::containers::Equal(map1, map4));
+
// Test that behavior appropriately differs from that of equal().
pw::Vector<int, 4> vector_plus = {1, 2, 3};
vector_plus.push_back(4);
diff --git a/pw_containers/docs.rst b/pw_containers/docs.rst
index 0af2eda2c..83f333402 100644
--- a/pw_containers/docs.rst
+++ b/pw_containers/docs.rst
@@ -1,12 +1,13 @@
.. _module-pw_containers:
--------------
+=============
pw_containers
--------------
+=============
The ``pw_containers`` module provides embedded-friendly container classes.
+----------
pw::Vector
-==========
+----------
The Vector class is similar to ``std::vector``, except it is backed by a
fixed-size buffer. Vectors must be declared with an explicit maximum size
(e.g. ``Vector<int, 10>``) but vectors can be used and referred to without the
@@ -18,8 +19,37 @@ size in a variable. This allows Vectors to be used without having to know
their maximum size at compile time. It also keeps code size small since
function implementations are shared for all maximum sizes.
+---------------
+pw::InlineDeque
+---------------
+.. doxygentypedef:: pw::InlineDeque
+
+---------------
+pw::InlineQueue
+---------------
+.. doxygentypedef:: pw::InlineQueue
+
+----------------------------
+pw::VariableLengthEntryQueue
+----------------------------
+.. doxygenfile:: pw_containers/variable_length_entry_queue.h
+ :sections: detaileddescription
+
+API Reference
+===============
+C
+-
+.. doxygengroup:: variable_length_entry_queue_c_api
+ :content-only:
+
+Python
+------
+.. automodule:: pw_containers.variable_length_entry_queue
+ :members:
+
+-----------------
pw::IntrusiveList
-=================
+-----------------
IntrusiveList provides an embedded-friendly singly-linked intrusive list
implementation. An intrusive list is a type of linked list that embeds the
"next" pointer into the list object itself. This allows the construction of a
@@ -33,7 +63,7 @@ pointer from being accessed by the item class, so only the ``pw::IntrusiveList``
class can modify the list.
Usage
------
+=====
While the API of ``pw::IntrusiveList`` is similar to a ``std::forward_list``,
there are extra steps to creating objects that can be stored in this data
structure. Objects that will be added to a ``IntrusiveList<T>`` must inherit
@@ -104,35 +134,53 @@ That means two key things:
}
}
+Performance Considerations
+==========================
+Items only include pointers to the next item. To reach previous items, the list
+maintains a cycle of items so that the first item can be reached from the last.
+This structure means certain operations have linear complexity in terms of the
+number of items in the list, i.e. they are "O(n)":
+
+- Adding to the end of a list with ``pw::IntrusiveList<T>::push_back(T&)``.
+- Accessing the last item in a list with ``pw::IntrusiveList<T>::back()``.
+- Destroying an item with ``pw::IntrusiveList<T>::Item::~Item()``.
+- Moving an item with either ``pw::IntrusiveList<T>::Item::Item(Item&&)`` or
+ ``pw::IntrusiveList<T>::Item::operator=(Item&&)``.
+- Removing an item from a list using ``pw::IntrusiveList<T>::remove(const T&)``.
+- Getting the list size using ``pw::IntrusiveList<T>::size()``.
+
+When using a ``pw::IntrusiveList<T>`` in a performance critical section or with
+many items, authors should prefer to avoid these methods. For example, it may be
+preferrable to create items that together with their storage outlive the list.
+
+Notably, ``pw::IntrusiveList<T>::end()`` is constant complexity (i.e. "O(1)").
+As a result iterating over a list does not incur an additional penalty.
+
+-----------------------
pw::containers::FlatMap
-=======================
-FlatMap provides a simple, fixed-size associative array with lookup by key or
-value. ``pw::containers::FlatMap`` contains the same methods and features for
-looking up data as std::map. However, there are no methods that modify the
-underlying data. The underlying array in ``pw::containers::FlatMap`` does not
-need to be sorted. During construction, ``pw::containers::FlatMap`` will
-perform a constexpr insertion sort.
-
+-----------------------
+``FlatMap`` provides a simple, fixed-size associative array with `O`\ (log `n`)
+lookup by key.
+
+``pw::containers::FlatMap`` contains the same methods and features for looking
+up data as ``std::map``. However, modification of the underlying data is limited
+to the mapped values, via ``.at()`` (key must exist) and ``mapped_iterator``
+objects returned by ``.mapped_begin()`` and ``.mapped_end()``.
+``mapped_iterator`` objects are bidirectional iterators that can be dereferenced
+to access and mutate the mapped value objects.
+
+The underlying array in ``pw::containers::FlatMap`` does not need to be sorted.
+During construction, ``pw::containers::FlatMap`` will perform a constexpr
+insertion sort.
+
+----------------------------
pw::containers::FilteredView
-============================
-``pw::containers::FilteredView`` provides a view of a container that only
-contains elements that match the specified filter. This class is similar to
-C++20's `std::ranges::filter_view
-<https://en.cppreference.com/w/cpp/ranges/filter_view>`_.
-
-To create a ``FilteredView``, pass a container and a filter object, which may be
-a lambda or class that implements ``operator()`` for the container's value type.
-
-.. code-block:: cpp
-
- std::array<int, 99> kNumbers = {3, 1, 4, 1, ...};
-
- for (int even : FilteredView(kNumbers, [](int n) { return n % 2 == 0; })) {
- PW_LOG_INFO("This number is even: %d", even);
- }
+----------------------------
+.. doxygenclass:: pw::containers::FilteredView
+-------------------------------
pw::containers::WrappedIterator
-===============================
+-------------------------------
``pw::containers::WrappedIterator`` is a class that makes it easy to wrap an
existing iterator type. It reduces boilerplate by providing ``operator++``,
``operator--``, ``operator==``, ``operator!=``, and the standard iterator
@@ -181,15 +229,17 @@ in Java 8. ``WrappedIterator`` and ``FilteredView`` require no memory
allocation, which is helpful when memory is too constrained to process the items
into a new container.
+------------------------
pw::containers::to_array
-========================
+------------------------
``pw::containers::to_array`` is a C++14-compatible implementation of C++20's
`std::to_array <https://en.cppreference.com/w/cpp/container/array/to_array>`_.
In C++20, it is an alias for ``std::to_array``. It converts a C array to a
``std::array``.
+-------------------------
pw_containers/algorithm.h
-=========================
+-------------------------
Pigweed provides a set of Container-based versions of algorithmic functions
within the C++ standard library, based on a subset of
``absl/algorithm/container.h``.
@@ -199,74 +249,62 @@ within the C++ standard library, based on a subset of
Container-based version of the <algorithm> ``std::all_of()`` function to
test if all elements within a container satisfy a condition.
-
.. cpp:function:: bool pw::containers::AnyOf()
Container-based version of the <algorithm> ``std::any_of()`` function to
test if any element in a container fulfills a condition.
-
.. cpp:function:: bool pw::containers::NoneOf()
Container-based version of the <algorithm> ``std::none_of()`` function to
test if no elements in a container fulfill a condition.
-
.. cpp:function:: pw::containers::ForEach()
Container-based version of the <algorithm> ``std::for_each()`` function to
apply a function to a container's elements.
-
.. cpp:function:: pw::containers::Find()
Container-based version of the <algorithm> ``std::find()`` function to find
the first element containing the passed value within a container value.
-
.. cpp:function:: pw::containers::FindIf()
Container-based version of the <algorithm> ``std::find_if()`` function to find
the first element in a container matching the given condition.
-
.. cpp:function:: pw::containers::FindIfNot()
Container-based version of the <algorithm> ``std::find_if_not()`` function to
find the first element in a container not matching the given condition.
-
.. cpp:function:: pw::containers::FindEnd()
Container-based version of the <algorithm> ``std::find_end()`` function to
find the last subsequence within a container.
-
.. cpp:function:: pw::containers::FindFirstOf()
Container-based version of the <algorithm> ``std::find_first_of()`` function
to find the first element within the container that is also within the options
container.
-
.. cpp:function:: pw::containers::AdjacentFind()
Container-based version of the <algorithm> ``std::adjacent_find()`` function
to find equal adjacent elements within a container.
-
.. cpp:function:: pw::containers::Count()
Container-based version of the <algorithm> ``std::count()`` function to count
values that match within a container.
-
.. cpp:function:: pw::containers::CountIf()
Container-based version of the <algorithm> ``std::count_if()`` function to
count values matching a condition within a container.
-
.. cpp:function:: pw::containers::Mismatch()
Container-based version of the <algorithm> ``std::mismatch()`` function to
@@ -276,7 +314,6 @@ within the C++ standard library, based on a subset of
``pred`` to the first N elements of ``c1`` and ``c2``, where
``N = min(size(c1), size(c2))``.
-
.. cpp:function:: bool pw::containers::Equal()
Container-based version of the <algorithm> ``std::equal()`` function to
@@ -295,27 +332,23 @@ within the C++ standard library, based on a subset of
Container-based version of the <algorithm> ``std::is_permutation()`` function
to test whether a container is a permutation of another.
-
.. cpp:function:: pw::containers::Search()
Container-based version of the <algorithm> ``std::search()`` function to
search a container for a subsequence.
-
.. cpp:function:: pw::containers::SearchN()
Container-based version of the <algorithm> ``std::search_n()`` function to
search a container for the first sequence of N elements.
+-------------
Compatibility
-=============
+-------------
- C++17
-Dependencies
-============
-- :bdg-ref-primary-line:`module-pw_span`
-
+------
Zephyr
-======
+------
To enable ``pw_containers`` for Zephyr add ``CONFIG_PIGWEED_CONTAINERS=y`` to
the project's configuration.
diff --git a/pw_containers/filtered_view_test.cc b/pw_containers/filtered_view_test.cc
index 4d32a58ce..331558d37 100644
--- a/pw_containers/filtered_view_test.cc
+++ b/pw_containers/filtered_view_test.cc
@@ -17,6 +17,8 @@
#include <array>
#include "gtest/gtest.h"
+#include "pw_containers/algorithm.h"
+#include "pw_containers/flat_map.h"
#include "pw_containers/intrusive_list.h"
#include "pw_span/span.h"
@@ -47,7 +49,7 @@ TEST(FilteredView, Array_MatchAll) {
std::array<bool, 6> found = {};
for (int value : view) {
- found[value] = true;
+ found[static_cast<size_t>(value)] = true;
}
EXPECT_TRUE(
std::all_of(found.begin(), found.end(), [](bool b) { return b; }));
@@ -102,12 +104,29 @@ TEST(FilteredView, IntrusiveList_MatchAll) {
for (const Item& item :
FilteredView(intrusive_list, [](const Item&) { return true; })) {
- found[item.value] = true;
+ found[static_cast<size_t>(item.value)] = true;
}
EXPECT_TRUE(
std::all_of(found.begin(), found.end(), [](bool b) { return b; }));
}
+TEST(FilteredView, IntrusiveList_CopyPredicate) {
+ Item item_1{1};
+ Item item_2{2};
+ Item item_3{3};
+ IntrusiveList<Item> intrusive_list({&item_1, &item_2, &item_3});
+
+ auto filter_to_copy = [](const Item& i) { return i.value % 2 != 0; };
+ FilteredView view(intrusive_list, filter_to_copy);
+
+ auto it = view.begin();
+ ASSERT_EQ(it->value, 1);
+ ++it;
+ ASSERT_EQ((*it).value, 3);
+ ++it;
+ EXPECT_EQ(it, view.end());
+}
+
TEST(FilteredView, IntrusiveList_MatchNone) {
Item item_1{0};
Item item_2{1};
@@ -172,5 +191,19 @@ TEST(FilteredView, Size_AllElements) {
EXPECT_FALSE(FilteredView(kArray, [](int x) { return x < 5; }).empty());
}
+TEST(FilteredView, FlatMap) {
+ pw::containers::FlatMap<char, int, 3> map({{{'A', 1}, {'B', 2}, {'C', 3}}});
+
+ auto odd_pred = [](pw::containers::Pair<char, int> p) {
+ return p.second % 2 != 0;
+ };
+
+ auto odd_map = pw::containers::FilteredView(map, std::move(odd_pred));
+
+ pw::containers::FlatMap<char, int, 2> odd_expect({{{'A', 1}, {'C', 3}}});
+
+ ASSERT_TRUE(pw::containers::Equal(odd_map, odd_expect));
+}
+
} // namespace
} // namespace pw::containers
diff --git a/pw_containers/flat_map_test.cc b/pw_containers/flat_map_test.cc
index bd236bd4c..da7028675 100644
--- a/pw_containers/flat_map_test.cc
+++ b/pw_containers/flat_map_test.cc
@@ -17,9 +17,13 @@
#include <limits>
#include "gtest/gtest.h"
+#include "pw_polyfill/language_feature_macros.h"
namespace pw::containers {
namespace {
+
+using Single = FlatMap<int, char, 1>;
+
constexpr FlatMap<int, char, 5> kOddMap({{
{-3, 'a'},
{0, 'b'},
@@ -27,46 +31,81 @@ constexpr FlatMap<int, char, 5> kOddMap({{
{50, 'd'},
{100, 'e'},
}});
+
} // namespace
+TEST(FlatMap, PairEquality) {
+ Pair<char, int> p1{'a', 1};
+ Pair<char, int> p2{'a', 1};
+ Pair<char, int> p3{'b', 1};
+ Pair<char, int> p4{'a', 2};
+
+ EXPECT_EQ(p1, p2);
+ EXPECT_NE(p1, p3);
+ EXPECT_NE(p1, p4);
+}
+
TEST(FlatMap, Size) { EXPECT_EQ(kOddMap.size(), static_cast<uint32_t>(5)); }
TEST(FlatMap, EmptyFlatMapSize) {
- constexpr FlatMap<int, char, 0> kEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 0> kEmpty({{}});
EXPECT_EQ(kEmpty.size(), static_cast<uint32_t>(0));
}
TEST(FlatMap, Empty) {
- constexpr FlatMap<int, char, 0> kEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 0> kEmpty({{}});
EXPECT_TRUE(kEmpty.empty());
}
TEST(FlatMap, NotEmpty) {
- constexpr FlatMap<int, char, 1> kNotEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 1> kNotEmpty({{}});
EXPECT_FALSE(kNotEmpty.empty());
}
TEST(FlatMap, EmptyFlatMapFind) {
- constexpr FlatMap<int, char, 0> kEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 0> kEmpty({{}});
EXPECT_EQ(kEmpty.find(0), kEmpty.end());
}
TEST(FlatMap, EmptyFlatMapLowerBound) {
- constexpr FlatMap<int, char, 0> kEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 0> kEmpty({{}});
EXPECT_EQ(kEmpty.lower_bound(0), kEmpty.end());
}
TEST(FlatMap, EmptyFlatMapUpperBound) {
- constexpr FlatMap<int, char, 0> kEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 0> kEmpty({{}});
EXPECT_EQ(kEmpty.upper_bound(0), kEmpty.end());
}
TEST(FlatMap, EmptyEqualRange) {
- constexpr FlatMap<int, char, 0> kEmpty({{}});
+ PW_CONSTEXPR_CPP20 FlatMap<int, char, 0> kEmpty({{}});
EXPECT_EQ(kEmpty.equal_range(0).first, kEmpty.end());
EXPECT_EQ(kEmpty.equal_range(0).second, kEmpty.end());
}
+TEST(FlatMap, ConstAtReturnsCorrectValues) {
+ for (const auto& [key, value] : kOddMap) {
+ EXPECT_EQ(value, kOddMap.at(key));
+ }
+}
+
+TEST(FlatMap, MutableAtReturnsCorrectMutableValues) {
+ FlatMap<int, char, 5> mutable_map({{
+ {-4, 'a'},
+ {-1, 'b'},
+ {0, 'c'},
+ {49, 'd'},
+ {99, 'e'},
+ }});
+
+ for (const auto& [key, value] : mutable_map) {
+ const char original_value{value};
+ EXPECT_EQ(original_value, mutable_map.at(key));
+ ++mutable_map.at(key);
+ EXPECT_EQ(original_value + 1, mutable_map.at(key));
+ }
+}
+
TEST(FlatMap, Contains) {
EXPECT_TRUE(kOddMap.contains(0));
EXPECT_FALSE(kOddMap.contains(10));
@@ -81,6 +120,40 @@ TEST(FlatMap, Iterate) {
}
}
+TEST(FlatMap, ForwardsMappedValuesIterationWithDeferenceWorks) {
+ FlatMap<int, char, 5> map({{
+ {-4, 'a'},
+ {-1, 'b'},
+ {0, 'c'},
+ {49, 'd'},
+ {99, 'e'},
+ }});
+
+ char value = 'a';
+ for (auto it = map.mapped_begin(); it != map.mapped_end(); ++it) {
+ EXPECT_EQ(value, *it);
+ ++value;
+ }
+}
+
+TEST(FlatMap, BackwardsMappedValuesIterationWithDereferenceWorks) {
+ FlatMap<int, char, 5> map({{
+ {-4, 'a'},
+ {-1, 'b'},
+ {0, 'c'},
+ {49, 'd'},
+ {99, 'e'},
+ }});
+
+ char value = 'e';
+ auto it = map.mapped_end();
+ do {
+ --it;
+ EXPECT_EQ(value, *it);
+ --value;
+ } while (it != map.mapped_begin());
+}
+
TEST(FlatMap, EqualRange) {
auto pair = kOddMap.equal_range(1);
EXPECT_EQ(1, pair.first->first);
@@ -187,4 +260,91 @@ TEST(FlatMap, DontDereferenceEnd) {
EXPECT_EQ(unsorted_array.contains(3), false);
}
+TEST(FlatMap, NullMappedIteratorNotEqualToValidOne) {
+ Single map({{{-4, 'a'}}});
+
+ EXPECT_NE(Single::mapped_iterator(), map.mapped_begin());
+}
+
+TEST(FlatMap, CopyConstructedMapIteratorEqualToSource) {
+ Single map({{{-4, 'a'}}});
+
+ EXPECT_EQ(Single::mapped_iterator(map.mapped_begin()), map.mapped_begin());
+}
+
+TEST(FlatMap, CopyAssignedMapIteratorEqualToSource) {
+ Single map({{{-4, 'a'}}});
+
+ Single::mapped_iterator it;
+ EXPECT_NE(it, map.mapped_begin());
+ it = map.mapped_begin();
+ EXPECT_EQ(it, map.mapped_begin());
+}
+
+TEST(FlatMap, MappedIteratorCorrectDereferenceMutation) {
+ constexpr int kKey = -4;
+ constexpr char kValue = 'a';
+ Single mutable_map({{{kKey, kValue}}});
+
+ ++*mutable_map.mapped_begin();
+ EXPECT_EQ(kValue + 1, mutable_map.at(kKey));
+}
+
+TEST(FlatMap, MappedIteratorValueCorrectMemberAccess) {
+ constexpr int kAValue{5};
+ struct A {
+ int a;
+ };
+ FlatMap<int, A, 1> map({{{-4, A{kAValue}}}});
+
+ EXPECT_EQ(kAValue, map.mapped_begin()->a);
+}
+
+TEST(FlatMap, MappedIteratorValueCorrectMemberMutation) {
+ constexpr int kAValue{5};
+ struct A {
+ int a;
+ };
+ FlatMap<int, A, 1> map({{{-4, A{kAValue}}}});
+
+ ++map.mapped_begin()->a;
+ EXPECT_EQ(kAValue + 1, map.mapped_begin()->a);
+}
+
+TEST(FlatMap, MappedIteratorPrefixIncrementCorrectReturnAndSideEffect) {
+ Single map({{{-4, 'a'}}});
+
+ auto it = map.mapped_begin();
+ auto it_incremented = ++it;
+ EXPECT_EQ(it, it_incremented);
+ EXPECT_NE(map.mapped_begin(), it_incremented);
+}
+
+TEST(FlatMap, MappedIteratorPostfixIncrementCorrectReturnAndSideEffect) {
+ Single map({{{-4, 'a'}}});
+
+ auto it = map.mapped_begin();
+ auto it_original = it++;
+ EXPECT_EQ(map.mapped_begin(), it_original);
+ EXPECT_NE(it, it_original);
+}
+
+TEST(FlatMap, MappedIteratorPrefixDecrementCorrectReturnAndSideEffect) {
+ Single map({{{-4, 'a'}}});
+
+ auto it = map.mapped_end();
+ auto it_decremented = --it;
+ EXPECT_EQ(it, it_decremented);
+ EXPECT_NE(map.mapped_end(), it_decremented);
+}
+
+TEST(FlatMap, MappedIteratorPostfixDecrementCorrectReturnAndSideEffect) {
+ Single map({{{-4, 'a'}}});
+
+ auto it = map.mapped_end();
+ auto it_original = it--;
+ EXPECT_EQ(map.mapped_end(), it_original);
+ EXPECT_NE(it, it_original);
+}
+
} // namespace pw::containers
diff --git a/pw_containers/inline_deque_test.cc b/pw_containers/inline_deque_test.cc
new file mode 100644
index 000000000..b78ece52e
--- /dev/null
+++ b/pw_containers/inline_deque_test.cc
@@ -0,0 +1,960 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_containers/inline_deque.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_compilation_testing/negative_compilation.h"
+#include "pw_containers/algorithm.h"
+#include "pw_containers_private/test_helpers.h"
+
+namespace pw::containers {
+namespace {
+
+using namespace std::literals::string_view_literals;
+using test::CopyOnly;
+using test::Counter;
+using test::MoveOnly;
+
+TEST(InlineDeque, Construct_Sized) {
+ InlineDeque<int, 3> deque;
+ EXPECT_TRUE(deque.empty());
+ EXPECT_EQ(deque.size(), 0u);
+ EXPECT_EQ(deque.max_size(), 3u);
+}
+
+TEST(InlineDeque, Construct_GenericSized) {
+ InlineDeque<int, 3> sized_deque;
+ InlineDeque<int>& deque(sized_deque);
+ EXPECT_TRUE(deque.empty());
+ EXPECT_EQ(deque.size(), 0u);
+ EXPECT_EQ(deque.max_size(), 3u);
+}
+
+TEST(InlineDeque, Construct_CopySameCapacity) {
+ InlineDeque<CopyOnly, 4> deque(4, CopyOnly(123));
+ InlineDeque<CopyOnly, 4> copied(deque);
+
+ EXPECT_EQ(4u, deque.size());
+ EXPECT_EQ(123, deque[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineDeque, Construct_CopyLargerCapacity) {
+ InlineDeque<CopyOnly, 4> deque(4, CopyOnly(123));
+ InlineDeque<CopyOnly, 5> copied(deque);
+
+ EXPECT_EQ(4u, deque.size());
+ EXPECT_EQ(123, deque[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineDeque, Construct_CopySmallerCapacity) {
+ InlineDeque<CopyOnly, 4> deque(3, CopyOnly(123));
+ InlineDeque<CopyOnly, 3> copied(deque);
+
+ EXPECT_EQ(3u, deque.size());
+ EXPECT_EQ(123, deque[2].value);
+
+ EXPECT_EQ(3u, copied.size());
+ EXPECT_EQ(123, copied[2].value);
+}
+
+TEST(InlineDeque, Destruct_ZeroLength) {
+ Counter::Reset();
+ {
+ InlineDeque<Counter, 0> deque;
+ EXPECT_EQ(deque.size(), 0u);
+ }
+ EXPECT_EQ(Counter::created, 0);
+ EXPECT_EQ(Counter::destroyed, 0);
+}
+
+TEST(InlineDeque, Destruct_Empty) {
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 3> deque;
+ EXPECT_EQ(deque.size(), 0u);
+ }
+ EXPECT_EQ(Counter::created, 0);
+ EXPECT_EQ(Counter::destroyed, 0);
+}
+
+TEST(InlineDeque, Destruct_MultipleEntries) {
+ Counter value;
+ Counter::Reset();
+
+ { InlineDeque<Counter, 128> deque(100, value); }
+
+ EXPECT_EQ(Counter::created, 100);
+ EXPECT_EQ(Counter::destroyed, 100);
+}
+
+TEST(InlineDeque, Assign_InitializerList) {
+ InlineDeque<int, 4> deque = {1, 3, 5, 7};
+
+ EXPECT_EQ(4u, deque.size());
+
+ EXPECT_EQ(1, deque[0]);
+ EXPECT_EQ(3, deque[1]);
+ EXPECT_EQ(5, deque[2]);
+ EXPECT_EQ(7, deque[3]);
+}
+
+TEST(InlineDeque, Assign_CopySameCapacity) {
+ InlineDeque<CopyOnly, 4> deque(4, CopyOnly(123));
+ InlineDeque<CopyOnly, 4> copied = deque;
+
+ EXPECT_EQ(4u, deque.size());
+ EXPECT_EQ(123, deque[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineDeque, Assign_CopyLargerCapacity) {
+ InlineDeque<CopyOnly, 4> deque(4, CopyOnly(123));
+ InlineDeque<CopyOnly, 5> copied = deque;
+
+ EXPECT_EQ(4u, deque.size());
+ EXPECT_EQ(123, deque[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineDeque, Assign_CopySmallerCapacity) {
+ InlineDeque<CopyOnly, 4> deque(3, CopyOnly(123));
+ InlineDeque<CopyOnly, 3> copied = deque;
+
+ EXPECT_EQ(3u, deque.size());
+ EXPECT_EQ(123, deque[2].value);
+
+ EXPECT_EQ(3u, copied.size());
+ EXPECT_EQ(123, copied[2].value);
+}
+
+TEST(InlineDeque, Access_Iterator) {
+ InlineDeque<Counter, 2> deque(2);
+ for (Counter& item : deque) {
+ EXPECT_EQ(item.value, 0);
+ }
+ for (const Counter& item : deque) {
+ EXPECT_EQ(item.value, 0);
+ }
+}
+
+TEST(InlineDeque, Access_ConstIterator) {
+ const InlineDeque<Counter, 2> deque(2);
+ for (const Counter& item : deque) {
+ EXPECT_EQ(item.value, 0);
+ }
+}
+
+TEST(InlineDeque, Access_ZeroLength) {
+ InlineDeque<Counter, 0> deque;
+
+ EXPECT_EQ(0u, deque.size());
+ EXPECT_EQ(0u, deque.max_size());
+ EXPECT_TRUE(deque.empty());
+ EXPECT_TRUE(deque.full());
+
+ for (Counter& item : deque) {
+ (void)item;
+ FAIL();
+ }
+}
+
+TEST(InlineDeque, Access_ContiguousData) {
+ // Content = {}, Storage = [x, x]
+ InlineDeque<int, 2> deque;
+
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_EQ(first.size(), 0u);
+ EXPECT_EQ(second.size(), 0u);
+ }
+
+ // Content = {1}, Storage = [1, x]
+ deque.push_back(1);
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{1}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {1, 2}, Storage = [1, 2]
+ deque.push_back(2);
+ EXPECT_TRUE(deque.full());
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 2>{1, 2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {2}, Storage = [x, 2]
+ deque.pop_front();
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {2, 1}, Storage = [1, 2]
+ deque.push_back(1);
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 1>{1}));
+ }
+
+ // Content = {1}, Storage = [1, x]
+ deque.pop_front();
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{1}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {1, 2}, Storage = [1, 2]
+ deque.push_back(2);
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 2>{1, 2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+}
+
+TEST(InlineDeque, Access_ConstContiguousData) {
+ // Content = {1, 2}, Storage = [1, 2]
+ const InlineDeque<int, 2> deque = {1, 2};
+
+ {
+ auto [first, second] = deque.contiguous_data();
+ EXPECT_EQ(first.size(), 2u);
+ EXPECT_EQ(second.size(), 0u);
+ }
+}
+
+TEST(InlineDeque, Modify_Clear) {
+ Counter::Reset();
+
+ InlineDeque<Counter, 100> deque;
+ deque.emplace_back();
+ deque.emplace_back();
+ deque.emplace_back();
+
+ deque.clear();
+
+ EXPECT_EQ(3, Counter::created);
+ EXPECT_EQ(3, Counter::destroyed);
+}
+
+TEST(InlineDeque, Modify_PushBack_Copy) {
+ Counter value(99);
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 10> deque;
+ deque.push_back(value);
+
+ EXPECT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque.front().value, 99);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 1);
+}
+
+TEST(InlineDeque, Modify_PushBack_Move) {
+ Counter::Reset();
+
+ {
+ Counter value(99);
+ InlineDeque<Counter, 10> deque;
+ deque.push_back(std::move(value));
+
+ EXPECT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque.front().value, 99);
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ EXPECT_EQ(value.value, 0);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 2);
+ EXPECT_EQ(Counter::moved, 1);
+}
+
+TEST(InlineDeque, Modify_EmplaceBack) {
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 10> deque;
+ deque.emplace_back(314);
+
+ EXPECT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque.front().value, 314);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 1);
+}
+
+TEST(InlineDeque, Modify_WrapForwards) {
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 3> deque;
+ deque.emplace_back(1);
+ deque.emplace_back(2);
+ deque.emplace_back(3);
+
+ ASSERT_EQ(deque.size(), 3u);
+ EXPECT_EQ(deque[0].value, 1);
+ EXPECT_EQ(deque.front().value, 1);
+ EXPECT_EQ(deque[1].value, 2);
+ EXPECT_EQ(deque[2].value, 3);
+ EXPECT_EQ(deque.back().value, 3);
+
+ deque.pop_front();
+ deque.emplace_back(4);
+
+ ASSERT_EQ(deque.size(), 3u);
+ EXPECT_EQ(deque[0].value, 2);
+ EXPECT_EQ(deque.front().value, 2);
+ EXPECT_EQ(deque[1].value, 3);
+ EXPECT_EQ(deque[2].value, 4);
+ EXPECT_EQ(deque.back().value, 4);
+ }
+
+ EXPECT_EQ(Counter::created, 4);
+ EXPECT_EQ(Counter::destroyed, 4);
+}
+
+TEST(InlineDeque, Modify_WrapBackwards) {
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 3> deque;
+ deque.emplace_front(1);
+ deque.emplace_front(2);
+ deque.emplace_front(3);
+
+ ASSERT_EQ(deque.size(), 3u);
+ EXPECT_EQ(deque[0].value, 3);
+ EXPECT_EQ(deque.front().value, 3);
+ EXPECT_EQ(deque[1].value, 2);
+ EXPECT_EQ(deque[2].value, 1);
+ EXPECT_EQ(deque.back().value, 1);
+
+ deque.pop_back();
+ deque.emplace_front(4);
+
+ ASSERT_EQ(deque.size(), 3u);
+ EXPECT_EQ(deque[0].value, 4);
+ EXPECT_EQ(deque.front().value, 4);
+ EXPECT_EQ(deque[1].value, 3);
+ EXPECT_EQ(deque[2].value, 2);
+ EXPECT_EQ(deque.back().value, 2);
+ }
+
+ EXPECT_EQ(Counter::created, 4);
+ EXPECT_EQ(Counter::destroyed, 4);
+}
+
+TEST(InlineDeque, Modify_PushFront_Copy) {
+ Counter value(99);
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 10> deque;
+ deque.push_front(value);
+
+ EXPECT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque.front().value, 99);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 1);
+}
+
+TEST(InlineDeque, Modify_PushFront_Move) {
+ Counter::Reset();
+
+ {
+ Counter value(99);
+ InlineDeque<Counter, 10> deque;
+ deque.push_front(std::move(value));
+
+ EXPECT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque.front().value, 99);
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ EXPECT_EQ(value.value, 0);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 2);
+ EXPECT_EQ(Counter::moved, 1);
+}
+
+TEST(InlineDeque, Modify_EmplaceFront) {
+ Counter::Reset();
+
+ {
+ InlineDeque<Counter, 10> deque;
+ deque.emplace_front(314);
+
+ EXPECT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque.front().value, 314);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 1);
+}
+
+TEST(InlineDeque, Modify_PopBack) {
+ Counter::Reset();
+
+ InlineDeque<Counter, 3> deque;
+ deque.emplace_front(1); // This wraps to the other end.
+ deque.emplace_back(2); // This is the first entry in storage.
+ deque.emplace_back(3);
+ // Content = {1, 2, 3}, Storage = [2, 3, 1]
+
+ ASSERT_EQ(deque.size(), 3u);
+ EXPECT_EQ(deque[0].value, 1);
+ EXPECT_EQ(deque[1].value, 2);
+ EXPECT_EQ(deque[2].value, 3);
+
+ deque.pop_back();
+ // Content = {1, 2}, Storage = [2, x, 1]
+ ASSERT_EQ(deque.size(), 2u);
+ EXPECT_EQ(deque[0].value, 1);
+ EXPECT_EQ(deque[1].value, 2);
+
+ // This wraps around.
+ deque.pop_back();
+ // Content = {1}, Storage = [x, x, 1]
+
+ ASSERT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque[0].value, 1);
+
+ EXPECT_EQ(Counter::created, 3);
+ EXPECT_EQ(Counter::destroyed, 2);
+}
+
+TEST(InlineDeque, Modify_PopFront) {
+ Counter::Reset();
+
+ InlineDeque<Counter, 3> deque;
+ deque.emplace_front(1); // This wraps to the other end.
+ deque.emplace_back(2); // This is the first entry in storage.
+ deque.emplace_back(3);
+ // Content = {1, 2, 3}, Storage = [2, 3, 1]
+
+ ASSERT_EQ(deque.size(), 3u);
+ EXPECT_EQ(deque[0].value, 1);
+ EXPECT_EQ(deque[1].value, 2);
+ EXPECT_EQ(deque[2].value, 3);
+
+ // This wraps around
+ deque.pop_front();
+ // Content = {2, 3}, Storage = [2, 3, x]
+
+ EXPECT_EQ(deque.size(), 2u);
+ EXPECT_EQ(deque[0].value, 2);
+ EXPECT_EQ(deque[1].value, 3);
+
+ deque.pop_front();
+ // Content = {3}, Storage = [x, 3, x]
+ ASSERT_EQ(deque.size(), 1u);
+ EXPECT_EQ(deque[0].value, 3);
+
+ EXPECT_EQ(Counter::created, 3);
+ EXPECT_EQ(Counter::destroyed, 2);
+}
+
+TEST(InlineDeque, Modify_Resize_Larger) {
+ InlineDeque<CopyOnly, 10> deque(1, CopyOnly(123));
+ deque.resize(3, CopyOnly(123));
+
+ EXPECT_EQ(deque.size(), 3u);
+ for (auto& i : deque) {
+ EXPECT_EQ(i.value, 123);
+ }
+}
+
+TEST(InlineDeque, Modify_Resize_LargerThanMax) {
+ InlineDeque<CopyOnly, 10> deque;
+ deque.resize(1000, CopyOnly(123));
+
+ EXPECT_EQ(deque.size(), 10u);
+ for (auto& i : deque) {
+ EXPECT_EQ(i.value, 123);
+ }
+}
+
+TEST(InlineDeque, Modify_Resize_Smaller) {
+ InlineDeque<CopyOnly, 10> deque(9, CopyOnly(123));
+ deque.resize(3, CopyOnly(123));
+
+ EXPECT_EQ(deque.size(), 3u);
+ for (auto& i : deque) {
+ EXPECT_EQ(i.value, 123);
+ }
+}
+
+TEST(InlineDeque, Modify_Resize_Zero) {
+ InlineDeque<CopyOnly, 10> deque(10, CopyOnly(123));
+ deque.resize(0, CopyOnly(123));
+
+ EXPECT_EQ(deque.size(), 0u);
+}
+
+TEST(InlineDeque, Generic) {
+ InlineDeque<int, 10> deque;
+ InlineDeque<int>& generic_deque(deque);
+ generic_deque = {1, 2, 3, 4, 5};
+
+ EXPECT_EQ(generic_deque.size(), deque.size());
+ EXPECT_EQ(generic_deque.max_size(), deque.max_size());
+
+ unsigned short i = 0;
+ for (int value : deque) {
+ EXPECT_EQ(value, generic_deque[i]);
+ i += 1;
+ }
+
+ i = 0;
+ for (int value : generic_deque) {
+ EXPECT_EQ(deque[i], value);
+ i += 1;
+ }
+}
+
+TEST(InlineDeque, ConstexprMaxSize) {
+ InlineDeque<int, 10> deque;
+ constexpr size_t kMaxSize = deque.max_size();
+ EXPECT_EQ(deque.max_size(), kMaxSize);
+
+ // Ensure the generic sized container does not have a constexpr max_size().
+ [[maybe_unused]] InlineDeque<int>& generic_deque(deque);
+#if PW_NC_TEST(InlineDeque_GenericMaxSize_NotConstexpr)
+ PW_NC_EXPECT_CLANG(
+ "kGenericMaxSize.* must be initialized by a constant expression");
+ PW_NC_EXPECT_GCC("call to non-'constexpr' function .*InlineDeque.*max_size");
+ [[maybe_unused]] constexpr size_t kGenericMaxSize = generic_deque.max_size();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineDeque, StdMaxElement) {
+ // Content = {1, 2, 3, 4}, Storage = [1, 2, 3, 4]
+ InlineDeque<int, 4> deque = {1, 2, 3, 4};
+
+ auto max_element_it = std::max_element(deque.begin(), deque.end());
+ ASSERT_NE(max_element_it, deque.end());
+ EXPECT_EQ(*max_element_it, 4);
+
+ // Content = {2, 3, 4}, Storage = [x, 2, 3, 4]
+ deque.pop_front();
+
+ max_element_it = std::max_element(deque.begin(), deque.end());
+ ASSERT_NE(max_element_it, deque.end());
+ EXPECT_EQ(*max_element_it, 4);
+
+ // Content = {2, 3, 4, 5}, Storage = [5, 2, 3, 4]
+ deque.push_back(5);
+ max_element_it = std::max_element(deque.begin(), deque.end());
+ ASSERT_NE(max_element_it, deque.end());
+ EXPECT_EQ(*max_element_it, 5);
+
+ // Content = {}, Storage = [x, x, x, x]
+ deque.clear();
+
+ max_element_it = std::max_element(deque.begin(), deque.end());
+ ASSERT_EQ(max_element_it, deque.end());
+}
+
+TEST(InlineDeque, StdMaxElementConst) {
+ // Content = {1, 2, 3, 4}, Storage = [1, 2, 3, 4]
+ InlineDeque<int, 4> deque = {1, 2, 3, 4};
+
+ auto max_element_it = std::max_element(deque.cbegin(), deque.cend());
+ ASSERT_NE(max_element_it, deque.cend());
+ EXPECT_EQ(*max_element_it, 4);
+
+ // Content = {2, 3, 4}, Storage = [x, 2, 3, 4]
+ deque.pop_front();
+
+ max_element_it = std::max_element(deque.cbegin(), deque.cend());
+ ASSERT_NE(max_element_it, deque.cend());
+ EXPECT_EQ(*max_element_it, 4);
+
+ // Content = {2, 3, 4, 5}, Storage = [5, 2, 3, 4]
+ deque.push_back(5);
+ max_element_it = std::max_element(deque.cbegin(), deque.cend());
+ ASSERT_NE(max_element_it, deque.cend());
+ EXPECT_EQ(*max_element_it, 5);
+
+ // Content = {}, Storage = [x, x, x, x]
+ deque.clear();
+
+ max_element_it = std::max_element(deque.cbegin(), deque.cend());
+ ASSERT_EQ(max_element_it, deque.cend());
+}
+
+TEST(InlineDeque, OperatorPlus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 0; i < 4; i++) {
+ ASSERT_EQ(*(deque.begin() + i), static_cast<int>(i + 1));
+ ASSERT_EQ(*(i + deque.begin()), static_cast<int>(i + 1));
+ }
+
+ ASSERT_EQ(deque.begin() + deque.size(), deque.end());
+}
+
+TEST(InlineDeque, OperatorPlusPlus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ auto it = deque.begin();
+
+ ASSERT_EQ(*it, 1);
+ it++;
+ ASSERT_EQ(*it, 2);
+ it++;
+ ASSERT_EQ(*it, 3);
+ it++;
+ ASSERT_EQ(*it, 4);
+ it++;
+
+ ASSERT_EQ(it, deque.end());
+}
+
+TEST(InlineDeque, OperatorPlusEquals) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ auto it = deque.begin();
+
+ ASSERT_EQ(*it, 1);
+ it += 1;
+ ASSERT_EQ(*it, 2);
+ it += 1;
+ ASSERT_EQ(*it, 3);
+ it += 1;
+ ASSERT_EQ(*it, 4);
+ it += 1;
+ ASSERT_EQ(it, deque.end());
+
+ it = deque.begin();
+ ASSERT_EQ(*it, 1);
+ it += 2;
+ ASSERT_EQ(*it, 3);
+ it += 2;
+ ASSERT_EQ(it, deque.end());
+
+ it = deque.begin();
+ it += deque.size();
+
+ ASSERT_EQ(it, deque.end());
+}
+
+TEST(InlineDeque, OpeartorMinus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 1; i <= 4; i++) {
+ ASSERT_EQ(*(deque.end() - i), static_cast<int>(5 - i));
+ }
+
+ ASSERT_EQ(deque.end() - deque.size(), deque.begin());
+}
+TEST(InlineDeque, OperatorMinusMinus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ auto it = deque.end();
+
+ it--;
+ ASSERT_EQ(*it, 4);
+ it--;
+ ASSERT_EQ(*it, 3);
+ it--;
+ ASSERT_EQ(*it, 2);
+ it--;
+ ASSERT_EQ(*it, 1);
+
+ ASSERT_EQ(it, deque.begin());
+}
+
+TEST(InlineDeque, OperatorMinusEquals) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ auto it = deque.end();
+
+ it -= 1;
+ ASSERT_EQ(*it, 4);
+ it -= 1;
+ ASSERT_EQ(*it, 3);
+ it -= 1;
+ ASSERT_EQ(*it, 2);
+ it -= 1;
+ ASSERT_EQ(*it, 1);
+
+ ASSERT_EQ(it, deque.begin());
+
+ it = deque.end();
+
+ it -= 2;
+ ASSERT_EQ(*it, 3);
+ it -= 2;
+ ASSERT_EQ(*it, 1);
+
+ ASSERT_EQ(it, deque.begin());
+
+ it = deque.end();
+ it -= deque.size();
+
+ ASSERT_EQ(it, deque.begin());
+}
+
+TEST(InlineDeque, OperatorSquareBracket) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (unsigned short i = 0; i < deque.size(); i++) {
+ ASSERT_EQ(deque.begin()[i], static_cast<int>(i + 1));
+ }
+}
+
+TEST(InlineDeque, OperatorLessThan) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 0; i < deque.size(); i++) {
+ for (int j = 0; j < i; j++) {
+ ASSERT_TRUE((deque.begin() + j) < (deque.begin() + i));
+ }
+
+ ASSERT_TRUE((deque.begin() + i) < deque.end());
+ }
+}
+
+TEST(InlineDeque, OperatorLessThanEqual) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 0; i < deque.size(); i++) {
+ for (int j = 0; j <= i; j++) {
+ ASSERT_TRUE((deque.begin() + j) <= (deque.begin() + i));
+ }
+
+ ASSERT_TRUE((deque.begin() + i) <= deque.end());
+ }
+}
+
+TEST(InlineDeque, OperatorGreater) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 0; i < deque.size(); i++) {
+ for (int j = i + 1; j < deque.size(); j++) {
+ ASSERT_TRUE((deque.begin() + j) > (deque.begin() + i));
+ }
+
+ ASSERT_TRUE(deque.end() > (deque.begin() + i));
+ }
+}
+
+TEST(InlineDeque, OperatorGreaterThanEqual) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 0; i < deque.size(); i++) {
+ for (int j = i; j < deque.size(); j++) {
+ ASSERT_TRUE((deque.begin() + j) >= (deque.begin() + i));
+ }
+
+ ASSERT_TRUE(deque.end() >= (deque.begin() + i));
+ }
+}
+
+TEST(InlineDeque, DereferenceOperator) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineDeque<int, 4> deque = {0, 0, 1, 2};
+ // Content = {0, 1, 2}, Storage = [x, 0, 1, 2]
+ deque.pop_front();
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ deque.push_back(3);
+ // Content = {1, 2, 3}, Storage = [3, x, 1, 2]
+ deque.pop_front();
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ deque.push_back(4);
+
+ for (int i = 0; i < deque.size(); i++) {
+ const auto it = deque.begin() + i;
+ ASSERT_EQ(*(it.operator->()), static_cast<int>(i + 1));
+ }
+}
+
+// Test that InlineDeque<T> is trivially destructible when its type is.
+static_assert(std::is_trivially_destructible_v<InlineDeque<int>>);
+static_assert(std::is_trivially_destructible_v<InlineDeque<int, 4>>);
+
+static_assert(std::is_trivially_destructible_v<MoveOnly>);
+static_assert(std::is_trivially_destructible_v<InlineDeque<MoveOnly>>);
+static_assert(std::is_trivially_destructible_v<InlineDeque<MoveOnly, 1>>);
+
+static_assert(std::is_trivially_destructible_v<CopyOnly>);
+static_assert(std::is_trivially_destructible_v<InlineDeque<CopyOnly>>);
+static_assert(std::is_trivially_destructible_v<InlineDeque<CopyOnly, 99>>);
+
+static_assert(!std::is_trivially_destructible_v<Counter>);
+static_assert(!std::is_trivially_destructible_v<InlineDeque<Counter>>);
+static_assert(!std::is_trivially_destructible_v<InlineDeque<Counter, 99>>);
+
+// Tests that InlineDeque<T> does not have any extra padding.
+static_assert(sizeof(InlineDeque<uint8_t, 1>) ==
+ sizeof(InlineDeque<uint8_t>::size_type) * 4 +
+ std::max(sizeof(InlineDeque<uint8_t>::size_type),
+ sizeof(uint8_t)));
+static_assert(sizeof(InlineDeque<uint8_t, 2>) ==
+ sizeof(InlineDeque<uint8_t>::size_type) * 4 +
+ 2 * sizeof(uint8_t));
+static_assert(sizeof(InlineDeque<uint16_t, 1>) ==
+ sizeof(InlineDeque<uint16_t>::size_type) * 4 + sizeof(uint16_t));
+static_assert(sizeof(InlineDeque<uint32_t, 1>) ==
+ sizeof(InlineDeque<uint32_t>::size_type) * 4 + sizeof(uint32_t));
+static_assert(sizeof(InlineDeque<uint64_t, 1>) ==
+ sizeof(InlineDeque<uint64_t>::size_type) * 4 + sizeof(uint64_t));
+
+// Test that InlineDeque<T> is copy assignable
+static_assert(std::is_copy_assignable_v<InlineDeque<int>::iterator>);
+static_assert(std::is_copy_assignable_v<InlineDeque<int, 4>::iterator>);
+
+// Test that InlineDeque<T>::iterator can be converted to a const_iterator
+static_assert(std::is_convertible<InlineDeque<int>::iterator,
+ InlineDeque<int>::const_iterator>::value);
+static_assert(std::is_convertible<InlineDeque<int, 4>::iterator,
+ InlineDeque<int, 4>::const_iterator>::value);
+
+// Test that InlineDeque<T>::const_iterator can NOT be converted to a iterator
+static_assert(!std::is_convertible<InlineDeque<int>::const_iterator,
+ InlineDeque<int>::iterator>::value);
+static_assert(!std::is_convertible<InlineDeque<int, 4>::const_iterator,
+ InlineDeque<int, 4>::iterator>::value);
+
+} // namespace
+} // namespace pw::containers
diff --git a/pw_containers/inline_queue_test.cc b/pw_containers/inline_queue_test.cc
new file mode 100644
index 000000000..5c4392cea
--- /dev/null
+++ b/pw_containers/inline_queue_test.cc
@@ -0,0 +1,777 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_containers/inline_queue.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_compilation_testing/negative_compilation.h"
+#include "pw_containers/algorithm.h"
+#include "pw_containers_private/test_helpers.h"
+
+namespace pw::containers {
+namespace {
+
+using namespace std::literals::string_view_literals;
+using test::CopyOnly;
+using test::Counter;
+using test::MoveOnly;
+
+TEST(InlineQueue, Construct_Sized) {
+ InlineQueue<int, 3> queue;
+ EXPECT_TRUE(queue.empty());
+ EXPECT_EQ(queue.size(), 0u);
+ EXPECT_EQ(queue.max_size(), 3u);
+}
+
+TEST(InlineQueue, Construct_GenericSized) {
+ InlineQueue<int, 3> sized_queue;
+ InlineQueue<int>& queue(sized_queue);
+ EXPECT_TRUE(queue.empty());
+ EXPECT_EQ(queue.size(), 0u);
+ EXPECT_EQ(queue.max_size(), 3u);
+}
+
+TEST(InlineQueue, Construct_CopySameCapacity) {
+ InlineQueue<CopyOnly, 4> queue(4, CopyOnly(123));
+ InlineQueue<CopyOnly, 4> copied(queue);
+
+ EXPECT_EQ(4u, queue.size());
+ EXPECT_EQ(123, queue[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineQueue, Construct_CopyLargerCapacity) {
+ InlineQueue<CopyOnly, 4> queue(4, CopyOnly(123));
+ InlineQueue<CopyOnly, 5> copied(queue);
+
+ EXPECT_EQ(4u, queue.size());
+ EXPECT_EQ(123, queue[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineQueue, Construct_CopySmallerCapacity) {
+ InlineQueue<CopyOnly, 4> queue(3, CopyOnly(123));
+ InlineQueue<CopyOnly, 3> copied(queue);
+
+ EXPECT_EQ(3u, queue.size());
+ EXPECT_EQ(123, queue[2].value);
+
+ EXPECT_EQ(3u, copied.size());
+ EXPECT_EQ(123, copied[2].value);
+}
+
+TEST(InlineQueue, Destruct_ZeroLength) {
+ Counter::Reset();
+ {
+ InlineQueue<Counter, 0> queue;
+ EXPECT_EQ(queue.size(), 0u);
+ }
+ EXPECT_EQ(Counter::created, 0);
+ EXPECT_EQ(Counter::destroyed, 0);
+}
+
+TEST(InlineQueue, Destruct_Empty) {
+ Counter::Reset();
+
+ {
+ InlineQueue<Counter, 3> queue;
+ EXPECT_EQ(queue.size(), 0u);
+ }
+ EXPECT_EQ(Counter::created, 0);
+ EXPECT_EQ(Counter::destroyed, 0);
+}
+
+TEST(InlineQueue, Destruct_MultipleEntries) {
+ Counter value;
+ Counter::Reset();
+
+ { InlineQueue<Counter, 128> queue(100, value); }
+
+ EXPECT_EQ(Counter::created, 100);
+ EXPECT_EQ(Counter::destroyed, 100);
+}
+
+TEST(InlineQueue, Assign_InitializerList) {
+ InlineQueue<int, 4> queue = {1, 3, 5, 7};
+
+ EXPECT_EQ(4u, queue.size());
+
+ EXPECT_EQ(1, queue[0]);
+ EXPECT_EQ(3, queue[1]);
+ EXPECT_EQ(5, queue[2]);
+ EXPECT_EQ(7, queue[3]);
+}
+
+TEST(InlineQueue, Assign_CopySameCapacity) {
+ InlineQueue<CopyOnly, 4> queue(4, CopyOnly(123));
+ InlineQueue<CopyOnly, 4> copied = queue;
+
+ EXPECT_EQ(4u, queue.size());
+ EXPECT_EQ(123, queue[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineQueue, Assign_CopyLargerCapacity) {
+ InlineQueue<CopyOnly, 4> queue(4, CopyOnly(123));
+ InlineQueue<CopyOnly, 5> copied = queue;
+
+ EXPECT_EQ(4u, queue.size());
+ EXPECT_EQ(123, queue[3].value);
+
+ EXPECT_EQ(4u, copied.size());
+ EXPECT_EQ(123, copied[3].value);
+}
+
+TEST(InlineQueue, Assign_CopySmallerCapacity) {
+ InlineQueue<CopyOnly, 4> queue(3, CopyOnly(123));
+ InlineQueue<CopyOnly, 3> copied = queue;
+
+ EXPECT_EQ(3u, queue.size());
+ EXPECT_EQ(123, queue[2].value);
+
+ EXPECT_EQ(3u, copied.size());
+ EXPECT_EQ(123, copied[2].value);
+}
+
+TEST(InlineQueue, Access_Iterator) {
+ InlineQueue<Counter, 2> queue(2);
+ for (Counter& item : queue) {
+ EXPECT_EQ(item.value, 0);
+ }
+ for (const Counter& item : queue) {
+ EXPECT_EQ(item.value, 0);
+ }
+}
+
+TEST(InlineQueue, Access_ConstIterator) {
+ const InlineQueue<Counter, 2> queue(2);
+ for (const Counter& item : queue) {
+ EXPECT_EQ(item.value, 0);
+ }
+}
+
+TEST(InlineQueue, Access_ZeroLength) {
+ InlineQueue<Counter, 0> queue;
+
+ EXPECT_EQ(0u, queue.size());
+ EXPECT_EQ(0u, queue.max_size());
+ EXPECT_TRUE(queue.empty());
+ EXPECT_TRUE(queue.full());
+
+ for (Counter& item : queue) {
+ (void)item;
+ FAIL();
+ }
+}
+
+TEST(InlineQueue, Access_ContiguousData) {
+ // Content = {}, Storage = [x, x]
+ InlineQueue<int, 2> queue;
+
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_EQ(first.size(), 0u);
+ EXPECT_EQ(second.size(), 0u);
+ }
+
+ // Content = {1}, Storage = [1, x]
+ queue.push(1);
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{1}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {1, 2}, Storage = [1, 2]
+ queue.push(2);
+ EXPECT_TRUE(queue.full());
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 2>{1, 2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {2}, Storage = [x, 2]
+ queue.pop();
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {2, 1}, Storage = [1, 2]
+ queue.push(1);
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 1>{1}));
+ }
+
+ // Content = {1}, Storage = [1, x]
+ queue.pop();
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 1>{1}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+
+ // Content = {1, 2}, Storage = [1, 2]
+ queue.push(2);
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_TRUE(Equal(first, std::array<int, 2>{1, 2}));
+ EXPECT_TRUE(Equal(second, std::array<int, 0>{}));
+ }
+}
+
+TEST(InlineQueue, Access_ConstContiguousData) {
+ // Content = {1, 2}, Storage = [1, 2]
+ const InlineQueue<int, 2> queue = {1, 2};
+
+ {
+ auto [first, second] = queue.contiguous_data();
+ EXPECT_EQ(first.size(), 2u);
+ EXPECT_EQ(second.size(), 0u);
+ }
+}
+
+TEST(InlineQueue, Modify_Clear) {
+ Counter::Reset();
+
+ InlineQueue<Counter, 100> queue;
+ queue.emplace();
+ queue.emplace();
+ queue.emplace();
+
+ queue.clear();
+
+ EXPECT_EQ(3, Counter::created);
+ EXPECT_EQ(3, Counter::destroyed);
+}
+
+TEST(InlineQueue, Modify_Push_Copy) {
+ Counter value(99);
+ Counter::Reset();
+
+ {
+ InlineQueue<Counter, 10> queue;
+ queue.push(value);
+
+ EXPECT_EQ(queue.size(), 1u);
+ EXPECT_EQ(queue.front().value, 99);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 1);
+}
+
+TEST(InlineQueue, Modify_Push_Move) {
+ Counter::Reset();
+
+ {
+ Counter value(99);
+ InlineQueue<Counter, 10> queue;
+ queue.push(std::move(value));
+
+ EXPECT_EQ(queue.size(), 1u);
+ EXPECT_EQ(queue.front().value, 99);
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ EXPECT_EQ(value.value, 0);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 2);
+ EXPECT_EQ(Counter::moved, 1);
+}
+
+TEST(InlineQueue, Modify_Emplace) {
+ Counter::Reset();
+
+ {
+ InlineQueue<Counter, 10> queue;
+ queue.emplace(314);
+
+ EXPECT_EQ(queue.size(), 1u);
+ EXPECT_EQ(queue.front().value, 314);
+ }
+
+ EXPECT_EQ(Counter::created, 1);
+ EXPECT_EQ(Counter::destroyed, 1);
+}
+
+TEST(InlineQueue, Modify_Overwrite) {
+ Counter::Reset();
+ {
+ InlineQueue<Counter, 2> queue(2);
+ queue.push_overwrite(1);
+ queue.emplace_overwrite(2);
+
+ EXPECT_EQ(queue.size(), 2u);
+ EXPECT_EQ(queue.front().value, 1);
+ EXPECT_EQ(queue.back().value, 2);
+ }
+}
+
+TEST(InlineQueue, Modify_Wrap) {
+ Counter::Reset();
+
+ {
+ InlineQueue<Counter, 3> queue;
+ queue.emplace(1);
+ queue.emplace(2);
+ queue.emplace(3);
+
+ ASSERT_EQ(queue.size(), 3u);
+ EXPECT_EQ(queue[0].value, 1);
+ EXPECT_EQ(queue[1].value, 2);
+ EXPECT_EQ(queue[2].value, 3);
+
+ queue.pop();
+ queue.emplace(4);
+
+ ASSERT_EQ(queue.size(), 3u);
+ EXPECT_EQ(queue[0].value, 2);
+ EXPECT_EQ(queue[1].value, 3);
+ EXPECT_EQ(queue[2].value, 4);
+ }
+
+ EXPECT_EQ(Counter::created, 4);
+ EXPECT_EQ(Counter::destroyed, 4);
+}
+
+TEST(InlineQueue, Modify_Pop) {
+ Counter::Reset();
+
+ InlineQueue<Counter, 3> queue;
+ queue.emplace(0);
+ queue.pop();
+ queue.emplace(0);
+ queue.pop();
+ queue.emplace(1); // This wraps to the other end.
+ queue.emplace(2); // This is the first entry in storage.
+ queue.emplace(3);
+ // Content = {1, 2, 3}, Storage = [2, 3, 1]
+
+ ASSERT_EQ(queue.size(), 3u);
+ EXPECT_EQ(queue[0].value, 1);
+ EXPECT_EQ(queue[1].value, 2);
+ EXPECT_EQ(queue[2].value, 3);
+
+ // This wraps around
+ queue.pop();
+ // Content = {2, 3}, Storage = [2, 3, x]
+
+ EXPECT_EQ(queue.size(), 2u);
+ EXPECT_EQ(queue[0].value, 2);
+ EXPECT_EQ(queue[1].value, 3);
+
+ queue.pop();
+ // Content = {3}, Storage = [x, 3, x]
+ ASSERT_EQ(queue.size(), 1u);
+ EXPECT_EQ(queue[0].value, 3);
+
+ EXPECT_EQ(Counter::created, 5);
+ EXPECT_EQ(Counter::destroyed, 4);
+}
+
+TEST(InlineQueue, Generic) {
+ InlineQueue<int, 10> queue({1, 2, 3, 4, 5});
+ InlineQueue<int>& generic_queue(queue);
+
+ EXPECT_EQ(generic_queue.size(), queue.size());
+ EXPECT_EQ(generic_queue.max_size(), queue.max_size());
+
+ uint16_t i = 0;
+ for (int value : queue) {
+ EXPECT_EQ(value, generic_queue[i]);
+ i += 1;
+ }
+
+ i = 0;
+ for (int value : generic_queue) {
+ EXPECT_EQ(queue[i], value);
+ i += 1;
+ }
+}
+
+TEST(InlineQueue, ConstexprMaxSize) {
+ InlineQueue<int, 10> queue;
+ constexpr size_t kMaxSize = queue.max_size();
+ EXPECT_EQ(queue.max_size(), kMaxSize);
+
+ // Ensure the generic sized container does not have a constexpr max_size().
+ [[maybe_unused]] InlineQueue<int>& generic_queue(queue);
+#if PW_NC_TEST(InlineQueue_GenericMaxSize_NotConstexpr)
+ PW_NC_EXPECT_CLANG(
+ "kGenericMaxSize.* must be initialized by a constant expression");
+ PW_NC_EXPECT_GCC("call to non-'constexpr' function .*InlineQueue.*max_size");
+ [[maybe_unused]] constexpr size_t kGenericMaxSize = generic_queue.max_size();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineQueue, StdMaxElement) {
+ // Content = {1, 2, 3, 4}, Storage = [1, 2, 3, 4]
+ InlineQueue<int, 4> queue = {1, 2, 3, 4};
+
+ auto max_element_it = std::max_element(queue.begin(), queue.end());
+ ASSERT_NE(max_element_it, queue.end());
+ EXPECT_EQ(*max_element_it, 4);
+
+ // Content = {2, 3, 4, 5}, Storage = [5, 2, 3, 4]
+ queue.push_overwrite(5);
+
+ max_element_it = std::max_element(queue.begin(), queue.end());
+ ASSERT_NE(max_element_it, queue.end());
+ EXPECT_EQ(*max_element_it, 5);
+
+ // Content = {3, 4, 5}, Storage = [5, x, 3, 4]
+ queue.pop();
+
+ max_element_it = std::max_element(queue.begin(), queue.end());
+ ASSERT_NE(max_element_it, queue.end());
+ EXPECT_EQ(*max_element_it, 5);
+
+ // Content = {}, Storage = [x, x, x, x]
+ queue.clear();
+
+ max_element_it = std::max_element(queue.begin(), queue.end());
+ ASSERT_EQ(max_element_it, queue.end());
+}
+
+TEST(InlineQueue, StdMaxElementConst) {
+ // Content = {1, 2, 3, 4}, Storage = [1, 2, 3, 4]
+ InlineQueue<int, 4> queue = {1, 2, 3, 4};
+
+ auto max_element_it = std::max_element(queue.cbegin(), queue.cend());
+ ASSERT_NE(max_element_it, queue.cend());
+ EXPECT_EQ(*max_element_it, 4);
+
+ // Content = {2, 3, 4, 5}, Storage = [5, 2, 3, 4]
+ queue.push_overwrite(5);
+
+ max_element_it = std::max_element(queue.cbegin(), queue.cend());
+ ASSERT_NE(max_element_it, queue.cend());
+ EXPECT_EQ(*max_element_it, 5);
+
+ // Content = {3, 4, 5}, Storage = [5, x, 3, 4]
+ queue.pop();
+
+ max_element_it = std::max_element(queue.cbegin(), queue.cend());
+ ASSERT_NE(max_element_it, queue.cend());
+ EXPECT_EQ(*max_element_it, 5);
+
+ // Content = {}, Storage = [x, x, x, x]
+ queue.clear();
+
+ max_element_it = std::max_element(queue.cbegin(), queue.cend());
+ ASSERT_EQ(max_element_it, queue.cend());
+}
+
+TEST(InlineQueue, OperatorPlus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 0; i < 4; i++) {
+ ASSERT_EQ(*(queue.begin() + i), static_cast<int>(i + 1));
+ ASSERT_EQ(*(i + queue.begin()), static_cast<int>(i + 1));
+ }
+
+ ASSERT_EQ(queue.begin() + queue.size(), queue.end());
+}
+
+TEST(InlineQueue, OperatorPlusPlus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ auto it = queue.begin();
+
+ ASSERT_EQ(*it, 1);
+ it++;
+ ASSERT_EQ(*it, 2);
+ it++;
+ ASSERT_EQ(*it, 3);
+ it++;
+ ASSERT_EQ(*it, 4);
+ it++;
+
+ ASSERT_EQ(it, queue.end());
+}
+
+TEST(InlineQueue, OperatorPlusEquals) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ auto it = queue.begin();
+
+ ASSERT_EQ(*it, 1);
+ it += 1;
+ ASSERT_EQ(*it, 2);
+ it += 1;
+ ASSERT_EQ(*it, 3);
+ it += 1;
+ ASSERT_EQ(*it, 4);
+ it += 1;
+ ASSERT_EQ(it, queue.end());
+
+ it = queue.begin();
+ ASSERT_EQ(*it, 1);
+ it += 2;
+ ASSERT_EQ(*it, 3);
+ it += 2;
+ ASSERT_EQ(it, queue.end());
+
+ it = queue.begin();
+ it += queue.size();
+
+ ASSERT_EQ(it, queue.end());
+}
+
+TEST(InlineQueue, OpeartorMinus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 1; i <= 4; i++) {
+ ASSERT_EQ(*(queue.end() - i), static_cast<int>(5 - i));
+ }
+
+ ASSERT_EQ(queue.end() - queue.size(), queue.begin());
+}
+TEST(InlineQueue, OperatorMinusMinus) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ auto it = queue.end();
+
+ it--;
+ ASSERT_EQ(*it, 4);
+ it--;
+ ASSERT_EQ(*it, 3);
+ it--;
+ ASSERT_EQ(*it, 2);
+ it--;
+ ASSERT_EQ(*it, 1);
+
+ ASSERT_EQ(it, queue.begin());
+}
+
+TEST(InlineQueue, OperatorMinusEquals) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ auto it = queue.end();
+
+ it -= 1;
+ ASSERT_EQ(*it, 4);
+ it -= 1;
+ ASSERT_EQ(*it, 3);
+ it -= 1;
+ ASSERT_EQ(*it, 2);
+ it -= 1;
+ ASSERT_EQ(*it, 1);
+
+ ASSERT_EQ(it, queue.begin());
+
+ it = queue.end();
+
+ it -= 2;
+ ASSERT_EQ(*it, 3);
+ it -= 2;
+ ASSERT_EQ(*it, 1);
+
+ ASSERT_EQ(it, queue.begin());
+
+ it = queue.end();
+ it -= queue.size();
+
+ ASSERT_EQ(it, queue.begin());
+}
+
+TEST(InlineQueue, OperatorSquareBracket) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (unsigned short i = 0; i < queue.size(); i++) {
+ ASSERT_EQ(queue.begin()[i], static_cast<int>(i + 1));
+ }
+}
+
+TEST(InlineQueue, OperatorLessThan) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 0; i < queue.size(); i++) {
+ for (int j = 0; j < i; j++) {
+ ASSERT_TRUE((queue.begin() + j) < (queue.begin() + i));
+ }
+
+ ASSERT_TRUE((queue.begin() + i) < queue.end());
+ }
+}
+
+TEST(InlineQueue, OperatorLessThanEqual) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 0; i < queue.size(); i++) {
+ for (int j = 0; j <= i; j++) {
+ ASSERT_TRUE((queue.begin() + j) <= (queue.begin() + i));
+ }
+
+ ASSERT_TRUE((queue.begin() + i) <= queue.end());
+ }
+}
+
+TEST(InlineQueue, OperatorGreater) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 0; i < queue.size(); i++) {
+ for (int j = i + 1; j < queue.size(); j++) {
+ ASSERT_TRUE((queue.begin() + j) > (queue.begin() + i));
+ }
+
+ ASSERT_TRUE(queue.end() > (queue.begin() + i));
+ }
+}
+
+TEST(InlineQueue, OperatorGreaterThanEqual) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 0; i < queue.size(); i++) {
+ for (int j = i; j < queue.size(); j++) {
+ ASSERT_TRUE((queue.begin() + j) >= (queue.begin() + i));
+ }
+
+ ASSERT_TRUE(queue.end() >= (queue.begin() + i));
+ }
+}
+
+TEST(InlineQueue, DereferenceOperator) {
+ // Content = {0, 0, 1, 2}, Storage = [0, 0, 1, 2]
+ InlineQueue<int, 4> queue = {0, 0, 1, 2};
+ // Content = {0, 1, 2, 3}, Storage = [3, 0, 1, 2]
+ queue.push_overwrite(3);
+ // Content = {1, 2, 3, 4}, Storage = [3, 4, 1, 2]
+ queue.push_overwrite(4);
+
+ for (int i = 0; i < queue.size(); i++) {
+ const auto it = queue.begin() + i;
+ ASSERT_EQ(*(it.operator->()), static_cast<int>(i + 1));
+ }
+}
+
+// Test that InlineQueue<T> is trivially destructible when its type is.
+static_assert(std::is_trivially_destructible_v<InlineQueue<int>>);
+static_assert(std::is_trivially_destructible_v<InlineQueue<int, 4>>);
+
+static_assert(std::is_trivially_destructible_v<MoveOnly>);
+static_assert(std::is_trivially_destructible_v<InlineQueue<MoveOnly>>);
+static_assert(std::is_trivially_destructible_v<InlineQueue<MoveOnly, 1>>);
+
+static_assert(std::is_trivially_destructible_v<CopyOnly>);
+static_assert(std::is_trivially_destructible_v<InlineQueue<CopyOnly>>);
+static_assert(std::is_trivially_destructible_v<InlineQueue<CopyOnly, 99>>);
+
+static_assert(!std::is_trivially_destructible_v<Counter>);
+static_assert(!std::is_trivially_destructible_v<InlineQueue<Counter>>);
+static_assert(!std::is_trivially_destructible_v<InlineQueue<Counter, 99>>);
+
+// Tests that InlineQueue<T> does not have any extra padding.
+static_assert(sizeof(InlineQueue<uint8_t, 1>) ==
+ sizeof(InlineQueue<uint8_t>::size_type) * 4 +
+ std::max(sizeof(InlineQueue<uint8_t>::size_type),
+ sizeof(uint8_t)));
+static_assert(sizeof(InlineQueue<uint8_t, 2>) ==
+ sizeof(InlineQueue<uint8_t>::size_type) * 4 +
+ 2 * sizeof(uint8_t));
+static_assert(sizeof(InlineQueue<uint16_t, 1>) ==
+ sizeof(InlineQueue<uint16_t>::size_type) * 4 + sizeof(uint16_t));
+static_assert(sizeof(InlineQueue<uint32_t, 1>) ==
+ sizeof(InlineQueue<uint32_t>::size_type) * 4 + sizeof(uint32_t));
+static_assert(sizeof(InlineQueue<uint64_t, 1>) ==
+ sizeof(InlineQueue<uint64_t>::size_type) * 4 + sizeof(uint64_t));
+
+// Test that InlineQueue<T> is copy assignable
+static_assert(std::is_copy_assignable_v<InlineQueue<int>::iterator>);
+static_assert(std::is_copy_assignable_v<InlineQueue<int, 4>::iterator>);
+
+// Test that InlineQueue<T>::iterator can be converted to a const_iterator
+static_assert(std::is_convertible<InlineQueue<int>::iterator,
+ InlineQueue<int>::const_iterator>::value);
+static_assert(std::is_convertible<InlineQueue<int, 4>::iterator,
+ InlineQueue<int, 4>::const_iterator>::value);
+
+// Test that InlineQueue<T>::const_iterator can NOT be converted to a iterator
+static_assert(!std::is_convertible<InlineQueue<int>::const_iterator,
+ InlineQueue<int>::iterator>::value);
+static_assert(!std::is_convertible<InlineQueue<int, 4>::const_iterator,
+ InlineQueue<int, 4>::iterator>::value);
+
+} // namespace
+} // namespace pw::containers
diff --git a/pw_containers/intrusive_list.cc b/pw_containers/intrusive_list.cc
index c005b5036..e485de620 100644
--- a/pw_containers/intrusive_list.cc
+++ b/pw_containers/intrusive_list.cc
@@ -14,10 +14,32 @@
#include "pw_containers/intrusive_list.h"
+#include <utility>
+
#include "pw_assert/check.h"
namespace pw::intrusive_list_impl {
+List::Item::Item(Item&& other) {
+ // Ensure `next_` is valid on instantiation.
+ next_ = this;
+ *this = std::move(other);
+}
+
+List::Item& List::Item::operator=(List::Item&& other) {
+ // Remove `this` object from its current list.
+ if (!unlisted()) {
+ unlist();
+ }
+ // If `other` is listed, remove it from its list and put `this` in its place.
+ if (!other.unlisted()) {
+ List::Item* prev = other.previous();
+ other.unlist(prev);
+ List::insert_after(prev, *this);
+ }
+ return *this;
+}
+
void List::Item::unlist(Item* prev) {
if (prev == nullptr) {
prev = previous();
diff --git a/pw_containers/intrusive_list_test.cc b/pw_containers/intrusive_list_test.cc
index 060a56c91..cd5b95ec1 100644
--- a/pw_containers/intrusive_list_test.cc
+++ b/pw_containers/intrusive_list_test.cc
@@ -20,6 +20,7 @@
#include "gtest/gtest.h"
#include "pw_compilation_testing/negative_compilation.h"
+#include "pw_containers/vector.h"
#include "pw_preprocessor/util.h"
namespace pw {
@@ -376,7 +377,7 @@ TEST(IntrusiveList, IteratorIncrement) {
TestItem item_array[20];
IntrusiveList<TestItem> list;
for (size_t i = 0; i < PW_ARRAY_SIZE(item_array); ++i) {
- item_array[i].SetNumber(i);
+ item_array[i].SetNumber(static_cast<int>(i));
list.push_back(item_array[i]);
}
@@ -449,7 +450,7 @@ TEST(IntrusiveList, ConstIteratorModify) {
}
#endif // PW_NC_TEST
-// TODO(b/235289499): These tests should trigger a CHECK failure. This requires
+// TODO: b/235289499 - These tests should trigger a CHECK failure. This requires
// using a testing version of pw_assert.
#define TESTING_CHECK_FAILURES_IS_SUPPORTED 0
#if TESTING_CHECK_FAILURES_IS_SUPPORTED
@@ -704,5 +705,24 @@ struct NotAnItem {};
#endif // PW_NC_TEST
+TEST(IntrusiveList, MoveListedItems) {
+ TestItem item1(1);
+ TestItem item2(2);
+ TestItem item3(3);
+
+ IntrusiveList<TestItem> list = {&item1, &item2, &item3};
+
+ Vector<TestItem, 3> vector;
+ vector.emplace_back(std::move(item1));
+ vector.emplace_back(std::move(item2));
+ vector.emplace_back(std::move(item3));
+
+ auto iter = list.begin();
+ EXPECT_EQ((*iter++).GetNumber(), 1);
+ EXPECT_EQ((*iter++).GetNumber(), 2);
+ EXPECT_EQ((*iter++).GetNumber(), 3);
+ EXPECT_EQ(iter, list.end());
+}
+
} // namespace
} // namespace pw
diff --git a/pw_containers/public/pw_containers/filtered_view.h b/pw_containers/public/pw_containers/filtered_view.h
index b1b499176..df5203439 100644
--- a/pw_containers/public/pw_containers/filtered_view.h
+++ b/pw_containers/public/pw_containers/filtered_view.h
@@ -21,11 +21,25 @@
namespace pw::containers {
-// FilteredView supports iterating over only elements that match a filter in a
-// container. FilteredView works with any container with an incrementable
-// iterator. The back() function currently requires a bidirectional iterator.
-//
-// FilteredView is similar to C++20's std::filter_view.
+/// `pw::containers::FilteredView` provides a view of a container with only
+/// elements that match the specified filter. This class is similar to C++20's
+/// [std::ranges::filter_view](
+/// https://en.cppreference.com/w/cpp/ranges/filter_view>).
+///
+/// `FilteredView` works with any container with an incrementable iterator. The
+/// `back()` function currently requires a bidirectional iterator.
+///
+/// To create a `FilteredView`, pass a container and a filter predicate, which
+/// may be any callable type including a function pointer, lambda, or
+/// `pw::Function`.
+///
+/// @code{cpp}
+/// std::array<int, 99> kNumbers = {3, 1, 4, 1, ...};
+///
+/// for (int n : FilteredView(kNumbers, [](int v) { return v % 2 == 0; })) {
+/// PW_LOG_INFO("This number is even: %d", n);
+/// }
+/// @endcode
template <typename Container, typename Filter>
class FilteredView {
public:
@@ -97,8 +111,11 @@ class FilteredView {
using const_iterator = iterator;
- template <typename... FilterArgs>
- constexpr FilteredView(const Container& container, Filter&& filter)
+ constexpr explicit FilteredView(const Container& container,
+ const Filter& filter)
+ : container_(container), filter_(filter) {}
+
+ constexpr explicit FilteredView(const Container& container, Filter&& filter)
: container_(container), filter_(std::move(filter)) {}
constexpr FilteredView(const FilteredView&) = delete;
@@ -117,7 +134,9 @@ class FilteredView {
const auto& back() const { return *std::prev(end()); }
// The number of elements in the container that match the filter.
- size_t size() const { return std::distance(begin(), end()); }
+ size_t size() const {
+ return static_cast<size_t>(std::distance(begin(), end()));
+ }
bool empty() const { return begin() == end(); }
diff --git a/pw_containers/public/pw_containers/flat_map.h b/pw_containers/public/pw_containers/flat_map.h
index 7ce97cab0..171d84d2c 100644
--- a/pw_containers/public/pw_containers/flat_map.h
+++ b/pw_containers/public/pw_containers/flat_map.h
@@ -17,8 +17,11 @@
#include <array>
#include <cstddef>
#include <cstdint>
+#include <iterator>
#include <type_traits>
+#include "pw_assert/assert.h"
+
namespace pw::containers {
// Define and use a custom Pair object. This is because std::pair does not
@@ -28,6 +31,12 @@ template <typename First, typename Second>
struct Pair {
First first;
Second second;
+
+ bool operator==(const Pair& p) const {
+ return first == p.first && second == p.second;
+ }
+
+ bool operator!=(const Pair& p) const { return !(*this == p); }
};
template <typename T1, typename T2>
@@ -36,7 +45,7 @@ Pair(T1, T2) -> Pair<T1, T2>;
// A simple, fixed-size associative array with lookup by key or value.
//
// FlatMaps are initialized with a std::array of Pair<K, V> objects:
-// FlatMap<int, int> map({{{1, 2}, {3, 4}}});
+// FlatMap<char, int, 3> map({{{'A', 1}, {'B', 2}, {'C', 3}}});
//
// The keys do not need to be sorted as the constructor will sort the items
// if need be.
@@ -46,12 +55,76 @@ class FlatMap {
using key_type = Key;
using mapped_type = Value;
using value_type = Pair<key_type, mapped_type>;
+ using pointer = value_type*;
+ using reference = value_type&;
using size_type = size_t;
using difference_type = ptrdiff_t;
using container_type = typename std::array<value_type, kArraySize>;
using iterator = typename container_type::iterator;
using const_iterator = typename container_type::const_iterator;
+ /// A bidirectional iterator for the mapped values.
+ ///
+ /// Also an output iterator.
+ class mapped_iterator {
+ public:
+ using value_type = FlatMap::mapped_type;
+ using difference_type = std::ptrdiff_t;
+ using pointer = value_type*;
+ using reference = value_type&;
+ using iterator_category = std::bidirectional_iterator_tag;
+
+ constexpr mapped_iterator() = default;
+
+ constexpr mapped_iterator(const mapped_iterator& other) = default;
+ constexpr mapped_iterator& operator=(const mapped_iterator& other) =
+ default;
+
+ constexpr reference operator*() const {
+ reference value = current_->second;
+ return value;
+ }
+
+ constexpr pointer operator->() const { return &operator*(); }
+
+ constexpr mapped_iterator& operator++() {
+ ++current_;
+ return *this;
+ }
+
+ constexpr mapped_iterator operator++(int) {
+ mapped_iterator original = *this;
+ operator++();
+ return original;
+ }
+
+ constexpr mapped_iterator& operator--() {
+ --current_;
+ return *this;
+ }
+
+ constexpr mapped_iterator operator--(int) {
+ mapped_iterator original = *this;
+ operator--();
+ return original;
+ }
+
+ constexpr bool operator==(const mapped_iterator& other) const {
+ return current_ == other.current_;
+ }
+
+ constexpr bool operator!=(const mapped_iterator& other) const {
+ return !(*this == other);
+ }
+
+ private:
+ friend class FlatMap;
+
+ constexpr mapped_iterator(FlatMap::iterator current) : current_(current) {}
+
+ FlatMap::iterator current_{nullptr};
+ };
+
constexpr FlatMap(const std::array<value_type, kArraySize>& items)
: items_(items) {
ConstexprSort(items_.data(), kArraySize);
@@ -66,6 +139,40 @@ class FlatMap {
constexpr size_type max_size() const { return kArraySize; }
// Lookup.
+ /// Accesses a mutable mapped value.
+ ///
+ /// @pre The key must exist.
+ ///
+ /// @param[in] key The key to the mapped value.
+ ///
+ /// @returns A reference to the mapped value.
+ constexpr mapped_type& at(const key_type& key) {
+ PW_ASSERT(end() != begin());
+ iterator it = std::lower_bound(
+ items_.begin(), items_.end(), key, IsItemKeyLessThanGivenKey);
+ const key_type& found_key = it->first;
+ PW_ASSERT(it != items_.end() && found_key == key);
+ mapped_type& found_value = it->second;
+ return found_value;
+ }
+
+ /// Accesses a mapped value.
+ ///
+ /// @pre The key must exist.
+ ///
+ /// @param[in] key The key to the mapped value.
+ ///
+ /// @returns A const reference to the mapped value.
+ constexpr const mapped_type& at(const key_type& key) const {
+ PW_ASSERT(end() != begin());
+ const_iterator it = std::lower_bound(
+ items_.cbegin(), items_.cend(), key, IsItemKeyLessThanGivenKey);
+ const key_type& found_key = it->first;
+ PW_ASSERT(it != items_.cend() && found_key == key);
+ const mapped_type& found_value = it->second;
+ return found_value;
+ }
+
constexpr bool contains(const key_type& key) const {
return find(key) != end();
}
@@ -83,10 +190,7 @@ class FlatMap {
}
constexpr const_iterator lower_bound(const key_type& key) const {
- return std::lower_bound(
- begin(), end(), key, [](const value_type& item, key_type lkey) {
- return item.first < lkey;
- });
+ return std::lower_bound(begin(), end(), key, IsItemKeyLessThanGivenKey);
}
constexpr const_iterator upper_bound(const key_type& key) const {
@@ -111,6 +215,26 @@ class FlatMap {
constexpr const_iterator end() const { return cend(); }
constexpr const_iterator cend() const { return items_.cend(); }
+ /// Gets an iterator to the first mapped value.
+ ///
+ /// Mapped iterators iterate through the mapped values, and allow mutation of
+ /// those values (for a non-const FlatMap object).
+ ///
+ /// @returns An iterator to the first mapped value.
+ constexpr mapped_iterator mapped_begin() {
+ return mapped_iterator(items_.begin());
+ }
+
+ /// Gets an iterator to one past the last mapped value.
+ ///
+ /// Mapped iterators iterate through the mapped values, and allow mutation of
+ /// those values (for a non-const FlatMap object).
+ ///
+ /// @returns An iterator to one past the last mapped value.
+ constexpr mapped_iterator mapped_end() {
+ return mapped_iterator(items_.end());
+ }
+
private:
// Simple stable insertion sort function for constexpr support.
// std::stable_sort is not constexpr. Should not be a problem with performance
@@ -137,6 +261,12 @@ class FlatMap {
}
}
+ static constexpr bool IsItemKeyLessThanGivenKey(const value_type& item,
+ key_type key) {
+ const key_type& item_key = item.first;
+ return item_key < key;
+ }
+
std::array<value_type, kArraySize> items_;
};
diff --git a/pw_containers/public/pw_containers/inline_deque.h b/pw_containers/public/pw_containers/inline_deque.h
new file mode 100644
index 000000000..77add283d
--- /dev/null
+++ b/pw_containers/public/pw_containers/inline_deque.h
@@ -0,0 +1,661 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <initializer_list>
+#include <iterator>
+#include <limits>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#include "pw_assert/assert.h"
+#include "pw_containers/internal/raw_storage.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
+
+namespace pw {
+namespace inline_circular_buffer_impl {
+
+enum Constness : bool { kMutable = false, kConst = true };
+
+template <typename ValueType, typename SizeType, Constness kIsConst>
+class InlineDequeIterator;
+
+} // namespace inline_circular_buffer_impl
+
+template <typename T, typename SizeType, size_t kCapacity>
+class BasicInlineDeque;
+
+/// The `InlineDeque` class is similar to the STL's double ended queue
+/// (`std::deque`), except it is backed by a fixed-size buffer.
+/// `InlineDeque`'s must be declared with an explicit maximum size (e.g.
+/// `InlineDeque<int, 10>>`) but deques can be used and referred to without
+/// the max size template parameter (e.g. `InlineDeque<int>`).
+///
+/// To allow referring to a `pw::InlineDeque` without an explicit maximum
+/// size, all `InlineDeque` classes inherit from the generic
+/// `InlineDeque<T>`, which stores the maximum size in a variable. This allows
+/// InlineDeques to be used without having to know their maximum size at compile
+/// time. It also keeps code size small since function implementations are
+/// shared for all maximum sizes.
+template <typename T, size_t kCapacity = containers::internal::kGenericSized>
+using InlineDeque = BasicInlineDeque<T, uint16_t, kCapacity>;
+
+template <typename ValueType,
+ typename SizeType,
+ size_t kCapacity = containers::internal::kGenericSized>
+class BasicInlineDeque
+ : public BasicInlineDeque<ValueType,
+ SizeType,
+ containers::internal::kGenericSized> {
+ private:
+ using Base = BasicInlineDeque<ValueType,
+ SizeType,
+ containers::internal::kGenericSized>;
+
+ public:
+ using typename Base::const_iterator;
+ using typename Base::const_pointer;
+ using typename Base::const_reference;
+ using typename Base::difference_type;
+ using typename Base::iterator;
+ using typename Base::pointer;
+ using typename Base::reference;
+ using typename Base::size_type;
+ using typename Base::value_type;
+
+ // Constructors
+
+ constexpr BasicInlineDeque() noexcept : Base(kCapacity) {}
+
+ BasicInlineDeque(size_type count, const_reference value) : Base(kCapacity) {
+ Base::assign(count, value);
+ }
+
+ explicit BasicInlineDeque(size_type count)
+ : BasicInlineDeque(count, value_type()) {}
+
+ template <
+ typename InputIterator,
+ typename = containers::internal::EnableIfInputIterator<InputIterator>>
+ BasicInlineDeque(InputIterator start, InputIterator finish)
+ : Base(kCapacity) {
+ Base::assign(start, finish);
+ }
+
+ BasicInlineDeque(std::initializer_list<value_type> list)
+ : BasicInlineDeque() {
+ *this = list;
+ }
+
+ BasicInlineDeque(const BasicInlineDeque& other) : BasicInlineDeque() {
+ *this = other;
+ }
+
+ template <typename T, typename = containers::internal::EnableIfIterable<T>>
+ BasicInlineDeque(const T& other) : BasicInlineDeque() {
+ *this = other;
+ }
+
+ // Assignment
+ // Use the operators from the base class, but return the correct type of
+ // reference.
+
+ BasicInlineDeque& operator=(std::initializer_list<value_type> list) {
+ Base::operator=(list);
+ return *this;
+ }
+
+ BasicInlineDeque& operator=(const BasicInlineDeque& other) {
+ Base::operator=(other);
+ return *this;
+ }
+
+ template <typename T, typename = containers::internal::EnableIfIterable<T>>
+ BasicInlineDeque& operator=(const T& other) {
+ Base::operator=(other);
+ return *this;
+ }
+
+ // Size
+
+ static constexpr size_type max_size() { return capacity(); }
+ static constexpr size_type capacity() { return kCapacity; }
+
+ // All other methods are implemented on the generic-sized base class.
+
+ private:
+ friend class BasicInlineDeque<value_type,
+ size_type,
+ containers::internal::kGenericSized>;
+
+ static_assert(kCapacity <= std::numeric_limits<size_type>::max());
+
+ // The data() function is defined differently for the generic-sized and
+ // known-sized specializations. This data() implementation simply returns the
+ // RawStorage's data(). The generic-sized data() function casts *this to a
+ // known zero-sized specialization to access this exact function.
+ pointer data() { return raw_storage_.data(); }
+ const_pointer data() const { return raw_storage_.data(); }
+
+ // Note that this is offset and aligned the same for all possible
+ // kCapacity values for the same value_type.
+ containers::internal::RawStorage<value_type, kCapacity> raw_storage_;
+};
+
+// Defines the generic-sized BasicInlineDeque<T> specialization, which
+// serves as the base class for BasicInlineDeque<T> of any capacity.
+//
+// Except for constructors, all other methods should be implemented on this
+// generic-sized specialization.
+template <typename ValueType, typename SizeType>
+class BasicInlineDeque<ValueType, SizeType, containers::internal::kGenericSized>
+ : public containers::internal::DestructorHelper<
+ BasicInlineDeque<ValueType,
+ SizeType,
+ containers::internal::kGenericSized>,
+ std::is_trivially_destructible<ValueType>::value> {
+ public:
+ using value_type = ValueType;
+ using size_type = SizeType;
+ using difference_type = ptrdiff_t;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using pointer = value_type*;
+ using const_pointer = const value_type*;
+ using iterator = inline_circular_buffer_impl::InlineDequeIterator<
+ value_type,
+ size_type,
+ inline_circular_buffer_impl::Constness::kMutable>;
+ using const_iterator = inline_circular_buffer_impl::InlineDequeIterator<
+ value_type,
+ size_type,
+ inline_circular_buffer_impl::Constness::kConst>;
+
+ // Assignment
+
+ BasicInlineDeque& operator=(std::initializer_list<value_type> list) {
+ assign(list);
+ return *this;
+ }
+
+ BasicInlineDeque& operator=(const BasicInlineDeque& other) {
+ assign(other.begin(), other.end());
+ return *this;
+ }
+
+ template <typename T, typename = containers::internal::EnableIfIterable<T>>
+ BasicInlineDeque& operator=(const T& other) {
+ assign(other.begin(), other.end());
+ return *this;
+ }
+
+ void assign(size_type count, const value_type& value) {
+ clear();
+ Append(count, value);
+ }
+
+ template <
+ typename InputIterator,
+ typename = containers::internal::EnableIfInputIterator<InputIterator>>
+ void assign(InputIterator start, InputIterator finish) {
+ clear();
+ CopyFrom(start, finish);
+ }
+
+ void assign(std::initializer_list<value_type> list) {
+ assign(list.begin(), list.end());
+ }
+
+ // Access
+
+ reference at(size_type index) {
+ PW_ASSERT(index < size());
+ return data()[AbsoluteIndex(index)];
+ }
+ const_reference at(size_type index) const {
+ PW_ASSERT(index < size());
+ return data()[AbsoluteIndex(index)];
+ }
+
+ reference operator[](size_type index) {
+ PW_DASSERT(index < size());
+ return data()[AbsoluteIndex(index)];
+ }
+ const_reference operator[](size_type index) const {
+ PW_DASSERT(index < size());
+ return data()[AbsoluteIndex(index)];
+ }
+
+ reference front() {
+ PW_DASSERT(!empty());
+ return data()[head_];
+ }
+ const_reference front() const {
+ PW_DASSERT(!empty());
+ return data()[head_];
+ }
+
+ reference back() {
+ PW_DASSERT(!empty());
+ return data()[AbsoluteIndex(size() - 1)];
+ }
+ const_reference back() const {
+ PW_DASSERT(!empty());
+ return data()[AbsoluteIndex(size() - 1)];
+ }
+
+ // Provides access to the valid data in a contiguous form.
+ std::pair<span<const value_type>, span<const value_type>> contiguous_data()
+ const;
+ std::pair<span<value_type>, span<value_type>> contiguous_data() {
+ auto [first, second] =
+ static_cast<const BasicInlineDeque&>(*this).contiguous_data();
+ return std::make_pair(
+ span<value_type>(const_cast<pointer>(first.data()), first.size()),
+ span<value_type>(const_cast<pointer>(second.data()), second.size()));
+ }
+
+ // Iterate
+
+ iterator begin() noexcept {
+ if (empty()) {
+ return end();
+ }
+
+ return iterator(this, 0);
+ }
+ const_iterator begin() const noexcept { return cbegin(); }
+ const_iterator cbegin() const noexcept {
+ if (empty()) {
+ return cend();
+ }
+ return const_iterator(this, 0);
+ }
+
+ iterator end() noexcept {
+ return iterator(this, std::numeric_limits<size_type>::max());
+ }
+ const_iterator end() const noexcept { return cend(); }
+ const_iterator cend() const noexcept {
+ return const_iterator(this, std::numeric_limits<size_type>::max());
+ }
+
+ // Size
+
+ [[nodiscard]] bool empty() const noexcept { return size() == 0; }
+
+ [[nodiscard]] bool full() const noexcept { return size() == max_size(); }
+
+ // Returns the number of elements in the `InlineDeque`. Disable MSAN since it
+ // thinks `count_` is uninitialized in the destructor.
+ size_type size() const noexcept PW_NO_SANITIZE("memory") { return count_; }
+
+ size_type max_size() const noexcept { return capacity(); }
+
+ // Returns the maximum number of elements in the `InlineDeque`. Disable MSAN
+ // since it thinks `capacity_` is uninitialized in the destructor.
+ size_type capacity() const noexcept PW_NO_SANITIZE("memory") {
+ return capacity_;
+ }
+
+ // Modify
+
+ void clear() noexcept;
+
+ void push_back(const value_type& value) { emplace_back(value); }
+
+ void push_back(value_type&& value) { emplace_back(std::move(value)); }
+
+ template <typename... Args>
+ void emplace_back(Args&&... args);
+
+ void pop_back();
+
+ void push_front(const value_type& value) { emplace_front(value); }
+
+ void push_front(value_type&& value) { emplace_front(std::move(value)); }
+
+ template <typename... Args>
+ void emplace_front(Args&&... args);
+
+ void pop_front();
+
+ void resize(size_type new_size) { resize(new_size, value_type()); }
+
+ void resize(size_type new_size, const value_type& value);
+
+ protected:
+ constexpr BasicInlineDeque(size_type capacity) noexcept
+ : capacity_(capacity), head_(0), tail_(0), count_(0) {}
+
+ private:
+ friend class inline_circular_buffer_impl::InlineDequeIterator<
+ ValueType,
+ SizeType,
+ inline_circular_buffer_impl::Constness::kMutable>;
+ friend class inline_circular_buffer_impl::InlineDequeIterator<
+ ValueType,
+ SizeType,
+ inline_circular_buffer_impl::Constness::kConst>;
+
+ // The underlying RawStorage is not part of the generic-sized class. It is
+ // provided in the derived class from which this instance was constructed. To
+ // access the data, down-cast this to a known max size specialization, and
+ // return the RawStorage's data, which is the same for all sizes.
+ pointer data() {
+ return static_cast<BasicInlineDeque<value_type, size_type, 0>*>(this)
+ ->data();
+ }
+ const_pointer data() const {
+ return static_cast<const BasicInlineDeque<value_type, size_type, 0>*>(this)
+ ->data();
+ }
+
+ void IncrementWithWrap(size_type& index) const {
+ index++;
+ // Note: branch is faster than mod (%) on common embedded
+ // architectures.
+ if (index == max_size()) {
+ index = 0;
+ }
+ }
+
+ void DecrementWithWrap(size_type& index) const {
+ if (index == 0) {
+ index = max_size();
+ }
+ index--;
+ }
+
+ // Returns the absolute index based on the relative index beyond the
+ // head offset.
+ //
+ // Precondition: The relative index must be valid, i.e. < size().
+ //
+ // Disable MSAN since it thinks `head_` is uninitialized in the destructor.
+ size_type AbsoluteIndex(const size_type relative_index) const
+ PW_NO_SANITIZE("memory") {
+ const size_type absolute_index = head_ + relative_index;
+ if (absolute_index < max_size()) {
+ return absolute_index;
+ }
+ // Offset wrapped across the end of the circular buffer.
+ return absolute_index - max_size();
+ }
+
+ template <typename Iterator>
+ void CopyFrom(Iterator start, Iterator finish);
+
+ void Append(size_type count, const value_type& value);
+
+ const size_type capacity_;
+ size_type head_; // Non-inclusive offset for the front.
+ size_type tail_; // Non-inclusive offset for the back.
+ size_type count_;
+};
+
+// Function implementations
+
+template <typename ValueType, typename SizeType>
+std::pair<span<const ValueType>, span<const ValueType>>
+BasicInlineDeque<ValueType, SizeType>::contiguous_data() const {
+ if (empty()) {
+ return std::make_pair(span<const value_type>(), span<const value_type>());
+ }
+ if (tail_ > head_) {
+ // If the newest entry is after the oldest entry, we have not wrapped:
+ // [ |head_|...more_entries...|tail_| ]
+ return std::make_pair(span<const value_type>(&data()[head_], size()),
+ span<const value_type>());
+ } else {
+ // If the newest entry is before or at the oldest entry and we know we are
+ // not empty, ergo we have wrapped:
+ // [..more_entries...|tail_| |head_|...more_entries...]
+ return std::make_pair(
+ span<const value_type>(&data()[head_], max_size() - head_),
+ span<const value_type>(&data()[0], tail_));
+ }
+}
+
+template <typename ValueType, typename SizeType>
+void BasicInlineDeque<ValueType, SizeType>::clear() noexcept {
+ if constexpr (!std::is_trivially_destructible_v<value_type>) {
+ for (auto& item : *this) {
+ item.~value_type();
+ }
+ }
+ head_ = 0;
+ tail_ = 0;
+ count_ = 0;
+}
+
+template <typename ValueType, typename SizeType>
+template <typename... Args>
+void BasicInlineDeque<ValueType, SizeType>::emplace_back(Args&&... args) {
+ PW_ASSERT(!full());
+ PW_DASSERT(tail_ < capacity());
+ new (&data()[tail_]) value_type(std::forward<Args>(args)...);
+ IncrementWithWrap(tail_);
+ ++count_;
+}
+
+template <typename ValueType, typename SizeType>
+void BasicInlineDeque<ValueType, SizeType>::pop_back() {
+ PW_ASSERT(!empty());
+ PW_DASSERT(tail_ < capacity());
+ if constexpr (!std::is_trivially_destructible_v<value_type>) {
+ back().~value_type();
+ }
+ DecrementWithWrap(tail_);
+ --count_;
+}
+
+template <typename ValueType, typename SizeType>
+template <typename... Args>
+void BasicInlineDeque<ValueType, SizeType>::emplace_front(Args&&... args) {
+ PW_ASSERT(!full());
+ DecrementWithWrap(head_);
+ PW_DASSERT(head_ < capacity());
+ new (&data()[head_]) value_type(std::forward<Args>(args)...);
+ ++count_;
+}
+
+template <typename ValueType, typename SizeType>
+void BasicInlineDeque<ValueType, SizeType>::pop_front() {
+ PW_ASSERT(!empty());
+ PW_DASSERT(head_ < capacity());
+ if constexpr (!std::is_trivially_destructible_v<value_type>) {
+ front().~value_type();
+ }
+ IncrementWithWrap(head_);
+ --count_;
+}
+
+template <typename ValueType, typename SizeType>
+void BasicInlineDeque<ValueType, SizeType>::resize(size_type new_size,
+ const value_type& value) {
+ if (size() < new_size) {
+ Append(std::min(max_size(), new_size) - size(), value);
+ } else {
+ while (size() > new_size) {
+ pop_back();
+ }
+ }
+}
+
+template <typename ValueType, typename SizeType>
+template <typename Iterator>
+void BasicInlineDeque<ValueType, SizeType>::CopyFrom(Iterator start,
+ Iterator finish) {
+ while (start != finish) {
+ push_back(*start++);
+ }
+}
+
+template <typename ValueType, typename SizeType>
+void BasicInlineDeque<ValueType, SizeType>::Append(size_type count,
+ const value_type& value) {
+ for (size_type i = 0; i < count; ++i) {
+ push_back(value);
+ }
+}
+
+namespace inline_circular_buffer_impl {
+
+// InlineDequeIterator meets the named requirements for
+// LegacyRandomAccessIterator
+template <typename ValueType, typename SizeType, Constness kIsConst>
+class InlineDequeIterator {
+ public:
+ using container_type =
+ typename std::conditional<kIsConst,
+ const BasicInlineDeque<ValueType, SizeType>,
+ BasicInlineDeque<ValueType, SizeType>>::type;
+ using value_type = ValueType;
+ using size_type = SizeType;
+ typedef typename std::conditional<kIsConst,
+ typename container_type::const_reference,
+ typename container_type::reference>::type
+ reference;
+ typedef
+ typename std::conditional<kIsConst,
+ typename container_type::const_pointer,
+ typename container_type::pointer>::type pointer;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::forward_iterator_tag;
+
+ constexpr InlineDequeIterator() = default;
+ constexpr InlineDequeIterator(container_type* container, size_type pos)
+ : container_(container), pos_(pos) {}
+
+ constexpr InlineDequeIterator(const InlineDequeIterator& other) = default;
+ constexpr InlineDequeIterator& operator=(const InlineDequeIterator& other) =
+ default;
+
+ operator InlineDequeIterator<ValueType, SizeType, Constness::kConst>() const {
+ return {container_, pos_};
+ }
+
+ constexpr InlineDequeIterator& Incr(difference_type n) {
+ const difference_type new_pos =
+ n + (pos_ == kEnd ? container_->size() : pos_);
+
+ PW_DASSERT(new_pos >= 0);
+ PW_DASSERT(new_pos <= container_->size());
+
+ pos_ =
+ new_pos == container_->size() ? kEnd : static_cast<size_type>(new_pos);
+
+ return *this;
+ }
+
+ constexpr InlineDequeIterator& operator+=(difference_type n) {
+ return Incr(n);
+ }
+ constexpr InlineDequeIterator& operator-=(difference_type n) {
+ return Incr(-n);
+ }
+ constexpr InlineDequeIterator& operator++() { return Incr(1); }
+ constexpr InlineDequeIterator operator++(int) {
+ InlineDequeIterator it(*this);
+ operator++();
+ return it;
+ }
+
+ constexpr InlineDequeIterator& operator--() { return Incr(-1); }
+ constexpr InlineDequeIterator operator--(int) {
+ InlineDequeIterator it = *this;
+ operator--();
+ return it;
+ }
+
+ constexpr friend InlineDequeIterator operator+(InlineDequeIterator it,
+ difference_type n) {
+ return it += n;
+ }
+ constexpr friend InlineDequeIterator operator+(difference_type n,
+ InlineDequeIterator it) {
+ return it += n;
+ }
+
+ constexpr friend InlineDequeIterator operator-(InlineDequeIterator it,
+ difference_type n) {
+ return it -= n;
+ }
+
+ constexpr friend difference_type operator-(const InlineDequeIterator& a,
+ const InlineDequeIterator& b) {
+ return static_cast<difference_type>(a.pos_ == kEnd ? a.container_->size()
+ : a.pos_) -
+ static_cast<difference_type>(b.pos_ == kEnd ? b.container_->size()
+ : b.pos_);
+ }
+
+ constexpr reference operator*() const {
+ PW_DASSERT(pos_ != kEnd);
+ PW_DASSERT(pos_ < container_->size());
+
+ return container_->at(pos_);
+ }
+
+ constexpr pointer operator->() const {
+ PW_DASSERT(pos_ != kEnd);
+ PW_DASSERT(pos_ < container_->size());
+
+ return &**this;
+ }
+
+ constexpr reference operator[](size_type n) { return *(*this + n); }
+
+ constexpr bool operator==(const InlineDequeIterator& other) const {
+ return container_ == other.container_ && pos_ == other.pos_;
+ }
+
+ constexpr bool operator!=(const InlineDequeIterator& other) const {
+ return !(*this == other);
+ }
+
+ constexpr friend bool operator<(InlineDequeIterator a,
+ InlineDequeIterator b) {
+ return b - a > 0;
+ }
+
+ constexpr friend bool operator>(InlineDequeIterator a,
+ InlineDequeIterator b) {
+ return b < a;
+ }
+
+ constexpr friend bool operator<=(InlineDequeIterator a,
+ InlineDequeIterator b) {
+ return !(a > b);
+ }
+
+ constexpr friend bool operator>=(InlineDequeIterator a,
+ InlineDequeIterator b) {
+ return !(a < b);
+ }
+
+ private:
+ static constexpr size_type kEnd = std::numeric_limits<size_type>::max();
+ container_type* container_; // pointer to container this iterator is from
+ size_type pos_; // logical index of iterator
+};
+
+} // namespace inline_circular_buffer_impl
+} // namespace pw
diff --git a/pw_containers/public/pw_containers/inline_queue.h b/pw_containers/public/pw_containers/inline_queue.h
new file mode 100644
index 000000000..8c43a787f
--- /dev/null
+++ b/pw_containers/public/pw_containers/inline_queue.h
@@ -0,0 +1,241 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <initializer_list>
+#include <utility>
+
+#include "pw_containers/inline_deque.h"
+
+namespace pw {
+
+template <typename T, typename SizeType, size_t kCapacity>
+class BasicInlineQueue;
+
+/// The `InlineQueue` class is similar to `std::queue<T, std::deque>`, except
+/// it is backed by a fixed-size buffer. `InlineQueue`'s must be declared with
+/// an explicit maximum size (e.g. `InlineQueue<int, 10>>`) but deques can be
+/// used and referred to without the max size template parameter (e.g.
+/// `InlineQueue<int>`).
+///
+/// `pw::InlineQueue` is wrapper around `pw::InlineDeque` with a simplified
+/// API and `push_overwrite()` & `emplace_overwrite()` helpers.
+template <typename T, size_t kCapacity = containers::internal::kGenericSized>
+using InlineQueue = BasicInlineQueue<T, uint16_t, kCapacity>;
+
+template <typename ValueType,
+ typename SizeType,
+ size_t kCapacity = containers::internal::kGenericSized>
+class BasicInlineQueue
+ : public BasicInlineQueue<ValueType,
+ SizeType,
+ containers::internal::kGenericSized> {
+ private:
+ using QueueBase = BasicInlineQueue<ValueType,
+ SizeType,
+ containers::internal::kGenericSized>;
+
+ public:
+ using typename QueueBase::const_iterator;
+ using typename QueueBase::const_pointer;
+ using typename QueueBase::const_reference;
+ using typename QueueBase::difference_type;
+ using typename QueueBase::iterator;
+ using typename QueueBase::pointer;
+ using typename QueueBase::reference;
+ using typename QueueBase::size_type;
+ using typename QueueBase::value_type;
+
+ // Constructors
+
+ constexpr BasicInlineQueue() noexcept : deque_() {}
+
+ BasicInlineQueue(size_type count, const_reference value)
+ : deque_(count, value) {}
+
+ explicit BasicInlineQueue(size_type count) : deque_(count) {}
+
+ template <
+ typename InputIterator,
+ typename = containers::internal::EnableIfInputIterator<InputIterator>>
+ BasicInlineQueue(InputIterator start, InputIterator finish)
+ : deque_(start, finish) {}
+
+ BasicInlineQueue(std::initializer_list<value_type> list) { *this = list; }
+
+ BasicInlineQueue(const BasicInlineQueue& other) { *this = other; }
+
+ template <typename T, typename = containers::internal::EnableIfIterable<T>>
+ BasicInlineQueue(const T& other) {
+ *this = other;
+ }
+
+ // Assignment
+
+ BasicInlineQueue& operator=(std::initializer_list<value_type> list) {
+ deque_ = std::move(list);
+ return *this;
+ }
+
+ BasicInlineQueue& operator=(const BasicInlineQueue& other) {
+ deque_ = other.deque_;
+ return *this;
+ }
+
+ template <typename T, typename = containers::internal::EnableIfIterable<T>>
+ BasicInlineQueue& operator=(const T& other) {
+ deque_ = BasicInlineDeque<ValueType, SizeType, kCapacity>(other.begin(),
+ other.end());
+ return *this;
+ }
+
+ // Size
+
+ static constexpr size_type max_size() { return capacity(); }
+ static constexpr size_type capacity() { return kCapacity; }
+
+ private:
+ friend class BasicInlineQueue<value_type,
+ size_type,
+ containers::internal::kGenericSized>;
+
+ // The deque() function is defined differently for the generic-sized and
+ // known-sized specializations. This data() implementation simply returns the
+ // generic-sized deque_. The generic-sized deque() function casts *this to a
+ // known zero-sized specialzation to access this exact function.
+ BasicInlineDeque<ValueType, SizeType>& deque() { return deque_; }
+ const BasicInlineDeque<ValueType, SizeType>& deque() const { return deque_; }
+
+ BasicInlineDeque<ValueType, SizeType, kCapacity> deque_;
+};
+
+// Defines the generic-sized BasicInlineDeque<T> specialization, which
+// serves as the base class for BasicInlineDeque<T> of any capacity.
+//
+// Except for constructors, all other methods should be implemented on this
+// generic-sized specialization.
+template <typename ValueType, typename SizeType>
+class BasicInlineQueue<ValueType, SizeType, containers::internal::kGenericSized>
+ : public containers::internal::DestructorHelper<
+ BasicInlineQueue<ValueType,
+ SizeType,
+ containers::internal::kGenericSized>,
+ std::is_trivially_destructible<ValueType>::value> {
+ private:
+ using Deque = BasicInlineDeque<ValueType, SizeType>;
+
+ public:
+ using const_iterator = typename Deque::const_iterator;
+ using const_pointer = typename Deque::const_pointer;
+ using const_reference = typename Deque::const_reference;
+ using difference_type = typename Deque::difference_type;
+ using iterator = typename Deque::iterator;
+ using pointer = typename Deque::pointer;
+ using reference = typename Deque::reference;
+ using size_type = typename Deque::size_type;
+ using value_type = typename Deque::value_type;
+
+ // Access
+
+ reference at(size_type index) { return deque().at(index); }
+ const_reference at(size_type index) const { return deque().at(index); }
+
+ reference operator[](size_type index) { return deque()[index]; }
+ const_reference operator[](size_type index) const { return deque()[index]; }
+
+ reference front() { return deque().front(); }
+ const_reference front() const { return deque().front(); }
+
+ reference back() { return deque().back(); }
+ const_reference back() const { return deque().back(); }
+
+ std::pair<span<const value_type>, span<const value_type>> contiguous_data()
+ const {
+ return deque().contiguous_data();
+ }
+ std::pair<span<value_type>, span<value_type>> contiguous_data() {
+ return deque().contiguous_data();
+ }
+
+ // Iterate
+
+ iterator begin() noexcept { return deque().begin(); }
+ const_iterator begin() const noexcept { return cbegin(); }
+ const_iterator cbegin() const noexcept { return deque().cbegin(); }
+
+ iterator end() noexcept { return deque().end(); }
+ const_iterator end() const noexcept { return cend(); }
+ const_iterator cend() const noexcept { return deque().cend(); }
+
+ // Size
+
+ [[nodiscard]] bool empty() const noexcept { return deque().empty(); }
+
+ [[nodiscard]] bool full() const noexcept { return deque().full(); }
+
+ size_type size() const noexcept { return deque().size(); }
+
+ size_type max_size() const noexcept { return capacity(); }
+
+ size_type capacity() const noexcept { return deque().capacity(); }
+
+ // Modify
+
+ void clear() noexcept { deque().clear(); }
+
+ void push(const value_type& value) { emplace(value); }
+
+ void push(value_type&& value) { emplace(std::move(value)); }
+
+ template <typename... Args>
+ void emplace(Args&&... args) {
+ deque().emplace_back(std::forward<Args>(args)...);
+ }
+
+ void push_overwrite(const value_type& value) { emplace_overwrite(value); }
+
+ void push_overwrite(value_type&& value) {
+ emplace_overwrite(std::move(value));
+ }
+
+ template <typename... Args>
+ void emplace_overwrite(Args&&... args) {
+ if (full()) {
+ pop();
+ }
+ emplace(std::forward<Args>(args)...);
+ }
+
+ void pop() { deque().pop_front(); }
+
+ protected:
+ constexpr BasicInlineQueue() noexcept = default;
+
+ private:
+ // The underlying BasicInlineDeque is not part of the generic-sized class. It
+ // is provided in the derived class from which this instance was constructed.
+ // To access the data, down-cast this to a known max size specialization, and
+ // return a reference to a generic-sized BasicInlineDeque, which is the same
+ // reference for all sizes.
+ BasicInlineDeque<ValueType, SizeType>& deque() {
+ return static_cast<BasicInlineQueue<value_type, size_type, 0>*>(this)
+ ->deque();
+ }
+ const BasicInlineDeque<ValueType, SizeType>& deque() const {
+ return static_cast<const BasicInlineQueue<value_type, size_type, 0>*>(this)
+ ->deque();
+ }
+};
+
+} // namespace pw
diff --git a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
index c77a2ca66..c10ff1d88 100644
--- a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
+++ b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
@@ -78,26 +78,69 @@ class Iterator {
class List {
public:
class Item {
+ public:
+ /// Items are not copyable.
+ Item(const Item&) = delete;
+
+ /// Items are not copyable.
+ Item& operator=(const Item&) = delete;
+
protected:
+ /// Default constructor.
+ ///
+ /// All other constructors must call this to preserve the invariant that an
+ /// item is always part of a cycle.
constexpr Item() : Item(this) {}
+ /// Destructor.
+ ///
+ /// This is O(n), where "n" is the number of items in this object's list.
~Item() { unlist(); }
+ /// Move constructor.
+ ///
+ /// This uses the move assignment operator. See the note on that method.
+ ///
+ /// This should NOT typically be used, except for testing.
+ Item(Item&& other);
+
+ /// Move assignment operators.
+ ///
+ /// This will unlist the current object, and replace other with this object
+ /// in the list that contained it. As a result, this O(m + n), where "m" is
+ /// the number of items in this object's list, and "n" is the number of
+ /// objects in the `other` object's list.
+ ///
+ /// As such, it should NOT typically be used, except for testing.
+ Item& operator=(Item&& other);
+
+ /// Returns whether this object is not part of a list.
+ ///
+ /// This is O(1) whether the object is in a list or not.
+ bool unlisted() const { return this == next_; }
+
+ /// Unlink this from the list it is apart of, if any.
+ ///
+ /// Specifying prev saves calling previous(), which requires looping around
+ /// the cycle. This is O(1) with `previous`, and O(n) without.
+ void unlist(Item* prev = nullptr);
+
private:
friend class List;
template <typename T, typename I>
friend class Iterator;
- constexpr Item(Item* next) : next_(next) {}
-
- bool unlisted() const { return this == next_; }
-
- // Unlink this from the list it is apart of, if any. Specifying prev saves
- // calling previous(), which requires looping around the cycle.
- void unlist(Item* prev = nullptr);
+ /// Explicit constructor.
+ ///
+ /// In addition to the default constructor for Item, this is used by the
+ /// default constructor for List to create its sentinel value.
+ explicit constexpr Item(Item* next) : next_(next) {}
- Item* previous(); // Note: O(n) since it loops around the cycle.
+ /// Return the previous item in the list by looping around the cycle.
+ ///
+ /// This is O(n), where "n" is the number of items in this object's list.
+ Item* previous();
// The next pointer. Unlisted items must be self-cycles (next_ == this).
Item* next_;
@@ -122,28 +165,51 @@ class List {
bool empty() const noexcept { return begin() == end(); }
+ /// Inserts an item into a list.
+ ///
+ /// The item given by `pos` is updated to point to the item as being next in
+ /// the list, while the item itself points to what `pos` previously pointed to
+ /// as next.
+ ///
+ /// This is O(1). The ownership of the item is not changed.
static void insert_after(Item* pos, Item& item);
+ /// Removes an item from a list.
+ ///
+ /// The item that `pos` points to as being next in the list is unlisted, while
+ /// `pos` is updated to point to the item following it as next.
+ ///
+ /// This is O(1). The item is not destroyed..
static void erase_after(Item* pos);
void clear();
bool remove(const Item& item_to_remove);
+ /// Returns a pointer to the sentinel item.
constexpr Item* before_begin() noexcept { return &head_; }
constexpr const Item* before_begin() const noexcept { return &head_; }
+ /// Returns a pointer to the first item.
constexpr Item* begin() noexcept { return head_.next_; }
constexpr const Item* begin() const noexcept { return head_.next_; }
+ /// Returns a pointer to the last item.
Item* before_end() noexcept;
+ /// Returns a pointer to the sentinel item.
constexpr Item* end() noexcept { return &head_; }
constexpr const Item* end() const noexcept { return &head_; }
+ /// Returns the number of items in the list by looping around the cycle.
+ ///
+ /// This is O(n), where "n" is the number of items in the list.
size_t size() const;
private:
+ /// Adds items to the list from the provided range.
+ ///
+ /// This is O(n), where "n" is the number of items in the range.
template <typename Iterator>
void AssignFromIterator(Iterator first, Iterator last);
diff --git a/pw_containers/public/pw_containers/internal/raw_storage.h b/pw_containers/public/pw_containers/internal/raw_storage.h
new file mode 100644
index 000000000..8ec98078a
--- /dev/null
+++ b/pw_containers/public/pw_containers/internal/raw_storage.h
@@ -0,0 +1,110 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <new>
+#include <type_traits>
+
+namespace pw::containers::internal {
+
+// Used as max_size in generic-sized interfaces using RawStorage<T>.
+inline constexpr size_t kGenericSized = static_cast<size_t>(-1);
+
+template <typename InputIterator>
+using EnableIfInputIterator = std::enable_if_t<std::is_convertible<
+ typename std::iterator_traits<InputIterator>::iterator_category,
+ std::input_iterator_tag>::value>;
+
+template <typename T>
+using EnableIfIterable =
+ std::enable_if_t<true, decltype(T().begin(), T().end())>;
+
+// The DestructorHelper is used to make a Container<T> trivially destructible if
+// T is. This could be replaced with a C++20 constraint.
+template <typename Container, bool kIsTriviallyDestructible>
+class DestructorHelper;
+
+template <typename Container>
+class DestructorHelper<Container, true> {
+ public:
+ ~DestructorHelper() = default;
+};
+
+template <typename Container>
+class DestructorHelper<Container, false> {
+ public:
+ ~DestructorHelper() { static_cast<Container*>(this)->clear(); }
+};
+
+// Container similar to std::array that provides an array of Elements which are
+// stored as uninitialized memory blocks aligned correctly for the type.
+//
+// The caller is responsible for constructing, accessing, and destructing
+// elements. In addition, the caller is responsible for element access and all
+// associated bounds checking.
+template <typename ValueType, size_t kCapacity>
+class RawStorage {
+ public:
+ using value_type = ValueType;
+ using size_type = size_t;
+ using difference_type = ptrdiff_t;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using pointer = value_type*;
+ using const_pointer = const value_type*;
+
+ // Construct
+ constexpr RawStorage() noexcept : null_bits_() {}
+
+ // Do not permit copying and move for now.
+ RawStorage(const RawStorage&) = delete;
+ RawStorage& operator=(const RawStorage&) = delete;
+ RawStorage(RawStorage&&) = delete;
+ RawStorage&& operator=(RawStorage&&) = delete;
+
+ pointer data() noexcept {
+ return std::launder(reinterpret_cast<pointer>(&bytes_));
+ }
+ const_pointer data() const noexcept {
+ return std::launder(reinterpret_cast<const_pointer>(&bytes_));
+ }
+
+ constexpr size_type size() const noexcept { return max_size(); }
+ constexpr size_type max_size() const noexcept { return kCapacity; }
+
+ private:
+ struct Empty {};
+
+ union {
+ // Elements must initialized on demand with placement new, this array
+ // provides the right amount of storage needed for the elements, however it
+ // is never constructed nor destructed by the RawStorage container, as
+ // null_bits_ is used instead.
+ //
+ // This uses std::array instead of a C array to support zero-length arrays.
+ // Zero-length C arrays are non-standard, but std::array<T, 0> is valid.
+ // The alignas specifier is required ensure that a zero-length array is
+ // aligned the same as an array with elements.
+ alignas(value_type)
+ std::array<std::byte, sizeof(value_type) * kCapacity> bytes_;
+
+ // Empty struct used when initializing the storage in the constexpr
+ // constructor.
+ Empty null_bits_;
+ };
+};
+
+} // namespace pw::containers::internal
diff --git a/pw_containers/public/pw_containers/intrusive_list.h b/pw_containers/public/pw_containers/intrusive_list.h
index 6bd486084..f7f3817d3 100644
--- a/pw_containers/public/pw_containers/intrusive_list.h
+++ b/pw_containers/public/pw_containers/intrusive_list.h
@@ -53,10 +53,6 @@ template <typename T>
class IntrusiveList {
public:
class Item : public intrusive_list_impl::List::Item {
- public:
- Item(const Item&) = delete;
- Item& operator=(const Item&) = delete;
-
protected:
constexpr Item() = default;
diff --git a/pw_containers/public/pw_containers/variable_length_entry_queue.h b/pw_containers/public/pw_containers/variable_length_entry_queue.h
new file mode 100644
index 000000000..53ccb766f
--- /dev/null
+++ b/pw_containers/public/pw_containers/variable_length_entry_queue.h
@@ -0,0 +1,330 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "pw_preprocessor/util.h"
+#include "pw_varint/varint.h"
+
+/// @file pw_containers/variable_length_entry_queue.h
+///
+/// A `VariableLengthEntryQueue` is a ended queue of inline variable-length
+/// binary entries. It is implemented as a ring (circular) buffer and supports
+/// operations to append entries and overwrite if necessary. Entries may be zero
+/// bytes up to the maximum size supported by the queue.
+///
+/// The `VariableLengthEntryQueue` has a few interesting properties.
+///
+/// - Data and metadata are stored inline in a contiguous block of
+/// `uint32_t`-aligned memory.
+/// - The data structure is trivially copyable.
+/// - All state changes are accomplished with a single update to a `uint32_t`.
+/// The memory is always in a valid state and may be parsed offline.
+///
+/// This data structure is a much simpler version of
+/// @cpp_class{pw::ring_buffer::PrefixedEntryRingBuffer}. Prefer this
+/// sized-entry ring buffer to `PrefixedEntryRingBuffer` when:
+/// - A simple ring buffer of variable-length entries is needed. Advanced
+/// features like multiple readers and a user-defined preamble are not
+/// required.
+/// - C support is required.
+/// - A consistent, parsable, in-memory representation is required (e.g. to
+/// decode the buffer from a block of memory).
+///
+/// A `VariableLengthEntryQueue` may be declared and initialized in C with the
+/// @c_macro{PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE} macro.
+///
+/// @code{c}
+///
+/// // Declares a queue with a maximum single entry size of 10 bytes.
+/// PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(queue, 10);
+///
+/// // Write some data
+/// pw_VariableLengthEntryQueue_PushOverwrite(queue, "123", 3);
+/// pw_VariableLengthEntryQueue_PushOverwrite(queue, "456", 3);
+///
+/// assert(pw_VariableLengthEntryQueue_Size(queue) == 2u);
+///
+/// // Remove the entries
+/// pw_VariableLengthEntryQueue_Pop(queue);
+/// pw_VariableLengthEntryQueue_Pop(queue);
+///
+/// @endcode
+///
+/// Alternately, a `VariableLengthEntryQueue` may also be initialized in an
+/// existing ``uint32_t`` array.
+///
+/// @code{c}
+///
+/// // Initialize a VariableLengthEntryQueue.
+/// uint32_t buffer[32];
+/// pw_VariableLengthEntryQueue_Init(buffer, 32);
+///
+/// // Largest supported entry works out to 114 B (13 B overhead + 1 B prefix)
+/// assert(pw_VariableLengthEntryQueue_MaxEntrySizeBytes(buffer) == 114u);
+///
+/// // Write some data
+/// pw_VariableLengthEntryQueue_PushOverwrite(buffer, "123", 3);
+///
+/// @endcode
+
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+/// @defgroup variable_length_entry_queue_c_api VariableLengthEntryQueue C API
+/// @{
+
+/// Handle that refers to a `VariableLengthEntryQueue`. In memory, the queue
+/// is a `uint32_t` array.
+typedef uint32_t* pw_VariableLengthEntryQueue_Handle;
+typedef const uint32_t* pw_VariableLengthEntryQueue_ConstHandle;
+
+/// Declares and initializes a `VariableLengthEntryQueue` that can hold up to
+/// `max_size_bytes` bytes. `max_size_bytes` is the largest supported size for a
+/// single entry; attempting to store larger entries is invalid and will fail an
+/// assertion.
+///
+/// @param variable variable name for the queue
+/// @param max_size_bytes the capacity of the queue
+#define PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(variable, max_size_bytes) \
+ uint32_t variable[PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32 + \
+ _PW_VAR_QUEUE_DATA_SIZE_UINT32(max_size_bytes)] = { \
+ _PW_VAR_QUEUE_DATA_SIZE_BYTES(max_size_bytes), /*head=*/0u, /*tail=*/0u}
+
+/// The size of the `VariableLengthEntryQueue` header, in `uint32_t` elements.
+/// This header stores the buffer length and head and tail offsets.
+///
+/// The underlying `uint32_t` array of a `VariableLengthEntryQueue` must be
+/// larger than this size.
+#define PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32 (3)
+
+/// Initializes a `VariableLengthEntryQueue` in place in a `uint32_t` array. The
+/// array MUST be larger than
+/// @c_macro{PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32} (3) elements.
+static inline void pw_VariableLengthEntryQueue_Init(uint32_t array[],
+ size_t array_size_uint32);
+
+/// Empties the queue.
+static inline void pw_VariableLengthEntryQueue_Clear(
+ pw_VariableLengthEntryQueue_Handle queue);
+
+/// Appends an entry to the end of the queue.
+///
+/// @pre The entry MUST NOT be larger than `max_size_bytes()`.
+void pw_VariableLengthEntryQueue_Push(pw_VariableLengthEntryQueue_Handle queue,
+ const void* data,
+ uint32_t data_size_bytes);
+
+/// Appends an entry to the end of the queue, removing entries with `Pop`
+/// as necessary to make room.
+///
+/// @pre The entry MUST NOT be larger than `max_size_bytes()`.
+void pw_VariableLengthEntryQueue_PushOverwrite(
+ pw_VariableLengthEntryQueue_Handle queue,
+ const void* data,
+ uint32_t data_size_bytes);
+
+/// Removes the first entry from queue.
+///
+/// @pre The queue MUST have at least one entry.
+void pw_VariableLengthEntryQueue_Pop(pw_VariableLengthEntryQueue_Handle queue);
+
+/// Iterator object for a `VariableLengthEntryQueue`. Iterators are checked for
+/// equality with
+/// @cpp_func{pw_VariableLengthEntryQueue_Iterator_Equal}.
+///
+/// Iterators are invalidated by any operations that change the container or
+/// its underlying data (push/pop/init).
+typedef struct {
+ // Private: do not access these fields directly!
+ pw_VariableLengthEntryQueue_ConstHandle _pw_queue;
+ uint32_t _pw_offset;
+} pw_VariableLengthEntryQueue_Iterator;
+
+/// An entry in the queue. Entries may be stored in up to two segments, so this
+/// struct includes pointers to both portions of the entry.
+typedef struct {
+ const uint8_t* data_1;
+ uint32_t size_1;
+ const uint8_t* data_2;
+ uint32_t size_2;
+} pw_VariableLengthEntryQueue_Entry;
+
+/// Returns an iterator to the start of the `VariableLengthEntryQueue`.
+static inline pw_VariableLengthEntryQueue_Iterator
+pw_VariableLengthEntryQueue_Begin(
+ pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// Returns an iterator that points past the end of the queue.
+static inline pw_VariableLengthEntryQueue_Iterator
+pw_VariableLengthEntryQueue_End(pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// Advances an iterator to point to the next entry in the queue. It is
+/// invalid to call `Advance` on an iterator equal to the `End` iterator.
+void pw_VariableLengthEntryQueue_Iterator_Advance(
+ pw_VariableLengthEntryQueue_Iterator* iterator);
+
+/// Compares two iterators for equality.
+static inline bool pw_VariableLengthEntryQueue_Iterator_Equal(
+ const pw_VariableLengthEntryQueue_Iterator* lhs,
+ const pw_VariableLengthEntryQueue_Iterator* rhs);
+
+/// Dereferences an iterator, loading the entry it points to.
+pw_VariableLengthEntryQueue_Entry pw_VariableLengthEntryQueue_GetEntry(
+ const pw_VariableLengthEntryQueue_Iterator* iterator);
+
+/// Copies the contents of the entry to the provided buffer. The entry may be
+/// split into two regions; this serializes it into one buffer.
+///
+/// @param entry The entry whose contents to copy
+/// @param dest The buffer into which to copy the serialized entry
+/// @param count Copy up to this many bytes; must not be larger than the `dest`
+/// buffer, but may be larger than the entry
+uint32_t pw_VariableLengthEntryQueue_Entry_Copy(
+ const pw_VariableLengthEntryQueue_Entry* entry, void* dest, uint32_t count);
+
+/// Returns the byte at the specified index in the entry. Asserts if index is
+/// out-of-bounds.
+static inline uint8_t pw_VariableLengthEntryQueue_Entry_At(
+ const pw_VariableLengthEntryQueue_Entry* entry, size_t index);
+
+/// Returns the number of variable-length entries in the queue. This is O(n) in
+/// the number of entries in the queue.
+uint32_t pw_VariableLengthEntryQueue_Size(
+ pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// Returns the combined size in bytes of all entries in the queue, excluding
+/// metadata. This is O(n) in the number of entries in the queue.
+uint32_t pw_VariableLengthEntryQueue_SizeBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// Returns the the maximum number of bytes that can be stored in the queue.
+/// This is largest possible value of `size_bytes()`, and the size of the
+/// largest single entry that can be stored in this queue. Attempting to store a
+/// larger entry is invalid and results in a crash.
+static inline uint32_t pw_VariableLengthEntryQueue_MaxSizeBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// Returns the size of the raw underlying `VariableLengthEntryQueue` storage.
+/// This size may be used to copy a `VariableLengthEntryQueue` into another
+/// 32-bit aligned memory location.
+static inline uint32_t pw_VariableLengthEntryQueue_RawStorageSizeBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// Returns true if the `VariableLengthEntryQueue` is empty, false if it has at
+/// least one entry.
+static inline bool pw_VariableLengthEntryQueue_Empty(
+ pw_VariableLengthEntryQueue_ConstHandle queue);
+
+/// @}
+
+// Implementation details.
+
+#define _PW_VAR_QUEUE_DATA_SIZE_UINT32(max_size_bytes) \
+ ((_PW_VAR_QUEUE_DATA_SIZE_BYTES(max_size_bytes) + 3 /* round up */) / 4)
+
+#define _PW_VAR_QUEUE_DATA_SIZE_BYTES(max_size_bytes) \
+ (PW_VARINT_ENCODED_SIZE_BYTES(max_size_bytes) + max_size_bytes + \
+ 1 /*end byte*/)
+
+#define _PW_VAR_QUEUE_ARRAY_SIZE_BYTES queue[0]
+#define _PW_VAR_QUEUE_HEAD queue[1]
+#define _PW_VAR_QUEUE_TAIL queue[2] // points after the last byte
+#define _PW_VAR_QUEUE_DATA ((const uint8_t*)&queue[3])
+
+#define _PW_VAR_QUEUE_GET_ARRAY_SIZE_BYTES(array_size_uint32) \
+ (uint32_t)(array_size_uint32 - \
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32) * \
+ sizeof(uint32_t)
+
+static inline void pw_VariableLengthEntryQueue_Init(uint32_t array[],
+ size_t array_size_uint32) {
+ array[0] = _PW_VAR_QUEUE_GET_ARRAY_SIZE_BYTES(array_size_uint32);
+ array[1] = 0; // head
+ array[2] = 0; // tail
+}
+
+static inline void pw_VariableLengthEntryQueue_Clear(
+ pw_VariableLengthEntryQueue_Handle queue) {
+ _PW_VAR_QUEUE_HEAD = 0; // head
+ _PW_VAR_QUEUE_TAIL = 0; // tail
+}
+
+static inline pw_VariableLengthEntryQueue_Iterator
+pw_VariableLengthEntryQueue_Begin(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ pw_VariableLengthEntryQueue_Iterator begin = {queue, _PW_VAR_QUEUE_HEAD};
+ return begin;
+}
+
+static inline pw_VariableLengthEntryQueue_Iterator
+pw_VariableLengthEntryQueue_End(pw_VariableLengthEntryQueue_ConstHandle queue) {
+ pw_VariableLengthEntryQueue_Iterator end = {queue, _PW_VAR_QUEUE_TAIL};
+ return end;
+}
+
+static inline bool pw_VariableLengthEntryQueue_Iterator_Equal(
+ const pw_VariableLengthEntryQueue_Iterator* lhs,
+ const pw_VariableLengthEntryQueue_Iterator* rhs) {
+ return lhs->_pw_offset == rhs->_pw_offset && lhs->_pw_queue == rhs->_pw_queue;
+}
+
+// Private function that returns a pointer to the specified index in the Entry.
+static inline const uint8_t* _pw_VariableLengthEntryQueue_Entry_GetPointer(
+ const pw_VariableLengthEntryQueue_Entry* entry, size_t index) {
+ if (index < entry->size_1) {
+ return &entry->data_1[index];
+ }
+ return &entry->data_2[index - entry->size_1];
+}
+
+const uint8_t* _pw_VariableLengthEntryQueue_Entry_GetPointerChecked(
+ const pw_VariableLengthEntryQueue_Entry* entry, size_t index);
+
+static inline uint8_t pw_VariableLengthEntryQueue_Entry_At(
+ const pw_VariableLengthEntryQueue_Entry* entry, size_t index) {
+ return *_pw_VariableLengthEntryQueue_Entry_GetPointerChecked(entry, index);
+}
+
+static inline uint32_t pw_VariableLengthEntryQueue_RawStorageSizeBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ return PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32 * sizeof(uint32_t) +
+ _PW_VAR_QUEUE_ARRAY_SIZE_BYTES;
+}
+
+static inline uint32_t pw_VariableLengthEntryQueue_MaxSizeBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ return _PW_VAR_QUEUE_ARRAY_SIZE_BYTES - 1 -
+ (uint32_t)pw_varint_EncodedSizeBytes(_PW_VAR_QUEUE_ARRAY_SIZE_BYTES -
+ 1);
+}
+
+static inline bool pw_VariableLengthEntryQueue_Empty(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ return _PW_VAR_QUEUE_HEAD == _PW_VAR_QUEUE_TAIL;
+}
+
+// These macros are not part of the public API, so undefine them.
+#undef _PW_VAR_QUEUE_ARRAY_SIZE_BYTES
+#undef _PW_VAR_QUEUE_HEAD
+#undef _PW_VAR_QUEUE_TAIL
+#undef _PW_VAR_QUEUE_DATA
+
+#ifdef __cplusplus
+} // extern "C"
+#endif // __cplusplus
diff --git a/pw_containers/public/pw_containers/vector.h b/pw_containers/public/pw_containers/vector.h
index 62ebc8aa1..ace0462f2 100644
--- a/pw_containers/public/pw_containers/vector.h
+++ b/pw_containers/public/pw_containers/vector.h
@@ -26,6 +26,7 @@
#include "pw_assert/assert.h"
#include "pw_polyfill/language_feature_macros.h"
+#include "pw_preprocessor/compiler.h"
namespace pw {
namespace vector_impl {
@@ -316,7 +317,7 @@ class Vector<T, vector_impl::kGeneric>
// Returns the number of elements in the Vector. Uses size_t instead of
// size_type for consistency with other containers.
- size_t size() const noexcept { return size_; }
+ size_t size() const noexcept PW_NO_SANITIZE("memory") { return size_; }
// Returns the maximum number of elements in this Vector.
size_t max_size() const noexcept { return max_size_; }
@@ -335,8 +336,13 @@ class Vector<T, vector_impl::kGeneric>
iterator insert(const_iterator index, T&& value);
- template <typename Iterator>
- iterator insert(const_iterator index, Iterator first, Iterator last);
+ template <
+ typename Iterator,
+ int&... ExplicitArgumentBarrier,
+ typename = std::enable_if_t<vector_impl::IsIterator<Iterator>::value>>
+ iterator insert(const_iterator index, Iterator first, Iterator last) {
+ return InsertFrom(index, first, last);
+ }
iterator insert(const_iterator index, std::initializer_list<T> list) {
return insert(index, list.begin(), list.end());
@@ -358,9 +364,9 @@ class Vector<T, vector_impl::kGeneric>
void pop_back();
- void resize(size_type new_size) { resize(new_size, T()); }
+ void resize(size_t new_size) { resize(new_size, T()); }
- void resize(size_type new_size, const T& value);
+ void resize(size_t new_size, const T& value);
protected:
// Vectors without an explicit size cannot be constructed directly. Instead,
@@ -403,6 +409,9 @@ class Vector<T, vector_impl::kGeneric>
void Append(size_type count, const T& value);
+ template <typename Iterator>
+ iterator InsertFrom(const_iterator index, Iterator first, Iterator last);
+
const size_type max_size_;
size_type size_ = 0;
};
@@ -476,10 +485,12 @@ void Vector<T, vector_impl::kGeneric>::pop_back() {
}
template <typename T>
-void Vector<T, vector_impl::kGeneric>::resize(size_type new_size,
- const T& value) {
+void Vector<T, vector_impl::kGeneric>::resize(size_t new_size, const T& value) {
+ PW_DASSERT(new_size <= std::numeric_limits<size_type>::max());
if (size() < new_size) {
- Append(std::min(max_size(), size_t(new_size)) - size(), value);
+ size_type count =
+ static_cast<size_type>(std::min(max_size(), new_size) - size());
+ Append(count, value);
} else {
while (size() > new_size) {
pop_back();
@@ -509,40 +520,6 @@ typename Vector<T>::iterator Vector<T>::insert(Vector<T>::const_iterator index,
}
template <typename T>
-template <typename Iterator>
-typename Vector<T>::iterator Vector<T>::insert(Vector<T>::const_iterator index,
- Iterator first,
- Iterator last) {
- PW_DASSERT(index >= cbegin());
- PW_DASSERT(index <= cend());
- PW_DASSERT(!full());
-
- iterator insertion_point = begin() + std::distance(cbegin(), index);
-
- const size_t insertion_count = std::distance(first, last);
- if (insertion_count == 0) {
- return insertion_point;
- }
- PW_DASSERT(size() + insertion_count <= max_size());
-
- iterator return_value = insertion_point;
-
- if (insertion_point != end()) {
- std::move_backward(insertion_point, end(), end() + insertion_count);
- }
-
- while (first != last) {
- *insertion_point = *first;
- ++first;
- ++insertion_point;
- }
- size_ += insertion_count;
-
- // Return an iterator pointing to the first element inserted.
- return return_value;
-}
-
-template <typename T>
typename Vector<T>::iterator Vector<T>::insert(Vector<T>::const_iterator index,
size_type count,
const T& value) {
@@ -585,7 +562,7 @@ typename Vector<T>::iterator Vector<T>::erase(Vector<T>::const_iterator first,
iterator destination = begin() + std::distance(cbegin(), first);
iterator new_end = std::move(source, end(), destination);
- size_ = std::distance(begin(), new_end);
+ size_ = static_cast<size_type>(std::distance(begin(), new_end));
// Return an iterator following the last removed element.
return new_end;
@@ -614,4 +591,25 @@ void Vector<T, vector_impl::kGeneric>::Append(size_type count, const T& value) {
}
}
+template <typename T>
+template <typename Iterator>
+typename Vector<T>::iterator Vector<T, vector_impl::kGeneric>::InsertFrom(
+ Vector<T>::const_iterator index, Iterator first, Iterator last) {
+ PW_DASSERT(index >= cbegin());
+ PW_DASSERT(index <= cend());
+
+ // Return an iterator pointing to the first element inserted.
+ iterator retval = begin() + std::distance(cbegin(), index);
+ size_t count = static_cast<size_t>(std::distance(first, last));
+ PW_DASSERT(count <= max_size() - size());
+
+ if (retval != end()) {
+ std::move_backward(retval, end(), end() + count);
+ }
+ std::move(first, last, retval);
+
+ size_ += static_cast<size_type>(count);
+ return retval;
+}
+
} // namespace pw
diff --git a/pw_containers/pw_containers_private/test_helpers.h b/pw_containers/pw_containers_private/test_helpers.h
new file mode 100644
index 000000000..a52fa08f9
--- /dev/null
+++ b/pw_containers/pw_containers_private/test_helpers.h
@@ -0,0 +1,84 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+namespace pw::containers::test {
+
+struct CopyOnly {
+ explicit CopyOnly(int val) : value(val) {}
+
+ CopyOnly(const CopyOnly& other) { value = other.value; }
+
+ CopyOnly& operator=(const CopyOnly& other) {
+ value = other.value;
+ return *this;
+ }
+
+ CopyOnly(CopyOnly&&) = delete;
+
+ int value;
+};
+
+struct MoveOnly {
+ explicit MoveOnly(int val) : value(val) {}
+
+ MoveOnly(const MoveOnly&) = delete;
+
+ MoveOnly(MoveOnly&& other) {
+ value = other.value;
+ other.value = kDeleted;
+ }
+
+ static constexpr int kDeleted = -1138;
+
+ int value;
+};
+
+struct Counter {
+ static int created;
+ static int destroyed;
+ static int moved;
+
+ static void Reset() { created = destroyed = moved = 0; }
+
+ Counter() : value(0) { created += 1; }
+
+ Counter(int val) : value(val) { created += 1; }
+
+ Counter(const Counter& other) : value(other.value) { created += 1; }
+
+ Counter(Counter&& other) : value(other.value) {
+ other.value = 0;
+ moved += 1;
+ }
+
+ Counter& operator=(const Counter& other) {
+ value = other.value;
+ created += 1;
+ return *this;
+ }
+
+ Counter& operator=(Counter&& other) {
+ value = other.value;
+ other.value = 0;
+ moved += 1;
+ return *this;
+ }
+
+ ~Counter() { destroyed += 1; }
+
+ int value;
+};
+
+} // namespace pw::containers::test
diff --git a/pw_containers/pw_containers_private/variable_length_entry_queue_test_oracle.h b/pw_containers/pw_containers_private/variable_length_entry_queue_test_oracle.h
new file mode 100644
index 000000000..5538ff669
--- /dev/null
+++ b/pw_containers/pw_containers_private/variable_length_entry_queue_test_oracle.h
@@ -0,0 +1,93 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <deque>
+#include <vector>
+
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_varint/varint.h"
+
+namespace pw::containers {
+
+// Behaves like a VariableLengthEntryQueue should, but with a std::deque-based
+// implementation.
+class VariableLengthEntryQueueTestOracle {
+ public:
+ VariableLengthEntryQueueTestOracle(uint32_t max_size_bytes)
+ : max_size_bytes_(max_size_bytes),
+ raw_size_bytes_(0),
+ raw_capacity_bytes_(
+ static_cast<uint32_t>(varint::EncodedSize(max_size_bytes)) +
+ max_size_bytes) {}
+
+ void clear() {
+ q_.clear();
+ raw_size_bytes_ = 0;
+ }
+
+ void push_overwrite(ConstByteSpan data) {
+ size_t encoded_size = varint::EncodedSize(data.size()) + data.size();
+ while (encoded_size > (raw_capacity_bytes_ - raw_size_bytes_)) {
+ pop();
+ }
+ push(data);
+ }
+
+ void push(ConstByteSpan data) {
+ PW_ASSERT(data.size() <= max_size_bytes_);
+
+ size_t encoded_size = varint::EncodedSize(data.size()) + data.size();
+ PW_ASSERT(encoded_size <= raw_capacity_bytes_ - raw_size_bytes_);
+
+ q_.emplace_back(data.begin(), data.end());
+ raw_size_bytes_ += encoded_size;
+ }
+
+ void pop() {
+ PW_ASSERT(!q_.empty());
+ raw_size_bytes_ -=
+ varint::EncodedSize(q_.front().size()) + q_.front().size();
+ q_.pop_front();
+ }
+
+ uint32_t size() const { return static_cast<uint32_t>(q_.size()); }
+ uint32_t size_bytes() const {
+ uint32_t total_bytes = 0;
+ for (const auto& entry : q_) {
+ total_bytes += static_cast<uint32_t>(entry.size());
+ }
+ return total_bytes;
+ }
+ uint32_t max_size_bytes() const { return max_size_bytes_; }
+
+ std::deque<std::vector<std::byte>>::const_iterator begin() const {
+ return q_.begin();
+ }
+
+ std::deque<std::vector<std::byte>>::const_iterator end() const {
+ return q_.end();
+ }
+
+ private:
+ std::deque<std::vector<std::byte>> q_;
+ const uint32_t max_size_bytes_;
+ uint32_t raw_size_bytes_;
+ const uint32_t raw_capacity_bytes_;
+};
+
+} // namespace pw::containers
diff --git a/pw_containers/py/BUILD.gn b/pw_containers/py/BUILD.gn
new file mode 100644
index 000000000..255076d61
--- /dev/null
+++ b/pw_containers/py/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ generate_setup = {
+ metadata = {
+ name = "pw_containers"
+ version = "0.0.1"
+ }
+ }
+ sources = [
+ "pw_containers/__init__.py",
+ "pw_containers/variable_length_entry_queue.py",
+ ]
+ tests = [ "variable_length_entry_queue_test.py" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
diff --git a/pw_containers/py/pw_containers/__init__.py b/pw_containers/py/pw_containers/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_containers/py/pw_containers/__init__.py
diff --git a/pw_containers/py/pw_containers/variable_length_entry_queue.py b/pw_containers/py/pw_containers/variable_length_entry_queue.py
new file mode 100644
index 000000000..2c6e53d12
--- /dev/null
+++ b/pw_containers/py/pw_containers/variable_length_entry_queue.py
@@ -0,0 +1,79 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Decodes the in-memory representation of a sized-entry ring buffer."""
+
+import struct
+from typing import Iterable, Tuple
+
+_HEADER = struct.Struct('III') # data_size_bytes, head, tail
+
+
+def _decode_leb128(
+ data: bytes, offset: int = 0, max_bits: int = 32
+) -> Tuple[int, int]:
+ count = value = shift = 0
+
+ while offset < len(data):
+ byte = data[offset]
+
+ count += 1
+ value |= (byte & 0x7F) << shift
+
+ if not byte & 0x80:
+ return offset + count, value
+
+ shift += 7
+ if shift >= max_bits:
+ raise ValueError(f'Varint exceeded {max_bits}-bit limit')
+
+ raise ValueError(f'Unterminated varint {data[offset:]!r}')
+
+
+def parse(queue: bytes) -> Iterable[bytes]:
+ """Decodes the in-memory representation of a variable-length entry queue.
+
+ Args:
+ queue: The bytes representation of a variable-length entry queue.
+
+ Yields:
+ Each entry in the buffer as bytes.
+ """
+ array_size_bytes, head, tail = _HEADER.unpack_from(queue)
+
+ total_encoded_size = _HEADER.size + array_size_bytes
+ if len(queue) < total_encoded_size:
+ raise ValueError(
+ f'Ring buffer data ({len(queue)} B) is smaller than the encoded '
+ f'size ({total_encoded_size} B)'
+ )
+
+ data = queue[_HEADER.size : total_encoded_size]
+
+ if tail < head:
+ data = data[head:] + data[:tail]
+ else:
+ data = data[head:tail]
+
+ index = 0
+ while index < len(data):
+ index, size = _decode_leb128(data, index)
+
+ if index + size > len(data):
+ raise ValueError(
+ f'Corruption detected; '
+ f'encoded size {size} B is too large for a {len(data)} B array'
+ )
+ yield data[index : index + size]
+
+ index += size
diff --git a/pw_containers/py/variable_length_entry_queue_test.py b/pw_containers/py/variable_length_entry_queue_test.py
new file mode 100644
index 000000000..3343fd199
--- /dev/null
+++ b/pw_containers/py/variable_length_entry_queue_test.py
@@ -0,0 +1,61 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests parsing the sized-entry ring buffer's in-memory representation."""
+
+import unittest
+
+from pw_containers import variable_length_entry_queue
+
+
+def _buffer(head: int, tail: int, data: bytes) -> bytes:
+ return (
+ b''.join(i.to_bytes(4, 'little') for i in [len(data), head, tail])
+ + data
+ )
+
+
+class TestEncodeTokenized(unittest.TestCase):
+ def test_one_entry(self) -> None:
+ self.assertEqual(
+ list(
+ variable_length_entry_queue.parse(
+ _buffer(1, 6, b'?\0041234?890')
+ )
+ ),
+ [b'1234'],
+ )
+
+ def test_two_entries(self) -> None:
+ self.assertEqual(
+ list(
+ variable_length_entry_queue.parse(
+ _buffer(1, 7, b'?\00212\00234?90')
+ )
+ ),
+ [b'12', b'34'],
+ )
+
+ def test_two_entries_wrapped(self) -> None:
+ self.assertEqual(
+ list(
+ variable_length_entry_queue.parse(
+ _buffer(6, 4, b'4\00212?x\004123')
+ )
+ ),
+ [b'1234', b'12'],
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_containers/raw_storage_test.cc b/pw_containers/raw_storage_test.cc
new file mode 100644
index 000000000..c1623a19a
--- /dev/null
+++ b/pw_containers/raw_storage_test.cc
@@ -0,0 +1,72 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_containers/internal/raw_storage.h"
+
+#include <array>
+#include <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_containers_private/test_helpers.h"
+
+namespace pw::containers {
+namespace {
+
+using test::CopyOnly;
+using test::Counter;
+using test::MoveOnly;
+
+TEST(RawStorage, Construct_ZeroSized) {
+ internal::RawStorage<int, 0> array;
+ EXPECT_EQ(array.max_size(), 0u);
+}
+
+TEST(RawStorage, Construct_NonZeroSized) {
+ internal::RawStorage<int, 3> array;
+ EXPECT_EQ(array.max_size(), 3u);
+}
+
+TEST(RawStorage, Construct_Constexpr) {
+ constexpr internal::RawStorage<int, 2> kArray;
+ EXPECT_EQ(kArray.max_size(), 2u);
+}
+
+TEST(RawStorage, Construct_CopyOnly) {
+ internal::RawStorage<CopyOnly, 2> array;
+ EXPECT_EQ(array.max_size(), 2u);
+}
+
+TEST(RawStorage, Construct_MoveOnly) {
+ internal::RawStorage<MoveOnly, 2> array;
+ EXPECT_EQ(array.max_size(), 2u);
+}
+
+TEST(RawStorage, Destruct) {
+ Counter::Reset();
+
+ { [[maybe_unused]] internal::RawStorage<Counter, 128> destroyed; }
+
+ EXPECT_EQ(Counter::created, 0);
+ EXPECT_EQ(Counter::destroyed, 0);
+}
+
+static_assert(sizeof(internal::RawStorage<uint8_t, 42>) ==
+ 42 * sizeof(uint8_t));
+static_assert(sizeof(internal::RawStorage<uint16_t, 42>) ==
+ 42 * sizeof(uint16_t));
+static_assert(sizeof(internal::RawStorage<uint32_t, 42>) ==
+ 42 * sizeof(uint32_t));
+
+} // namespace
+} // namespace pw::containers
diff --git a/pw_containers/test_helpers.cc b/pw_containers/test_helpers.cc
new file mode 100644
index 000000000..e7bc2f25f
--- /dev/null
+++ b/pw_containers/test_helpers.cc
@@ -0,0 +1,22 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_containers_private/test_helpers.h"
+
+namespace pw::containers::test {
+
+int Counter::created = 0;
+int Counter::destroyed = 0;
+int Counter::moved = 0;
+
+} // namespace pw::containers::test
diff --git a/pw_containers/variable_length_entry_queue.c b/pw_containers/variable_length_entry_queue.c
new file mode 100644
index 000000000..7089b6fe3
--- /dev/null
+++ b/pw_containers/variable_length_entry_queue.c
@@ -0,0 +1,249 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_containers/variable_length_entry_queue.h"
+
+#include <string.h>
+
+#include "pw_assert/check.h"
+#include "pw_varint/varint.h"
+
+// Access the underlying buffer size and capacity (one less than size).
+static uint32_t BufferSize(const uint32_t* queue) { return queue[0]; }
+static uint32_t Capacity(const uint32_t* queue) {
+ return BufferSize(queue) - 1;
+}
+
+// Access the head and tail offsets.
+#define HEAD(queue) queue[1]
+#define TAIL(queue) queue[2]
+
+// Access the data. Strict aliasing rules do not apply to byte pointers.
+static uint8_t* WritableData(uint32_t* queue) { return (uint8_t*)&queue[3]; }
+static const uint8_t* Data(const uint32_t* queue) {
+ return (const uint8_t*)&queue[3];
+}
+
+static uint32_t WrapIndex(pw_VariableLengthEntryQueue_ConstHandle queue,
+ uint32_t offset) {
+ if (offset >= BufferSize(queue)) {
+ offset -= BufferSize(queue);
+ }
+ return offset;
+}
+
+typedef struct {
+ uint32_t prefix;
+ uint32_t data;
+} EntrySize;
+
+// Returns the size of an entry, including both the prefix length and data size.
+static EntrySize ReadEntrySize(pw_VariableLengthEntryQueue_ConstHandle queue,
+ uint32_t offset) {
+ EntrySize size = {0, 0};
+
+ bool keep_going;
+ do {
+ PW_DCHECK_UINT_NE(size.prefix, PW_VARINT_MAX_INT32_SIZE_BYTES);
+
+ keep_going = pw_varint_DecodeOneByte32(
+ Data(queue)[offset], size.prefix++, &size.data);
+ offset = WrapIndex(queue, offset + 1);
+ } while (keep_going);
+
+ return size;
+}
+
+static uint32_t EncodePrefix(pw_VariableLengthEntryQueue_ConstHandle queue,
+ uint8_t prefix[PW_VARINT_MAX_INT32_SIZE_BYTES],
+ uint32_t data_size_bytes) {
+ const uint32_t prefix_size = (uint32_t)pw_varint_Encode32(
+ data_size_bytes, prefix, PW_VARINT_MAX_INT32_SIZE_BYTES);
+
+ // Check that the ring buffer is capable of holding entries of this size.
+ PW_CHECK_UINT_LE(prefix_size + data_size_bytes,
+ Capacity(queue),
+ "Entry is too large for this VariableLengthEntryQueue");
+ return prefix_size;
+}
+
+// Returns the total encoded size of an entry.
+static uint32_t ReadEncodedEntrySize(
+ pw_VariableLengthEntryQueue_ConstHandle queue, uint32_t offset) {
+ const EntrySize entry_size = ReadEntrySize(queue, offset);
+ return entry_size.prefix + entry_size.data;
+}
+
+static uint32_t PopNonEmpty(pw_VariableLengthEntryQueue_Handle queue) {
+ const uint32_t entry_size = ReadEncodedEntrySize(queue, HEAD(queue));
+ HEAD(queue) = WrapIndex(queue, HEAD(queue) + entry_size);
+ return entry_size;
+}
+
+// Copies data to the buffer, wrapping around the end if needed.
+static uint32_t CopyAndWrap(pw_VariableLengthEntryQueue_Handle queue,
+ uint32_t tail,
+ const uint8_t* data,
+ uint32_t size) {
+ // Copy the new data in one or two chunks. The first chunk is copied to the
+ // byte after the tail, the second from the beginning of the buffer. Both may
+ // be zero bytes.
+ uint32_t first_chunk = BufferSize(queue) - tail;
+ if (first_chunk >= size) {
+ first_chunk = size;
+ } else { // Copy 2nd chunk from the beginning of the buffer (may be 0 bytes).
+ memcpy(WritableData(queue),
+ (const uint8_t*)data + first_chunk,
+ size - first_chunk);
+ }
+ memcpy(&WritableData(queue)[tail], data, first_chunk);
+ return WrapIndex(queue, tail + size);
+}
+
+static void AppendEntryKnownToFit(pw_VariableLengthEntryQueue_Handle queue,
+ const uint8_t* prefix,
+ uint32_t prefix_size,
+ const void* data,
+ uint32_t size) {
+ // Calculate the new tail offset. Don't update it until the copy is complete.
+ uint32_t tail = TAIL(queue);
+ tail = CopyAndWrap(queue, tail, prefix, prefix_size);
+ TAIL(queue) = CopyAndWrap(queue, tail, data, size);
+}
+
+static inline uint32_t AvailableBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ uint32_t tail = TAIL(queue);
+ if (tail < HEAD(queue)) {
+ tail += BufferSize(queue);
+ }
+ return Capacity(queue) - (tail - HEAD(queue));
+}
+
+void pw_VariableLengthEntryQueue_Push(pw_VariableLengthEntryQueue_Handle queue,
+ const void* data,
+ const uint32_t data_size_bytes) {
+ uint8_t prefix[PW_VARINT_MAX_INT32_SIZE_BYTES];
+ uint32_t prefix_size = EncodePrefix(queue, prefix, data_size_bytes);
+
+ PW_CHECK(prefix_size + data_size_bytes <= AvailableBytes(queue),
+ "Insufficient remaining space for entry");
+
+ AppendEntryKnownToFit(queue, prefix, prefix_size, data, data_size_bytes);
+}
+
+void pw_VariableLengthEntryQueue_PushOverwrite(
+ pw_VariableLengthEntryQueue_Handle queue,
+ const void* data,
+ const uint32_t data_size_bytes) {
+ uint8_t prefix[PW_VARINT_MAX_INT32_SIZE_BYTES];
+ uint32_t prefix_size = EncodePrefix(queue, prefix, data_size_bytes);
+
+ uint32_t available_bytes = AvailableBytes(queue);
+ while (data_size_bytes + prefix_size > available_bytes) {
+ available_bytes += PopNonEmpty(queue);
+ }
+
+ AppendEntryKnownToFit(queue, prefix, prefix_size, data, data_size_bytes);
+}
+
+void pw_VariableLengthEntryQueue_Pop(pw_VariableLengthEntryQueue_Handle queue) {
+ PW_CHECK(!pw_VariableLengthEntryQueue_Empty(queue));
+ PopNonEmpty(queue);
+}
+
+void pw_VariableLengthEntryQueue_Iterator_Advance(
+ pw_VariableLengthEntryQueue_Iterator* iterator) {
+ iterator->_pw_offset = WrapIndex(
+ iterator->_pw_queue,
+ iterator->_pw_offset +
+ ReadEncodedEntrySize(iterator->_pw_queue, iterator->_pw_offset));
+}
+
+pw_VariableLengthEntryQueue_Entry pw_VariableLengthEntryQueue_GetEntry(
+ const pw_VariableLengthEntryQueue_Iterator* iterator) {
+ pw_VariableLengthEntryQueue_ConstHandle queue = iterator->_pw_queue;
+
+ pw_VariableLengthEntryQueue_Entry entry;
+ EntrySize size = ReadEntrySize(queue, iterator->_pw_offset);
+ uint32_t offset_1 = WrapIndex(queue, iterator->_pw_offset + size.prefix);
+
+ const uint32_t first_chunk = BufferSize(queue) - offset_1;
+
+ if (size.data <= first_chunk) {
+ entry.size_1 = size.data;
+ entry.size_2 = 0;
+ } else {
+ entry.size_1 = first_chunk;
+ entry.size_2 = size.data - first_chunk;
+ }
+
+ entry.data_1 = Data(queue) + offset_1;
+ entry.data_2 = Data(queue) + WrapIndex(queue, offset_1 + entry.size_1);
+ return entry;
+}
+
+uint32_t pw_VariableLengthEntryQueue_Entry_Copy(
+ const pw_VariableLengthEntryQueue_Entry* entry,
+ void* dest,
+ uint32_t count) {
+ PW_DCHECK(dest != NULL || count == 0u);
+
+ const uint32_t entry_size = entry->size_1 + entry->size_2;
+ const uint32_t to_copy = count < entry_size ? count : entry_size;
+
+ if (to_copy == 0u) {
+ return 0u;
+ }
+
+ memcpy(dest, entry->data_1, entry->size_1);
+
+ const uint32_t remaining = to_copy - entry->size_1;
+ if (remaining != 0u) {
+ memcpy((uint8_t*)dest + entry->size_1, entry->data_2, remaining);
+ }
+
+ return to_copy;
+}
+
+const uint8_t* _pw_VariableLengthEntryQueue_Entry_GetPointerChecked(
+ const pw_VariableLengthEntryQueue_Entry* entry, size_t index) {
+ PW_CHECK_UINT_LT(index, entry->size_1 + entry->size_2);
+ return _pw_VariableLengthEntryQueue_Entry_GetPointer(entry, index);
+}
+
+uint32_t pw_VariableLengthEntryQueue_Size(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ uint32_t entry_count = 0;
+ uint32_t offset = HEAD(queue);
+
+ while (offset != TAIL(queue)) {
+ offset = WrapIndex(queue, offset + ReadEncodedEntrySize(queue, offset));
+ entry_count += 1;
+ }
+ return entry_count;
+}
+
+uint32_t pw_VariableLengthEntryQueue_SizeBytes(
+ pw_VariableLengthEntryQueue_ConstHandle queue) {
+ uint32_t total_entry_size_bytes = 0;
+ uint32_t offset = HEAD(queue);
+
+ while (offset != TAIL(queue)) {
+ const EntrySize size = ReadEntrySize(queue, offset);
+ offset = WrapIndex(queue, offset + size.prefix + size.data);
+ total_entry_size_bytes += size.data;
+ }
+ return total_entry_size_bytes;
+}
diff --git a/pw_containers/variable_length_entry_queue_test.cc b/pw_containers/variable_length_entry_queue_test.cc
new file mode 100644
index 000000000..2e4fb90d7
--- /dev/null
+++ b/pw_containers/variable_length_entry_queue_test.cc
@@ -0,0 +1,271 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_containers/variable_length_entry_queue.h"
+
+#include <cstring>
+#include <string_view>
+#include <variant>
+
+#include "gtest/gtest.h"
+#include "pw_containers_private/variable_length_entry_queue_test_oracle.h"
+
+namespace {
+
+struct PushOverwrite {
+ std::string_view data;
+};
+struct Push {
+ std::string_view data;
+};
+struct Pop {};
+struct Clear {};
+struct SizeEquals {
+ size_t expected;
+};
+
+using TestStep = std::variant<PushOverwrite, Push, Pop, Clear, SizeEquals>;
+
+// Copies an entry, which might be wrapped, to a single std::vector.
+std::vector<std::byte> ReadEntry(
+ const pw_VariableLengthEntryQueue_Iterator& it) {
+ auto entry = pw_VariableLengthEntryQueue_GetEntry(&it);
+ std::vector<std::byte> value(entry.size_1 + entry.size_2);
+ EXPECT_EQ(value.size(),
+ pw_VariableLengthEntryQueue_Entry_Copy(
+ &entry, value.data(), entry.size_1 + entry.size_2));
+ return value;
+}
+
+#define ASSERT_CONTENTS_EQ(oracle, queue) \
+ auto oracle_it = oracle.begin(); \
+ auto queue_it = pw_VariableLengthEntryQueue_Begin(queue); \
+ const auto queue_end = pw_VariableLengthEntryQueue_End(queue); \
+ uint32_t entries_compared = 0; \
+ while (oracle_it != oracle.end() && \
+ !pw_VariableLengthEntryQueue_Iterator_Equal(&queue_it, &queue_end)) { \
+ ASSERT_EQ(*oracle_it++, ReadEntry(queue_it)); \
+ pw_VariableLengthEntryQueue_Iterator_Advance(&queue_it); \
+ entries_compared += 1; \
+ } \
+ ASSERT_EQ(entries_compared, oracle.size())
+
+// Declares a test that performs a series of operations on a
+// VariableLengthEntryQueue and the "oracle" class, and checks that they match
+// after every step.
+#define DATA_DRIVEN_TEST(program, max_entry_size) \
+ TEST(VariableLengthEntryQueue, \
+ DataDrivenTest_##program##_MaxSizeBytes##max_entry_size) { \
+ pw::containers::VariableLengthEntryQueueTestOracle oracle(max_entry_size); \
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(c_queue, max_entry_size); \
+ \
+ for (const TestStep& step : program) { \
+ /* Take the action */ \
+ if (auto ow = std::get_if<PushOverwrite>(&step); ow != nullptr) { \
+ pw_VariableLengthEntryQueue_PushOverwrite( \
+ c_queue, ow->data.data(), static_cast<uint32_t>(ow->data.size())); \
+ oracle.push_overwrite(pw::as_bytes(pw::span(ow->data))); \
+ } else if (auto push = std::get_if<Push>(&step); push != nullptr) { \
+ pw_VariableLengthEntryQueue_Push( \
+ c_queue, \
+ push->data.data(), \
+ static_cast<uint32_t>(push->data.size())); \
+ oracle.push(pw::as_bytes(pw::span(push->data))); \
+ } else if (std::holds_alternative<Pop>(step)) { \
+ pw_VariableLengthEntryQueue_Pop(c_queue); \
+ oracle.pop(); \
+ } else if (auto size = std::get_if<SizeEquals>(&step); \
+ size != nullptr) { \
+ size_t actual = pw_VariableLengthEntryQueue_Size(c_queue); \
+ ASSERT_EQ(oracle.size(), actual); \
+ ASSERT_EQ(size->expected, actual); \
+ } else if (std::holds_alternative<Clear>(step)) { \
+ pw_VariableLengthEntryQueue_Clear(c_queue); \
+ oracle.clear(); \
+ } else { \
+ FAIL() << "Unhandled case"; \
+ } \
+ /* Check size and other functions */ \
+ ASSERT_EQ(pw_VariableLengthEntryQueue_Size(c_queue), oracle.size()); \
+ ASSERT_EQ(pw_VariableLengthEntryQueue_SizeBytes(c_queue), \
+ oracle.size_bytes()); \
+ ASSERT_EQ(pw_VariableLengthEntryQueue_MaxSizeBytes(c_queue), \
+ oracle.max_size_bytes()); \
+ ASSERT_CONTENTS_EQ(oracle, c_queue); \
+ } \
+ } \
+ static_assert(true, "use a semicolon")
+
+constexpr TestStep kPop[] = {
+ SizeEquals{0},
+ PushOverwrite{""},
+ SizeEquals{1},
+ Pop{},
+ SizeEquals{0},
+};
+
+DATA_DRIVEN_TEST(kPop, 0); // Only holds one empty entry.
+DATA_DRIVEN_TEST(kPop, 1);
+DATA_DRIVEN_TEST(kPop, 6);
+
+constexpr TestStep kOverwriteLargeEntriesWithSmall[] = {
+ PushOverwrite{"12345"},
+ PushOverwrite{"abcde"},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ SizeEquals{6},
+ Pop{},
+ Pop{},
+ Pop{},
+ Pop{},
+ Pop{},
+ Pop{},
+ SizeEquals{0},
+};
+DATA_DRIVEN_TEST(kOverwriteLargeEntriesWithSmall, 5);
+DATA_DRIVEN_TEST(kOverwriteLargeEntriesWithSmall, 6);
+DATA_DRIVEN_TEST(kOverwriteLargeEntriesWithSmall, 7);
+
+constexpr TestStep kOverwriteVaryingSizes012[] = {
+ PushOverwrite{""}, PushOverwrite{""}, PushOverwrite{""},
+ PushOverwrite{""}, PushOverwrite{""}, PushOverwrite{"1"},
+ PushOverwrite{"2"}, PushOverwrite{""}, PushOverwrite{"3"},
+ PushOverwrite{"4"}, PushOverwrite{""}, PushOverwrite{"5"},
+ PushOverwrite{"6"}, PushOverwrite{"ab"}, PushOverwrite{"cd"},
+ PushOverwrite{""}, PushOverwrite{"ef"}, PushOverwrite{"gh"},
+ PushOverwrite{"ij"},
+};
+DATA_DRIVEN_TEST(kOverwriteVaryingSizes012, 2);
+DATA_DRIVEN_TEST(kOverwriteVaryingSizes012, 3);
+
+constexpr TestStep kOverwriteVaryingSizesUpTo4[] = {
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{"1"},
+ PushOverwrite{"2"},
+ PushOverwrite{"3"},
+ PushOverwrite{"ab"},
+ PushOverwrite{"cd"},
+ PushOverwrite{"ef"},
+ PushOverwrite{"123"},
+ PushOverwrite{"456"},
+ PushOverwrite{"789"},
+ PushOverwrite{"abcd"},
+ PushOverwrite{"efgh"},
+ PushOverwrite{"ijkl"},
+ Pop{},
+ SizeEquals{0},
+};
+DATA_DRIVEN_TEST(kOverwriteVaryingSizesUpTo4, 4);
+DATA_DRIVEN_TEST(kOverwriteVaryingSizesUpTo4, 5);
+DATA_DRIVEN_TEST(kOverwriteVaryingSizesUpTo4, 6);
+
+constexpr char kBigEntryBytes[196]{};
+
+constexpr TestStep kTwoBytePrefix[] = {
+ PushOverwrite{std::string_view(kBigEntryBytes, 128)},
+ PushOverwrite{std::string_view(kBigEntryBytes, 128)},
+ PushOverwrite{std::string_view(kBigEntryBytes, 127)},
+ PushOverwrite{std::string_view(kBigEntryBytes, 128)},
+ PushOverwrite{std::string_view(kBigEntryBytes, 127)},
+ SizeEquals{1},
+ Pop{},
+ SizeEquals{0},
+};
+DATA_DRIVEN_TEST(kTwoBytePrefix, 128);
+DATA_DRIVEN_TEST(kTwoBytePrefix, 129);
+
+constexpr TestStep kClear[] = {
+ Push{"abcdefg"},
+ PushOverwrite{""},
+ PushOverwrite{""},
+ PushOverwrite{"a"},
+ PushOverwrite{"b"},
+ Clear{},
+ SizeEquals{0},
+ Clear{},
+};
+DATA_DRIVEN_TEST(kClear, 7);
+DATA_DRIVEN_TEST(kClear, 100);
+
+TEST(VariableLengthEntryQueue, DeclareMacro) {
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(queue, 123);
+
+ constexpr size_t kArraySizeBytes =
+ 123 + 1 /*prefix*/ + 1 /* end */ + 3 /* round up */ +
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32 * 4;
+ static_assert(sizeof(queue) == kArraySizeBytes);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_RawStorageSizeBytes(queue),
+ kArraySizeBytes - 3 /* padding isn't included */);
+
+ EXPECT_EQ(pw_VariableLengthEntryQueue_MaxSizeBytes(queue), 123u);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_SizeBytes(queue), 0u);
+ EXPECT_TRUE(pw_VariableLengthEntryQueue_Empty(queue));
+}
+
+TEST(VariableLengthEntryQueue, InitializeExistingBuffer) {
+ constexpr size_t kArraySize =
+ 10 + PW_VARIABLE_LENGTH_ENTRY_QUEUE_HEADER_SIZE_UINT32;
+ uint32_t queue[kArraySize];
+ pw_VariableLengthEntryQueue_Init(queue, kArraySize);
+
+ EXPECT_EQ(pw_VariableLengthEntryQueue_RawStorageSizeBytes(queue),
+ sizeof(queue));
+ EXPECT_EQ(pw_VariableLengthEntryQueue_MaxSizeBytes(queue),
+ sizeof(uint32_t) * 10u - 1 /*prefix*/ - 1 /*end*/);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_SizeBytes(queue), 0u);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_Size(queue), 0u);
+ EXPECT_TRUE(pw_VariableLengthEntryQueue_Empty(queue));
+}
+
+TEST(VariableLengthEntryQueue, MaxSizeElement) {
+ // Test max size elements for a few sizes. Commented out statements fail an
+ // assert because the elements are too large.
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q16, 126);
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q17, 127);
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q18, 128);
+ PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(q19, 129);
+
+ pw_VariableLengthEntryQueue_PushOverwrite(q16, kBigEntryBytes, 126);
+ pw_VariableLengthEntryQueue_PushOverwrite(q17, kBigEntryBytes, 126);
+ pw_VariableLengthEntryQueue_PushOverwrite(q18, kBigEntryBytes, 126);
+ pw_VariableLengthEntryQueue_PushOverwrite(q19, kBigEntryBytes, 126);
+
+ // pw_VariableLengthEntryQueue_PushOverwrite(q16, kBigEntryBytes, 127);
+ pw_VariableLengthEntryQueue_PushOverwrite(q17, kBigEntryBytes, 127);
+ pw_VariableLengthEntryQueue_PushOverwrite(q18, kBigEntryBytes, 127);
+ pw_VariableLengthEntryQueue_PushOverwrite(q19, kBigEntryBytes, 127);
+
+ // pw_VariableLengthEntryQueue_PushOverwrite(q16, kBigEntryBytes, 128);
+ // pw_VariableLengthEntryQueue_PushOverwrite(q17, kBigEntryBytes, 128);
+ pw_VariableLengthEntryQueue_PushOverwrite(q18, kBigEntryBytes, 128);
+ pw_VariableLengthEntryQueue_PushOverwrite(q19, kBigEntryBytes, 128);
+
+ // pw_VariableLengthEntryQueue_PushOverwrite(q16, kBigEntryBytes, 129);
+ // pw_VariableLengthEntryQueue_PushOverwrite(q17, kBigEntryBytes, 129);
+ // pw_VariableLengthEntryQueue_PushOverwrite(q18, kBigEntryBytes, 129);
+ pw_VariableLengthEntryQueue_PushOverwrite(q19, kBigEntryBytes, 129);
+
+ EXPECT_EQ(pw_VariableLengthEntryQueue_Size(q16), 1u);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_Size(q17), 1u);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_Size(q18), 1u);
+ EXPECT_EQ(pw_VariableLengthEntryQueue_Size(q19), 1u);
+}
+
+} // namespace
diff --git a/pw_containers/vector_test.cc b/pw_containers/vector_test.cc
index 4bde24758..814268f2b 100644
--- a/pw_containers/vector_test.cc
+++ b/pw_containers/vector_test.cc
@@ -17,87 +17,21 @@
#include <cstddef>
#include "gtest/gtest.h"
+#include "pw_containers_private/test_helpers.h"
namespace pw {
namespace {
using namespace std::literals::string_view_literals;
+using containers::test::CopyOnly;
+using containers::test::Counter;
+using containers::test::MoveOnly;
// Since pw::Vector<T, N> downcasts to a pw::Vector<T, 0>, ensure that the
// alignment doesn't change.
static_assert(alignof(Vector<std::max_align_t, 0>) ==
alignof(Vector<std::max_align_t, 1>));
-struct CopyOnly {
- explicit CopyOnly(int val) : value(val) {}
-
- CopyOnly(const CopyOnly& other) { value = other.value; }
-
- CopyOnly& operator=(const CopyOnly& other) {
- value = other.value;
- return *this;
- }
-
- CopyOnly(CopyOnly&&) = delete;
-
- int value;
-};
-
-struct MoveOnly {
- explicit MoveOnly(int val) : value(val) {}
-
- MoveOnly(const MoveOnly&) = delete;
-
- MoveOnly(MoveOnly&& other) {
- value = other.value;
- other.value = kDeleted;
- }
-
- static constexpr int kDeleted = -1138;
-
- int value;
-};
-
-struct Counter {
- static int created;
- static int destroyed;
- static int moved;
-
- static void Reset() { created = destroyed = moved = 0; }
-
- Counter() : value(0) { created += 1; }
-
- Counter(int val) : value(val) { created += 1; }
-
- Counter(const Counter& other) : value(other.value) { created += 1; }
-
- Counter(Counter&& other) : value(other.value) {
- other.value = 0;
- moved += 1;
- }
-
- Counter& operator=(const Counter& other) {
- value = other.value;
- created += 1;
- return *this;
- }
-
- Counter& operator=(Counter&& other) {
- value = other.value;
- other.value = 0;
- moved += 1;
- return *this;
- }
-
- ~Counter() { destroyed += 1; }
-
- int value;
-};
-
-int Counter::created = 0;
-int Counter::destroyed = 0;
-int Counter::moved = 0;
-
TEST(Vector, Construct_NoArg) {
Vector<int, 3> vector;
EXPECT_TRUE(vector.empty());
@@ -127,7 +61,7 @@ TEST(Vector, Construct_Iterators) {
Vector<int, 64> vector(array.begin(), array.end());
EXPECT_EQ(vector.size(), array.size());
- for (size_t i = 0; i < array.size(); ++i) {
+ for (unsigned short i = 0; i < array.size(); ++i) {
EXPECT_EQ(vector[i], array[i]);
}
}
@@ -140,7 +74,7 @@ TEST(Vector, Construct_Copy) {
EXPECT_EQ(3u, vector.size());
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, origin.value);
}
}
@@ -156,12 +90,12 @@ TEST(Vector, Construct_Move) {
EXPECT_EQ(5u, vector.size());
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, 421);
}
// NOLINTNEXTLINE(bugprone-use-after-move)
- for (size_t i = 0; i < origin_vector.size(); ++i) {
+ for (unsigned short i = 0; i < origin_vector.size(); ++i) {
EXPECT_EQ(origin_vector[i].value, MoveOnly::kDeleted);
}
}
@@ -263,7 +197,7 @@ TEST(Vector, Assign_Copy_SmallerToLarger) {
EXPECT_EQ(2u, vector.size());
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, origin.value);
}
}
@@ -320,12 +254,12 @@ TEST(Vector, Assign_Move) {
EXPECT_EQ(5u, vector.size());
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, 421);
}
// NOLINTNEXTLINE(bugprone-use-after-move)
- for (size_t i = 0; i < origin_vector.size(); ++i) {
+ for (unsigned short i = 0; i < origin_vector.size(); ++i) {
EXPECT_EQ(origin_vector[i].value, MoveOnly::kDeleted);
}
}
@@ -490,7 +424,7 @@ TEST(Vector, Modify_Erase_TrivialRangeBegin) {
vector.erase(vector.begin() + 2, vector.end());
EXPECT_EQ(vector.size(), 2u);
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i], i);
}
}
@@ -505,7 +439,7 @@ TEST(Vector, Modify_Erase_TrivialRangeEnd) {
vector.erase(vector.begin(), vector.end() - 2);
EXPECT_EQ(vector.size(), 2u);
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i], 8u + i);
}
}
@@ -536,14 +470,14 @@ TEST(Vector, Modify_Erase_NonTrivialRangeBegin) {
vector.emplace_back(static_cast<int>(i));
}
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, static_cast<int>(i));
}
vector.erase(vector.begin() + 2, vector.end());
EXPECT_EQ(vector.size(), 2u);
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, static_cast<int>(i));
}
@@ -562,7 +496,7 @@ TEST(Vector, Modify_Erase_NonTrivialRangeEnd) {
vector.erase(vector.begin(), vector.end() - 2);
EXPECT_EQ(vector.size(), 2u);
- for (size_t i = 0; i < vector.size(); ++i) {
+ for (unsigned short i = 0; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, static_cast<int>(8u + i));
}
@@ -590,8 +524,8 @@ TEST(Vector, Modify_Insert_End) {
Counter::Reset();
Vector<Counter, 10> vector;
- for (size_t i = 0; i < 8u; ++i) {
- vector.emplace_back(static_cast<int>(i));
+ for (int i = 0; i < 8; ++i) {
+ vector.emplace_back(i);
}
EXPECT_EQ(vector.size(), 8u);
@@ -610,8 +544,8 @@ TEST(Vector, Modify_Insert_Begin) {
Counter::Reset();
Vector<Counter, 10> vector;
- for (size_t i = 0; i < 8u; ++i) {
- vector.emplace_back(static_cast<int>(i));
+ for (int i = 0; i < 8; ++i) {
+ vector.emplace_back(i);
}
EXPECT_EQ(vector.size(), 8u);
@@ -633,7 +567,7 @@ TEST(Vector, Modify_Insert_CountCopies) {
EXPECT_EQ(vector.at(0).value, 123);
EXPECT_EQ(vector.size(), 9u);
- for (size_t i = 1; i < vector.size(); ++i) {
+ for (unsigned short i = 1; i < vector.size(); ++i) {
EXPECT_EQ(vector[i].value, 421);
}
@@ -653,11 +587,19 @@ TEST(Vector, Modify_Insert_IteratorRange) {
array_to_insert_middle.end());
EXPECT_EQ(*it, array_to_insert_middle.front());
- for (size_t i = 0; i < vector.max_size(); ++i) {
+ for (unsigned short i = 0; i < vector.max_size(); ++i) {
EXPECT_EQ(vector[i], static_cast<int>(i));
}
}
+TEST(Vector, Modify_Insert_IteratorEmptyRange) {
+ Vector<int, 10> src;
+ Vector<int, 10> dst(10, 1);
+ dst.insert(dst.end(), src.begin(), src.end());
+ EXPECT_EQ(dst.size(), 10U);
+ EXPECT_EQ(dst.back(), 1);
+}
+
TEST(Vector, Modify_Insert_InitializerListRange) {
std::array array_to_insert_first{0, 1, 2, 8, 9};
Vector<int, 10> vector(array_to_insert_first.begin(),
@@ -667,7 +609,7 @@ TEST(Vector, Modify_Insert_InitializerListRange) {
vector.insert(vector.cbegin() + 3, {3, 4, 5, 6, 7});
EXPECT_EQ(*it, 3);
- for (size_t i = 0; i < vector.max_size(); ++i) {
+ for (unsigned short i = 0; i < vector.max_size(); ++i) {
EXPECT_EQ(vector[i], static_cast<int>(i));
}
}
@@ -681,7 +623,7 @@ TEST(Vector, Modify_Insert_NonTrivial_InitializerListRange) {
vector.cbegin() + 3, std::initializer_list<Counter>{3, 4, 5, 6, 7});
EXPECT_EQ(it->value, 3);
- for (size_t i = 0; i < vector.max_size(); ++i) {
+ for (unsigned short i = 0; i < vector.max_size(); ++i) {
EXPECT_EQ(vector[i].value, static_cast<int>(i));
}
}
@@ -694,7 +636,7 @@ TEST(Vector, Generic) {
EXPECT_EQ(generic_vector.size(), vector.size());
EXPECT_EQ(generic_vector.max_size(), vector.max_size());
- int i = 0;
+ unsigned short i = 0;
for (int value : vector) {
EXPECT_EQ(value, generic_vector[i]);
i += 1;
diff --git a/pw_cpu_exception/BUILD.bazel b/pw_cpu_exception/BUILD.bazel
index 1a0b02491..4af80c9ac 100644
--- a/pw_cpu_exception/BUILD.bazel
+++ b/pw_cpu_exception/BUILD.bazel
@@ -12,18 +12,166 @@
# License for the specific language governing permissions and limitations under
# the License.
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_facade",
+ "pw_cc_library",
+)
+
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-filegroup(
- name = "pw_cpu_exception",
- srcs = [
- "basic_handler.cc",
+# This module has three facades, each of whose backends are set with a
+# different constraint_setting.
+#
+# - entry: This is the library that handles early exception entry and prepares
+# any CPU state that must be available to the exception handler via the
+# pw_cpu_exception_State object. The backend for this facade is
+# architecture-specific.
+constraint_setting(
+ name = "entry_constraint_setting",
+)
+
+# - handler: This facade is backed by an application-specific handler that
+# determines what to do when an exception is encountered. This may be
+# capturing a crash report before resetting the device, or in some cases
+# handling the exception to allow execution to continue.
+constraint_setting(
+ name = "handler_constraint_setting",
+)
+
+# - support: This facade provides architecture-independent functions that may be
+# helpful for dumping CPU state in various forms. This allows an application
+# to create an application-specific handler that is portable across multiple
+# architectures.
+constraint_setting(
+ name = "support_constraint_setting",
+)
+
+pw_cc_facade(
+ name = "entry_facade",
+ hdrs = [
"public/pw_cpu_exception/entry.h",
- "public/pw_cpu_exception/handler.h",
"public/pw_cpu_exception/state.h",
- "public/pw_cpu_exception/support.h",
- "start_exception_handler.cc",
],
+ includes = ["public"],
+ deps = ["//pw_preprocessor"],
+)
+
+pw_cc_library(
+ name = "entry",
+ hdrs = [
+ "public/pw_cpu_exception/entry.h",
+ "public/pw_cpu_exception/state.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":entry_backend",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_facade(
+ name = "handler_facade",
+ hdrs = ["public/pw_cpu_exception/handler.h"],
+ includes = ["public"],
+ deps = [":entry"],
+)
+
+pw_cc_library(
+ name = "handler",
+ srcs = ["start_exception_handler.cc"],
+ hdrs = ["public/pw_cpu_exception/handler.h"],
+ includes = ["public"],
+ deps = [
+ ":handler_backend",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_facade(
+ name = "support_facade",
+ hdrs = ["public/pw_cpu_exception/support.h"],
+ includes = ["public"],
+ deps = [":entry"],
+)
+
+pw_cc_library(
+ name = "support",
+ hdrs = ["public/pw_cpu_exception/support.h"],
+ includes = ["public"],
+ deps = [
+ ":entry",
+ ":support_backend",
+ ],
+)
+
+constraint_value(
+ name = "basic_handler_backend",
+ constraint_setting = "//pw_cpu_exception:handler_constraint_setting",
+)
+
+pw_cc_library(
+ name = "basic_handler",
+ srcs = ["basic_handler.cc"],
+ deps = [
+ ":entry",
+ ":handler_facade",
+ "//pw_log",
+ ],
+)
+
+# Override-able flags for each facade backend.
+label_flag(
+ name = "entry_backend",
+ build_setting_default = ":entry_backend_multiplexer",
+)
+
+label_flag(
+ name = "entry_backend_impl",
+ build_setting_default = ":entry_backend_impl_multiplexer",
+)
+
+label_flag(
+ name = "handler_backend",
+ build_setting_default = ":handler_backend_multiplexer",
+)
+
+label_flag(
+ name = "support_backend",
+ build_setting_default = ":support_backend_multiplexer",
+)
+
+# Default facade backends.
+alias(
+ name = "entry_backend_multiplexer",
+ actual = select({
+ "//pw_cpu_exception_cortex_m:entry_backend": "@pigweed//pw_cpu_exception_cortex_m:cpu_exception",
+ "//conditions:default": "//pw_build:unspecified_backend",
+ }),
+)
+
+alias(
+ name = "entry_backend_impl_multiplexer",
+ actual = select({
+ "//pw_cpu_exception_cortex_m:entry_backend": "@pigweed//pw_cpu_exception_cortex_m:cpu_exception_impl",
+ "//conditions:default": "//pw_build:unspecified_backend",
+ }),
+)
+
+alias(
+ name = "handler_backend_multiplexer",
+ actual = select({
+ ":basic_handler_backend": ":basic_handler",
+ "//conditions:default": "//pw_build:unspecified_backend",
+ }),
+)
+
+alias(
+ name = "support_backend_multiplexer",
+ actual = select({
+ "//pw_cpu_exception_cortex_m:support_backend": "@pigweed//pw_cpu_exception_cortex_m:support",
+ "//conditions:default": "//pw_build:unspecified_backend",
+ }),
)
diff --git a/pw_cpu_exception/docs.rst b/pw_cpu_exception/docs.rst
index 0cd214012..225382ac6 100644
--- a/pw_cpu_exception/docs.rst
+++ b/pw_cpu_exception/docs.rst
@@ -13,11 +13,11 @@ otherwise be clobbered by an application's exception handler.
-----
Setup
-----
-This module has three facades, each of whose backends are set with a
-different GN variable.
+This module has three facades, each of whose backends must be provided by the
+target or application.
-``pw_cpu_exception_ENTRY_BACKEND``
-==================================
+Entry facade
+============
This is the library that handles early exception entry and prepares any CPU
state that must be available to the exception handler via the
pw_cpu_exception_State object. The backend for this facade is
@@ -28,8 +28,11 @@ the platform's CPU exception handler interrupt so ``pw_cpu_exception_Entry()`` i
called immediately upon a CPU exception. For specifics on how this may be done,
see the backend documentation for your architecture.
-``pw_cpu_exception_HANDLER_BACKEND``
-====================================
+The GN variable to set the backend for this facade is
+``pw_cpu_exception_ENTRY_BACKEND``.
+
+Handler facade
+==============
This facade is backed by an application-specific handler that determines what to
do when an exception is encountered. This may be capturing a crash report before
resetting the device, or in some cases handling the exception to allow execution
@@ -39,21 +42,27 @@ Applications must also provide an implementation for
``pw_cpu_exception_DefaultHandler()``. The behavior of this functions is entirely
up to the application/project, but some examples are provided below:
- * Enter an infinite loop so the device can be debugged by JTAG.
- * Reset the device.
- * Attempt to handle the exception so execution can continue.
- * Capture and record additional device state and save to flash for a crash
- report.
- * A combination of the above, using logic that fits the needs of your project.
+* Enter an infinite loop so the device can be debugged by JTAG.
+* Reset the device.
+* Attempt to handle the exception so execution can continue.
+* Capture and record additional device state and save to flash for a crash
+ report.
+* A combination of the above, using logic that fits the needs of your project.
+
+The GN variable to set the backend for this facade is
+``pw_cpu_exception_HANDLER_BACKEND``.
-``pw_cpu_exception_SUPPORT_BACKEND``
-====================================
+Support facade
+==============
This facade provides architecture-independent functions that may be helpful for
dumping CPU state in various forms. This allows an application to create an
application-specific handler that is portable across multiple architectures.
-Avoiding circular dependencies with ``pw_cpu_exception_ENTRY_BACKEND``
-======================================================================
+The GN variable to set the backend for this facade is
+``pw_cpu_exception_SUPPORT_BACKEND``.
+
+Avoiding circular dependencies with the entry facade
+====================================================
The entry facade is hard tied to the definition of the
``pw_cpu_exception_State``, so spliting them into separate facades would require
extra configurations along with extra compatibility checks to ensure they are
@@ -68,6 +77,12 @@ is set, ``$dir_pw_cpu_exception:entry_impl`` must listed in the
Entry backends must provide their own ``*.impl`` target that collects their
entry implementation.
+In Bazel, this circular dependency is avoided by putting the backend's full
+implementation including the entry method into a separate override-able
+``entry_backend_impl`` library. When the entry facade is being used, the
+application should add a dependency on the
+``//pw_cpu_exception:entry_backend_impl`` label_flag.
+
------------
Module Usage
------------
@@ -102,22 +117,20 @@ CPU exception backends do not provide an exception handler, but instead provide
mechanisms to capture CPU state for use by an application's exception handler,
and allow recovery from CPU exceptions when possible.
- * The entry backend should provide a definition for the
- ``pw_cpu_exception_State`` object through
- ``pw_cpu_exception_backend/state.h``.
- * In GN, the entry backend should also provide a ``.impl`` suffixed form of
- the entry backend target which collects the actual entry implementation to
- avoid circular dependencies due to the state definition in the entry backend
- target.
- * The entry backend should implement the ``pw_cpu_exception_Entry()`` function
- that will call ``pw_cpu_exception_HandleException()`` after performing any
- necessary actions prior to handing control to the application's exception
- handler (e.g. capturing necessary CPU state).
- * If an application's exception handler backend modifies the captured CPU
- state, the state should be treated as though it were the original state of
- the CPU when the exception occurred. The backend may need to manually
- restore some of the modified state to ensure this on exception handler
- return.
+* The entry backend should provide a definition for the
+ ``pw_cpu_exception_State`` object through
+ ``pw_cpu_exception_backend/state.h``.
+* In GN, the entry backend should also provide a ``.impl`` suffixed form of the
+ entry backend target which collects the actual entry implementation to avoid
+ circular dependencies due to the state definition in the entry backend target.
+* The entry backend should implement the ``pw_cpu_exception_Entry()`` function
+ that will call ``pw_cpu_exception_HandleException()`` after performing any
+ necessary actions prior to handing control to the application's exception
+ handler (e.g. capturing necessary CPU state).
+* If an application's exception handler backend modifies the captured CPU state,
+ the state should be treated as though it were the original state of the CPU
+ when the exception occurred. The backend may need to manually restore some of
+ the modified state to ensure this on exception handler return.
-------------
Compatibility
@@ -128,5 +141,5 @@ the "support" facade and library, which requires C++.
------------
Dependencies
------------
- * ``pw_span``
- * ``pw_preprocessor``
+- :ref:`module-pw_span`
+- :ref:`module-pw_preprocessor`
diff --git a/pw_cpu_exception/start_exception_handler.cc b/pw_cpu_exception/start_exception_handler.cc
index 284bb34b3..82b3528c8 100644
--- a/pw_cpu_exception/start_exception_handler.cc
+++ b/pw_cpu_exception/start_exception_handler.cc
@@ -33,4 +33,4 @@ extern "C" void pw_cpu_exception_HandleException(void* cpu_state) {
exception_handler(reinterpret_cast<pw_cpu_exception_State*>(cpu_state));
}
-} // namespace pw::cpu_exception \ No newline at end of file
+} // namespace pw::cpu_exception
diff --git a/pw_cpu_exception_cortex_m/BUILD.bazel b/pw_cpu_exception_cortex_m/BUILD.bazel
index 102d5ebf8..3880193f6 100644
--- a/pw_cpu_exception_cortex_m/BUILD.bazel
+++ b/pw_cpu_exception_cortex_m/BUILD.bazel
@@ -12,18 +12,28 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
"pw_cc_test",
)
-load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "entry_backend",
+ constraint_setting = "//pw_cpu_exception:entry_constraint_setting",
+)
+
+constraint_value(
+ name = "support_backend",
+ constraint_setting = "//pw_cpu_exception:support_constraint_setting",
+)
+
pw_cc_library(
name = "config",
hdrs = ["pw_cpu_exception_cortex_m_private/config.h"],
@@ -68,6 +78,7 @@ pw_cc_library(
":cortex_m_constants",
":cpu_state",
":util",
+ "//pw_cpu_exception:support_facade",
"//pw_log",
"//pw_preprocessor",
"//pw_preprocessor:cortex_m",
@@ -110,27 +121,35 @@ pw_proto_library(
pw_cc_library(
name = "cpu_exception",
- srcs = ["entry.cc"],
hdrs = [
- "public/pw_cpu_exception_cortex_m/cpu_state.h",
"public_overrides/pw_cpu_exception_backend/state.h",
],
- includes = ["public"],
+ includes = [
+ "public_overrides",
+ ],
deps = [
- ":config",
":cpu_state",
- ":cortex_m_constants",
- ":proto_dump",
- ":support",
- ":util",
- # TODO(b/242183021): Need to add support for facades/backends to Bazel.
- # "//pw_cpu_exception",
+ "//pw_cpu_exception:entry_facade",
"//pw_preprocessor",
"//pw_preprocessor:cortex_m",
],
)
pw_cc_library(
+ name = "cpu_exception_impl",
+ srcs = ["entry.cc"],
+ deps = [
+ ":config",
+ ":cortex_m_constants",
+ ":cpu_state",
+ ":util",
+ # Depend on the full backends in the impl target.
+ "//pw_cpu_exception:entry",
+ "//pw_cpu_exception:handler",
+ ],
+)
+
+pw_cc_library(
name = "snapshot",
srcs = ["snapshot.cc"],
hdrs = ["public/pw_cpu_exception_cortex_m/snapshot.h"],
@@ -165,6 +184,7 @@ pw_cc_test(
":config",
":cpu_exception",
":cpu_state",
+ "//pw_cpu_exception:handler",
],
)
diff --git a/pw_cpu_exception_cortex_m/BUILD.gn b/pw_cpu_exception_cortex_m/BUILD.gn
index 7f029d5ab..51f07f27a 100644
--- a/pw_cpu_exception_cortex_m/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/BUILD.gn
@@ -210,7 +210,7 @@ pw_test_group("tests") {
tests = [ ":cpu_exception_entry_test" ]
}
-# TODO(b/234888156): Add ARMv8-M mainline coverage.
+# TODO: b/234888156 - Add ARMv8-M mainline coverage.
pw_test("cpu_exception_entry_test") {
enable_if = pw_cpu_exception_ENTRY_BACKEND ==
"$dir_pw_cpu_exception_cortex_m:cpu_exception"
diff --git a/pw_cpu_exception_cortex_m/CMakeLists.txt b/pw_cpu_exception_cortex_m/CMakeLists.txt
index fe0eaa108..a6ff07f3a 100644
--- a/pw_cpu_exception_cortex_m/CMakeLists.txt
+++ b/pw_cpu_exception_cortex_m/CMakeLists.txt
@@ -133,7 +133,7 @@ pw_add_library(pw_cpu_exception_cortex_m.constants INTERFACE
pw_preprocessor.arch
)
-# TODO(b/234888156): Add ARMv8-M mainline coverage.
+# TODO: b/234888156 - Add ARMv8-M mainline coverage.
if("${pw_cpu_exception.entry_BACKEND}" STREQUAL
"pw_cpu_exception_cortex_m.cpu_exception")
pw_add_test(pw_cpu_exception_cortex_m.cpu_exception_entry_test
diff --git a/pw_cpu_exception_cortex_m/docs.rst b/pw_cpu_exception_cortex_m/docs.rst
index b8726b222..b52e17e94 100644
--- a/pw_cpu_exception_cortex_m/docs.rst
+++ b/pw_cpu_exception_cortex_m/docs.rst
@@ -15,60 +15,63 @@ Setup
There are a few ways to set up the Cortex M exception handler so the
application's exception handler is properly called during an exception.
-**1. Use existing CMSIS functions**
- Inside of CMSIS fault handler functions, branch to ``pw_cpu_exception_Entry``.
-
- .. code-block:: cpp
-
- __attribute__((naked)) void HardFault_Handler(void) {
- asm volatile(
- " ldr r0, =pw_cpu_exception_Entry \n"
- " bx r0 \n");
- }
-
-**2. Modify a startup file**
- Assembly startup files for some microcontrollers initialize the interrupt
- vector table. The functions to call for fault handlers can be changed here.
- For ARMv7-M and ARMv8-M, the fault handlers are indexes 3 to 6 of the
- interrupt vector table. It's also may be helpful to redirect the NMI handler
- to the entry function (if it's otherwise unused in your project).
-
- Default:
-
- .. code-block:: cpp
-
- __isr_vector_table:
- .word __stack_start
- .word Reset_Handler
- .word NMI_Handler
- .word HardFault_Handler
- .word MemManage_Handler
- .word BusFault_Handler
- .word UsageFault_Handler
-
- Using CPU exception module:
-
- .. code-block:: cpp
-
- __isr_vector_table:
- .word __stack_start
- .word Reset_Handler
- .word pw_cpu_exception_Entry
- .word pw_cpu_exception_Entry
- .word pw_cpu_exception_Entry
- .word pw_cpu_exception_Entry
- .word pw_cpu_exception_Entry
-
- Note: ``__isr_vector_table`` and ``__stack_start`` are example names, and may
- vary by platform. See your platform's assembly startup script.
-
-**3. Modify interrupt vector table at runtime**
- Some applications may choose to modify their interrupt vector tables at
- runtime. The exception handler works with this use case (see the
- exception_entry_test integration test), but keep in mind that your
- application's exception handler will not be entered if an exception occurs
- before the vector table entries are updated to point to
- ``pw_cpu_exception_Entry``.
+1. Use existing CMSIS functions
+-------------------------------
+Inside of CMSIS fault handler functions, branch to ``pw_cpu_exception_Entry``.
+
+.. code-block:: cpp
+
+ __attribute__((naked)) void HardFault_Handler(void) {
+ asm volatile(
+ " ldr r0, =pw_cpu_exception_Entry \n"
+ " bx r0 \n");
+ }
+
+2. Modify a startup file
+------------------------
+Assembly startup files for some microcontrollers initialize the interrupt
+vector table. The functions to call for fault handlers can be changed here.
+For ARMv7-M and ARMv8-M, the fault handlers are indexes 3 to 6 of the
+interrupt vector table. It's also may be helpful to redirect the NMI handler
+to the entry function (if it's otherwise unused in your project).
+
+Default:
+
+.. code-block:: cpp
+
+ __isr_vector_table:
+ .word __stack_start
+ .word Reset_Handler
+ .word NMI_Handler
+ .word HardFault_Handler
+ .word MemManage_Handler
+ .word BusFault_Handler
+ .word UsageFault_Handler
+
+Using CPU exception module:
+
+.. code-block:: cpp
+
+ __isr_vector_table:
+ .word __stack_start
+ .word Reset_Handler
+ .word pw_cpu_exception_Entry
+ .word pw_cpu_exception_Entry
+ .word pw_cpu_exception_Entry
+ .word pw_cpu_exception_Entry
+ .word pw_cpu_exception_Entry
+
+Note: ``__isr_vector_table`` and ``__stack_start`` are example names, and may
+vary by platform. See your platform's assembly startup script.
+
+3. Modify interrupt vector table at runtime
+-------------------------------------------
+Some applications may choose to modify their interrupt vector tables at
+runtime. The exception handler works with this use case (see the
+exception_entry_test integration test), but keep in mind that your
+application's exception handler will not be entered if an exception occurs
+before the vector table entries are updated to point to
+``pw_cpu_exception_Entry``.
Module Usage
============
@@ -115,7 +118,6 @@ nest.
Configuration Options
=====================
-
- ``PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP``: Enable extended logging in
``pw::cpu_exception::LogCpuState()`` that dumps the active CFSR fields with
help strings. This is disabled by default since it increases the binary size
@@ -139,30 +141,30 @@ human-readable information (e.g. "Encountered invalid instruction").
For example:
- .. code-block::
-
- $ python -m pw_cpu_exception_cortex_m.cfsr_decoder 0x00010100
- 20210412 15:11:14 INF Exception caused by a usage fault, bus fault.
-
- Active Crash Fault Status Register (CFSR) fields:
- IBUSERR Instruction bus error.
- The processor attempted to issue an invalid instruction. It
- detects the instruction bus error on prefecting, but this
- flag is only set to 1 if it attempts to issue the faulting
- instruction. When this bit is set, the processor has not
- written a fault address to the BFAR.
- UNDEFINSTR Encountered invalid instruction.
- The processor has attempted to execute an undefined
- instruction. When this bit is set to 1, the PC value stacked
- for the exception return points to the undefined instruction.
- An undefined instruction is an instruction that the processor
- cannot decode.
+.. code-block::
- All registers:
- cfsr 0x00010100
+ $ python -m pw_cpu_exception_cortex_m.cfsr_decoder 0x00010100
+ 20210412 15:11:14 INF Exception caused by a usage fault, bus fault.
+
+ Active Crash Fault Status Register (CFSR) fields:
+ IBUSERR Instruction bus error.
+ The processor attempted to issue an invalid instruction. It
+ detects the instruction bus error on prefecting, but this
+ flag is only set to 1 if it attempts to issue the faulting
+ instruction. When this bit is set, the processor has not
+ written a fault address to the BFAR.
+ UNDEFINSTR Encountered invalid instruction.
+ The processor has attempted to execute an undefined
+ instruction. When this bit is set to 1, the PC value stacked
+ for the exception return points to the undefined instruction.
+ An undefined instruction is an instruction that the processor
+ cannot decode.
+
+ All registers:
+ cfsr 0x00010100
.. note::
- The CFSR is not supported on ARMv6-M CPUs (Cortex M0, M0+, M1).
+ The CFSR is not supported on ARMv6-M CPUs (Cortex M0, M0+, M1).
--------------------
Snapshot integration
@@ -186,10 +188,10 @@ limits must be provided along with a stack processing callback. All of this
information is captured by a ``pw::thread::Thread`` protobuf encoder.
.. note::
- We recommend providing the ``pw_cpu_exception_State``, for example through
- ``pw_cpu_exception_DefaultHandler()`` instead of using the current running
- context to capture the main stack to minimize how much of the snapshot
- handling is captured in the stack.
+ We recommend providing the ``pw_cpu_exception_State``, for example through
+ ``pw_cpu_exception_DefaultHandler()`` instead of using the current running
+ context to capture the main stack to minimize how much of the snapshot
+ handling is captured in the stack.
Python processor
================
@@ -199,43 +201,43 @@ dump from a serialized snapshot proto, for example:
.. code-block::
- Exception caused by a usage fault.
-
- Active Crash Fault Status Register (CFSR) fields:
- UNDEFINSTR Undefined Instruction UsageFault.
- The processor has attempted to execute an undefined
- instruction. When this bit is set to 1, the PC value stacked
- for the exception return points to the undefined instruction.
- An undefined instruction is an instruction that the processor
- cannot decode.
-
- All registers:
- pc 0x0800e1c4 example::Service::Crash(_example_service_CrashRequest const&, _pw_protobuf_Empty&) (src/example_service/service.cc:131)
- lr 0x0800e141 example::Service::Crash(_example_service_CrashRequest const&, _pw_protobuf_Empty&) (src/example_service/service.cc:128)
- psr 0x81000000
- msp 0x20040fd8
- psp 0x20001488
- exc_return 0xffffffed
- cfsr 0x00010000
- mmfar 0xe000ed34
- bfar 0xe000ed38
- icsr 0x00000803
- hfsr 0x40000000
- shcsr 0x00000000
- control 0x00000000
- r0 0xe03f7847
- r1 0x714083dc
- r2 0x0b36dc49
- r3 0x7fbfbe1a
- r4 0xc36e8efb
- r5 0x69a14b13
- r6 0x0ec35eaa
- r7 0xa5df5543
- r8 0xc892b931
- r9 0xa2372c94
- r10 0xbd15c968
- r11 0x759b95ab
- r12 0x00000000
+ Exception caused by a usage fault.
+
+ Active Crash Fault Status Register (CFSR) fields:
+ UNDEFINSTR Undefined Instruction UsageFault.
+ The processor has attempted to execute an undefined
+ instruction. When this bit is set to 1, the PC value stacked
+ for the exception return points to the undefined instruction.
+ An undefined instruction is an instruction that the processor
+ cannot decode.
+
+ All registers:
+ pc 0x0800e1c4 example::Service::Crash(_example_service_CrashRequest const&, _pw_protobuf_Empty&) (src/example_service/service.cc:131)
+ lr 0x0800e141 example::Service::Crash(_example_service_CrashRequest const&, _pw_protobuf_Empty&) (src/example_service/service.cc:128)
+ psr 0x81000000
+ msp 0x20040fd8
+ psp 0x20001488
+ exc_return 0xffffffed
+ cfsr 0x00010000
+ mmfar 0xe000ed34
+ bfar 0xe000ed38
+ icsr 0x00000803
+ hfsr 0x40000000
+ shcsr 0x00000000
+ control 0x00000000
+ r0 0xe03f7847
+ r1 0x714083dc
+ r2 0x0b36dc49
+ r3 0x7fbfbe1a
+ r4 0xc36e8efb
+ r5 0x69a14b13
+ r6 0x0ec35eaa
+ r7 0xa5df5543
+ r8 0xc892b931
+ r9 0xa2372c94
+ r10 0xbd15c968
+ r11 0x759b95ab
+ r12 0x00000000
Module Configuration Options
============================
@@ -246,17 +248,17 @@ more details.
.. c:macro:: PW_CPU_EXCEPTION_CORTEX_M_LOG_LEVEL
- The log level to use for this module. Logs below this level are omitted.
+ The log level to use for this module. Logs below this level are omitted.
- This defaults to ``PW_LOG_LEVEL_DEBUG``.
+ This defaults to ``PW_LOG_LEVEL_DEBUG``.
.. c:macro:: PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
- Enables extended logging in pw::cpu_exception::LogCpuState() and
- pw::cpu_exception::cortex_m::LogExceptionAnalysis() that dumps the active
- CFSR fields with help strings. This is disabled by default since it
- increases the binary size by >1.5KB when using plain-text logs, or ~460
- Bytes when using tokenized logging. It's useful to enable this for device
- bringup until your application has an end-to-end crash reporting solution.
+ Enables extended logging in pw::cpu_exception::LogCpuState() and
+ pw::cpu_exception::cortex_m::LogExceptionAnalysis() that dumps the active
+ CFSR fields with help strings. This is disabled by default since it
+ increases the binary size by >1.5KB when using plain-text logs, or ~460
+ Bytes when using tokenized logging. It's useful to enable this for device
+ bringup until your application has an end-to-end crash reporting solution.
- This is disabled by default.
+ This is disabled by default.
diff --git a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
index 67f4fae11..fed0ff61d 100644
--- a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
+++ b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
@@ -42,7 +42,7 @@
#endif // PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
// Whether the floating-point unit is enabled.
-// TODO(b/264897542): This should be an Arm target trait.
+// TODO: b/264897542 - This should be an Arm target trait.
#ifndef PW_ARMV7M_ENABLE_FPU
#define PW_ARMV7M_ENABLE_FPU 1
#endif // PW_ARMV7M_ENABLE_FPU
diff --git a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h
index 255057669..c6f479ec3 100644
--- a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h
+++ b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h
@@ -85,7 +85,7 @@ constexpr uint32_t kXpsrIpsrMask = 0b1'1111'1111;
constexpr uint32_t kControlThreadModeStackMask = 0x1u << 1; // 0=MSP, 1=PSP
// Memory mapped registers. (ARMv7-M Section B3.2.2, Table B3-4)
-// TODO(b/234891073): Only some of these are supported on ARMv6-M.
+// TODO: b/234891073 - Only some of these are supported on ARMv6-M.
inline volatile uint32_t& cortex_m_cfsr =
*reinterpret_cast<volatile uint32_t*>(0xE000ED28u);
inline volatile uint32_t& cortex_m_mmfar =
diff --git a/pw_cpu_exception_cortex_m/py/BUILD.bazel b/pw_cpu_exception_cortex_m/py/BUILD.bazel
index 93f5f82a4..ca91f852e 100644
--- a/pw_cpu_exception_cortex_m/py/BUILD.bazel
+++ b/pw_cpu_exception_cortex_m/py/BUILD.bazel
@@ -23,7 +23,12 @@ py_library(
"pw_cpu_exception_cortex_m/cortex_m_constants.py",
"pw_cpu_exception_cortex_m/exception_analyzer.py",
],
- imports = ["."],
+ imports = [
+ # For importing pw_cpu_exception_cortex_m
+ ".",
+ # For importing pw_cpu_exception_cortex_m_protos
+ "..",
+ ],
deps = [
"//pw_cpu_exception_cortex_m:cpu_state_protos_pb2",
"//pw_symbolizer/py:pw_symbolizer",
diff --git a/pw_cpu_exception_cortex_m/py/BUILD.gn b/pw_cpu_exception_cortex_m/py/BUILD.gn
index c6425ee7d..975bef7b1 100644
--- a/pw_cpu_exception_cortex_m/py/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_cpu_exception_cortex_m/__init__.py",
diff --git a/pw_cpu_exception_cortex_m/py/setup.py b/pw_cpu_exception_cortex_m/py/setup.py
deleted file mode 100644
index cbbf5d37b..000000000
--- a/pw_cpu_exception_cortex_m/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_cpu_exception_cortex_m"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_crypto/BUILD.bazel b/pw_crypto/BUILD.bazel
index f0fb37245..56364710e 100644
--- a/pw_crypto/BUILD.bazel
+++ b/pw_crypto/BUILD.bazel
@@ -40,12 +40,37 @@ pw_cc_facade(
pw_cc_library(
name = "sha256",
+ hdrs = [
+ "public/pw_crypto/sha256.h",
+ ],
+ includes = ["public"],
deps = [
- ":sha256_facade",
- "@pigweed_config//:pw_crypto_sha256_backend",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_status",
+ "//pw_stream",
+ "@pigweed//targets:pw_crypto_sha256_backend",
],
)
+constraint_setting(
+ name = "sha256_backend_constraint_setting",
+)
+
+constraint_value(
+ name = "sha256_mbedtls_backend",
+ constraint_setting = ":sha256_backend_constraint_setting",
+)
+
+alias(
+ name = "sha256_backend_multiplexer",
+ actual = select({
+ ":sha256_mbedtls_backend": ":sha256_mbedtls",
+ "//conditions:default": ":sha256_mbedtls",
+ }),
+)
+
pw_cc_library(
name = "sha256_mbedtls",
srcs = ["sha256_mbedtls.cc"],
@@ -54,26 +79,9 @@ pw_cc_library(
"public_overrides/mbedtls/pw_crypto/sha256_backend.h",
],
includes = ["public_overrides/mbedtls"],
- # TODO(b/236321905): Requires BUILD.bazel files for mbedtls
- tags = ["manual"],
- deps = [":sha256_facade"],
-)
-
-pw_cc_library(
- name = "sha256_boringssl",
- srcs = ["sha256_boringssl.cc"],
- hdrs = [
- "public/pw_crypto/sha256_boringssl.h",
- "public_overrides/boringssl/pw_crypto/sha256_backend.h",
- ],
- includes = [
- "public",
- "public_overrides/boringssl",
- ],
deps = [
":sha256_facade",
- "//pw_log",
- "@boringssl//:ssl",
+ "@mbedtls",
],
)
@@ -103,8 +111,6 @@ pw_cc_library(
pw_cc_test(
name = "sha256_mock_test",
srcs = ["sha256_mock_test.cc"],
- # TODO(b/236321905): Requires BUILD.bazel files for mbedtls
- tags = ["manual"],
deps = [
":sha256_mock",
"//pw_unit_test",
@@ -125,27 +131,48 @@ pw_cc_facade(
pw_cc_library(
name = "ecdsa",
+ hdrs = [
+ "public/pw_crypto/ecdsa.h",
+ ],
+ includes = ["public"],
deps = [
- ":ecdsa_facade",
- "@pigweed_config//:pw_crypto_ecdsa_backend",
+ "//pw_bytes",
+ "//pw_status",
+ "@pigweed//targets:pw_crypto_ecdsa_backend",
],
)
-pw_cc_library(
- name = "ecdsa_mbedtls",
- srcs = ["ecdsa_mbedtls.cc"],
- # TODO(b/236321905): Requires BUILD.bazel files for mbedtls
- tags = ["manual"],
- deps = [":ecdsa_facade"],
+constraint_setting(
+ name = "ecdsa_backend_constraint_setting",
+)
+
+constraint_value(
+ name = "ecdsa_mbedtls_backend",
+ constraint_setting = ":ecdsa_backend_constraint_setting",
+)
+
+constraint_value(
+ name = "ecdsa_uecc_backend",
+ constraint_setting = ":ecdsa_backend_constraint_setting",
+)
+
+alias(
+ name = "ecdsa_backend_multiplexer",
+ actual = select({
+ ":ecdsa_mbedtls_backend": ":ecdsa_mbedtls",
+ ":ecdsa_uecc_backend": ":ecdsa_uecc",
+ "//conditions:default": ":ecdsa_mbedtls",
+ }),
)
pw_cc_library(
- name = "ecdsa_boringssl",
- srcs = ["ecdsa_boringssl.cc"],
+ name = "ecdsa_mbedtls",
+ srcs = ["ecdsa_mbedtls.cc"],
deps = [
":ecdsa_facade",
+ "//pw_function",
"//pw_log",
- "@boringssl//:ssl",
+ "@mbedtls",
],
)
diff --git a/pw_crypto/BUILD.gn b/pw_crypto/BUILD.gn
index 919ab7bf0..3ac6fa5b4 100644
--- a/pw_crypto/BUILD.gn
+++ b/pw_crypto/BUILD.gn
@@ -84,8 +84,10 @@ pw_test_group("tests") {
":sha256_test",
":sha256_mock_test",
":ecdsa_test",
- ":ecdsa_uecc_little_endian_test",
]
+ if (dir_pw_third_party_micro_ecc != "") {
+ tests += [ ":ecdsa_uecc_little_endian_test" ]
+ }
}
# Sha256 tests against the selected real backend.
@@ -139,21 +141,16 @@ pw_source_set("sha256_mbedtls") {
]
}
-config("boringssl_config") {
- visibility = [ ":*" ]
- include_dirs = [ "public_overrides/boringssl" ]
-}
-
-pw_source_set("sha256_boringssl") {
- public_configs = [ ":boringssl_config" ]
+pw_source_set("sha256_mbedtls_v3") {
+ public_configs = [ ":mbedtls_config" ]
public = [
- "public/pw_crypto/sha256_boringssl.h",
- "public_overrides/boringssl/pw_crypto/sha256_backend.h",
+ "public/pw_crypto/sha256_mbedtls.h",
+ "public_overrides/mbedtls/pw_crypto/sha256_backend.h",
]
- sources = [ "sha256_boringssl.cc" ]
+ sources = [ "sha256_mbedtls.cc" ]
public_deps = [
":sha256.facade",
- "$dir_pw_third_party/boringssl",
+ "$dir_pw_third_party/mbedtls:mbedtls_v3",
]
}
@@ -177,11 +174,12 @@ pw_source_set("ecdsa_mbedtls") {
public_deps = [ ":ecdsa.facade" ]
}
-pw_source_set("ecdsa_boringssl") {
- sources = [ "ecdsa_boringssl.cc" ]
+pw_source_set("ecdsa_mbedtls_v3") {
+ sources = [ "ecdsa_mbedtls.cc" ]
deps = [
+ "$dir_pw_function",
"$dir_pw_log",
- "$dir_pw_third_party/boringssl",
+ "$dir_pw_third_party/mbedtls:mbedtls_v3",
]
public_deps = [ ":ecdsa.facade" ]
}
@@ -195,13 +193,23 @@ pw_source_set("ecdsa_uecc") {
public_deps = [ ":ecdsa.facade" ]
}
-pw_source_set("ecdsa_uecc_little_endian") {
- sources = [ "ecdsa_uecc.cc" ]
- deps = [
- "$dir_pw_log",
- "$dir_pw_third_party/micro_ecc:micro_ecc_little_endian",
- ]
- public_deps = [ ":ecdsa.facade" ]
+if (dir_pw_third_party_micro_ecc != "") {
+ pw_source_set("ecdsa_uecc_little_endian") {
+ sources = [ "ecdsa_uecc.cc" ]
+ deps = [
+ "$dir_pw_log",
+ "$dir_pw_third_party/micro_ecc:micro_ecc_little_endian",
+ ]
+ public_deps = [ ":ecdsa.facade" ]
+ }
+
+ # This test targets the micro_ecc little endian backend specifically.
+ #
+ # TODO: b/273819841 - deduplicate all backend tests.
+ pw_test("ecdsa_uecc_little_endian_test") {
+ sources = [ "ecdsa_test.cc" ]
+ deps = [ ":ecdsa_uecc_little_endian" ]
+ }
}
# This test targets the specific backend pointed to by
@@ -211,12 +219,3 @@ pw_test("ecdsa_test") {
deps = [ ":ecdsa" ]
sources = [ "ecdsa_test.cc" ]
}
-
-# This test targets the micro_ecc little endian backend specifically.
-#
-# TODO(b/273819841) deduplicate all backend tests.
-pw_test("ecdsa_uecc_little_endian_test") {
- enable_if = dir_pw_third_party_micro_ecc != ""
- sources = [ "ecdsa_test.cc" ]
- deps = [ ":ecdsa_uecc_little_endian" ]
-}
diff --git a/pw_crypto/docs.rst b/pw_crypto/docs.rst
index 1230cd35d..db3b0c2e3 100644
--- a/pw_crypto/docs.rst
+++ b/pw_crypto/docs.rst
@@ -1,5 +1,6 @@
.. _module-pw_crypto:
+=========
pw_crypto
=========
A set of safe (read: easy to use, hard to misuse) crypto APIs.
@@ -10,6 +11,7 @@ The following crypto services are provided by this module.
2. Verifying a digital signature signed with `ECDSA`_ over the NIST P256 curve.
3. Many more to come ...
+------
SHA256
------
@@ -43,6 +45,7 @@ SHA256
// Handle errors.
}
+-----
ECDSA
-----
@@ -82,6 +85,7 @@ ECDSA
// Handle errors.
}
+-------------
Configuration
-------------
@@ -89,25 +93,47 @@ The crypto services offered by pw_crypto can be backed by different backend
crypto libraries.
Mbed TLS
-^^^^^^^^
+========
+
+The `Mbed TLS project <https://www.trustedfirmware.org/projects/mbed-tls/>`_
+is a mature and full-featured crypto library that implements cryptographic
+primitives, X.509 certificate manipulation and the SSL/TLS and DTLS protocols.
+
+The project also has good support for interfacing to cryptographic accelerators.
+
+The small code footprint makes the project suitable and popular for embedded
+systems.
To select the Mbed TLS backend, the MbedTLS library needs to be installed and
-configured.
+configured. If using GN, do,
.. code-block:: sh
# Install and configure MbedTLS
pw package install mbedtls
gn gen out --args='
- import("//build_overrides/pigweed_environment.gni")
-
- dir_pw_third_party_mbedtls=pw_env_setup_PACKAGE_ROOT+"/mbedtls"
- pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_mbedtls"
- pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_mbedtls"
+ dir_pw_third_party_mbedtls=getenv("PW_PACKAGE_ROOT")+"/mbedtls"
+ pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_mbedtls_v3"
+ pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_mbedtls_v3"
'
ninja -C out
+If using Bazel, add the Mbed TLS repository to your WORKSPACE and select
+appropriate backends by adding them to your project's `platform
+<https://bazel.build/extending/platforms>`_:
+
+.. code-block:: python
+
+ platform(
+ name = "my_platform",
+ constraint_values = [
+ "@pigweed//pw_crypto:sha256_mbedtls_backend",
+ "@pigweed//pw_crypto:ecdsa_mbedtls_backend",
+ # ... other constraint_values
+ ],
+ )
+
For optimal code size and/or performance, the Mbed TLS library can be configured
per product. Mbed TLS configuration is achieved by turning on and off MBEDTLS_*
options in a config.h file. See //third_party/mbedtls for how this is done.
@@ -136,31 +162,8 @@ a code size of ~12KiB.
#define MBEDTLS_ECP_NO_INTERNAL_RNG
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
-BoringSSL
-^^^^^^^^^
-
-To select the BoringSSL backend, the BoringSSL library needs to be installed and
-configured.
-
-.. code-block:: sh
-
- # Install and configure BoringSSL
- pw package install boringssl
- gn gen out --args='
- import("//build_overrides/pigweed_environment.gni")
-
- dir_pw_third_party_boringssl=pw_env_setup_PACKAGE_ROOT+"/boringssl"
- pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_boringssl"
- pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_boringssl"
- '
-
- ninja -C out
-
-BoringSSL does not provide a public configuration interface to reduce the code
-size.
-
Micro ECC
-^^^^^^^^^
+=========
To select Micro ECC, the library needs to be installed and configured.
@@ -169,9 +172,7 @@ To select Micro ECC, the library needs to be installed and configured.
# Install and configure Micro ECC
pw package install micro-ecc
gn gen out --args='
- import("//build_overrides/pigweed_environment.gni")
-
- dir_pw_third_party_micro_ecc=pw_env_setup_PACKAGE_ROOT+"/micro-ecc"
+ dir_pw_third_party_micro_ecc=getenv("PW_PACKAGE_ROOT")+"/micro-ecc"
pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc"
'
@@ -183,6 +184,7 @@ can be selected with ``pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc_little_en
Note Micro-ECC does not implement any hashing functions, so you will need to use other backends for SHA256 functionality if needed.
+------------
Size Reports
------------
@@ -190,3 +192,14 @@ Below are size reports for each crypto service. These vary across
configurations.
.. include:: size_report
+
+-------------
+API reference
+-------------
+.. doxygenfunction:: pw::crypto::ecdsa::VerifyP256Signature(ConstByteSpan public_key, ConstByteSpan digest, ConstByteSpan signature)
+.. doxygenfunction:: pw::crypto::sha256::Hash(ConstByteSpan message, ByteSpan out_digest)
+.. doxygenfunction:: pw::crypto::sha256::Hash(stream::Reader& reader, ByteSpan out_digest)
+.. doxygenvariable:: pw::crypto::sha256::kDigestSizeBytes
+.. doxygenfunction:: pw::crypto::sha256::Sha256::Final(ByteSpan out_digest)
+.. doxygenfunction:: pw::crypto::sha256::Sha256::Update(ConstByteSpan data)
+.. doxygenenum:: pw::crypto::sha256::Sha256State
diff --git a/pw_crypto/ecdsa_boringssl.cc b/pw_crypto/ecdsa_boringssl.cc
deleted file mode 100644
index 6f87d9cfa..000000000
--- a/pw_crypto/ecdsa_boringssl.cc
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#define PW_LOG_MODULE_NAME "ECDSA-BSSL"
-#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
-
-#include "pw_preprocessor/compiler.h"
-
-PW_MODIFY_DIAGNOSTICS_PUSH();
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wcast-qual");
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wgnu-anonymous-struct");
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wnested-anon-types");
-#include "openssl/bn.h"
-#include "openssl/ec.h"
-#include "openssl/ecdsa.h"
-#include "openssl/nid.h"
-PW_MODIFY_DIAGNOSTICS_POP();
-
-#include "pw_crypto/ecdsa.h"
-#include "pw_log/log.h"
-
-namespace pw::crypto::ecdsa {
-
-constexpr size_t kP256CurveOrderBytes = 32;
-
-Status VerifyP256Signature(ConstByteSpan public_key,
- ConstByteSpan digest,
- ConstByteSpan signature) {
- const uint8_t* public_key_bytes =
- reinterpret_cast<const uint8_t*>(public_key.data());
- const uint8_t* digest_bytes = reinterpret_cast<const uint8_t*>(digest.data());
- const uint8_t* signature_bytes =
- reinterpret_cast<const uint8_t*>(signature.data());
-
- // Allocate objects needed for ECDSA verification. BoringSSL relies on
- // dynamic allocation.
- bssl::UniquePtr<EC_GROUP> group(
- EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1));
- if (!group) {
- return Status::ResourceExhausted();
- }
-
- bssl::UniquePtr<EC_POINT> pub_key(EC_POINT_new(group.get()));
- bssl::UniquePtr<EC_KEY> key(EC_KEY_new());
- bssl::UniquePtr<ECDSA_SIG> sig(ECDSA_SIG_new());
- if (!(pub_key && key && sig)) {
- return Status::ResourceExhausted();
- }
-
- // Load the public key.
- if (!EC_POINT_oct2point(group.get(),
- pub_key.get(),
- public_key_bytes,
- public_key.size(),
- nullptr)) {
- PW_LOG_DEBUG("Bad public key format");
- return Status::InvalidArgument();
- }
-
- if (!EC_KEY_set_group(key.get(), group.get())) {
- return Status::InvalidArgument();
- }
-
- if (!EC_KEY_set_public_key(key.get(), pub_key.get())) {
- return Status::InvalidArgument();
- }
-
- // Load the signature.
- if (signature.size() != kP256CurveOrderBytes * 2) {
- PW_LOG_DEBUG("Bad signature format");
- return Status::InvalidArgument();
- }
-
- if (!(BN_bin2bn(signature_bytes, kP256CurveOrderBytes, sig->r) &&
- BN_bin2bn(signature_bytes + kP256CurveOrderBytes,
- kP256CurveOrderBytes,
- sig->s))) {
- return Status::Internal();
- }
-
- // Digest must be 32 bytes or longer (and will be truncated).
- if (digest.size() < kP256CurveOrderBytes) {
- PW_LOG_DEBUG("Digest is too short");
- return Status::InvalidArgument();
- }
-
- // Verify the signature.
- if (!ECDSA_do_verify(digest_bytes, digest.size(), sig.get(), key.get())) {
- PW_LOG_DEBUG("Signature verification failed");
- return Status::Unauthenticated();
- }
-
- return OkStatus();
-}
-
-} // namespace pw::crypto::ecdsa
diff --git a/pw_crypto/public/pw_crypto/ecdsa.h b/pw_crypto/public/pw_crypto/ecdsa.h
index 3aa3766b4..38326db1b 100644
--- a/pw_crypto/public/pw_crypto/ecdsa.h
+++ b/pw_crypto/public/pw_crypto/ecdsa.h
@@ -19,18 +19,35 @@
namespace pw::crypto::ecdsa {
-// VerifyP256Signature verifies the `signature` of `digest` using `public_key`.
-//
-// `public_key` is a byte string in SEC 1 uncompressed form (0x04||X||Y), which
-// is exactly 65 bytes. Compressed forms (02/03||X) *may* not be supported
-// by some backends, e.g. Mbed TLS.
-//
-// `digest` is a raw byte string, truncated to 32 bytes.
-//
-// `signature` is a raw byte string (r||s) of exactly 64 bytes.
-//
-// Returns Status::OkStatus() for a successful verification, or an error Status
-// otherwise.
+/// Verifies the `signature` of `digest` using `public_key`.
+///
+/// Example:
+///
+/// @code{.cpp}
+/// #include "pw_crypto/sha256.h"
+///
+/// // Verify a digital signature signed with ECDSA over the NIST P256 curve.
+/// std::byte digest[32];
+/// if (!pw::crypto::sha256::Hash(message, digest).ok()) {
+/// // handle errors.
+/// }
+///
+/// if (!pw::crypto::ecdsa::VerifyP256Signature(public_key, digest,
+/// signature).ok()) {
+/// // handle errors.
+/// }
+/// @endcode
+///
+/// @param[in] public_key A byte string in SEC 1 uncompressed form
+/// ``(0x04||X||Y)``, which is exactly 65 bytes. Compressed forms
+/// ``(02/03||X)`` *may* not be supported by some backends, e.g. Mbed TLS.
+///
+/// @param[in] digest A raw byte string, truncated to 32 bytes.
+///
+/// @param[in] signature A raw byte string ``(r||s)`` of exactly 64 bytes.
+///
+/// @returns @pw_status{OK} for a successful verification, or an error
+/// ``Status`` otherwise.
Status VerifyP256Signature(ConstByteSpan public_key,
ConstByteSpan digest,
ConstByteSpan signature);
diff --git a/pw_crypto/public/pw_crypto/sha256.h b/pw_crypto/public/pw_crypto/sha256.h
index 1389e28df..d2e7d97a3 100644
--- a/pw_crypto/public/pw_crypto/sha256.h
+++ b/pw_crypto/public/pw_crypto/sha256.h
@@ -25,19 +25,19 @@
namespace pw::crypto::sha256 {
-// Size in bytes of a SHA256 digest.
+/// The size of a SHA256 digest in bytes.
constexpr uint32_t kDigestSizeBytes = 32;
-// State machine of a hashing session.
+/// A state machine of a hashing session.
enum class Sha256State {
- // Initialized and accepting input (via Update()).
+ /// Initialized and accepting input (via `Update()`).
kReady = 1,
- // Finalized by Final(). Any additional requests, Update() or Final(), will
- // trigger a transition to kError.
+ /// Finalized by `Final()`. Any additional requests to `Update()` or `Final()`
+ /// will trigger a transition to `kError`.
kFinalized = 2,
- // In an unrecoverable error state.
+ /// In an unrecoverable error state.
kError = 3,
};
@@ -50,14 +50,16 @@ Status DoFinal(NativeSha256Context& ctx, ByteSpan out_digest);
} // namespace backend
-// Sha256 computes the SHA256 digest of potentially long, non-contiguous input
-// messages.
-//
-// Usage:
-//
-// if (!Sha256().Update(message).Update(more_message).Final(out_digest).ok()) {
-// // Error handling.
-// }
+/// Computes the SHA256 digest of potentially long, non-contiguous input
+/// messages.
+///
+/// Usage:
+///
+/// @code{.cpp}
+/// if (!Sha256().Update(message).Update(more_message).Final(out_digest).ok()) {
+/// // Error handling.
+/// }
+/// @endcode
class Sha256 {
public:
Sha256() {
@@ -70,8 +72,8 @@ class Sha256 {
state_ = Sha256State::kReady;
}
- // Update feeds `data` to the running hasher. The feeding can involve zero
- // or more `Update()` calls and the order matters.
+ /// Feeds `data` to the running hasher. The feeding can involve zero
+ /// or more `Update()` calls and the order matters.
Sha256& Update(ConstByteSpan data) {
if (state_ != Sha256State::kReady) {
PW_LOG_DEBUG("The backend is not ready/initialized");
@@ -87,14 +89,14 @@ class Sha256 {
return *this;
}
- // Final wraps up the hashing session and outputs the final digest in the
- // first `kDigestSizeBytes` of `out_digest`. `out_digest` must be at least
- // `kDigestSizeBytes` long.
- //
- // Final locks down the Sha256 instance from any additional use.
- //
- // Any error, including those occurr inside `Init()` or `Update()` will be
- // reflected in the return value of Final();
+ /// Finishes the hashing session and outputs the final digest in the
+ /// first `kDigestSizeBytes` of `out_digest`. `out_digest` must be at least
+ /// `kDigestSizeBytes` long.
+ ///
+ /// `Final()` locks down the `Sha256` instance from any additional use.
+ ///
+ /// Any error, including those occurring inside the constructor or `Update()`
+ /// will be reflected in the return value of `Final()`.
Status Final(ByteSpan out_digest) {
if (out_digest.size() < kDigestSizeBytes) {
PW_LOG_DEBUG("Digest output buffer is too small");
@@ -125,8 +127,38 @@ class Sha256 {
backend::NativeSha256Context native_ctx_;
};
-// Hash calculates the SHA256 digest of `message` and stores the result
-// in `out_digest`. `out_digest` must be at least `kDigestSizeBytes` long.
+/// Calculates the SHA256 digest of `message` and stores the result
+/// in `out_digest`. `out_digest` must be at least `kDigestSizeBytes` long.
+///
+/// One-shot digest example:
+///
+/// @code{.cpp}
+/// #include "pw_crypto/sha256.h"
+///
+/// std::byte digest[32];
+/// if (!pw::crypto::sha256::Hash(message, digest).ok()) {
+/// // Handle errors.
+/// }
+///
+/// // The content can also come from a pw::stream::Reader.
+/// if (!pw::crypto::sha256::Hash(reader, digest).ok()) {
+/// // Handle errors.
+/// }
+/// @endcode
+///
+/// Long, potentially non-contiguous message example:
+///
+/// @code{.cpp}
+/// #include "pw_crypto/sha256.h"
+///
+/// std::byte digest[32];
+///
+/// if (!pw::crypto::sha256::Sha256()
+/// .Update(chunk1).Update(chunk2).Update(chunk...)
+/// .Final().ok()) {
+/// // Handle errors.
+/// }
+/// @endcode
inline Status Hash(ConstByteSpan message, ByteSpan out_digest) {
return Sha256().Update(message).Final(out_digest);
}
diff --git a/pw_crypto/public/pw_crypto/sha256_boringssl.h b/pw_crypto/public/pw_crypto/sha256_boringssl.h
deleted file mode 100644
index b157c8ba3..000000000
--- a/pw_crypto/public/pw_crypto/sha256_boringssl.h
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-#pragma once
-
-#include "pw_preprocessor/compiler.h"
-
-PW_MODIFY_DIAGNOSTICS_PUSH();
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wcast-qual");
-// "-Wpedantic" because some downstream compiler don't recognize the following
-// two commented-out options.
-// PW_MODIFY_DIAGNOSTIC(ignored, "-Wgnu-anonymous-struct");
-// PW_MODIFY_DIAGNOSTIC(ignored, "-Wnested-anon-types");
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wpedantic");
-#include "openssl/sha.h"
-PW_MODIFY_DIAGNOSTICS_POP();
-
-namespace pw::crypto::sha256::backend {
-
-typedef SHA256_CTX NativeSha256Context;
-
-} // namespace pw::crypto::sha256::backend
diff --git a/pw_crypto/public/pw_crypto/sha256_mbedtls.h b/pw_crypto/public/pw_crypto/sha256_mbedtls.h
index 63c017c4a..9e063d8f5 100644
--- a/pw_crypto/public/pw_crypto/sha256_mbedtls.h
+++ b/pw_crypto/public/pw_crypto/sha256_mbedtls.h
@@ -15,6 +15,13 @@
#pragma once
#include "mbedtls/sha256.h"
+#include "mbedtls/version.h"
+
+#if MBEDTLS_VERSION_MAJOR < 3
+#define mbedtls_sha256_starts mbedtls_sha256_starts_ret
+#define mbedtls_sha256_update mbedtls_sha256_update_ret
+#define mbedtls_sha256_finish mbedtls_sha256_finish_ret
+#endif
namespace pw::crypto::sha256::backend {
diff --git a/pw_crypto/public_overrides/mbedtls/pw_crypto/sha256_backend.h b/pw_crypto/public_overrides/mbedtls/pw_crypto/sha256_backend.h
index adff812d7..31466ec7e 100644
--- a/pw_crypto/public_overrides/mbedtls/pw_crypto/sha256_backend.h
+++ b/pw_crypto/public_overrides/mbedtls/pw_crypto/sha256_backend.h
@@ -14,4 +14,4 @@
#pragma once
-#include "pw_crypto/sha256_mbedtls.h" \ No newline at end of file
+#include "pw_crypto/sha256_mbedtls.h"
diff --git a/pw_crypto/sha256_boringssl.cc b/pw_crypto/sha256_boringssl.cc
deleted file mode 100644
index fb9c783d1..000000000
--- a/pw_crypto/sha256_boringssl.cc
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#define PW_LOG_MODULE_NAME "SHA256-BSSL"
-#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
-
-#include "pw_crypto/sha256.h"
-#include "pw_status/status.h"
-
-namespace pw::crypto::sha256::backend {
-
-Status DoInit(NativeSha256Context& ctx) {
- if (!SHA256_Init(&ctx)) {
- return Status::Internal();
- }
- return OkStatus();
-}
-
-Status DoUpdate(NativeSha256Context& ctx, ConstByteSpan data) {
- if (!SHA256_Update(&ctx,
- reinterpret_cast<const unsigned char*>(data.data()),
- data.size())) {
- return Status::Internal();
- }
-
- return OkStatus();
-}
-
-Status DoFinal(NativeSha256Context& ctx, ByteSpan out_digest) {
- if (!SHA256_Final(reinterpret_cast<unsigned char*>(out_digest.data()),
- &ctx)) {
- return Status::Internal();
- }
-
- return OkStatus();
-}
-
-} // namespace pw::crypto::sha256::backend
diff --git a/pw_crypto/sha256_mbedtls.cc b/pw_crypto/sha256_mbedtls.cc
index 8cb6595ba..8e09248e1 100644
--- a/pw_crypto/sha256_mbedtls.cc
+++ b/pw_crypto/sha256_mbedtls.cc
@@ -23,7 +23,7 @@ Status DoInit(NativeSha256Context& ctx) {
// mbedtsl_sha256_init() never fails (returns void).
mbedtls_sha256_init(&ctx);
- if (mbedtls_sha256_starts_ret(&ctx, /* is224 = */ 0)) {
+ if (mbedtls_sha256_starts(&ctx, /* is224 = */ 0)) {
return Status::Internal();
}
@@ -31,10 +31,9 @@ Status DoInit(NativeSha256Context& ctx) {
}
Status DoUpdate(NativeSha256Context& ctx, ConstByteSpan data) {
- if (mbedtls_sha256_update_ret(
- &ctx,
- reinterpret_cast<const unsigned char*>(data.data()),
- data.size())) {
+ if (mbedtls_sha256_update(&ctx,
+ reinterpret_cast<const unsigned char*>(data.data()),
+ data.size())) {
return Status::Internal();
}
@@ -42,7 +41,7 @@ Status DoUpdate(NativeSha256Context& ctx, ConstByteSpan data) {
}
Status DoFinal(NativeSha256Context& ctx, ByteSpan out_digest) {
- if (mbedtls_sha256_finish_ret(
+ if (mbedtls_sha256_finish(
&ctx, reinterpret_cast<unsigned char*>(out_digest.data()))) {
return Status::Internal();
}
diff --git a/pw_digital_io/Android.bp b/pw_digital_io/Android.bp
new file mode 100644
index 000000000..25a34ef6a
--- /dev/null
+++ b/pw_digital_io/Android.bp
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+genrule {
+ name: "pw_digital_io_proto_with_prefix",
+ defaults: ["pw_rpc_add_prefix_to_proto"],
+ srcs: [ "digital_io.proto" ],
+ out: [ "pw_digital_io/digital_io.proto" ],
+}
+
+genrule {
+ name: "pw_digital_io_pwpb_rpc_header",
+ defaults: ["pw_rpc_generate_pwpb_rpc_header_with_prefix"],
+ srcs: [":pw_digital_io_proto_with_prefix"],
+ out: ["pw_digital_io/digital_io.rpc.pwpb.h"],
+}
+
+genrule {
+ name: "pw_digital_io_pwpb_proto_header",
+ defaults: ["pw_rpc_generate_pwpb_proto_with_prefix"],
+ srcs: [":pw_digital_io_proto_with_prefix"],
+ out: ["pw_digital_io/digital_io.pwpb.h"],
+}
+
+cc_library_headers {
+ name: "pw_digital_io_service_pwpb_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ generated_headers: [
+ "pw_digital_io_pwpb_proto_header",
+ "pw_digital_io_pwpb_rpc_header",
+ ],
+ export_generated_headers: [
+ "pw_digital_io_pwpb_proto_header",
+ "pw_digital_io_pwpb_rpc_header",
+ ],
+}
diff --git a/pw_digital_io/BUILD.bazel b/pw_digital_io/BUILD.bazel
index a4632a5e9..5ccd45e5e 100644
--- a/pw_digital_io/BUILD.bazel
+++ b/pw_digital_io/BUILD.bazel
@@ -17,6 +17,10 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load(
+ "//pw_protobuf_compiler:pw_proto_library.bzl",
+ "pw_proto_library",
+)
package(default_visibility = ["//visibility:public"])
@@ -46,3 +50,34 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+pw_cc_library(
+ name = "digital_io_controller",
+ hdrs = ["public/pw_digital_io/digital_io_controller.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
+ name = "digital_io_service",
+ srcs = ["digital_io_service.cc"],
+ hdrs = ["public/pw_digital_io/digital_io_service.h"],
+ includes = ["public"],
+ deps = [
+ ":digital_io_proto_cc.pwpb",
+ ":digital_io_proto_cc.pwpb_rpc",
+ ":pw_digital_io",
+ "//pw_function",
+ "//pw_rpc/pwpb:server_api",
+ "//pw_span",
+ ],
+)
+
+proto_library(
+ name = "digital_io_proto",
+ srcs = ["digital_io.proto"],
+)
+
+pw_proto_library(
+ name = "digital_io_proto_cc",
+ deps = [":digital_io_proto"],
+)
diff --git a/pw_digital_io/BUILD.gn b/pw_digital_io/BUILD.gn
index 2cf894932..b56492f00 100644
--- a/pw_digital_io/BUILD.gn
+++ b/pw_digital_io/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_unit_test/test.gni")
@@ -39,6 +40,21 @@ pw_source_set("pw_digital_io") {
]
}
+pw_source_set("digital_io_service") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_digital_io/digital_io_service.h" ]
+ sources = [ "digital_io_service.cc" ]
+ public_deps = [
+ ":protos.pwpb_rpc",
+ ":pw_digital_io",
+ ]
+}
+
+pw_proto_library("protos") {
+ sources = [ "digital_io.proto" ]
+ prefix = "pw_digital_io"
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_digital_io/CMakeLists.txt b/pw_digital_io/CMakeLists.txt
index 8c1dbe181..bb8b4469b 100644
--- a/pw_digital_io/CMakeLists.txt
+++ b/pw_digital_io/CMakeLists.txt
@@ -28,9 +28,6 @@ pw_add_library(pw_digital_io STATIC
pw_result
pw_status
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_DIGITAL_IO)
- zephyr_link_libraries(pw_digital_io)
-endif()
pw_add_test(pw_digital_io.stream_test
SOURCES
diff --git a/pw_digital_io/digital_io.proto b/pw_digital_io/digital_io.proto
new file mode 100644
index 000000000..0900be6ad
--- /dev/null
+++ b/pw_digital_io/digital_io.proto
@@ -0,0 +1,55 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+syntax = "proto3";
+
+package pw.digital_io;
+
+enum DigitalIoState {
+ INACTIVE = 0;
+ ACTIVE = 1;
+}
+
+message DigitalIoEnableRequest {
+ // Which line to select.
+ uint32 line_index = 1;
+ // Whether to enable or disable.
+ bool enable = 2;
+}
+
+message DigitalIoEnableResponse {}
+
+message DigitalIoSetStateRequest {
+ // Which line to select.
+ uint32 line_index = 1;
+ // Which state to set to.
+ DigitalIoState state = 2;
+}
+
+message DigitalIoSetStateResponse {}
+
+message DigitalIoGetStateRequest {
+ // Which line to select.
+ uint32 line_index = 1;
+}
+
+message DigitalIoGetStateResponse {
+ DigitalIoState state = 1;
+}
+
+service DigitalIo {
+ rpc Enable(DigitalIoEnableRequest) returns (DigitalIoEnableResponse) {}
+ rpc SetState(DigitalIoSetStateRequest) returns (DigitalIoSetStateResponse) {}
+ rpc GetState(DigitalIoGetStateRequest) returns (DigitalIoGetStateResponse) {}
+}
diff --git a/pw_digital_io/digital_io_service.cc b/pw_digital_io/digital_io_service.cc
new file mode 100644
index 000000000..8a7ea3676
--- /dev/null
+++ b/pw_digital_io/digital_io_service.cc
@@ -0,0 +1,104 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#define PW_LOG_MODULE_NAME "IO"
+
+#include "pw_digital_io/digital_io_service.h"
+
+#include "pw_digital_io/digital_io.pwpb.h"
+#include "pw_log/log.h"
+#include "pw_result/result.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+
+void DigitalIoService::Enable(
+ const pwpb::DigitalIoEnableRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::DigitalIoEnableResponse::Message>&
+ responder) {
+ Status result = OkStatus();
+ if (request.line_index >= lines_.size()) {
+ result = Status::InvalidArgument();
+ } else {
+ auto& line = lines_[request.line_index].get();
+ if (request.enable) {
+ result = line.Enable();
+ } else {
+ result = line.Disable();
+ }
+ }
+
+ if (const auto finished = responder.Finish({}, result); !finished.ok()) {
+ PW_LOG_ERROR("Enable failed to send response %d", finished.code());
+ }
+}
+
+void DigitalIoService::SetState(
+ const pwpb::DigitalIoSetStateRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::DigitalIoSetStateResponse::Message>&
+ responder) {
+ Status result = OkStatus();
+ if (request.line_index >= lines_.size()) {
+ result = Status::InvalidArgument();
+ } else {
+ auto& line = lines_[request.line_index].get();
+ if (!line.provides_output()) {
+ result = Status::InvalidArgument();
+ } else {
+ if (request.state == pwpb::DigitalIoState::kActive) {
+ result = line.SetStateActive();
+ } else {
+ result = line.SetStateInactive();
+ }
+ }
+ }
+
+ if (const auto finished = responder.Finish({}, result); !finished.ok()) {
+ PW_LOG_ERROR("SetState failed to send response %d", finished.code());
+ }
+}
+
+void DigitalIoService::GetState(
+ const pwpb::DigitalIoGetStateRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::DigitalIoGetStateResponse::Message>&
+ responder) {
+ pw::Result<State> result;
+ if (request.line_index >= lines_.size()) {
+ result = pw::Status::InvalidArgument();
+ } else {
+ auto& line = lines_[request.line_index].get();
+ if (!line.provides_input()) {
+ result = pw::Status::InvalidArgument();
+ } else {
+ result = line.GetState();
+ }
+ }
+
+ auto finished = pw::OkStatus();
+ if (result.ok()) {
+ pwpb::DigitalIoState state = *result == State::kActive
+ ? pwpb::DigitalIoState::kActive
+ : pwpb::DigitalIoState::kInactive;
+ finished = responder.Finish({.state = state}, OkStatus());
+ } else {
+ finished = responder.Finish({}, result.status());
+ }
+
+ if (!finished.ok()) {
+ PW_LOG_ERROR("GetState failed to send response %d", finished.code());
+ }
+}
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io/docs.rst b/pw_digital_io/docs.rst
index 46f3ba077..1d35468d9 100644
--- a/pw_digital_io/docs.rst
+++ b/pw_digital_io/docs.rst
@@ -58,7 +58,8 @@ There are 3 basic capabilities of a Digital IO line:
Additionally, all lines can be *enabled* and *disabled*:
* Enable - tell the hardware to apply power to an output line, connect any
- pull-up/down resistors, etc.
+ pull-up/down resistors, etc. For output lines, the line is set to an initial
+ output state that is backend-specific.
* Disable - tell the hardware to stop applying power and return the line to its
default state. This may save power or allow some other component to drive a
shared line.
@@ -266,7 +267,46 @@ Backend Implemention Notes
state. i.e. the same state it would be in after calling ``Enable()``
followed by ``Disable()``.
* Calling ``Disable()`` on an uninitialized line must put it into the disabled
- state.
+ state. In general, ``Disable()`` can be called in any state.
+
+* Calling ``Enable()`` on a line that is already enabled should be a no-op. In
+ particular, the state of an already-enabled output line should not change.
+
+-----------
+RPC Service
+-----------
+The ``DigitalIoService`` pw_rpc service is provided to support bringup/debug
+efforts. It allows manual control of individual DigitalIo lines for both input
+and output.
+
+.. code-block:: cpp
+
+ std::array<std::reference_wrapper<DigitalIoOptional>> lines = {
+ ...DigitalIn(),
+ ...DigitalOut(),
+ };
+ DigitalIoService service(lines);
+ rpc_server.RegisterService(service);
+
+Set the state of the output line via RPC. This snippet demonstrates how you
+might do that using a Pigweed console device object.
+
+.. code-block:: python
+
+ from pw_digital_io import digital_io_pb2
+
+ device.rpcs.pw.digital_io.DigitalIo.SetState(
+ line_index=0, state=digital_io_pb2.DigitalIoState.ACTIVE)
+
+
+-------------
+API reference
+-------------
+.. note::
+ This API reference is incomplete.
+
+.. doxygenclass:: pw::digital_io::DigitalIoOptional
+ :members:
------------
Dependencies
diff --git a/pw_digital_io/public/pw_digital_io/digital_io.h b/pw_digital_io/public/pw_digital_io/digital_io.h
index ba8516b01..8855f019b 100644
--- a/pw_digital_io/public/pw_digital_io/digital_io.h
+++ b/pw_digital_io/public/pw_digital_io/digital_io.h
@@ -42,136 +42,135 @@ enum class InterruptTrigger : int {
// the line. It is backend-specific if, when, and how this state is updated.
using InterruptHandler = ::pw::Function<void(State sampled_state)>;
-// A digital I/O line that may support input, output, and interrupts, but makes
-// no guarantees about whether any operations are supported. You must check the
-// various provides_* flags before calling optional methods. Unsupported methods
-// invoke PW_CRASH.
-//
-// All methods are potentially blocking. Unless otherwise specified, access from
-// multiple threads to a single line must be externally synchronized - for
-// example using pw::Borrowable. Unless otherwise specified, none of the methods
-// are safe to call from an interrupt handler. Therefore, this abstraction may
-// not be suitable for bitbanging and other low-level uses of GPIO.
-//
-// Note that the initial state of a line is not guaranteed to be consistent with
-// either the "enabled" or "disabled" state. Users of the API who need to ensure
-// the line is disabled (ex. output not driving the line) should call Disable.
-//
-// This class should almost never be used in APIs directly. Instead, use one of
-// the derived classes that explicitly supports the functionality that your
-// API needs.
-//
-// This class cannot be extended directly. Instead, extend one of the
-// derived classes that explicitly support the functionality that you want to
-// implement.
-//
+/// A digital I/O line that may support input, output, and interrupts, but makes
+/// no guarantees about whether any operations are supported. You must check the
+/// various `provides_*` flags before calling optional methods. Unsupported
+/// methods invoke `PW_CRASH`.
+///
+/// All methods are potentially blocking. Unless otherwise specified, access
+/// from multiple threads to a single line must be externally synchronized - for
+/// example using `pw::Borrowable`. Unless otherwise specified, none of the
+/// methods are safe to call from an interrupt handler. Therefore, this
+/// abstraction may not be suitable for bitbanging and other low-level uses of
+/// GPIO.
+///
+/// Note that the initial state of a line is not guaranteed to be consistent
+/// with either the "enabled" or "disabled" state. Users of the API who need to
+/// ensure the line is disabled (ex. output not driving the line) should call
+/// `Disable()`.
+///
+/// This class should almost never be used in APIs directly. Instead, use one of
+/// the derived classes that explicitly supports the functionality that your
+/// API needs.
+///
+/// This class cannot be extended directly. Instead, extend one of the
+/// derived classes that explicitly support the functionality that you want to
+/// implement.
class DigitalIoOptional {
public:
virtual ~DigitalIoOptional() = default;
- // True if input (getting state) is supported.
+ /// @returns `true` if input (getting state) is supported.
constexpr bool provides_input() const { return config_.input; }
- // True if output (setting state) is supported.
+ /// @returns `true` if output (setting state) is supported.
constexpr bool provides_output() const { return config_.output; }
- // True if interrupt handlers can be registered.
+ /// @returns `true` if interrupt handlers can be registered.
constexpr bool provides_interrupt() const { return config_.interrupt; }
- // Get the state of the line.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- //
- // OK - an active or inactive state.
- // FAILED_PRECONDITION - The line has not been enabled.
- // Other status codes as defined by the backend.
- //
+ /// Gets the state of the line.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - An active or inactive state.
+ /// * @pw_status{FAILED_PRECONDITION} - The line has not been enabled.
+ /// * Other status codes as defined by the backend.
Result<State> GetState() { return DoGetState(); }
- // Set the state of the line.
- //
- // Callers are responsible to wait for the voltage level to settle after this
- // call returns.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- //
- // OK - the state has been set.
- // FAILED_PRECONDITION - The line has not been enabled.
- // Other status codes as defined by the backend.
- //
+ /// Sets the state of the line.
+ ///
+ /// Callers are responsible to wait for the voltage level to settle after this
+ /// call returns.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The state has been set.
+ /// * @pw_status{FAILED_PRECONDITION} - The line has not been enabled.
+ /// * Other status codes as defined by the backend.
Status SetState(State state) { return DoSetState(state); }
- // Check if the line is in the active state.
- //
- // The line is in the active state when GetState() returns State::kActive.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- //
- // OK - true if the line is in the active state, otherwise false.
- // FAILED_PRECONDITION - The line has not been enabled.
- // Other status codes as defined by the backend.
- //
+ /// Checks if the line is in the active state.
+ ///
+ /// The line is in the active state when `GetState()` returns
+ /// `State::kActive`.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - `true` if the line is in the active state, otherwise
+ /// `false`.
+ /// * @pw_status{FAILED_PRECONDITION} - The line has not been enabled.
+ /// * Other status codes as defined by the backend.
Result<bool> IsStateActive() {
PW_TRY_ASSIGN(const State state, GetState());
return state == State::kActive;
}
- // Sets the line to the active state. Equivalent to SetState(State::kActive).
- //
- // Callers are responsible to wait for the voltage level to settle after this
- // call returns.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- //
- // OK - the state has been set.
- // FAILED_PRECONDITION - The line has not been enabled.
- // Other status codes as defined by the backend.
- //
+ /// Sets the line to the active state. Equivalent to
+ /// `SetState(State::kActive)`.
+ ///
+ /// Callers are responsible to wait for the voltage level to settle after this
+ /// call returns.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The state has been set.
+ /// * @pw_status{FAILED_PRECONDITION} - The line has not been enabled.
+ /// * Other status codes as defined by the backend.
Status SetStateActive() { return SetState(State::kActive); }
- // Sets the line to the inactive state. Equivalent to
- // SetState(State::kInactive).
- //
- // Callers are responsible to wait for the voltage level to settle after this
- // call returns.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- //
- // OK - the state has been set.
- // FAILED_PRECONDITION - The line has not been enabled.
- // Other status codes as defined by the backend.
- //
+ /// Sets the line to the inactive state. Equivalent to
+ /// `SetState(State::kInactive)`.
+ ///
+ /// Callers are responsible to wait for the voltage level to settle after
+ /// this call returns.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The state has been set.
+ /// * @pw_status{FAILED_PRECONDITION} - The line has not been enabled.
+ /// * Other status codes as defined by the backend.
Status SetStateInactive() { return SetState(State::kInactive); }
- // Set an interrupt handler to execute when an interrupt is triggered, and
- // Configure the condition for triggering the interrupt.
- //
- // The handler is executed in a backend-specific context - this may be a
- // system interrupt handler or a shared notification thread. Do not do any
- // blocking or expensive work in the handler. The only universally safe
- // operations are the IRQ-safe functions on pw_sync primitives.
- //
- // In particular, it is NOT safe to get the state of a DigitalIo line - either
- // from this line or any other DigitalIoOptional instance - inside the
- // handler.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Precondition: no handler is currently set.
- //
- // Returns:
- // OK - the interrupt handler was configured.
- // INVALID_ARGUMENT - handler is empty.
- // Other status codes as defined by the backend.
- //
+ /// Sets an interrupt handler to execute when an interrupt is triggered, and
+ /// configures the condition for triggering the interrupt.
+ ///
+ /// The handler is executed in a backend-specific context—this may be a
+ /// system interrupt handler or a shared notification thread. Do not do any
+ /// blocking or expensive work in the handler. The only universally safe
+ /// operations are the IRQ-safe functions on `pw_sync` primitives.
+ ///
+ /// In particular, it is NOT safe to get the state of a `DigitalIo`
+ /// line—either from this line or any other `DigitalIoOptional`
+ /// instance—inside the handler.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @pre No handler is currently set.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The interrupt handler was configured.
+ /// * @pw_status{INVALID_ARGUMENT} - The handler is empty.
+ /// * Other status codes as defined by the backend.
Status SetInterruptHandler(InterruptTrigger trigger,
InterruptHandler&& handler) {
if (handler == nullptr) {
@@ -180,70 +179,73 @@ class DigitalIoOptional {
return DoSetInterruptHandler(trigger, std::move(handler));
}
- // Clear the interrupt handler and disable interrupts if enabled.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- // OK - the itnerrupt handler was cleared.
- // Other status codes as defined by the backend.
- //
+ /// Clears the interrupt handler and disables any existing interrupts that
+ /// are enabled.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The interrupt handler was cleared.
+ /// * Other status codes as defined by the backend.
Status ClearInterruptHandler() {
return DoSetInterruptHandler(InterruptTrigger::kActivatingEdge, nullptr);
}
- // Enable interrupts which will trigger the interrupt handler.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Precondition: a handler has been set using SetInterruptHandler.
- //
- // Returns:
- // OK - the interrupt handler was configured.
- // FAILED_PRECONDITION - The line has not been enabled.
- // Other status codes as defined by the backend.
- //
+ /// Enables interrupts which will trigger the interrupt handler.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @pre A handler has been set using `SetInterruptHandler()`.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The interrupt handler was configured.
+ /// * @pw_status{FAILED_PRECONDITION} - The line has not been enabled.
+ /// * Other status codes as defined by the backend.
Status EnableInterruptHandler() { return DoEnableInterruptHandler(true); }
- // Disable the interrupt handler. This is a no-op if interrupts are disabled.
- //
- // This method can be called inside the interrupt handler for this line
- // without any external synchronization. However, the exact behavior is
- // backend-specific. There may be queued events that will trigger the handler
- // again after this call returns.
- //
- // Returns:
- // OK - the interrupt handler was configured.
- // Other status codes as defined by the backend.
- //
+ /// Disables the interrupt handler. This is a no-op if interrupts are
+ /// disabled.
+ ///
+ /// This method can be called inside the interrupt handler for this line
+ /// without any external synchronization. However, the exact behavior is
+ /// backend-specific. There may be queued events that will trigger the handler
+ /// again after this call returns.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The interrupt handler was disabled.
+ /// * Other status codes as defined by the backend.
Status DisableInterruptHandler() { return DoEnableInterruptHandler(false); }
- // Enable the line to initialize it into the default state as determined by
- // the backend. This may enable pull-up/down resistors, drive the line high or
- // low, etc. The line must be enabled before getting/setting the state
- // or enabling interrupts.
- //
- // Callers are responsible to wait for the voltage level to settle after this
- // call returns.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- // OK - the line is enabled and ready for use.
- // Other status codes as defined by the backend.
- //
+ /// Enables the line to initialize it into the default state as determined by
+ /// the backend.
+ ///
+ /// This may enable pull-up/down resistors, drive the line high/low, etc.
+ /// The line must be enabled before getting/setting the state or enabling
+ /// interrupts. Callers are responsible for waiting for the voltage level to
+ /// settle after this call returns.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The line is enabled and ready for use.
+ /// * Other status codes as defined by the backend.
Status Enable() { return DoEnable(true); }
- // Disable the line to power down any pull-up/down resistors and disconnect
- // from any voltage sources. This is usually done to save power. Interrupt
- // handlers are automatically disabled.
- //
- // This method is not thread-safe and cannot be used in interrupt handlers.
- //
- // Returns:
- // OK - the line is disabled.
- // Other status codes as defined by the backend.
- //
+ /// Disables the line to power down any pull-up/down resistors and disconnect
+ /// from any voltage sources.
+ ///
+ /// This is usually done to save power. Interrupt handlers are automatically
+ /// disabled.
+ ///
+ /// @warning This method is not thread-safe and cannot be used in interrupt
+ /// handlers.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The line is disabled.
+ /// * Other status codes as defined by the backend.
Status Disable() { return DoEnable(false); }
private:
diff --git a/pw_digital_io/public/pw_digital_io/digital_io_service.h b/pw_digital_io/public/pw_digital_io/digital_io_service.h
new file mode 100644
index 000000000..e243839c2
--- /dev/null
+++ b/pw_digital_io/public/pw_digital_io/digital_io_service.h
@@ -0,0 +1,54 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "pw_digital_io/digital_io.h"
+#include "pw_digital_io/digital_io.pwpb.h"
+#include "pw_digital_io/digital_io.rpc.pwpb.h"
+#include "pw_function/function.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_span/span.h"
+
+namespace pw::digital_io {
+
+// Implementation class for pw.digital_io.DigitalIo.
+//
+// Takes an array of DigitalIoOptional lines to be exposed via the service.
+class DigitalIoService
+ : public pw_rpc::pwpb::DigitalIo::Service<DigitalIoService> {
+ public:
+ explicit DigitalIoService(
+ pw::span<std::reference_wrapper<DigitalIoOptional>> lines)
+ : lines_(lines) {}
+
+ void Enable(const pwpb::DigitalIoEnableRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::DigitalIoEnableResponse::Message>&
+ responder);
+
+ void SetState(
+ const pwpb::DigitalIoSetStateRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::DigitalIoSetStateResponse::Message>&
+ responder);
+
+ void GetState(
+ const pwpb::DigitalIoGetStateRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::DigitalIoGetStateResponse::Message>&
+ responder);
+
+ private:
+ pw::span<std::reference_wrapper<DigitalIoOptional>> lines_;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_mcuxpresso/BUILD.bazel b/pw_digital_io_mcuxpresso/BUILD.bazel
new file mode 100644
index 000000000..184270157
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/BUILD.bazel
@@ -0,0 +1,50 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_digital_io_mcuxpresso",
+ srcs = [
+ "digital_io.cc",
+ "interrupt_controller.cc",
+ "interrupt_line.cc",
+ ],
+ hdrs = [
+ "public/pw_digital_io_mcuxpresso/digital_io.h",
+ "public/pw_digital_io_mcuxpresso/interrupt_controller.h",
+ "public/pw_digital_io_mcuxpresso/interrupt_line.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_digital_io",
+ "//pw_preprocessor",
+ "//pw_sync:borrow",
+ "@pigweed//targets:mcuxpresso_sdk",
+ ],
+)
+
+pw_cc_test(
+ name = "mimxrt595_test",
+ srcs = ["mimxrt595_test.cc"],
+ deps = [":pw_digital_io_mcuxpresso"],
+)
diff --git a/pw_digital_io_mcuxpresso/BUILD.gn b/pw_digital_io_mcuxpresso/BUILD.gn
new file mode 100644
index 000000000..261226e21
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/BUILD.gn
@@ -0,0 +1,70 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+if (pw_third_party_mcuxpresso_SDK != "") {
+ pw_source_set("pw_digital_io_mcuxpresso") {
+ public_configs = [ ":default_config" ]
+ public = [
+ "public/pw_digital_io_mcuxpresso/digital_io.h",
+ "public/pw_digital_io_mcuxpresso/interrupt_controller.h",
+ "public/pw_digital_io_mcuxpresso/interrupt_line.h",
+ ]
+ public_deps = [
+ "$dir_pw_digital_io",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_sync:borrow",
+ "$pw_third_party_mcuxpresso_SDK",
+ ]
+ deps = [ "$dir_pw_assert" ]
+ sources = [
+ "digital_io.cc",
+ "interrupt_controller.cc",
+ "interrupt_line.cc",
+ ]
+ }
+}
+
+pw_test("mimxrt595_test") {
+ enable_if =
+ pw_third_party_mcuxpresso_SDK == "//targets/mimxrt595_evk_freertos:sdk" &&
+ (pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_debug" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_size_optimized" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_speed_optimized")
+ sources = [ "mimxrt595_test.cc" ]
+ deps = [
+ ":pw_digital_io_mcuxpresso",
+ "//targets/mimxrt595_evk_freertos:sdk",
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":mimxrt595_test" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_digital_io_mcuxpresso/OWNERS b/pw_digital_io_mcuxpresso/OWNERS
new file mode 100644
index 000000000..00a4b4002
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/OWNERS
@@ -0,0 +1,2 @@
+afoxley@google.com
+amarkov@google.com
diff --git a/pw_digital_io_mcuxpresso/digital_io.cc b/pw_digital_io_mcuxpresso/digital_io.cc
new file mode 100644
index 000000000..c599d58bf
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/digital_io.cc
@@ -0,0 +1,125 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_digital_io/digital_io.h"
+
+#include <array>
+
+#include "fsl_clock.h"
+#include "fsl_gpio.h"
+#include "fsl_reset.h"
+#include "pw_assert/check.h"
+#include "pw_digital_io_mcuxpresso/digital_io.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+namespace {
+
+constexpr std::array kGpioClocks = GPIO_CLOCKS;
+constexpr std::array kGpioResets = GPIO_RSTS_N;
+
+} // namespace
+
+McuxpressoDigitalOut::McuxpressoDigitalOut(GPIO_Type* base,
+ uint32_t port,
+ uint32_t pin,
+ pw::digital_io::State initial_state)
+ : base_(base), port_(port), pin_(pin), initial_state_(initial_state) {
+ PW_CHECK(base != nullptr);
+ PW_CHECK(port < kGpioClocks.size());
+ PW_CHECK(port < kGpioResets.size());
+}
+
+pw::Status McuxpressoDigitalOut::DoEnable(bool enable) {
+ if (enable) {
+ if (is_enabled()) {
+ return pw::OkStatus();
+ }
+
+ CLOCK_EnableClock(kGpioClocks[port_]);
+ RESET_ClearPeripheralReset(kGpioResets[port_]);
+
+ gpio_pin_config_t config = {
+ .pinDirection = kGPIO_DigitalOutput,
+ .outputLogic = static_cast<uint8_t>(
+ initial_state_ == pw::digital_io::State::kActive ? 1U : 0U),
+ };
+ GPIO_PinInit(base_, port_, pin_, &config);
+
+ } else {
+ // Set to input on disable.
+ gpio_pin_config_t config = {
+ .pinDirection = kGPIO_DigitalInput,
+ .outputLogic = 0,
+ };
+ GPIO_PinInit(base_, port_, pin_, &config);
+
+ // Can't disable clock as other users on same port may be active.
+ }
+ enabled_ = enable;
+ return pw::OkStatus();
+}
+
+pw::Status McuxpressoDigitalOut::DoSetState(pw::digital_io::State state) {
+ if (!is_enabled()) {
+ return pw::Status::FailedPrecondition();
+ }
+ GPIO_PinWrite(
+ base_, port_, pin_, state == pw::digital_io::State::kActive ? 1 : 0);
+ return pw::OkStatus();
+}
+
+McuxpressoDigitalIn::McuxpressoDigitalIn(GPIO_Type* base,
+ uint32_t port,
+ uint32_t pin)
+ : base_(base), port_(port), pin_(pin) {
+ PW_CHECK(base != nullptr);
+ PW_CHECK(port < kGpioClocks.size());
+ PW_CHECK(port < kGpioResets.size());
+}
+
+pw::Status McuxpressoDigitalIn::DoEnable(bool enable) {
+ if (!enable) {
+ enabled_ = enable;
+ // Can't disable clock as other users on same port may be active.
+ return pw::OkStatus();
+ }
+
+ if (is_enabled()) {
+ return pw::Status::FailedPrecondition();
+ }
+
+ CLOCK_EnableClock(kGpioClocks[port_]);
+ RESET_ClearPeripheralReset(kGpioResets[port_]);
+
+ gpio_pin_config_t config = {
+ .pinDirection = kGPIO_DigitalInput,
+ .outputLogic = 0,
+ };
+ GPIO_PinInit(base_, port_, pin_, &config);
+
+ enabled_ = enable;
+ return pw::OkStatus();
+}
+
+pw::Result<pw::digital_io::State> McuxpressoDigitalIn::DoGetState() {
+ if (!is_enabled()) {
+ return pw::Status::FailedPrecondition();
+ }
+ uint32_t value = GPIO_PinRead(base_, port_, pin_);
+ return value == 1 ? pw::digital_io::State::kActive
+ : pw::digital_io::State::kInactive;
+}
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_mcuxpresso/docs.rst b/pw_digital_io_mcuxpresso/docs.rst
new file mode 100644
index 000000000..d8bdabf3d
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/docs.rst
@@ -0,0 +1,62 @@
+.. _module-pw_digital_io_mcuxpresso:
+
+------------------------
+pw_digital_io_mcuxpresso
+------------------------
+``pw_digital_io_mcuxpresso`` implements the :ref:`module-pw_digital_io` interface using
+the NXP MCUXpresso SDK.
+
+Setup
+=====
+Use of this module requires setting up the MCUXpresso SDK for use with Pigweed. Follow
+the steps in :ref:`module-pw_build_mcuxpresso` to create a ``pw_source_set`` for an
+MCUXpresso SDK. Include the GPIO and PINT driver components in this SDK definition.
+
+This example shows what your SDK setup would look like if using an RT595 board.
+
+.. code-block:: text
+
+ import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+
+ pw_mcuxpresso_sdk("sample_project_sdk") {
+ manifest = "$dir_pw_third_party/mcuxpresso/evkmimxrt595/EVK-MIMXRT595_manifest_v3_8.xml"
+ include = [
+ "component.serial_manager_uart.MIMXRT595S",
+ "platform.drivers.lpc_gpio.MIMXRT595S",
+ "platform.drivers.pint.MIMXRT595S",
+ "project_template.evkmimxrt595.MIMXRT595S",
+ "utility.debug_console.MIMXRT595S",
+ ]
+ }
+
+Next, specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
+the name of this source set. Edit your GN args with ``gn args out``.
+
+.. code-block:: text
+
+ pw_third_party_mcuxpresso_SDK = "//targets/mimxrt595_evk:sample_project_sdk"
+
+Then, depend on this module in your BUILD.gn to use.
+
+.. code-block:: text
+
+ deps = [ dir_pw_digital_io_mcuxpresso ]
+
+Examples
+========
+Use ``pw::digital_io::McuxpressoDigitalIn`` and ``pw::digital_io::McuxpressoDigitalOut``
+classes to control GPIO pins.
+
+Example code to use GPIO pins from an NXP SDK board definition:
+
+.. code-block:: text
+
+ McuxpressoDigitalOut out(BOARD_INITPINS_D8_GPIO,
+ BOARD_INITPINS_D8_PORT,
+ BOARD_INITPINS_D8_PIN,
+ pw::digital_io::State::kActive);
+ out.SetState(pw::digital_io::State::kInactive);
+
+ McuxpressoDigitalIn in(
+ BOARD_INITPINS_D9_GPIO, BOARD_INITPINS_D9_PORT, BOARD_INITPINS_D9_PIN);
+ auto state = in.GetState();
diff --git a/pw_digital_io_mcuxpresso/interrupt_controller.cc b/pw_digital_io_mcuxpresso/interrupt_controller.cc
new file mode 100644
index 000000000..0bd2f3829
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/interrupt_controller.cc
@@ -0,0 +1,108 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_digital_io_mcuxpresso/interrupt_controller.h"
+
+#include <array>
+#include <cstdint>
+
+#include "fsl_pint.h"
+#include "pw_digital_io/digital_io.h"
+#include "pw_function/function.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+namespace {
+
+using ::pw::digital_io::InterruptTrigger;
+using ::pw::digital_io::State;
+
+// PINT API doesn't allow context on callback API, so store globally.
+std::array<pw::digital_io::InterruptHandler,
+ FSL_FEATURE_PINT_NUMBER_OF_CONNECTED_OUTPUTS>
+ interrupt_handlers;
+std::array<PINT_Type*, FSL_FEATURE_PINT_NUMBER_OF_CONNECTED_OUTPUTS> bases;
+
+void PintCallback(pint_pin_int_t pin, uint32_t) {
+ PW_CHECK(pin < interrupt_handlers.size());
+ State state = PINT_PinInterruptGetStatus(bases[pin], pin) == 1
+ ? State::kActive
+ : State::kInactive;
+ interrupt_handlers[pin](state);
+ SDK_ISR_EXIT_BARRIER;
+}
+
+} // namespace
+
+McuxpressoInterruptController::McuxpressoInterruptController(PINT_Type* base)
+ : base_(base) {
+ PINT_Init(base_);
+}
+
+McuxpressoInterruptController::~McuxpressoInterruptController() {
+ PINT_Deinit(base_);
+}
+
+pw::Status McuxpressoInterruptController::Config(
+ pint_pin_int_t pin,
+ InterruptTrigger trigger,
+ pw::digital_io::InterruptHandler&& handler) {
+ if (pin >= interrupt_handlers.size()) {
+ return pw::Status::InvalidArgument();
+ }
+ interrupt_handlers[pin] = std::move(handler);
+ bases[pin] = base_;
+ switch (trigger) {
+ case InterruptTrigger::kActivatingEdge:
+ PINT_PinInterruptConfig(
+ base_, pin, kPINT_PinIntEnableRiseEdge, PintCallback);
+ break;
+ case InterruptTrigger::kDeactivatingEdge:
+ PINT_PinInterruptConfig(
+ base_, pin, kPINT_PinIntEnableFallEdge, PintCallback);
+ break;
+ case InterruptTrigger::kBothEdges:
+ PINT_PinInterruptConfig(
+ base_, pin, kPINT_PinIntEnableBothEdges, PintCallback);
+ break;
+ default:
+ return pw::Status::InvalidArgument();
+ }
+ return pw::OkStatus();
+}
+
+pw::Status McuxpressoInterruptController::EnableHandler(pint_pin_int_t pin,
+ bool enable) {
+ if (enable) {
+ PINT_EnableCallbackByIndex(base_, pin);
+ } else {
+ PINT_DisableCallbackByIndex(base_, pin);
+ }
+ return pw::OkStatus();
+}
+
+pw::Result<pw::digital_io::State> McuxpressoInterruptController::GetState(
+ pint_pin_int_t pin) {
+ switch (PINT_PinInterruptGetStatus(base_, pin)) {
+ case 0:
+ return State::kInactive;
+ case 1:
+ return State::kActive;
+ default:
+ return pw::Status::Unknown();
+ }
+}
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_mcuxpresso/interrupt_line.cc b/pw_digital_io_mcuxpresso/interrupt_line.cc
new file mode 100644
index 000000000..a4fc4d311
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/interrupt_line.cc
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_digital_io_mcuxpresso/interrupt_line.h"
+
+#include "pw_digital_io/digital_io.h"
+#include "pw_digital_io_mcuxpresso/interrupt_controller.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_sync/borrow.h"
+
+namespace pw::digital_io {
+
+McuxpressoDigitalInInterrupt::McuxpressoDigitalInInterrupt(
+ pw::sync::Borrowable<McuxpressoInterruptController>& controller,
+ pint_pin_int_t pin)
+ : controller_(controller), pin_(pin) {}
+
+pw::Status McuxpressoDigitalInInterrupt::DoEnable(bool) {
+ // Can not enabled at individual line level. Only at controller level, which
+ // is always enabled.
+ return pw::OkStatus();
+}
+
+pw::Result<pw::digital_io::State> McuxpressoDigitalInInterrupt::DoGetState() {
+ return controller_.acquire()->GetState(pin_);
+}
+
+pw::Status McuxpressoDigitalInInterrupt::DoSetInterruptHandler(
+ pw::digital_io::InterruptTrigger trigger,
+ pw::digital_io::InterruptHandler&& handler) {
+ return controller_.acquire()->Config(pin_, trigger, std::move(handler));
+}
+
+pw::Status McuxpressoDigitalInInterrupt::DoEnableInterruptHandler(bool enable) {
+ return controller_.acquire()->EnableHandler(pin_, enable);
+}
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_mcuxpresso/mimxrt595_test.cc b/pw_digital_io_mcuxpresso/mimxrt595_test.cc
new file mode 100644
index 000000000..4f81d14da
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/mimxrt595_test.cc
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <cstdint>
+
+#include "gtest/gtest.h"
+#include "pw_digital_io/digital_io.h"
+#include "pw_digital_io_mcuxpresso/digital_io.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+namespace {
+
+constexpr uint32_t kPort = 0;
+constexpr uint32_t kPin = 8;
+
+TEST(DigitalIoTest, SetOk) {
+ McuxpressoDigitalOut out(GPIO, kPort, kPin, pw::digital_io::State::kActive);
+ EXPECT_TRUE(out.SetState(pw::digital_io::State::kInactive).ok());
+}
+
+TEST(DigitalIoTest, GetOk) {
+ McuxpressoDigitalIn in(GPIO, kPort, kPin);
+ EXPECT_TRUE(in.GetState().ok());
+}
+
+} // namespace
+} // namespace pw::digital_io \ No newline at end of file
diff --git a/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/digital_io.h b/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/digital_io.h
new file mode 100644
index 000000000..258f71875
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/digital_io.h
@@ -0,0 +1,57 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "fsl_gpio.h"
+#include "pw_digital_io/digital_io.h"
+
+namespace pw::digital_io {
+
+class McuxpressoDigitalOut : public pw::digital_io::DigitalOut {
+ public:
+ McuxpressoDigitalOut(GPIO_Type* base,
+ uint32_t port,
+ uint32_t pin,
+ pw::digital_io::State initial_state);
+
+ bool is_enabled() const { return enabled_; }
+
+ private:
+ pw::Status DoEnable(bool enable) override;
+ pw::Status DoSetState(pw::digital_io::State state) override;
+
+ GPIO_Type* base_;
+ const uint32_t port_;
+ const uint32_t pin_;
+ const pw::digital_io::State initial_state_;
+ bool enabled_ = false;
+};
+
+class McuxpressoDigitalIn : public pw::digital_io::DigitalIn {
+ public:
+ McuxpressoDigitalIn(GPIO_Type* base, uint32_t port, uint32_t pin);
+
+ bool is_enabled() const { return enabled_; }
+
+ private:
+ pw::Status DoEnable(bool enable) override;
+ pw::Result<pw::digital_io::State> DoGetState() override;
+
+ GPIO_Type* base_;
+ const uint32_t port_;
+ const uint32_t pin_;
+ bool enabled_ = false;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_controller.h b/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_controller.h
new file mode 100644
index 000000000..33b7e2d6b
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_controller.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "fsl_pint.h"
+#include "pw_digital_io/digital_io.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+
+class McuxpressoInterruptController {
+ public:
+ McuxpressoInterruptController(PINT_Type* base);
+ ~McuxpressoInterruptController();
+
+ McuxpressoInterruptController(const McuxpressoInterruptController&) = delete;
+ McuxpressoInterruptController& operator=(
+ const McuxpressoInterruptController&) = delete;
+
+ pw::Status Config(pint_pin_int_t pin,
+ pw::digital_io::InterruptTrigger trigger,
+ pw::digital_io::InterruptHandler&& handler);
+ pw::Status EnableHandler(pint_pin_int_t pin, bool enable);
+ pw::Result<pw::digital_io::State> GetState(pint_pin_int_t pin);
+
+ private:
+ PINT_Type* base_;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_line.h b/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_line.h
new file mode 100644
index 000000000..929c5497a
--- /dev/null
+++ b/pw_digital_io_mcuxpresso/public/pw_digital_io_mcuxpresso/interrupt_line.h
@@ -0,0 +1,47 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_digital_io/digital_io.h"
+#include "pw_digital_io_mcuxpresso/interrupt_controller.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_sync/borrow.h"
+
+namespace pw::digital_io {
+
+class McuxpressoDigitalInInterrupt : public pw::digital_io::DigitalInInterrupt {
+ public:
+ McuxpressoDigitalInInterrupt(
+ pw::sync::Borrowable<McuxpressoInterruptController>& controller,
+ pint_pin_int_t pin);
+
+ McuxpressoDigitalInInterrupt(const McuxpressoDigitalInInterrupt&) = delete;
+ McuxpressoDigitalInInterrupt& operator=(const McuxpressoDigitalInInterrupt&) =
+ delete;
+
+ private:
+ // pw::digital_io::DigitalInInterrupt implementation
+ pw::Status DoEnable(bool enable) override;
+ pw::Result<pw::digital_io::State> DoGetState() override;
+ pw::Status DoSetInterruptHandler(
+ pw::digital_io::InterruptTrigger trigger,
+ pw::digital_io::InterruptHandler&& handler) override;
+ pw::Status DoEnableInterruptHandler(bool enable) override;
+
+ pw::sync::Borrowable<McuxpressoInterruptController>& controller_;
+ pint_pin_int_t pin_;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_rp2040/BUILD.bazel b/pw_digital_io_rp2040/BUILD.bazel
new file mode 100644
index 000000000..fd045a604
--- /dev/null
+++ b/pw_digital_io_rp2040/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# RP2040 is not supported on bazel.
+filegroup(
+ name = "pw_digital_io_rp2040",
+ srcs = [
+ "digital_io.cc",
+ "digital_io_test.cc",
+ "public/pw_digital_io_rp2040/digital_io.h",
+ ],
+)
diff --git a/pw_digital_io_rp2040/BUILD.gn b/pw_digital_io_rp2040/BUILD.gn
new file mode 100644
index 000000000..9cf080093
--- /dev/null
+++ b/pw_digital_io_rp2040/BUILD.gn
@@ -0,0 +1,56 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_includes") {
+ include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_digital_io_rp2040") {
+ public_configs = [ ":public_includes" ]
+ public = [ "public/pw_digital_io_rp2040/digital_io.h" ]
+ deps = [
+ "$PICO_ROOT/src/common/pico_base",
+ "$PICO_ROOT/src/common/pico_stdlib",
+ ]
+ public_deps = [
+ "$dir_pw_digital_io",
+ "$dir_pw_status",
+ ]
+ sources = [ "digital_io.cc" ]
+ remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
+
+pw_test("digital_io_test") {
+ enable_if = pw_build_EXECUTABLE_TARGET_TYPE == "pico_executable"
+ sources = [ "digital_io_test.cc" ]
+ deps = [ ":pw_digital_io_rp2040" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = []
+
+ if (PICO_SRC_DIR != "") {
+ tests += [ ":digital_io_test" ]
+ }
+}
diff --git a/pw_digital_io_rp2040/OWNERS b/pw_digital_io_rp2040/OWNERS
new file mode 100644
index 000000000..ca011e872
--- /dev/null
+++ b/pw_digital_io_rp2040/OWNERS
@@ -0,0 +1 @@
+tonymd@google.com
diff --git a/pw_digital_io_rp2040/digital_io.cc b/pw_digital_io_rp2040/digital_io.cc
new file mode 100644
index 000000000..b6baeb71a
--- /dev/null
+++ b/pw_digital_io_rp2040/digital_io.cc
@@ -0,0 +1,64 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_digital_io_rp2040/digital_io.h"
+
+#include "hardware/gpio.h"
+#include "pico/stdlib.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+
+Rp2040DigitalIn::Rp2040DigitalIn(uint32_t pin) : pin_(pin) {}
+
+Status Rp2040DigitalIn::DoEnable(bool enable) {
+ if (!enable) {
+ gpio_deinit(pin_);
+ return OkStatus();
+ }
+
+ gpio_init(pin_);
+ gpio_set_dir(pin_, GPIO_IN);
+ return OkStatus();
+}
+
+Result<State> Rp2040DigitalIn::DoGetState() {
+ const pw::Result<State> result(State(gpio_get(pin_)));
+ return result;
+}
+
+Rp2040DigitalInOut::Rp2040DigitalInOut(uint32_t pin) : pin_(pin) {}
+
+Status Rp2040DigitalInOut::DoEnable(bool enable) {
+ if (!enable) {
+ gpio_deinit(pin_);
+ return OkStatus();
+ }
+
+ gpio_init(pin_);
+ gpio_set_dir(pin_, GPIO_OUT);
+ return OkStatus();
+}
+
+Status Rp2040DigitalInOut::DoSetState(State level) {
+ gpio_put(pin_, level == State::kActive);
+ return OkStatus();
+}
+
+Result<State> Rp2040DigitalInOut::DoGetState() {
+ const pw::Result<State> result(State(gpio_get(pin_)));
+ return result;
+}
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io_rp2040/digital_io_test.cc b/pw_digital_io_rp2040/digital_io_test.cc
new file mode 100644
index 000000000..32107606b
--- /dev/null
+++ b/pw_digital_io_rp2040/digital_io_test.cc
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_digital_io/digital_io.h"
+
+#include <gtest/gtest.h>
+
+#include "pw_digital_io_rp2040/digital_io.h"
+
+using pw::digital_io::Rp2040DigitalIn;
+using pw::digital_io::Rp2040DigitalInOut;
+
+namespace pw::digital_io {
+namespace {
+
+Rp2040DigitalInOut output_pin(/*gpio_pin=*/15);
+Rp2040DigitalIn input_pin(/*gpio_pin=*/16);
+
+TEST(DigitalIoTest, Init) {
+ // Simple test only meant to ensure module is compiled.
+ output_pin.Enable();
+ output_pin.SetState(pw::digital_io::State::kInactive);
+ output_pin.SetState(pw::digital_io::State::kActive);
+
+ input_pin.Enable();
+ auto state_result = input_pin.GetState();
+ ASSERT_EQ(OkStatus(), state_result.status());
+ ASSERT_EQ(State::kInactive, state_result.value());
+}
+
+} // namespace
+} // namespace pw::digital_io
diff --git a/pw_digital_io_rp2040/docs.rst b/pw_digital_io_rp2040/docs.rst
new file mode 100644
index 000000000..6bc017ee8
--- /dev/null
+++ b/pw_digital_io_rp2040/docs.rst
@@ -0,0 +1,34 @@
+.. _module-pw_digital_io_rp2040:
+
+--------------------
+pw_digital_io_rp2040
+--------------------
+``pw_digital_io_rp2040`` implements the :ref:`module-pw_digital_io` interface using
+the `Raspberry Pi Pico SDK <https://github.com/raspberrypi/pico-sdk/>`_.
+
+Setup
+=====
+Use of this module requires setting up the Pico SDK for use with Pigweed. Follow
+the steps in :ref:`target-raspberry-pi-pico` to get setup.
+
+Examples
+========
+Use ``pw::digital_io::Rp2040DigitalIn`` and
+``pw::digital_io::Rp2040DigitalInOut`` classes to control GPIO pins.
+
+Example code to use GPIO pins:
+
+.. code-block:: cpp
+
+ #include "pw_digital_io_rp2040/digital_io.h"
+
+ using pw::digital_io::Rp2040DigitalIn;
+ using pw::digital_io::Rp2040DigitalInOut;
+
+ Rp2040DigitalInOut out(/*gpio_pin=*/ 15);
+ out.Enable();
+ out.SetState(pw::digital_io::State::kInactive);
+
+ Rp2040DigitalIn in(/*gpio_pin=*/ 16);
+ in.Enable();
+ auto state = in.GetState();
diff --git a/pw_digital_io_rp2040/public/pw_digital_io_rp2040/digital_io.h b/pw_digital_io_rp2040/public/pw_digital_io_rp2040/digital_io.h
new file mode 100644
index 000000000..4d96f3574
--- /dev/null
+++ b/pw_digital_io_rp2040/public/pw_digital_io_rp2040/digital_io.h
@@ -0,0 +1,46 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <cstdint>
+
+#include "pw_digital_io/digital_io.h"
+
+namespace pw::digital_io {
+
+class Rp2040DigitalInOut : public DigitalInOut {
+ public:
+ Rp2040DigitalInOut(uint32_t pin);
+
+ Status DoEnable(bool enable) override;
+ Status DoSetState(State level) override;
+ Result<State> DoGetState() override;
+
+ private:
+ uint32_t pin_;
+};
+
+class Rp2040DigitalIn : public DigitalIn {
+ public:
+ Rp2040DigitalIn(uint32_t pin);
+
+ Status DoEnable(bool enable) override;
+ Result<State> DoGetState() override;
+
+ private:
+ uint32_t pin_;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_docgen/docs.gni b/pw_docgen/docs.gni
index 2d0ae00cf..ed157f914 100644
--- a/pw_docgen/docs.gni
+++ b/pw_docgen/docs.gni
@@ -22,7 +22,10 @@ declare_args() {
pw_docgen_BUILD_DOCS = false
# Set to enable Google Analytics tracking of generated docs.
- pw_docs_google_analytics_id = ""
+ pw_docgen_GOOGLE_ANALYTICS_ID = ""
+
+ # Set to define the number of parallel threads to use during the Sphinx build.
+ pw_docgen_THREADS = ""
}
# Defines a group of documentation files and assets.
@@ -34,40 +37,38 @@ declare_args() {
# report_deps: Report card targets on which documentation depends.
# other_deps: Dependencies on any other types of targets.
template("pw_doc_group") {
- if (defined(invoker.sources)) {
- _sources = invoker.sources
- } else {
- _sources = []
- }
-
- if (defined(invoker.inputs)) {
- _inputs = invoker.inputs
- } else {
- _inputs = []
- }
-
assert(defined(invoker.sources) || defined(invoker.inputs),
"pw_doc_group requires at least one of sources or inputs")
- _all_deps = []
- if (defined(invoker.group_deps)) {
- _all_deps += invoker.group_deps
- }
- if (defined(invoker.report_deps)) {
- _all_deps += invoker.report_deps
- }
- if (defined(invoker.other_deps)) {
- _all_deps += invoker.other_deps
- }
-
# Create a group containing the source and input files so that docs are
# rebuilt on file modifications.
pw_input_group(target_name) {
+ _sources = []
+ if (defined(invoker.sources)) {
+ _sources = invoker.sources
+ }
+
+ _inputs = []
+ if (defined(invoker.inputs)) {
+ _inputs = invoker.inputs
+ }
+
metadata = {
pw_doc_sources = rebase_path(_sources, root_build_dir)
pw_doc_inputs = rebase_path(_inputs, root_build_dir)
}
- deps = _all_deps
+
+ deps = []
+ if (defined(invoker.group_deps)) {
+ deps += invoker.group_deps
+ }
+ if (defined(invoker.report_deps)) {
+ deps += invoker.report_deps
+ }
+ if (defined(invoker.other_deps)) {
+ deps += invoker.other_deps
+ }
+
inputs = _sources + _inputs
}
}
@@ -76,6 +77,10 @@ template("pw_doc_group") {
#
# Args:
# deps: List of pw_doc_group targets.
+# python_metadata_deps: Python-related dependencies that are only used as deps
+# for generating Python package metadata list, not the
+# overall documentation generation. This should rarely
+# be used by non-Pigweed code.
# sources: Top-level documentation .rst source files.
# conf: Configuration script (conf.py) for Sphinx.
# output_directory: Path to directory to which HTML output is rendered.
@@ -89,61 +94,68 @@ template("pw_doc_gen") {
assert(defined(invoker.output_directory),
"pw_doc_gen requires an 'output_directory' argument")
- # Collects all dependency metadata into a single JSON file.
- _metadata_file_target = "${target_name}_metadata"
- generated_file(_metadata_file_target) {
- outputs = [ "$target_gen_dir/$target_name.json" ]
- data_keys = [
- "pw_doc_sources",
- "pw_doc_inputs",
- ]
- output_conversion = "json"
- deps = invoker.deps
- }
-
- _script_args = [
- "--gn-root",
- rebase_path("//", root_build_dir),
- "--gn-gen-root",
- rebase_path(root_gen_dir, root_build_dir) + "/",
- "--sphinx-build-dir",
- rebase_path("$target_gen_dir/pw_docgen_tree", root_build_dir),
- "--conf",
- rebase_path(invoker.conf, root_build_dir),
- "--out-dir",
- rebase_path(invoker.output_directory, root_build_dir),
- ]
-
- # Enable Google Analytics if a measurement ID is provided
- if (pw_docs_google_analytics_id != "") {
- _script_args += [
- "--google-analytics-id",
- pw_docs_google_analytics_id,
- ]
- }
-
- # Metadata JSON file path.
- _script_args += [ "--metadata" ]
- _script_args +=
- rebase_path(get_target_outputs(":$_metadata_file_target"), root_build_dir)
-
- _script_args += rebase_path(invoker.sources, root_build_dir)
-
- # Required to set the PYTHONPATH for any automodule/class/function RST
- # directives.
- _python_metadata_deps = [ "$dir_pw_docgen/py" ]
- if (defined(invoker.python_metadata_deps)) {
- _python_metadata_deps += invoker.python_metadata_deps
- }
-
if (pw_docgen_BUILD_DOCS) {
+ # Collects all dependency metadata into a single JSON file.
+ _metadata_file_target = "${target_name}_metadata"
+ generated_file(_metadata_file_target) {
+ outputs = [ "$target_gen_dir/$target_name.json" ]
+ data_keys = [
+ "pw_doc_sources",
+ "pw_doc_inputs",
+ ]
+ output_conversion = "json"
+ deps = invoker.deps
+ }
+
pw_python_action(target_name) {
- script = "$dir_pw_docgen/py/pw_docgen/docgen.py"
- args = _script_args
+ module = "pw_docgen.docgen"
+ args = [
+ "--gn-root",
+ rebase_path("//", root_build_dir),
+ "--gn-gen-root",
+ rebase_path(root_gen_dir, root_build_dir) + "/",
+ "--sphinx-build-dir",
+ rebase_path("$target_gen_dir/pw_docgen_tree", root_build_dir),
+ "--conf",
+ rebase_path(invoker.conf, root_build_dir),
+ "--out-dir",
+ rebase_path(invoker.output_directory, root_build_dir),
+ ]
+
+ # Enable Google Analytics if a measurement ID is provided
+ if (pw_docgen_GOOGLE_ANALYTICS_ID != "") {
+ args += [
+ "--google-analytics-id",
+ pw_docgen_GOOGLE_ANALYTICS_ID,
+ ]
+ }
+
+ # Override the default number of threads for the Sphinx build.
+ if (pw_docgen_THREADS != "") {
+ args += [
+ "-j",
+ pw_docgen_THREADS,
+ ]
+ }
+
+ # Metadata JSON file path.
+ args += [ "--metadata" ] +
+ rebase_path(get_target_outputs(":$_metadata_file_target"),
+ root_build_dir)
+
+ args += rebase_path(invoker.sources, root_build_dir)
+
+ python_deps = [ "$dir_pw_docgen/py" ]
deps = [ ":$_metadata_file_target" ]
- python_metadata_deps = _python_metadata_deps
- inputs = [ invoker.conf ]
- inputs += invoker.sources
+
+ # Required to set the PYTHONPATH for any automodule/class/function RST
+ # directives.
+ python_metadata_deps = [ "$dir_pw_docgen/py" ]
+ if (defined(invoker.python_metadata_deps)) {
+ python_metadata_deps += invoker.python_metadata_deps
+ }
+
+ inputs = [ invoker.conf ] + invoker.sources
stamp = true
}
} else {
diff --git a/pw_docgen/docs.rst b/pw_docgen/docs.rst
index 6971384a3..b36de7524 100644
--- a/pw_docgen/docs.rst
+++ b/pw_docgen/docs.rst
@@ -71,17 +71,20 @@ groups, causing them to be built with it.
* ``group_deps``: Other ``pw_doc_group`` targets required by this one.
* ``report_deps``: Report card generating targets (e.g. ``pw_size_diff``) on
which the docs depend.
+* ``other_deps``: Any other GN targets that should be run before this
+ ``pw_doc_group`` runs that is not included in one of the above ``dep``
+ categories.
**Example**
-.. code::
+.. code-block::
- pw_doc_group("my_doc_group") {
- sources = [ "docs.rst" ]
- inputs = [ "face-with-tears-of-joy-emoji.svg" ]
- group_deps = [ ":sub_doc_group" ]
- report_deps = [ ":my_size_report" ]
- }
+ pw_doc_group("my_doc_group") {
+ sources = [ "docs.rst" ]
+ inputs = [ "face-with-tears-of-joy-emoji.svg" ]
+ group_deps = [ ":sub_doc_group" ]
+ report_deps = [ ":my_size_report" ]
+ }
pw_doc_gen
__________
@@ -100,19 +103,22 @@ to tie everything together.
* ``index``: Path to the top-level ``index.rst`` file.
* ``output_directory``: Directory in which to render HTML output.
* ``deps``: List of all ``pw_doc_group`` targets required for the documentation.
+* ``python_metadata_deps``: Python-related dependencies that are only used as
+ deps for generating Python package metadata list, not the overall
+ documentation generation. This should rarely be used by non-Pigweed code.
**Example**
-.. code::
+.. code-block::
- pw_doc_gen("my_docs") {
- conf = "//my_docs/conf.py"
- index = "//my_docs/index.rst"
- output_directory = target_gen_dir
- deps = [
- "//my_module:my_doc_group",
- ]
- }
+ pw_doc_gen("my_docs") {
+ conf = "//my_docs/conf.py"
+ index = "//my_docs/index.rst"
+ output_directory = target_gen_dir
+ deps = [
+ "//my_module:my_doc_group",
+ ]
+ }
Generating Documentation
------------------------
@@ -123,18 +129,18 @@ using a subset of Pigweed's core documentation.
Consider the following target in ``$dir_pigweed/docs/BUILD.gn``:
-.. code::
+.. code-block::
- pw_doc_gen("docs") {
- conf = "conf.py"
- index = "index.rst"
- output_directory = target_gen_dir
- deps = [
- "$dir_pw_bloat:docs",
- "$dir_pw_docgen:docs",
- "$dir_pw_preprocessor:docs",
- ]
- }
+ pw_doc_gen("docs") {
+ conf = "conf.py"
+ index = "index.rst"
+ output_directory = target_gen_dir
+ deps = [
+ "$dir_pw_bloat:docs",
+ "$dir_pw_docgen:docs",
+ "$dir_pw_preprocessor:docs",
+ ]
+ }
A documentation tree is created under the output directory. Each of the sources
and inputs in the target's dependency graph is copied under this tree in the
@@ -142,19 +148,19 @@ same directory structure as they appear under the root GN build directory
(``$dir_pigweed`` in this case). The ``conf.py`` and ``index.rst`` provided
directly to the ``pw_doc_gen`` template are copied in at the root of the tree.
-.. code::
+.. code-block::
- out/gen/docs/pw_docgen_tree/
- ├── conf.py
- ├── index.rst
- ├── pw_bloat
- │ ├── bloat.rst
- │ └── examples
- │ └── simple_bloat.rst
- ├── pw_docgen
- │ └── docgen.rst
- └── pw_preprocessor
- └── docs.rst
+ out/gen/docs/pw_docgen_tree/
+ ├── conf.py
+ ├── index.rst
+ ├── pw_bloat
+ │ ├── bloat.rst
+ │ └── examples
+ │ └── simple_bloat.rst
+ ├── pw_docgen
+ │ └── docgen.rst
+ └── pw_preprocessor
+ └── docs.rst
This is the documentation tree which gets passed to Sphinx to build HTML output.
Imports within documentation files must be relative to this structure. In
@@ -190,12 +196,8 @@ example:
:name: pw_string
:tagline: Efficient, easy, and safe string manipulation
:status: stable
- :languages: C++14, C++17
+ :languages: C++17, Rust
:code-size-impact: 500 to 1500 bytes
- :get-started: module-pw_string-get-started
- :design: module-pw_string-design
- :guides: module-pw_string-guide
- :api: module-pw_string-api
Module sales pitch goes here!
@@ -205,14 +207,9 @@ _________________
- ``tagline``: A very short tagline that summarizes the module (required)
- ``status``: One of ``experimental``, ``unstable``, and ``stable`` (required)
- ``is-deprecated``: A flag indicating that the module is deprecated
-- ``languages``: A comma-separated list of languages the module supports
+- ``languages``: A comma-separated list of languages the module supports. If
+ the language has API docs (Rust), they will be linked from the metadata block.
- ``code-size-impact``: A summarize of the average code size impact
-- ``get-started``: A reference to the getting started section (required)
-- ``tutorials``: A reference to the tutorials section
-- ``guides``: A reference to the guides section
-- ``design``: A reference to the design considerations section (required)
-- ``concepts``: A reference to the concepts documentation
-- ``api``: A reference to the API documentation
google_analytics
----------------
@@ -225,3 +222,31 @@ automatically based on the value of the GN argument
``pw_docs_google_analytics_id``, allowing you to control whether tracking is
enabled or not in your build configuration. Typically, you would only enable
this for documentation builds intended for deployment on the web.
+
+Debugging Pigweed's Sphinx extensions
+-------------------------------------
+To step through your Pigweed extension code with
+`pdb <https://docs.python.org/3/library/pdb.html>`_:
+
+#. Set a breakpoint in your extension code:
+
+ .. code-block::
+
+ breakpoint()
+
+#. Build ``python.install`` to install the code change into the bootstrap venv
+ (``environment/pigweed-venv/lib/python3.8/site-packages/pw_docgen``):
+
+ .. code-block::
+
+ ninja -C out python.install
+
+#. Manually invoke Sphinx to build the docs and trigger your breakpoint:
+
+ .. code-block::
+
+ cd out
+ sphinx-build -W -b html -d docs/gen/docs/help docs/gen/docs/pw_docgen_tree docs/gen/docs/html -v -v -v
+
+ You should see build output from Sphinx. The build should pause at your
+ breakpoint and you should then see pdb's prompt (``(Pdb)``).
diff --git a/pw_docgen/py/BUILD.gn b/pw_docgen/py/BUILD.gn
index 55c48aaeb..a52f3ab31 100644
--- a/pw_docgen/py/BUILD.gn
+++ b/pw_docgen/py/BUILD.gn
@@ -20,14 +20,16 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_docgen/__init__.py",
"pw_docgen/docgen.py",
"pw_docgen/sphinx/__init__.py",
"pw_docgen/sphinx/google_analytics.py",
+ "pw_docgen/sphinx/kconfig.py",
"pw_docgen/sphinx/module_metadata.py",
+ "pw_docgen/sphinx/pigweed_live.py",
+ "pw_docgen/sphinx/seed_metadata.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
diff --git a/pw_docgen/py/pw_docgen/docgen.py b/pw_docgen/py/pw_docgen/docgen.py
index 764717d4a..24040e7cf 100644
--- a/pw_docgen/py/pw_docgen/docgen.py
+++ b/pw_docgen/py/pw_docgen/docgen.py
@@ -50,6 +50,13 @@ def parse_args() -> argparse.Namespace:
'--conf', required=True, help='Path to conf.py file for Sphinx'
)
parser.add_argument(
+ '-j',
+ '--parallel',
+ type=int,
+ default=os.cpu_count(),
+ help='Number of parallel processes to run',
+ )
+ parser.add_argument(
'--gn-root', required=True, help='Root of the GN build tree'
)
parser.add_argument(
@@ -78,13 +85,25 @@ def parse_args() -> argparse.Namespace:
def build_docs(
- src_dir: str, dst_dir: str, google_analytics_id: Optional[str] = None
+ src_dir: str,
+ dst_dir: str,
+ parallel: int,
+ google_analytics_id: Optional[str] = None,
) -> int:
"""Runs Sphinx to render HTML documentation from a doc tree."""
# TODO(frolv): Specify the Sphinx script from a prebuilts path instead of
# requiring it in the tree.
- command = ['sphinx-build', '-W', '-b', 'html', '-d', f'{dst_dir}/help']
+ command = [
+ 'sphinx-build',
+ '-W',
+ '-j',
+ str(parallel),
+ '-b',
+ 'html',
+ '-d',
+ f'{dst_dir}/help',
+ ]
if google_analytics_id is not None:
command.append(f'-Dgoogle_analytics_id={google_analytics_id}')
@@ -138,8 +157,8 @@ def main() -> int:
if os.path.exists(args.sphinx_build_dir):
shutil.rmtree(args.sphinx_build_dir)
- # TODO(b/235349854): Printing the header causes unicode problems on Windows.
- # Disabled for now; re-enable once the root issue is fixed.
+ # TODO: b/235349854 - Printing the header causes unicode problems on
+ # Windows. Disabled for now; re-enable once the root issue is fixed.
# print(SCRIPT_HEADER)
copy_doc_tree(args)
@@ -147,7 +166,10 @@ def main() -> int:
print('-' * 80, flush=True)
return build_docs(
- args.sphinx_build_dir, args.out_dir, args.google_analytics_id
+ args.sphinx_build_dir,
+ args.out_dir,
+ args.parallel,
+ args.google_analytics_id,
)
diff --git a/pw_docgen/py/pw_docgen/sphinx/kconfig.py b/pw_docgen/py/pw_docgen/sphinx/kconfig.py
new file mode 100644
index 000000000..c18f3d816
--- /dev/null
+++ b/pw_docgen/py/pw_docgen/sphinx/kconfig.py
@@ -0,0 +1,137 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Auto-generate the Kconfig reference in //docs/os/zephyr/kconfig.rst"""
+
+
+import os
+import re
+from typing import Iterable, Dict
+
+import docutils
+from docutils.core import publish_doctree
+from sphinx.application import Sphinx
+from sphinx.addnodes import document
+
+
+try:
+ import kconfiglib # type: ignore
+
+ KCONFIGLIB_AVAILABLE = True
+except ImportError:
+ KCONFIGLIB_AVAILABLE = False
+
+
+def rst_to_doctree(rst: str) -> Iterable[docutils.nodes.Node]:
+ """Convert raw reStructuredText into doctree nodes."""
+ # TODO: b/288127315 - Properly resolve references within the rst so that
+ # links are generated more robustly.
+ while ':ref:`module-' in rst:
+ rst = re.sub(
+ r':ref:`module-(.*?)`', r'`\1 <https://pigweed.dev/\1>`_', rst
+ )
+ doctree = publish_doctree(rst)
+ return doctree.children
+
+
+def create_source_paragraph(name_and_loc: str) -> Iterable[docutils.nodes.Node]:
+ """Convert kconfiglib's name and location string into a source code link."""
+ start = name_and_loc.index('pw_')
+ end = name_and_loc.index(':')
+ file_path = name_and_loc[start:end]
+ url = f'https://cs.opensource.google/pigweed/pigweed/+/main:{file_path}'
+ link = f'`//{file_path} <{url}>`_'
+ return rst_to_doctree(f'Source: {link}')
+
+
+def process_node(
+ node: kconfiglib.MenuNode, parent: docutils.nodes.Node
+) -> None:
+ """Recursively generate documentation for the Kconfig nodes."""
+ while node:
+ if node.item == kconfiglib.MENU:
+ name = node.prompt[0]
+ # All auto-generated sections must have an ID or else the
+ # get_secnumber() function in Sphinx's HTML5 writer throws an
+ # IndexError.
+ menu_section = docutils.nodes.section(ids=[name])
+ menu_section += docutils.nodes.title(text=f'{name} options')
+ if node.list:
+ process_node(node.list, menu_section)
+ parent += menu_section
+ elif isinstance(node.item, kconfiglib.Symbol):
+ name = f'CONFIG_{node.item.name}'
+ symbol_section = docutils.nodes.section(ids=[name])
+ symbol_section += docutils.nodes.title(text=name)
+ symbol_section += docutils.nodes.paragraph(
+ text=f'Type: {kconfiglib.TYPE_TO_STR[node.item.type]}'
+ )
+ if node.item.defaults:
+ try:
+ default_value = node.item.defaults[0][0].str_value
+ symbol_section += docutils.nodes.paragraph(
+ text=f'Default value: {default_value}'
+ )
+ # If the data wasn't found, just contine trying to process
+ # rest of the documentation for the node.
+ except IndexError:
+ pass
+ if node.item.ranges:
+ try:
+ low = node.item.ranges[0][0].str_value
+ high = node.item.ranges[0][1].str_value
+ symbol_section += docutils.nodes.paragraph(
+ text=f'Range of valid values: {low} to {high}'
+ )
+ except IndexError:
+ pass
+ if node.prompt:
+ try:
+ symbol_section += docutils.nodes.paragraph(
+ text=f'Description: {node.prompt[0]}'
+ )
+ except IndexError:
+ pass
+ if node.help:
+ symbol_section += rst_to_doctree(node.help)
+ if node.list:
+ process_node(node.list, symbol_section)
+ symbol_section += create_source_paragraph(node.item.name_and_loc)
+ parent += symbol_section
+ # TODO: b/288127315 - Render choices?
+ # elif isinstance(node.item, kconfiglib.Choice):
+ node = node.next
+
+
+def generate_kconfig_reference(_, doctree: document, docname: str) -> None:
+ """Parse the Kconfig and kick off the doc generation process."""
+ if 'docs/os/zephyr/kconfig' not in docname:
+ return
+ # Assume that the new content should be appended to the last section
+ # in the doctree.
+ for child in doctree.children:
+ if isinstance(child, docutils.nodes.section):
+ root = child
+ pw_root = os.environ['PW_ROOT']
+ file_path = f'{pw_root}/Kconfig.zephyr'
+ kconfig = kconfiglib.Kconfig(file_path)
+ # There's no need to render kconfig.top_node (the main menu) or
+ # kconfig.top_node.list (ZEPHYR_PIGWEED_MODULE).
+ process_node(kconfig.top_node.list.next, root)
+
+
+def setup(app: Sphinx) -> Dict[str, bool]:
+ """Initialize the Sphinx extension."""
+ if KCONFIGLIB_AVAILABLE:
+ app.connect('doctree-resolved', generate_kconfig_reference)
+ return {'parallel_read_safe': True, 'parallel_write_safe': True}
diff --git a/pw_docgen/py/pw_docgen/sphinx/module_metadata.py b/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
index d6fe05497..fad1736d7 100644
--- a/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
+++ b/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
@@ -13,18 +13,76 @@
# the License.
"""Sphinx directives for Pigweed module metadata"""
-from typing import List
+from dataclasses import dataclass
+from typing import cast, Dict, List, Optional, TypeVar, Union
+
+# We use BeautifulSoup for certain docs rendering features. It may not be
+# available in downstream projects. If so, no problem. We fall back to simpler
+# docs rendering.
+# pylint: disable=import-error
+try:
+ from bs4 import BeautifulSoup # type: ignore
+ from bs4.element import Tag as HTMLTag # type: ignore
+
+ bs_enabled = True
+except ModuleNotFoundError:
+ bs_enabled = False
+# pylint: enable=import-error
import docutils
+from docutils import nodes
+import docutils.statemachine
# pylint: disable=consider-using-from-import
import docutils.parsers.rst.directives as directives # type: ignore
# pylint: enable=consider-using-from-import
from sphinx.application import Sphinx as SphinxApplication
+from sphinx.environment import BuildEnvironment
from sphinx.util.docutils import SphinxDirective
-from sphinx_design.badges_buttons import ButtonRefDirective # type: ignore
-from sphinx_design.cards import CardDirective # type: ignore
+
+from sphinx_design.cards import CardDirective
+
+EnvAttrT = TypeVar('EnvAttrT')
+
+
+@dataclass
+class ParsedBody:
+ topnav: str
+ body_without_topnav: str
+
+
+class EnvMetadata:
+ """Easier access to the Sphinx `env` for custom metadata.
+
+ You can store things in the Sphinx `env`, which is just a dict. But each
+ time you do, you have to handle the possibility that the key you want
+ hasn't been set yet, and set it to a default. The `env` is also untyped,
+ so you have to cast the value you get to whatever type you expect it to be.
+
+ Or you can use this class to define your metadata keys up front, and just
+ access them like: `value = EnvMetadata(env).my_value`
+
+ ... which will handle initializing the value if it hasn't been yet and
+ provide you a typed result.
+ """
+
+ def __init__(self, env: BuildEnvironment):
+ self._env = env
+
+ def _get_env_attr(self, attr: str, default: EnvAttrT) -> EnvAttrT:
+ if not hasattr(self._env, attr):
+ value: EnvAttrT = default
+ setattr(self._env, attr, value)
+ else:
+ value = getattr(self._env, attr)
+
+ return value
+
+ @property
+ def pw_parsed_bodies(self) -> Dict[str, ParsedBody]:
+ default: Dict[str, ParsedBody] = {}
+ return self._get_env_attr('pw_module_nav', default)
def status_choice(arg):
@@ -32,15 +90,19 @@ def status_choice(arg):
def status_badge(module_status: str) -> str:
+ """Given a module status, return the status badge for rendering."""
role = ':bdg-primary:'
return role + f'`{module_status.title()}`'
-def cs_url(module_name: str):
+def cs_url(module_name: str) -> str:
+ """Return the codesearch URL for the given module."""
return f'https://cs.opensource.google/pigweed/pigweed/+/main:{module_name}/'
-def concat_tags(*tag_lists: List[str]):
+def concat_tags(*tag_lists: List[str]) -> List[str]:
+ """Given a list of tag lists, return them concat'ed and ready for render."""
+
all_tags = tag_lists[0]
for tag_list in tag_lists[1:]:
@@ -51,6 +113,27 @@ def concat_tags(*tag_lists: List[str]):
return all_tags
+def create_topnav(
+ subtitle: str,
+ extra_classes: Optional[List[str]] = None,
+) -> nodes.Node:
+ """Create the nodes for the top title and navigation bar."""
+
+ topnav_classes = (
+ ['pw-topnav'] + extra_classes if extra_classes is not None else []
+ )
+
+ topnav_container = nodes.container(classes=topnav_classes)
+
+ subtitle_node = nodes.paragraph(
+ classes=['pw-topnav-subtitle'],
+ text=subtitle,
+ )
+
+ topnav_container += subtitle_node
+ return topnav_container
+
+
class PigweedModuleDirective(SphinxDirective):
"""Directive registering module metadata, rendering title & info card."""
@@ -65,74 +148,27 @@ class PigweedModuleDirective(SphinxDirective):
'languages': directives.unchanged,
'code-size-impact': directives.unchanged,
'facade': directives.unchanged,
- 'get-started': directives.unchanged_required,
- 'tutorials': directives.unchanged,
- 'guides': directives.unchanged,
- 'concepts': directives.unchanged,
- 'design': directives.unchanged_required,
- 'api': directives.unchanged,
+ 'nav': directives.unchanged_required,
}
- def try_get_option(self, option: str):
- try:
- return self.options[option]
- except KeyError:
- raise self.error(f' :{option}: option is required')
+ def _try_get_option(self, option: str):
+ """Try to get an option by name and raise on failure."""
- def maybe_get_option(self, option: str):
try:
return self.options[option]
except KeyError:
- return None
-
- def create_section_button(self, title: str, ref: str):
- node = docutils.nodes.list_item(classes=['pw-module-section-button'])
- node += ButtonRefDirective(
- name='',
- arguments=[ref],
- options={'color': 'primary'},
- content=[title],
- lineno=0,
- content_offset=0,
- block_text='',
- state=self.state,
- state_machine=self.state_machine,
- ).run()
-
- return node
-
- def register_metadata(self):
- module_name = self.try_get_option('name')
-
- if 'facade' in self.options:
- facade = self.options['facade']
-
- # Initialize the module relationship dict if needed
- if not hasattr(self.env, 'pw_module_relationships'):
- self.env.pw_module_relationships = {}
-
- # Initialize the backend list for this facade if needed
- if facade not in self.env.pw_module_relationships:
- self.env.pw_module_relationships[facade] = []
-
- # Add this module as a backend of the provided facade
- self.env.pw_module_relationships[facade].append(module_name)
-
- if 'is-deprecated' in self.options:
- # Initialize the deprecated modules list if needed
- if not hasattr(self.env, 'pw_modules_deprecated'):
- self.env.pw_modules_deprecated = []
+ raise self.error(f' :{option}: option is required')
- self.env.pw_modules_deprecated.append(module_name)
+ def _maybe_get_option(self, option: str):
+ """Try to get an option by name and return None on failure."""
+ return self.options.get(option, None)
- def run(self):
- tagline = docutils.nodes.paragraph(
- text=self.try_get_option('tagline'),
- classes=['section-subtitle'],
- )
+ def run(self) -> List[nodes.Node]:
+ module_name = self._try_get_option('name')
+ tagline = self._try_get_option('tagline')
status_tags: List[str] = [
- status_badge(self.try_get_option('status')),
+ status_badge(self._try_get_option('status')),
]
if 'is-deprecated' in self.options:
@@ -145,17 +181,25 @@ class PigweedModuleDirective(SphinxDirective):
if len(languages) > 0:
for language in languages:
- language_tags.append(f':bdg-info:`{language.strip()}`')
+ language = language.strip()
+ if language == 'Rust':
+ language_tags.append(
+ f':bdg-link-info:`{language}'
+ + f'</rustdoc/{module_name}>`'
+ )
+ else:
+ language_tags.append(f':bdg-info:`{language}`')
code_size_impact = []
- if code_size_text := self.maybe_get_option('code-size-impact'):
+ if code_size_text := self._maybe_get_option('code-size-impact'):
code_size_impact.append(f'**Code Size Impact:** {code_size_text}')
# Move the directive content into a section that we can render wherever
# we want.
- content = docutils.nodes.paragraph()
- self.state.nested_parse(self.content, 0, content)
+ raw_content = cast(List[str], self.content) # type: ignore
+ content = nodes.paragraph()
+ self.state.nested_parse(raw_content, 0, content)
# The card inherits its content from this node's content, which we've
# already pulled out. So we can replace this node's content with the
@@ -170,55 +214,140 @@ class PigweedModuleDirective(SphinxDirective):
options={},
)
- # Create the top-level section buttons.
- section_buttons = docutils.nodes.bullet_list(
- classes=['pw-module-section-buttons']
+ topbar = create_topnav(
+ tagline,
+ ['pw-module-index'],
)
- # This is the pattern for required sections.
- section_buttons += self.create_section_button(
- 'Get Started', self.try_get_option('get-started')
- )
+ return [topbar, card, content]
- # This is the pattern for optional sections.
- if (tutorials_ref := self.maybe_get_option('tutorials')) is not None:
- section_buttons += self.create_section_button(
- 'Tutorials', tutorials_ref
- )
- if (guides_ref := self.maybe_get_option('guides')) is not None:
- section_buttons += self.create_section_button('Guides', guides_ref)
+class PigweedModuleSubpageDirective(PigweedModuleDirective):
+ """Directive registering module metadata, rendering title & info card."""
- if (concepts_ref := self.maybe_get_option('concepts')) is not None:
- section_buttons += self.create_section_button(
- 'Concepts', concepts_ref
- )
+ required_arguments = 0
+ final_argument_whitespace = True
+ has_content = True
+ option_spec = {
+ 'name': directives.unchanged_required,
+ 'tagline': directives.unchanged_required,
+ 'nav': directives.unchanged_required,
+ }
+
+ def run(self) -> List[nodes.Node]:
+ tagline = self._try_get_option('tagline')
- section_buttons += self.create_section_button(
- 'Design', self.try_get_option('design')
+ topbar = create_topnav(
+ tagline,
+ ['pw-module-subpage'],
)
- if (api_ref := self.maybe_get_option('api')) is not None:
- section_buttons += self.create_section_button(
- 'API Reference', api_ref
- )
+ return [topbar]
+
+
+def _parse_body(body: str) -> ParsedBody:
+ """From the `body` HTML, return the topnav and the body without topnav.
+
+ The fundamental idea is this: Our Sphinx directives can only render nodes
+ *within* the docutils doc, but we want to elevate the top navbar *outside*
+ of that doc into the web theme. Sphinx by itself provides no mechanism for
+ this, since it's model looks something like this:
+
+ ┌──────────────────┐
+ │ Theme │
+ │ ┌──────────────┐│ When Sphinx builds HTML, the output is plain HTML
+ │ │ Sphinx HTML ││ with a structure defined by docutils. Themes can
+ │ │ ││ build *around* that and cascade styles down *into*
+ │ │ ││ that HTML, but there's no mechanism in the Sphinx
+ │ └──────────────┘│ build to render docutils nodes in the theme.
+ └──────────────────┘
+
+ The escape hatch is this:
+ - Render things within the Sphinx HTML output (`body`)
+ - Use Sphinx theme templates to run code during the final render phase
+ - Extract the HTML from the `body` and insert it in the theme via templates
+
+ So this function extracts the things that we rendered in the `body` but
+ actually want in the theme (the top navbar), returns them for rendering in
+ the template, and returns the `body` with those things removed.
+ """
+ if not bs_enabled:
+ return ParsedBody('', body)
+
+ def _add_class_to_tag(tag: HTMLTag, classname: str) -> None:
+ tag['class'] = tag.get('class', []) + [classname] # type: ignore
+
+ def _add_classes_to_tag(
+ tag: HTMLTag, classnames: Union[str, List[str], None]
+ ) -> None:
+ tag['class'] = tag.get('class', []) + classnames # type: ignore
+
+ html = BeautifulSoup(body, features='html.parser')
+
+ # Render the doc unchanged, unless it has the module doc topnav
+ if (topnav := html.find('div', attrs={'class': 'pw-topnav'})) is None:
+ return ParsedBody('', body)
+
+ assert isinstance(topnav, HTMLTag)
+
+ # Find the topnav title and subtitle
+ topnav_title = topnav.find('p', attrs={'class': 'pw-topnav-title'})
+ topnav_subtitle = topnav.find('p', attrs={'class': 'pw-topnav-subtitle'})
+ assert isinstance(topnav_title, HTMLTag)
+ assert isinstance(topnav_subtitle, HTMLTag)
+
+ # Find the single `h1` element, the doc's canonical title
+ doc_title = html.find('h1')
+ assert isinstance(doc_title, HTMLTag)
+
+ topnav_str = ''
+
+ if 'pw-module-index' in topnav['class']:
+ # Take the standard Sphinx/docutils title and add topnav styling
+ _add_class_to_tag(doc_title, 'pw-topnav-title')
+ # Replace the placeholder title in the topnav with the "official" `h1`
+ topnav_title.replace_with(doc_title)
+ # Promote the subtitle to `h2`
+ topnav_subtitle.name = 'h2'
+ # We're done mutating topnav; write it to string for rendering elsewhere
+ topnav_str = str(topnav)
+ # Destroy the instance that was rendered in the document
+ topnav.decompose()
+
+ elif 'pw-module-subpage' in topnav['class']:
+ # Take the title from the topnav (the module name), promote it to `h1`
+ topnav_title.name = 'h1'
+ # Add the heading link, but pointed to the module index page
+ heading_link = html.new_tag(
+ 'a',
+ attrs={
+ 'class': ['headerlink'],
+ 'href': 'docs.html',
+ 'title': 'Permalink to module index',
+ },
+ )
+ heading_link.string = '#'
+ topnav_title.append(heading_link)
+ # Promote the subtitle to `h2`
+ topnav_subtitle.name = 'h2'
+ # We're done mutating topnav; write it to string for rendering elsewhere
+ topnav_str = str(topnav)
+ # Destroy the instance that was rendered in the document
+ topnav.decompose()
- return [tagline, section_buttons, content, card]
+ return ParsedBody(topnav_str, str(html))
-def build_backend_lists(app, _doctree, _fromdocname):
- env = app.builder.env
+def setup_parse_body(_app, _pagename, _templatename, context, _doctree):
+ def parse_body(body: str) -> ParsedBody:
+ return _parse_body(body)
- if not hasattr(env, 'pw_module_relationships'):
- env.pw_module_relationships = {}
+ context['parse_body'] = parse_body
def setup(app: SphinxApplication):
app.add_directive('pigweed-module', PigweedModuleDirective)
-
- # At this event, the documents and metadata have been generated, and now we
- # can modify the doctree to reflect the metadata.
- app.connect('doctree-resolved', build_backend_lists)
+ app.add_directive('pigweed-module-subpage', PigweedModuleSubpageDirective)
return {
'parallel_read_safe': True,
diff --git a/pw_docgen/py/pw_docgen/sphinx/pigweed_live.py b/pw_docgen/py/pw_docgen/sphinx/pigweed_live.py
new file mode 100644
index 000000000..d7382cd38
--- /dev/null
+++ b/pw_docgen/py/pw_docgen/sphinx/pigweed_live.py
@@ -0,0 +1,130 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Docs widget that provides up-to-date info about the next Pigweed Live."""
+
+
+import datetime
+import sys
+from typing import Dict, List
+
+from docutils import nodes
+from docutils.parsers.rst import Directive
+from sphinx.application import Sphinx
+
+try:
+ import pytz # type: ignore
+
+ PYTZ_AVAILABLE = True
+except ImportError:
+ PYTZ_AVAILABLE = False
+
+
+class PigweedLiveDirective(Directive):
+ """Generates the up-to-date Pigweed Live info."""
+
+ datetime_format = '%Y-%m-%d %H:%M:%S'
+ # TODO: b/303859828 - Update this data sometime between 2024-09-23
+ # and 2024-10-07.
+ meetings = [
+ '2023-10-09 13:00:00',
+ '2023-10-23 13:00:00',
+ '2023-11-06 13:00:00',
+ # 2023-11-20 skipped since it's a holiday(ish)
+ '2023-12-04 13:00:00',
+ '2023-12-18 13:00:00',
+ # 2024-01-01 and 2024-01-15 are skipped because they're holidays.
+ '2024-01-29 13:00:00',
+ '2024-02-12 13:00:00',
+ '2024-02-26 13:00:00',
+ '2024-03-11 13:00:00',
+ '2024-03-25 13:00:00',
+ '2024-04-08 13:00:00',
+ '2024-04-22 13:00:00',
+ '2024-05-06 13:00:00',
+ '2024-05-20 13:00:00',
+ '2024-06-03 13:00:00',
+ '2024-06-17 13:00:00',
+ '2024-07-01 13:00:00',
+ '2024-07-15 13:00:00',
+ '2024-07-29 13:00:00',
+ '2024-08-12 13:00:00',
+ '2024-08-26 13:00:00',
+ '2024-09-09 13:00:00',
+ '2024-09-23 13:00:00',
+ '2024-10-07 13:00:00',
+ ]
+ timezone = pytz.timezone('US/Pacific')
+
+ def run(self) -> List[nodes.Node]:
+ return [self._make_paragraph()]
+
+ def _make_paragraph(self) -> nodes.Node:
+ next_meeting = self._find_next_meeting()
+ paragraph = nodes.paragraph()
+ paragraph += nodes.Text('Our next Pigweed Live is ')
+ meeting_text = nodes.strong()
+ meeting_text += nodes.Text(next_meeting)
+ paragraph += meeting_text
+ paragraph += nodes.Text(
+ (
+ ". Please join us to discuss what's new in Pigweed and "
+ "anything else Pigweed-related. Or stop in just to say hi and "
+ "meet the team! You'll find a link for the meeting in the "
+ "#pigweed-live channel of our "
+ )
+ )
+ link = nodes.reference(refuri='https://discord.gg/M9NSeTA')
+ link += nodes.Text('Discord')
+ paragraph += link
+ paragraph += nodes.Text(
+ (
+ '. We meet bi-weekly. The meeting is public. Everyone is '
+ 'welcome to join.'
+ )
+ )
+ return paragraph
+
+ def _find_next_meeting(self) -> str:
+ current_datetime = self.timezone.localize(datetime.datetime.now())
+ next_meeting = None
+ for meeting in self.meetings:
+ unlocalized_datetime = datetime.datetime.strptime(
+ meeting, self.datetime_format
+ )
+ meeting_datetime = self.timezone.localize(unlocalized_datetime)
+ if current_datetime > meeting_datetime:
+ continue
+ next_meeting = meeting_datetime
+ break
+ if next_meeting is None:
+ sys.exit(
+ 'ERROR: Pigweed Live meeting data needs to be updated. '
+ 'Update the `meetings` list in `PigweedLiveDirective`. '
+ 'See b/303859828.'
+ )
+ else:
+ date = next_meeting.strftime('%a %b %d, %Y')
+ hour = next_meeting.strftime('%I%p').lstrip('0')
+ timezone = 'PDT' if next_meeting.dst() else 'PST'
+ return f'{date} {hour} ({timezone})'
+
+
+def setup(app: Sphinx) -> Dict[str, bool]:
+ """Initialize the directive."""
+ if PYTZ_AVAILABLE:
+ app.add_directive('pigweed-live', PigweedLiveDirective)
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/pw_docgen/py/pw_docgen/sphinx/seed_metadata.py b/pw_docgen/py/pw_docgen/sphinx/seed_metadata.py
new file mode 100644
index 000000000..9134081ea
--- /dev/null
+++ b/pw_docgen/py/pw_docgen/sphinx/seed_metadata.py
@@ -0,0 +1,133 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Sphinx directives for Pigweed SEEDs"""
+
+from typing import List
+
+import docutils
+from docutils import nodes
+import docutils.statemachine
+
+# pylint: disable=consider-using-from-import
+import docutils.parsers.rst.directives as directives # type: ignore
+
+# pylint: enable=consider-using-from-import
+from sphinx.application import Sphinx as SphinxApplication
+from sphinx.util.docutils import SphinxDirective
+
+from sphinx_design.cards import CardDirective
+
+
+def status_choice(arg) -> str:
+ return directives.choice(
+ arg, ('open_for_comments', 'last_call', 'accepted', 'rejected')
+ )
+
+
+def parse_status(arg) -> str:
+ """Support variations on the status choices.
+
+ For example, you can use capital letters and spaces.
+ """
+
+ return status_choice('_'.join([token.lower() for token in arg.split(' ')]))
+
+
+def status_badge(seed_status: str, badge_status) -> str:
+ """Given a SEED status, return the status badge for rendering."""
+
+ return (
+ ':bdg-primary:'
+ if seed_status == badge_status
+ else ':bdg-secondary-line:'
+ )
+
+
+def cl_link(cl_num):
+ return (
+ f'`pwrev/{cl_num} '
+ '<https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/'
+ f'{cl_num}>`_'
+ )
+
+
+class PigweedSeedDirective(SphinxDirective):
+ """Directive registering & rendering SEED metadata."""
+
+ required_arguments = 0
+ final_argument_whitespace = True
+ has_content = True
+ option_spec = {
+ 'number': directives.positive_int,
+ 'name': directives.unchanged_required,
+ 'status': parse_status,
+ 'proposal_date': directives.unchanged_required,
+ 'cl': directives.positive_int_list,
+ }
+
+ def _try_get_option(self, option: str):
+ """Try to get an option by name and raise on failure."""
+
+ try:
+ return self.options[option]
+ except KeyError:
+ raise self.error(f' :{option}: option is required')
+
+ def run(self) -> List[nodes.Node]:
+ seed_number = '{:04d}'.format(self._try_get_option('number'))
+ seed_name = self._try_get_option('name')
+ status = self._try_get_option('status')
+ proposal_date = self._try_get_option('proposal_date')
+ cl_nums = self._try_get_option('cl')
+
+ title = (
+ f':fas:`seedling` SEED-{seed_number}: :ref:'
+ f'`{seed_name}<seed-{seed_number}>`\n'
+ )
+
+ self.content = docutils.statemachine.StringList(
+ [
+ ':octicon:`comment-discussion` Status:',
+ f'{status_badge(status, "open_for_comments")}'
+ '`Open for Comments`',
+ ':octicon:`chevron-right`',
+ f'{status_badge(status, "last_call")}`Last Call`',
+ ':octicon:`chevron-right`',
+ f'{status_badge(status, "accepted")}`Accepted`',
+ ':octicon:`kebab-horizontal`',
+ f'{status_badge(status, "rejected")}`Rejected`',
+ '\n',
+ f':octicon:`calendar` Proposal Date: {proposal_date}',
+ '\n',
+ ':octicon:`code-review` CL: ',
+ ', '.join([cl_link(cl_num) for cl_num in cl_nums]),
+ ]
+ )
+
+ card = CardDirective.create_card(
+ inst=self,
+ arguments=[title],
+ options={},
+ )
+
+ return [card]
+
+
+def setup(app: SphinxApplication):
+ app.add_directive('seed', PigweedSeedDirective)
+
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/pw_docgen/py/setup.cfg b/pw_docgen/py/setup.cfg
index 41f7505ef..c139c9633 100644
--- a/pw_docgen/py/setup.cfg
+++ b/pw_docgen/py/setup.cfg
@@ -24,7 +24,6 @@ zip_safe = False
install_requires =
sphinx>=5.3.0
sphinx-argparse
- sphinx-rtd-theme
sphinxcontrib-mermaid>=0.7.1
sphinx-design>=0.3.0
diff --git a/pw_docgen/py/setup.py b/pw_docgen/py/setup.py
deleted file mode 100644
index 02300488c..000000000
--- a/pw_docgen/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_docgen"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_doctor/docs.rst b/pw_doctor/docs.rst
index 3ed403dae..bc0ad582a 100644
--- a/pw_doctor/docs.rst
+++ b/pw_doctor/docs.rst
@@ -28,3 +28,25 @@ Checks carried out by pw_doctor include:
.. note::
The documentation for this module is currently incomplete.
+
+Configuration
+=============
+Options for ``pw doctor`` can be specified in the ``pigweed.json`` file
+(see also :ref:`SEED-0101 <seed-0101>`). This is currently limited to one
+option.
+
+* ``new_bug_url``: What link is given to users be given for filing bugs. By
+ default this is to the `Pigweed Bug Tracker_`.
+
+.. _Pigweed Bug Tracker: https://issues.pigweed.dev/new
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_doctor": {
+ "new_bug_url": "https://example.com/bugs/new"
+ }
+ }
+ }
+
diff --git a/pw_doctor/py/BUILD.gn b/pw_doctor/py/BUILD.gn
index f3b2101d4..4abe981e5 100644
--- a/pw_doctor/py/BUILD.gn
+++ b/pw_doctor/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_doctor/__init__.py",
diff --git a/pw_doctor/py/pw_doctor/doctor.py b/pw_doctor/py/pw_doctor/doctor.py
index cf3f8651f..3e10a7ecd 100755
--- a/pw_doctor/py/pw_doctor/doctor.py
+++ b/pw_doctor/py/pw_doctor/doctor.py
@@ -28,6 +28,7 @@ from typing import Callable, Iterable, List, Optional, Set
import pw_cli.pw_command_plugins
import pw_env_setup.cipd_setup.update as cipd_update
+from pw_env_setup import config_file
def call_stdout(*args, **kwargs):
@@ -210,11 +211,19 @@ def pw_root(ctx: DoctorContext):
)
git_root = git_root.resolve()
if root != git_root:
- ctx.error(
- 'PW_ROOT (%s) != `git rev-parse --show-toplevel` (%s)',
- root,
- git_root,
- )
+ if str(root).lower() != str(git_root).lower():
+ ctx.error(
+ 'PW_ROOT (%s) != `git rev-parse --show-toplevel` (%s)',
+ root,
+ git_root,
+ )
+ else:
+ ctx.warning(
+ 'PW_ROOT (%s) differs in case from '
+ '`git rev-parse --show-toplevel` (%s)',
+ root,
+ git_root,
+ )
@register_into(CHECKS)
@@ -483,6 +492,9 @@ def symlinks(ctx: DoctorContext):
def run_doctor(strict=False, checks=None):
"""Run all the Check subclasses defined in this file."""
+ config = config_file.load().get('pw', {}).get('pw_doctor', {})
+ new_bug_url = config.get('new_bug_url', 'https://issues.pigweed.dev/new')
+
if checks is None:
checks = tuple(CHECKS)
@@ -496,8 +508,9 @@ def run_doctor(strict=False, checks=None):
doctor.log.info(
"Your environment setup has completed, but something isn't right "
'and some things may not work correctly. You may continue with '
- 'development, but please seek support at '
- 'https://issues.pigweed.dev/new or by reaching out to your team.'
+ 'development, but please seek support at %s or by '
+ 'reaching out to your team.',
+ new_bug_url,
)
else:
doctor.log.info('Environment passes all checks!')
diff --git a/pw_doctor/py/setup.py b/pw_doctor/py/setup.py
deleted file mode 100644
index d8ec21631..000000000
--- a/pw_doctor/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_doctor"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_emu/BUILD.gn b/pw_emu/BUILD.gn
new file mode 100644
index 000000000..081dfe40a
--- /dev/null
+++ b/pw_emu/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ inputs = [
+ "qemu-lm3s6965evb.json",
+ "renode-stm32f4_discovery.json",
+ ]
+ sources = [
+ "api.rst",
+ "cli.rst",
+ "config.rst",
+ "design.rst",
+ "docs.rst",
+ "guide.rst",
+ ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_emu/OWNERS b/pw_emu/OWNERS
new file mode 100644
index 000000000..1d0c39143
--- /dev/null
+++ b/pw_emu/OWNERS
@@ -0,0 +1,2 @@
+keir@google.com
+tavip@google.com
diff --git a/pw_emu/README.md b/pw_emu/README.md
new file mode 100644
index 000000000..1a5894c3e
--- /dev/null
+++ b/pw_emu/README.md
@@ -0,0 +1,2 @@
+This directory contains the `pw emu` subcommand and python host APIs for
+emulator instances management.
diff --git a/pw_emu/api.rst b/pw_emu/api.rst
new file mode 100644
index 000000000..4ba2a8a70
--- /dev/null
+++ b/pw_emu/api.rst
@@ -0,0 +1,19 @@
+.. _module-pw_emu-api:
+
+=============
+API reference
+=============
+.. pigweed-module-subpage::
+ :name: pw_emu
+ :tagline: Emulators frontend
+
+.. automodule:: pw_emu.frontend
+ :members:
+
+.. automodule:: pw_emu.core
+ :noindex:
+ :members:
+
+.. autoclass:: pw_emu.core.Launcher
+ :members:
+ :private-members: _config, _handles, _pre_start, _post_start, _get_connector
diff --git a/pw_emu/cli.rst b/pw_emu/cli.rst
new file mode 100644
index 000000000..7b2d75b59
--- /dev/null
+++ b/pw_emu/cli.rst
@@ -0,0 +1,16 @@
+.. _module-pw_emu-cli:
+
+=============
+CLI reference
+=============
+.. pigweed-module-subpage::
+ :name: pw_emu
+ :tagline: Pigweed emulator frontend
+
+.. argparse::
+ :module: pw_emu.__main__
+ :func: get_parser
+ :prog: pw emu
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
diff --git a/pw_emu/config.rst b/pw_emu/config.rst
new file mode 100644
index 000000000..41d1b2fe4
--- /dev/null
+++ b/pw_emu/config.rst
@@ -0,0 +1,155 @@
+.. _module-pw_emu-config:
+
+=============
+Configuration
+=============
+.. pigweed-module-subpage::
+ :name: pw_emu
+ :tagline: Emulators frontend
+
+The emulators configuration is part of the Pigweed root configuration file
+(``pigweed.json``) and reside in the ``pw:pw_emu`` namespace.
+
+Projects can define emulation targets in the Pigweed root configuration file and
+can also import predefined targets from other files (configuration
+fragments). The ``pw_emu`` module provides a set of targets as examples and to
+promote reusability.
+
+The target configuration allows users to start other programs before
+or after starting the main emulator process. This allows extending the
+emulated target with simulation or emulation outside of the main
+emulator. For example, for BLE emulation the main emulator could
+emulate just the serial port while the HCI emulation done is in an
+external program (e.g. `bumble <https://google.github.io/bumble>`_,
+`netsim <https://android.googlesource.com/platform/tools/netsim>`_).
+
+.. _module-pw_emu-config-options:
+
+-----------------------
+Configuration reference
+-----------------------
+* ``gdb``: top level gdb command as a list of strings
+ (e.g. ``[gdb-multiarch]``); this can be overridden at the target level
+
+* ``target_files``: list of paths to json files to include targets from; the
+ target configuration should be placed under a ``target`` key; if paths are
+ relative they are interpreted relative to the project root directory
+
+* ``qemu``: options for the QEMU emulator:
+
+ * ``executable`: command used to start QEMU (e.g. `system-arm-qemu``); this
+ can be overridden at the target level
+
+ * ``args``: list of command line options to pass directly to QEMU
+ when starting an emulator instance; can be *extended* at the
+ target level
+
+ * ``channels``: options for channel configuration:
+
+ * ``type``: optional type for channels, see channel types below
+
+ * ``gdb``, ``qmp``, ``monitor``: optional channel configuration entries
+
+ * ``type``: channel type, see channel types below
+
+* ``renode``: options for the renode emulator:
+
+ * ``executable``: command used to start renode (e.g. "system-arm-qemu"); this
+ can be overridden at the target level
+
+ * ``channels``: options for channel configuration:
+
+ * ``terminals``: exposed terminal devices (serial ports) generic options:
+
+ * ``type``: optional type for channels, see channel types below
+
+* ``targets``: target configurations with target names as keys:
+
+ * ``<target-name>``: target options:
+
+ * ``gdb``: gdb command as a list of strings (e.g. ``[arm-none-eabi-gdb]``)
+
+ * ``pre-start-cmds``: commands to run before the emulator is started
+
+ * ``<name>``: command for starting the process as a list of strings
+
+ * ``post-start-cmds``: processes to run after the emulator is started
+
+ * ``<name>``: command for starting the process as a list of strings
+
+ * ``qemu``: options for the QEMU emulator:
+
+ * ``executable``: command used to start QEMU (e.g. ``system-arm-qemu``)
+
+ * ``args``: list of command line options passed directly to QEMU when
+ starting this target
+
+ * ``machine``: QEMU machine name; passed to the QEMU ``-machine`` command
+ line argument
+
+ * ``channels``: exposed channels:
+
+ * ``chardevs``: exposed QEMU chardev devices (typically serial
+ ports):
+
+ * ``<channel-name>``: channel options:
+
+ * ``id``: the id of the QEMU chardev
+
+ * ``type``: optional type of the channel see channel types below
+
+ * ``gdb``, ``qmp`, ``monitor``: optional channel configuration
+ entries
+
+ * ``type``: channel type, see channel types below
+
+ * ``renode``: options for the renode emulator:
+
+ * ``executable``: command used to start renode
+
+ * ``machine``: machine script to use when running this target
+
+ * ``channels``: exposed channels:
+
+ * ``terminals``: exposed terminal devices (serial ports):
+
+ * ``<channel-name>``: channel options:
+
+ * ``device-path``: device path
+
+ * ``type``: optional type of the channel see channel types below
+
+The following channel types are currently supported:
+
+* ``pty``: supported on Mac and Linux; renode only supports ptys for
+ ``terminals`` channels
+
+* ``tcp``: supported on all platforms and for all channels; it is also the
+ default type if no channel type is configured
+
+The channel configuration can be set at multiple levels: emulator, target, or
+specific channel. The channel configuration takes precedence, then the target
+channel configuration then the emulator channel configuration.
+
+The following expressions are substituted in the ``pre-start-cmd`` and
+``post-start-cmd`` strings:
+
+* ``$pw_emu_wdir{relative-path}``: replaces statement with an absolute path
+ by concatenating the emulator's working directory with the given relative path
+
+* ``$pw_emu_channel_port{channel-name}``: replaces the statement with the port
+ number for the given channel name; the channel type should be ``tcp``
+
+* ``$pw_emu_channel_host{channel-name}``: replaces the statement with the host
+ for the given channel name; the channel type should be ``tcp``
+
+* ``$pw_emu_channel_path{channel-name}``: replaces the statement with the path
+ for the given channel name; the channel type should be ``pty``
+
+
+The followng expressions are substituted in configuration strings, including
+configuration framents:
+
+* ``$pw_env{envvar}``: replaces statement with the value of the ``envar``
+ environment variable; if the variable does not exists in the environment a
+ configuration error is raised
diff --git a/pw_emu/design.rst b/pw_emu/design.rst
new file mode 100644
index 000000000..67e220729
--- /dev/null
+++ b/pw_emu/design.rst
@@ -0,0 +1,103 @@
+.. _module-pw_emu-design:
+
+======
+Design
+======
+.. pigweed-module-subpage::
+ :name: pw_emu
+ :tagline: Emulators frontend
+
+Multiple instances are supported in order to enable developers who work on
+multiple downstream Pigweed projects to work unhindered and also to run multiple
+test instances in parallel on the same machine.
+
+Each instance is identified by a system absolute path that is also used to store
+state about the running instance such as pid files for running processes,
+current emulator and target, etc. This directory also contains information about
+how to access the emulator channels (e.g. socket ports, pty paths)
+
+.. mermaid::
+
+ graph TD;
+ TemporaryEmulator & pw_emu_cli[pw emu cli] <--> Emulator
+ Emulator <--> Launcher & Connector
+ Launcher <--> Handles
+ Connector <--> Handles
+ Launcher <--> Config
+ Handles --Save--> WD --Load--> Handles
+ WD[Working Directory]
+
+
+The implementation uses the following classes:
+
+* :py:class:`pw_emu.frontend.Emulator`: the user visible API
+
+* :py:class:`pw_emu.core.Launcher`: an abstract class that starts an
+ emulator instance for a given configuration and target
+
+* :py:class:`pw_emu.core.Connector`: an abstract class that is the
+ interface between a running emulator and the user visible APIs
+
+* :py:class:`pw_emu.core.Handles`: class that stores specific
+ information about a running emulator instance such as ports to reach emulator
+ channels; it is populated by :py:class:`pw_emu.core.Launcher` and
+ saved in the working directory and used by
+ :py:class:`pw_emu.core.Connector` to access the emulator channels,
+ process pids, etc.
+
+* :py:class:`pw_emu.core.Config`: loads the pw_emu configuration and provides
+ helper methods to get and validate configuration options
+
+-------------------
+Emulator properties
+-------------------
+The implementation exposes the ability to list, read and write emulator
+properties. The frontend does not abstract properties in a way that is emulator
+or even emulator target independent, other than mandating that each property is
+identified by a path. Note that the format of the path is also emulator specific
+and not standardized.
+
+----
+QEMU
+----
+The QEMU frontend is using `QMP <https://wiki.qemu.org/Documentation/QMP>`_ to
+communicate with the running QEMU process and implement emulator specific
+functionality like reset, list or reading properties, etc.
+
+QMP is exposed to the host through two channels: a temporary one to establish
+the initial connection that is used to read the dynamic configuration (e.g. TCP
+ports, pty paths) and a permanent one that can be used thought the life of the
+QEMU processes. The frontend is configuring QEMU to expose QMP to a
+``localhost`` TCP port reserved by the frontend and then waiting for QEMU to
+establish the connection on that port. Once the connection is established the
+frontend will read the configuration of the permanent QMP channel (which can be
+either a TCP port or a PTY path) and save it as a channel named ``qmp`` in the
+:py:class:`pw_emu.core.Handles` object.
+
+------
+renode
+------
+The renode frontend is using `renode's robot port
+<https://renode.readthedocs.io/en/latest/introduction/testing.html>`_ to
+interact with the renode process. Although the robot interface is designed for
+testing and not as a control interface, it is more robust and better suited to
+be used as a machine interface than the alternative ``monitor`` interface which
+is user oriented, ANSI colored, echoed, log mixed, telnet interface.
+
+Bugs
+====
+While renode allows passing 0 for ports to allocate a dynamic port, it does not
+have APIs to retrieve the allocated port. Until support for such a feature is
+added upstream, the implementation is using the following technique to allocate
+a port dynamically:
+
+.. code-block:: python
+
+ sock = socket.socket(socket.SOCK_INET, socket.SOCK_STREAM)
+ sock.bind(('', 0))
+ _, port = socket.getsockname()
+ sock.close()
+
+There is a race condition that allows another program to fetch the same port,
+but it should work in most light use cases until the issue is properly resolved
+upstream.
diff --git a/pw_emu/docs.rst b/pw_emu/docs.rst
new file mode 100644
index 000000000..6b251ec4d
--- /dev/null
+++ b/pw_emu/docs.rst
@@ -0,0 +1,80 @@
+.. _module-pw_emu:
+
+.. rst-class:: with-subtitle
+
+======
+pw_emu
+======
+.. pigweed-module::
+ :name: pw_emu
+ :tagline: Emulators frontend
+ :status: experimental
+
+--------
+Overview
+--------
+``pw_emu`` is an emulators frontend with the following features:
+
+* It allows users to define an emulation target that encapsulates the emulated
+ machine configuration, the tools configuration and the host channels
+ configuration.
+
+* It provides a command line interface that manages multiple emulator instances
+ and provides interactive access to the emulator's host channels.
+
+* It provides a Python API to control emulator instances and access the
+ emulator's host channels (e.g. serial ports).
+
+* It supports multiple emulators, QEMU and renode.
+
+* It expose channels for gdb, monitor and user selected devices through
+ configurable host resources like sockets and ptys.
+
+For background on why the module exists and some of the design
+choices see :ref:`seed-0108`.
+
+.. _module-pw_emu-get-started:
+
+-----------
+Get started
+-----------
+Include the desired emulator target files in the ``pigweed.json`` configuration
+file, e.g.:
+
+.. code-block::
+
+ ...
+ "pw_emu": {
+ "target_files": [
+
+ "pw_emu/qemu-lm3s6965evb.json",
+ "pw_emu/qemu-stm32vldiscovery.json",
+ "pw_emu/qemu-netduinoplus2.json",
+ "renode-stm32f4_discovery.json"
+ ]
+ }
+ ...
+
+
+Build the ``qemu_gcc`` target and use ``pw emu run`` command to run the target
+binaries on the host using the ``qemu-lm3s6965evb`` emulation target:
+
+.. code:: bash
+
+ ninja -C out qemu_gcc
+ pw emu run --args=-no-reboot qemu-lm3s6965evb out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_snapshot/test/cpp_compile_test
+
+See :ref:`module-pw_emu-guide` for more examples,
+:ref:`module-pw_emu-config` for detailed configuration information,
+:ref:`module-pw_emu-cli` for detailed CLI usage information and
+:ref:`module-pw_emu-api` for managing emulators with Python APIs.
+
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+
+ guide
+ config
+ cli
+ api
+ design
diff --git a/pw_emu/guide.rst b/pw_emu/guide.rst
new file mode 100644
index 000000000..87e2a080e
--- /dev/null
+++ b/pw_emu/guide.rst
@@ -0,0 +1,285 @@
+.. _module-pw_emu-guide:
+
+============
+How-to guide
+============
+.. pigweed-module-subpage::
+ :name: pw_emu
+ :tagline: Emulators frontend
+
+This guide shows you how to do common ``pw_emu`` tasks.
+
+------------------------
+Set up emulation targets
+------------------------
+An emulator target can be defined in the top level ``pigweed.json`` under
+``pw:pw_emu:target`` or defined in a separate json file under ``targets`` and
+then included in the top level ``pigweed.json`` via ``pw:pw_emu:target_files``.
+
+For example, for the following layout:
+
+.. code-block::
+
+ ├── pigweed.json
+ └── pw_emu
+ └── qemu-lm3s6965evb.json
+
+the following can be added to the ``pigweed.json`` file:
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_emu": {
+ "target_files": [
+ "pw_emu/qemu-lm3s6965evb.json"
+ ]
+ }
+ }
+ }
+
+The following subsections presents examples of how to define new emulation
+targets for QEMU and renode. For detailed emulation target configuration please
+see :ref:`module-pw_emu-config`.
+
+QEMU targets
+============
+When defining a QEMU emulation target the following keys have to be defined
+under ``<target>:qemu``:
+
+ * ``executable``: name of the QEMU executable, e.g. ``qemu-system-arm``,
+ ``qemusystem-riscv64``, etc.
+
+ * ``machine``: the QEMU machine name; see ``qemu-system-<arch> -machine help``
+ for a list of supported machines names
+
+Here is an example that defines the ``qemu-lm3s6965evb`` target as configuration
+fragment:
+
+.. literalinclude:: qemu-lm3s6965evb.json
+
+Since this is an ARM machine note that QEMU executable is defined as
+``qemu-system-arm``.
+
+QEMU chardevs can be exposed as host channels under
+``<target>:qemu:channels:chardevs:<chan-name>`` where ``chan-name`` is
+the name that the channel can be accessed with (e.g. ``pw emu term
+<chan-name>``). The ``id`` option is mandatory and it should match a valid
+chardev id for the particular QEMU machine.
+
+This target emulates a `Stellaris EEVB <https://www.ti.com/product/LM3S6965>`_
+and is compatible with the :ref:`target-lm3s6965evb-qemu` Pigweed target. The
+configuration defines a ``serial0`` channel to be the QEMU **chardev** with the
+``serial0`` id. The default type of the channel is used, which is TCP and which is
+supported by all platforms. The user can change the type by adding a ``type`` key
+set to the desired type (e.g. ``pty``)
+
+renode targets
+==============
+The following configuration fragment defines a target that uses renode:
+
+.. literalinclude:: renode-stm32f4_discovery.json
+
+Note that ``machine`` is used to identify which renode script to use for the
+machine definitions and ``terminals`` to define which UART devices to expose to
+the host.
+
+This target emulates the `ST 32F429I Discovery
+kit <https://www.st.com/en/evaluation-tools/32f429idiscovery.html>`_ and is
+compatible with the :ref:`target-stm32f429i-disc1` Pigweed target. The
+configuration defines a ``serial0`` channel as the serial port identified as
+``sysbus.usart1`` in the renode machine definition script.
+
+-------------------
+Run target binaries
+-------------------
+To quickly run target binaries on the host using an emulation target the ``pw
+emu run`` command can be used. It will start an emulator instance, connect to a
+(given) serial port and load and run the given binary.
+
+.. code-block::
+
+ $ pw emu run --args=-no-reboot qemu-lm3s6965evb out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_snapshot/test/cpp_compile_test
+
+ --- Miniterm on serial0 ---
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
+ INF [==========] Running all tests.
+ INF [ RUN ] Status.CompileTest
+ INF [ OK ] Status.CompileTest
+ INF [==========] Done running all tests.
+ INF [ PASSED ] 1 test(s).
+ --- exit ---
+
+Note the ``-no-reboot`` option is passed directly to QEMU and instructs the
+emulator to quit instead of rebooting.
+
+---------
+Debugging
+---------
+Debugging target binaries can be done using the ``pw emu gdb`` command. It
+requires a running emulator instance which can be started with the ``pw emu
+start`` command.
+
+In the following example we are going to debug the ``status_test`` from the
+``pw_status`` module compiled for :ref:`target-lm3s6965evb-qemu`. First we are
+going to start an emulator instance using the ``qemu-lm3s6965evb`` emulator
+target and load the test file:
+
+.. code-block::
+
+ $ pw emu start qemu-lm3s6965evb \
+ --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
+
+
+Next, we will start a gdb session connected to the emulator instance:
+
+.. code-block::
+
+ $ pw emu gdb -e out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
+ GNU gdb (Arm GNU Toolchain 12.2.MPACBTI-Rel1 ...
+ ...
+ Reading symbols from out/stm32f429i_disc1_debug/obj/pw_status/test/status_test.elf...
+ Remote debugging using ::1:32979
+ pw::sys_io::WriteByte (b=(unknown: 0x20)) at pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc:117
+ 117 uart0.data_register = static_cast<uint32_t>(b);
+ (gdb) bt
+ #0 pw::sys_io::WriteByte (b=(unknown: 0x20)) at pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc:117
+ #1 0x00002f6a in pw::sys_io::WriteBytes (src=...) at pw_span/public/pw_span/internal/span_impl.h:408
+ #2 0x00002eca in pw::sys_io::WriteLine (s=...) at pw_span/public/pw_span/internal/span_impl.h:264
+ #3 0x00002f92 in operator() (log=..., __closure=0x0 <vector_table>) at pw_log_basic/log_basic.cc:87
+ #4 _FUN () at pw_log_basic/log_basic.cc:89
+ #5 0x00002fec in pw::log_basic::pw_Log (level=<optimized out>, flags=<optimized out>, module_name=<optimized out>, file_name=<optimized out>, line_number=95,
+ function_name=0x6e68 "TestCaseStart", message=0x6e55 "[ RUN ] %s.%s") at pw_log_basic/log_basic.cc:155
+ #6 0x00002b0a in pw::unit_test::LoggingEventHandler::TestCaseStart (this=<optimized out>, test_case=...) at pw_unit_test/logging_event_handler.cc:95
+ #7 0x00000f54 in pw::unit_test::internal::Framework::CreateAndRunTest<pw::(anonymous namespace)::Status_Strings_Test> (test_info=...)
+ at pw_unit_test/public/pw_unit_test/internal/framework.h:266
+ #8 0x0000254a in pw::unit_test::internal::TestInfo::run (this=0x20000280 <_pw_unit_test_Info_Status_Strings>)
+ at pw_unit_test/public/pw_unit_test/internal/framework.h:413
+ #9 pw::unit_test::internal::Framework::RunAllTests (this=0x20000350 <pw::unit_test::internal::Framework::framework_>) at pw_unit_test/framework.cc:64
+ #10 0x000022b0 in main (argc=<optimized out>, argv=<optimized out>) at pw_unit_test/public/pw_unit_test/internal/framework.h:218
+
+At this point gdb commands can be used to debug the program.
+
+Once the debugging session is over the emulator instance should be stopped:
+
+.. code-block::
+
+ $ pw emu stop
+
+--------------
+Boot debugging
+--------------
+To debug bootstraping code the ``-p`` option can be used when the emulator is
+started:
+
+.. code-block::
+
+ $ pw emu start -p qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
+
+That will load the given program but the emulator will not start executing. Next, we can start a debugging session using the ``pw emu gdb`` commands:
+
+.. code-block::
+
+ $ pw emu gdb -e out/lm3s6965evb_qemu_gcc_size_optimized//obj/pw_status/test/status_test
+ GNU gdb (Arm GNU Toolchain 12.2.MPACBTI-Rel1 ...
+ ...
+ Reading symbols from out/lm3s6965evb_qemu_gcc_size_optimized//obj/pw_status/test/status_test...
+ Remote debugging using ::1:38723
+ pw_boot_Entry () at pw_boot_cortex_m/core_init.c:122
+ 122 asm volatile("cpsid i");
+ (gdb)
+
+Note that the program is stopped at the ``pw_boot_Entry`` function. From here
+you can add breakpoints or step through the program with gdb commands.
+
+------------------------
+Using multiple instances
+------------------------
+To use multiple emulator instances the ``--instance <instance-id>`` option can
+be used. The default ``pw emu`` instance id is ``default``.
+
+.. note::
+ Internally each emulator instance is identified by a working directory. pw
+ emu's working directory is ``$PROJECT_ROOT/.pw_emu/<instance-id>``
+
+
+As an example, we will try to start the same emulator instance twice:
+
+.. code-block::
+
+ $ pw emu start qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
+ $ pw emu start qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
+ pigweed/.pw_emu/default: emulator already started
+
+Note that the second command failed because the default emulator instance is
+already running. To start another emulator instance we will use the
+``--instance`` or ``-i`` option:
+
+
+.. code-block::
+
+ $ pw emu -i my-instance start qemu-lm3s6965evb --file out/lm3s6965evb_qemu_gcc_size_optimized/obj/pw_status/test/status_test
+
+
+Note that no error was triggered this time. Finally, lets stop both emulator
+instances:
+
+.. code-block::
+
+ $ pw emu stop -i my-instance
+ $ pw emu stop
+
+-------------------------
+Adding new emulator types
+-------------------------
+The pw_emu module can be extended to support new emulator types by providing
+implementations for :py:class:`pw_emu.core.Launcher` and
+:py:class:`pw_emu.core.Connector` in a dedicated ``pw_emu`` Python module
+(e.g. :py:mod:`pw_emu.myemu`) or in an external Python module.
+
+Internal ``pw_emu`` modules must register the connector and launcher
+classes by updating
+:py:obj:`pw_emu.pigweed_emulators.pigweed_emulators`. For example, the
+QEMU implementation sets the following values:
+
+.. code-block:: python
+
+ pigweed_emulators: Dict[str, Dict[str, str]] = {
+ ...
+ 'qemu': {
+ 'connector': 'pw_emu.qemu.QemuConnector',
+ 'launcher': 'pw_emu.qemu.QemuLauncher',
+ },
+ ...
+
+For external emulator frontend modules ``pw_emu`` is using the Pigweed
+configuration file to determine the connector and launcher classes, under the
+following keys: ``pw_emu:emulators:<emulator_name>:connector`` and
+``pw_emu:emulators:<emulator_name>:connector``. Configuration example:
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_emu": {
+ "emulators": [
+ "myemu": {
+ "launcher": "mypkg.mymod.mylauncher",
+ "connector": "mypkg.mymod.myconnector",
+ }
+ ]
+ }
+ }
+ }
+
+The :py:class:`pw_emu.core.Launcher` implementation must implement the following
+methods: :py:meth:`pw_emu.core.Launcher._pre_start`,
+:py:class:`pw_emu.core.Launcher._post_start` and
+:py:class:`pw_emu.core.Launcher._get_connector`.
+
+There are several abstract methods that need to be implement for the connector,
+like :py:meth:`pw_emu.core.Connector.reset` or
+:py:meth:`pw_emu.core.Connector.cont`. These are typically implemented using
+internal channels and :py:class:`pw_emu.core.Connector.get_channel_stream`. See
+:py:class:`pw_emu.core.Connector` for a complete list of abstract methods that
+need to be implemented.
diff --git a/pw_emu/py/BUILD.gn b/pw_emu/py/BUILD.gn
new file mode 100644
index 000000000..bcf26e44e
--- /dev/null
+++ b/pw_emu/py/BUILD.gn
@@ -0,0 +1,68 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
+ sources = [
+ "pw_emu/__init__.py",
+ "pw_emu/__main__.py",
+ "pw_emu/core.py",
+ "pw_emu/frontend.py",
+ "pw_emu/pigweed_emulators.py",
+ "pw_emu/qemu.py",
+ "pw_emu/renode.py",
+ ]
+ tests = [
+ "tests/__init__.py",
+ "tests/cli_test.py",
+ "tests/common.py",
+ "tests/core_test.py",
+ "tests/frontend_test.py",
+ "tests/qemu_test.py",
+ "tests/renode_test.py",
+ ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_build/py",
+ "$dir_pw_env_setup/py",
+ ]
+}
+
+pw_python_script("mock_emu") {
+ sources = [ "mock_emu.py" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ action = {
+ stamp = true
+ }
+}
+
+pw_python_script("mock_emu_frontend") {
+ sources = [ "mock_emu_frontend.py" ]
+ python_deps = [ "$dir_pw_env_setup/py" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ action = {
+ stamp = true
+ }
+}
diff --git a/pw_emu/py/mock_emu.py b/pw_emu/py/mock_emu.py
new file mode 100644
index 000000000..1a49b2ba2
--- /dev/null
+++ b/pw_emu/py/mock_emu.py
@@ -0,0 +1,111 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Mock emulator used for testing process and channel management."""
+
+import argparse
+import os
+import socket
+import sys
+import time
+
+from threading import Thread
+
+
+def _tcp_thread(sock: socket.socket) -> None:
+ conn, _ = sock.accept()
+ while True:
+ data = conn.recv(1)
+ conn.send(data)
+
+
+def _pty_thread(fd: int) -> None:
+ while True:
+ data = os.read(fd, 1)
+ os.write(fd, data)
+
+
+def _get_parser() -> argparse.ArgumentParser:
+ """Command line parser."""
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '-C', '--working-dir', metavar='PATH', help='working directory'
+ )
+ parser.add_argument(
+ 'echo', metavar='STRING', nargs='*', help='write STRING to stdout'
+ )
+ parser.add_argument(
+ '--tcp-channel',
+ action='append',
+ default=[],
+ metavar='NAME',
+ help='listen for TCP connections, write port WDIR/NAME',
+ )
+ if sys.platform != 'win32':
+ parser.add_argument(
+ '--pty-channel',
+ action='append',
+ default=[],
+ metavar='NAME',
+ help='create pty channel and link in WDIR/NAME',
+ )
+ parser.add_argument(
+ '--exit', action='store_true', default=False, help='exit when done'
+ )
+
+ return parser
+
+
+def main() -> None:
+ """Mock emulator."""
+
+ args = _get_parser().parse_args()
+
+ if len(args.echo) > 0:
+ print(' '.join(args.echo))
+ sys.stdout.flush()
+
+ threads = []
+
+ for chan in args.tcp_channel:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.bind(('localhost', 0))
+ port = sock.getsockname()[1]
+ sock.listen()
+ with open(os.path.join(args.working_dir, chan), 'w') as file:
+ file.write(str(port))
+ thread = Thread(target=_tcp_thread, args=(sock,))
+ thread.start()
+ threads.append(thread)
+
+ if sys.platform != 'win32':
+ for chan in args.pty_channel:
+ controller, tty = os.openpty()
+ with open(os.path.join(args.working_dir, chan), 'w') as file:
+ file.write(os.ttyname(tty))
+ thread = Thread(target=_pty_thread, args=(controller,))
+ thread.start()
+ threads.append(thread)
+
+ for thread in threads:
+ thread.join()
+
+ if not args.exit:
+ while True:
+ time.sleep(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_emu/py/mock_emu_frontend.py b/pw_emu/py/mock_emu_frontend.py
new file mode 100644
index 000000000..5ab370ac9
--- /dev/null
+++ b/pw_emu/py/mock_emu_frontend.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launcher and Connector for mock emulator"""
+
+import json
+import os
+from pathlib import Path
+from typing import Any, Optional, List, Union
+import time
+
+from pw_emu.core import (
+ Connector,
+ Launcher,
+ InvalidProperty,
+ InvalidPropertyPath,
+)
+
+# mock emulator script
+_mock_emu = [
+ 'python',
+ os.path.join(Path(os.path.dirname(__file__)).resolve(), 'mock_emu.py'),
+]
+
+
+def wait_for_file_size(
+ path: Union[os.PathLike, str], size: int, timeout: int = 5
+) -> None:
+ deadline = time.monotonic() + timeout
+ while not os.path.exists(path):
+ if time.monotonic() > deadline:
+ break
+ time.sleep(0.1)
+
+ while os.path.getsize(path) < size:
+ if time.monotonic() > deadline:
+ break
+ time.sleep(0.1)
+
+
+class MockEmuLauncher(Launcher):
+ """Launcher for mock emulator"""
+
+ def __init__(
+ self,
+ config_path: Path,
+ ):
+ super().__init__('mock-emu', config_path)
+ self._wdir: Optional[Path] = None
+ self.log = True
+
+ def _pre_start(
+ self,
+ target: str,
+ file: Optional[Path] = None,
+ pause: bool = False,
+ debug: bool = False,
+ args: Optional[str] = None,
+ ) -> List[str]:
+ channels = []
+ if self._config.get_target(['pre-start-cmds']):
+ self._handles.add_channel_tcp('test_subst_tcp', 'localhost', 1234)
+ self._handles.add_channel_pty('test_subst_pty', 'pty-path')
+ if self._config.get_emu(['gdb_channel']):
+ channels += ['--tcp-channel', 'gdb']
+ if self._config.get_emu(['tcp_channel']):
+ channels += ['--tcp-channel', 'tcp']
+ if self._config.get_emu(['pty_channel']):
+ channels += ['--pty-channel', 'pty']
+ if len(channels) > 0:
+ channels += ['--working-dir', str(self._wdir)]
+ return _mock_emu + channels + ['starting mock emulator']
+
+ def _post_start(self) -> None:
+ if not self._wdir:
+ return
+
+ if self._config.get_emu(['gdb_channel']):
+ path = os.path.join(self._wdir, 'gdb')
+ wait_for_file_size(path, 5, 5)
+ with open(path, 'r') as file:
+ port = int(file.read())
+ self._handles.add_channel_tcp('gdb', 'localhost', port)
+
+ if self._config.get_emu(['tcp_channel']):
+ path = os.path.join(self._wdir, 'tcp')
+ wait_for_file_size(path, 5, 5)
+ with open(path, 'r') as file:
+ port = int(file.read())
+ self._handles.add_channel_tcp('tcp', 'localhost', port)
+
+ if self._config.get_emu(['pty_channel']):
+ path = os.path.join(self._wdir, 'pty')
+ wait_for_file_size(path, 5, 5)
+ with open(path, 'r') as file:
+ pty_path = file.read()
+ self._handles.add_channel_pty('pty', pty_path)
+
+ def _get_connector(self, wdir: Path) -> Connector:
+ return MockEmuConnector(wdir)
+
+
+class MockEmuConnector(Connector):
+ """Connector for mock emulator"""
+
+ _props = {
+ 'path1': {
+ 'prop1': 'val1',
+ }
+ }
+
+ def reset(self) -> None:
+ Path(os.path.join(self._wdir, 'reset')).touch()
+
+ def cont(self) -> None:
+ Path(os.path.join(self._wdir, 'cont')).touch()
+
+ def list_properties(self, path: str) -> List[Any]:
+ try:
+ return list(self._props[path].keys())
+ except KeyError:
+ raise InvalidPropertyPath(path)
+
+ def set_property(self, path: str, prop: str, value: str) -> None:
+ if not self._props.get(path):
+ raise InvalidPropertyPath(path)
+ if not self._props[path].get(prop):
+ raise InvalidProperty(path, prop)
+ self._props[path][prop] = value
+ with open(os.path.join(self._wdir, 'props.json'), 'w') as file:
+ json.dump(self._props, file)
+
+ def get_property(self, path: str, prop: str) -> Any:
+ try:
+ with open(os.path.join(self._wdir, 'props.json'), 'r') as file:
+ self._props = json.load(file)
+ except OSError:
+ pass
+ if not self._props.get(path):
+ raise InvalidPropertyPath(path)
+ if not self._props[path].get(prop):
+ raise InvalidProperty(path, prop)
+ return self._props[path][prop]
diff --git a/pw_emu/py/pw_emu/__init__.py b/pw_emu/py/pw_emu/__init__.py
new file mode 100644
index 000000000..0f2cb9104
--- /dev/null
+++ b/pw_emu/py/pw_emu/__init__.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
diff --git a/pw_emu/py/pw_emu/__main__.py b/pw_emu/py/pw_emu/__main__.py
new file mode 100644
index 000000000..253edd931
--- /dev/null
+++ b/pw_emu/py/pw_emu/__main__.py
@@ -0,0 +1,448 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Command line interface for the Pigweed emulators frontend"""
+
+import argparse
+import json
+import os
+from pathlib import Path
+import signal
+import subprocess
+import sys
+import threading
+
+from typing import Any
+
+from pw_emu.core import Error
+from pw_emu.frontend import Emulator
+from serial import serial_for_url, SerialException
+from serial.tools.miniterm import Miniterm, key_description
+
+_TERM_CMD = ['python', '-m', 'serial', '--raw']
+
+
+def _cmd_gdb_cmds(emu, args: argparse.Namespace) -> None:
+ """Run gdb commands in batch mode."""
+
+ emu.run_gdb_cmds(args.gdb_cmd, executable=args.executable, pause=args.pause)
+
+
+def _cmd_load(emu: Emulator, args: argparse.Namespace) -> None:
+ """Load an executable image via gdb start executing it if pause is
+ not set"""
+
+ args.gdb_cmd = ['load']
+ _cmd_gdb_cmds(emu, args)
+
+
+def _cmd_start(emu: Emulator, args: argparse.Namespace) -> None:
+ """Launch the emulator and start executing, unless pause is set."""
+
+ if args.runner:
+ emu.set_emu(args.runner)
+
+ emu.start(
+ target=args.target,
+ file=args.file,
+ pause=args.pause,
+ args=args.args,
+ debug=args.debug,
+ foreground=args.foreground,
+ )
+
+
+def _get_miniterm(emu: Emulator, chan: str) -> Miniterm:
+ chan_type = emu.get_channel_type(chan)
+ if chan_type == 'tcp':
+ host, port = emu.get_channel_addr(chan)
+ url = f'socket://[{host}]:{port}'
+ elif chan_type == 'pty':
+ url = emu.get_channel_path(chan)
+ else:
+ raise Error(f'unknown channel type `{chan_type}`')
+ ser = serial_for_url(url)
+ ser.timeout = 1
+ miniterm = Miniterm(ser)
+ miniterm.raw = True
+ miniterm.set_tx_encoding('UTF-8')
+ miniterm.set_rx_encoding('UTF-8')
+
+ quit_key = key_description(miniterm.exit_character)
+ menu_key = key_description(miniterm.menu_character)
+ help_key = key_description('\x08')
+ help_desc = f'Help: {menu_key} followed by {help_key} ---'
+
+ print(f'--- Miniterm on {chan} ---')
+ print(f'--- Quit: {quit_key} | Menu: {menu_key} | {help_desc}')
+
+ # On POSIX systems miniterm uses TIOCSTI to "cancel" the TX thread
+ # (reading from the console, sending to the serial) which is
+ # disabled on Linux kernels > 6.2 see
+ # https://github.com/pyserial/pyserial/issues/243
+ #
+ # On Windows the cancel method does not seem to work either with
+ # recent win10 versions.
+ #
+ # Workaround by terminating the process for exceptions in the read
+ # and write threads.
+ threading.excepthook = lambda args: signal.raise_signal(signal.SIGTERM)
+
+ return miniterm
+
+
+def _cmd_run(emu: Emulator, args: argparse.Namespace) -> None:
+ """Start the emulator and connect the terminal to a channel. Stop
+ the emulator when exiting the terminal"""
+
+ emu.start(
+ target=args.target,
+ file=args.file,
+ pause=True,
+ args=args.args,
+ )
+
+ ctrl_chans = ['gdb', 'monitor', 'qmp', 'robot']
+ if not args.channel:
+ for chan in emu.get_channels():
+ if chan not in ctrl_chans:
+ args.channel = chan
+ break
+ if not args.channel:
+ raise Error(f'only control channels {ctrl_chans} found')
+
+ try:
+ miniterm = _get_miniterm(emu, args.channel)
+ emu.cont()
+ miniterm.start()
+ miniterm.join(True)
+ print('--- exit ---')
+ miniterm.stop()
+ miniterm.join()
+ miniterm.close()
+ except SerialException as err:
+ raise Error(f'error connecting to channel `{args.channel}`: {err}')
+ finally:
+ emu.stop()
+
+
+def _cmd_restart(emu: Emulator, args: argparse.Namespace) -> None:
+ """Restart the emulator and start executing, unless pause is set."""
+
+ if emu.running():
+ emu.stop()
+ _cmd_start(emu, args)
+
+
+def _cmd_stop(emu: Emulator, _args: argparse.Namespace) -> None:
+ """Stop the emulator"""
+
+ emu.stop()
+
+
+def _cmd_reset(emu: Emulator, _args: argparse.Namespace) -> None:
+ """Perform a software reset."""
+
+ emu.reset()
+
+
+def _cmd_gdb(emu: Emulator, args: argparse.Namespace) -> None:
+ """Start a gdb interactive session"""
+
+ executable = args.executable if args.executable else ""
+
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+ try:
+ cmd = emu.get_gdb_cmd() + [
+ '-ex',
+ f'target remote {emu.get_gdb_remote()}',
+ executable,
+ ]
+ subprocess.run(cmd)
+ finally:
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+
+
+def _cmd_prop_ls(emu: Emulator, args: argparse.Namespace) -> None:
+ """List emulator object properties."""
+
+ props = emu.list_properties(args.path)
+ print(json.dumps(props, indent=4))
+
+
+def _cmd_prop_get(emu: Emulator, args: argparse.Namespace) -> None:
+ """Show the emulator's object properties."""
+
+ print(emu.get_property(args.path, args.property))
+
+
+def _cmd_prop_set(emu: Emulator, args: argparse.Namespace) -> None:
+ """Set emulator's object properties."""
+
+ emu.set_property(args.path, args.property, args.value)
+
+
+def _cmd_term(emu: Emulator, args: argparse.Namespace) -> None:
+ """Connect with an interactive terminal to an emulator channel"""
+
+ try:
+ miniterm = _get_miniterm(emu, args.channel)
+ miniterm.start()
+ miniterm.join(True)
+ print('--- exit ---')
+ miniterm.stop()
+ miniterm.join()
+ miniterm.close()
+ except SerialException as err:
+ raise Error(f'error connecting to channel `{args.channel}`: {err}')
+
+
+def _cmd_resume(emu: Emulator, _args: argparse.Namespace) -> None:
+ """Resume the execution of a paused emulator."""
+
+ emu.cont()
+
+
+def get_parser() -> argparse.ArgumentParser:
+ """Command line parser"""
+
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ '-i',
+ '--instance',
+ help='instance to use (default: %(default)s)',
+ type=str,
+ metavar='STRING',
+ default='default',
+ )
+ parser.add_argument(
+ '-C',
+ '--working-dir',
+ help='path to working directory (default: %(default)s)',
+ type=Path,
+ default=os.getenv('PW_EMU_WDIR'),
+ )
+ parser.add_argument(
+ '-c',
+ '--config',
+ help='path config file (default: %(default)s)',
+ type=str,
+ default=None,
+ )
+
+ subparsers = parser.add_subparsers(dest='command', required=True)
+
+ def add_cmd(name: str, func: Any) -> argparse.ArgumentParser:
+ subparser = subparsers.add_parser(
+ name, description=func.__doc__, help=func.__doc__
+ )
+ subparser.set_defaults(func=func)
+ return subparser
+
+ start = add_cmd('start', _cmd_start)
+ restart = add_cmd('restart', _cmd_restart)
+
+ for subparser in [start, restart]:
+ subparser.add_argument(
+ 'target',
+ type=str,
+ )
+ subparser.add_argument(
+ '--file',
+ '-f',
+ metavar='FILE',
+ help='file to load before starting',
+ )
+ subparser.add_argument(
+ '--runner',
+ '-r',
+ help='emulator to use, automatically detected if not set',
+ choices=[None, 'qemu', 'renode'],
+ default=None,
+ )
+ subparser.add_argument(
+ '--args',
+ '-a',
+ help='options to pass to the emulator',
+ )
+ subparser.add_argument(
+ '--pause',
+ '-p',
+ action='store_true',
+ help='pause the emulator after starting it',
+ )
+ subparser.add_argument(
+ '--debug',
+ '-d',
+ action='store_true',
+ help='start the emulator in debug mode',
+ )
+ subparser.add_argument(
+ '--foreground',
+ '-F',
+ action='store_true',
+ help='start the emulator in foreground mode',
+ )
+
+ run = add_cmd('run', _cmd_run)
+ run.add_argument(
+ 'target',
+ type=str,
+ )
+ run.add_argument(
+ 'file',
+ metavar='FILE',
+ help='file to load before starting',
+ )
+ run.add_argument(
+ '--args',
+ '-a',
+ help='options to pass to the emulator',
+ )
+ run.add_argument(
+ '--channel',
+ '-n',
+ help='channel to connect the terminal to',
+ )
+
+ stop = add_cmd('stop', _cmd_stop)
+
+ load = add_cmd('load', _cmd_load)
+ load.add_argument(
+ 'executable',
+ metavar='FILE',
+ help='file to load via gdb',
+ )
+ load.add_argument(
+ '--pause',
+ '-p',
+ help='pause the emulator after loading the file',
+ action='store_true',
+ )
+ load.add_argument(
+ '--offset',
+ '-o',
+ metavar='ADDRESS',
+ help='address to load the file at',
+ )
+
+ reset = add_cmd('reset', _cmd_reset)
+
+ gdb = add_cmd('gdb', _cmd_gdb)
+ gdb.add_argument(
+ '--executable',
+ '-e',
+ metavar='FILE',
+ help='file to use for the debugging session',
+ )
+
+ prop_ls = add_cmd('prop-ls', _cmd_prop_ls)
+ prop_ls.add_argument(
+ 'path',
+ help='path of the emulator object',
+ )
+
+ prop_get = add_cmd('prop-get', _cmd_prop_get)
+ prop_get.add_argument(
+ 'path',
+ help='path of the emulator object',
+ )
+ prop_get.add_argument(
+ 'property',
+ help='name of the object property',
+ )
+
+ prop_set = add_cmd('prop-set', _cmd_prop_set)
+ prop_set.add_argument(
+ 'path',
+ help='path of the emulator object',
+ )
+ prop_set.add_argument(
+ 'property',
+ help='name of the object property',
+ )
+ prop_set.add_argument(
+ 'value',
+ help='value to set for the object property',
+ )
+
+ gdb_cmds = add_cmd('gdb-cmds', _cmd_gdb_cmds)
+ gdb_cmds.add_argument(
+ '--pause',
+ '-p',
+ help='do not resume execution after running the commands',
+ action='store_true',
+ )
+ gdb_cmds.add_argument(
+ '--executable',
+ '-e',
+ metavar='FILE',
+ help='executable to use while running the gdb commands',
+ )
+ gdb_cmds.add_argument(
+ 'gdb_cmd',
+ nargs='+',
+ help='gdb command to execute',
+ )
+
+ term = add_cmd('term', _cmd_term)
+ term.add_argument(
+ 'channel',
+ help='channel name',
+ )
+
+ resume = add_cmd('resume', _cmd_resume)
+
+ parser.epilog = f"""commands usage:
+ {start.format_usage().strip()}
+ {restart.format_usage().strip()}
+ {stop.format_usage().strip()}
+ {run.format_usage().strip()}
+ {load.format_usage().strip()}
+ {reset.format_usage().strip()}
+ {gdb.format_usage().strip()}
+ {prop_ls.format_usage().strip()}
+ {prop_get.format_usage().strip()}
+ {prop_set.format_usage().strip()}
+ {gdb_cmds.format_usage().strip()}
+ {term.format_usage().strip()}
+ {resume.format_usage().strip()}
+ """
+
+ return parser
+
+
+def main() -> int:
+ """Emulators frontend command line interface."""
+
+ args = get_parser().parse_args()
+ if not args.working_dir:
+ args.working_dir = (
+ f'{os.getenv("PW_PROJECT_ROOT")}/.pw_emu/{args.instance}'
+ )
+
+ try:
+ emu = Emulator(args.working_dir, args.config)
+ args.func(emu, args)
+ except Error as err:
+ print(err)
+ return 1
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_emu/py/pw_emu/core.py b/pw_emu/py/pw_emu/core.py
new file mode 100644
index 000000000..1e5c2ea9f
--- /dev/null
+++ b/pw_emu/py/pw_emu/core.py
@@ -0,0 +1,956 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Infrastructure used by the user interface or specific emulators."""
+
+import io
+import json
+import logging
+import os
+import re
+import socket
+import subprocess
+import sys
+import time
+
+from abc import ABC, abstractmethod
+from importlib import import_module
+from pathlib import Path
+from typing import Optional, Dict, List, Union, Any, Type
+
+import psutil # type: ignore
+
+from pw_emu.pigweed_emulators import pigweed_emulators
+from pw_env_setup.config_file import load as pw_config_load
+from pw_env_setup.config_file import path as pw_config_path
+from serial import Serial
+
+
+_LAUNCHER_LOG = logging.getLogger('pw_qemu.core.launcher')
+
+
+def _stop_process(pid: int) -> None:
+ """Gracefully stop a running process."""
+
+ try:
+ proc = psutil.Process(pid)
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except psutil.TimeoutExpired:
+ proc.kill()
+ except psutil.NoSuchProcess:
+ pass
+
+
+def _get_class(name: str) -> type:
+ """Returns a class from a full qualified class name
+ (e.g. "package.module.Class").
+
+ """
+ try:
+ module_path, class_name = name.rsplit('.', 1)
+ module = import_module(module_path)
+ return getattr(module, class_name)
+ except (ImportError, AttributeError):
+ raise ImportError(name)
+
+
+class Error(Exception):
+ """Generic pw_emu exception."""
+
+
+class ConfigError(Error):
+ """Exception raised for configuration errors."""
+
+ def __init__(self, config: Optional[Path], err: str) -> None:
+ msg = f'{config}: {err}\n'
+ try:
+ if config:
+ with open(config, 'r') as file:
+ msg += json.dumps(json.load(file), indent=4)
+ except (OSError, json.decoder.JSONDecodeError):
+ pass
+ super().__init__(msg)
+
+
+class AlreadyRunning(Error):
+ """Exception raised if an emulator process is already running."""
+
+ def __init__(self, wdir: Path) -> None:
+ super().__init__(f'{wdir}: emulator already started')
+
+
+class NotRunning(Error):
+ """Exception raised if an emulator process is not running."""
+
+ def __init__(self, wdir: Path) -> None:
+ super().__init__(f'{wdir}: emulator not started')
+
+
+class InvalidEmulator(Error):
+ """Exception raised if an different backend is running."""
+
+ def __init__(self, emu: str) -> None:
+ super().__init__(f'invalid emulator `{emu}`')
+
+
+class InvalidTarget(Error):
+ """Exception raised if the target is invalid."""
+
+ def __init__(self, config: Path, emu: Optional[str], target: str) -> None:
+ emu_str = f'for `{emu}`' if emu else ''
+ super().__init__(f'{config}: invalid target `{target}` {emu_str}')
+
+
+class InvalidChannelName(Error):
+ """Exception raised if a channel name is invalid."""
+
+ def __init__(self, name: str, target: str, valid: str) -> None:
+ msg = f"""
+ `{name}` is not a valid device name for {target}`
+ try: {valid}
+ """
+ super().__init__(msg)
+
+
+class InvalidChannelType(Error):
+ """Exception raised if a channel type is invalid."""
+
+ def __init__(self, name: str) -> None:
+ super().__init__(f'`{name}` is not a valid channel type')
+
+
+class WrongEmulator(Error):
+ """Exception raised if an different backend is running."""
+
+ def __init__(self, exp: str, found: str) -> None:
+ super().__init__(f'wrong emulator: expected `{exp}, found {found}`')
+
+
+class RunError(Error):
+ """Exception raised when a command failed to run."""
+
+ def __init__(self, proc: str, msg: str) -> None:
+ super().__init__(f'error running `{proc}`: {msg}')
+
+
+class InvalidPropertyPath(Error):
+ """Exception raised for an invalid property path."""
+
+ def __init__(self, path: str) -> None:
+ super().__init__(f'invalid property path `{path}`')
+
+
+class InvalidProperty(Error):
+ """Exception raised for an invalid property path."""
+
+ def __init__(self, path: str, name: str) -> None:
+ super().__init__(f'invalid property `{name}` at `{path}`')
+
+
+class HandlesError(Error):
+ """Exception raised while trying to load emulator handles."""
+
+ def __init__(self, msg: str) -> None:
+ super().__init__(f'error loading handles: {msg}')
+
+
+class Handles:
+ """Running emulator handles."""
+
+ class Channel:
+ def __init__(self, chan_type: str):
+ self.type = chan_type
+
+ class PtyChannel(Channel):
+ def __init__(self, path: str):
+ super().__init__('pty')
+ self.path = path
+
+ class TcpChannel(Channel):
+ def __init__(self, host: str, port: int):
+ super().__init__('tcp')
+ self.host = host
+ self.port = port
+
+ class Proc:
+ def __init__(self, pid: int):
+ self.pid = pid
+
+ @staticmethod
+ def _ser_obj(obj) -> Any:
+ if isinstance(obj, dict):
+ data = {}
+ for key, val in obj.items():
+ data[key] = Handles._ser_obj(val)
+ return data
+ if hasattr(obj, "__iter__") and not isinstance(obj, str):
+ return [Handles._ser_obj(item) for item in obj]
+ if hasattr(obj, "__dict__"):
+ return Handles._ser_obj(obj.__dict__)
+ return obj
+
+ def _serialize(self):
+ return Handles._ser_obj(self)
+
+ def save(self, wdir: Path) -> None:
+ """Saves handles to the given working directory."""
+
+ with open(os.path.join(wdir, 'handles.json'), 'w') as file:
+ json.dump(self._serialize(), file)
+
+ @staticmethod
+ def load(wdir: Path):
+ try:
+ with open(os.path.join(wdir, 'handles.json'), 'r') as file:
+ data = json.load(file)
+ except (KeyError, OSError, json.decoder.JSONDecodeError):
+ raise NotRunning(wdir)
+
+ handles = Handles(data['emu'], data['config'])
+ handles.set_target(data['target'])
+ gdb_cmd = data.get('gdb_cmd')
+ if gdb_cmd:
+ handles.set_gdb_cmd(gdb_cmd)
+ for name, chan in data['channels'].items():
+ chan_type = chan['type']
+ if chan_type == 'tcp':
+ handles.add_channel_tcp(name, chan['host'], chan['port'])
+ elif chan_type == 'pty':
+ handles.add_channel_pty(name, chan['path'])
+ else:
+ raise InvalidChannelType(chan_type)
+ for name, proc in data['procs'].items():
+ handles.add_proc(name, proc['pid'])
+ return handles
+
+ def __init__(self, emu: str, config: str) -> None:
+ self.emu = emu
+ self.config = config
+ self.gdb_cmd: List[str] = []
+ self.target = ''
+ self.channels: Dict[str, Handles.Channel] = {}
+ self.procs: Dict[str, Handles.Proc] = {}
+
+ def add_channel_tcp(self, name: str, host: str, port: int) -> None:
+ """Add a TCP channel."""
+
+ self.channels[name] = self.TcpChannel(host, port)
+
+ def add_channel_pty(self, name: str, path: str) -> None:
+ """Add a pty channel."""
+
+ self.channels[name] = self.PtyChannel(path)
+
+ def add_proc(self, name: str, pid: int) -> None:
+ """Add a pid."""
+
+ self.procs[name] = self.Proc(pid)
+
+ def set_target(self, target: str) -> None:
+ """Set the target."""
+
+ self.target = target
+
+ def set_gdb_cmd(self, cmd: List[str]) -> None:
+ """Set the gdb command."""
+
+ self.gdb_cmd = cmd.copy()
+
+
+def _stop_processes(handles: Handles, wdir: Path) -> None:
+ """Stop all processes for a (partially) running emulator instance.
+
+ Remove pid files as well.
+ """
+
+ for _, proc in handles.procs.items():
+ _stop_process(proc.pid)
+ path = os.path.join(wdir, f'{proc}.pid')
+ if os.path.exists(path):
+ os.unlink(path)
+
+
+class Config:
+ """Get and validate options from the configuration file."""
+
+ def __init__(
+ self,
+ config_path: Optional[Path] = None,
+ target: Optional[str] = None,
+ emu: Optional[str] = None,
+ ) -> None:
+ """Load the emulator configuration.
+
+ If no configuration file path is given, the root project
+ configuration is used.
+
+ This method set ups the generic configuration (e.g. gdb).
+
+ It loads emulator target files and gathers them under the 'targets' key
+ for each emulator backend. The 'targets' settings in the configuration
+ file takes precedence over the loaded target files.
+
+ """
+ try:
+ if config_path:
+ with open(config_path, 'r') as file:
+ config = json.load(file)['pw']['pw_emu']
+ else:
+ config_path = pw_config_path()
+ config = pw_config_load()['pw']['pw_emu']
+ except KeyError:
+ raise ConfigError(config_path, 'missing `pw_emu` configuration')
+
+ if not config_path:
+ raise ConfigError(None, 'unable to deterine config path')
+
+ if config.get('target_files'):
+ tmp = {}
+ for path in config['target_files']:
+ if not os.path.isabs(path):
+ path = os.path.join(os.path.dirname(config_path), path)
+ with open(path, 'r') as file:
+ tmp.update(json.load(file).get('targets'))
+ if config.get('targets'):
+ tmp.update(config['targets'])
+ config['targets'] = tmp
+
+ self.path = config_path
+ self._config = {'emulators': pigweed_emulators}
+ self._config.update(config)
+ self._emu = emu
+ self._target = target
+
+ def set_target(self, target: str) -> None:
+ """Sets the current target.
+
+ The current target is used by the get_target method.
+
+ """
+
+ self._target = target
+ try:
+ self.get(['targets', target], optional=False, entry_type=dict)
+ except ConfigError:
+ raise InvalidTarget(self.path, self._emu, self._target)
+
+ def get_targets(self) -> List[str]:
+ return list(self.get(['targets'], entry_type=dict).keys())
+
+ def _subst(self, string: str) -> str:
+ """Substitutes $pw_<subst_type>{arg} statements."""
+
+ match = re.search(r'\$pw_([^{]+){([^}]+)}', string)
+ if not match:
+ return string
+
+ subst_type = match.group(1)
+ arg = match.group(2)
+
+ if subst_type == 'env':
+ value = os.environ.get(arg)
+ if value is None:
+ msg = f'Environment variable `{arg}` not set'
+ raise ConfigError(self.path, msg)
+ return string.replace(f'$pw_{subst_type}{{{arg}}}', value)
+
+ raise ConfigError(self.path, f'Invalid substitution type: {subst_type}')
+
+ def _subst_list(self, items: List[Any]) -> List[Any]:
+ new_list = []
+ for item in items:
+ if isinstance(item, str):
+ new_list.append(self._subst(item))
+ else:
+ new_list.append(item)
+ return new_list
+
+ def get(
+ self,
+ keys: List[str],
+ optional: bool = True,
+ entry_type: Optional[Type] = None,
+ ) -> Any:
+ """Get a config entry.
+
+ keys is a list of string that identifies the config entry, e.g.
+ ['targets', 'test-target'] is going to look in the config dicionary for
+ ['targets']['test-target'].
+
+ If the option is not found and optional is True it returns None if
+ entry_type is none or a new (empty) object of type entry_type.
+
+ If the option is not found an optional is False it raises ConfigError.
+
+ If entry_type is not None it will check the option to be of
+ that type. If it is not it will raise ConfigError.
+
+ """
+
+ keys_str = ': '.join(keys)
+ entry: Optional[Dict[str, Any]] = self._config
+
+ for key in keys:
+ if not isinstance(entry, dict):
+ if optional:
+ if entry_type:
+ return entry_type()
+ return None
+ raise ConfigError(self.path, f'{keys_str}: not found')
+ entry = entry.get(key)
+
+ if entry is None:
+ if optional:
+ if entry_type:
+ return entry_type()
+ return None
+ raise ConfigError(self.path, f'{keys_str}: not found')
+
+ if entry_type and not isinstance(entry, entry_type):
+ msg = f'{keys_str}: expected entry of type `{entry_type}`'
+ raise ConfigError(self.path, msg)
+
+ if isinstance(entry, str):
+ entry = self._subst(entry)
+ elif isinstance(entry, list):
+ entry = self._subst_list(entry)
+
+ return entry
+
+ def get_target(
+ self,
+ keys: List[str],
+ optional: bool = True,
+ entry_type: Optional[Type] = None,
+ ) -> Any:
+ """Get a config option starting at ['targets'][target]."""
+
+ if not self._target:
+ raise Error('target not set')
+ return self.get(['targets', self._target] + keys, optional, entry_type)
+
+ def get_emu(
+ self,
+ keys: List[str],
+ optional: bool = True,
+ entry_type: Optional[Type] = None,
+ ) -> Any:
+ """Get a config option starting at [emu]."""
+
+ if not self._emu:
+ raise Error('emu not set')
+ return self.get([self._emu] + keys, optional, entry_type)
+
+ def get_target_emu(
+ self,
+ keys: List[str],
+ optional: bool = True,
+ entry_type: Optional[Type] = None,
+ ) -> Any:
+ """Get a config option starting at ['targets'][target][emu]."""
+
+ if not self._emu or not self._target:
+ raise Error('emu or target not set')
+ return self.get(
+ ['targets', self._target, self._emu] + keys, optional, entry_type
+ )
+
+
+class Connector(ABC):
+ """Interface between a running emulator and the user visible APIs."""
+
+ def __init__(self, wdir: Path) -> None:
+ self._wdir = wdir
+ self._handles = Handles.load(wdir)
+ self._channels = self._handles.channels
+ self._target = self._handles.target
+
+ @staticmethod
+ def get(wdir: Path) -> Any:
+ """Return a connector instace for a given emulator type."""
+ handles = Handles.load(wdir)
+ config = Config(handles.config)
+ emu = handles.emu
+ try:
+ name = config.get(['emulators', emu, 'connector'])
+ cls = _get_class(name)
+ except (ConfigError, ImportError):
+ raise InvalidEmulator(emu)
+ return cls(wdir)
+
+ def get_emu(self) -> str:
+ """Returns the emulator type."""
+
+ return self._handles.emu
+
+ def get_gdb_cmd(self) -> List[str]:
+ """Returns the configured gdb command."""
+ return self._handles.gdb_cmd
+
+ def get_config_path(self) -> Path:
+ """Returns the configuration path."""
+
+ return self._handles.config
+
+ def get_procs(self) -> Dict[str, Handles.Proc]:
+ """Returns the running processes indexed by the process name."""
+
+ return self._handles.procs
+
+ def get_channel_type(self, name: str) -> str:
+ """Returns the channel type."""
+
+ try:
+ return self._channels[name].type
+ except KeyError:
+ channels = ' '.join(self._channels.keys())
+ raise InvalidChannelName(name, self._target, channels)
+
+ def get_channel_path(self, name: str) -> str:
+ """Returns the channel path. Raises InvalidChannelType if this
+ is not a pty channel.
+
+ """
+
+ try:
+ if self._channels[name].type != 'pty':
+ raise InvalidChannelType(self._channels[name].type)
+ return self._channels[name].path
+ except KeyError:
+ raise InvalidChannelName(name, self._target, self._channels.keys())
+
+ def get_channel_addr(self, name: str) -> tuple:
+ """Returns a pair of (host, port) for the channel. Raises
+ InvalidChannelType if this is not a tcp channel.
+
+ """
+
+ try:
+ if self._channels[name].type != 'tcp':
+ raise InvalidChannelType(self._channels[name].type)
+ return (self._channels[name].host, self._channels[name].port)
+ except KeyError:
+ raise InvalidChannelName(name, self._target, self._channels.keys())
+
+ def get_channel_stream(
+ self,
+ name: str,
+ timeout: Optional[float] = None,
+ ) -> io.RawIOBase:
+ """Returns a file object for a given host exposed device.
+
+ If timeout is None than reads and writes are blocking. If
+ timeout is zero the stream is operating in non-blocking
+ mode. Otherwise read and write will timeout after the given
+ value.
+
+ """
+
+ chan_type = self.get_channel_type(name)
+ if chan_type == 'tcp':
+ host, port = self.get_channel_addr(name)
+ if ':' in host:
+ sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ else:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((host, port))
+ sock.settimeout(timeout)
+ ret = sock.makefile('rwb', buffering=0)
+ sock.close()
+ return ret
+ if chan_type == 'pty':
+ ser = Serial(self.get_channel_path(name))
+ ser.timeout = timeout
+ return ser
+ raise InvalidChannelType(chan_type)
+
+ def get_channels(self) -> List[str]:
+ return self._handles.channels.keys()
+
+ def stop(self) -> None:
+ """Stop the emulator."""
+
+ _stop_processes(self._handles, self._wdir)
+
+ try:
+ os.unlink(os.path.join(self._wdir, 'handles.json'))
+ except OSError:
+ pass
+
+ def proc_running(self, proc: str) -> bool:
+ try:
+ return psutil.pid_exists(self._handles.procs[proc].pid)
+ except (NotRunning, KeyError):
+ return False
+
+ def running(self) -> bool:
+ """Check if the main emulator process is already running."""
+
+ try:
+ return psutil.pid_exists(self._handles.procs[self._handles.emu].pid)
+ except (NotRunning, KeyError):
+ return False
+
+ @abstractmethod
+ def reset(self) -> None:
+ """Perform a software reset."""
+
+ @abstractmethod
+ def cont(self) -> None:
+ """Resume the emulator's execution."""
+
+ @abstractmethod
+ def list_properties(self, path: str) -> List[Any]:
+ """Returns the property list for an emulator object."""
+
+ @abstractmethod
+ def set_property(self, path: str, prop: str, value: str) -> None:
+ """Sets the value of an emulator's object property."""
+
+ @abstractmethod
+ def get_property(self, path: str, prop: str) -> Any:
+ """Returns the value of an emulator's object property."""
+
+
+class Launcher(ABC):
+ """Starts an emulator based on the target and configuration file."""
+
+ def __init__(
+ self,
+ emu: str,
+ config_path: Optional[Path] = None,
+ ) -> None:
+ """Initializes a Launcher instance."""
+
+ self._wdir: Optional[Path] = None
+ """Working directory"""
+
+ self._emu = emu
+ """Emulator type (e.g. "qemu", "renode")."""
+
+ self._target: Optional[str] = None
+ """Target, initialized to None and set with _prep_start."""
+
+ self._config = Config(config_path, emu=emu)
+ """Global, emulator and target configuration."""
+
+ self._handles = Handles(self._emu, str(self._config.path))
+ """Handles for processes, channels, etc."""
+
+ gdb_cmd = self._config.get(['gdb'], entry_type=list)
+ if gdb_cmd:
+ self._handles.set_gdb_cmd(gdb_cmd)
+
+ @staticmethod
+ def get(emu: str, config_path: Optional[Path] = None) -> Any:
+ """Returns a launcher for a given emulator type."""
+ config = Config(config_path)
+ try:
+ name = config.get(['emulators', emu, 'launcher'])
+ cls = _get_class(name)
+ except (ConfigError, ImportError):
+ raise InvalidEmulator(str(emu))
+ return cls(config_path)
+
+ @abstractmethod
+ def _pre_start(
+ self,
+ target: str,
+ file: Optional[Path] = None,
+ pause: bool = False,
+ debug: bool = False,
+ args: Optional[str] = None,
+ ) -> List[str]:
+ """Pre start work, returns command to start the emulator.
+
+ The target and emulator configuration can be accessed through
+ :py:attr:`pw_emu.core.Launcher._config` with
+ :py:meth:`pw_emu.core.Config.get`,
+ :py:meth:`pw_emu.core.Config.get_target`,
+ :py:meth:`pw_emu.core.Config.get_emu`,
+ :py:meth:`pw_emu.core.Config.get_target_emu`.
+ """
+
+ @abstractmethod
+ def _post_start(self) -> None:
+ """Post start work, finalize emulator handles.
+
+ Perform any post start emulator initialization and finalize the emulator
+ handles information.
+
+ Typically an internal monitor channel is used to inquire information
+ about the the configured channels (e.g. TCP ports, pty paths) and
+ :py:attr:`pw_emu.core.Launcher._handles` is updated via
+ :py:meth:`pw_emu.core.Handles.add_channel_tcp`,
+ :py:meth:`pw_emu.core.Handles.add_channel_pty`, etc.
+
+ """
+
+ @abstractmethod
+ def _get_connector(self, wdir: Path) -> Connector:
+ """Get a connector for this emulator type."""
+
+ def _path(self, name: Union[Path, str]) -> Path:
+ """Returns the full path for a given emulator file."""
+ if self._wdir is None:
+ raise Error('internal error')
+ return Path(os.path.join(self._wdir, name))
+
+ def _subst_channel(self, subst_type: str, arg: str, string: str) -> str:
+ """Substitutes $pw_emu_channel_{func}{arg} statements."""
+
+ try:
+ chan = self._handles.channels[arg]
+ except KeyError:
+ return string
+
+ if subst_type == 'channel_port':
+ if not isinstance(chan, Handles.TcpChannel):
+ return string
+ return str(chan.port)
+
+ if subst_type == 'channel_host':
+ if not isinstance(chan, Handles.TcpChannel):
+ return string
+ return chan.host
+
+ if subst_type == 'channel_path':
+ if not isinstance(chan, Handles.PtyChannel):
+ return string
+ return chan.path
+
+ return string
+
+ def _subst(self, string: str) -> str:
+ """Substitutes $pw_emu_<subst_type>{arg} statements."""
+
+ match = re.search(r'\$pw_emu_([^{]+){([^}]+)}', string)
+ if not match:
+ return string
+
+ subst_type = match.group(1)
+ arg = match.group(2)
+
+ if subst_type == 'wdir':
+ if self._wdir:
+ return os.path.join(self._wdir, arg)
+ return string
+
+ if 'channel_' in subst_type:
+ return self._subst_channel(subst_type, arg, string)
+
+ return string
+
+ # pylint: disable=protected-access
+ # use os._exit after fork instead of os.exit
+ def _daemonize(
+ self,
+ name: str,
+ cmd: List[str],
+ ) -> None:
+ """Daemonize process for UNIX hosts."""
+
+ if sys.platform == 'win32':
+ raise Error('_daemonize not supported on win32')
+
+ # pylint: disable=no-member
+ # avoid pylint false positive on win32
+ pid = os.fork()
+ if pid < 0:
+ raise RunError(name, f'fork failed: {pid}')
+ if pid > 0:
+ return
+
+ path: Path = Path('/dev/null')
+ fd = os.open(path, os.O_RDONLY)
+ os.dup2(fd, sys.stdin.fileno())
+ os.close(fd)
+
+ path = self._path(f'{name}.log')
+ fd = os.open(path, os.O_WRONLY | os.O_TRUNC | os.O_CREAT)
+ os.dup2(fd, sys.stdout.fileno())
+ os.dup2(fd, sys.stderr.fileno())
+ os.close(fd)
+
+ os.setsid()
+
+ if os.fork() > 0:
+ os._exit(0)
+
+ try:
+ # Make the pid file create and pid write operations atomic to avoid
+ # races with readers.
+ with open(self._path(f'{name}.pid.tmp'), 'w') as file:
+ file.write(f'{os.getpid()}')
+ os.rename(self._path(f'{name}.pid.tmp'), self._path(f'{name}.pid'))
+ os.execvp(cmd[0], cmd)
+ finally:
+ os._exit(1)
+
+ def _start_proc(
+ self,
+ name: str,
+ cmd: List[str],
+ foreground: bool = False,
+ ) -> Union[subprocess.Popen, None]:
+ """Run the main emulator process.
+
+ The process pid is stored and can later be accessed by its name to
+ terminate it when the emulator is stopped.
+
+ If foreground is True the process run in the foreground and a
+ subprocess.Popen object is returned. Otherwise the process is started in
+ the background and None is returned.
+
+ When running in the background stdin is redirected to the NULL device
+ and stdout and stderr are redirected to a file named <name>.log which is
+ stored in the emulator's instance working directory.
+
+ """
+ for idx, item in enumerate(cmd):
+ cmd[idx] = self._subst(item)
+
+ pid_file_path = self._path(f'{name}.pid')
+ if os.path.exists(pid_file_path):
+ os.unlink(pid_file_path)
+
+ if foreground:
+ proc = subprocess.Popen(cmd)
+ self._handles.add_proc(name, proc.pid)
+ with open(pid_file_path, 'w') as file:
+ file.write(f'{proc.pid}')
+ return proc
+
+ if sys.platform == 'win32':
+ file = open(self._path(f'{name}.log'), 'w')
+ proc = subprocess.Popen(
+ cmd,
+ stdin=subprocess.DEVNULL,
+ stdout=file,
+ stderr=file,
+ creationflags=subprocess.DETACHED_PROCESS,
+ )
+ file.close()
+ with open(pid_file_path, 'w') as file:
+ file.write(f'{proc.pid}')
+ self._handles.add_proc(name, proc.pid)
+ # avoids resource warnings due to not calling wait which
+ # we don't want to do since we've started the process in
+ # the background
+ proc.returncode = 0
+ else:
+ self._daemonize(name, cmd)
+
+ # wait for the pid file to avoid double start race conditions
+ timeout = time.monotonic() + 30
+ while not os.path.exists(self._path(f'{name}.pid')):
+ time.sleep(0.1)
+ if time.monotonic() > timeout:
+ break
+ if not os.path.exists(self._path(f'{name}.pid')):
+ raise RunError(name, 'pid file timeout')
+ try:
+ with open(pid_file_path, 'r') as file:
+ pid = int(file.readline())
+ self._handles.add_proc(name, pid)
+ except (OSError, ValueError) as err:
+ raise RunError(name, str(err))
+
+ return None
+
+ def _stop_procs(self):
+ """Stop all registered processes."""
+
+ for name, proc in self._handles.procs.items():
+ _stop_process(proc.pid)
+ if os.path.exists(self._path(f'{name}.pid')):
+ os.unlink(self._path(f'{name}.pid'))
+
+ def _start_procs(self, procs_list: str) -> None:
+ """Start additional processes besides the main emulator one."""
+
+ procs = self._config.get_target([procs_list], entry_type=dict)
+ for name, cmd in procs.items():
+ self._start_proc(name, cmd)
+
+ def start(
+ self,
+ wdir: Path,
+ target: str,
+ file: Optional[Path] = None,
+ pause: bool = False,
+ debug: bool = False,
+ foreground: bool = False,
+ args: Optional[str] = None,
+ ) -> Connector:
+ """Start the emulator for the given target.
+
+ If file is set that the emulator will load the file before starting.
+
+ If pause is True the emulator is paused.
+
+ If debug is True the emulator is run in foreground with debug output
+ enabled. This is useful for seeing errors, traces, etc.
+
+ If foreground is True the emulator is run in foreground otherwise it is
+ started in daemon mode. This is useful when there is another process
+ controlling the emulator's life cycle (e.g. cuttlefish)
+
+ args are passed directly to the emulator
+
+ """
+
+ try:
+ handles = Handles.load(wdir)
+ if psutil.pid_exists(handles.procs[handles.emu].pid):
+ raise AlreadyRunning(wdir)
+ except NotRunning:
+ pass
+
+ self._wdir = wdir
+ self._target = target
+ self._config.set_target(target)
+ self._handles.set_target(target)
+ gdb_cmd = self._config.get_target(['gdb'], entry_type=list)
+ if gdb_cmd:
+ self._handles.set_gdb_cmd(gdb_cmd)
+ os.makedirs(wdir, mode=0o700, exist_ok=True)
+
+ cmd = self._pre_start(
+ target=target, file=file, pause=pause, debug=debug, args=args
+ )
+
+ if debug:
+ foreground = True
+ _LAUNCHER_LOG.setLevel(logging.DEBUG)
+
+ _LAUNCHER_LOG.debug('starting emulator with command: %s', ' '.join(cmd))
+
+ try:
+ self._start_procs('pre-start-cmds')
+ proc = self._start_proc(self._emu, cmd, foreground)
+ self._start_procs('post-start-cmds')
+ except RunError as err:
+ self._stop_procs()
+ raise err
+
+ self._post_start()
+ self._handles.save(wdir)
+
+ if proc:
+ proc.wait()
+ self._stop_procs()
+
+ return self._get_connector(self._wdir)
diff --git a/pw_emu/py/pw_emu/frontend.py b/pw_emu/py/pw_emu/frontend.py
new file mode 100644
index 000000000..8ecf7d3fe
--- /dev/null
+++ b/pw_emu/py/pw_emu/frontend.py
@@ -0,0 +1,328 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""User API"""
+
+import io
+import os
+import subprocess
+import tempfile
+
+from pathlib import Path
+from typing import Any, Optional, List, Union, Dict
+
+from pw_emu.core import (
+ AlreadyRunning,
+ Config,
+ ConfigError,
+ Connector,
+ Launcher,
+ InvalidEmulator,
+ InvalidChannelType,
+ NotRunning,
+)
+
+
+class Emulator:
+ """Launches, controls and interacts with an emulator instance."""
+
+ def __init__(self, wdir: Path, config_path: Optional[Path] = None) -> None:
+ self._wdir = wdir
+ self._config_path = config_path
+ self._connector: Optional[Connector] = None
+ self._launcher: Optional[Launcher] = None
+
+ def _get_launcher(self, target: str) -> Launcher:
+ """Returns an emulator for a given target.
+
+ If there are multiple emulators for the same target it will return
+ an arbitrary emulator launcher.
+ """
+ config = Config(self._config_path)
+ target_config = config.get(
+ ['targets', target],
+ optional=False,
+ entry_type=dict,
+ )
+ for key in target_config.keys():
+ try:
+ return Launcher.get(key, self._config_path)
+ except InvalidEmulator:
+ pass
+ raise ConfigError(
+ self._config_path,
+ f'could not determine emulator for target `{target}`',
+ )
+
+ def start(
+ self,
+ target: str,
+ file: Optional[Path] = None,
+ pause: bool = False,
+ debug: bool = False,
+ foreground: bool = False,
+ args: Optional[str] = None,
+ ) -> None:
+ """Start the emulator for the given target.
+
+ If file is set the emulator will load the file before starting.
+
+ If pause is True the emulator is paused until the debugger is
+ connected.
+
+ If debug is True the emulator is run in foreground with debug
+ output enabled. This is useful for seeing errors, traces, etc.
+
+ If foreground is True the emulator is run in foreground otherwise
+ it is started in daemon mode. This is useful when there is
+ another process controlling the emulator's life cycle
+ (e.g. cuttlefish)
+
+ args are passed directly to the emulator
+
+ """
+ if self._connector:
+ raise AlreadyRunning(self._wdir)
+
+ if self._launcher is None:
+ self._launcher = self._get_launcher(target)
+ self._connector = self._launcher.start(
+ wdir=self._wdir,
+ target=target,
+ file=file,
+ pause=pause,
+ debug=debug,
+ foreground=foreground,
+ args=args,
+ )
+
+ def _c(self) -> Connector:
+ if self._connector is None:
+ self._connector = Connector.get(self._wdir)
+ if not self.running():
+ raise NotRunning(self._wdir)
+ return self._connector
+
+ def running(self) -> bool:
+ """Check if the main emulator process is already running."""
+
+ try:
+ return self._c().running()
+ except NotRunning:
+ return False
+
+ def _path(self, name: Union[Path, str]) -> Union[Path, str]:
+ """Returns the full path for a given emulator file."""
+
+ return os.path.join(self._wdir, name)
+
+ def stop(self):
+ """Stop the emulator."""
+
+ return self._c().stop()
+
+ def get_gdb_remote(self) -> str:
+ """Return a string that can be passed to the target remote gdb
+ command.
+
+ """
+
+ chan_type = self._c().get_channel_type('gdb')
+
+ if chan_type == 'tcp':
+ host, port = self._c().get_channel_addr('gdb')
+ return f'{host}:{port}'
+
+ if chan_type == 'pty':
+ return self._c().get_channel_path('gdb')
+
+ raise InvalidChannelType(chan_type)
+
+ def get_gdb_cmd(self) -> List[str]:
+ """Returns the gdb command for current target."""
+ return self._c().get_gdb_cmd()
+
+ def run_gdb_cmds(
+ self,
+ commands: List[str],
+ executable: Optional[str] = None,
+ pause: bool = False,
+ ) -> subprocess.CompletedProcess:
+ """Connect to the target and run the given commands silently
+ in batch mode.
+
+ The executable is optional but it may be required by some gdb
+ commands.
+
+ If pause is set do not continue execution after running the
+ given commands.
+
+ """
+
+ cmd = self._c().get_gdb_cmd().copy()
+ if not cmd:
+ raise ConfigError(self._c().get_config_path(), 'gdb not configured')
+
+ cmd.append('-batch-silent')
+ cmd.append('-ex')
+ cmd.append(f'target remote {self.get_gdb_remote()}')
+ for gdb_cmd in commands:
+ cmd.append('-ex')
+ cmd.append(gdb_cmd)
+ if pause:
+ cmd.append('-ex')
+ cmd.append('disconnect')
+ if executable:
+ cmd.append(executable)
+ return subprocess.run(cmd, capture_output=True)
+
+ def reset(self) -> None:
+ """Perform a software reset."""
+ self._c().reset()
+
+ def list_properties(self, path: str) -> List[Dict]:
+ """Returns the property list for an emulator object.
+
+ The object is identified by a full path. The path is target
+ specific and the format of the path is backend specific.
+
+ qemu path example: /machine/unattached/device[10]
+
+ renode path example: sysbus.uart
+
+ """
+ return self._c().list_properties(path)
+
+ def set_property(self, path: str, prop: str, value: Any) -> None:
+ """Sets the value of an emulator's object property."""
+
+ self._c().set_property(path, prop, value)
+
+ def get_property(self, path: str, prop: str) -> Any:
+ """Returns the value of an emulator's object property."""
+
+ return self._c().get_property(path, prop)
+
+ def get_channel_type(self, name: str) -> str:
+ """Returns the channel type
+
+ Currently `pty` or `tcp` are the only supported types.
+
+ """
+
+ return self._c().get_channel_type(name)
+
+ def get_channel_path(self, name: str) -> str:
+ """Returns the channel path. Raises InvalidChannelType if this
+ is not a pty channel.
+
+ """
+
+ return self._c().get_channel_path(name)
+
+ def get_channel_addr(self, name: str) -> tuple:
+ """Returns a pair of (host, port) for the channel. Raises
+ InvalidChannelType if this is not a tcp channel.
+
+ """
+
+ return self._c().get_channel_addr(name)
+
+ def get_channel_stream(
+ self,
+ name: str,
+ timeout: Optional[float] = None,
+ ) -> io.RawIOBase:
+ """Returns a file object for a given host exposed device.
+
+ If timeout is None than reads and writes are blocking. If
+ timeout is zero the stream is operating in non-blocking
+ mode. Otherwise read and write will timeout after the given
+ value.
+
+ """
+
+ return self._c().get_channel_stream(name, timeout)
+
+ def get_channels(self) -> List[str]:
+ """Returns the list of available channels."""
+
+ return self._c().get_channels()
+
+ def set_emu(self, emu: str) -> None:
+ """Set the emulator type for this instance."""
+
+ self._launcher = Launcher.get(emu, self._config_path)
+
+ def cont(self) -> None:
+ """Resume the emulator's execution."""
+
+ self._c().cont()
+
+
+class TemporaryEmulator(Emulator):
+ """Temporary emulator instances.
+
+ Manages emulator instances that run in temporary working
+ directories. The emulator instance is stopped and the working
+ directory is cleared when the with block completes.
+
+ It also supports interoperability with the pw emu cli, i.e.
+ starting the emulator with the CLI and controlling / interacting
+ with it from the API.
+
+ Usage example:
+
+ .. code-block:: python
+
+ # programatically start and load an executable then access it
+ with TemporaryEmulator() as emu:
+ emu.start(target, file)
+ with emu.get_channel_stream(chan) as stream:
+ ...
+
+ .. code-block:: python
+
+ # or start it form the command line then access it
+ with TemporaryEmulator() as emu:
+ build.bazel(
+ ctx,
+ "run",
+ exec_path,
+ "--run_under=pw emu start <target> --file "
+ )
+ with emu.get_channel_stream(chan) as stream:
+ ...
+
+ """
+
+ def __init__(
+ self,
+ config_path: Optional[Path] = None,
+ cleanup: bool = True,
+ ) -> None:
+ self._temp = tempfile.TemporaryDirectory()
+ self._cleanup = cleanup
+ super().__init__(Path(self._temp.name), config_path)
+
+ def __enter__(self):
+ # Interoperability with pw emu cli.
+ os.environ["PW_EMU_WDIR"] = self._wdir
+ return self
+
+ def __exit__(self, exc, value, traceback) -> None:
+ self.stop()
+ del os.environ["PW_EMU_WDIR"]
+ if self._cleanup:
+ self._temp.cleanup()
diff --git a/pw_emu/py/pw_emu/pigweed_emulators.py b/pw_emu/py/pw_emu/pigweed_emulators.py
new file mode 100644
index 000000000..764ed2ace
--- /dev/null
+++ b/pw_emu/py/pw_emu/pigweed_emulators.py
@@ -0,0 +1,27 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Pigweed built-in emulators frontends."""
+
+from typing import Dict
+
+pigweed_emulators: Dict[str, Dict[str, str]] = {
+ 'qemu': {
+ 'connector': 'pw_emu.qemu.QemuConnector',
+ 'launcher': 'pw_emu.qemu.QemuLauncher',
+ },
+ 'renode': {
+ 'connector': 'pw_emu.renode.RenodeConnector',
+ 'launcher': 'pw_emu.renode.RenodeLauncher',
+ },
+}
diff --git a/pw_emu/py/pw_emu/py.typed b/pw_emu/py/pw_emu/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_emu/py/pw_emu/py.typed
diff --git a/pw_emu/py/pw_emu/qemu.py b/pw_emu/py/pw_emu/qemu.py
new file mode 100644
index 000000000..50112cda3
--- /dev/null
+++ b/pw_emu/py/pw_emu/qemu.py
@@ -0,0 +1,343 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Pigweed qemu frontend."""
+
+
+import io
+import json
+import logging
+import os
+import re
+import socket
+import sys
+
+from pathlib import Path
+from typing import Optional, Dict, List, Any
+
+from pw_emu.core import (
+ ConfigError,
+ Connector,
+ Launcher,
+ Error,
+ InvalidChannelType,
+ WrongEmulator,
+)
+
+_QMP_LOG = logging.getLogger('pw_qemu.qemu.qmp')
+
+
+class QmpError(Error):
+ """Exception for QMP errors."""
+
+ def __init__(self, err: str):
+ super().__init__(err)
+
+
+class QmpClient:
+ """Send qmp requests the server."""
+
+ def __init__(self, stream: io.RawIOBase):
+ self._stream = stream
+
+ json.loads(self._stream.readline())
+ cmd = json.dumps({'execute': 'qmp_capabilities'})
+ self._stream.write(cmd.encode('utf-8'))
+ resp = json.loads(self._stream.readline().decode('ascii'))
+ if not 'return' in resp:
+ raise QmpError(f'qmp init failed: {resp.get("error")}')
+
+ def request(self, cmd: str, args: Optional[Dict[str, Any]] = None) -> Any:
+ """Issue a command using the qmp interface.
+
+ Returns a map with the response or None if there is no
+ response for this command.
+
+ """
+
+ req: Dict[str, Any] = {'execute': cmd}
+ if args:
+ req['arguments'] = args
+ _QMP_LOG.debug(' -> {json.dumps(cmd)}')
+ self._stream.write(json.dumps(req).encode('utf-8'))
+ while True:
+ line = self._stream.readline()
+ _QMP_LOG.debug(' <- {line}')
+ resp = json.loads(line)
+ if 'error' in resp.keys():
+ raise QmpError(resp['error']['desc'])
+ if 'return' in resp.keys():
+ return resp['return']
+
+
+class QemuLauncher(Launcher):
+ """Start a new qemu process for a given target and config file."""
+
+ def __init__(self, config_path: Optional[Path] = None):
+ super().__init__('qemu', config_path)
+ self._start_cmd: List[str] = []
+ self._chardevs_id_to_name = {
+ 'compat_monitor0': 'qmp',
+ 'compat_monitor1': 'monitor',
+ 'gdb': 'gdb',
+ }
+ self._chardevs: Dict[str, Any] = {}
+ self._qmp_init_sock: Optional[socket.socket] = None
+
+ def _set_qemu_channel_tcp(self, name: str, filename: str) -> None:
+ """Parse a TCP chardev and return (host, port) tuple.
+
+ Format for the tcp chardev backend:
+
+ [disconnected|isconnected:]tcp:<host>:<port>[,<options>][ <->
+ <host>:<port>]
+
+ """
+
+ host_port: Any = filename.split(',')[0]
+ if host_port.split(':')[0] != 'tcp':
+ host_port = host_port.split(':')[2:]
+ else:
+ host_port = host_port.split(':')[1:]
+ # IPV6 hosts have :
+ host = ':'.join(host_port[0:-1])
+ port = host_port[-1]
+ self._handles.add_channel_tcp(name, host, int(port))
+
+ def _set_qemu_channel_pty(self, name: str, filename: str) -> None:
+ """Parse a PTY chardev and return the path.
+
+ Format for the pty chardev backend: pty:<path>
+ """
+
+ path = filename.split(':')[1]
+
+ self._handles.add_channel_pty(name, path)
+
+ if os.path.lexists(self._path(name)):
+ os.unlink(self._path(name))
+ os.symlink(path, self._path(name))
+
+ def _set_qemu_channel(self, name: str, filename: str) -> None:
+ """Setups a chardev channel type."""
+
+ if filename.startswith('pty'):
+ self._set_qemu_channel_pty(name, filename)
+ elif 'tcp' in filename:
+ self._set_qemu_channel_tcp(name, filename)
+
+ def _get_channels_config(self, chan: str, opt: str) -> Any:
+ val = self._config.get_emu(['channels', chan, opt])
+ if val is not None:
+ return val
+ return self._config.get_emu(['channels', opt])
+
+ def _configure_default_channels(self) -> None:
+ """Configure the default channels."""
+
+ # keep qmp first so that it gets the compat_monitor0 label
+ for chan in ['qmp', 'monitor', 'gdb']:
+ chan_type = self._get_channels_config(chan, 'type')
+ if not chan_type:
+ chan_type = 'tcp'
+ if chan_type == 'pty':
+ if sys.platform == 'win32':
+ raise InvalidChannelType(chan_type)
+ backend = 'pty'
+ elif chan_type == 'tcp':
+ backend = 'tcp:localhost:0,ipv4=on,server=on,wait=off'
+ else:
+ raise InvalidChannelType(chan_type)
+ self._start_cmd.extend([f'-{chan}', backend])
+
+ def _get_chardev_config(self, name: str, opt: str) -> Any:
+ val = self._config.get_target_emu(['channels', 'chardevs', name, opt])
+ if not val:
+ val = self._get_channels_config(name, opt)
+ return val
+
+ def _configure_serial_channels(self, serials: Dict) -> None:
+ """Create "standard" serial devices.
+
+ We can't control the serial allocation number for "standard"
+ -serial devices so fill the slots for the not needed serials
+ with null chardevs e.g. for serial3, serial1 generate the
+ following arguments, in this order:
+
+ -serial null -serial {backend} -serial null - serial {backend}
+
+ """
+
+ min_ser = sys.maxsize
+ max_ser = -1
+ for serial in serials.keys():
+ num = int(serial.split('serial')[1])
+ if num < min_ser:
+ min_ser = num
+ if num > max_ser:
+ max_ser = num
+ for i in range(min_ser, max_ser + 1):
+ if serials.get(f'serial{i}'):
+ name = serials[f'serial{i}']
+ chan_type = self._get_chardev_config(name, 'type')
+ if not chan_type:
+ chan_type = 'tcp'
+ if chan_type == 'pty':
+ backend = 'pty'
+ elif chan_type == 'tcp':
+ backend = 'tcp:localhost:0,ipv4=on,server=on,wait=off'
+ else:
+ raise InvalidChannelType(chan_type)
+ self._start_cmd.extend(['-serial', backend])
+ else:
+ self._start_cmd.extend(['-serial', 'null'])
+
+ def _configure_chardev_channels(self) -> None:
+ """Configure chardevs."""
+
+ self._chardevs = self._config.get_target_emu(
+ ['channels', 'chardevs'], True, dict
+ )
+
+ serials = {}
+ for name, config in self._chardevs.items():
+ chardev_id = config['id']
+ self._chardevs_id_to_name[chardev_id] = name
+
+ chardev_type = self._get_chardev_config(name, 'type')
+ if chardev_type is None:
+ chardev_type = 'tcp'
+
+ if chardev_type == 'pty':
+ backend = 'pty'
+ elif chardev_type == 'tcp':
+ backend = 'socket,host=localhost,port=0,server=on,wait=off'
+ else:
+ raise InvalidChannelType(chardev_type)
+
+ # serials are configured differently
+ if re.search(r'serial[0-9]*', chardev_id):
+ serials[chardev_id] = name
+ else:
+ self._start_cmd.extend(
+ ['-chardev', f'{backend},id={chardev_id}']
+ )
+
+ self._configure_serial_channels(serials)
+
+ def _pre_start(
+ self,
+ target: str,
+ file: Optional[Path] = None,
+ pause: bool = False,
+ debug: bool = False,
+ args: Optional[str] = None,
+ ) -> List[str]:
+ qemu = self._config.get_target_emu(['executable'])
+ if not qemu:
+ qemu = self._config.get_emu(['executable'], optional=False)
+ machine = self._config.get_target_emu(['machine'], optional=False)
+
+ self._start_cmd = [f'{qemu}', '-nographic', '-nodefaults']
+ self._start_cmd.extend(['-display', 'none'])
+ self._start_cmd.extend(['-machine', f'{machine}'])
+
+ try:
+ self._configure_default_channels()
+ self._configure_chardev_channels()
+ except KeyError as err:
+ raise ConfigError(self._config.path, str(err))
+
+ if pause:
+ self._start_cmd.append('-S')
+ if debug:
+ self._start_cmd.extend(['-d', 'guest_errors'])
+
+ if file:
+ self._start_cmd.extend(['-kernel', str(file)])
+
+ self._start_cmd.extend(self._config.get_emu(['args'], entry_type=list))
+ self._start_cmd.extend(
+ self._config.get_target_emu(['args'], entry_type=list)
+ )
+ if args:
+ self._start_cmd.extend(args.split(' '))
+
+ # initial/bootstrap qmp connection
+ self._qmp_init_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._qmp_init_sock.bind(('localhost', 0))
+ port = self._qmp_init_sock.getsockname()[1]
+ self._qmp_init_sock.listen()
+ self._qmp_init_sock.settimeout(30)
+ self._start_cmd.extend(['-qmp', f'tcp:localhost:{port}'])
+
+ return self._start_cmd
+
+ def _post_start(self) -> None:
+ assert self._qmp_init_sock is not None
+ conn, _ = self._qmp_init_sock.accept()
+ self._qmp_init_sock.close()
+ qmp = QmpClient(conn.makefile('rwb', buffering=0))
+ conn.close()
+
+ resp = qmp.request('query-chardev')
+ for chardev in resp:
+ label = chardev['label']
+ name = self._chardevs_id_to_name.get(label)
+ if name:
+ self._set_qemu_channel(name, chardev['filename'])
+
+ def _get_connector(self, wdir: Path) -> Connector:
+ return QemuConnector(wdir)
+
+
+class QemuConnector(Connector):
+ """qemu implementation for the emulator specific connector methods."""
+
+ def __init__(self, wdir: Path) -> None:
+ super().__init__(wdir)
+ if self.get_emu() != 'qemu':
+ raise WrongEmulator('qemu', self.get_emu())
+ self._qmp: Optional[QmpClient] = None
+
+ def _q(self) -> QmpClient:
+ if not self._qmp:
+ self._qmp = QmpClient(self.get_channel_stream('qmp'))
+ return self._qmp
+
+ def reset(self) -> None:
+ self._q().request('system_reset')
+
+ def cont(self) -> None:
+ self._q().request('cont')
+
+ def set_property(self, path: str, prop: str, value: Any) -> None:
+ args = {
+ 'path': '{}'.format(path),
+ 'property': prop,
+ 'value': value,
+ }
+ self._q().request('qom-set', args)
+
+ def get_property(self, path: str, prop: str) -> Any:
+ args = {
+ 'path': '{}'.format(path),
+ 'property': prop,
+ }
+ return self._q().request('qom-get', args)
+
+ def list_properties(self, path: str) -> List[Any]:
+ args = {
+ 'path': '{}'.format(path),
+ }
+ return self._q().request('qom-list', args)
diff --git a/pw_emu/py/pw_emu/renode.py b/pw_emu/py/pw_emu/renode.py
new file mode 100644
index 000000000..1e3ccb548
--- /dev/null
+++ b/pw_emu/py/pw_emu/renode.py
@@ -0,0 +1,209 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Pigweed renode frontend."""
+
+import socket
+import time
+import xmlrpc.client
+
+from pathlib import Path
+from typing import Optional, List, Any
+
+from pw_emu.core import (
+ Connector,
+ Handles,
+ InvalidChannelType,
+ Launcher,
+ Error,
+ WrongEmulator,
+)
+
+
+class RenodeRobotError(Error):
+ """Exception for Renode robot errors."""
+
+ def __init__(self, err: str):
+ super().__init__(err)
+
+
+class RenodeLauncher(Launcher):
+ """Start a new renode process for a given target and config file."""
+
+ def __init__(self, config_path: Optional[Path] = None):
+ super().__init__('renode', config_path)
+ self._start_cmd: List[str] = []
+
+ @staticmethod
+ def _allocate_port() -> int:
+ """Allocate renode ports.
+
+ This is inherently racy but renode currently does not have proper
+ support for dynamic ports. It accecept 0 as a port and the OS allocates
+ a dynamic port but there is no API to retrive the port.
+
+ """
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.bind(('localhost', 0))
+ port = sock.getsockname()[1]
+ sock.close()
+
+ return port
+
+ def _pre_start(
+ self,
+ target: str,
+ file: Optional[Path] = None,
+ pause: bool = False,
+ debug: bool = False,
+ args: Optional[str] = None,
+ ) -> List[str]:
+ renode = self._config.get_target_emu(['executable'])
+ if not renode:
+ renode = self._config.get_emu(['executable'], optional=False)
+
+ self._start_cmd.extend([f'{renode}', '--disable-xwt'])
+ port = self._allocate_port()
+ self._start_cmd.extend(['--robot-server-port', str(port)])
+ self._handles.add_channel_tcp('robot', 'localhost', port)
+
+ machine = self._config.get_target_emu(['machine'], optional=False)
+ self._start_cmd.extend(['--execute', f'mach add "{target}"'])
+
+ self._start_cmd.extend(
+ ['--execute', f'machine LoadPlatformDescription @{machine}']
+ )
+
+ terms = self._config.get_target_emu(
+ ['channels', 'terminals'], entry_type=dict
+ )
+ for name in terms.keys():
+ port = self._allocate_port()
+ dev_path = self._config.get_target_emu(
+ ['channels', 'terminals', name, 'device-path'],
+ optional=False,
+ entry_type=str,
+ )
+ term_type = self._config.get_target_emu(
+ ['channels', 'terminals', name, 'type'],
+ entry_type=str,
+ )
+ if not term_type:
+ term_type = self._config.get_emu(
+ ['channels', 'terminals', 'type'],
+ entry_type=str,
+ )
+ if not term_type:
+ term_type = 'tcp'
+
+ cmd = 'emulation '
+ if term_type == 'tcp':
+ cmd += f'CreateServerSocketTerminal {port} "{name}" false'
+ self._handles.add_channel_tcp(name, 'localhost', port)
+ elif term_type == 'pty':
+ path = self._path(name)
+ cmd += f'CreateUartPtyTerminal "{name}" "{path}"'
+ self._handles.add_channel_pty(name, str(path))
+ else:
+ raise InvalidChannelType(term_type)
+
+ self._start_cmd.extend(['--execute', cmd])
+ self._start_cmd.extend(
+ ['--execute', f'connector Connect {dev_path} {name}']
+ )
+
+ port = self._allocate_port()
+ self._start_cmd.extend(['--execute', f'machine StartGdbServer {port}'])
+ self._handles.add_channel_tcp('gdb', 'localhost', port)
+
+ if file:
+ self._start_cmd.extend(['--execute', f'sysbus LoadELF @{file}'])
+
+ if not pause:
+ self._start_cmd.extend(['--execute', 'start'])
+
+ return self._start_cmd
+
+ def _post_start(self) -> None:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ robot = self._handles.channels['gdb']
+ assert isinstance(robot, Handles.TcpChannel)
+
+ # renode is slow to start especially during host load
+ deadline = time.monotonic() + 120
+ connected = False
+ err = None
+ while time.monotonic() < deadline:
+ try:
+ sock.connect((robot.host, robot.port))
+ connected = True
+ break
+ except OSError as exc:
+ err = exc
+ time.sleep(1)
+
+ if not connected:
+ msg = 'failed to connect to robot channel'
+ msg += f'({robot.host}:{robot.port}): {err}'
+ raise RenodeRobotError(msg)
+
+ sock.close()
+
+ def _get_connector(self, wdir: Path) -> Connector:
+ return RenodeConnector(wdir)
+
+
+class RenodeConnector(Connector):
+ """renode implementation for the emulator specific connector methods."""
+
+ def __init__(self, wdir: Path) -> None:
+ super().__init__(wdir)
+ if self.get_emu() != 'renode':
+ raise WrongEmulator('renode', self.get_emu())
+ robot = self._handles.channels['robot']
+ host = robot.host
+ port = robot.port
+ self._proxy = xmlrpc.client.ServerProxy(f'http://{host}:{port}/')
+
+ def _request(self, cmd: str, args: List[str]) -> Any:
+ """Send a request using the robot interface.
+
+ Using the robot interface is not ideal since it is designed
+ for testing. However, it is more robust than the ANSI colored,
+ echoed, log mixed, telnet interface.
+
+ """
+
+ resp = self._proxy.run_keyword(cmd, args)
+ if not isinstance(resp, dict):
+ raise RenodeRobotError('expected dictionary in response')
+ if resp['status'] != 'PASS':
+ raise RenodeRobotError(resp['error'])
+ if resp.get('return'):
+ return resp['return']
+ return None
+
+ def reset(self) -> None:
+ self._request('ResetEmulation', [])
+
+ def cont(self) -> None:
+ self._request('StartEmulation', [])
+
+ def list_properties(self, path: str) -> List[Any]:
+ return self._request('ExecuteCommand', [f'{path}'])
+
+ def get_property(self, path: str, prop: str) -> Any:
+ return self._request('ExecuteCommand', [f'{path} {prop}'])
+
+ def set_property(self, path: str, prop: str, value: Any) -> None:
+ return self._request('ExecuteCommand', [f'{path} {prop} {value}'])
diff --git a/pw_emu/py/pyproject.toml b/pw_emu/py/pyproject.toml
new file mode 100644
index 000000000..78668a709
--- /dev/null
+++ b/pw_emu/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_emu/py/setup.cfg b/pw_emu/py/setup.cfg
new file mode 100644
index 000000000..5dd480aca
--- /dev/null
+++ b/pw_emu/py/setup.cfg
@@ -0,0 +1,28 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[metadata]
+name = pw_emu
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Pigweed Emulators Frontend
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+ python-daemon
+
+[options.package_data]
+pw_emu = py.typed
diff --git a/pw_cli/py/setup.py b/pw_emu/py/setup.py
index 73ab7458c..7b1cffbda 100644
--- a/pw_cli/py/setup.py
+++ b/pw_emu/py/setup.py
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,7 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_cli"""
+"""pw_emu"""
import setuptools # type: ignore
diff --git a/pw_emu/py/tests/__init__.py b/pw_emu/py/tests/__init__.py
new file mode 100644
index 000000000..0f2cb9104
--- /dev/null
+++ b/pw_emu/py/tests/__init__.py
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
diff --git a/pw_emu/py/tests/cli_test.py b/pw_emu/py/tests/cli_test.py
new file mode 100644
index 000000000..86bde6432
--- /dev/null
+++ b/pw_emu/py/tests/cli_test.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for the command line interface"""
+
+import os
+import signal
+import subprocess
+import sys
+import time
+import unittest
+
+from pathlib import Path
+from typing import List
+
+from mock_emu_frontend import _mock_emu
+from tests.common import ConfigHelper
+
+
+# TODO: b/301382004 - The Python Pigweed package install (into python-venv)
+# races with running this test and there is no way to add that package as a test
+# depedency without creating circular depedencies. This means we can't rely on
+# using Pigweed tools like pw cli or the arm-none-eabi-gdb wrapper.
+#
+# Run the CLI directly instead of going through pw cli.
+_cli_path = Path(
+ os.path.join(os.environ['PW_ROOT'], 'pw_emu', 'py', 'pw_emu', '__main__.py')
+).resolve()
+
+
+class TestCli(ConfigHelper):
+ """Test non-interactive commands"""
+
+ _config = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'mock-emu': {
+ 'tcp_channel': True,
+ 'gdb_channel': True,
+ },
+ 'gdb': _mock_emu + ['--exit', '--'],
+ 'targets': {'test-target': {'mock-emu': {}}},
+ }
+
+ def _build_cmd(self, args: List[str]) -> List[str]:
+ cmd = [
+ 'python',
+ str(_cli_path),
+ '--working-dir',
+ self._wdir.name,
+ '--config',
+ self._config_file,
+ ] + args
+ return cmd
+
+ def _run(self, args: List[str], **kwargs) -> subprocess.CompletedProcess:
+ """Run the CLI and wait for completion"""
+ return subprocess.run(self._build_cmd(args), **kwargs)
+
+ def _popen(self, args: List[str], **kwargs) -> subprocess.Popen:
+ """Run the CLI in the background"""
+ return subprocess.Popen(self._build_cmd(args), **kwargs)
+
+
+class TestNonInteractive(TestCli):
+ """Test non interactive commands."""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.assertEqual(self._run(['start', 'test-target']).returncode, 0)
+
+ def tearDown(self) -> None:
+ self.assertEqual(self._run(['stop']).returncode, 0)
+ super().tearDown()
+
+ def test_already_running(self) -> None:
+ self.assertNotEqual(self._run(['start', 'test-target']).returncode, 0)
+
+ def test_gdb_cmds(self) -> None:
+ status = self._run(
+ ['gdb-cmds', 'show version'],
+ )
+ self.assertEqual(status.returncode, 0)
+
+ def test_prop_ls(self) -> None:
+ status = self._run(['prop-ls', 'path1'], stdout=subprocess.PIPE)
+ self.assertEqual(status.returncode, 0)
+ self.assertTrue('prop1' in status.stdout.decode('ascii'))
+ status = self._run(['prop-ls', 'invalid path'], stdout=subprocess.PIPE)
+ self.assertNotEqual(status.returncode, 0)
+
+ def test_prop_get(self) -> None:
+ status = self._run(
+ ['prop-get', 'invalid path', 'prop1'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertNotEqual(status.returncode, 0)
+ status = self._run(
+ ['prop-get', 'path1', 'invalid prop'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertNotEqual(status.returncode, 0)
+ status = self._run(
+ ['prop-get', 'path1', 'prop1'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertEqual(status.returncode, 0)
+ self.assertTrue('val1' in status.stdout.decode('ascii'))
+
+ def test_prop_set(self) -> None:
+ status = self._run(
+ ['prop-set', 'invalid path', 'prop1', 'v'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertNotEqual(status.returncode, 0)
+ status = self._run(
+ ['prop-set', 'path1', 'invalid prop', 'v'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertNotEqual(status.returncode, 0)
+ status = self._run(
+ ['prop-set', 'path1', 'prop1', 'value'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertEqual(status.returncode, 0)
+ status = self._run(
+ ['prop-get', 'path1', 'prop1'],
+ stdout=subprocess.PIPE,
+ )
+ self.assertEqual(status.returncode, 0)
+ self.assertTrue('value' in status.stdout.decode('ascii'), status.stdout)
+
+ def test_reset(self) -> None:
+ self.assertEqual(self._run(['reset']).returncode, 0)
+ self.assertTrue(os.path.exists(os.path.join(self._wdir.name, 'reset')))
+
+ def test_load(self) -> None:
+ self.assertEqual(self._run(['load', 'executable']).returncode, 0)
+
+ def test_resume(self) -> None:
+ self.assertEqual(self._run(['resume']).returncode, 0)
+
+
+class TestForeground(TestCli):
+ """Test starting in foreground"""
+
+ def _test_common(self, cmd) -> None:
+ # Run the CLI process in a new session so that we can terminate both the
+ # CLI and the mock emulator it spawns in the foreground.
+ args = {}
+ if sys.platform == 'win32':
+ args['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
+ else:
+ args['start_new_session'] = True
+ proc = self._popen(cmd, stdout=subprocess.PIPE, **args)
+ assert proc.stdout
+ output = proc.stdout.readline()
+ self.assertTrue(
+ 'starting mock emulator' in output.decode('utf-8'),
+ output.decode('utf-8'),
+ )
+ if sys.platform == 'win32':
+ # See https://bugs.python.org/issue26350
+ os.kill(proc.pid, signal.CTRL_BREAK_EVENT)
+ else:
+ os.kill(-proc.pid, signal.SIGTERM)
+ proc.wait()
+ proc.stdout.close()
+
+ def test_foreground(self) -> None:
+ self._test_common(['start', '--foreground', 'test-target'])
+
+ def test_debug(self) -> None:
+ self._test_common(['start', '--debug', 'test-target'])
+
+
+class TestInteractive(TestCli):
+ """Test interactive commands"""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self.assertEqual(self._run(['start', 'test-target']).returncode, 0)
+
+ def tearDown(self) -> None:
+ self.assertEqual(self._run(['stop']).returncode, 0)
+ super().tearDown()
+
+ @staticmethod
+ def _read_nonblocking(fd: int, size: int) -> bytes:
+ try:
+ return os.read(fd, size)
+ except BlockingIOError:
+ return b''
+
+ def test_term(self) -> None:
+ """Test the pw emu term command"""
+
+ if sys.platform == 'win32':
+ self.skipTest('pty not supported on win32')
+
+ # pylint: disable=import-outside-toplevel
+ # Can't import pty on win32.
+ import pty
+
+ # pylint: disable=no-member
+ # Avoid pylint false positive on win32.
+ pid, fd = pty.fork()
+ if pid == 0:
+ status = self._run(['term', 'tcp'])
+ # pylint: disable=protected-access
+ # Use os._exit instead of os.exit after fork.
+ os._exit(status.returncode)
+ else:
+ expected = '--- Miniterm on tcp ---'
+
+ # Read the expected string with a timeout.
+ os.set_blocking(fd, False)
+ deadline = time.monotonic() + 5
+ data = self._read_nonblocking(fd, len(expected))
+ while len(data) < len(expected):
+ time.sleep(0.1)
+ data += self._read_nonblocking(fd, len(expected) - len(data))
+ if time.monotonic() > deadline:
+ break
+ self.assertTrue(
+ expected in data.decode('ascii'),
+ data + self._read_nonblocking(fd, 100),
+ )
+
+ # send CTRL + ']' to terminate miniterm
+ os.write(fd, b'\x1d')
+
+ # wait for the process to exit, with a timeout
+ deadline = time.monotonic() + 5
+ wait_pid, ret = os.waitpid(pid, os.WNOHANG)
+ while wait_pid == 0:
+ time.sleep(0.1)
+ # Discard input to avoid writer hang on MacOS,
+ # see https://github.com/python/cpython/issues/97001.
+ try:
+ self._read_nonblocking(fd, 100)
+ except OSError:
+ # Avoid read errors when the child pair of the pty
+ # closes when the child terminates.
+ pass
+ wait_pid, ret = os.waitpid(pid, os.WNOHANG)
+ if time.monotonic() > deadline:
+ break
+ self.assertEqual(wait_pid, pid)
+ self.assertEqual(ret, 0)
+
+ def test_gdb(self) -> None:
+ res = self._run(['gdb', '-e', 'executable'], stdout=subprocess.PIPE)
+ self.assertEqual(res.returncode, 0)
+ output = res.stdout.decode('ascii')
+ self.assertTrue('target remote' in output, output)
+ self.assertTrue('executable' in output, output)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_emu/py/tests/common.py b/pw_emu/py/tests/common.py
new file mode 100644
index 000000000..9d2513fcc
--- /dev/null
+++ b/pw_emu/py/tests/common.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Common utilities for tests."""
+
+import json
+import os
+import subprocess
+import tempfile
+import unittest
+
+from pathlib import Path
+from typing import Any, Optional, Dict
+
+from pw_emu.frontend import Emulator
+
+
+def check_prog(prog: str) -> tuple:
+ msg = f'running {prog}'
+ try:
+ proc = subprocess.run([prog, '--help'], capture_output=True)
+ if proc.returncode != 0:
+ output = proc.stdout.decode('ascii') + proc.stderr.decode('ascii')
+ msg = f'error {msg}: {output}'
+ return (False, msg)
+ except OSError as err:
+ msg = f'error {msg}: {str(err)}'
+ return (False, msg)
+ return (True, msg)
+
+
+class ConfigHelper(unittest.TestCase):
+ """Helper that setups and tears down the configuration file"""
+
+ _config: Optional[Dict[str, Any]] = None
+
+ def setUp(self) -> None:
+ self._wdir = tempfile.TemporaryDirectory()
+ with tempfile.NamedTemporaryFile('wt', delete=False) as file:
+ pw_emu_config: Dict[str, Any] = {'pw': {'pw_emu': {}}}
+ if self._config:
+ pw_emu_config['pw']['pw_emu'].update(self._config)
+ json.dump(pw_emu_config, file)
+ self._config_file = file.name
+
+ def tearDown(self) -> None:
+ self._wdir.cleanup()
+ os.unlink(self._config_file)
+
+
+class ConfigHelperWithEmulator(ConfigHelper):
+ """Helper that setups and tears down the configuration file"""
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._emu = Emulator(Path(self._wdir.name), Path(self._config_file))
diff --git a/pw_emu/py/tests/core_test.py b/pw_emu/py/tests/core_test.py
new file mode 100644
index 000000000..1131d5dd3
--- /dev/null
+++ b/pw_emu/py/tests/core_test.py
@@ -0,0 +1,480 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for core / infrastructure code."""
+
+import copy
+import io
+import json
+import os
+import socket
+import sys
+import tempfile
+import time
+import unittest
+from pathlib import Path
+from typing import Any, Dict
+
+from unittest.mock import patch
+
+from pw_emu.core import (
+ AlreadyRunning,
+ Config,
+ ConfigError,
+ Handles,
+ InvalidTarget,
+ InvalidChannelName,
+ InvalidChannelType,
+ Launcher,
+)
+from mock_emu_frontend import _mock_emu
+from tests.common import ConfigHelper
+
+
+class ConfigHelperWithLauncher(ConfigHelper):
+ def setUp(self) -> None:
+ super().setUp()
+ self._launcher = Launcher.get('mock-emu', Path(self._config_file))
+
+
+class TestInvalidTarget(ConfigHelperWithLauncher):
+ """Check that InvalidTarget is raised with an empty config."""
+
+ _config: Dict[str, Any] = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ }
+
+ def test_invalid_target(self) -> None:
+ with self.assertRaises(InvalidTarget):
+ self._launcher.start(Path(self._wdir.name), 'test-target')
+
+
+class TestStart(ConfigHelperWithLauncher):
+ """Start tests for valid config."""
+
+ _config: Dict[str, Any] = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'targets': {'test-target': {'mock-emu': {}}},
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._connector = self._launcher.start(
+ Path(self._wdir.name), 'test-target'
+ )
+
+ def tearDown(self) -> None:
+ self._connector.stop()
+ super().tearDown()
+
+ def test_running(self) -> None:
+ self.assertTrue(self._connector.running())
+
+ def test_pid_file(self) -> None:
+ self.assertTrue(
+ os.path.exists(os.path.join(self._wdir.name, 'mock-emu.pid'))
+ )
+
+ def test_handles_file(self) -> None:
+ self.assertTrue(
+ os.path.exists(os.path.join(self._wdir.name, 'handles.json'))
+ )
+
+ def test_already_running(self) -> None:
+ with self.assertRaises(AlreadyRunning):
+ self._launcher.start(Path(self._wdir.name), 'test-target')
+
+ def test_log(self) -> None:
+ exp = 'starting mock emulator'
+ path = os.path.join(self._wdir.name, 'mock-emu.log')
+ deadline = time.monotonic() + 100
+ while os.path.getsize(path) < len(exp):
+ time.sleep(0.1)
+ if time.monotonic() > deadline:
+ break
+
+ with open(os.path.join(self._wdir.name, 'mock-emu.log'), 'rt') as file:
+ data = file.read()
+ self.assertTrue(exp in data, data)
+
+
+class TestPrePostStartCmds(ConfigHelperWithLauncher):
+ """Tests for configurations with pre-start commands."""
+
+ _config: Dict[str, Any] = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'targets': {
+ 'test-target': {
+ 'pre-start-cmds': {
+ 'pre-1': _mock_emu + ['pre-1'],
+ 'pre-2': _mock_emu + ['$pw_emu_wdir{pre-2}'],
+ },
+ 'post-start-cmds': {
+ 'post-1': _mock_emu
+ + ['$pw_emu_channel_path{test_subst_pty}'],
+ 'post-2': _mock_emu
+ + [
+ '$pw_emu_channel_host{test_subst_tcp}',
+ '$pw_emu_channel_port{test_subst_tcp}',
+ ],
+ },
+ 'mock-emu': {},
+ }
+ },
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._connector = self._launcher.start(
+ Path(self._wdir.name), 'test-target'
+ )
+
+ def tearDown(self) -> None:
+ self._connector.stop()
+ super().tearDown()
+
+ def test_running(self) -> None:
+ for proc in self._connector.get_procs().keys():
+ self.assertTrue(self._connector.proc_running(proc))
+
+ def test_stop(self) -> None:
+ self._connector.stop()
+ for proc in self._connector.get_procs().keys():
+ self.assertFalse(self._connector.proc_running(proc))
+
+ def test_pid_files(self) -> None:
+ for proc in ['pre-1', 'pre-2', 'post-1', 'post-2']:
+ self.assertTrue(
+ os.path.exists(os.path.join(self._wdir.name, f'{proc}.pid'))
+ )
+
+ def test_logs(self):
+ expect = {
+ 'pre-1.log': 'pre-1',
+ 'pre-2.log': os.path.join(self._wdir.name, 'pre-2'),
+ 'post-1.log': 'pty-path',
+ 'post-2.log': 'localhost 1234',
+ }
+
+ for log, pattern in expect.items():
+ path = os.path.join(self._wdir.name, log)
+ deadline = time.monotonic() + 100
+ while os.path.getsize(path) < len(pattern):
+ time.sleep(0.1)
+ if time.monotonic() > deadline:
+ break
+ with open(os.path.join(self._wdir.name, log)) as file:
+ data = file.read()
+ self.assertTrue(pattern in data, f'`{pattern}` not in `{data}`')
+
+
+class TestStop(ConfigHelperWithLauncher):
+ """Stop tests for valid config."""
+
+ _config: Dict[str, Any] = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'targets': {'test-target': {'mock-emu': {}}},
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._connector = self._launcher.start(
+ Path(self._wdir.name), 'test-target'
+ )
+ self._connector.stop()
+
+ def test_pid_files(self) -> None:
+ self.assertFalse(
+ os.path.exists(os.path.join(self._wdir.name, 'emu.pid'))
+ )
+
+ def test_target_file(self) -> None:
+ self.assertFalse(
+ os.path.exists(os.path.join(self._wdir.name, 'target'))
+ )
+
+ def test_running(self) -> None:
+ self.assertFalse(self._connector.running())
+
+
+class TestChannels(ConfigHelperWithLauncher):
+ """Test Connector channels APIs."""
+
+ _config: Dict[str, Any] = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'mock-emu': {
+ 'tcp_channel': True,
+ 'pty_channel': sys.platform != 'win32',
+ },
+ 'targets': {'test-target': {'mock-emu': {}}},
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._connector = self._launcher.start(
+ Path(self._wdir.name), 'test-target'
+ )
+
+ def tearDown(self) -> None:
+ self._connector.stop()
+ super().tearDown()
+
+ def test_tcp_channel_addr(self) -> None:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect(self._connector.get_channel_addr('tcp'))
+ sock.close()
+
+ def test_get_channel_type(self) -> None:
+ self.assertEqual(self._connector.get_channel_type('tcp'), 'tcp')
+ if sys.platform != 'win32':
+ self.assertEqual(self._connector.get_channel_type('pty'), 'pty')
+ with self.assertRaises(InvalidChannelName):
+ self._connector.get_channel_type('invalid channel')
+
+ def test_pty_channel_path(self) -> None:
+ if sys.platform == 'win32':
+ self.skipTest('pty not supported on win32')
+ self.assertTrue(os.path.exists(self._connector.get_channel_path('pty')))
+ with self.assertRaises(InvalidChannelType):
+ self._connector.get_channel_path('tcp')
+
+ def _test_stream(self, stream: io.RawIOBase) -> None:
+ for char in [b'1', b'2', b'3', b'4']:
+ stream.write(char)
+ self.assertEqual(stream.read(1), char)
+
+ def test_tcp_stream(self) -> None:
+ with self._connector.get_channel_stream('tcp') as stream:
+ self._test_stream(stream)
+
+ def test_pty_stream(self) -> None:
+ if sys.platform == 'win32':
+ self.skipTest('pty not supported on win32')
+ with self._connector.get_channel_stream('pty') as stream:
+ self._test_stream(stream)
+
+
+class TestTargetFragments(unittest.TestCase):
+ """Tests for configurations using target fragments."""
+
+ _config_templ: Dict[str, Any] = {
+ 'pw': {
+ 'pw_emu': {
+ 'target_files': [],
+ 'targets': {
+ 'test-target': {
+ 'test-key': 'test-value',
+ }
+ },
+ }
+ }
+ }
+
+ _tf1_config: Dict[str, Any] = {
+ 'targets': {
+ 'test-target1': {},
+ 'test-target': {
+ 'test-key': 'test-value-file1',
+ },
+ }
+ }
+
+ _tf2_config: Dict[str, Any] = {'targets': {'test-target2': {}}}
+
+ def setUp(self) -> None:
+ with tempfile.NamedTemporaryFile(
+ 'wt', delete=False
+ ) as config_file, tempfile.NamedTemporaryFile(
+ 'wt', delete=False
+ ) as targets1_file, tempfile.NamedTemporaryFile(
+ 'wt', delete=False
+ ) as targets2_file:
+ self._config_path = config_file.name
+ self._targets1_path = targets1_file.name
+ self._targets2_path = targets2_file.name
+ json.dump(self._tf1_config, targets1_file)
+ json.dump(self._tf2_config, targets2_file)
+ config = copy.deepcopy(self._config_templ)
+ config['pw']['pw_emu']['target_files'].append(self._targets1_path)
+ config['pw']['pw_emu']['target_files'].append(self._targets2_path)
+ json.dump(config, config_file)
+ self._config = Config(Path(self._config_path))
+
+ def tearDown(self) -> None:
+ os.unlink(self._config_path)
+ os.unlink(self._targets1_path)
+ os.unlink(self._targets2_path)
+
+ def test_targets_loaded(self) -> None:
+ self.assertIsNotNone(self._config.get(['targets', 'test-target']))
+ self.assertIsNotNone(self._config.get(['targets', 'test-target1']))
+ self.assertIsNotNone(self._config.get(['targets', 'test-target2']))
+
+ def test_targets_priority(self) -> None:
+ self.assertEqual(
+ self._config.get(['targets', 'test-target', 'test-key']),
+ 'test-value',
+ )
+
+
+class TestHandles(unittest.TestCase):
+ """Tests for Handles."""
+
+ _config = {
+ 'emu': 'mock-emu',
+ 'config': 'test-config',
+ 'target': 'test-target',
+ 'gdb_cmd': ['test-gdb'],
+ 'channels': {
+ 'tcp_chan': {'type': 'tcp', 'host': 'localhost', 'port': 1234},
+ 'pty_chan': {'type': 'pty', 'path': 'path'},
+ },
+ 'procs': {'proc0': {'pid': 1983}, 'proc1': {'pid': 1234}},
+ }
+
+ def test_serialize(self):
+ handles = Handles('mock-emu', 'test-config')
+ handles.add_channel_tcp('tcp_chan', 'localhost', 1234)
+ handles.add_channel_pty('pty_chan', 'path')
+ handles.add_proc('proc0', 1983)
+ handles.add_proc('proc1', 1234)
+ handles.set_target('test-target')
+ handles.set_gdb_cmd(['test-gdb'])
+ tmp = tempfile.TemporaryDirectory()
+ handles.save(Path(tmp.name))
+ with open(os.path.join(tmp.name, 'handles.json'), 'rt') as file:
+ self.assertTrue(json.load(file) == self._config)
+ tmp.cleanup()
+
+ def test_load(self):
+ tmp = tempfile.TemporaryDirectory()
+ with open(os.path.join(tmp.name, 'handles.json'), 'wt') as file:
+ json.dump(self._config, file)
+ handles = Handles.load(Path(tmp.name))
+ self.assertEqual(handles.emu, 'mock-emu')
+ self.assertEqual(handles.gdb_cmd, ['test-gdb'])
+ self.assertEqual(handles.target, 'test-target')
+ self.assertEqual(handles.config, 'test-config')
+ tmp.cleanup()
+
+
+class TestConfig(ConfigHelper):
+ """Stop tests for valid config."""
+
+ _config: Dict[str, Any] = {
+ 'top': 'entry',
+ 'multi': {
+ 'level': {
+ 'entry': 0,
+ },
+ },
+ 'subst': 'a/$pw_env{PW_EMU_TEST_ENV_SUBST}/c',
+ 'targets': {
+ 'test-target': {
+ 'entry': [1, 2, 3],
+ 'mock-emu': {
+ 'entry': 'test',
+ },
+ }
+ },
+ 'mock-emu': {
+ 'executable': _mock_emu,
+ },
+ 'list': ['a', '$pw_env{PW_EMU_TEST_ENV_SUBST}', 'c'],
+ 'bad-subst-type': '$pw_bad_subst_type{test}',
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._cfg = Config(Path(self._config_file), 'test-target', 'mock-emu')
+
+ def test_top_entry(self) -> None:
+ self.assertEqual(self._cfg.get(['top']), 'entry')
+
+ def test_empty_subst(self) -> None:
+ with self.assertRaises(ConfigError):
+ self._cfg.get(['subst'])
+
+ def test_subst(self) -> None:
+ with patch.dict('os.environ', {'PW_EMU_TEST_ENV_SUBST': 'b'}):
+ self.assertEqual(self._cfg.get(['subst']), 'a/b/c')
+
+ def test_multi_level_entry(self) -> None:
+ self.assertEqual(self._cfg.get(['multi', 'level', 'entry']), 0)
+
+ def test_get_target(self) -> None:
+ self.assertEqual(self._cfg.get_targets(), ['test-target'])
+
+ def test_target(self) -> None:
+ self.assertEqual(self._cfg.get_target(['entry']), [1, 2, 3])
+
+ def test_target_emu(self) -> None:
+ self.assertEqual(self._cfg.get_target_emu(['entry']), 'test')
+
+ def test_type_checking(self) -> None:
+ with self.assertRaises(ConfigError):
+ self._cfg.get(['top'], entry_type=int)
+ self._cfg.get(['top'], entry_type=str)
+ self._cfg.get_target(['entry'], entry_type=list)
+ self._cfg.get_target_emu(['entry'], entry_type=str)
+ self._cfg.get(['targets'], entry_type=dict)
+
+ def test_non_optional(self) -> None:
+ with self.assertRaises(ConfigError):
+ self._cfg.get(['non-existing'], optional=False)
+
+ def test_optional(self) -> None:
+ self.assertEqual(self._cfg.get(['non-existing']), None)
+ self.assertEqual(self._cfg.get(['non-existing'], entry_type=int), 0)
+ self.assertEqual(self._cfg.get(['non-existing'], entry_type=str), '')
+ self.assertEqual(self._cfg.get(['non-existing'], entry_type=list), [])
+
+ def test_list(self) -> None:
+ with self.assertRaises(ConfigError):
+ self._cfg.get(['list'])
+ with patch.dict('os.environ', {'PW_EMU_TEST_ENV_SUBST': 'b'}):
+ self.assertEqual(self._cfg.get(['list']), ['a', 'b', 'c'])
+
+ def test_bad_subst(self) -> None:
+ with self.assertRaises(ConfigError):
+ self._cfg.get(['bad-subst-type'])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_emu/py/tests/frontend_test.py b/pw_emu/py/tests/frontend_test.py
new file mode 100644
index 000000000..36ea3d7b2
--- /dev/null
+++ b/pw_emu/py/tests/frontend_test.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Emulator API tests."""
+
+import unittest
+
+from typing import Any, Dict
+
+from pw_emu.core import (
+ ConfigError,
+ InvalidChannelType,
+ InvalidProperty,
+ InvalidPropertyPath,
+)
+from mock_emu_frontend import _mock_emu
+from tests.common import ConfigHelperWithEmulator
+
+
+class TestEmulator(ConfigHelperWithEmulator):
+ """Test Emulator APIs."""
+
+ _config = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'mock-emu': {
+ 'gdb_channel': True,
+ },
+ 'gdb': _mock_emu + ['--exit', '--'],
+ 'targets': {'test-target': {'mock-emu': {}}},
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._emu.start('test-target')
+
+ def tearDown(self) -> None:
+ self._emu.stop()
+ super().tearDown()
+
+ def test_gdb_target_remote(self) -> None:
+ output = self._emu.run_gdb_cmds([]).stdout
+ host, port = self._emu.get_channel_addr('gdb')
+ self.assertTrue(f'{host}:{port}' in output.decode('utf-8'))
+
+ def test_gdb_commands(self) -> None:
+ output = self._emu.run_gdb_cmds(['test_gdb_cmd']).stdout
+ self.assertTrue(
+ output and '-ex test_gdb_cmd' in output.decode('utf-8'), output
+ )
+
+ def test_gdb_executable(self) -> None:
+ output = self._emu.run_gdb_cmds([], 'test_gdb_exec').stdout
+ self.assertTrue('test_gdb_exec' in output.decode('utf-8'))
+
+ def test_gdb_pause(self) -> None:
+ output = self._emu.run_gdb_cmds([], pause=True).stdout
+ self.assertTrue('-ex disconnect' in output.decode('utf-8'))
+
+ # Minimal testing for APIs that are straight wrappers over Connector APIs.
+ def test_running(self) -> None:
+ self.assertTrue(self._emu.running())
+
+ def test_reset(self) -> None:
+ self._emu.reset()
+
+ def test_cont(self) -> None:
+ self._emu.cont()
+
+ def test_list_properties(self) -> None:
+ with self.assertRaises(InvalidPropertyPath):
+ self._emu.list_properties('invalid path')
+ self.assertEqual(self._emu.list_properties('path1'), ['prop1'])
+
+ def test_get_property(self) -> None:
+ with self.assertRaises(InvalidProperty):
+ self._emu.get_property('path1', 'invalid property')
+ with self.assertRaises(InvalidPropertyPath):
+ self._emu.get_property('invalid path', 'prop1')
+ self.assertEqual(self._emu.get_property('path1', 'prop1'), 'val1')
+
+ def test_set_property(self) -> None:
+ with self.assertRaises(InvalidPropertyPath):
+ self._emu.set_property('invalid path', 'prop1', 'test')
+ with self.assertRaises(InvalidProperty):
+ self._emu.set_property('path1', 'invalid property', 'test')
+ self._emu.set_property('path1', 'prop1', 'val2')
+ self.assertEqual(self._emu.get_property('path1', 'prop1'), 'val2')
+
+ def test_get_channel_type(self) -> None:
+ self.assertEqual(self._emu.get_channel_type('gdb'), 'tcp')
+
+ def test_get_channel_path(self) -> None:
+ with self.assertRaises(InvalidChannelType):
+ self._emu.get_channel_path('gdb')
+
+ def test_get_channel_addr(self) -> None:
+ self.assertEqual(len(self._emu.get_channel_addr('gdb')), 2)
+
+ def test_channel_stream(self) -> None:
+ with self._emu.get_channel_stream('gdb') as _:
+ pass
+
+
+class TestGdbEmptyConfig(ConfigHelperWithEmulator):
+ """Check that ConfigError is raised when running gdb with an empty
+ gdb config.
+
+ """
+
+ _config: Dict[str, Any] = {
+ 'emulators': {
+ 'mock-emu': {
+ 'launcher': 'mock_emu_frontend.MockEmuLauncher',
+ 'connector': 'mock_emu_frontend.MockEmuConnector',
+ }
+ },
+ 'targets': {'test-target': {'mock-emu': {}}},
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ self._emu.start('test-target')
+
+ def tearDown(self) -> None:
+ self._emu.stop()
+ super().tearDown()
+
+ def test_gdb_config_error(self) -> None:
+ with self.assertRaises(ConfigError):
+ self._emu.run_gdb_cmds([])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_emu/py/tests/py.typed b/pw_emu/py/tests/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_emu/py/tests/py.typed
diff --git a/pw_emu/py/tests/qemu_test.py b/pw_emu/py/tests/qemu_test.py
new file mode 100644
index 000000000..c84fb2b6d
--- /dev/null
+++ b/pw_emu/py/tests/qemu_test.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""QEMU emulator tests."""
+
+import json
+import os
+import socket
+import sys
+import tempfile
+import time
+import unittest
+
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+from pw_emu.core import InvalidChannelName, InvalidChannelType
+from tests.common import check_prog, ConfigHelperWithEmulator
+
+
+# TODO: b/301382004 - The Python Pigweed package install (into python-venv)
+# races with running this test and there is no way to add that package as a test
+# depedency without creating circular depedencies. This means we can't rely on
+# using Pigweed tools like pw cli or the arm-none-eabi-gdb wrapper.
+#
+# run the arm_gdb.py wrapper directly
+_arm_none_eabi_gdb_path = Path(
+ os.path.join(
+ os.environ['PW_ROOT'],
+ 'pw_env_setup',
+ 'py',
+ 'pw_env_setup',
+ 'entry_points',
+ 'arm_gdb.py',
+ )
+).resolve()
+
+
+class TestQemu(ConfigHelperWithEmulator):
+ """Tests for a valid qemu configuration."""
+
+ _config = {
+ 'gdb': ['python', str(_arm_none_eabi_gdb_path)],
+ 'qemu': {
+ 'executable': 'qemu-system-arm',
+ },
+ 'targets': {
+ 'test-target': {
+ 'ignore1': None,
+ 'qemu': {
+ 'machine': 'lm3s6965evb',
+ 'channels': {
+ 'chardevs': {
+ 'test_uart': {
+ 'id': 'serial0',
+ }
+ }
+ },
+ },
+ 'ignore2': None,
+ }
+ },
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ # No image so start paused to avoid crashing.
+ self._emu.start(target='test-target', pause=True)
+
+ def tearDown(self) -> None:
+ self._emu.stop()
+ super().tearDown()
+
+ def test_running(self) -> None:
+ self.assertTrue(self._emu.running())
+
+ def test_list_properties(self) -> None:
+ self.assertIsNotNone(self._emu.list_properties('/machine'))
+
+ def test_get_property(self) -> None:
+ self.assertEqual(
+ self._emu.get_property('/machine', 'type'), 'lm3s6965evb-machine'
+ )
+
+ def test_set_property(self) -> None:
+ self._emu.set_property('/machine', 'graphics', False)
+ self.assertFalse(self._emu.get_property('/machine', 'graphics'))
+
+ def test_bad_channel_name(self) -> None:
+ with self.assertRaises(InvalidChannelName):
+ self._emu.get_channel_addr('serial1')
+
+ def get_reg(self, addr: int) -> bytes:
+ temp = tempfile.NamedTemporaryFile(delete=False)
+ temp.close()
+
+ res = self._emu.run_gdb_cmds(
+ [
+ f'dump val {temp.name} *(char*){addr}',
+ 'disconnect',
+ ]
+ )
+ self.assertEqual(res.returncode, 0, res.stderr.decode('ascii'))
+
+ with open(temp.name, 'rb') as file:
+ ret = file.read(1)
+
+ self.assertNotEqual(ret, b'', res.stderr.decode('ascii'))
+
+ os.unlink(temp.name)
+
+ return ret
+
+ def poll_data(self, timeout: int) -> Optional[bytes]:
+ uartris = 0x4000C03C
+ uartrd = 0x4000C000
+
+ deadline = time.monotonic() + timeout
+ while self.get_reg(uartris) == b'\x00':
+ time.sleep(0.1)
+ if time.monotonic() > deadline:
+ return None
+ return self.get_reg(uartrd)
+
+ def test_channel_stream(self) -> None:
+ ok, msg = check_prog('arm-none-eabi-gdb')
+ if not ok:
+ self.skipTest(msg)
+
+ stream = self._emu.get_channel_stream('test_uart')
+ stream.write('test\n'.encode('ascii'))
+
+ self.assertEqual(self.poll_data(5), b't')
+ self.assertEqual(self.poll_data(5), b'e')
+ self.assertEqual(self.poll_data(5), b's')
+ self.assertEqual(self.poll_data(5), b't')
+
+ def test_gdb(self) -> None:
+ self._emu.run_gdb_cmds(['c'])
+ deadline = time.monotonic() + 5
+ while self._emu.running():
+ if time.monotonic() > deadline:
+ return
+ self.assertFalse(self._emu.running())
+
+
+class TestQemuChannelsTcp(TestQemu):
+ """Tests for configurations using TCP channels."""
+
+ _config: Dict[str, Any] = {}
+ _config.update(json.loads(json.dumps(TestQemu._config)))
+ _config['qemu']['channels'] = {'type': 'tcp'}
+
+ def test_get_channel_addr(self) -> None:
+ host, port = self._emu.get_channel_addr('test_uart')
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((host, port))
+ sock.close()
+
+
+class TestQemuChannelsPty(TestQemu):
+ """Tests for configurations using PTY channels."""
+
+ _config: Dict[str, Any] = {}
+ _config.update(json.loads(json.dumps(TestQemu._config)))
+ _config['qemu']['channels'] = {'type': 'pty'}
+
+ def setUp(self):
+ if sys.platform == 'win32':
+ self.skipTest('pty not supported on win32')
+ super().setUp()
+
+ def test_get_path(self) -> None:
+ self.assertTrue(os.path.exists(self._emu.get_channel_path('test_uart')))
+
+
+class TestQemuInvalidChannelType(ConfigHelperWithEmulator):
+ """Test invalid channel type configuration."""
+
+ _config = {
+ 'qemu': {
+ 'executable': 'qemu-system-arm',
+ 'channels': {'type': 'invalid'},
+ },
+ 'targets': {
+ 'test-target': {
+ 'qemu': {
+ 'machine': 'lm3s6965evb',
+ }
+ }
+ },
+ }
+
+ def test_start(self) -> None:
+ with self.assertRaises(InvalidChannelType):
+ self._emu.start('test-target', pause=True)
+
+
+class TestQemuTargetChannelsMixed(ConfigHelperWithEmulator):
+ """Test configuration with mixed channels types."""
+
+ _config = {
+ 'qemu': {
+ 'executable': 'qemu-system-arm',
+ },
+ 'targets': {
+ 'test-target': {
+ 'qemu': {
+ 'machine': 'lm3s6965evb',
+ 'channels': {
+ 'chardevs': {
+ 'test_uart0': {
+ 'id': 'serial0',
+ },
+ 'test_uart1': {
+ 'id': 'serial1',
+ 'type': 'tcp',
+ },
+ 'test_uart2': {
+ 'id': 'serial2',
+ 'type': 'pty',
+ },
+ }
+ },
+ }
+ }
+ },
+ }
+
+ def setUp(self) -> None:
+ if sys.platform == 'win32':
+ self.skipTest('pty not supported on win32')
+ super().setUp()
+ # no image to run so start paused
+ self._emu.start('test-target', pause=True)
+
+ def tearDown(self) -> None:
+ self._emu.stop()
+ super().tearDown()
+
+ def test_uart0_addr(self) -> None:
+ host, port = self._emu.get_channel_addr('test_uart0')
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((host, port))
+ sock.close()
+
+ def test_uart1_addr(self) -> None:
+ host, port = self._emu.get_channel_addr('test_uart1')
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.connect((host, port))
+ sock.close()
+
+ def test_uart2_path(self) -> None:
+ self.assertTrue(
+ os.path.exists(self._emu.get_channel_path('test_uart2'))
+ )
+
+
+def main() -> None:
+ ok, msg = check_prog('qemu-system-arm')
+ if not ok:
+ print(f'skipping tests: {msg}')
+ sys.exit(0)
+
+ unittest.main()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_emu/py/tests/renode_test.py b/pw_emu/py/tests/renode_test.py
new file mode 100644
index 000000000..acb9e47a9
--- /dev/null
+++ b/pw_emu/py/tests/renode_test.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""renode emulator tests."""
+
+import json
+import os
+import sys
+import struct
+import tempfile
+import time
+import unittest
+
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+from pw_emu.core import InvalidChannelName, InvalidChannelType
+from tests.common import check_prog, ConfigHelperWithEmulator
+
+
+# TODO: b/301382004 - The Python Pigweed package install (into python-venv)
+# races with running this test and there is no way to add that package as a test
+# depedency without creating circular depedencies. This means we can't rely on
+# using Pigweed tools like pw cli or the arm-none-eabi-gdb wrapper.
+#
+# run the arm_gdb.py wrapper directly
+_arm_none_eabi_gdb_path = Path(
+ os.path.join(
+ os.environ['PW_ROOT'],
+ 'pw_env_setup',
+ 'py',
+ 'pw_env_setup',
+ 'entry_points',
+ 'arm_gdb.py',
+ )
+).resolve()
+
+
+class TestRenode(ConfigHelperWithEmulator):
+ """Tests for a valid renode configuration."""
+
+ _config = {
+ 'gdb': ['python', str(_arm_none_eabi_gdb_path)],
+ 'renode': {
+ 'executable': 'renode',
+ },
+ 'targets': {
+ 'test-target': {
+ 'renode': {
+ 'machine': 'platforms/boards/stm32f4_discovery-kit.repl',
+ }
+ }
+ },
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ # no image to run so start paused
+ self._emu.start(target='test-target', pause=True)
+
+ def tearDown(self) -> None:
+ self._emu.stop()
+ super().tearDown()
+
+ def test_running(self) -> None:
+ self.assertTrue(self._emu.running())
+
+ def test_list_properties(self) -> None:
+ self.assertIsNotNone(self._emu.list_properties('sysbus.usart1'))
+
+ def test_get_property(self) -> None:
+ self.assertIsNotNone(
+ self._emu.get_property('sysbus.usart1', 'BaudRate')
+ )
+
+ def test_set_property(self) -> None:
+ self._emu.set_property('sysbus.timer1', 'Frequency', 100)
+ self.assertEqual(
+ int(self._emu.get_property('sysbus.timer1', 'Frequency'), 16), 100
+ )
+
+
+class TestRenodeInvalidChannelType(ConfigHelperWithEmulator):
+ """Test invalid channel type configuration."""
+
+ _config = {
+ 'renode': {
+ 'executable': 'renode',
+ },
+ 'targets': {
+ 'test-target': {
+ 'renode': {
+ 'machine': 'platforms/boards/stm32f4_discovery-kit.repl',
+ 'channels': {
+ 'terminals': {
+ 'test_uart': {
+ 'device-path': 'sysbus.usart1',
+ 'type': 'invalid',
+ }
+ }
+ },
+ }
+ }
+ },
+ }
+
+ def test_start(self) -> None:
+ with self.assertRaises(InvalidChannelType):
+ self._emu.start('test-target', pause=True)
+
+
+class TestRenodeChannels(ConfigHelperWithEmulator):
+ """Tests for a valid renode channels configuration."""
+
+ _config = {
+ 'gdb': ['python', str(_arm_none_eabi_gdb_path)],
+ 'renode': {
+ 'executable': 'renode',
+ },
+ 'targets': {
+ 'test-target': {
+ 'renode': {
+ 'machine': 'platforms/boards/stm32f4_discovery-kit.repl',
+ 'channels': {
+ 'terminals': {
+ 'test_uart': {
+ 'device-path': 'sysbus.usart1',
+ }
+ }
+ },
+ }
+ }
+ },
+ }
+
+ def setUp(self) -> None:
+ super().setUp()
+ # no image to run so start paused
+ self._emu.start(target='test-target', pause=True)
+
+ def tearDown(self) -> None:
+ self._emu.stop()
+ super().tearDown()
+
+ def test_bad_channel_name(self) -> None:
+ with self.assertRaises(InvalidChannelName):
+ self._emu.get_channel_addr('serial1')
+
+ def set_reg(self, addr: int, val: int) -> None:
+ self._emu.run_gdb_cmds([f'set *(unsigned int*){addr}={val}'])
+
+ def get_reg(self, addr: int) -> int:
+ temp = tempfile.NamedTemporaryFile(delete=False)
+ temp.close()
+
+ res = self._emu.run_gdb_cmds(
+ [
+ f'dump val {temp.name} *(char*){addr}',
+ 'disconnect',
+ ]
+ )
+ self.assertEqual(res.returncode, 0, res.stderr.decode('ascii'))
+
+ with open(temp.name, 'rb') as file:
+ ret = file.read(1)
+
+ self.assertNotEqual(ret, b'', res.stderr.decode('ascii'))
+
+ os.unlink(temp.name)
+
+ return struct.unpack('B', ret)[0]
+
+ def poll_data(self, timeout: int) -> Optional[int]:
+ usart_sr = 0x40011000
+ usart_dr = 0x40011004
+ deadline = time.monotonic() + timeout
+ while self.get_reg(usart_sr) & 0x20 == 0:
+ time.sleep(0.1)
+ if time.monotonic() > deadline:
+ return None
+ return self.get_reg(usart_dr)
+
+ def test_channel_stream(self) -> None:
+ ok, msg = check_prog('arm-none-eabi-gdb')
+ if not ok:
+ self.skipTest(msg)
+
+ usart_cr1 = 0x4001100C
+ # enable RX and TX
+ self.set_reg(usart_cr1, 0xC)
+
+ stream = self._emu.get_channel_stream('test_uart')
+ stream.write('test\n'.encode('ascii'))
+
+ self.assertEqual(self.poll_data(5), ord('t'))
+ self.assertEqual(self.poll_data(5), ord('e'))
+ self.assertEqual(self.poll_data(5), ord('s'))
+ self.assertEqual(self.poll_data(5), ord('t'))
+
+
+class TestRenodeChannelsPty(TestRenodeChannels):
+ """Tests for configurations using PTY channels."""
+
+ _config: Dict[str, Any] = {}
+ _config.update(json.loads(json.dumps(TestRenodeChannels._config)))
+ _config['renode']['channels'] = {'terminals': {'type': 'pty'}}
+
+ def setUp(self):
+ if sys.platform == 'win32':
+ self.skipTest('pty not supported on win32')
+ super().setUp()
+
+ def test_get_path(self) -> None:
+ self.assertTrue(os.path.exists(self._emu.get_channel_path('test_uart')))
+
+
+def main() -> None:
+ ok, msg = check_prog('renode')
+ if not ok:
+ print(f'skipping tests: {msg}')
+ sys.exit(0)
+
+ unittest.main()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_emu/qemu-lm3s6965evb.json b/pw_emu/qemu-lm3s6965evb.json
new file mode 100644
index 000000000..c1724e44c
--- /dev/null
+++ b/pw_emu/qemu-lm3s6965evb.json
@@ -0,0 +1,20 @@
+{
+ "targets": {
+ "qemu-lm3s6965evb": {
+ "gdb": [
+ "arm-none-eabi-gdb"
+ ],
+ "qemu": {
+ "executable": "qemu-system-arm",
+ "machine": "lm3s6965evb",
+ "channels": {
+ "chardevs": {
+ "serial0": {
+ "id": "serial0"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/pw_emu/qemu-netduinoplus2.json b/pw_emu/qemu-netduinoplus2.json
new file mode 100644
index 000000000..bc733867d
--- /dev/null
+++ b/pw_emu/qemu-netduinoplus2.json
@@ -0,0 +1,20 @@
+{
+ "targets": {
+ "qemu-netduinoplus2": {
+ "gdb": [
+ "arm-none-eabi-gdb"
+ ],
+ "qemu": {
+ "executable": "qemu-system-arm",
+ "machine": "netduinoplus2",
+ "channels": {
+ "chardevs": {
+ "serial0": {
+ "id": "serial0"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/pw_emu/qemu-stm32vldiscovery.json b/pw_emu/qemu-stm32vldiscovery.json
new file mode 100644
index 000000000..7e08f3696
--- /dev/null
+++ b/pw_emu/qemu-stm32vldiscovery.json
@@ -0,0 +1,20 @@
+{
+ "targets": {
+ "qemu-stm32vldiscovery": {
+ "gdb": [
+ "arm-none-eabi-gdb"
+ ],
+ "qemu": {
+ "executable": "qemu-system-arm",
+ "machine": "stm32vldiscovery",
+ "channels": {
+ "chardevs": {
+ "serial0": {
+ "id": "serial0"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/pw_emu/renode-stm32f4_discovery.json b/pw_emu/renode-stm32f4_discovery.json
new file mode 100644
index 000000000..0c84af386
--- /dev/null
+++ b/pw_emu/renode-stm32f4_discovery.json
@@ -0,0 +1,20 @@
+{
+ "targets": {
+ "renode-stm32f4_discovery": {
+ "gdb": [
+ "arm-none-eabi-gdb"
+ ],
+ "renode": {
+ "executable": "renode",
+ "machine": "platforms/boards/stm32f4_discovery-kit.repl",
+ "channels": {
+ "terminals": {
+ "serial0": {
+ "device-path": "sysbus.usart1"
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/pw_env_setup/BUILD.gn b/pw_env_setup/BUILD.gn
index 20e4bedd5..dcc953aac 100644
--- a/pw_env_setup/BUILD.gn
+++ b/pw_env_setup/BUILD.gn
@@ -22,7 +22,6 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
- inputs = [ "doc_resources/pw_env_setup_output.png" ]
sources = [ "docs.rst" ]
}
@@ -38,13 +37,19 @@ pw_python_group("core_pigweed_python_packages") {
"$dir_pw_cli/py",
"$dir_pw_compilation_testing/py",
"$dir_pw_console/py",
+ "$dir_pw_containers/py",
"$dir_pw_cpu_exception_cortex_m/py",
+ "$dir_pw_digital_io:protos.python",
"$dir_pw_docgen/py",
"$dir_pw_doctor/py",
+ "$dir_pw_emu/py",
"$dir_pw_env_setup/py",
"$dir_pw_hdlc/py",
+ "$dir_pw_i2c:protos.python",
"$dir_pw_ide/py",
"$dir_pw_log:protos.python",
+ "$dir_pw_log/py",
+ "$dir_pw_log_rpc/py",
"$dir_pw_log_tokenized/py",
"$dir_pw_metric/py",
"$dir_pw_module/py",
@@ -72,6 +77,17 @@ pw_python_group("core_pigweed_python_packages") {
"$dir_pw_watch/py",
]
}
+
+# Python packages for supporting specific targets. This includes flashing tools
+# and unit test runners.
+pw_python_group("pigweed_target_support_packages") {
+ python_deps = [
+ "$dir_pigweed/targets/lm3s6965evb_qemu/py",
+ "$dir_pigweed/targets/rp2040/py",
+ "$dir_pigweed/targets/stm32f429i_disc1/py",
+ ]
+}
+
pw_python_group("python") {
python_deps = [
":core_pigweed_python_packages",
@@ -82,11 +98,10 @@ pw_python_group("python") {
# Standalone scripts
# These targets are included here in order to ensure they are linted.
- "$dir_pw_hdlc/rpc_example:example_script",
- "$dir_pw_rpc/py:python_client_cpp_server_test",
+ "$dir_pw_emu/py:mock_emu",
+ "$dir_pw_emu/py:mock_emu_frontend",
"$dir_pw_third_party/fuchsia:generate_fuchsia_patch",
"$dir_pw_third_party/nanopb:generate_nanopb_proto",
- "$dir_pw_unit_test:test_group_metadata_test",
]
}
@@ -95,13 +110,18 @@ pw_python_venv("pigweed_build_venv") {
source_packages = [ ":core_pigweed_python_packages" ]
}
-# Python packages for supporting specific targets.
pw_python_pip_install("target_support_packages") {
- packages = [
- "$dir_pigweed/targets/lm3s6965evb_qemu/py",
- "$dir_pigweed/targets/stm32f429i_disc1/py",
- ]
- editable = true
+ packages = [ ":target_support_package_distribution" ]
+}
+
+pw_python_distribution("target_support_package_distribution") {
+ packages = [ ":pigweed_target_support_packages" ]
+ generate_setup_cfg = {
+ name = "pigweed_target_support"
+ version = "0.0.1"
+ include_default_pyproject_file = true
+ append_date_to_version = true
+ }
}
# This target is responsible for building the Python source uploaded to PyPI:
@@ -209,3 +229,7 @@ if (current_toolchain != default_toolchain) {
pw_test_group("tests") {
}
}
+
+pw_python_script("sample_project_action") {
+ sources = [ "sample_project_action/__init__.py" ]
+}
diff --git a/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl b/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
index c6d462557..6c613a393 100644
--- a/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
+++ b/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
@@ -48,9 +48,20 @@ def cipd_client_repository():
cipd_repository = repository_rule(
_cipd_repository_impl,
attrs = {
- "_cipd_client": attr.label(default = "@cipd_client//:cipd"),
- "path": attr.string(),
- "tag": attr.string(),
+ "_cipd_client": attr.label(
+ default = "@cipd_client//:cipd",
+ doc = "Location of the CIPD client binary (internal).",
+ ),
+ "build_file": attr.label(
+ allow_single_file = True,
+ doc = "A BUILD file to use instead of generating one internally.",
+ ),
+ "path": attr.string(
+ doc = "Path within CIPD where this repository lives.",
+ ),
+ "tag": attr.string(
+ doc = "Tag specifying which version of the repository to fetch.",
+ ),
},
doc = """
Downloads a singular CIPD dependency to the root of a remote repository.
@@ -80,9 +91,6 @@ _pigweed_deps = repository_rule(
"_pigweed_packages_json": attr.label(
default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/pigweed.json",
),
- "_python_packages_json": attr.label(
- default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/python.json",
- ),
"_upstream_testing_packages_json": attr.label(
default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/testing.json",
),
diff --git a/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl b/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
index 6bbc06585..0a0b45b9f 100644
--- a/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
+++ b/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
@@ -43,7 +43,7 @@ def platform_normalized(rctx):
else:
fail("Could not normalize os:", rctx.os.name)
-# TODO(b/234879770): Enable unused variable check.
+# TODO: b/234879770 - Enable unused variable check.
# buildifier: disable=unused-variable
def arch_normalized(rctx):
"""Normalizes the architecture string to match CIPDs naming system.
@@ -55,7 +55,7 @@ def arch_normalized(rctx):
str: Normalized architecture.
"""
- # TODO(b/234879770): Find a way to get host architecture information from a
+ # TODO: b/234879770 - Find a way to get host architecture information from a
# repository context.
return "amd64"
@@ -71,6 +71,11 @@ def get_client_cipd_version(rctx):
return rctx.read(rctx.attr._cipd_version_file).strip()
def _platform(rctx):
+ """Generates a normalized platform string from the host OS/architecture.
+
+ Args:
+ rctx: Repository context.
+ """
return "{}-{}".format(platform_normalized(rctx), arch_normalized(rctx))
def get_client_cipd_digest(rctx):
@@ -101,6 +106,11 @@ def get_client_cipd_digest(rctx):
fail("Could not find CIPD digest that matches this platform.")
def cipd_client_impl(rctx):
+ """Initializes the CIPD client repository.
+
+ Args:
+ rctx: Repository context.
+ """
platform = _platform(rctx)
path = "/client?platform={}&version={}".format(
platform,
@@ -115,7 +125,12 @@ def cipd_client_impl(rctx):
rctx.file("BUILD", "exports_files([\"cipd\"])")
def cipd_repository_base(rctx):
- cipd_path = rctx.path(rctx.attr._cipd_client).basename
+ """Populates the base contents of a CIPD repository.
+
+ Args:
+ rctx: Repository context.
+ """
+ cipd_path = rctx.path(rctx.attr._cipd_client)
ensure_path = rctx.name + ".ensure"
rctx.template(
ensure_path,
@@ -125,19 +140,30 @@ def cipd_repository_base(rctx):
"%{tag}": rctx.attr.tag,
},
)
- rctx.execute([cipd_path, "ensure", "-root", ".", "-ensure-file", ensure_path])
+ result = rctx.execute([cipd_path, "ensure", "-root", ".", "-ensure-file", ensure_path])
+
+ if result.return_code != 0:
+ fail("Failed to fetch CIPD repsoitory `{}`:\n{}".format(rctx.name, result.stderr))
def cipd_repository_impl(rctx):
+ """Generates an external repository from a CIPD package.
+
+ Args:
+ rctx: Repository context.
+ """
cipd_repository_base(rctx)
- rctx.file("BUILD", """
-exports_files(glob([\"**/*\"]))
+ if rctx.attr.build_file:
+ rctx.file("BUILD", rctx.read(rctx.attr.build_file))
+ else:
+ rctx.file("BUILD", """
+exports_files(glob(["**"]))
filegroup(
name = "all",
- srcs = glob(["**/*"]),
+ srcs = glob(["**"]),
visibility = ["//visibility:public"],
)
-""")
+ """)
def _cipd_path_to_repository_name(path, platform):
""" Converts a cipd path to a repository name
@@ -176,8 +202,6 @@ def cipd_deps_impl(repository_ctx):
""" Generates a CIPD dependencies file """
pigweed_deps = json.decode(
repository_ctx.read(repository_ctx.attr._pigweed_packages_json),
- )["packages"] + json.decode(
- repository_ctx.read(repository_ctx.attr._python_packages_json),
)["packages"]
repository_ctx.file("BUILD", "exports_files(glob([\"**/*\"]))\n")
diff --git a/pw_env_setup/config.json b/pw_env_setup/config.json
deleted file mode 100644
index 327e2b78e..000000000
--- a/pw_env_setup/config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "root_variable": "PW_ROOT",
- "cipd_package_files": [
- "pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json"
- ],
- "virtualenv": {
- "gn_root": ".",
- "gn_targets": [
- ":python.install"
- ],
- "requirements": [
- "pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt"
- ],
- "constraints": [
- "pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list"
- ]
- },
- "pw_packages": [],
- "rosetta": "allow",
- "gni_file": "build_overrides/pigweed_environment.gni"
-}
diff --git a/pw_env_setup/doc_resources/pw_env_setup_output.png b/pw_env_setup/doc_resources/pw_env_setup_output.png
deleted file mode 100644
index 204ed1856..000000000
--- a/pw_env_setup/doc_resources/pw_env_setup_output.png
+++ /dev/null
Binary files differ
diff --git a/pw_env_setup/docs.rst b/pw_env_setup/docs.rst
index 85cd7cb95..84300adc6 100644
--- a/pw_env_setup/docs.rst
+++ b/pw_env_setup/docs.rst
@@ -102,7 +102,7 @@ Bazel Usage
It is possible to pull in a CIPD dependency into Bazel using WORKSPACE rules
rather than using `bootstrap.sh`. e.g.
-.. code:: python
+.. code-block:: python
# WORKSPACE
@@ -122,7 +122,7 @@ This will make the entire set of Pigweeds remote repositories available to your
project. Though these repositories will only be donwloaded if you use them. To
get a full list of the remote repositories that this configures, run:
-.. code:: sh
+.. code-block:: sh
bazel query //external:all | grep cipd_
@@ -133,7 +133,7 @@ either directely (`@cipd_<dep>//:<file>`) or from 'all' filegroup
From here it is possible to get access to the Bloaty binaries using the
following command. For example;
-.. code:: sh
+.. code-block:: sh
bazel run @cipd_pigweed_third_party_bloaty_embedded_linux_amd64//:bloaty \
-- --help
@@ -204,20 +204,25 @@ process. To check for this add the following.
Downstream Projects Using Different Packages
********************************************
Projects depending on Pigweed but using additional or different packages should
-copy the Pigweed `sample project`'s ``bootstrap.sh`` and ``config.json`` and
+copy the Pigweed `sample project`'s ``bootstrap.sh`` and ``pigweed.json`` and
update the call to ``pw_bootstrap``. Search for "downstream" for other places
that may require changes, like setting the ``PW_ROOT`` and ``PW_PROJECT_ROOT``
-environment variables. Explanations of parts of ``config.json`` are described
+environment variables. Explanations of parts of ``pigweed.json`` are described
here.
.. _sample project: https://pigweed.googlesource.com/pigweed/sample_project/+/HEAD
-``root_variable``
+``pw.pw_env_setup.root_variable``
Variable used to point to the root of the source tree. Optional, can always
use ``PW_PROJECT_ROOT`` instead. (That variable will be set regardless of
whether this is provided.)
-``cipd_package_files``
+``pw.pw_env_setup.relative_pigweed_root``
+ Location of the Pigweed submodule within the source tree. Optional—environment
+ setup will work correctly without this. If present, will confirm that it's
+ correct. May be used by other tooling.
+
+``pw.pw_env_setup.cipd_package_files``
CIPD package file. JSON file consisting of a list of additional CIPD package
files to import and a list of dictionaries with "path", "platforms", "subdir",
"tags", and "version_file" keys. Both top-level lists are optional. An
@@ -250,60 +255,138 @@ here.
]
}
-``virtualenv.gn_args``
+``pw.pw_env_setup.project_actions``
+ A list of plugins to load and run after CIPD setup, but prior to virtualenv
+ setup, for e.g. downloading project-specific tools or artifacts needed by
+ later steps. Particularly useful for downstream projects with limited CIPD
+ access.
+
+ A plugin is specified as a dictionary with two keys: "import_path" and
+ "module_name"
+
+ The specified module must provide a "run_actions" method which takes a single
+ argument, "env_vars", which is a pw_env_setup.Environment instance.
+
+ NB: This feature is not supported when using a python2.7 system python.
+
+ Sample plugin and pigweed.json blob:
+
+.. code-block:: python
+
+ """Sample pw_env_setup project action plugin.
+
+ A sample/starter project action plugin template for pw_env_setup.
+ """
+ def run_action(**kwargs):
+ """Sample project action."""
+ if "env" not in kwargs:
+ raise ValueError(f"Missing required kwarg 'env', got %{kwargs}")
+
+ kwargs["env"].prepend("PATH", "PATH_TO_NEW_TOOLS")
+ raise NotImplementedError("Sample project action running!")
+
+.. code-block:: json
+
+ "project_actions" : [
+ {
+ "import_path": "pw_env_setup",
+ "module_name": "sample_project_action"
+ }
+ ],
+
+``pw.pw_env_setup.virtualenv.gn_args``
Any necessary GN args to be used when installing Python packages.
-``virtualenv.gn_targets``
+``pw.pw_env_setup.virtualenv.gn_targets``
Target for installing Python packages. Downstream projects will need to
create targets to install their packages or only use Pigweed Python packages.
-``virtualenv.gn_root``
+``pw.pw_env_setup.virtualenv.gn_root``
The root directory of your GN build tree, relative to ``PW_PROJECT_ROOT``.
This is the directory your project's ``.gn`` file is located in. If you're
only installing Pigweed Python packages, use the location of the Pigweed
submodule.
-``virtualenv.requirements``
+``pw.pw_env_setup.virtualenv.requirements``
A list of Python Pip requirements files for installing into the Pigweed
virtualenv. Each file will be passed as additional ``--requirement`` argument
to a single ```pip install`` at the beginning of bootstrap's ``Python
environment`` setup stage. See the `Requirements Files documentation`_ for
details on what can be specified using requirements files.
-``virtualenv.constraints``
+``pw.pw_env_setup.virtualenv.constraints``
A list of Python Pip constraints files. These constraints will be passed to
every ``pip`` invocation as an additional ``--constraint`` argument during
bootstrap. virtualenv. See the `Constraints Files documentation`_ for details
on formatting.
-``virtualenv.system_packages``
+``pw.pw_env_setup.virtualenv.system_packages``
A boolean value that can be used the give the Python virtual environment
access to the system site packages. Defaults to ``false``.
-``optional_submodules``
+``pw.pw_env_setup.virtualenv.pip_install_offline``
+ A boolean value that adds ``--no-index`` to all ``pip install`` commands that
+ are part of bootstrap. This forces pip to not reach out to the internet
+ (usually `pypi.org <https://pypi.org/>`_) to download packages. Using this
+ option requires setting
+ ``pw.pw_env_setup.virtualenv.pip_install_find_links``. Defaults to
+ ``false``.
+
+ .. seealso::
+ The Python GN guide for offline pip installation:
+ :ref:`docs-python-build-installing-offline`
+
+``pw.pw_env_setup.virtualenv.pip_install_find_links``
+ List of paths to folders containing Python wheels (``*.whl``) or source tar
+ files (``*.tar.gz``). Pip will check each of these directories when looking
+ for potential install candidates. Each path will be passed to all ``pip
+ install`` commands as ``--find-links PATH``.
+
+ .. tip::
+ Environment variables may be used in these paths. For example:
+
+ .. code-block:: json
+
+ "virtualenv": {
+ "pip_install_find_links": [
+ "${PW_PROJECT_ROOT}/pip_cache"
+ ]
+ }
+
+``pw.pw_env_setup.virtualenv.pip_install_require_hashes``
+ Adds ``--require-hashes`` This option enforces hash checking on Python
+ package files. Defaults to ``false``.
+
+``pw.pw_env_setup.virtualenv.pip_install_disable_cache``
+ A boolean value that adds ``--no-cache-dir`` to all ``pip install`` commands
+ that are part of bootstrap. This forces pip to ignore any previously cached
+ Python packages. On most systems this is located in
+ ``~/.cache/pip/``. Defaults to ``false``.
+
+``pw.pw_env_setup.optional_submodules``
By default environment setup will check that all submodules are present in
the checkout. Any submodules in this list are excluded from that check.
-``required_submodules``
+``pw.pw_env_setup.required_submodules``
If this is specified instead of ``optional_submodules`` bootstrap will only
complain if one of the required submodules is not present. Combining this
with ``optional_submodules`` is not supported.
-``pw_packages``
+``pw.pw_env_setup.pw_packages``
A list of packages to install using :ref:`pw_package <module-pw_package>`
after the rest of bootstrap completes.
-``gni_file``
+``pw.pw_env_setup.gni_file``
Location to write a ``.gni`` file containing paths to many things within the
environment directory. Defaults to
``build_overrides/pigweed_environment.gni``.
-``json_file``
+``pw.pw_env_setup.json_file``
Location to write a ``.json`` file containing step-by-step modifications to
the environment, for reading by tools that don't inherit an environment from
a sourced ``bootstrap.sh``.
-``rosetta``
+``pw.pw_env_setup.rosetta``
Whether to use Rosetta to use amd64 packages on arm64 Macs. Accepted values
are ``never``, ``allow``, and ``force``. For now, ``allow`` means ``force``.
At some point in the future ``allow`` will be changed to mean ``never``.
@@ -313,33 +396,37 @@ An example of a config file is below.
.. code-block:: json
{
- "root_variable": "EXAMPLE_ROOT",
- "cipd_package_files": [
- "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json",
- "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json"
- "tools/myprojectname.json"
- ],
- "virtualenv": {
- "gn_root": ".",
- "gn_targets": [
- ":python.install",
- ],
- "system_packages": false
- },
- "pw_packages": [],
- "optional_submodules": [
- "optional/submodule/one",
- "optional/submodule/two"
- ],
- "gni_file": "tools/environment.gni",
- "json_file": "tools/environment.json",
- "rosetta": "allow"
+ "pw": {
+ "pw_env_setup": {
+ "root_variable": "EXAMPLE_ROOT",
+ "cipd_package_files": [
+ "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json",
+ "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json"
+ "tools/myprojectname.json"
+ ],
+ "virtualenv": {
+ "gn_root": ".",
+ "gn_targets": [
+ ":python.install",
+ ],
+ "system_packages": false
+ },
+ "pw_packages": [],
+ "optional_submodules": [
+ "optional/submodule/one",
+ "optional/submodule/two"
+ ],
+ "gni_file": "tools/environment.gni",
+ "json_file": "tools/environment.json",
+ "rosetta": "allow"
+ }
+ }
}
Only the packages necessary for almost all projects based on Pigweed are
-included in the ``pigweed.json`` file. A number of other files are present in
-that directory for projects that need more than the minimum. Internal-Google
-projects using LUCI should at least include ``luci.json``.
+included in the ``cipd_setup/pigweed.json`` file. A number of other files are
+present in that directory for projects that need more than the minimum.
+Internal-Google projects using LUCI should at least include ``luci.json``.
In case the CIPD packages need to be referenced from other scripts, variables
like ``PW_${BASENAME}_CIPD_INSTALL_DIR`` point to the CIPD install directories,
@@ -367,20 +454,66 @@ last topologically takes priority. For example, with the file contents below,
``d.json``'s entries will appear in ``PATH`` before ``c.json``'s, which will
appear before ``b.json``'s, which will appear before ``a.json``'s.
-``config.json``
- ``{"cipd_package_files": ["a.json", "b.json", "d.json"], ...}``
+.. code-block:: json
+ :caption: :octicon:`file;1em` pigweed.json
-``a.json``
- ``{"package_files": [...]}``
+ {
+ "pw": {
+ "pw_env_setup": {
+ "cipd_package_files": [
+ "a.json",
+ "b.json",
+ "d.json"
+ ]
+ }
+ }
+ }
-``b.json``
- ``{"included_files": ["c.json"], "package_files": [...]}``
+.. code-block:: json
+ :caption: :octicon:`file;1em` a.json
+
+ {
+ "package_files": [
+ // ...
+ ]
+ }
+
+.. code-block:: json
+ :caption: :octicon:`file;1em` b.json
+
+ {
+ "included_files": ["c.json"],
+ "package_files": [
+ // ...
+ ]
+ }
+
+.. code-block:: json
+ :caption: :octicon:`file;1em` c.json
+
+ {
+ "package_files": [
+ // ...
+ ]
+ }
+
+.. code-block:: json
+ :caption: :octicon:`file;1em` d.json
+
+ {
+ "package_files": [
+ // ...
+ ]
+ }
-``c.json``
- ``{"package_files": [...]}``
+.. code-block::
+ :caption: Effective File Loading Order
-``d.json``
- ``{"package_files": [...]}``
+ pigweed.json
+ a.json
+ b.json
+ c.json
+ d.json
Pinning Python Packages
***********************
@@ -415,6 +548,9 @@ never need to set these.
Location of CIPD cache dir. Read by CIPD, but if unset will be defaulted to
``$HOME/.cipd-cache-dir``.
+``PW_NO_CIPD_CACHE_DIR``
+ Disables the CIPD cache.
+
``PW_ACTIVATE_SKIP_CHECKS``
If set, skip running ``pw doctor`` at end of bootstrap/activate. Intended to
be used by automated tools but not interactively.
@@ -535,10 +671,10 @@ the GNI file specified in the environment config file.
.. code-block::
declare_args() {
- pw_env_setup_CIPD_PIGWEED = "<environment-root>/cipd/packages/pigweed"
pw_env_setup_CIPD_LUCI = "<environment-root>/cipd/packages/luci"
- pw_env_setup_VIRTUAL_ENV = "<environment-root>/pigweed-venv"
+ pw_env_setup_CIPD_PIGWEED = "<environment-root>/cipd/packages/pigweed"
pw_env_setup_PACKAGE_ROOT = "<environment-root>/packages"
+ pw_env_setup_VIRTUAL_ENV = "<environment-root>/pigweed-venv"
}
It's straightforward to use these variables.
@@ -560,9 +696,115 @@ the future when running ``activate.sh`` instead of ``bootstrap.sh``. In the
future these could be extended to C shell and PowerShell. A logical mapping of
high-level commands to system-specific initialization files is shown below.
-.. image:: doc_resources/pw_env_setup_output.png
- :alt: Mapping of high-level commands to system-specific commands.
- :align: left
+.. grid:: 1
+ :padding: 0
+
+ .. grid-item-card::
+ :columns: 12
+ :class-header: font-monospace
+
+ SET $PW_ROOT /home/$USER/pigweed
+ ^^^
+
+ .. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item:: **Windows**
+
+ .. grid-item:: **Linux & Mac (sh-compatible shells)**
+
+ .. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item::
+
+ .. code-block:: dosbatch
+
+ set PW_ROOT /home/%USER%/pigweed
+
+ .. grid-item::
+
+ .. code-block:: shell
+
+ PW_ROOT="/home/$USER/pigweed"
+ export PW_ROOT
+
+.. grid:: 1
+ :padding: 0
+
+ .. grid-item-card::
+ :columns: 12
+ :class-header: font-monospace
+
+ PREPEND $PATH $PW_ROOT/.env/bin
+ ^^^
+ .. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item:: **Windows**
+
+ .. grid-item:: **Linux & Mac (sh-compatible shells)**
+
+ .. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item::
+
+ .. code-block:: dosbatch
+
+ set PATH=%PW_ROOT%/.env/bin;%PATH%
+
+ .. grid-item::
+
+ .. code-block:: shell
+
+ PATH="$(\
+ echo "$PATH" | \
+ sed "s|:$PW_ROOT/.env/bin:|:|g;" | \
+ sed "s|^$PW_ROOT/.env/bin:||g;" | \
+ sed "s|:$PW_ROOT/.env/bin$||g;")"
+ PATH="$PW_ROOT/.env/bin;$PATH"
+ export PATH
+
+.. grid:: 1
+ :padding: 0
+
+ .. grid-item-card::
+ :columns: 12
+ :class-header: font-monospace
+
+ ECHO "Setup Complete!"
+ ^^^
+
+ .. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item:: **Windows**
+
+ .. grid-item:: **Linux & Mac (sh-compatible shells)**
+
+
+ .. grid:: 2
+ :margin: 0
+ :padding: 0
+
+ .. grid-item::
+
+ .. code-block:: dosbatch
+
+ echo Setup Complete!
+
+ .. grid-item::
+
+ .. code-block:: shell
+
+ echo "Setup Complete!"
+
.. _Requirements Files documentation: https://pip.pypa.io/en/stable/user_guide/#requirements-files
.. _Constraints Files documentation: https://pip.pypa.io/en/stable/user_guide/#constraints-files
diff --git a/pw_env_setup/py/BUILD.gn b/pw_env_setup/py/BUILD.gn
index 7b68fcbda..b5e61631e 100644
--- a/pw_env_setup/py/BUILD.gn
+++ b/pw_env_setup/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_env_setup/__init__.py",
@@ -31,6 +30,8 @@ pw_python_package("py") {
"pw_env_setup/cipd_setup/wrapper.py",
"pw_env_setup/colors.py",
"pw_env_setup/config_file.py",
+ "pw_env_setup/entry_points/__init__.py",
+ "pw_env_setup/entry_points/arm_gdb.py",
"pw_env_setup/env_setup.py",
"pw_env_setup/environment.py",
"pw_env_setup/gni_visitor.py",
@@ -44,10 +45,38 @@ pw_python_package("py") {
"pw_env_setup/windows_env_start.py",
]
tests = [
+ "cipd_setup_update_test.py",
"environment_test.py",
- "python_packages_test.py",
"json_visitor_test.py",
+ "python_packages_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
+ inputs = [
+ "pw_env_setup/cipd_setup/.cipd_version",
+ "pw_env_setup/cipd_setup/.cipd_version.digests",
+ "pw_env_setup/cipd_setup/arm.json",
+ "pw_env_setup/cipd_setup/bazel.json",
+ "pw_env_setup/cipd_setup/black.json",
+ "pw_env_setup/cipd_setup/buildifier.json",
+ "pw_env_setup/cipd_setup/cmake.json",
+ "pw_env_setup/cipd_setup/compatibility.json",
+ "pw_env_setup/cipd_setup/default.json",
+ "pw_env_setup/cipd_setup/doxygen.json",
+ "pw_env_setup/cipd_setup/go.json",
+ "pw_env_setup/cipd_setup/host_tools.json",
+ "pw_env_setup/cipd_setup/kythe.json",
+ "pw_env_setup/cipd_setup/luci.json",
+ "pw_env_setup/cipd_setup/msrv_python.json",
+ "pw_env_setup/cipd_setup/pigweed.json",
+ "pw_env_setup/cipd_setup/python.json",
+ "pw_env_setup/cipd_setup/rbe.json",
+ "pw_env_setup/cipd_setup/riscv.json",
+ "pw_env_setup/cipd_setup/testing.json",
+ "pw_env_setup/cipd_setup/upstream.json",
+ "pw_env_setup/cipd_setup/web.json",
+ "pw_env_setup/virtualenv_setup/constraint.list",
+ "pw_env_setup/virtualenv_setup/constraint_hashes_linux.list",
+ "pw_env_setup/virtualenv_setup/python_base_requirements.txt",
+ ]
}
diff --git a/pw_env_setup/py/cipd_setup_update_test.py b/pw_env_setup/py/cipd_setup_update_test.py
new file mode 100644
index 000000000..76a724660
--- /dev/null
+++ b/pw_env_setup/py/cipd_setup_update_test.py
@@ -0,0 +1,186 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for cipd_setup.update logic."""
+
+import importlib.resources
+import operator
+from pathlib import Path
+import unittest
+
+from parameterized import parameterized # type: ignore
+
+from pw_env_setup.cipd_setup.update import (
+ all_package_files,
+ deduplicate_packages,
+)
+
+
+class TestCipdSetupUpdate(unittest.TestCase):
+ """Tests for cipd_setup.update logic."""
+
+ def setUp(self):
+ self.maxDiff = None # pylint: disable=invalid-name
+
+ @parameterized.expand(
+ [
+ (
+ 'overriden Python',
+ [
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@12.2.mpacbti-rel1.1'],
+ 'subdir': 'arm',
+ },
+ {
+ 'path': 'infra/3pp/tools/cpython3/${platform}',
+ 'tags': ['version:2@3.8.10.chromium.24'],
+ 'subdir': 'arm/python',
+ 'original_subdir': 'python',
+ },
+ # Python 3.11.3
+ {
+ 'path': 'infra/3pp/tools/cpython3/${platform}',
+ 'tags': ['version:2@3.11.3.chromium.29'],
+ 'subdir': 'python',
+ },
+ # Duplicate Python, different version 3.11.4
+ # This should take precedence.
+ {
+ 'path': 'infra/3pp/tools/cpython3/${platform}',
+ 'tags': ['version:2@3.11.4.chromium.29'],
+ 'subdir': 'python',
+ },
+ ],
+ [
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@12.2.mpacbti-rel1.1'],
+ 'subdir': 'arm',
+ },
+ {
+ 'path': 'infra/3pp/tools/cpython3/${platform}',
+ 'tags': ['version:2@3.8.10.chromium.24'],
+ 'subdir': 'arm/python',
+ 'original_subdir': 'python',
+ },
+ {
+ 'path': 'infra/3pp/tools/cpython3/${platform}',
+ 'tags': ['version:2@3.11.4.chromium.29'],
+ 'subdir': 'python',
+ },
+ ],
+ ),
+ (
+ 'duplicate package in a different subdir',
+ [
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@12.2.mpacbti-rel1.1'],
+ 'subdir': 'arm',
+ },
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@10.3-2021.10.1'],
+ 'subdir': 'another_arm',
+ },
+ ],
+ [
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@10.3-2021.10.1'],
+ 'subdir': 'another_arm',
+ },
+ ],
+ ),
+ (
+ 'duplicate package in the same subdir',
+ [
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@12.2.mpacbti-rel1.1'],
+ 'subdir': 'arm',
+ },
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@10.3-2021.10.1'],
+ 'subdir': 'arm',
+ },
+ ],
+ [
+ # The second older version takes precedence
+ {
+ 'path': 'fuchsia/third_party/armgcc/${platform}',
+ 'tags': ['version:2@10.3-2021.10.1'],
+ 'subdir': 'arm',
+ },
+ ],
+ ),
+ ]
+ )
+ def test_deduplicate_packages(
+ self,
+ _name,
+ packages,
+ expected_packages,
+ ) -> None:
+ """Test package deduplication logic."""
+ pkgs = sorted(
+ deduplicate_packages(packages),
+ key=operator.itemgetter('path'),
+ )
+ expected_pkgs = sorted(
+ expected_packages,
+ key=operator.itemgetter('path'),
+ )
+ self.assertSequenceEqual(expected_pkgs, pkgs)
+
+ def test_all_package_files(self) -> None:
+ """Test that CIPD files are loaded in the correct order."""
+
+ upstream_load_order = [
+ Path('upstream.json'),
+ Path('bazel.json'),
+ Path('buildifier.json'),
+ Path('cmake.json'),
+ Path('coverage.json'),
+ Path('default.json'),
+ Path('arm.json'),
+ Path('pigweed.json'),
+ Path('clang.json'),
+ Path('python.json'),
+ Path('python311.json'),
+ Path('doxygen.json'),
+ Path('go.json'),
+ Path('host_tools.json'),
+ Path('kythe.json'),
+ Path('luci.json'),
+ Path('msrv_python.json'),
+ Path('python38.json'),
+ Path('rbe.json'),
+ Path('testing.json'),
+ Path('web.json'),
+ ]
+
+ with importlib.resources.path(
+ 'pw_env_setup.cipd_setup', 'upstream.json'
+ ) as upstream_json:
+ all_files = all_package_files(None, [upstream_json])
+ all_files_relative = [
+ Path(f).relative_to(upstream_json.parent) for f in all_files
+ ]
+ self.assertEqual(upstream_load_order, all_files_relative)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_env_setup/py/pw_env_setup/apply_visitor.py b/pw_env_setup/py/pw_env_setup/apply_visitor.py
index c3449e413..f821ec7d9 100644
--- a/pw_env_setup/py/pw_env_setup/apply_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/apply_visitor.py
@@ -30,6 +30,13 @@ class ApplyVisitor(object): # pylint: disable=useless-object-inheritance
self._unapply_steps = None
def apply(self, env, environ):
+ """Apply the given environment to the current process.
+
+ Args:
+ env (environment.Environment): Environment variables to use.
+ environ (dict): Where to set variables. In most cases this should be
+ os.environ.
+ """
self._unapply_steps = []
try:
self._environ = environ
diff --git a/pw_env_setup/py/pw_env_setup/batch_visitor.py b/pw_env_setup/py/pw_env_setup/batch_visitor.py
index 4ca43ba1e..666240d6e 100644
--- a/pw_env_setup/py/pw_env_setup/batch_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/batch_visitor.py
@@ -31,6 +31,12 @@ class BatchVisitor(object): # pylint: disable=useless-object-inheritance
self._pathsep = pathsep
def serialize(self, env, outs):
+ """Write a batch file based on the given environment.
+
+ Args:
+ env (environment.Environment): Environment variables to use.
+ outs (file): Batch file to write.
+ """
try:
self._replacements = tuple(
(key, env.get(key) if value is None else value)
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version
index 8c208ba63..f85993dff 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version
@@ -1 +1 @@
-git_revision:fc9d7a3dff50d001c7c7dcf9f5f312be5d2d2ab2
+git_revision:0f08b927516afe56fec88b3472088deddc5b6a89
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests
index 83cff0d34..658aaf7fd 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests
@@ -1,25 +1,34 @@
# This file was generated by
#
# cipd selfupdate-roll -version-file .cipd_version \
-# -version git_revision:fc9d7a3dff50d001c7c7dcf9f5f312be5d2d2ab2
+# -version git_revision:0f08b927516afe56fec88b3472088deddc5b6a89
#
# Do not modify manually. All changes will be overwritten.
# Use 'cipd selfupdate-roll ...' to modify.
-aix-ppc64 sha256 85a888acea9b4ac164f5cd5372e49817b8d370bd5c8231ac1bb77f2b8ed01e69
-linux-386 sha256 3201e8c5ad57b010a5fbd89783a56fbddd4dc75a96d3117017f3c15c16139736
-linux-amd64 sha256 f907698b1717f54834adc921eb080021efb76b3345daf470d10b53cbe7b37b6e
-linux-arm64 sha256 270881ea4f432d2503f828222f9e2cc63b03f6a7716b2da0922a70ce84c83ff7
-linux-armv6l sha256 ae222dfee5fdd224f1c55cec215a6fde3839adcc342bc82bfcea238e5d399a89
-linux-mips64 sha256 235bf307c9e1b35127249289c82bbc52a713d8cbc931f967963ea4b9d56b3c17
-linux-mips64le sha256 c40ef945f04023fca51cbb9aaf478585bdcc87aad74f2373896070d6b7d0f9e2
-linux-mipsle sha256 647820d9efc212cf640ad1ddd7fb9436ec6cefee10906b36ec08436df9112d0f
-linux-ppc64 sha256 364e433abd4b4e313a0d14e888430613a6fb58903bc5456782ecadb5583d7f8c
-linux-ppc64le sha256 3a26b608c6a93141c9bd2dc10ffc97214ab5b79eb67452f2235c3c903f3f82cb
-linux-riscv64 sha256 35e4fc4c5f1bafdd9028e082c1cc8d565cb0c407d1574b2ff29bd491bd7ba780
-linux-s390x sha256 80918c0fc689ac9efa22cc4929a370114b133cb97ba79e42ea4332418db404da
-mac-amd64 sha256 5ae46728fb897d28e9688272a517dee4e554c999134fdd5d0db66f6661170a1b
-mac-arm64 sha256 723ee4f5fd9d39f3e2f27d9fe1e2354da45c28ad861726937eabff7325659290
-windows-386 sha256 d7553e8a758339343d583929ff9ca6ae6d4b0e81928803abb28193a954243164
-windows-amd64 sha256 3ef438c1a2a5fc86b52043519786331b0a79f9d226f3fa5b2294299857f04766
-windows-arm64 sha256 744e9ddda0f5560a0e7e34108969054c1d3626305a5d51b507e70b36f3c2cbb9
+aix-ppc64 sha256 8ffe6d0beffcc6889e84e11ca047d5b140421a764ca7d3fdfb66b1fb0862fc05
+freebsd-amd64 sha256 5e678bbde4da25328d7160131b7abdcee33d8b38cd50bcb38a5db68ce8ec8f54
+freebsd-arm64 sha256 7d872c2ef4f345304392cc986f741df4a35acc4f273f329fc5554c31d3b3711e
+illumos-amd64 sha256 3160a4d72603d8e7bf053c4def90e8fcf5d3ecce3bfaedaf8f9d42b66fa1cc85
+linux-386 sha256 35efb66f86e42fcbaf6963b56cd0acbd930f8ac1b4b328b6a40abe16e962df04
+linux-amd64 sha256 5df8080af532ca5b446e5b49999cf15e57b701b944995be5913c088af795a51b
+linux-arm64 sha256 0b722b3cc478ccd471a173d620cdf7f5d7c03504f2fbe8fa2649e7d02d610e8d
+linux-armv6l sha256 fdb7ebebb78feca8b96b254bbcea75129e5c69037c07f85e240c306a6aafe126
+linux-mips sha256 8b691d8de7449662ab7f7834eac4e78dfd6283c75df65f4762da1f8a6b58b69f
+linux-mips64 sha256 7a42fc00100fdf0f72fda53e4dfba904baf2e8f7232660efa49c027d03bfcfac
+linux-mips64le sha256 015fb514ea326a306955ce561061f2f82b31c337454b68255efb58f3f3428293
+linux-mipsle sha256 95181fed5eac512afcd5ef617911b27a0e9b0fc3ac3af7f65cdc4b6086ea719c
+linux-ppc64 sha256 1b88edfe135bce16dd22c4b609a00f1491b23d6b91da4c7b9053572577ce5a15
+linux-ppc64le sha256 7e45ee956ac243b79acaa63fe9a85bcd23831e68432b53c9599ca2a5f6ec93ec
+linux-riscv64 sha256 5409d012d5c184878d4aef27cd293e36c4651ef249992f79233357117aa73f18
+linux-s390x sha256 862eb6e6e4bf1e6d4aa5447420c8a0126eddf31593705fb37a800bf130a9c0a4
+mac-amd64 sha256 fa76505e7017b04ee87b98d52e0714429502145a9f9b9be4c9ee963057380d22
+mac-arm64 sha256 fcfca8d51b62f82bb6a1d51dffc6be9ec8d16ed568c206d149256191190b5b2a
+netbsd-amd64 sha256 8cf814eed27c7bab079b8ee5a94d4d2cc39588d1706756095e89aa1866d191fb
+netbsd-arm64 sha256 2ac6d65599e2a09a65ebf00282babffc4ec3a9d87a77318860754c21e0298c52
+openbsd-amd64 sha256 81c38db1ef03ba8f607926e275971cec4278aed497ee52efae126e9d50ed5694
+openbsd-arm64 sha256 57af9a8c4628478f4b6d1dc497420a000b04f8836d3929ae50d3ff2fa93a03ec
+solaris-amd64 sha256 458d45236f5a671f2425636cbd98391063d21e82ef80ec3c4101ad6b6c583ce3
+windows-386 sha256 aef697c4e94e01593e9b6bd4a25b6ebfa148a733eac50a90d21313c012d7a1b8
+windows-amd64 sha256 0a241893653b1afd203b35c23a417ec01a4168837f3ab952ea417e5351593f99
+windows-arm64 sha256 9574fd143b602a647aa8ebf2db138d7f69fc3aa4a6eedea7f68309659e963693
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/OWNERS b/pw_env_setup/py/pw_env_setup/cipd_setup/OWNERS
new file mode 100644
index 000000000..134f1b8ab
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/OWNERS
@@ -0,0 +1 @@
+per-file *.json=pigweed-roller@pigweed-service-accounts.iam.gserviceaccount.com
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json b/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json
index 31cac6493..678300a18 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json
@@ -9,9 +9,23 @@
"windows-amd64"
],
"tags": [
- "version:2@10.3-2021.10.1"
+ "version:2@12.2.mpacbti-rel1.1"
],
"version_file": ".versions/gcc-arm-none-eabi.version"
+ },
+ {
+ "path": "infra/3pp/tools/cpython3/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@3.8.10.chromium.24"
+ ],
+ "subdir": "python"
}
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json b/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json
index f555cca25..8b8bd5ce6 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json
@@ -4,16 +4,29 @@
],
"packages": [
{
- "path": "fuchsia/third_party/bazel/${platform}",
+ "path": "fuchsia/third_party/bazel-prerelease/${platform}",
"platforms": [
"linux-amd64",
"mac-amd64",
+ "mac-arm64",
"windows-amd64"
],
"tags": [
- "version:2@6.0.0.5"
+ "version:2@7.0.0-pre.20231011.2.6"
],
"version_file": ".versions/bazel.cipd_version"
+ },
+ {
+ "path": "flutter/java/openjdk/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:17"
+ ]
}
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json b/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json
index d21126d82..cfe27667f 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "version:2@6.0.1"
+ "version:2@v6.3.3"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/clang.json b/pw_env_setup/py/pw_env_setup/cipd_setup/clang.json
new file mode 100644
index 000000000..7bd54374f
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/clang.json
@@ -0,0 +1,3 @@
+{
+ "packages": []
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/clang_next.json b/pw_env_setup/py/pw_env_setup/cipd_setup/clang_next.json
new file mode 100644
index 000000000..a69189a41
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/clang_next.json
@@ -0,0 +1,18 @@
+{
+ "packages": [
+ {
+ "path": "fuchsia/third_party/clang/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "git_revision:00396e6a1a0b79fda008cb4e86b616d7952b33c8"
+ ],
+ "version_file": ".versions/clang.cipd_version"
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json b/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
index e200afe69..dfac1f243 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
@@ -13,7 +13,7 @@
"windows-amd64"
],
"tags": [
- "version:2@3.26.0.chromium.7"
+ "version:2@3.27.7.chromium.8"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json b/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json
index ddbcdac15..7ba5dc48f 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "version:2@3.8.10.chromium.24"
+ "version:2@3.11.3.chromium.29"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/coverage.json b/pw_env_setup/py/pw_env_setup/cipd_setup/coverage.json
new file mode 100644
index 000000000..ac0998f0a
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/coverage.json
@@ -0,0 +1,22 @@
+{
+ "packages": [
+ {
+ "path": "fuchsia/coverage/absolute_uploader/${platform}",
+ "platforms": [
+ "linux-amd64"
+ ],
+ "tags": [
+ "g3-revision:fuchsia.infra.coverage.upload_clients_20231013_RC00"
+ ]
+ },
+ {
+ "path": "fuchsia/coverage/incremental_uploader/${platform}",
+ "platforms": [
+ "linux-amd64"
+ ],
+ "tags": [
+ "g3-revision:fuchsia.infra.coverage.upload_clients_20231013_RC00"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/default.json b/pw_env_setup/py/pw_env_setup/cipd_setup/default.json
index 292544092..bddf6155b 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/default.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/default.json
@@ -3,6 +3,7 @@
"arm.json",
"buildifier.json",
"pigweed.json",
+ "clang.json",
"python.json"
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/go.json b/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
index 0feb0dbaa..b4d4bdd3c 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "version:2@1.20.2"
+ "version:2@1.21.4"
]
},
{
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json b/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
index cf0e8f7bf..5dd487a2c 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
@@ -8,7 +8,7 @@
"windows-amd64"
],
"tags": [
- "git_revision:284a05aeae3cf2e4579e6518ab3a5316058da6d4"
+ "git_revision:0280acaad36a2544c150e1246cb344a1b6e6a02d"
],
"version_file": ".versions/host_tools.cipd_version"
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json b/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json
index 87c746c53..1f3f5cd5c 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json
@@ -110,6 +110,32 @@
"latest"
],
"version_file": ".versions/logdog.cipd_version"
+ },
+ {
+ "path": "infra/tools/prpc/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "latest"
+ ],
+ "version_file": ".versions/prpc.cipd_version"
+ },
+ {
+ "path": "infra/3pp/tools/gsutil",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "mac-arm64"
+ ],
+ "tags": [
+ "latest"
+ ],
+ "version_file": ".versions/gsutil.cipd_version"
}
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/msrv_python.json b/pw_env_setup/py/pw_env_setup/cipd_setup/msrv_python.json
new file mode 100644
index 000000000..badfbdd8e
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/msrv_python.json
@@ -0,0 +1,5 @@
+{
+ "included_files": [
+ "python38.json"
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json b/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
index 117ad9712..8201b8d64 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "git_revision:41fef642de70ecdcaaa26be96d56a0398f95abd4"
+ "git_revision:e4702d7409069c4f12d45ea7b7f0890717ca3f4b"
],
"version_file": ".versions/gn.cipd_version"
},
@@ -47,7 +47,7 @@
"windows-amd64"
],
"tags": [
- "version:2@3.17.3"
+ "version:2@24.4"
]
},
{
@@ -57,7 +57,7 @@
"mac-arm64"
],
"tags": [
- "version:2@3.17.3"
+ "version:2@24.4"
]
},
{
@@ -66,10 +66,11 @@
"linux-amd64",
"linux-arm64",
"mac-amd64",
+ "mac-arm64",
"windows-amd64"
],
"tags": [
- "git_revision:3a20597776a5d2920e511d81653b4d2b6ca0c855"
+ "git_revision:00396e6a1a0b79fda008cb4e86b616d7952b33c8"
],
"version_file": ".versions/clang.cipd_version"
},
@@ -95,7 +96,7 @@
"mac-arm64"
],
"tags": [
- "version:2@0.11.0-3"
+ "version:2@0.12.0-2"
]
},
{
@@ -115,7 +116,7 @@
"mac-amd64"
],
"tags": [
- "git_revision:823a3f11fb8f04c3c3cc0f95f968fef1bfc6534f,1"
+ "git_revision:a342ce9dfeed8088c426e5d51d4a7e47f3764b84"
],
"version_file": ".versions/qemu.cipd_version"
},
@@ -127,7 +128,16 @@
],
"subdir": "clang_sysroot",
"tags": [
- "git_revision:b07475f83bae0e0744ce5ab5c07b602ececc7fd2"
+ "git_revision:d342388843734b6c5c50fb7e18cd3a76476b93aa"
+ ]
+ },
+ {
+ "path": "infra/3pp/tools/renode/${platform}",
+ "platforms": [
+ "linux-amd64"
+ ],
+ "tags": [
+ "version:2@renode-1.14.0"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
index 02dcdfcdb..6dbbbc849 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
@@ -1,18 +1,5 @@
{
- "packages": [
- {
- "path": "infra/3pp/tools/cpython3/${platform}",
- "platforms": [
- "linux-amd64",
- "linux-arm64",
- "mac-amd64",
- "mac-arm64",
- "windows-amd64"
- ],
- "tags": [
- "version:2@3.10.8.chromium.23"
- ],
- "version_file": ".versions/cpython3.cipd_version"
- }
+ "included_files": [
+ "python311.json"
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python310.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python310.json
new file mode 100644
index 000000000..d7b664d67
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python310.json
@@ -0,0 +1,29 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/cpython3/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@3.10.8.chromium.23"
+ ]
+ },
+ {
+ "path": "pigweed/third_party/python_packages/310/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "subdir": "pip_cache",
+ "tags": [
+ "git_revision:9c0316b7067a011583fe245fa764bba382f92d43"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python311.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python311.json
new file mode 100644
index 000000000..86363b72b
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python311.json
@@ -0,0 +1,30 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/cpython3/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@3.11.3.chromium.29"
+ ],
+ "version_file": ".versions/cpython3.cipd_version"
+ },
+ {
+ "path": "pigweed/third_party/python_packages/311/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "subdir": "pip_cache",
+ "tags": [
+ "git_revision:9c0316b7067a011583fe245fa764bba382f92d43"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python38.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python38.json
new file mode 100644
index 000000000..c751acc48
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python38.json
@@ -0,0 +1,29 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/cpython3/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@3.8.10.chromium.24"
+ ]
+ },
+ {
+ "path": "pigweed/third_party/python_packages/38/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "subdir": "pip_cache",
+ "tags": [
+ "git_revision:9c0316b7067a011583fe245fa764bba382f92d43"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python39.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python39.json
new file mode 100644
index 000000000..0a2b89364
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python39.json
@@ -0,0 +1,29 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/cpython3/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@3.9.13.chromium.22"
+ ]
+ },
+ {
+ "path": "pigweed/third_party/python_packages/39/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "subdir": "pip_cache",
+ "tags": [
+ "git_revision:9c0316b7067a011583fe245fa764bba382f92d43"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json b/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json
index e42babbc7..4c1e317e5 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json
@@ -1,14 +1,14 @@
{
- "packages": [
- {
- "_comment": "Binaries used for pw_transfer backwards-compatibility testing",
- "path": "pigweed/pw_transfer_test_binaries/${platform}",
- "platforms": [
- "linux-amd64"
- ],
- "tags": [
- "version:pw_transfer_test_binaries_528098d588f307881af83f769207b8e6e1b57520-linux-amd64-cipd.cipd"
- ]
- }
- ]
- }
+ "packages": [
+ {
+ "_comment": "Binaries used for pw_transfer backwards-compatibility testing",
+ "path": "pigweed/pw_transfer_test_binaries/${platform}",
+ "platforms": [
+ "linux-amd64"
+ ],
+ "tags": [
+ "version:pw_transfer_test_binaries_528098d588f307881af83f769207b8e6e1b57520-linux-amd64-cipd.cipd"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
index deac4b0df..b34fb0086 100755
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
@@ -162,7 +162,6 @@ def platform(rosetta=False):
def all_package_files(env_vars, package_files):
"""Recursively retrieve all package files."""
- result = []
to_process = []
for pkg_file in package_files:
args = []
@@ -178,20 +177,29 @@ def all_package_files(env_vars, package_files):
to_process.append(path)
- while to_process:
- package_file = to_process.pop(0)
- result.append(package_file)
+ processed_files = []
- with open(package_file, 'r') as ins:
- entries = json.load(ins).get('included_files', ())
+ def flatten_package_files(package_files):
+ """Flatten nested package files."""
+ for package_file in package_files:
+ yield package_file
+ processed_files.append(package_file)
- for entry in entries:
- entry = os.path.join(os.path.dirname(package_file), entry)
+ with open(package_file, 'r') as ins:
+ entries = json.load(ins).get('included_files', ())
+ entries = [
+ os.path.join(os.path.dirname(package_file), entry)
+ for entry in entries
+ ]
+ entries = [
+ entry for entry in entries if entry not in processed_files
+ ]
- if entry not in result and entry not in to_process:
- to_process.append(entry)
+ if entries:
+ for entry in flatten_package_files(entries):
+ yield entry
- return result
+ return list(flatten_package_files(to_process))
def all_packages(package_files):
@@ -202,7 +210,8 @@ def all_packages(package_files):
file_packages = json.load(ins).get('packages', ())
for package in file_packages:
if 'subdir' in package:
- package['subdir'] = os.path.join(name, package['subdir'])
+ package['original_subdir'] = package['subdir']
+ package['subdir'] = '/'.join([name, package['subdir']])
else:
package['subdir'] = name
packages.extend(file_packages)
@@ -211,18 +220,30 @@ def all_packages(package_files):
def deduplicate_packages(packages):
deduped = collections.OrderedDict()
- for package in reversed(packages):
- if package['path'] in deduped:
- del deduped[package['path']]
- deduped[package['path']] = package
- return reversed(list(deduped.values()))
+ for package in packages:
+ # Use the package + the subdir as the key
+ pkg_key = package['path']
+ pkg_key += package.get('original_subdir', '')
+
+ if pkg_key in deduped:
+ # Delete the old package
+ del deduped[pkg_key]
+
+ # Insert the new package at the end
+ deduped[pkg_key] = package
+ return list(deduped.values())
def write_ensure_file(
package_files, ensure_file, platform
): # pylint: disable=redefined-outer-name
+ logdir = os.path.dirname(ensure_file)
packages = all_packages(package_files)
+ with open(os.path.join(logdir, 'all-packages.json'), 'w') as outs:
+ json.dump(packages, outs, indent=4)
deduped_packages = deduplicate_packages(packages)
+ with open(os.path.join(logdir, 'deduped-packages.json'), 'w') as outs:
+ json.dump(deduped_packages, outs, indent=4)
with open(ensure_file, 'w') as outs:
outs.write(
@@ -286,7 +307,8 @@ def update( # pylint: disable=too-many-locals
if env_vars:
env_vars.prepend('PATH', root_install_dir)
env_vars.set('PW_CIPD_INSTALL_DIR', root_install_dir)
- env_vars.set('CIPD_CACHE_DIR', cache_dir)
+ if cache_dir:
+ env_vars.set('CIPD_CACHE_DIR', cache_dir)
pw_root = None
@@ -313,12 +335,13 @@ def update( # pylint: disable=too-many-locals
'debug',
'-json-output',
os.path.join(root_install_dir, 'packages.json'),
- '-cache-dir',
- cache_dir,
'-max-threads',
'0', # 0 means use CPU count.
]
+ if cache_dir:
+ cmd.extend(('-cache-dir', cache_dir))
+
cipd_service_account = None
if env_vars:
cipd_service_account = env_vars.get('PW_CIPD_SERVICE_ACCOUNT_JSON')
@@ -373,6 +396,21 @@ def update( # pylint: disable=too-many-locals
for package_file in reversed(package_files):
name = package_file_name(package_file)
file_install_dir = os.path.join(install_dir, name)
+
+ # If this package file has no packages and just includes one other
+ # file, there won't be any contents of the folder for this package.
+ # In that case, point the variable that would point to this folder
+ # to the folder of the included file.
+ with open(package_file) as ins:
+ contents = json.load(ins)
+ entries = contents.get('included_files', ())
+ file_packages = contents.get('packages', ())
+ if not file_packages and len(entries) == 1:
+ file_install_dir = os.path.join(
+ install_dir,
+ package_file_name(os.path.basename(entries[0])),
+ )
+
# Some executables get installed at top-level and some get
# installed under 'bin'. A small number of old packages prefix the
# entire tree with the platform (e.g., chromium/third_party/tcl).
@@ -384,7 +422,8 @@ def update( # pylint: disable=too-many-locals
if os.path.isdir(bin_dir):
env_vars.prepend('PATH', bin_dir)
env_vars.set(
- 'PW_{}_CIPD_INSTALL_DIR'.format(name.upper()), file_install_dir
+ 'PW_{}_CIPD_INSTALL_DIR'.format(name.upper().replace('-', '_')),
+ file_install_dir,
)
# Windows has its own special toolchain.
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json b/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json
index a91f638cf..5f3e71e19 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json
@@ -2,12 +2,14 @@
"included_files": [
"bazel.json",
"cmake.json",
+ "coverage.json",
"default.json",
"doxygen.json",
"go.json",
"host_tools.json",
"kythe.json",
"luci.json",
+ "msrv_python.json",
"rbe.json",
"testing.json",
"web.json"
diff --git a/pw_env_setup/py/pw_env_setup/config_file.py b/pw_env_setup/py/pw_env_setup/config_file.py
index fc8fa40c5..56bc142e7 100644
--- a/pw_env_setup/py/pw_env_setup/config_file.py
+++ b/pw_env_setup/py/pw_env_setup/config_file.py
@@ -20,24 +20,51 @@ import json
import os
-def _get_project_root(env=None):
- if not env:
- env = os.environ
+def _resolve_env(env):
+ if env:
+ return env
+ return os.environ
+
+
+def _get_project_root(env):
for var in ('PW_PROJECT_ROOT', 'PW_ROOT'):
if var in env:
return env[var]
raise ValueError('environment variable PW_PROJECT_ROOT not set')
+def _pw_env_substitute(env, string):
+ if not isinstance(string, str):
+ return string
+
+ # Substitute in environment variables based on $pw_env{VAR_NAME} tokens.
+ for key, value in env.items():
+ string = string.replace('$pw_env{' + key + '}', value)
+
+ if '$pw_env{' in string:
+ raise ValueError(f'Unresolved $pw_env\\{...} in JSON string: {string}')
+
+ return string
+
+
def path(env=None):
"""Return the path where pigweed.json should reside."""
+ env = _resolve_env(env)
return os.path.join(_get_project_root(env=env), 'pigweed.json')
def load(env=None):
"""Load pigweed.json if it exists and return the contents."""
+ env = _resolve_env(env)
config_path = path(env=env)
if not os.path.isfile(config_path):
return {}
- with open(config_path, 'r') as ins:
- return json.load(ins)
+
+ def hook(obj):
+ out = {}
+ for key, val in obj.items():
+ out[key] = _pw_env_substitute(env, val)
+ return out
+
+ with open(config_path, 'r') as file:
+ return json.load(file, object_hook=hook)
diff --git a/pw_env_setup/py/pw_env_setup/entry_points/__init__.py b/pw_env_setup/py/pw_env_setup/entry_points/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/entry_points/__init__.py
diff --git a/pw_env_setup/py/pw_env_setup/entry_points/arm_gdb.py b/pw_env_setup/py/pw_env_setup/entry_points/arm_gdb.py
new file mode 100644
index 000000000..7bcf076d8
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/entry_points/arm_gdb.py
@@ -0,0 +1,66 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Wrapper script to run arm-none-eabi-gdb with Python 3.8."""
+
+import os
+from pathlib import Path
+import shutil
+import signal
+import sys
+import subprocess
+
+
+def main() -> int:
+ """arm-gdb wrapper that sets up the Python environment for gdb"""
+
+ # Find 'arm-none-eabi-gdb' as long as it isn't in the current Python
+ # virtualenv entry point. In other words: not this script.
+ exclude_paths = sys.path
+ venv = os.environ.get('VIRTUAL_ENV')
+ if venv:
+ venv_path = Path(venv).resolve()
+ exclude_paths.append(os.path.join(venv_path, 'Scripts'))
+ arm_gdb_binary = shutil.which(
+ 'arm-none-eabi-gdb',
+ path=os.pathsep.join(
+ [
+ path_entry
+ for path_entry in os.environ.get('PATH', '').split(os.pathsep)
+ if str(Path(path_entry).resolve()) not in exclude_paths
+ ]
+ ),
+ )
+ assert arm_gdb_binary
+
+ arm_gdb_path = Path(arm_gdb_binary)
+ arm_install_prefix = arm_gdb_path.parent.parent
+ python_home = arm_install_prefix / 'python/bin/python3'
+ python_path = arm_install_prefix / 'python/lib/python3.8'
+ assert arm_gdb_path.is_file()
+
+ env = os.environ.copy()
+ # Only set Python if it's in the expected location.
+ if python_home.is_file() and python_path.is_dir():
+ env['PYTHONHOME'] = str(python_home)
+ env['PYTHONPATH'] = str(python_path)
+
+ # Ignore Ctrl-C to allow gdb to handle normally
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+ return subprocess.run(
+ [str(arm_gdb_path)] + sys.argv[1:], env=env, check=False
+ ).returncode
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_env_setup/py/pw_env_setup/env_setup.py b/pw_env_setup/py/pw_env_setup/env_setup.py
index 7b1966847..d4f37575b 100755
--- a/pw_env_setup/py/pw_env_setup/env_setup.py
+++ b/pw_env_setup/py/pw_env_setup/env_setup.py
@@ -160,6 +160,11 @@ class MissingSubmodulesError(Exception):
pass
+def _assert_sequence(value):
+ assert isinstance(value, (list, tuple))
+ return value
+
+
# TODO(mohrr) remove disable=useless-object-inheritance once in Python 3.
# pylint: disable=useless-object-inheritance
# pylint: disable=too-many-instance-attributes
@@ -184,6 +189,7 @@ class EnvSetup(object):
use_pinned_pip_packages,
cipd_only,
trust_cipd_hash,
+ additional_cipd_file,
):
self._env = environment.Environment()
self._project_root = project_root
@@ -200,6 +206,7 @@ class EnvSetup(object):
self._strict = strict
self._cipd_only = cipd_only
self._trust_cipd_hash = trust_cipd_hash
+ self._additional_cipd_file = additional_cipd_file
if os.path.isfile(shell_file):
os.unlink(shell_file)
@@ -208,10 +215,15 @@ class EnvSetup(object):
self._pw_root = self._pw_root.decode()
self._cipd_package_file = []
+ self._project_actions = []
self._virtualenv_requirements = []
self._virtualenv_constraints = []
self._virtualenv_gn_targets = []
self._virtualenv_gn_args = []
+ self._virtualenv_pip_install_disable_cache = False
+ self._virtualenv_pip_install_find_links = []
+ self._virtualenv_pip_install_offline = False
+ self._virtualenv_pip_install_require_hashes = False
self._use_pinned_pip_packages = use_pinned_pip_packages
self._optional_submodules = []
self._required_submodules = []
@@ -224,8 +236,10 @@ class EnvSetup(object):
self._json_file = json_file
self._gni_file = None
- self._config_file_name = getattr(config_file, 'name', 'config file')
- self._env.set('_PW_ENVIRONMENT_CONFIG_FILE', self._config_file_name)
+ self._config_file_name = config_file
+ self._env.set(
+ '_PW_ENVIRONMENT_CONFIG_FILE', os.path.abspath(config_file)
+ )
if config_file:
self._parse_config_file(config_file)
@@ -239,6 +253,7 @@ class EnvSetup(object):
self._env.set('PW_PROJECT_ROOT', project_root, deactivate=False)
self._env.set('PW_ROOT', pw_root, deactivate=False)
self._env.set('_PW_ACTUAL_ENVIRONMENT_ROOT', install_dir)
+ self._env.set('VIRTUAL_ENV', self._virtualenv_root)
self._env.add_replacement('_PW_ACTUAL_ENVIRONMENT_ROOT', install_dir)
self._env.add_replacement('PW_ROOT', pw_root)
@@ -269,10 +284,31 @@ class EnvSetup(object):
return files, warnings
def _parse_config_file(self, config_file):
- config = json.load(config_file)
+ # This should use pw_env_setup.config_file instead.
+ with open(config_file, 'r') as ins:
+ config = json.load(ins)
+
+ # While transitioning, allow environment config to be at the top of
+ # the JSON file or at '.["pw"]["pw_env_setup"]'.
+ config = config.get('pw', config)
+ config = config.get('pw_env_setup', config)
self._root_variable = config.pop('root_variable', None)
+ # This variable is not used by env setup since we already have it.
+ # However, other tools may use it, so we double-check that it's correct.
+ pigweed_root = os.path.join(
+ self._project_root,
+ config.pop('relative_pigweed_root', self._pw_root),
+ )
+ if os.path.abspath(self._pw_root) != os.path.abspath(pigweed_root):
+ raise ValueError(
+ 'expected Pigweed root {!r} in config but found {!r}'.format(
+ os.path.relpath(self._pw_root, self._project_root),
+ os.path.relpath(pigweed_root, self._project_root),
+ )
+ )
+
rosetta = config.pop('rosetta', 'allow')
if rosetta not in ('never', 'allow', 'force'):
raise ValueError(rosetta)
@@ -284,8 +320,12 @@ class EnvSetup(object):
self._gni_file = config.pop('gni_file', None)
- self._optional_submodules.extend(config.pop('optional_submodules', ()))
- self._required_submodules.extend(config.pop('required_submodules', ()))
+ self._optional_submodules.extend(
+ _assert_sequence(config.pop('optional_submodules', ()))
+ )
+ self._required_submodules.extend(
+ _assert_sequence(config.pop('required_submodules', ()))
+ )
if self._optional_submodules and self._required_submodules:
raise ValueError(
@@ -296,10 +336,21 @@ class EnvSetup(object):
self._cipd_package_file.extend(
os.path.join(self._project_root, x)
- for x in config.pop('cipd_package_files', ())
+ for x in _assert_sequence(config.pop('cipd_package_files', ()))
)
+ self._cipd_package_file.extend(
+ os.path.join(self._project_root, x)
+ for x in self._additional_cipd_file or ()
+ )
+
+ for action in config.pop('project_actions', {}):
+ # We can add a 'phase' option in the future if we end up needing to
+ # support project actions at more than one point in the setup flow.
+ self._project_actions.append(
+ (action['import_path'], action['module_name'])
+ )
- for pkg in config.pop('pw_packages', ()):
+ for pkg in _assert_sequence(config.pop('pw_packages', ())):
self._pw_packages.append(pkg)
virtualenv = config.pop('virtualenv', {})
@@ -309,27 +360,46 @@ class EnvSetup(object):
else:
root = self._project_root
- for target in virtualenv.pop('gn_targets', ()):
+ for target in _assert_sequence(virtualenv.pop('gn_targets', ())):
self._virtualenv_gn_targets.append(
virtualenv_setup.GnTarget('{}#{}'.format(root, target))
)
- self._virtualenv_gn_args = virtualenv.pop('gn_args', ())
+ self._virtualenv_gn_args = _assert_sequence(
+ virtualenv.pop('gn_args', ())
+ )
self._virtualenv_system_packages = virtualenv.pop(
'system_packages', False
)
- for req_txt in virtualenv.pop('requirements', ()):
+ for req_txt in _assert_sequence(virtualenv.pop('requirements', ())):
self._virtualenv_requirements.append(
os.path.join(self._project_root, req_txt)
)
- for constraint_txt in virtualenv.pop('constraints', ()):
+ for constraint_txt in _assert_sequence(
+ virtualenv.pop('constraints', ())
+ ):
self._virtualenv_constraints.append(
os.path.join(self._project_root, constraint_txt)
)
+ for pip_cache_dir in _assert_sequence(
+ virtualenv.pop('pip_install_find_links', ())
+ ):
+ self._virtualenv_pip_install_find_links.append(pip_cache_dir)
+
+ self._virtualenv_pip_install_disable_cache = virtualenv.pop(
+ 'pip_install_disable_cache', False
+ )
+ self._virtualenv_pip_install_offline = virtualenv.pop(
+ 'pip_install_offline', False
+ )
+ self._virtualenv_pip_install_require_hashes = virtualenv.pop(
+ 'pip_install_require_hashes', False
+ )
+
if virtualenv:
raise ConfigFileError(
'unrecognized option in {}: "virtualenv.{}"'.format(
@@ -417,6 +487,7 @@ class EnvSetup(object):
with open(gni_file, 'w') as outs:
self._env.gni(outs, self._project_root)
+ shutil.copy(gni_file, os.path.join(self._install_dir, 'logs'))
def _log(self, *args, **kwargs):
# Not using logging module because it's awkward to flush a log handler.
@@ -437,6 +508,7 @@ class EnvSetup(object):
steps = [
('CIPD package manager', self.cipd),
+ ('Project actions', self.project_actions),
('Python environment', self.virtualenv),
('pw packages', self.pw_package),
('Host tools', self.host_tools),
@@ -625,6 +697,35 @@ Then use `set +x` to go back to normal.
return result(_Result.Status.DONE)
+ def project_actions(self, unused_spin):
+ """Perform project install actions.
+
+ This is effectively a limited plugin system for performing
+ project-specific actions (e.g. fetching tools) after CIPD but before
+ virtualenv setup.
+ """
+ result = result_func()
+
+ if not self._project_actions:
+ return result(_Result.Status.SKIPPED)
+
+ if sys.version_info[0] < 3:
+ raise ValueError(
+ 'Project Actions require Python 3 or higher. '
+ 'The current python version is %s' % sys.version_info
+ )
+
+ # Once Keir okays removing 2.7 support for env_setup, move this import
+ # to the main list of imports at the top of the file.
+ import importlib # pylint: disable=import-outside-toplevel
+
+ for import_path, module_name in self._project_actions:
+ sys.path.append(import_path)
+ mod = importlib.import_module(module_name)
+ mod.run_action(env=self._env)
+
+ return result(_Result.Status.DONE)
+
def virtualenv(self, unused_spin):
"""Setup virtualenv."""
@@ -663,6 +764,14 @@ Then use `set +x` to go back to normal.
venv_path=self._virtualenv_root,
requirements=requirements,
constraints=constraints,
+ pip_install_find_links=self._virtualenv_pip_install_find_links,
+ pip_install_offline=self._virtualenv_pip_install_offline,
+ pip_install_require_hashes=(
+ self._virtualenv_pip_install_require_hashes
+ ),
+ pip_install_disable_cache=(
+ self._virtualenv_pip_install_disable_cache
+ ),
gn_args=self._virtualenv_gn_args,
gn_targets=self._virtualenv_gn_targets,
gn_out_dir=self._virtualenv_gn_out_dir,
@@ -691,7 +800,7 @@ Then use `set +x` to go back to normal.
for pkg in self._pw_packages:
print('installing {}'.format(pkg))
- cmd = ['pw', 'package', 'install', '--force', pkg]
+ cmd = ['pw', 'package', 'install', pkg]
log = os.path.join(pkg_dir, '{}.log'.format(pkg))
try:
@@ -755,11 +864,19 @@ def parse(argv=None):
required=not project_root,
)
+ default_cipd_cache_dir = os.environ.get(
+ 'CIPD_CACHE_DIR', os.path.expanduser('~/.cipd-cache-dir')
+ )
+ if 'PW_NO_CIPD_CACHE_DIR' in os.environ:
+ default_cipd_cache_dir = None
+
+ parser.add_argument('--cipd-cache-dir', default=default_cipd_cache_dir)
+
parser.add_argument(
- '--cipd-cache-dir',
- default=os.environ.get(
- 'CIPD_CACHE_DIR', os.path.expanduser('~/.cipd-cache-dir')
- ),
+ '--no-cipd-cache-dir',
+ action='store_const',
+ const=None,
+ dest='cipd_cache_dir',
)
parser.add_argument(
@@ -791,9 +908,17 @@ def parse(argv=None):
parser.add_argument(
'--config-file',
- help='JSON file describing CIPD and virtualenv requirements.',
- type=argparse.FileType('r'),
- required=True,
+ help='Path to pigweed.json file.',
+ default=os.path.join(project_root, 'pigweed.json'),
+ )
+
+ parser.add_argument(
+ '--additional-cipd-file',
+ help=(
+ 'Path to additional CIPD files, in addition to those referenced by '
+ 'the --config-file file.'
+ ),
+ action='append',
)
parser.add_argument(
diff --git a/pw_env_setup/py/pw_env_setup/gni_visitor.py b/pw_env_setup/py/pw_env_setup/gni_visitor.py
index c24d35d76..ca46b10ce 100644
--- a/pw_env_setup/py/pw_env_setup/gni_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/gni_visitor.py
@@ -30,12 +30,12 @@ class GNIVisitor(object): # pylint: disable=useless-object-inheritance
Example gni file:
declare_args() {
- pw_env_setup_CIPD_DEFAULT = "//<ENVIRONMENT_DIR>/cipd/packages/default"
- pw_env_setup_CIPD_PIGWEED = "//<ENVIRONMENT_DIR>/cipd/packages/pigweed"
pw_env_setup_CIPD_ARM = "//<ENVIRONMENT_DIR>/cipd/packages/arm"
- pw_env_setup_CIPD_PYTHON = "//<ENVIRONMENT_DIR>/cipd/packages/python"
pw_env_setup_CIPD_BAZEL = "//<ENVIRONMENT_DIR>/cipd/packages/bazel"
+ pw_env_setup_CIPD_DEFAULT = "//<ENVIRONMENT_DIR>/cipd/packages/default"
pw_env_setup_CIPD_LUCI = "//<ENVIRONMENT_DIR>/cipd/packages/luci"
+ pw_env_setup_CIPD_PIGWEED = "//<ENVIRONMENT_DIR>/cipd/packages/pigweed"
+ pw_env_setup_CIPD_PYTHON = "//<ENVIRONMENT_DIR>/cipd/packages/python"
pw_env_setup_VIRTUAL_ENV = "//<ENVIRONMENT_DIR>/pigweed-venv"
}
"""
@@ -43,25 +43,35 @@ class GNIVisitor(object): # pylint: disable=useless-object-inheritance
def __init__(self, project_root, *args, **kwargs):
super(GNIVisitor, self).__init__(*args, **kwargs)
self._project_root = project_root
- self._lines = []
+ self._variables = {} # Dict of variables to set.
+
+ def _gn_variables(self, env):
+ self._variables.clear()
+ env.accept(self)
+ variables = dict(self._variables)
+ self._variables.clear()
+ return variables
def serialize(self, env, outs):
- self._lines.append(
+ """Write a gni file based on the given environment.
+
+ Args:
+ env (environment.Environment): Environment variables to use.
+ outs (file): GNI file to write.
+ """
+
+ print(
"""
# This file is automatically generated by Pigweed's environment setup. Do not
# edit it manually or check it in.
-""".strip()
+ """.strip(),
+ file=outs,
)
- self._lines.append('declare_args() {')
-
- env.accept(self)
-
- self._lines.append('}')
-
- for line in self._lines:
- print(line, file=outs)
- self._lines = []
+ print('declare_args() {', file=outs)
+ for name, value in sorted(self._gn_variables(env).items()):
+ print(' {} = "{}"'.format(name, value), file=outs)
+ print('}', file=outs)
def _abspath_to_gn_path(self, path):
gn_path = os.path.relpath(path, start=self._project_root)
@@ -74,25 +84,17 @@ class GNIVisitor(object): # pylint: disable=useless-object-inheritance
def visit_set(self, set): # pylint: disable=redefined-builtin
match = re.search(r'PW_(.*)_CIPD_INSTALL_DIR', set.name)
+ name = None
+
if match:
name = 'pw_env_setup_CIPD_{}'.format(match.group(1))
- self._lines.append(
- ' {} = "{}"'.format(name, self._abspath_to_gn_path(set.value))
- )
-
if set.name == 'VIRTUAL_ENV':
- self._lines.append(
- ' pw_env_setup_VIRTUAL_ENV = "{}"'.format(
- self._abspath_to_gn_path(set.value)
- )
- )
-
+ name = 'pw_env_setup_VIRTUAL_ENV'
if set.name == 'PW_PACKAGE_ROOT':
- self._lines.append(
- ' pw_env_setup_PACKAGE_ROOT = "{}"'.format(
- self._abspath_to_gn_path(set.value)
- )
- )
+ name = 'pw_env_setup_PACKAGE_ROOT'
+
+ if name:
+ self._variables[name] = self._abspath_to_gn_path(set.value)
def visit_clear(self, clear):
pass
diff --git a/pw_env_setup/py/pw_env_setup/json_visitor.py b/pw_env_setup/py/pw_env_setup/json_visitor.py
index 8c217bf74..0f924c2dd 100644
--- a/pw_env_setup/py/pw_env_setup/json_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/json_visitor.py
@@ -27,6 +27,12 @@ class JSONVisitor(object): # pylint: disable=useless-object-inheritance
self._data = {}
def serialize(self, env, outs):
+ """Write a json file based on the given environment.
+
+ Args:
+ env (environment.Environment): Environment variables to use.
+ outs (file): JSON file to write.
+ """
self._data = {
'modify': {},
'set': {},
diff --git a/pw_env_setup/py/pw_env_setup/python_packages.py b/pw_env_setup/py/pw_env_setup/python_packages.py
index 34cd04307..e55bcde4b 100644
--- a/pw_env_setup/py/pw_env_setup/python_packages.py
+++ b/pw_env_setup/py/pw_env_setup/python_packages.py
@@ -33,7 +33,7 @@ def _installed_packages() -> Iterator[str]:
if isinstance(pkg, pkg_resources.DistInfoDistribution) # type: ignore
# This will skip packages with local versions.
# For example text after a plus sign: 1.2.3+dev456
- and not pkg.parsed_version.local
+ and not pkg.parsed_version.local # type: ignore
# These are always installed by default in:
# pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
and pkg.key not in ['pip', 'setuptools', 'wheel']
@@ -108,8 +108,8 @@ def _stderr(*args, **kwargs):
def _load_requirements_lines(*req_files: Path) -> Iterator[str]:
for req_file in req_files:
for line in req_file.read_text().splitlines():
- # Ignore comments and blank lines
- if line.startswith('#') or line == '':
+ # Ignore constraints, comments and blank lines
+ if line.startswith('-c') or line.startswith('#') or line == '':
continue
yield line
diff --git a/pw_env_setup/py/pw_env_setup/shell_visitor.py b/pw_env_setup/py/pw_env_setup/shell_visitor.py
index 71849294d..c76653c6b 100644
--- a/pw_env_setup/py/pw_env_setup/shell_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/shell_visitor.py
@@ -61,6 +61,12 @@ class ShellVisitor(_BaseShellVisitor):
self._replacements = ()
def serialize(self, env, outs):
+ """Write a shell file based on the given environment.
+
+ Args:
+ env (environment.Environment): Environment variables to use.
+ outs (file): Shell file to write.
+ """
try:
self._replacements = tuple(
(key, env.get(key) if value is None else value)
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/BUILD.bazel b/pw_env_setup/py/pw_env_setup/virtualenv_setup/BUILD.bazel
new file mode 100644
index 000000000..18b0865da
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/BUILD.bazel
@@ -0,0 +1,15 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
index bee66aaf3..0b78fa271 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
@@ -1,98 +1,106 @@
-alabaster==0.7.12
+-c ./python_base_requirements.txt
+
+alabaster==0.7.13
appdirs==1.4.4
-astroid==2.14.2
-Babel==2.9.1
+astroid==3.0.1
+asttokens==2.4.0
+babel==2.12.1
backcall==0.2.0
black==23.1.0
-build==0.8.0
cachetools==5.0.0
certifi==2021.10.8
-cffi==1.15.0
-charset-normalizer==2.0.10
+cffi==1.15.1
+charset-normalizer==3.2.0
+click==8.1.3
coloredlogs==15.0.1
-coverage==6.3
-cryptography==36.0.1
+coverage==7.2.7
+cryptography==41.0.2
decorator==5.1.1
dill==0.3.6
-docutils==0.17.1
-breathe==4.34.0
-google-api-core==2.7.1
-google-auth==2.6.3
-google-cloud-core==2.2.3
-google-cloud-storage==2.2.1
-google-crc32c==1.3.0
-google-resumable-media==2.3.2
-googleapis-common-protos==1.56.2
+docutils==0.20.1
+executing==2.0.0
+google-api-core==2.12.0
+googleapis-common-protos==1.61.0
+google-auth==2.23.3
+google-cloud-core==2.3.3
+google-cloud-storage==2.12.0
+google-crc32c==1.5.0
+google-resumable-media==2.6.0
graphlib-backport==1.0.3; python_version < "3.9"
humanfriendly==10.0
-idna==3.3
-imagesize==1.3.0
-ipython==7.31.0
+idna==3.4
+imagesize==1.4.1
+importlib-metadata==6.8.0
+ipython==8.12.2
isort==5.10.1
-jedi==0.18.1
-Jinja2==3.0.3
-lazy-object-proxy==1.7.1
-MarkupSafe==2.0.1
+jedi==0.19.1
+Jinja2==3.1.2
+json5==0.9.11
+lockfile==0.12.2
+kconfiglib==14.1.0
+markupsafe==2.1.3
matplotlib-inline==0.1.3
mccabe==0.6.1
-mypy==1.0.1
+mypy==1.6.1
mypy-extensions==1.0.0
-mypy-protobuf==3.3.0
-packaging==23.0
-parameterized==0.8.1
+mypy-protobuf==3.5.0
+parameterized==0.9.0
parso==0.8.3
-pep517==0.12.0
+pathspec==0.11.1
pexpect==4.8.0
-platformdirs==3.0.0
pickleshare==0.7.5
-prompt-toolkit==3.0.36
-protobuf==3.20.2
+platformdirs==3.0.0
+prompt-toolkit==3.0.39
+protobuf==4.24.4
psutil==5.9.4
-ptpython==3.0.22
+ptpython==3.0.23
ptyprocess==0.7.0
+pure-eval==0.2.2
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycparser==2.21
pyelftools==0.27
-Pygments==2.14.0
-pylint==2.16.2
-pyparsing==3.0.6
+pygments==2.16.1
+pylint==3.0.1
pyperclip==1.8.2
pyserial==3.5
-pytz==2021.3
-PyYAML==6.0
-requests==2.27.1
+python-daemon==3.0.1
+pytz==2023.3
+pyusb==1.2.1
+pyyaml==6.0.1
+requests==2.31.0
rsa==4.8
-scan-build==2.0.19
-setuptools==63.4.3
+setuptools==68.0.0
six==1.16.0
snowballstemmer==2.2.0
-Sphinx==5.3.0
-sphinx-rtd-theme==1.2.0
+sphinx==7.1.2
sphinx-argparse==0.4.0
-sphinx-copybutton==0.5.1
-sphinx-design==0.3.0
-sphinxcontrib-applehelp==1.0.2
+sphinxcontrib-applehelp==1.0.4
sphinxcontrib-devhelp==1.0.2
-sphinxcontrib-htmlhelp==2.0.0
+sphinxcontrib-htmlhelp==2.0.1
sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-mermaid==0.8
+sphinxcontrib-mermaid==0.9.2
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
+sphinx-design==0.5.0
+stack-data==0.6.3
toml==0.10.2
-tomli==2.0.1
+tomli==2.0.1; python_version < "3.11"
tomlkit==0.11.6
traitlets==5.1.1
-types-docutils==0.17.4
-types-futures==3.3.2
-types-protobuf==3.20.4.6
-types-Pygments==2.9.13
-types-PyYAML==6.0.7
-types-setuptools==63.4.1
+types-docutils==0.20.0.3
+types-protobuf==4.24.0.2
+types-Pygments==2.16.0.0
+types-pyserial==3.5.0.7
+types-PyYAML==6.0.12.11
+types-requests==2.31.0.2
+types-setuptools==67.8.0.0
types-six==1.16.9
+types-urllib3==1.26.25.14
typing-extensions==4.4.0
-urllib3==1.26.8
-watchdog==2.1.6
-wcwidth==0.2.5
-wrapt==1.12.1
+urllib3==2.0.4
+watchdog==2.3.1
+wcwidth==0.2.6
+websockets==10.4
yapf==0.31.0
+zipp==3.16.2
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_darwin.list b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_darwin.list
new file mode 100644
index 000000000..7d9f7e4b9
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_darwin.list
@@ -0,0 +1,1177 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes --output-file=/constraint_hashes_darwin.list --strip-extras /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+#
+alabaster==0.7.13 \
+ --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+ --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ptpython
+appnope==0.1.3 \
+ --hash=sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24 \
+ --hash=sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+astroid==3.0.1 \
+ --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \
+ --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+asttokens==2.4.0 \
+ --hash=sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e \
+ --hash=sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+babel==2.12.1 \
+ --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+ --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+backcall==0.2.0 \
+ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
+ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+black==23.1.0 \
+ --hash=sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd \
+ --hash=sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555 \
+ --hash=sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481 \
+ --hash=sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468 \
+ --hash=sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9 \
+ --hash=sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a \
+ --hash=sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958 \
+ --hash=sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580 \
+ --hash=sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26 \
+ --hash=sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32 \
+ --hash=sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8 \
+ --hash=sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753 \
+ --hash=sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b \
+ --hash=sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074 \
+ --hash=sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651 \
+ --hash=sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24 \
+ --hash=sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6 \
+ --hash=sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad \
+ --hash=sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac \
+ --hash=sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221 \
+ --hash=sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06 \
+ --hash=sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27 \
+ --hash=sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648 \
+ --hash=sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739 \
+ --hash=sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+cachetools==5.0.0 \
+ --hash=sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6 \
+ --hash=sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cryptography
+charset-normalizer==3.2.0 \
+ --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
+ --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
+ --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
+ --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
+ --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
+ --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
+ --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
+ --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
+ --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
+ --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
+ --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
+ --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
+ --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
+ --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
+ --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
+ --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
+ --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
+ --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
+ --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
+ --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
+ --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
+ --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
+ --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
+ --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
+ --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
+ --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
+ --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
+ --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
+ --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
+ --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
+ --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
+ --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
+ --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
+ --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
+ --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
+ --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
+ --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
+ --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
+ --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
+ --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
+ --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
+ --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
+ --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
+ --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
+ --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
+ --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
+ --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
+ --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
+ --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
+ --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
+ --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
+ --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
+ --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
+ --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
+ --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
+ --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
+ --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
+ --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
+ --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
+ --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
+ --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
+ --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
+ --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
+ --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
+ --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
+ --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
+ --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
+ --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
+ --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
+ --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
+ --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
+ --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
+ --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
+ --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
+ --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pip-tools
+coloredlogs==15.0.1 \
+ --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \
+ --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+coverage==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+cryptography==41.0.2 \
+ --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
+ --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
+ --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
+ --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
+ --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
+ --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
+ --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
+ --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
+ --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
+ --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
+ --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
+ --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
+ --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
+ --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
+ --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
+ --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
+ --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
+ --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
+ --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
+ --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
+ --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
+ --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
+ --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+decorator==5.1.1 \
+ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
+ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+docutils==0.20.1 \
+ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \
+ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # python-daemon
+ # sphinx
+executing==2.0.0 \
+ --hash=sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657 \
+ --hash=sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+google-api-core==2.12.0 \
+ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \
+ --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-core
+ # google-cloud-storage
+google-auth==2.23.3 \
+ --hash=sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3 \
+ --hash=sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-core
+ # google-cloud-storage
+google-cloud-core==2.3.3 \
+ --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
+ --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+google-cloud-storage==2.12.0 \
+ --hash=sha256:57c0bcda2f5e11f008a155d8636d8381d5abab46b58e0cae0e46dd5e595e6b46 \
+ --hash=sha256:bc52563439d42981b6e21b071a76da2791672776eda3ba99d13a8061ebbd6e5e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+google-crc32c==1.5.0 \
+ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+ --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+ --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+ --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+ --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+ --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+ --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+ --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+ --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+ --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+ --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+ --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+ --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+ --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+ --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+ --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+ --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+ --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+ --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+ --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+ --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+ --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+ --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+ --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+ --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+ --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+ --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+ --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+ --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+ --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+ --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+ --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+ --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+ --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+ --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+ --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+ --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+ --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+ --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+ --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+ --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+ --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+ --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+ --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+ --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+ --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+ --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+ --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+ --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+ --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+ --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+ --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+ --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+ --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+ --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+ --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+ --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+ --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+ --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+ --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+ --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+ --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+ --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+ --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+ --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.6.0 \
+ --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \
+ --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+googleapis-common-protos==1.61.0 \
+ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
+ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+graphlib-backport==1.0.3 ; python_version < "3.9" \
+ --hash=sha256:24246967b9e7e6a91550bc770e6169585d35aa32790258579a8a3899a8c18fde \
+ --hash=sha256:7bb8fc7757b8ae4e6d8000a26cd49e9232aaa9a3aa57edb478474b8424bfaae2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+humanfriendly==10.0 \
+ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \
+ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # coloredlogs
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+imagesize==1.4.1 \
+ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+ipython==8.12.2 \
+ --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \
+ --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+jedi==0.19.1 \
+ --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \
+ --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+json5==0.9.11 \
+ --hash=sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd \
+ --hash=sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+lockfile==0.12.2 \
+ --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \
+ --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # python-daemon
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jinja2
+matplotlib-inline==0.1.3 \
+ --hash=sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee \
+ --hash=sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+mccabe==0.6.1 \
+ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
+ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+mypy==1.6.1 \
+ --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
+ --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
+ --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
+ --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
+ --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
+ --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
+ --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
+ --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
+ --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
+ --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
+ --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
+ --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
+ --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
+ --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
+ --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
+ --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
+ --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
+ --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
+ --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
+ --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
+ --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
+ --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
+ --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
+ --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
+ --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
+ --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
+ --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # mypy
+mypy-protobuf==3.5.0 \
+ --hash=sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393 \
+ --hash=sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # sphinx
+parameterized==0.9.0 \
+ --hash=sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b \
+ --hash=sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+parso==0.8.3 \
+ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
+ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jedi
+pathspec==0.11.1 \
+ --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \
+ --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+pexpect==4.8.0 \
+ --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
+ --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pickleshare==0.7.5 \
+ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
+ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pylint
+prompt-toolkit==3.0.39 \
+ --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
+ --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+protobuf==4.24.4 \
+ --hash=sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe \
+ --hash=sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085 \
+ --hash=sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b \
+ --hash=sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667 \
+ --hash=sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37 \
+ --hash=sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92 \
+ --hash=sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4 \
+ --hash=sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b \
+ --hash=sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9 \
+ --hash=sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd \
+ --hash=sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e \
+ --hash=sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46 \
+ --hash=sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # googleapis-common-protos
+ # mypy-protobuf
+psutil==5.9.4 \
+ --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+ --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+ --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+ --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+ --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+ --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+ --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+ --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+ --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+ --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+ --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+ --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+ --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+ --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptpython==3.0.23 \
+ --hash=sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c \
+ --hash=sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptyprocess==0.7.0 \
+ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
+ --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pexpect
+pure-eval==0.2.2 \
+ --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
+ --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+pyasn1==0.4.8 \
+ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
+ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.2.8 \
+ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
+ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cffi
+pyelftools==0.27 \
+ --hash=sha256:5609aa6da1123fccfae2e8431a67b4146aa7fad5b3889f808df12b110f230937 \
+ --hash=sha256:cde854e662774c5457d688ca41615f6594187ba7067af101232df889a6b7a66b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pygments==2.16.1 \
+ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
+ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+ # sphinx
+pylint==3.0.1 \
+ --hash=sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40 \
+ --hash=sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyperclip==1.8.2 \
+ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # build
+pyserial==3.5 \
+ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
+ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+python-daemon==3.0.1 \
+ --hash=sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341 \
+ --hash=sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pytz==2023.3 \
+ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
+ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # babel
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-storage
+ # sphinx
+rsa==4.8 \
+ --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \
+ --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # asttokens
+snowballstemmer==2.2.0 \
+ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinx==7.1.2 \
+ --hash=sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f \
+ --hash=sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx-argparse
+ # sphinx-design
+sphinx-argparse==0.4.0 \
+ --hash=sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037 \
+ --hash=sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-design==0.5.0 \
+ --hash=sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e \
+ --hash=sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-applehelp==1.0.4 \
+ --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+ --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+ --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+ --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-mermaid==0.9.2 \
+ --hash=sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af \
+ --hash=sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+stack-data==0.6.3 \
+ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
+ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+tomli==2.0.1 ; python_version < "3.11" \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.11.6 \
+ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
+ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+traitlets==5.1.1 \
+ --hash=sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7 \
+ --hash=sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # matplotlib-inline
+types-docutils==0.20.0.3 \
+ --hash=sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd \
+ --hash=sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-protobuf==4.24.0.2 \
+ --hash=sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f \
+ --hash=sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # mypy-protobuf
+types-pygments==2.16.0.0 \
+ --hash=sha256:4624a547d5ba73c971fac5d6fd327141e85e65f6123448bee76f0c8557652a71 \
+ --hash=sha256:aa93e4664e2d6cfea7570cde156e3966bf939f9c7d736cd179c4c8e94f7600b2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyserial==3.5.0.7 \
+ --hash=sha256:22b665a336539b85108e2f5d61e6cde1b59818eae78324c17bfe64edc1a2bd66 \
+ --hash=sha256:aa5d5536ea914ca9be85f496e0914b10a58bbb1d2b1f54ce756be45ecb65378b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyyaml==6.0.12.11 \
+ --hash=sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b \
+ --hash=sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-requests==2.31.0.2 \
+ --hash=sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a \
+ --hash=sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-setuptools==67.8.0.0 \
+ --hash=sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff \
+ --hash=sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-six==1.16.9 \
+ --hash=sha256:04da8bad920aea300e13d99c8948fd0914abdfc5d9421bad636137f9829dcbf6 \
+ --hash=sha256:da5748f10eec44accbd393e7f5a834991aff8f37522472166fbc8233cc735b84
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-urllib3==1.26.25.14 \
+ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \
+ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-requests
+typing-extensions==4.4.0 \
+ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
+ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # astroid
+ # black
+ # ipython
+ # mypy
+ # pylint
+urllib3==2.0.4 \
+ --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \
+ --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+watchdog==2.3.1 \
+ --hash=sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190 \
+ --hash=sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43 \
+ --hash=sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d \
+ --hash=sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256 \
+ --hash=sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd \
+ --hash=sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96 \
+ --hash=sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5 \
+ --hash=sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad \
+ --hash=sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42 \
+ --hash=sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225 \
+ --hash=sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb \
+ --hash=sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc \
+ --hash=sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79 \
+ --hash=sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf \
+ --hash=sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44 \
+ --hash=sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc \
+ --hash=sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131 \
+ --hash=sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c \
+ --hash=sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565 \
+ --hash=sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab \
+ --hash=sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed \
+ --hash=sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16 \
+ --hash=sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf \
+ --hash=sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375 \
+ --hash=sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697 \
+ --hash=sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906 \
+ --hash=sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b \
+ --hash=sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wcwidth==0.2.6 \
+ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # prompt-toolkit
+websockets==10.4 \
+ --hash=sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41 \
+ --hash=sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96 \
+ --hash=sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4 \
+ --hash=sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72 \
+ --hash=sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576 \
+ --hash=sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63 \
+ --hash=sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b \
+ --hash=sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d \
+ --hash=sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032 \
+ --hash=sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393 \
+ --hash=sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50 \
+ --hash=sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631 \
+ --hash=sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f \
+ --hash=sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c \
+ --hash=sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6 \
+ --hash=sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4 \
+ --hash=sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6 \
+ --hash=sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0 \
+ --hash=sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8 \
+ --hash=sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112 \
+ --hash=sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94 \
+ --hash=sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4 \
+ --hash=sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb \
+ --hash=sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331 \
+ --hash=sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c \
+ --hash=sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c \
+ --hash=sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193 \
+ --hash=sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b \
+ --hash=sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b \
+ --hash=sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038 \
+ --hash=sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089 \
+ --hash=sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa \
+ --hash=sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9 \
+ --hash=sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56 \
+ --hash=sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4 \
+ --hash=sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179 \
+ --hash=sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c \
+ --hash=sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882 \
+ --hash=sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28 \
+ --hash=sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1 \
+ --hash=sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a \
+ --hash=sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033 \
+ --hash=sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1 \
+ --hash=sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13 \
+ --hash=sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8 \
+ --hash=sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c \
+ --hash=sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74 \
+ --hash=sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab \
+ --hash=sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3 \
+ --hash=sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588 \
+ --hash=sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485 \
+ --hash=sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342 \
+ --hash=sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48 \
+ --hash=sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf \
+ --hash=sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0 \
+ --hash=sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a \
+ --hash=sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea \
+ --hash=sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf \
+ --hash=sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8 \
+ --hash=sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df \
+ --hash=sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc \
+ --hash=sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f \
+ --hash=sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269 \
+ --hash=sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3 \
+ --hash=sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c \
+ --hash=sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46 \
+ --hash=sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f \
+ --hash=sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106 \
+ --hash=sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wheel==0.40.0 \
+ --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
+ --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+yapf==0.31.0 \
+ --hash=sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d \
+ --hash=sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+zipp==3.16.2 \
+ --hash=sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0 \
+ --hash=sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # importlib-metadata
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.2.1 \
+ --hash=sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be \
+ --hash=sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+setuptools==68.0.0 \
+ --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \
+ --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+ # python-daemon
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_linux.list b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_linux.list
new file mode 100644
index 000000000..db05de24a
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_linux.list
@@ -0,0 +1,1171 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes --output-file=/constraint_hashes_linux.list --strip-extras /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+#
+alabaster==0.7.13 \
+ --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+ --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ptpython
+astroid==3.0.1 \
+ --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \
+ --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+asttokens==2.4.0 \
+ --hash=sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e \
+ --hash=sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+babel==2.12.1 \
+ --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+ --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+backcall==0.2.0 \
+ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
+ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+black==23.1.0 \
+ --hash=sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd \
+ --hash=sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555 \
+ --hash=sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481 \
+ --hash=sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468 \
+ --hash=sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9 \
+ --hash=sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a \
+ --hash=sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958 \
+ --hash=sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580 \
+ --hash=sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26 \
+ --hash=sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32 \
+ --hash=sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8 \
+ --hash=sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753 \
+ --hash=sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b \
+ --hash=sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074 \
+ --hash=sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651 \
+ --hash=sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24 \
+ --hash=sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6 \
+ --hash=sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad \
+ --hash=sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac \
+ --hash=sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221 \
+ --hash=sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06 \
+ --hash=sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27 \
+ --hash=sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648 \
+ --hash=sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739 \
+ --hash=sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+cachetools==5.0.0 \
+ --hash=sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6 \
+ --hash=sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cryptography
+charset-normalizer==3.2.0 \
+ --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
+ --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
+ --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
+ --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
+ --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
+ --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
+ --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
+ --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
+ --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
+ --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
+ --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
+ --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
+ --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
+ --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
+ --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
+ --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
+ --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
+ --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
+ --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
+ --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
+ --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
+ --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
+ --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
+ --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
+ --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
+ --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
+ --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
+ --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
+ --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
+ --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
+ --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
+ --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
+ --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
+ --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
+ --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
+ --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
+ --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
+ --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
+ --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
+ --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
+ --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
+ --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
+ --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
+ --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
+ --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
+ --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
+ --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
+ --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
+ --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
+ --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
+ --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
+ --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
+ --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
+ --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
+ --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
+ --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
+ --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
+ --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
+ --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
+ --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
+ --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
+ --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
+ --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
+ --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
+ --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
+ --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
+ --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
+ --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
+ --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
+ --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
+ --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
+ --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
+ --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
+ --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
+ --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pip-tools
+coloredlogs==15.0.1 \
+ --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \
+ --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+coverage==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+cryptography==41.0.2 \
+ --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
+ --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
+ --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
+ --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
+ --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
+ --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
+ --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
+ --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
+ --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
+ --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
+ --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
+ --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
+ --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
+ --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
+ --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
+ --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
+ --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
+ --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
+ --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
+ --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
+ --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
+ --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
+ --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+decorator==5.1.1 \
+ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
+ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+docutils==0.20.1 \
+ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \
+ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # python-daemon
+ # sphinx
+executing==2.0.0 \
+ --hash=sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657 \
+ --hash=sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+google-api-core==2.12.0 \
+ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \
+ --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-core
+ # google-cloud-storage
+google-auth==2.23.3 \
+ --hash=sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3 \
+ --hash=sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-core
+ # google-cloud-storage
+google-cloud-core==2.3.3 \
+ --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
+ --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+google-cloud-storage==2.12.0 \
+ --hash=sha256:57c0bcda2f5e11f008a155d8636d8381d5abab46b58e0cae0e46dd5e595e6b46 \
+ --hash=sha256:bc52563439d42981b6e21b071a76da2791672776eda3ba99d13a8061ebbd6e5e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+google-crc32c==1.5.0 \
+ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+ --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+ --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+ --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+ --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+ --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+ --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+ --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+ --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+ --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+ --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+ --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+ --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+ --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+ --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+ --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+ --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+ --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+ --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+ --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+ --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+ --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+ --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+ --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+ --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+ --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+ --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+ --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+ --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+ --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+ --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+ --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+ --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+ --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+ --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+ --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+ --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+ --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+ --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+ --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+ --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+ --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+ --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+ --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+ --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+ --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+ --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+ --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+ --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+ --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+ --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+ --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+ --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+ --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+ --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+ --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+ --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+ --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+ --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+ --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+ --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+ --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+ --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+ --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+ --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.6.0 \
+ --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \
+ --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+googleapis-common-protos==1.61.0 \
+ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
+ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+graphlib-backport==1.0.3 ; python_version < "3.9" \
+ --hash=sha256:24246967b9e7e6a91550bc770e6169585d35aa32790258579a8a3899a8c18fde \
+ --hash=sha256:7bb8fc7757b8ae4e6d8000a26cd49e9232aaa9a3aa57edb478474b8424bfaae2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+humanfriendly==10.0 \
+ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \
+ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # coloredlogs
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+imagesize==1.4.1 \
+ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+ipython==8.12.2 \
+ --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \
+ --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+jedi==0.19.1 \
+ --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \
+ --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+json5==0.9.11 \
+ --hash=sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd \
+ --hash=sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+lockfile==0.12.2 \
+ --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \
+ --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # python-daemon
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jinja2
+matplotlib-inline==0.1.3 \
+ --hash=sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee \
+ --hash=sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+mccabe==0.6.1 \
+ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
+ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+mypy==1.6.1 \
+ --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
+ --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
+ --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
+ --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
+ --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
+ --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
+ --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
+ --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
+ --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
+ --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
+ --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
+ --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
+ --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
+ --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
+ --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
+ --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
+ --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
+ --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
+ --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
+ --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
+ --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
+ --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
+ --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
+ --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
+ --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
+ --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
+ --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # mypy
+mypy-protobuf==3.5.0 \
+ --hash=sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393 \
+ --hash=sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # sphinx
+parameterized==0.9.0 \
+ --hash=sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b \
+ --hash=sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+parso==0.8.3 \
+ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
+ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jedi
+pathspec==0.11.1 \
+ --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \
+ --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+pexpect==4.8.0 \
+ --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
+ --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pickleshare==0.7.5 \
+ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
+ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pylint
+prompt-toolkit==3.0.39 \
+ --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
+ --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+protobuf==4.24.4 \
+ --hash=sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe \
+ --hash=sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085 \
+ --hash=sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b \
+ --hash=sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667 \
+ --hash=sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37 \
+ --hash=sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92 \
+ --hash=sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4 \
+ --hash=sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b \
+ --hash=sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9 \
+ --hash=sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd \
+ --hash=sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e \
+ --hash=sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46 \
+ --hash=sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # googleapis-common-protos
+ # mypy-protobuf
+psutil==5.9.4 \
+ --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+ --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+ --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+ --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+ --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+ --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+ --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+ --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+ --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+ --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+ --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+ --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+ --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+ --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptpython==3.0.23 \
+ --hash=sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c \
+ --hash=sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptyprocess==0.7.0 \
+ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
+ --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pexpect
+pure-eval==0.2.2 \
+ --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
+ --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+pyasn1==0.4.8 \
+ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
+ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.2.8 \
+ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
+ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cffi
+pyelftools==0.27 \
+ --hash=sha256:5609aa6da1123fccfae2e8431a67b4146aa7fad5b3889f808df12b110f230937 \
+ --hash=sha256:cde854e662774c5457d688ca41615f6594187ba7067af101232df889a6b7a66b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pygments==2.16.1 \
+ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
+ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+ # sphinx
+pylint==3.0.1 \
+ --hash=sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40 \
+ --hash=sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyperclip==1.8.2 \
+ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # build
+pyserial==3.5 \
+ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
+ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+python-daemon==3.0.1 \
+ --hash=sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341 \
+ --hash=sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pytz==2023.3 \
+ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
+ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # babel
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-storage
+ # sphinx
+rsa==4.8 \
+ --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \
+ --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # asttokens
+snowballstemmer==2.2.0 \
+ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinx==7.1.2 \
+ --hash=sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f \
+ --hash=sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx-argparse
+ # sphinx-design
+sphinx-argparse==0.4.0 \
+ --hash=sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037 \
+ --hash=sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-design==0.5.0 \
+ --hash=sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e \
+ --hash=sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-applehelp==1.0.4 \
+ --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+ --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+ --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+ --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-mermaid==0.9.2 \
+ --hash=sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af \
+ --hash=sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+stack-data==0.6.3 \
+ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
+ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+tomli==2.0.1 ; python_version < "3.11" \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.11.6 \
+ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
+ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+traitlets==5.1.1 \
+ --hash=sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7 \
+ --hash=sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # matplotlib-inline
+types-docutils==0.20.0.3 \
+ --hash=sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd \
+ --hash=sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-protobuf==4.24.0.2 \
+ --hash=sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f \
+ --hash=sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # mypy-protobuf
+types-pygments==2.16.0.0 \
+ --hash=sha256:4624a547d5ba73c971fac5d6fd327141e85e65f6123448bee76f0c8557652a71 \
+ --hash=sha256:aa93e4664e2d6cfea7570cde156e3966bf939f9c7d736cd179c4c8e94f7600b2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyserial==3.5.0.7 \
+ --hash=sha256:22b665a336539b85108e2f5d61e6cde1b59818eae78324c17bfe64edc1a2bd66 \
+ --hash=sha256:aa5d5536ea914ca9be85f496e0914b10a58bbb1d2b1f54ce756be45ecb65378b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyyaml==6.0.12.11 \
+ --hash=sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b \
+ --hash=sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-requests==2.31.0.2 \
+ --hash=sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a \
+ --hash=sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-setuptools==67.8.0.0 \
+ --hash=sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff \
+ --hash=sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-six==1.16.9 \
+ --hash=sha256:04da8bad920aea300e13d99c8948fd0914abdfc5d9421bad636137f9829dcbf6 \
+ --hash=sha256:da5748f10eec44accbd393e7f5a834991aff8f37522472166fbc8233cc735b84
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-urllib3==1.26.25.14 \
+ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \
+ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-requests
+typing-extensions==4.4.0 \
+ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
+ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # astroid
+ # black
+ # ipython
+ # mypy
+ # pylint
+urllib3==2.0.4 \
+ --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \
+ --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+watchdog==2.3.1 \
+ --hash=sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190 \
+ --hash=sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43 \
+ --hash=sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d \
+ --hash=sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256 \
+ --hash=sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd \
+ --hash=sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96 \
+ --hash=sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5 \
+ --hash=sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad \
+ --hash=sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42 \
+ --hash=sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225 \
+ --hash=sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb \
+ --hash=sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc \
+ --hash=sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79 \
+ --hash=sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf \
+ --hash=sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44 \
+ --hash=sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc \
+ --hash=sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131 \
+ --hash=sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c \
+ --hash=sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565 \
+ --hash=sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab \
+ --hash=sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed \
+ --hash=sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16 \
+ --hash=sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf \
+ --hash=sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375 \
+ --hash=sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697 \
+ --hash=sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906 \
+ --hash=sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b \
+ --hash=sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wcwidth==0.2.6 \
+ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # prompt-toolkit
+websockets==10.4 \
+ --hash=sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41 \
+ --hash=sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96 \
+ --hash=sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4 \
+ --hash=sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72 \
+ --hash=sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576 \
+ --hash=sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63 \
+ --hash=sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b \
+ --hash=sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d \
+ --hash=sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032 \
+ --hash=sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393 \
+ --hash=sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50 \
+ --hash=sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631 \
+ --hash=sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f \
+ --hash=sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c \
+ --hash=sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6 \
+ --hash=sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4 \
+ --hash=sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6 \
+ --hash=sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0 \
+ --hash=sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8 \
+ --hash=sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112 \
+ --hash=sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94 \
+ --hash=sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4 \
+ --hash=sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb \
+ --hash=sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331 \
+ --hash=sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c \
+ --hash=sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c \
+ --hash=sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193 \
+ --hash=sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b \
+ --hash=sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b \
+ --hash=sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038 \
+ --hash=sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089 \
+ --hash=sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa \
+ --hash=sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9 \
+ --hash=sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56 \
+ --hash=sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4 \
+ --hash=sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179 \
+ --hash=sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c \
+ --hash=sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882 \
+ --hash=sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28 \
+ --hash=sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1 \
+ --hash=sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a \
+ --hash=sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033 \
+ --hash=sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1 \
+ --hash=sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13 \
+ --hash=sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8 \
+ --hash=sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c \
+ --hash=sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74 \
+ --hash=sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab \
+ --hash=sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3 \
+ --hash=sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588 \
+ --hash=sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485 \
+ --hash=sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342 \
+ --hash=sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48 \
+ --hash=sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf \
+ --hash=sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0 \
+ --hash=sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a \
+ --hash=sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea \
+ --hash=sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf \
+ --hash=sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8 \
+ --hash=sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df \
+ --hash=sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc \
+ --hash=sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f \
+ --hash=sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269 \
+ --hash=sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3 \
+ --hash=sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c \
+ --hash=sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46 \
+ --hash=sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f \
+ --hash=sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106 \
+ --hash=sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wheel==0.40.0 \
+ --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
+ --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+yapf==0.31.0 \
+ --hash=sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d \
+ --hash=sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+zipp==3.16.2 \
+ --hash=sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0 \
+ --hash=sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # importlib-metadata
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.2.1 \
+ --hash=sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be \
+ --hash=sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+setuptools==68.0.0 \
+ --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \
+ --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+ # python-daemon
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_windows.list b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_windows.list
new file mode 100644
index 000000000..961f31bf6
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint_hashes_windows.list
@@ -0,0 +1,1175 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes --output-file='\constraint_hashes_windows.list' --strip-extras '\python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt'
+#
+alabaster==0.7.13 \
+ --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+ --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ptpython
+astroid==3.0.1 \
+ --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \
+ --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+asttokens==2.4.0 \
+ --hash=sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e \
+ --hash=sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # stack-data
+babel==2.12.1 \
+ --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+ --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+backcall==0.2.0 \
+ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
+ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+black==23.1.0 \
+ --hash=sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd \
+ --hash=sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555 \
+ --hash=sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481 \
+ --hash=sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468 \
+ --hash=sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9 \
+ --hash=sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a \
+ --hash=sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958 \
+ --hash=sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580 \
+ --hash=sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26 \
+ --hash=sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32 \
+ --hash=sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8 \
+ --hash=sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753 \
+ --hash=sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b \
+ --hash=sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074 \
+ --hash=sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651 \
+ --hash=sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24 \
+ --hash=sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6 \
+ --hash=sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad \
+ --hash=sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac \
+ --hash=sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221 \
+ --hash=sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06 \
+ --hash=sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27 \
+ --hash=sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648 \
+ --hash=sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739 \
+ --hash=sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+cachetools==5.0.0 \
+ --hash=sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6 \
+ --hash=sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-auth
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # cryptography
+charset-normalizer==3.2.0 \
+ --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
+ --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
+ --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
+ --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
+ --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
+ --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
+ --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
+ --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
+ --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
+ --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
+ --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
+ --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
+ --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
+ --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
+ --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
+ --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
+ --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
+ --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
+ --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
+ --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
+ --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
+ --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
+ --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
+ --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
+ --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
+ --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
+ --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
+ --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
+ --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
+ --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
+ --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
+ --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
+ --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
+ --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
+ --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
+ --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
+ --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
+ --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
+ --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
+ --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
+ --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
+ --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
+ --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
+ --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
+ --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
+ --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
+ --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
+ --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
+ --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
+ --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
+ --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
+ --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
+ --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
+ --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
+ --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
+ --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
+ --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
+ --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
+ --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
+ --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
+ --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
+ --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
+ --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
+ --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
+ --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
+ --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
+ --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
+ --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
+ --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
+ --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
+ --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
+ --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
+ --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
+ --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
+ --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # pip-tools
+colorama==0.4.6 \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # build
+ # click
+ # ipython
+ # pylint
+ # sphinx
+coloredlogs==15.0.1 \
+ --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \
+ --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+coverage==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+cryptography==41.0.2 \
+ --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
+ --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
+ --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
+ --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
+ --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
+ --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
+ --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
+ --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
+ --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
+ --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
+ --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
+ --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
+ --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
+ --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
+ --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
+ --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
+ --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
+ --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
+ --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
+ --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
+ --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
+ --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
+ --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+decorator==5.1.1 \
+ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
+ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+docutils==0.20.1 \
+ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \
+ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # python-daemon
+ # sphinx
+executing==2.0.0 \
+ --hash=sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657 \
+ --hash=sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # stack-data
+google-api-core==2.12.0 \
+ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \
+ --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-core
+ # google-cloud-storage
+google-auth==2.23.3 \
+ --hash=sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3 \
+ --hash=sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+ # google-cloud-core
+ # google-cloud-storage
+google-cloud-core==2.3.3 \
+ --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
+ --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-storage
+google-cloud-storage==2.12.0 \
+ --hash=sha256:57c0bcda2f5e11f008a155d8636d8381d5abab46b58e0cae0e46dd5e595e6b46 \
+ --hash=sha256:bc52563439d42981b6e21b071a76da2791672776eda3ba99d13a8061ebbd6e5e
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+google-crc32c==1.5.0 \
+ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+ --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+ --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+ --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+ --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+ --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+ --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+ --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+ --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+ --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+ --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+ --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+ --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+ --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+ --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+ --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+ --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+ --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+ --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+ --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+ --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+ --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+ --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+ --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+ --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+ --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+ --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+ --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+ --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+ --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+ --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+ --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+ --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+ --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+ --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+ --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+ --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+ --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+ --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+ --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+ --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+ --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+ --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+ --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+ --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+ --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+ --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+ --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+ --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+ --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+ --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+ --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+ --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+ --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+ --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+ --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+ --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+ --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+ --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+ --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+ --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+ --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+ --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+ --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+ --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.6.0 \
+ --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \
+ --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-storage
+googleapis-common-protos==1.61.0 \
+ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
+ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+graphlib-backport==1.0.3 ; python_version < "3.9" \
+ --hash=sha256:24246967b9e7e6a91550bc770e6169585d35aa32790258579a8a3899a8c18fde \
+ --hash=sha256:7bb8fc7757b8ae4e6d8000a26cd49e9232aaa9a3aa57edb478474b8424bfaae2
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+humanfriendly==10.0 \
+ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \
+ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # coloredlogs
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+imagesize==1.4.1 \
+ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+ipython==8.12.2 \
+ --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \
+ --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+jedi==0.19.1 \
+ --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \
+ --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # ptpython
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+json5==0.9.11 \
+ --hash=sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd \
+ --hash=sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+lockfile==0.12.2 \
+ --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \
+ --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # python-daemon
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # jinja2
+matplotlib-inline==0.1.3 \
+ --hash=sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee \
+ --hash=sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+mccabe==0.6.1 \
+ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
+ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+mypy==1.6.1 \
+ --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
+ --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
+ --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
+ --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
+ --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
+ --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
+ --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
+ --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
+ --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
+ --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
+ --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
+ --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
+ --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
+ --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
+ --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
+ --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
+ --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
+ --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
+ --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
+ --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
+ --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
+ --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
+ --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
+ --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
+ --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
+ --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
+ --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # mypy
+mypy-protobuf==3.5.0 \
+ --hash=sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393 \
+ --hash=sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # build
+ # sphinx
+parameterized==0.9.0 \
+ --hash=sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b \
+ --hash=sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+parso==0.8.3 \
+ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
+ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # jedi
+pathspec==0.11.1 \
+ --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \
+ --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+pickleshare==0.7.5 \
+ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
+ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # pylint
+prompt-toolkit==3.0.39 \
+ --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
+ --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # ptpython
+protobuf==4.24.4 \
+ --hash=sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe \
+ --hash=sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085 \
+ --hash=sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b \
+ --hash=sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667 \
+ --hash=sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37 \
+ --hash=sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92 \
+ --hash=sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4 \
+ --hash=sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b \
+ --hash=sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9 \
+ --hash=sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd \
+ --hash=sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e \
+ --hash=sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46 \
+ --hash=sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+ # googleapis-common-protos
+ # mypy-protobuf
+psutil==5.9.4 \
+ --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+ --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+ --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+ --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+ --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+ --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+ --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+ --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+ --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+ --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+ --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+ --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+ --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+ --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ptpython==3.0.23 \
+ --hash=sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c \
+ --hash=sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pure-eval==0.2.2 \
+ --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
+ --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # stack-data
+pyasn1==0.4.8 \
+ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
+ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.2.8 \
+ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
+ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-auth
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # cffi
+pyelftools==0.27 \
+ --hash=sha256:5609aa6da1123fccfae2e8431a67b4146aa7fad5b3889f808df12b110f230937 \
+ --hash=sha256:cde854e662774c5457d688ca41615f6594187ba7067af101232df889a6b7a66b
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pygments==2.16.1 \
+ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
+ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # ptpython
+ # sphinx
+pylint==3.0.1 \
+ --hash=sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40 \
+ --hash=sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pyperclip==1.8.2 \
+ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # build
+pyreadline3==3.4.1 \
+ --hash=sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae \
+ --hash=sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # humanfriendly
+pyserial==3.5 \
+ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
+ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+python-daemon==3.0.1 \
+ --hash=sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341 \
+ --hash=sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pytz==2023.3 \
+ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
+ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # babel
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+ # google-cloud-storage
+ # sphinx
+rsa==4.8 \
+ --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \
+ --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-auth
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # asttokens
+snowballstemmer==2.2.0 \
+ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinx==7.1.2 \
+ --hash=sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f \
+ --hash=sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx-argparse
+ # sphinx-design
+sphinx-argparse==0.4.0 \
+ --hash=sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037 \
+ --hash=sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinx-design==0.5.0 \
+ --hash=sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e \
+ --hash=sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinxcontrib-applehelp==1.0.4 \
+ --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+ --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+ --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+ --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-mermaid==0.9.2 \
+ --hash=sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af \
+ --hash=sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+stack-data==0.6.3 \
+ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
+ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+tomli==2.0.1 ; python_version < "3.11" \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # build
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.11.6 \
+ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
+ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+traitlets==5.1.1 \
+ --hash=sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7 \
+ --hash=sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # matplotlib-inline
+types-docutils==0.20.0.3 \
+ --hash=sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd \
+ --hash=sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # types-pygments
+types-protobuf==4.24.0.2 \
+ --hash=sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f \
+ --hash=sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # mypy-protobuf
+types-pygments==2.16.0.0 \
+ --hash=sha256:4624a547d5ba73c971fac5d6fd327141e85e65f6123448bee76f0c8557652a71 \
+ --hash=sha256:aa93e4664e2d6cfea7570cde156e3966bf939f9c7d736cd179c4c8e94f7600b2
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-pyserial==3.5.0.7 \
+ --hash=sha256:22b665a336539b85108e2f5d61e6cde1b59818eae78324c17bfe64edc1a2bd66 \
+ --hash=sha256:aa5d5536ea914ca9be85f496e0914b10a58bbb1d2b1f54ce756be45ecb65378b
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-pyyaml==6.0.12.11 \
+ --hash=sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b \
+ --hash=sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-requests==2.31.0.2 \
+ --hash=sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a \
+ --hash=sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-setuptools==67.8.0.0 \
+ --hash=sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff \
+ --hash=sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # types-pygments
+types-six==1.16.9 \
+ --hash=sha256:04da8bad920aea300e13d99c8948fd0914abdfc5d9421bad636137f9829dcbf6 \
+ --hash=sha256:da5748f10eec44accbd393e7f5a834991aff8f37522472166fbc8233cc735b84
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-urllib3==1.26.25.14 \
+ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \
+ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # types-requests
+typing-extensions==4.4.0 \
+ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
+ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # astroid
+ # black
+ # ipython
+ # mypy
+ # pylint
+urllib3==2.0.4 \
+ --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \
+ --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+watchdog==2.3.1 \
+ --hash=sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190 \
+ --hash=sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43 \
+ --hash=sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d \
+ --hash=sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256 \
+ --hash=sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd \
+ --hash=sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96 \
+ --hash=sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5 \
+ --hash=sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad \
+ --hash=sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42 \
+ --hash=sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225 \
+ --hash=sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb \
+ --hash=sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc \
+ --hash=sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79 \
+ --hash=sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf \
+ --hash=sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44 \
+ --hash=sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc \
+ --hash=sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131 \
+ --hash=sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c \
+ --hash=sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565 \
+ --hash=sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab \
+ --hash=sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed \
+ --hash=sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16 \
+ --hash=sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf \
+ --hash=sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375 \
+ --hash=sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697 \
+ --hash=sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906 \
+ --hash=sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b \
+ --hash=sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+wcwidth==0.2.6 \
+ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # prompt-toolkit
+websockets==10.4 \
+ --hash=sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41 \
+ --hash=sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96 \
+ --hash=sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4 \
+ --hash=sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72 \
+ --hash=sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576 \
+ --hash=sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63 \
+ --hash=sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b \
+ --hash=sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d \
+ --hash=sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032 \
+ --hash=sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393 \
+ --hash=sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50 \
+ --hash=sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631 \
+ --hash=sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f \
+ --hash=sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c \
+ --hash=sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6 \
+ --hash=sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4 \
+ --hash=sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6 \
+ --hash=sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0 \
+ --hash=sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8 \
+ --hash=sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112 \
+ --hash=sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94 \
+ --hash=sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4 \
+ --hash=sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb \
+ --hash=sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331 \
+ --hash=sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c \
+ --hash=sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c \
+ --hash=sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193 \
+ --hash=sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b \
+ --hash=sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b \
+ --hash=sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038 \
+ --hash=sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089 \
+ --hash=sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa \
+ --hash=sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9 \
+ --hash=sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56 \
+ --hash=sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4 \
+ --hash=sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179 \
+ --hash=sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c \
+ --hash=sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882 \
+ --hash=sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28 \
+ --hash=sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1 \
+ --hash=sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a \
+ --hash=sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033 \
+ --hash=sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1 \
+ --hash=sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13 \
+ --hash=sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8 \
+ --hash=sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c \
+ --hash=sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74 \
+ --hash=sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab \
+ --hash=sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3 \
+ --hash=sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588 \
+ --hash=sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485 \
+ --hash=sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342 \
+ --hash=sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48 \
+ --hash=sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf \
+ --hash=sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0 \
+ --hash=sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a \
+ --hash=sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea \
+ --hash=sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf \
+ --hash=sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8 \
+ --hash=sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df \
+ --hash=sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc \
+ --hash=sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f \
+ --hash=sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269 \
+ --hash=sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3 \
+ --hash=sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c \
+ --hash=sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46 \
+ --hash=sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f \
+ --hash=sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106 \
+ --hash=sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+wheel==0.40.0 \
+ --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
+ --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+yapf==0.31.0 \
+ --hash=sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d \
+ --hash=sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+zipp==3.16.2 \
+ --hash=sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0 \
+ --hash=sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # importlib-metadata
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.2.1 \
+ --hash=sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be \
+ --hash=sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+setuptools==68.0.0 \
+ --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \
+ --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+ # python-daemon
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py b/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
index f346d0557..7f4886cb9 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
@@ -32,6 +32,10 @@ import tempfile
_DATETIME_STRING = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
+def _is_windows() -> bool:
+ return platform.system().lower() == 'windows'
+
+
class GnTarget(object): # pylint: disable=useless-object-inheritance
def __init__(self, val):
self.directory, self.target = val.split('#', 1)
@@ -58,7 +62,7 @@ class GitRepoNotFound(Exception):
def _installed_packages(venv_python):
- cmd = (venv_python, '-m', 'pip', 'list', '--disable-pip-version-check')
+ cmd = (venv_python, '-m', 'pip', '--disable-pip-version-check', 'list')
output = subprocess.check_output(cmd).splitlines()
return set(x.split()[0].lower() for x in output[2:])
@@ -110,7 +114,7 @@ def _find_files_by_name(roots, name, allow_nesting=False):
def _check_venv(python, version, venv_path, pyvenv_cfg):
- if platform.system().lower() == 'windows':
+ if _is_windows():
return
# Check if the python location and version used for the existing virtualenv
@@ -126,14 +130,14 @@ def _check_venv(python, version, venv_path, pyvenv_cfg):
home = pyvenv_values.get('home')
if pydir != home and not pydir.startswith(venv_path):
shutil.rmtree(venv_path)
- elif pyvenv_values.get('version') not in version:
+ elif pyvenv_values.get('version') not in '.'.join(map(str, version)):
shutil.rmtree(venv_path)
def _check_python_install_permissions(python):
# These pickle files are not included on windows.
# The path on windows is environment/cipd/packages/python/bin/Lib/lib2to3/
- if platform.system().lower() == 'windows':
+ if _is_windows():
return
# Make any existing lib2to3 pickle files read+write. This is needed for
@@ -169,12 +173,32 @@ def _flatten(*items):
yield item
-def install( # pylint: disable=too-many-arguments,too-many-locals
+def _python_version(python_path: str):
+ """Returns the version (major, minor, rev) of the `python_path` binary."""
+ # Prints values like "3.10.0"
+ command = (
+ python_path,
+ '-c',
+ 'import sys; print(".".join(map(str, sys.version_info[:3])))',
+ )
+ version_str = (
+ subprocess.check_output(command, stderr=subprocess.STDOUT)
+ .strip()
+ .decode()
+ )
+ return tuple(map(int, version_str.split('.')))
+
+
+def install( # pylint: disable=too-many-arguments,too-many-locals,too-many-statements
project_root,
venv_path,
full_envsetup=True,
requirements=None,
constraints=None,
+ pip_install_disable_cache=None,
+ pip_install_find_links=None,
+ pip_install_offline=None,
+ pip_install_require_hashes=None,
gn_args=(),
gn_targets=(),
gn_out_dir=None,
@@ -185,12 +209,8 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
):
"""Creates a venv and installs all packages in this Git repo."""
- version = (
- subprocess.check_output((python, '--version'), stderr=subprocess.STDOUT)
- .strip()
- .decode()
- )
- if ' 3.' not in version:
+ version = _python_version(python)
+ if version[0] != 3:
print('=' * 60, file=sys.stderr)
print('Unexpected Python version:', version, file=sys.stderr)
print('=' * 60, file=sys.stderr)
@@ -207,12 +227,15 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
else:
env = contextlib.nullcontext()
- # Delete activation scripts. Typically they're created read-only and venv
- # will complain when trying to write over them fails.
- if os.path.isdir(venv_bin):
- for entry in os.listdir(venv_bin):
- if entry.lower().startswith('activate'):
- os.unlink(os.path.join(venv_bin, entry))
+ # Virtual environments may contain read-only files (notably activate
+ # scripts). `venv` calls below will fail if they are not writeable.
+ if os.path.isdir(venv_path):
+ for root, _dirs, files in os.walk(venv_path):
+ for file in files:
+ path = os.path.join(root, file)
+ mode = os.lstat(path).st_mode
+ if not (stat.S_ISLNK(mode) or (mode & stat.S_IWRITE)):
+ os.chmod(path, mode | stat.S_IWRITE)
pyvenv_cfg = os.path.join(venv_path, 'pyvenv.cfg')
@@ -231,7 +254,27 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
# TODO(spang): Pass --upgrade-deps and remove pip & setuptools
# upgrade below. This can only be done once the minimum python
# version is at least 3.9.
- cmd = [python, '-m', 'venv', '--upgrade']
+ cmd = [python, '-m', 'venv']
+
+ # Windows requires strange wizardry, and must follow symlinks
+ # starting with 3.11.
+ #
+ # Without this, windows fails bootstrap trying to copy
+ # "environment\cipd\packages\python\bin\venvlauncher.exe"
+ #
+ # This file doesn't exist in Python 3.11 on Windows and may be a bug
+ # in venv. Pigweed already uses symlinks on Windows for the GN build,
+ # so adding this option is not an issue.
+ #
+ # Further excitement is had when trying to update a virtual environment
+ # that is created using symlinks under Windows. `venv` will fail with
+ # and error that the source and destination are the same file. To work
+ # around this, we run `venv` in `--clear` mode under Windows.
+ if _is_windows() and version >= (3, 11):
+ cmd += ['--clear', '--symlinks']
+ else:
+ cmd += ['--upgrade']
+
cmd += ['--system-site-packages'] if system_packages else []
cmd += [venv_path]
_check_call(cmd, env=envcopy)
@@ -255,10 +298,33 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
):
os.unlink(egg_link)
+ pip_install_args = []
+ if pip_install_find_links:
+ for package_dir in pip_install_find_links:
+ pip_install_args.append('--find-links')
+ with env():
+ pip_install_args.append(os.path.expandvars(package_dir))
+ if pip_install_require_hashes:
+ pip_install_args.append('--require-hashes')
+ if pip_install_offline:
+ pip_install_args.append('--no-index')
+ if pip_install_disable_cache:
+ pip_install_args.append('--no-cache-dir')
+
def pip_install(*args):
args = list(_flatten(args))
with env():
- cmd = [venv_python, '-m', 'pip', 'install'] + args
+ cmd = (
+ [
+ venv_python,
+ '-m',
+ 'pip',
+ '--disable-pip-version-check',
+ 'install',
+ ]
+ + pip_install_args
+ + args
+ )
return _check_call(cmd)
constraint_args = []
@@ -277,6 +343,7 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
# Include wheel so pip installs can be done without build
# isolation.
'wheel',
+ 'pip-tools',
constraint_args,
)
@@ -301,7 +368,7 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
def install_packages(gn_target):
if gn_out_dir is None:
- build_dir = os.path.join(venv_path, 'gn-install-dir')
+ build_dir = os.path.join(venv_path, 'gn')
else:
build_dir = gn_out_dir
@@ -360,7 +427,13 @@ def install( # pylint: disable=too-many-arguments,too-many-locals
with open(os.path.join(venv_path, 'pip-list.log'), 'w') as outs:
subprocess.check_call(
- [venv_python, '-m', 'pip', 'list'],
+ [
+ venv_python,
+ '-m',
+ 'pip',
+ '--disable-pip-version-check',
+ 'list',
+ ],
stdout=outs,
)
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
index fcb9baf65..9989f8469 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
@@ -14,10 +14,15 @@
# Pigweed upstream specific depenencies:
# pigweed.dev Sphinx themes
-furo==2022.12.7
+beautifulsoup4==4.12.2
+furo==2023.8.19
sphinx-copybutton==0.5.1
-myst-parser==0.18.1
-breathe==4.34.0
+breathe==4.35.0
+kconfiglib==14.1.0
+pytz==2023.3
+sphinx-sitemap==2.5.1
# Renode requirements
psutil==5.9.4
-robotframework==5.0.1
+robotframework==6.0.2
+# RP2040 utils.
+pyusb==1.2.1
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/python_base_requirements.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/python_base_requirements.txt
new file mode 100644
index 000000000..eb737e7fc
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/python_base_requirements.txt
@@ -0,0 +1,9 @@
+build==0.10.0
+click==8.1.3
+packaging==23.0
+pip-tools==7.3.0
+pip==23.2.1
+pyproject-hooks==1.0.0
+setuptools==68.0.0
+tomli==2.0.1; python_version < "3.11"
+wheel==0.40.0
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_darwin_lock.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_darwin_lock.txt
new file mode 100644
index 000000000..386a39720
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_darwin_lock.txt
@@ -0,0 +1,1230 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes --output-file=/upstream_requirements_darwin_lock.txt --strip-extras /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+#
+alabaster==0.7.13 \
+ --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+ --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ptpython
+appnope==0.1.3 \
+ --hash=sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24 \
+ --hash=sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+astroid==3.0.1 \
+ --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \
+ --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+asttokens==2.4.0 \
+ --hash=sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e \
+ --hash=sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+babel==2.12.1 \
+ --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+ --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+backcall==0.2.0 \
+ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
+ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+beautifulsoup4==4.12.2 \
+ --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \
+ --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # furo
+black==23.1.0 \
+ --hash=sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd \
+ --hash=sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555 \
+ --hash=sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481 \
+ --hash=sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468 \
+ --hash=sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9 \
+ --hash=sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a \
+ --hash=sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958 \
+ --hash=sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580 \
+ --hash=sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26 \
+ --hash=sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32 \
+ --hash=sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8 \
+ --hash=sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753 \
+ --hash=sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b \
+ --hash=sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074 \
+ --hash=sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651 \
+ --hash=sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24 \
+ --hash=sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6 \
+ --hash=sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad \
+ --hash=sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac \
+ --hash=sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221 \
+ --hash=sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06 \
+ --hash=sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27 \
+ --hash=sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648 \
+ --hash=sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739 \
+ --hash=sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+breathe==4.35.0 \
+ --hash=sha256:5165541c3c67b6c7adde8b3ecfe895c6f7844783c4076b6d8d287e4f33d62386 \
+ --hash=sha256:52c581f42ca4310737f9e435e3851c3d1f15446205a85fbc272f1f97ed74f5be
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+cachetools==5.0.0 \
+ --hash=sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6 \
+ --hash=sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cryptography
+charset-normalizer==3.2.0 \
+ --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
+ --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
+ --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
+ --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
+ --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
+ --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
+ --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
+ --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
+ --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
+ --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
+ --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
+ --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
+ --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
+ --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
+ --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
+ --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
+ --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
+ --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
+ --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
+ --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
+ --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
+ --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
+ --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
+ --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
+ --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
+ --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
+ --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
+ --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
+ --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
+ --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
+ --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
+ --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
+ --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
+ --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
+ --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
+ --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
+ --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
+ --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
+ --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
+ --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
+ --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
+ --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
+ --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
+ --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
+ --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
+ --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
+ --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
+ --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
+ --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
+ --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
+ --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
+ --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
+ --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
+ --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
+ --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
+ --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
+ --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
+ --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
+ --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
+ --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
+ --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
+ --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
+ --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
+ --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
+ --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
+ --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
+ --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
+ --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
+ --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
+ --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
+ --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
+ --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
+ --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
+ --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
+ --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pip-tools
+coloredlogs==15.0.1 \
+ --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \
+ --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+coverage==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+cryptography==41.0.2 \
+ --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
+ --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
+ --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
+ --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
+ --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
+ --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
+ --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
+ --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
+ --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
+ --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
+ --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
+ --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
+ --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
+ --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
+ --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
+ --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
+ --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
+ --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
+ --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
+ --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
+ --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
+ --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
+ --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+decorator==5.1.1 \
+ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
+ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+docutils==0.20.1 \
+ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \
+ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # breathe
+ # python-daemon
+ # sphinx
+executing==2.0.0 \
+ --hash=sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657 \
+ --hash=sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+furo==2023.8.19 \
+ --hash=sha256:12f99f87a1873b6746228cfde18f77244e6c1ffb85d7fed95e638aae70d80590 \
+ --hash=sha256:e671ee638ab3f1b472f4033b0167f502ab407830e0db0f843b1c1028119c9cd1
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+google-api-core==2.12.0 \
+ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \
+ --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-core
+ # google-cloud-storage
+google-auth==2.23.3 \
+ --hash=sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3 \
+ --hash=sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-core
+ # google-cloud-storage
+google-cloud-core==2.3.3 \
+ --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
+ --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+google-cloud-storage==2.12.0 \
+ --hash=sha256:57c0bcda2f5e11f008a155d8636d8381d5abab46b58e0cae0e46dd5e595e6b46 \
+ --hash=sha256:bc52563439d42981b6e21b071a76da2791672776eda3ba99d13a8061ebbd6e5e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+google-crc32c==1.5.0 \
+ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+ --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+ --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+ --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+ --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+ --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+ --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+ --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+ --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+ --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+ --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+ --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+ --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+ --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+ --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+ --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+ --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+ --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+ --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+ --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+ --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+ --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+ --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+ --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+ --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+ --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+ --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+ --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+ --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+ --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+ --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+ --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+ --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+ --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+ --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+ --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+ --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+ --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+ --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+ --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+ --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+ --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+ --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+ --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+ --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+ --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+ --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+ --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+ --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+ --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+ --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+ --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+ --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+ --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+ --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+ --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+ --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+ --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+ --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+ --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+ --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+ --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+ --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+ --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+ --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.6.0 \
+ --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \
+ --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+googleapis-common-protos==1.61.0 \
+ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
+ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+graphlib-backport==1.0.3 ; python_version < "3.9" \
+ --hash=sha256:24246967b9e7e6a91550bc770e6169585d35aa32790258579a8a3899a8c18fde \
+ --hash=sha256:7bb8fc7757b8ae4e6d8000a26cd49e9232aaa9a3aa57edb478474b8424bfaae2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+humanfriendly==10.0 \
+ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \
+ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # coloredlogs
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+imagesize==1.4.1 \
+ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+ipython==8.12.2 \
+ --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \
+ --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+jedi==0.19.1 \
+ --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \
+ --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+json5==0.9.11 \
+ --hash=sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd \
+ --hash=sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+kconfiglib==14.1.0 \
+ --hash=sha256:bed2cc2216f538eca4255a83a4588d8823563cdd50114f86cf1a2674e602c93c \
+ --hash=sha256:edcd35a20e7e138a9a9e96149027f805f785e818d2eae400b6fa8b0c8845114a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+lockfile==0.12.2 \
+ --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \
+ --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # python-daemon
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jinja2
+matplotlib-inline==0.1.3 \
+ --hash=sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee \
+ --hash=sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+mccabe==0.6.1 \
+ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
+ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+mypy==1.6.1 \
+ --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
+ --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
+ --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
+ --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
+ --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
+ --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
+ --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
+ --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
+ --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
+ --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
+ --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
+ --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
+ --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
+ --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
+ --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
+ --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
+ --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
+ --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
+ --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
+ --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
+ --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
+ --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
+ --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
+ --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
+ --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
+ --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
+ --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # mypy
+mypy-protobuf==3.5.0 \
+ --hash=sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393 \
+ --hash=sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # sphinx
+parameterized==0.9.0 \
+ --hash=sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b \
+ --hash=sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+parso==0.8.3 \
+ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
+ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jedi
+pathspec==0.11.1 \
+ --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \
+ --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+pexpect==4.8.0 \
+ --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
+ --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pickleshare==0.7.5 \
+ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
+ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pylint
+prompt-toolkit==3.0.39 \
+ --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
+ --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+protobuf==4.24.4 \
+ --hash=sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe \
+ --hash=sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085 \
+ --hash=sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b \
+ --hash=sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667 \
+ --hash=sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37 \
+ --hash=sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92 \
+ --hash=sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4 \
+ --hash=sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b \
+ --hash=sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9 \
+ --hash=sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd \
+ --hash=sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e \
+ --hash=sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46 \
+ --hash=sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # googleapis-common-protos
+ # mypy-protobuf
+psutil==5.9.4 \
+ --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+ --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+ --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+ --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+ --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+ --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+ --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+ --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+ --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+ --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+ --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+ --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+ --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+ --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptpython==3.0.23 \
+ --hash=sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c \
+ --hash=sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptyprocess==0.7.0 \
+ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
+ --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pexpect
+pure-eval==0.2.2 \
+ --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
+ --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+pyasn1==0.4.8 \
+ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
+ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.2.8 \
+ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
+ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cffi
+pyelftools==0.27 \
+ --hash=sha256:5609aa6da1123fccfae2e8431a67b4146aa7fad5b3889f808df12b110f230937 \
+ --hash=sha256:cde854e662774c5457d688ca41615f6594187ba7067af101232df889a6b7a66b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pygments==2.16.1 \
+ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
+ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # furo
+ # ipython
+ # ptpython
+ # sphinx
+pylint==3.0.1 \
+ --hash=sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40 \
+ --hash=sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyperclip==1.8.2 \
+ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # build
+pyserial==3.5 \
+ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
+ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+python-daemon==3.0.1 \
+ --hash=sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341 \
+ --hash=sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pytz==2023.3 \
+ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
+ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # babel
+pyusb==1.2.1 \
+ --hash=sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36 \
+ --hash=sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-storage
+ # sphinx
+robotframework==6.0.2 \
+ --hash=sha256:634cd6f9fdc21142eadd9aacdddbfe7d3e2a757532b71d866dbb404cd0e66dac \
+ --hash=sha256:6a9c06deb220099990f190c6e4e772675f625e4d5d84640fca6f0ad46ff538d0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+rsa==4.8 \
+ --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \
+ --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # asttokens
+snowballstemmer==2.2.0 \
+ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+soupsieve==2.5 \
+ --hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \
+ --hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # beautifulsoup4
+sphinx==7.1.2 \
+ --hash=sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f \
+ --hash=sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # breathe
+ # furo
+ # sphinx-argparse
+ # sphinx-basic-ng
+ # sphinx-copybutton
+ # sphinx-design
+ # sphinx-sitemap
+sphinx-argparse==0.4.0 \
+ --hash=sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037 \
+ --hash=sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-basic-ng==1.0.0b2 \
+ --hash=sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9 \
+ --hash=sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # furo
+sphinx-copybutton==0.5.1 \
+ --hash=sha256:0842851b5955087a7ec7fc870b622cb168618ad408dee42692e9a5c97d071da8 \
+ --hash=sha256:366251e28a6f6041514bfb5439425210418d6c750e98d3a695b73e56866a677a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-design==0.5.0 \
+ --hash=sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e \
+ --hash=sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-sitemap==2.5.1 \
+ --hash=sha256:0b7bce2835f287687f75584d7695e4eb8efaec028e5e7b36e9f791de3c344686 \
+ --hash=sha256:984bef068bbdbc26cfae209a8b61392e9681abc9191b477cd30da406e3a60ee5
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-applehelp==1.0.4 \
+ --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+ --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+ --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+ --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-mermaid==0.9.2 \
+ --hash=sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af \
+ --hash=sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+stack-data==0.6.3 \
+ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
+ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+tomli==2.0.1 ; python_version < "3.11" \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.11.6 \
+ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
+ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+traitlets==5.1.1 \
+ --hash=sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7 \
+ --hash=sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # matplotlib-inline
+types-docutils==0.20.0.3 \
+ --hash=sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd \
+ --hash=sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-protobuf==4.24.0.2 \
+ --hash=sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f \
+ --hash=sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # mypy-protobuf
+types-pygments==2.16.0.0 \
+ --hash=sha256:4624a547d5ba73c971fac5d6fd327141e85e65f6123448bee76f0c8557652a71 \
+ --hash=sha256:aa93e4664e2d6cfea7570cde156e3966bf939f9c7d736cd179c4c8e94f7600b2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyserial==3.5.0.7 \
+ --hash=sha256:22b665a336539b85108e2f5d61e6cde1b59818eae78324c17bfe64edc1a2bd66 \
+ --hash=sha256:aa5d5536ea914ca9be85f496e0914b10a58bbb1d2b1f54ce756be45ecb65378b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyyaml==6.0.12.11 \
+ --hash=sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b \
+ --hash=sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-requests==2.31.0.2 \
+ --hash=sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a \
+ --hash=sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-setuptools==67.8.0.0 \
+ --hash=sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff \
+ --hash=sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-six==1.16.9 \
+ --hash=sha256:04da8bad920aea300e13d99c8948fd0914abdfc5d9421bad636137f9829dcbf6 \
+ --hash=sha256:da5748f10eec44accbd393e7f5a834991aff8f37522472166fbc8233cc735b84
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-urllib3==1.26.25.14 \
+ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \
+ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-requests
+typing-extensions==4.4.0 \
+ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
+ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # astroid
+ # black
+ # ipython
+ # mypy
+ # pylint
+urllib3==2.0.4 \
+ --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \
+ --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+watchdog==2.3.1 \
+ --hash=sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190 \
+ --hash=sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43 \
+ --hash=sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d \
+ --hash=sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256 \
+ --hash=sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd \
+ --hash=sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96 \
+ --hash=sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5 \
+ --hash=sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad \
+ --hash=sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42 \
+ --hash=sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225 \
+ --hash=sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb \
+ --hash=sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc \
+ --hash=sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79 \
+ --hash=sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf \
+ --hash=sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44 \
+ --hash=sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc \
+ --hash=sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131 \
+ --hash=sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c \
+ --hash=sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565 \
+ --hash=sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab \
+ --hash=sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed \
+ --hash=sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16 \
+ --hash=sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf \
+ --hash=sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375 \
+ --hash=sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697 \
+ --hash=sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906 \
+ --hash=sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b \
+ --hash=sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wcwidth==0.2.6 \
+ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # prompt-toolkit
+websockets==10.4 \
+ --hash=sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41 \
+ --hash=sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96 \
+ --hash=sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4 \
+ --hash=sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72 \
+ --hash=sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576 \
+ --hash=sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63 \
+ --hash=sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b \
+ --hash=sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d \
+ --hash=sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032 \
+ --hash=sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393 \
+ --hash=sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50 \
+ --hash=sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631 \
+ --hash=sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f \
+ --hash=sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c \
+ --hash=sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6 \
+ --hash=sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4 \
+ --hash=sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6 \
+ --hash=sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0 \
+ --hash=sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8 \
+ --hash=sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112 \
+ --hash=sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94 \
+ --hash=sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4 \
+ --hash=sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb \
+ --hash=sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331 \
+ --hash=sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c \
+ --hash=sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c \
+ --hash=sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193 \
+ --hash=sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b \
+ --hash=sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b \
+ --hash=sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038 \
+ --hash=sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089 \
+ --hash=sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa \
+ --hash=sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9 \
+ --hash=sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56 \
+ --hash=sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4 \
+ --hash=sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179 \
+ --hash=sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c \
+ --hash=sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882 \
+ --hash=sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28 \
+ --hash=sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1 \
+ --hash=sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a \
+ --hash=sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033 \
+ --hash=sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1 \
+ --hash=sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13 \
+ --hash=sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8 \
+ --hash=sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c \
+ --hash=sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74 \
+ --hash=sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab \
+ --hash=sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3 \
+ --hash=sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588 \
+ --hash=sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485 \
+ --hash=sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342 \
+ --hash=sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48 \
+ --hash=sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf \
+ --hash=sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0 \
+ --hash=sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a \
+ --hash=sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea \
+ --hash=sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf \
+ --hash=sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8 \
+ --hash=sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df \
+ --hash=sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc \
+ --hash=sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f \
+ --hash=sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269 \
+ --hash=sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3 \
+ --hash=sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c \
+ --hash=sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46 \
+ --hash=sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f \
+ --hash=sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106 \
+ --hash=sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wheel==0.40.0 \
+ --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
+ --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+yapf==0.31.0 \
+ --hash=sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d \
+ --hash=sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+zipp==3.16.2 \
+ --hash=sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0 \
+ --hash=sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # importlib-metadata
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.2.1 \
+ --hash=sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be \
+ --hash=sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+setuptools==68.0.0 \
+ --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \
+ --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+ # python-daemon
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_linux_lock.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_linux_lock.txt
new file mode 100644
index 000000000..8c2afe95f
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_linux_lock.txt
@@ -0,0 +1,1224 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes --output-file=/upstream_requirements_linux_lock.txt --strip-extras /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+#
+alabaster==0.7.13 \
+ --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+ --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ptpython
+astroid==3.0.1 \
+ --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \
+ --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+asttokens==2.4.0 \
+ --hash=sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e \
+ --hash=sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+babel==2.12.1 \
+ --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+ --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+backcall==0.2.0 \
+ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
+ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+beautifulsoup4==4.12.2 \
+ --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \
+ --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # furo
+black==23.1.0 \
+ --hash=sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd \
+ --hash=sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555 \
+ --hash=sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481 \
+ --hash=sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468 \
+ --hash=sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9 \
+ --hash=sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a \
+ --hash=sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958 \
+ --hash=sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580 \
+ --hash=sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26 \
+ --hash=sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32 \
+ --hash=sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8 \
+ --hash=sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753 \
+ --hash=sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b \
+ --hash=sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074 \
+ --hash=sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651 \
+ --hash=sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24 \
+ --hash=sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6 \
+ --hash=sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad \
+ --hash=sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac \
+ --hash=sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221 \
+ --hash=sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06 \
+ --hash=sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27 \
+ --hash=sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648 \
+ --hash=sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739 \
+ --hash=sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+breathe==4.35.0 \
+ --hash=sha256:5165541c3c67b6c7adde8b3ecfe895c6f7844783c4076b6d8d287e4f33d62386 \
+ --hash=sha256:52c581f42ca4310737f9e435e3851c3d1f15446205a85fbc272f1f97ed74f5be
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+cachetools==5.0.0 \
+ --hash=sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6 \
+ --hash=sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cryptography
+charset-normalizer==3.2.0 \
+ --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
+ --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
+ --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
+ --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
+ --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
+ --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
+ --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
+ --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
+ --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
+ --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
+ --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
+ --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
+ --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
+ --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
+ --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
+ --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
+ --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
+ --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
+ --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
+ --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
+ --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
+ --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
+ --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
+ --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
+ --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
+ --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
+ --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
+ --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
+ --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
+ --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
+ --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
+ --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
+ --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
+ --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
+ --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
+ --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
+ --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
+ --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
+ --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
+ --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
+ --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
+ --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
+ --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
+ --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
+ --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
+ --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
+ --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
+ --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
+ --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
+ --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
+ --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
+ --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
+ --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
+ --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
+ --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
+ --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
+ --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
+ --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
+ --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
+ --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
+ --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
+ --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
+ --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
+ --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
+ --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
+ --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
+ --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
+ --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
+ --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
+ --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
+ --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
+ --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
+ --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
+ --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
+ --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pip-tools
+coloredlogs==15.0.1 \
+ --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \
+ --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+coverage==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+cryptography==41.0.2 \
+ --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
+ --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
+ --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
+ --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
+ --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
+ --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
+ --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
+ --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
+ --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
+ --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
+ --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
+ --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
+ --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
+ --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
+ --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
+ --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
+ --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
+ --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
+ --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
+ --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
+ --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
+ --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
+ --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+decorator==5.1.1 \
+ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
+ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+docutils==0.20.1 \
+ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \
+ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # breathe
+ # python-daemon
+ # sphinx
+executing==2.0.0 \
+ --hash=sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657 \
+ --hash=sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+furo==2023.8.19 \
+ --hash=sha256:12f99f87a1873b6746228cfde18f77244e6c1ffb85d7fed95e638aae70d80590 \
+ --hash=sha256:e671ee638ab3f1b472f4033b0167f502ab407830e0db0f843b1c1028119c9cd1
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+google-api-core==2.12.0 \
+ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \
+ --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-core
+ # google-cloud-storage
+google-auth==2.23.3 \
+ --hash=sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3 \
+ --hash=sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-core
+ # google-cloud-storage
+google-cloud-core==2.3.3 \
+ --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
+ --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+google-cloud-storage==2.12.0 \
+ --hash=sha256:57c0bcda2f5e11f008a155d8636d8381d5abab46b58e0cae0e46dd5e595e6b46 \
+ --hash=sha256:bc52563439d42981b6e21b071a76da2791672776eda3ba99d13a8061ebbd6e5e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+google-crc32c==1.5.0 \
+ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+ --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+ --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+ --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+ --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+ --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+ --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+ --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+ --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+ --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+ --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+ --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+ --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+ --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+ --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+ --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+ --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+ --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+ --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+ --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+ --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+ --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+ --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+ --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+ --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+ --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+ --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+ --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+ --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+ --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+ --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+ --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+ --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+ --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+ --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+ --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+ --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+ --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+ --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+ --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+ --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+ --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+ --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+ --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+ --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+ --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+ --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+ --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+ --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+ --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+ --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+ --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+ --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+ --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+ --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+ --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+ --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+ --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+ --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+ --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+ --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+ --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+ --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+ --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+ --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.6.0 \
+ --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \
+ --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-cloud-storage
+googleapis-common-protos==1.61.0 \
+ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
+ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+graphlib-backport==1.0.3 ; python_version < "3.9" \
+ --hash=sha256:24246967b9e7e6a91550bc770e6169585d35aa32790258579a8a3899a8c18fde \
+ --hash=sha256:7bb8fc7757b8ae4e6d8000a26cd49e9232aaa9a3aa57edb478474b8424bfaae2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+humanfriendly==10.0 \
+ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \
+ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # coloredlogs
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+imagesize==1.4.1 \
+ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+ipython==8.12.2 \
+ --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \
+ --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+jedi==0.19.1 \
+ --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \
+ --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+json5==0.9.11 \
+ --hash=sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd \
+ --hash=sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+kconfiglib==14.1.0 \
+ --hash=sha256:bed2cc2216f538eca4255a83a4588d8823563cdd50114f86cf1a2674e602c93c \
+ --hash=sha256:edcd35a20e7e138a9a9e96149027f805f785e818d2eae400b6fa8b0c8845114a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+lockfile==0.12.2 \
+ --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \
+ --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # python-daemon
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jinja2
+matplotlib-inline==0.1.3 \
+ --hash=sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee \
+ --hash=sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+mccabe==0.6.1 \
+ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
+ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+mypy==1.6.1 \
+ --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
+ --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
+ --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
+ --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
+ --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
+ --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
+ --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
+ --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
+ --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
+ --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
+ --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
+ --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
+ --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
+ --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
+ --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
+ --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
+ --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
+ --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
+ --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
+ --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
+ --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
+ --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
+ --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
+ --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
+ --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
+ --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
+ --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # mypy
+mypy-protobuf==3.5.0 \
+ --hash=sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393 \
+ --hash=sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # sphinx
+parameterized==0.9.0 \
+ --hash=sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b \
+ --hash=sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+parso==0.8.3 \
+ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
+ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # jedi
+pathspec==0.11.1 \
+ --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \
+ --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+pexpect==4.8.0 \
+ --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \
+ --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pickleshare==0.7.5 \
+ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
+ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # pylint
+prompt-toolkit==3.0.39 \
+ --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
+ --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # ptpython
+protobuf==4.24.4 \
+ --hash=sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe \
+ --hash=sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085 \
+ --hash=sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b \
+ --hash=sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667 \
+ --hash=sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37 \
+ --hash=sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92 \
+ --hash=sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4 \
+ --hash=sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b \
+ --hash=sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9 \
+ --hash=sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd \
+ --hash=sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e \
+ --hash=sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46 \
+ --hash=sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # googleapis-common-protos
+ # mypy-protobuf
+psutil==5.9.4 \
+ --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+ --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+ --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+ --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+ --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+ --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+ --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+ --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+ --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+ --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+ --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+ --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+ --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+ --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptpython==3.0.23 \
+ --hash=sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c \
+ --hash=sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ptyprocess==0.7.0 \
+ --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \
+ --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pexpect
+pure-eval==0.2.2 \
+ --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
+ --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # stack-data
+pyasn1==0.4.8 \
+ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
+ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.2.8 \
+ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
+ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # cffi
+pyelftools==0.27 \
+ --hash=sha256:5609aa6da1123fccfae2e8431a67b4146aa7fad5b3889f808df12b110f230937 \
+ --hash=sha256:cde854e662774c5457d688ca41615f6594187ba7067af101232df889a6b7a66b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pygments==2.16.1 \
+ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
+ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # furo
+ # ipython
+ # ptpython
+ # sphinx
+pylint==3.0.1 \
+ --hash=sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40 \
+ --hash=sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyperclip==1.8.2 \
+ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # build
+pyserial==3.5 \
+ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
+ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+python-daemon==3.0.1 \
+ --hash=sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341 \
+ --hash=sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pytz==2023.3 \
+ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
+ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # babel
+pyusb==1.2.1 \
+ --hash=sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36 \
+ --hash=sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-api-core
+ # google-cloud-storage
+ # sphinx
+robotframework==6.0.2 \
+ --hash=sha256:634cd6f9fdc21142eadd9aacdddbfe7d3e2a757532b71d866dbb404cd0e66dac \
+ --hash=sha256:6a9c06deb220099990f190c6e4e772675f625e4d5d84640fca6f0ad46ff538d0
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+rsa==4.8 \
+ --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \
+ --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # google-auth
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # asttokens
+snowballstemmer==2.2.0 \
+ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+soupsieve==2.5 \
+ --hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \
+ --hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # beautifulsoup4
+sphinx==7.1.2 \
+ --hash=sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f \
+ --hash=sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # breathe
+ # furo
+ # sphinx-argparse
+ # sphinx-basic-ng
+ # sphinx-copybutton
+ # sphinx-design
+ # sphinx-sitemap
+sphinx-argparse==0.4.0 \
+ --hash=sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037 \
+ --hash=sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-basic-ng==1.0.0b2 \
+ --hash=sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9 \
+ --hash=sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # furo
+sphinx-copybutton==0.5.1 \
+ --hash=sha256:0842851b5955087a7ec7fc870b622cb168618ad408dee42692e9a5c97d071da8 \
+ --hash=sha256:366251e28a6f6041514bfb5439425210418d6c750e98d3a695b73e56866a677a
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-design==0.5.0 \
+ --hash=sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e \
+ --hash=sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinx-sitemap==2.5.1 \
+ --hash=sha256:0b7bce2835f287687f75584d7695e4eb8efaec028e5e7b36e9f791de3c344686 \
+ --hash=sha256:984bef068bbdbc26cfae209a8b61392e9681abc9191b477cd30da406e3a60ee5
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-applehelp==1.0.4 \
+ --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+ --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+ --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+ --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-mermaid==0.9.2 \
+ --hash=sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af \
+ --hash=sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # sphinx
+stack-data==0.6.3 \
+ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
+ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+tomli==2.0.1 ; python_version < "3.11" \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # black
+ # build
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.11.6 \
+ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
+ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pylint
+traitlets==5.1.1 \
+ --hash=sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7 \
+ --hash=sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # ipython
+ # matplotlib-inline
+types-docutils==0.20.0.3 \
+ --hash=sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd \
+ --hash=sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-protobuf==4.24.0.2 \
+ --hash=sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f \
+ --hash=sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # mypy-protobuf
+types-pygments==2.16.0.0 \
+ --hash=sha256:4624a547d5ba73c971fac5d6fd327141e85e65f6123448bee76f0c8557652a71 \
+ --hash=sha256:aa93e4664e2d6cfea7570cde156e3966bf939f9c7d736cd179c4c8e94f7600b2
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyserial==3.5.0.7 \
+ --hash=sha256:22b665a336539b85108e2f5d61e6cde1b59818eae78324c17bfe64edc1a2bd66 \
+ --hash=sha256:aa5d5536ea914ca9be85f496e0914b10a58bbb1d2b1f54ce756be45ecb65378b
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-pyyaml==6.0.12.11 \
+ --hash=sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b \
+ --hash=sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-requests==2.31.0.2 \
+ --hash=sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a \
+ --hash=sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-setuptools==67.8.0.0 \
+ --hash=sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff \
+ --hash=sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-pygments
+types-six==1.16.9 \
+ --hash=sha256:04da8bad920aea300e13d99c8948fd0914abdfc5d9421bad636137f9829dcbf6 \
+ --hash=sha256:da5748f10eec44accbd393e7f5a834991aff8f37522472166fbc8233cc735b84
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+types-urllib3==1.26.25.14 \
+ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \
+ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # types-requests
+typing-extensions==4.4.0 \
+ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
+ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # astroid
+ # black
+ # ipython
+ # mypy
+ # pylint
+urllib3==2.0.4 \
+ --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \
+ --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # requests
+watchdog==2.3.1 \
+ --hash=sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190 \
+ --hash=sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43 \
+ --hash=sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d \
+ --hash=sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256 \
+ --hash=sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd \
+ --hash=sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96 \
+ --hash=sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5 \
+ --hash=sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad \
+ --hash=sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42 \
+ --hash=sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225 \
+ --hash=sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb \
+ --hash=sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc \
+ --hash=sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79 \
+ --hash=sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf \
+ --hash=sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44 \
+ --hash=sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc \
+ --hash=sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131 \
+ --hash=sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c \
+ --hash=sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565 \
+ --hash=sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab \
+ --hash=sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed \
+ --hash=sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16 \
+ --hash=sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf \
+ --hash=sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375 \
+ --hash=sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697 \
+ --hash=sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906 \
+ --hash=sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b \
+ --hash=sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wcwidth==0.2.6 \
+ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # prompt-toolkit
+websockets==10.4 \
+ --hash=sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41 \
+ --hash=sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96 \
+ --hash=sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4 \
+ --hash=sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72 \
+ --hash=sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576 \
+ --hash=sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63 \
+ --hash=sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b \
+ --hash=sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d \
+ --hash=sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032 \
+ --hash=sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393 \
+ --hash=sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50 \
+ --hash=sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631 \
+ --hash=sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f \
+ --hash=sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c \
+ --hash=sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6 \
+ --hash=sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4 \
+ --hash=sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6 \
+ --hash=sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0 \
+ --hash=sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8 \
+ --hash=sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112 \
+ --hash=sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94 \
+ --hash=sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4 \
+ --hash=sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb \
+ --hash=sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331 \
+ --hash=sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c \
+ --hash=sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c \
+ --hash=sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193 \
+ --hash=sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b \
+ --hash=sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b \
+ --hash=sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038 \
+ --hash=sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089 \
+ --hash=sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa \
+ --hash=sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9 \
+ --hash=sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56 \
+ --hash=sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4 \
+ --hash=sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179 \
+ --hash=sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c \
+ --hash=sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882 \
+ --hash=sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28 \
+ --hash=sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1 \
+ --hash=sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a \
+ --hash=sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033 \
+ --hash=sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1 \
+ --hash=sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13 \
+ --hash=sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8 \
+ --hash=sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c \
+ --hash=sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74 \
+ --hash=sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab \
+ --hash=sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3 \
+ --hash=sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588 \
+ --hash=sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485 \
+ --hash=sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342 \
+ --hash=sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48 \
+ --hash=sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf \
+ --hash=sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0 \
+ --hash=sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a \
+ --hash=sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea \
+ --hash=sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf \
+ --hash=sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8 \
+ --hash=sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df \
+ --hash=sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc \
+ --hash=sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f \
+ --hash=sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269 \
+ --hash=sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3 \
+ --hash=sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c \
+ --hash=sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46 \
+ --hash=sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f \
+ --hash=sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106 \
+ --hash=sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+wheel==0.40.0 \
+ --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
+ --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+yapf==0.31.0 \
+ --hash=sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d \
+ --hash=sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e
+ # via -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+zipp==3.16.2 \
+ --hash=sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0 \
+ --hash=sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # importlib-metadata
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.2.1 \
+ --hash=sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be \
+ --hash=sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+setuptools==68.0.0 \
+ --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \
+ --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235
+ # via
+ # -r /python/gen/pw_env_setup/pigweed_build_venv/compiled_requirements.txt
+ # pip-tools
+ # python-daemon
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_windows_lock.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_windows_lock.txt
new file mode 100644
index 000000000..37fbbc8c2
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/upstream_requirements_windows_lock.txt
@@ -0,0 +1,1228 @@
+#
+# This file is autogenerated by pip-compile with Python 3.8
+# by the following command:
+#
+# pip-compile --allow-unsafe --generate-hashes --output-file='\upstream_requirements_windows_lock.txt' --strip-extras '\python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt'
+#
+alabaster==0.7.13 \
+ --hash=sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3 \
+ --hash=sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+appdirs==1.4.4 \
+ --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \
+ --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ptpython
+astroid==3.0.1 \
+ --hash=sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca \
+ --hash=sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+asttokens==2.4.0 \
+ --hash=sha256:2e0171b991b2c959acc6c49318049236844a5da1d65ba2672c4880c1c894834e \
+ --hash=sha256:cf8fc9e61a86461aa9fb161a14a0841a03c405fa829ac6b202670b3495d2ce69
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # stack-data
+babel==2.12.1 \
+ --hash=sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610 \
+ --hash=sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+backcall==0.2.0 \
+ --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \
+ --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+beautifulsoup4==4.12.2 \
+ --hash=sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da \
+ --hash=sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # furo
+black==23.1.0 \
+ --hash=sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd \
+ --hash=sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555 \
+ --hash=sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481 \
+ --hash=sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468 \
+ --hash=sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9 \
+ --hash=sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a \
+ --hash=sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958 \
+ --hash=sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580 \
+ --hash=sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26 \
+ --hash=sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32 \
+ --hash=sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8 \
+ --hash=sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753 \
+ --hash=sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b \
+ --hash=sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074 \
+ --hash=sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651 \
+ --hash=sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24 \
+ --hash=sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6 \
+ --hash=sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad \
+ --hash=sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac \
+ --hash=sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221 \
+ --hash=sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06 \
+ --hash=sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27 \
+ --hash=sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648 \
+ --hash=sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739 \
+ --hash=sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+breathe==4.35.0 \
+ --hash=sha256:5165541c3c67b6c7adde8b3ecfe895c6f7844783c4076b6d8d287e4f33d62386 \
+ --hash=sha256:52c581f42ca4310737f9e435e3851c3d1f15446205a85fbc272f1f97ed74f5be
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+build==0.10.0 \
+ --hash=sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171 \
+ --hash=sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+cachetools==5.0.0 \
+ --hash=sha256:486471dfa8799eb7ec503a8059e263db000cdda20075ce5e48903087f79d5fd6 \
+ --hash=sha256:8fecd4203a38af17928be7b90689d8083603073622229ca7077b72d8e5a976e4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-auth
+certifi==2021.10.8 \
+ --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \
+ --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+cffi==1.15.1 \
+ --hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
+ --hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
+ --hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
+ --hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
+ --hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
+ --hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
+ --hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
+ --hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
+ --hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
+ --hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
+ --hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
+ --hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
+ --hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
+ --hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
+ --hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
+ --hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
+ --hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
+ --hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
+ --hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
+ --hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
+ --hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
+ --hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
+ --hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
+ --hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
+ --hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
+ --hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
+ --hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
+ --hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
+ --hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
+ --hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
+ --hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
+ --hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
+ --hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
+ --hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
+ --hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
+ --hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
+ --hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
+ --hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
+ --hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
+ --hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
+ --hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
+ --hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
+ --hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
+ --hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
+ --hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
+ --hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
+ --hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
+ --hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
+ --hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
+ --hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
+ --hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
+ --hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
+ --hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
+ --hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
+ --hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
+ --hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
+ --hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
+ --hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
+ --hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
+ --hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
+ --hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
+ --hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
+ --hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
+ --hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # cryptography
+charset-normalizer==3.2.0 \
+ --hash=sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96 \
+ --hash=sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c \
+ --hash=sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710 \
+ --hash=sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706 \
+ --hash=sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020 \
+ --hash=sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252 \
+ --hash=sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad \
+ --hash=sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329 \
+ --hash=sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a \
+ --hash=sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f \
+ --hash=sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6 \
+ --hash=sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4 \
+ --hash=sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a \
+ --hash=sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46 \
+ --hash=sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2 \
+ --hash=sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23 \
+ --hash=sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace \
+ --hash=sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd \
+ --hash=sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982 \
+ --hash=sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10 \
+ --hash=sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2 \
+ --hash=sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea \
+ --hash=sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09 \
+ --hash=sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5 \
+ --hash=sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149 \
+ --hash=sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489 \
+ --hash=sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9 \
+ --hash=sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80 \
+ --hash=sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592 \
+ --hash=sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3 \
+ --hash=sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6 \
+ --hash=sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed \
+ --hash=sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c \
+ --hash=sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200 \
+ --hash=sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a \
+ --hash=sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e \
+ --hash=sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d \
+ --hash=sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6 \
+ --hash=sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623 \
+ --hash=sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669 \
+ --hash=sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3 \
+ --hash=sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa \
+ --hash=sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9 \
+ --hash=sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2 \
+ --hash=sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f \
+ --hash=sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1 \
+ --hash=sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4 \
+ --hash=sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a \
+ --hash=sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8 \
+ --hash=sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3 \
+ --hash=sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029 \
+ --hash=sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f \
+ --hash=sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959 \
+ --hash=sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22 \
+ --hash=sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7 \
+ --hash=sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952 \
+ --hash=sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346 \
+ --hash=sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e \
+ --hash=sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d \
+ --hash=sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299 \
+ --hash=sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd \
+ --hash=sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a \
+ --hash=sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3 \
+ --hash=sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037 \
+ --hash=sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94 \
+ --hash=sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c \
+ --hash=sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858 \
+ --hash=sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a \
+ --hash=sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449 \
+ --hash=sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c \
+ --hash=sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918 \
+ --hash=sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1 \
+ --hash=sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c \
+ --hash=sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac \
+ --hash=sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+click==8.1.3 \
+ --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \
+ --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # pip-tools
+colorama==0.4.6 \
+ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # build
+ # click
+ # ipython
+ # pylint
+ # sphinx
+coloredlogs==15.0.1 \
+ --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934 \
+ --hash=sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+coverage==7.2.7 \
+ --hash=sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f \
+ --hash=sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2 \
+ --hash=sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a \
+ --hash=sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a \
+ --hash=sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01 \
+ --hash=sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6 \
+ --hash=sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7 \
+ --hash=sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f \
+ --hash=sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02 \
+ --hash=sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c \
+ --hash=sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063 \
+ --hash=sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a \
+ --hash=sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5 \
+ --hash=sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959 \
+ --hash=sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97 \
+ --hash=sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6 \
+ --hash=sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f \
+ --hash=sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9 \
+ --hash=sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5 \
+ --hash=sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f \
+ --hash=sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562 \
+ --hash=sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe \
+ --hash=sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9 \
+ --hash=sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f \
+ --hash=sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb \
+ --hash=sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb \
+ --hash=sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1 \
+ --hash=sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb \
+ --hash=sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250 \
+ --hash=sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e \
+ --hash=sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511 \
+ --hash=sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5 \
+ --hash=sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59 \
+ --hash=sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2 \
+ --hash=sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d \
+ --hash=sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3 \
+ --hash=sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4 \
+ --hash=sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de \
+ --hash=sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9 \
+ --hash=sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833 \
+ --hash=sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0 \
+ --hash=sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9 \
+ --hash=sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d \
+ --hash=sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050 \
+ --hash=sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d \
+ --hash=sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6 \
+ --hash=sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353 \
+ --hash=sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb \
+ --hash=sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e \
+ --hash=sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8 \
+ --hash=sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495 \
+ --hash=sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2 \
+ --hash=sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd \
+ --hash=sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27 \
+ --hash=sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1 \
+ --hash=sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818 \
+ --hash=sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4 \
+ --hash=sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e \
+ --hash=sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850 \
+ --hash=sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+cryptography==41.0.2 \
+ --hash=sha256:01f1d9e537f9a15b037d5d9ee442b8c22e3ae11ce65ea1f3316a41c78756b711 \
+ --hash=sha256:079347de771f9282fbfe0e0236c716686950c19dee1b76240ab09ce1624d76d7 \
+ --hash=sha256:182be4171f9332b6741ee818ec27daff9fb00349f706629f5cbf417bd50e66fd \
+ --hash=sha256:192255f539d7a89f2102d07d7375b1e0a81f7478925b3bc2e0549ebf739dae0e \
+ --hash=sha256:2a034bf7d9ca894720f2ec1d8b7b5832d7e363571828037f9e0c4f18c1b58a58 \
+ --hash=sha256:342f3767e25876751e14f8459ad85e77e660537ca0a066e10e75df9c9e9099f0 \
+ --hash=sha256:439c3cc4c0d42fa999b83ded80a9a1fb54d53c58d6e59234cfe97f241e6c781d \
+ --hash=sha256:49c3222bb8f8e800aead2e376cbef687bc9e3cb9b58b29a261210456a7783d83 \
+ --hash=sha256:674b669d5daa64206c38e507808aae49904c988fa0a71c935e7006a3e1e83831 \
+ --hash=sha256:7a9a3bced53b7f09da251685224d6a260c3cb291768f54954e28f03ef14e3766 \
+ --hash=sha256:7af244b012711a26196450d34f483357e42aeddb04128885d95a69bd8b14b69b \
+ --hash=sha256:7d230bf856164de164ecb615ccc14c7fc6de6906ddd5b491f3af90d3514c925c \
+ --hash=sha256:84609ade00a6ec59a89729e87a503c6e36af98ddcd566d5f3be52e29ba993182 \
+ --hash=sha256:9a6673c1828db6270b76b22cc696f40cde9043eb90373da5c2f8f2158957f42f \
+ --hash=sha256:9b6d717393dbae53d4e52684ef4f022444fc1cce3c48c38cb74fca29e1f08eaa \
+ --hash=sha256:9c3fe6534d59d071ee82081ca3d71eed3210f76ebd0361798c74abc2bcf347d4 \
+ --hash=sha256:a719399b99377b218dac6cf547b6ec54e6ef20207b6165126a280b0ce97e0d2a \
+ --hash=sha256:b332cba64d99a70c1e0836902720887fb4529ea49ea7f5462cf6640e095e11d2 \
+ --hash=sha256:d124682c7a23c9764e54ca9ab5b308b14b18eba02722b8659fb238546de83a76 \
+ --hash=sha256:d73f419a56d74fef257955f51b18d046f3506270a5fd2ac5febbfa259d6c0fa5 \
+ --hash=sha256:f0dc40e6f7aa37af01aba07277d3d64d5a03dc66d682097541ec4da03cc140ee \
+ --hash=sha256:f14ad275364c8b4e525d018f6716537ae7b6d369c094805cae45300847e0894f \
+ --hash=sha256:f772610fe364372de33d76edcd313636a25684edb94cee53fd790195f5989d14
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+decorator==5.1.1 \
+ --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \
+ --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+dill==0.3.6 \
+ --hash=sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0 \
+ --hash=sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+docutils==0.20.1 \
+ --hash=sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6 \
+ --hash=sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # breathe
+ # python-daemon
+ # sphinx
+executing==2.0.0 \
+ --hash=sha256:06df6183df67389625f4e763921c6cf978944721abf3e714000200aab95b0657 \
+ --hash=sha256:0ff053696fdeef426cda5bd18eacd94f82c91f49823a2e9090124212ceea9b08
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # stack-data
+furo==2023.8.19 \
+ --hash=sha256:12f99f87a1873b6746228cfde18f77244e6c1ffb85d7fed95e638aae70d80590 \
+ --hash=sha256:e671ee638ab3f1b472f4033b0167f502ab407830e0db0f843b1c1028119c9cd1
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+google-api-core==2.12.0 \
+ --hash=sha256:c22e01b1e3c4dcd90998494879612c38d0a3411d1f7b679eb89e2abe3ce1f553 \
+ --hash=sha256:ec6054f7d64ad13b41e43d96f735acbd763b0f3b695dabaa2d579673f6a6e160
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-core
+ # google-cloud-storage
+google-auth==2.23.3 \
+ --hash=sha256:6864247895eea5d13b9c57c9e03abb49cb94ce2dc7c58e91cba3248c7477c9e3 \
+ --hash=sha256:a8f4608e65c244ead9e0538f181a96c6e11199ec114d41f1d7b1bffa96937bda
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+ # google-cloud-core
+ # google-cloud-storage
+google-cloud-core==2.3.3 \
+ --hash=sha256:37b80273c8d7eee1ae816b3a20ae43585ea50506cb0e60f3cf5be5f87f1373cb \
+ --hash=sha256:fbd11cad3e98a7e5b0343dc07cb1039a5ffd7a5bb96e1f1e27cee4bda4a90863
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-storage
+google-cloud-storage==2.12.0 \
+ --hash=sha256:57c0bcda2f5e11f008a155d8636d8381d5abab46b58e0cae0e46dd5e595e6b46 \
+ --hash=sha256:bc52563439d42981b6e21b071a76da2791672776eda3ba99d13a8061ebbd6e5e
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+google-crc32c==1.5.0 \
+ --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \
+ --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \
+ --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \
+ --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \
+ --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \
+ --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \
+ --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \
+ --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \
+ --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \
+ --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \
+ --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \
+ --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \
+ --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \
+ --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \
+ --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \
+ --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \
+ --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \
+ --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \
+ --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \
+ --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \
+ --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \
+ --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \
+ --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \
+ --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \
+ --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \
+ --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \
+ --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \
+ --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \
+ --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \
+ --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \
+ --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \
+ --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \
+ --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \
+ --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \
+ --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \
+ --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \
+ --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \
+ --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \
+ --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \
+ --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \
+ --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \
+ --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \
+ --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \
+ --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \
+ --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \
+ --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \
+ --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \
+ --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \
+ --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \
+ --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \
+ --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \
+ --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \
+ --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \
+ --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \
+ --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \
+ --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \
+ --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \
+ --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \
+ --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \
+ --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \
+ --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \
+ --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \
+ --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \
+ --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \
+ --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \
+ --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \
+ --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \
+ --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-storage
+ # google-resumable-media
+google-resumable-media==2.6.0 \
+ --hash=sha256:972852f6c65f933e15a4a210c2b96930763b47197cdf4aa5f5bea435efb626e7 \
+ --hash=sha256:fc03d344381970f79eebb632a3c18bb1828593a2dc5572b5f90115ef7d11e81b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-cloud-storage
+googleapis-common-protos==1.61.0 \
+ --hash=sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0 \
+ --hash=sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+graphlib-backport==1.0.3 ; python_version < "3.9" \
+ --hash=sha256:24246967b9e7e6a91550bc770e6169585d35aa32790258579a8a3899a8c18fde \
+ --hash=sha256:7bb8fc7757b8ae4e6d8000a26cd49e9232aaa9a3aa57edb478474b8424bfaae2
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+humanfriendly==10.0 \
+ --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477 \
+ --hash=sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # coloredlogs
+idna==3.4 \
+ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
+ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+imagesize==1.4.1 \
+ --hash=sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b \
+ --hash=sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+importlib-metadata==6.8.0 \
+ --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
+ --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+ipython==8.12.2 \
+ --hash=sha256:c7b80eb7f5a855a88efc971fda506ff7a91c280b42cdae26643e0f601ea281ea \
+ --hash=sha256:ea8801f15dfe4ffb76dea1b09b847430ffd70d827b41735c64a0638a04103bfc
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+isort==5.10.1 \
+ --hash=sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7 \
+ --hash=sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+jedi==0.19.1 \
+ --hash=sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd \
+ --hash=sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # ptpython
+jinja2==3.1.2 \
+ --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
+ --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+json5==0.9.11 \
+ --hash=sha256:1aa54b80b5e507dfe31d12b7743a642e2ffa6f70bf73b8e3d7d1d5fba83d99bd \
+ --hash=sha256:4f1e196acc55b83985a51318489f345963c7ba84aa37607e49073066c562e99b
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+kconfiglib==14.1.0 \
+ --hash=sha256:bed2cc2216f538eca4255a83a4588d8823563cdd50114f86cf1a2674e602c93c \
+ --hash=sha256:edcd35a20e7e138a9a9e96149027f805f785e818d2eae400b6fa8b0c8845114a
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+lockfile==0.12.2 \
+ --hash=sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799 \
+ --hash=sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # python-daemon
+markupsafe==2.1.3 \
+ --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
+ --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
+ --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
+ --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
+ --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
+ --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
+ --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
+ --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
+ --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
+ --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
+ --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
+ --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
+ --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
+ --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
+ --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
+ --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
+ --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
+ --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
+ --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
+ --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
+ --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
+ --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
+ --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
+ --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
+ --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
+ --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
+ --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
+ --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
+ --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
+ --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
+ --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
+ --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
+ --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
+ --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
+ --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
+ --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
+ --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
+ --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
+ --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
+ --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
+ --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
+ --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
+ --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
+ --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
+ --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
+ --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
+ --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
+ --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
+ --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
+ --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
+ --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
+ --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
+ --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
+ --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
+ --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
+ --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
+ --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
+ --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
+ --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
+ --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # jinja2
+matplotlib-inline==0.1.3 \
+ --hash=sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee \
+ --hash=sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+mccabe==0.6.1 \
+ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
+ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+mypy==1.6.1 \
+ --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
+ --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
+ --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
+ --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
+ --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
+ --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
+ --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
+ --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
+ --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
+ --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
+ --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
+ --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
+ --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
+ --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
+ --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
+ --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
+ --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
+ --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
+ --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
+ --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
+ --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
+ --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
+ --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
+ --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
+ --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
+ --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
+ --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+mypy-extensions==1.0.0 \
+ --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
+ --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # mypy
+mypy-protobuf==3.5.0 \
+ --hash=sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393 \
+ --hash=sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+packaging==23.0 \
+ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
+ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # build
+ # sphinx
+parameterized==0.9.0 \
+ --hash=sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b \
+ --hash=sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+parso==0.8.3 \
+ --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \
+ --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # jedi
+pathspec==0.11.1 \
+ --hash=sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687 \
+ --hash=sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+pickleshare==0.7.5 \
+ --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \
+ --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+pip-tools==7.3.0 \
+ --hash=sha256:8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e \
+ --hash=sha256:8e9c99127fe024c025b46a0b2d15c7bd47f18f33226cf7330d35493663fc1d1d
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+platformdirs==3.0.0 \
+ --hash=sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9 \
+ --hash=sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # pylint
+prompt-toolkit==3.0.39 \
+ --hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
+ --hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # ptpython
+protobuf==4.24.4 \
+ --hash=sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe \
+ --hash=sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085 \
+ --hash=sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b \
+ --hash=sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667 \
+ --hash=sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37 \
+ --hash=sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92 \
+ --hash=sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4 \
+ --hash=sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b \
+ --hash=sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9 \
+ --hash=sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd \
+ --hash=sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e \
+ --hash=sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46 \
+ --hash=sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+ # googleapis-common-protos
+ # mypy-protobuf
+psutil==5.9.4 \
+ --hash=sha256:149555f59a69b33f056ba1c4eb22bb7bf24332ce631c44a319cec09f876aaeff \
+ --hash=sha256:16653106f3b59386ffe10e0bad3bb6299e169d5327d3f187614b1cb8f24cf2e1 \
+ --hash=sha256:3d7f9739eb435d4b1338944abe23f49584bde5395f27487d2ee25ad9a8774a62 \
+ --hash=sha256:3ff89f9b835100a825b14c2808a106b6fdcc4b15483141482a12c725e7f78549 \
+ --hash=sha256:54c0d3d8e0078b7666984e11b12b88af2db11d11249a8ac8920dd5ef68a66e08 \
+ --hash=sha256:54d5b184728298f2ca8567bf83c422b706200bcbbfafdc06718264f9393cfeb7 \
+ --hash=sha256:6001c809253a29599bc0dfd5179d9f8a5779f9dffea1da0f13c53ee568115e1e \
+ --hash=sha256:68908971daf802203f3d37e78d3f8831b6d1014864d7a85937941bb35f09aefe \
+ --hash=sha256:6b92c532979bafc2df23ddc785ed116fced1f492ad90a6830cf24f4d1ea27d24 \
+ --hash=sha256:852dd5d9f8a47169fe62fd4a971aa07859476c2ba22c2254d4a1baa4e10b95ad \
+ --hash=sha256:9120cd39dca5c5e1c54b59a41d205023d436799b1c8c4d3ff71af18535728e94 \
+ --hash=sha256:c1ca331af862803a42677c120aff8a814a804e09832f166f226bfd22b56feee8 \
+ --hash=sha256:efeae04f9516907be44904cc7ce08defb6b665128992a56957abc9b61dca94b7 \
+ --hash=sha256:fd8522436a6ada7b4aad6638662966de0d61d241cb821239b2ae7013d41a43d4
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ptpython==3.0.23 \
+ --hash=sha256:51069503684169b21e1980734a9ba2e104643b7e6a50d3ca0e5669ea70d9e21c \
+ --hash=sha256:9fc9bec2cc51bc4000c1224d8c56241ce8a406b3d49ec8dc266f78cd3cd04ba4
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pure-eval==0.2.2 \
+ --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \
+ --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # stack-data
+pyasn1==0.4.8 \
+ --hash=sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d \
+ --hash=sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pyasn1-modules
+ # rsa
+pyasn1-modules==0.2.8 \
+ --hash=sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e \
+ --hash=sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-auth
+pycparser==2.21 \
+ --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
+ --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # cffi
+pyelftools==0.27 \
+ --hash=sha256:5609aa6da1123fccfae2e8431a67b4146aa7fad5b3889f808df12b110f230937 \
+ --hash=sha256:cde854e662774c5457d688ca41615f6594187ba7067af101232df889a6b7a66b
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pygments==2.16.1 \
+ --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
+ --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # furo
+ # ipython
+ # ptpython
+ # sphinx
+pylint==3.0.1 \
+ --hash=sha256:81c6125637be216b4652ae50cc42b9f8208dfb725cdc7e04c48f6902f4dbdf40 \
+ --hash=sha256:9c90b89e2af7809a1697f6f5f93f1d0e518ac566e2ac4d2af881a69c13ad01ea
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pyperclip==1.8.2 \
+ --hash=sha256:105254a8b04934f0bc84e9c24eb360a591aaf6535c9def5f29d92af107a9bf57
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pyproject-hooks==1.0.0 \
+ --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \
+ --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # build
+pyreadline3==3.4.1 \
+ --hash=sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae \
+ --hash=sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # humanfriendly
+pyserial==3.5 \
+ --hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
+ --hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+python-daemon==3.0.1 \
+ --hash=sha256:42bb848a3260a027fa71ad47ecd959e471327cb34da5965962edd5926229f341 \
+ --hash=sha256:6c57452372f7eaff40934a1c03ad1826bf5e793558e87fef49131e6464b4dae5
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pytz==2023.3 \
+ --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
+ --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # babel
+pyusb==1.2.1 \
+ --hash=sha256:2b4c7cb86dbadf044dfb9d3a4ff69fd217013dbe78a792177a3feb172449ea36 \
+ --hash=sha256:a4cc7404a203144754164b8b40994e2849fde1cfff06b08492f12fff9d9de7b9
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+pyyaml==6.0.1 \
+ --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+ --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+ --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+ --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+ --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+ --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+ --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+ --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+ --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+ --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+ --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+ --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+ --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+ --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+ --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+ --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+ --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+ --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+ --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+ --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+ --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+ --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+ --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+ --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+ --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+ --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+ --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+ --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+ --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+ --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+ --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+ --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+ --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+ --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+ --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+ --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+ --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+ --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+ --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+ --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+ --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+ --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+ --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+ --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+ --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+ --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+ --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+ --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+ --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+ --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+requests==2.31.0 \
+ --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
+ --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-api-core
+ # google-cloud-storage
+ # sphinx
+robotframework==6.0.2 \
+ --hash=sha256:634cd6f9fdc21142eadd9aacdddbfe7d3e2a757532b71d866dbb404cd0e66dac \
+ --hash=sha256:6a9c06deb220099990f190c6e4e772675f625e4d5d84640fca6f0ad46ff538d0
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+rsa==4.8 \
+ --hash=sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17 \
+ --hash=sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # google-auth
+six==1.16.0 \
+ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
+ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # asttokens
+snowballstemmer==2.2.0 \
+ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \
+ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+soupsieve==2.5 \
+ --hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \
+ --hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # beautifulsoup4
+sphinx==7.1.2 \
+ --hash=sha256:780f4d32f1d7d1126576e0e5ecc19dc32ab76cd24e950228dcf7b1f6d3d9e22f \
+ --hash=sha256:d170a81825b2fcacb6dfd5a0d7f578a053e45d3f2b153fecc948c37344eb4cbe
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # breathe
+ # furo
+ # sphinx-argparse
+ # sphinx-basic-ng
+ # sphinx-copybutton
+ # sphinx-design
+ # sphinx-sitemap
+sphinx-argparse==0.4.0 \
+ --hash=sha256:73bee01f7276fae2bf621ccfe4d167af7306e7288e3482005405d9f826f9b037 \
+ --hash=sha256:e0f34184eb56f12face774fbc87b880abdb9017a0998d1ec559b267e9697e449
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinx-basic-ng==1.0.0b2 \
+ --hash=sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9 \
+ --hash=sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # furo
+sphinx-copybutton==0.5.1 \
+ --hash=sha256:0842851b5955087a7ec7fc870b622cb168618ad408dee42692e9a5c97d071da8 \
+ --hash=sha256:366251e28a6f6041514bfb5439425210418d6c750e98d3a695b73e56866a677a
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinx-design==0.5.0 \
+ --hash=sha256:1af1267b4cea2eedd6724614f19dcc88fe2e15aff65d06b2f6252cee9c4f4c1e \
+ --hash=sha256:e8e513acea6f92d15c6de3b34e954458f245b8e761b45b63950f65373352ab00
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinx-sitemap==2.5.1 \
+ --hash=sha256:0b7bce2835f287687f75584d7695e4eb8efaec028e5e7b36e9f791de3c344686 \
+ --hash=sha256:984bef068bbdbc26cfae209a8b61392e9681abc9191b477cd30da406e3a60ee5
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinxcontrib-applehelp==1.0.4 \
+ --hash=sha256:29d341f67fb0f6f586b23ad80e072c8e6ad0b48417db2bde114a4c9746feb228 \
+ --hash=sha256:828f867945bbe39817c210a1abfd1bc4895c8b73fcaade56d45357a348a07d7e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-devhelp==1.0.2 \
+ --hash=sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e \
+ --hash=sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-htmlhelp==2.0.1 \
+ --hash=sha256:0cbdd302815330058422b98a113195c9249825d681e18f11e8b1f78a2f11efff \
+ --hash=sha256:c38cb46dccf316c79de6e5515e1770414b797162b23cd3d06e67020e1d2a6903
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-jsmath==1.0.1 \
+ --hash=sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178 \
+ --hash=sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-mermaid==0.9.2 \
+ --hash=sha256:252ef13dd23164b28f16d8b0205cf184b9d8e2b714a302274d9f59eb708e77af \
+ --hash=sha256:6795a72037ca55e65663d2a2c1a043d636dc3d30d418e56dd6087d1459d98a5d
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+sphinxcontrib-qthelp==1.0.3 \
+ --hash=sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72 \
+ --hash=sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+sphinxcontrib-serializinghtml==1.1.5 \
+ --hash=sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd \
+ --hash=sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # sphinx
+stack-data==0.6.3 \
+ --hash=sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9 \
+ --hash=sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+toml==0.10.2 \
+ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \
+ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+tomli==2.0.1 ; python_version < "3.11" \
+ --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
+ --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # black
+ # build
+ # mypy
+ # pip-tools
+ # pylint
+ # pyproject-hooks
+tomlkit==0.11.6 \
+ --hash=sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b \
+ --hash=sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pylint
+traitlets==5.1.1 \
+ --hash=sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7 \
+ --hash=sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # ipython
+ # matplotlib-inline
+types-docutils==0.20.0.3 \
+ --hash=sha256:4928e790f42b99d5833990f99c8dd9fa9f16825f6ed30380ca981846d36870cd \
+ --hash=sha256:a930150d8e01a9170f9bca489f46808ddebccdd8bc1e47c07968a77e49fb9321
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # types-pygments
+types-protobuf==4.24.0.2 \
+ --hash=sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f \
+ --hash=sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # mypy-protobuf
+types-pygments==2.16.0.0 \
+ --hash=sha256:4624a547d5ba73c971fac5d6fd327141e85e65f6123448bee76f0c8557652a71 \
+ --hash=sha256:aa93e4664e2d6cfea7570cde156e3966bf939f9c7d736cd179c4c8e94f7600b2
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-pyserial==3.5.0.7 \
+ --hash=sha256:22b665a336539b85108e2f5d61e6cde1b59818eae78324c17bfe64edc1a2bd66 \
+ --hash=sha256:aa5d5536ea914ca9be85f496e0914b10a58bbb1d2b1f54ce756be45ecb65378b
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-pyyaml==6.0.12.11 \
+ --hash=sha256:7d340b19ca28cddfdba438ee638cd4084bde213e501a3978738543e27094775b \
+ --hash=sha256:a461508f3096d1d5810ec5ab95d7eeecb651f3a15b71959999988942063bf01d
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-requests==2.31.0.2 \
+ --hash=sha256:56d181c85b5925cbc59f4489a57e72a8b2166f18273fd8ba7b6fe0c0b986f12a \
+ --hash=sha256:6aa3f7faf0ea52d728bb18c0a0d1522d9bfd8c72d26ff6f61bfc3d06a411cf40
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-setuptools==67.8.0.0 \
+ --hash=sha256:6df73340d96b238a4188b7b7668814b37e8018168aef1eef94a3b1872e3f60ff \
+ --hash=sha256:95c9ed61871d6c0e258433373a4e1753c0a7c3627a46f4d4058c7b5a08ab844f
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # types-pygments
+types-six==1.16.9 \
+ --hash=sha256:04da8bad920aea300e13d99c8948fd0914abdfc5d9421bad636137f9829dcbf6 \
+ --hash=sha256:da5748f10eec44accbd393e7f5a834991aff8f37522472166fbc8233cc735b84
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+types-urllib3==1.26.25.14 \
+ --hash=sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f \
+ --hash=sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # types-requests
+typing-extensions==4.4.0 \
+ --hash=sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa \
+ --hash=sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # astroid
+ # black
+ # ipython
+ # mypy
+ # pylint
+urllib3==2.0.4 \
+ --hash=sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11 \
+ --hash=sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # requests
+watchdog==2.3.1 \
+ --hash=sha256:03f342a9432fe08107defbe8e405a2cb922c5d00c4c6c168c68b633c64ce6190 \
+ --hash=sha256:0d9878be36d2b9271e3abaa6f4f051b363ff54dbbe7e7df1af3c920e4311ee43 \
+ --hash=sha256:0e1dd6d449267cc7d6935d7fe27ee0426af6ee16578eed93bacb1be9ff824d2d \
+ --hash=sha256:2caf77ae137935c1466f8cefd4a3aec7017b6969f425d086e6a528241cba7256 \
+ --hash=sha256:3d2dbcf1acd96e7a9c9aefed201c47c8e311075105d94ce5e899f118155709fd \
+ --hash=sha256:4109cccf214b7e3462e8403ab1e5b17b302ecce6c103eb2fc3afa534a7f27b96 \
+ --hash=sha256:4cd61f98cb37143206818cb1786d2438626aa78d682a8f2ecee239055a9771d5 \
+ --hash=sha256:53f3e95081280898d9e4fc51c5c69017715929e4eea1ab45801d5e903dd518ad \
+ --hash=sha256:564e7739abd4bd348aeafbf71cc006b6c0ccda3160c7053c4a53b67d14091d42 \
+ --hash=sha256:5b848c71ef2b15d0ef02f69da8cc120d335cec0ed82a3fa7779e27a5a8527225 \
+ --hash=sha256:5defe4f0918a2a1a4afbe4dbb967f743ac3a93d546ea4674567806375b024adb \
+ --hash=sha256:6f5d0f7eac86807275eba40b577c671b306f6f335ba63a5c5a348da151aba0fc \
+ --hash=sha256:7a1876f660e32027a1a46f8a0fa5747ad4fcf86cb451860eae61a26e102c8c79 \
+ --hash=sha256:7a596f9415a378d0339681efc08d2249e48975daae391d58f2e22a3673b977cf \
+ --hash=sha256:85bf2263290591b7c5fa01140601b64c831be88084de41efbcba6ea289874f44 \
+ --hash=sha256:8a4d484e846dcd75e96b96d80d80445302621be40e293bfdf34a631cab3b33dc \
+ --hash=sha256:8f2df370cd8e4e18499dd0bfdef476431bcc396108b97195d9448d90924e3131 \
+ --hash=sha256:91fd146d723392b3e6eb1ac21f122fcce149a194a2ba0a82c5e4d0ee29cd954c \
+ --hash=sha256:95ad708a9454050a46f741ba5e2f3468655ea22da1114e4c40b8cbdaca572565 \
+ --hash=sha256:964fd236cd443933268ae49b59706569c8b741073dbfd7ca705492bae9d39aab \
+ --hash=sha256:9da7acb9af7e4a272089bd2af0171d23e0d6271385c51d4d9bde91fe918c53ed \
+ --hash=sha256:a073c91a6ef0dda488087669586768195c3080c66866144880f03445ca23ef16 \
+ --hash=sha256:a74155398434937ac2780fd257c045954de5b11b5c52fc844e2199ce3eecf4cf \
+ --hash=sha256:aa8b028750b43e80eea9946d01925168eeadb488dfdef1d82be4b1e28067f375 \
+ --hash=sha256:d1f1200d4ec53b88bf04ab636f9133cb703eb19768a39351cee649de21a33697 \
+ --hash=sha256:d9f9ed26ed22a9d331820a8432c3680707ea8b54121ddcc9dc7d9f2ceeb36906 \
+ --hash=sha256:ea5d86d1bcf4a9d24610aa2f6f25492f441960cf04aed2bd9a97db439b643a7b \
+ --hash=sha256:efe3252137392a471a2174d721e1037a0e6a5da7beb72a021e662b7000a9903f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+wcwidth==0.2.6 \
+ --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \
+ --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # prompt-toolkit
+websockets==10.4 \
+ --hash=sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41 \
+ --hash=sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96 \
+ --hash=sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4 \
+ --hash=sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72 \
+ --hash=sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576 \
+ --hash=sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63 \
+ --hash=sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b \
+ --hash=sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d \
+ --hash=sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032 \
+ --hash=sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393 \
+ --hash=sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50 \
+ --hash=sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631 \
+ --hash=sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f \
+ --hash=sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c \
+ --hash=sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6 \
+ --hash=sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4 \
+ --hash=sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6 \
+ --hash=sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0 \
+ --hash=sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8 \
+ --hash=sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112 \
+ --hash=sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94 \
+ --hash=sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4 \
+ --hash=sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb \
+ --hash=sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331 \
+ --hash=sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c \
+ --hash=sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c \
+ --hash=sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193 \
+ --hash=sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b \
+ --hash=sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b \
+ --hash=sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038 \
+ --hash=sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089 \
+ --hash=sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa \
+ --hash=sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9 \
+ --hash=sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56 \
+ --hash=sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4 \
+ --hash=sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179 \
+ --hash=sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c \
+ --hash=sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882 \
+ --hash=sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28 \
+ --hash=sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1 \
+ --hash=sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a \
+ --hash=sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033 \
+ --hash=sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1 \
+ --hash=sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13 \
+ --hash=sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8 \
+ --hash=sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c \
+ --hash=sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74 \
+ --hash=sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab \
+ --hash=sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3 \
+ --hash=sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588 \
+ --hash=sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485 \
+ --hash=sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342 \
+ --hash=sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48 \
+ --hash=sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf \
+ --hash=sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0 \
+ --hash=sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a \
+ --hash=sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea \
+ --hash=sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf \
+ --hash=sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8 \
+ --hash=sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df \
+ --hash=sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc \
+ --hash=sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f \
+ --hash=sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269 \
+ --hash=sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3 \
+ --hash=sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c \
+ --hash=sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46 \
+ --hash=sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f \
+ --hash=sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106 \
+ --hash=sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+wheel==0.40.0 \
+ --hash=sha256:cd1196f3faee2b31968d626e1731c94f99cbdb67cf5a46e4f5656cbee7738873 \
+ --hash=sha256:d236b20e7cb522daf2390fa84c55eea81c5c30190f90f29ae2ca1ad8355bf247
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+yapf==0.31.0 \
+ --hash=sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d \
+ --hash=sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e
+ # via -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+zipp==3.16.2 \
+ --hash=sha256:679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0 \
+ --hash=sha256:ebc15946aa78bd63458992fc81ec3b6f7b1e92d51c35e6de1c3804e73b799147
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # importlib-metadata
+# The following packages are considered to be unsafe in a requirements file:
+pip==23.2.1 \
+ --hash=sha256:7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be \
+ --hash=sha256:fb0bd5435b3200c602b5bf61d2d43c2f13c02e29c1707567ae7fbc514eb9faf2
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+setuptools==68.0.0 \
+ --hash=sha256:11e52c67415a381d10d6b462ced9cfb97066179f0e871399e006c4ab101fc85f \
+ --hash=sha256:baf1fdb41c6da4cd2eae722e135500da913332ab3f2f5c7d33af9b492acb5235
+ # via
+ # -r \python\gen\pw_env_setup\pigweed_build_venv\compiled_requirements.txt
+ # pip-tools
+ # python-daemon
diff --git a/pw_env_setup/py/pw_env_setup/windows_env_start.py b/pw_env_setup/py/pw_env_setup/windows_env_start.py
index 652496071..bce9af54a 100644
--- a/pw_env_setup/py/pw_env_setup/windows_env_start.py
+++ b/pw_env_setup/py/pw_env_setup/windows_env_start.py
@@ -26,7 +26,12 @@ import argparse
import os
import sys
-from .colors import Color, enable_colors # type: ignore
+try:
+ from pw_env_setup.colors import Color, enable_colors
+except ImportError:
+ # Load from this directory if pw_env_setup is not available.
+ from colors import Color, enable_colors # type: ignore
+
_PIGWEED_BANNER = u'''
▒█████▄ █▓ ▄███▒ ▒█ ▒█ ░▓████▒ ░▓████▒ ▒▓████▄
@@ -42,7 +47,21 @@ def print_banner(bootstrap, no_shell_file):
enable_colors()
print(Color.green('\n WELCOME TO...'))
- print(Color.magenta(_PIGWEED_BANNER))
+
+ banner_file = os.environ.get('PW_BRANDING_BANNER', None)
+ banner_str = None
+ if banner_file:
+ try:
+ banner_str = open(
+ banner_file, 'r', encoding='utf-8', errors='replace'
+ ).read()
+ except FileNotFoundError:
+ pass
+ if banner_str:
+ print()
+ print(banner_str, end='')
+ else:
+ print(Color.magenta(_PIGWEED_BANNER), end='')
if bootstrap:
print(
diff --git a/pw_env_setup/py/setup.cfg b/pw_env_setup/py/setup.cfg
index bbcb87f9b..b9976c9f9 100644
--- a/pw_env_setup/py/setup.cfg
+++ b/pw_env_setup/py/setup.cfg
@@ -28,12 +28,34 @@ install_requires =
[options.entry_points]
console_scripts =
_pw_env_setup = pw_env_setup.env_setup:main
+ arm-none-eabi-gdb = pw_env_setup.entry_points.arm_gdb:main
[options.package_data]
pw_env_setup =
- py.typed
- cargo_setup/packages.txt
+ cipd_setup/.cipd_version
+ cipd_setup/.cipd_version.digests
+ cipd_setup/arm.json
+ cipd_setup/bazel.json
+ cipd_setup/black.json
+ cipd_setup/buildifier.json
+ cipd_setup/cmake.json
+ cipd_setup/compatibility.json
+ cipd_setup/default.json
+ cipd_setup/doxygen.json
+ cipd_setup/go.json
+ cipd_setup/host_tools.json
+ cipd_setup/kythe.json
cipd_setup/luci.json
+ cipd_setup/msrv_python.json
cipd_setup/pigweed.json
- virtualenv_setup/requirements.in
- virtualenv_setup/requirements.txt
+ cipd_setup/clang.json
+ cipd_setup/python.json
+ cipd_setup/rbe.json
+ cipd_setup/riscv.json
+ cipd_setup/testing.json
+ cipd_setup/upstream.json
+ cipd_setup/web.json
+ py.typed
+ virtualenv_setup/constraint.list
+ virtualenv_setup/constraint_hashes_linux.list
+ virtualenv_setup/python_base_requirements.txt
diff --git a/pw_env_setup/py/setup.py b/pw_env_setup/py/setup.py
deleted file mode 100644
index ff2824deb..000000000
--- a/pw_env_setup/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_env_setup"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_env_setup/pypi_common_setup.cfg b/pw_env_setup/pypi_common_setup.cfg
index e9a410e1f..e09779b0b 100644
--- a/pw_env_setup/pypi_common_setup.cfg
+++ b/pw_env_setup/pypi_common_setup.cfg
@@ -13,7 +13,7 @@
# the License.
[metadata]
name = pigweed
-version = 0.0.13
+version = 0.0.15
author = Pigweed Authors
author_email = pigweed-developers@googlegroups.com
description = Pigweed Python modules
diff --git a/pw_env_setup/sample_project_action/__init__.py b/pw_env_setup/sample_project_action/__init__.py
new file mode 100644
index 000000000..725cbf92d
--- /dev/null
+++ b/pw_env_setup/sample_project_action/__init__.py
@@ -0,0 +1,26 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Sample pw_env_setup project action plugin.
+
+A sample/starter project action plugin template for pw_env_setup.
+"""
+
+
+def run_action(**kwargs):
+ """Sample project action."""
+ if "env" not in kwargs:
+ raise ValueError(f"Missing required kwarg 'env', got %{kwargs}")
+
+ kwargs["env"].prepend("PATH", "PATH_TO_NEW_TOOLS")
+ raise NotImplementedError("Sample project action running!")
diff --git a/pw_env_setup/util.sh b/pw_env_setup/util.sh
index ffcc0c308..111624ea3 100644
--- a/pw_env_setup/util.sh
+++ b/pw_env_setup/util.sh
@@ -288,10 +288,10 @@ pw_bootstrap() {
# Write the directory path at bootstrap time into the directory. This helps
# us double-check things are still in the same space when calling activate.
_PW_ENV_ROOT_TXT="$_PW_ACTUAL_ENVIRONMENT_ROOT/env_root.txt"
- echo "$_PW_ACTUAL_ENVIRONMENT_ROOT" > "$_PW_ENV_ROOT_TXT"
+ echo "$_PW_ACTUAL_ENVIRONMENT_ROOT" > "$_PW_ENV_ROOT_TXT" 2> /dev/null
# Create the environment README file. Use quotes to prevent alias expansion.
- "cp" "$PW_ROOT/pw_env_setup/destination.md" "$_PW_ACTUAL_ENVIRONMENT_ROOT/README.md"
+ "cp" "$PW_ROOT/pw_env_setup/destination.md" "$_PW_ACTUAL_ENVIRONMENT_ROOT/README.md" &> /dev/null
}
pw_activate() {
diff --git a/pw_file/BUILD.bazel b/pw_file/BUILD.bazel
index 53f83ebc4..de45b342f 100644
--- a/pw_file/BUILD.bazel
+++ b/pw_file/BUILD.bazel
@@ -17,8 +17,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -43,7 +43,7 @@ pw_proto_library(
py_proto_library(
name = "file_proto_py_pb2",
srcs = ["file.proto"],
- # TODO(b/241456982): Get this target to build.
+ # TODO: b/241456982 - Get this target to build.
tags = ["manual"],
)
diff --git a/pw_file/BUILD.gn b/pw_file/BUILD.gn
index f9ee8a64f..280c7d162 100644
--- a/pw_file/BUILD.gn
+++ b/pw_file/BUILD.gn
@@ -26,6 +26,7 @@ pw_proto_library("proto") {
sources = [ "file.proto" ]
prefix = "pw_file"
deps = [ "$dir_pw_protobuf:common_protos" ]
+ python_package = "py"
}
pw_source_set("flat_file_system") {
diff --git a/pw_file/CMakeLists.txt b/pw_file/CMakeLists.txt
index 9bb56d1ea..671f34056 100644
--- a/pw_file/CMakeLists.txt
+++ b/pw_file/CMakeLists.txt
@@ -16,6 +16,8 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
pw_add_library(pw_file.flat_file_system INTERFACE
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_file.proto.pwpb
pw_file.proto.raw_rpc
diff --git a/pw_file/flat_file_system.cc b/pw_file/flat_file_system.cc
index 9960bd348..ce57a2d84 100644
--- a/pw_file/flat_file_system.cc
+++ b/pw_file/flat_file_system.cc
@@ -73,12 +73,12 @@ void FlatFileSystemService::EnumerateAllFiles(RawServerWriter& writer) {
Status write_status = writer.Write(encoder);
if (!write_status.ok()) {
writer.Finish(write_status)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
}
writer.Finish(OkStatus())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
void FlatFileSystemService::List(ConstByteSpan request,
@@ -92,10 +92,9 @@ void FlatFileSystemService::List(ConstByteSpan request,
}
std::string_view file_name_view;
- if (!decoder.ReadString(&file_name_view).ok() ||
- file_name_view.length() == 0) {
+ if (!decoder.ReadString(&file_name_view).ok() || file_name_view.empty()) {
writer.Finish(Status::DataLoss())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
@@ -103,7 +102,7 @@ void FlatFileSystemService::List(ConstByteSpan request,
Result<Entry*> result = FindFile(file_name_view);
if (!result.ok()) {
writer.Finish(result.status())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
@@ -111,12 +110,12 @@ void FlatFileSystemService::List(ConstByteSpan request,
Status proto_encode_status = EnumerateFile(*result.value(), encoder);
if (!proto_encode_status.ok()) {
writer.Finish(proto_encode_status)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
writer.Finish(writer.Write(encoder))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
diff --git a/pw_file/py/BUILD.gn b/pw_file/py/BUILD.gn
new file mode 100644
index 000000000..f42bcfed7
--- /dev/null
+++ b/pw_file/py/BUILD.gn
@@ -0,0 +1,31 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ generate_setup = {
+ metadata = {
+ name = "pw_file"
+ version = "0.0.1"
+ }
+ }
+ sources = [ "pw_file/__init__.py" ]
+ python_deps = []
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ proto_library = "..:proto"
+}
diff --git a/pw_file/py/pw_file/__init__.py b/pw_file/py/pw_file/__init__.py
new file mode 100644
index 000000000..c3e1bdbd5
--- /dev/null
+++ b/pw_file/py/pw_file/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
diff --git a/pw_format/BUILD.gn b/pw_format/BUILD.gn
new file mode 100644
index 000000000..394bbd336
--- /dev/null
+++ b/pw_format/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_arduino_build/arduino.gni")
+import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/module_config.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_fuzzer/fuzzer.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+# Since this module only contains Rust code, it is only built and tested under
+# Bazel at the moment.
+pw_test_group("tests") {
+ tests = []
+}
diff --git a/pw_format/docs.rst b/pw_format/docs.rst
new file mode 100644
index 000000000..3cd388d76
--- /dev/null
+++ b/pw_format/docs.rst
@@ -0,0 +1,22 @@
+.. _module-pw_format:
+
+=========
+pw_format
+=========
+.. pigweed-module::
+ :name: pw_format
+ :tagline: String formatting
+ :status: experimental
+ :languages: Rust
+
+``pw_format`` supports parsing ``printf`` and Rust ``core::fmt`` style format
+strings and using them to format output in languages other than C/C++.
+
+Disambiguation: If you're looking for code formatting support, see
+:ref:`pw_presubmit <module-pw_presubmit>`.
+
+----
+Rust
+----
+``pw_format``'s Rust API is documented in the
+`pw_format crate's docs </rustdoc/pw_format>`_.
diff --git a/pw_format/rust/BUILD.bazel b/pw_format/rust/BUILD.bazel
new file mode 100644
index 000000000..e7b85a9a6
--- /dev/null
+++ b/pw_format/rust/BUILD.bazel
@@ -0,0 +1,112 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_doc_test", "rust_library", "rust_proc_macro", "rust_test")
+
+rust_library(
+ name = "pw_format",
+ srcs = [
+ "pw_format/lib.rs",
+ "pw_format/macros.rs",
+ "pw_format/tests.rs",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_status/rust:pw_status",
+ "@rust_crates//:nom",
+ "@rust_crates//:proc-macro2",
+ "@rust_crates//:quote",
+ "@rust_crates//:syn",
+ ],
+)
+
+rust_test(
+ name = "pw_format_test",
+ crate = ":pw_format",
+)
+
+rust_doc_test(
+ name = "pw_format_doc_test",
+ crate = ":pw_format",
+)
+
+rust_doc(
+ name = "pw_format_doc",
+ crate = ":pw_format",
+)
+
+rust_proc_macro(
+ name = "pw_format_example_macro",
+ srcs = [
+ "pw_format_example_macro.rs",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":pw_format",
+ "//pw_status/rust:pw_status",
+ "@rust_crates//:proc-macro2",
+ "@rust_crates//:quote",
+ "@rust_crates//:syn",
+ ],
+)
+
+rust_library(
+ name = "pw_format_example_macro_test",
+ srcs = [
+ "pw_format_example_macro_test.rs",
+ ],
+ proc_macro_deps = [
+ ":pw_format_example_macro",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+rust_test(
+ name = "pw_format_example_macro_test_test",
+ crate = ":pw_format_example_macro_test",
+)
+
+rust_proc_macro(
+ name = "pw_format_test_macros",
+ srcs = [
+ "pw_format_test_macros.rs",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":pw_format",
+ "//pw_status/rust:pw_status",
+ "@rust_crates//:proc-macro2",
+ "@rust_crates//:quote",
+ "@rust_crates//:syn",
+ ],
+)
+
+rust_library(
+ name = "pw_format_test_macros_test",
+ srcs = [
+ "pw_format_test_macros_test.rs",
+ ],
+ proc_macro_deps = [
+ ":pw_format_test_macros",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":pw_format",
+ ],
+)
+
+rust_test(
+ name = "pw_format_test_macros_test_test",
+ crate = ":pw_format_test_macros_test",
+)
diff --git a/pw_format/rust/pw_format/lib.rs b/pw_format/rust/pw_format/lib.rs
new file mode 100644
index 000000000..59954a1f3
--- /dev/null
+++ b/pw_format/rust/pw_format/lib.rs
@@ -0,0 +1,411 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! The `pw_format` crate is a parser used to implement proc macros that:
+//! * Understand format string argument types at compile time.
+//! * Syntax check format strings.
+//!
+//! `pw_format` is written against `std` and is not intended to be
+//! used in an embedded context. Some efficiency and memory is traded for a
+//! more expressive interface that exposes the format string's "syntax tree"
+//! to the API client.
+//!
+//! # Proc Macros
+//!
+//! The [`macros`] module provides infrastructure for implementing proc macros
+//! that take format strings as arguments.
+//!
+//! # Example
+//!
+//! ```
+//! use pw_format::{
+//! ConversionSpec, Flag, FormatFragment, FormatString, Length, Precision, Specifier, MinFieldWidth,
+//! };
+//!
+//! let format_string =
+//! FormatString::parse("long double %+ 4.2Lg is %-03hd%%.").unwrap();
+//!
+//! assert_eq!(format_string, FormatString {
+//! fragments: vec![
+//! FormatFragment::Literal("long double ".to_string()),
+//! FormatFragment::Conversion(ConversionSpec {
+//! flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+//! min_field_width: MinFieldWidth::Fixed(4),
+//! precision: Precision::Fixed(2),
+//! length: Some(Length::LongDouble),
+//! specifier: Specifier::SmallDouble
+//! }),
+//! FormatFragment::Literal(" is ".to_string()),
+//! FormatFragment::Conversion(ConversionSpec {
+//! flags: [Flag::LeftJustify, Flag::LeadingZeros]
+//! .into_iter()
+//! .collect(),
+//! min_field_width: MinFieldWidth::Fixed(3),
+//! precision: Precision::None,
+//! length: Some(Length::Short),
+//! specifier: Specifier::Decimal
+//! }),
+//! FormatFragment::Percent,
+//! FormatFragment::Literal(".".to_string()),
+//! ]
+//! });
+//! ```
+#![deny(missing_docs)]
+
+use std::collections::HashSet;
+
+use nom::{
+ branch::alt,
+ bytes::complete::tag,
+ bytes::complete::take_till1,
+ character::complete::{anychar, digit1},
+ combinator::{map, map_res},
+ multi::many0,
+ IResult,
+};
+
+pub mod macros;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+/// A printf specifier (the 'd' in %d).
+pub enum Specifier {
+ /// `%d`
+ Decimal,
+
+ /// `%i`
+ Integer,
+
+ /// `%o`
+ Octal,
+
+ /// `%u`
+ Unsigned,
+
+ /// `%x`
+ Hex,
+
+ /// `%X`
+ UpperHex,
+
+ /// `%f`
+ Double,
+
+ /// `%F`
+ UpperDouble,
+
+ /// `%e`
+ Exponential,
+
+ /// `%E`
+ UpperExponential,
+
+ /// `%g`
+ SmallDouble,
+
+ /// `%G`
+ UpperSmallDouble,
+
+ /// `%c`
+ Char,
+
+ /// `%s`
+ String,
+
+ /// `%p`
+ Pointer,
+}
+
+impl TryFrom<char> for Specifier {
+ type Error = String;
+
+ fn try_from(value: char) -> Result<Self, Self::Error> {
+ match value {
+ 'd' => Ok(Self::Decimal),
+ 'i' => Ok(Self::Integer),
+ 'o' => Ok(Self::Octal),
+ 'u' => Ok(Self::Unsigned),
+ 'x' => Ok(Self::Hex),
+ 'X' => Ok(Self::UpperHex),
+ 'f' => Ok(Self::Double),
+ 'F' => Ok(Self::UpperDouble),
+ 'e' => Ok(Self::Exponential),
+ 'E' => Ok(Self::UpperExponential),
+ 'g' => Ok(Self::SmallDouble),
+ 'G' => Ok(Self::UpperSmallDouble),
+ 'c' => Ok(Self::Char),
+ 's' => Ok(Self::String),
+ 'p' => Ok(Self::Pointer),
+ _ => Err(format!("Unsupported format specifier '{}'", value)),
+ }
+ }
+}
+
+#[derive(Debug, Hash, PartialEq, Eq)]
+/// A printf flag (the '+' in %+d).
+pub enum Flag {
+ /// `-`
+ LeftJustify,
+
+ /// `+`
+ ForceSign,
+
+ /// ` `
+ SpaceSign,
+
+ /// `#`
+ AlternateSyntax,
+
+ /// `0`
+ LeadingZeros,
+}
+
+impl TryFrom<char> for Flag {
+ type Error = String;
+
+ fn try_from(value: char) -> Result<Self, Self::Error> {
+ match value {
+ '-' => Ok(Self::LeftJustify),
+ '+' => Ok(Self::ForceSign),
+ ' ' => Ok(Self::SpaceSign),
+ '#' => Ok(Self::AlternateSyntax),
+ '0' => Ok(Self::LeadingZeros),
+ _ => Err(format!("Unsupported flag '{}'", value)),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+/// A printf minimum field width (the 5 in %5d).
+pub enum MinFieldWidth {
+ /// No field width specified.
+ None,
+
+ /// Fixed field with.
+ Fixed(u32),
+
+ /// Variable field width passed as an argument (i.e. %*d).
+ Variable,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+/// A printf precision (the .5 in %.5d).
+///
+/// For string conversions (%s) this is treated as the maximum number of
+/// bytes of the string to output.
+pub enum Precision {
+ /// No precision specified.
+ None,
+
+ /// Fixed precision.
+ Fixed(u32),
+
+ /// Variable precision passed as an argument (i.e. %.*f).
+ Variable,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+/// A printf length (the l in %ld).
+pub enum Length {
+ /// `hh`
+ Char,
+
+ /// `h`
+ Short,
+
+ /// `l`
+ Long,
+
+ /// `ll`
+ LongLong,
+
+ /// `L`
+ LongDouble,
+
+ /// `j`
+ IntMax,
+
+ /// `z`
+ Size,
+
+ /// `t`
+ PointerDiff,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+/// A printf conversion specification aka a % clause.
+pub struct ConversionSpec {
+ /// ConversionSpec's set of [Flag]s.
+ pub flags: HashSet<Flag>,
+ /// ConversionSpec's minimum field width argument.
+ pub min_field_width: MinFieldWidth,
+ /// ConversionSpec's [Precision] argument.
+ pub precision: Precision,
+ /// ConversionSpec's [Length] argument.
+ pub length: Option<Length>,
+ /// ConversionSpec's [Specifier].
+ pub specifier: Specifier,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+/// A fragment of a printf format string.
+pub enum FormatFragment {
+ /// A literal string value.
+ Literal(String),
+
+ /// A conversion specification (i.e. %d).
+ Conversion(ConversionSpec),
+
+ /// An escaped %.
+ Percent,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+/// A parsed printf format string.
+pub struct FormatString {
+ /// The [FormatFragment]s that comprise the [FormatString].
+ pub fragments: Vec<FormatFragment>,
+}
+
+fn specifier(input: &str) -> IResult<&str, Specifier> {
+ map_res(anychar, Specifier::try_from)(input)
+}
+
+fn flags(input: &str) -> IResult<&str, HashSet<Flag>> {
+ let (input, flags) = many0(map_res(anychar, Flag::try_from))(input)?;
+
+ Ok((input, flags.into_iter().collect()))
+}
+
+fn variable_width(input: &str) -> IResult<&str, MinFieldWidth> {
+ map(tag("*"), |_| MinFieldWidth::Variable)(input)
+}
+
+fn fixed_width(input: &str) -> IResult<&str, MinFieldWidth> {
+ map_res(
+ digit1,
+ |value: &str| -> Result<MinFieldWidth, std::num::ParseIntError> {
+ Ok(MinFieldWidth::Fixed(value.parse()?))
+ },
+ )(input)
+}
+
+fn no_width(input: &str) -> IResult<&str, MinFieldWidth> {
+ Ok((input, MinFieldWidth::None))
+}
+
+fn width(input: &str) -> IResult<&str, MinFieldWidth> {
+ alt((variable_width, fixed_width, no_width))(input)
+}
+
+fn variable_precision(input: &str) -> IResult<&str, Precision> {
+ let (input, _) = tag(".")(input)?;
+ map(tag("*"), |_| Precision::Variable)(input)
+}
+
+fn fixed_precision(input: &str) -> IResult<&str, Precision> {
+ let (input, _) = tag(".")(input)?;
+ map_res(
+ digit1,
+ |value: &str| -> Result<Precision, std::num::ParseIntError> {
+ Ok(Precision::Fixed(value.parse()?))
+ },
+ )(input)
+}
+
+fn no_precision(input: &str) -> IResult<&str, Precision> {
+ Ok((input, Precision::None))
+}
+
+fn precision(input: &str) -> IResult<&str, Precision> {
+ alt((variable_precision, fixed_precision, no_precision))(input)
+}
+
+fn length(input: &str) -> IResult<&str, Option<Length>> {
+ alt((
+ map(tag("hh"), |_| Some(Length::Char)),
+ map(tag("h"), |_| Some(Length::Short)),
+ map(tag("ll"), |_| Some(Length::LongLong)), // ll must precede l
+ map(tag("l"), |_| Some(Length::Long)),
+ map(tag("L"), |_| Some(Length::LongDouble)),
+ map(tag("j"), |_| Some(Length::IntMax)),
+ map(tag("z"), |_| Some(Length::Size)),
+ map(tag("t"), |_| Some(Length::PointerDiff)),
+ map(tag(""), |_| None),
+ ))(input)
+}
+
+fn conversion_spec(input: &str) -> IResult<&str, ConversionSpec> {
+ let (input, _) = tag("%")(input)?;
+ let (input, flags) = flags(input)?;
+ let (input, width) = width(input)?;
+ let (input, precision) = precision(input)?;
+ let (input, length) = length(input)?;
+ let (input, specifier) = specifier(input)?;
+
+ Ok((
+ input,
+ ConversionSpec {
+ flags,
+ min_field_width: width,
+ precision,
+ length,
+ specifier,
+ },
+ ))
+}
+
+fn literal_fragment(input: &str) -> IResult<&str, FormatFragment> {
+ map(take_till1(|c| c == '%'), |s: &str| {
+ FormatFragment::Literal(s.to_string())
+ })(input)
+}
+
+fn percent_fragment(input: &str) -> IResult<&str, FormatFragment> {
+ map(tag("%%"), |_| FormatFragment::Percent)(input)
+}
+
+fn conversion_fragment(input: &str) -> IResult<&str, FormatFragment> {
+ map(conversion_spec, FormatFragment::Conversion)(input)
+}
+
+fn fragment(input: &str) -> IResult<&str, FormatFragment> {
+ alt((percent_fragment, conversion_fragment, literal_fragment))(input)
+}
+
+fn format_string(input: &str) -> IResult<&str, FormatString> {
+ let (input, fragments) = many0(fragment)(input)?;
+
+ Ok((input, FormatString { fragments }))
+}
+
+impl FormatString {
+ /// Parses a printf style format string.
+ pub fn parse(s: &str) -> Result<Self, String> {
+ // TODO: b/281858500 - Add better errors to failed parses.
+ let (rest, result) =
+ format_string(s).map_err(|e| format!("Failed to parse format string \"{s}\": {e}"))?;
+
+ // If the parser did not consume all the input, return an error.
+ if !rest.is_empty() {
+ return Err(format!(
+ "Failed to parse format string fragment: \"{rest}\""
+ ));
+ }
+
+ Ok(result)
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/pw_format/rust/pw_format/macros.rs b/pw_format/rust/pw_format/macros.rs
new file mode 100644
index 000000000..1c049471d
--- /dev/null
+++ b/pw_format/rust/pw_format/macros.rs
@@ -0,0 +1,487 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! The `macro` module provides helpers that simplify writing proc macros
+//! that take format strings and arguments. This is accomplish with three
+//! main constructs:
+//! * [`FormatAndArgs`]: A struct that implements [syn::parse::Parse] to
+//! parse a format string and its following arguments.
+//! * [`FormatMacroGenerator`]: A trait used to implement the macro specific
+//! logic to generate code.
+//! * [`generate`]: A function to handle the execution of the proc macro by
+//! calling into a [FormatMacroGenerator].
+//!
+//! Additionally [`PrintfFormatMacroGenerator`] trait and [`generate_printf`]
+//! function are provided to help when implementing generators that need to
+//! produce `printf` style format strings as part of their code generation.
+//!
+//! ## Example
+//!
+//! An example of implementing a proc macro is provided in the
+//! [pw_format_example_macro crate](https://pigweed.googlesource.com/pigweed/pigweed/+/refs/heads/main/pw_format/rust/pw_format_example_macro.rs)
+//!
+//!
+
+use std::collections::VecDeque;
+
+use proc_macro2::Ident;
+use quote::{format_ident, quote, ToTokens};
+use syn::{
+ parse::{Parse, ParseStream},
+ punctuated::Punctuated,
+ Expr, LitStr, Token,
+};
+
+use crate::{
+ ConversionSpec, FormatFragment, FormatString, Length, MinFieldWidth, Precision, Specifier,
+};
+
+type TokenStream2 = proc_macro2::TokenStream;
+
+/// An error occurring during proc macro evaluation.
+///
+/// In order to stay as flexible as possible to implementors of
+/// [`FormatMacroGenerator`], the error is simply represent by a
+/// string.
+#[derive(Debug)]
+pub struct Error {
+ text: String,
+}
+
+impl Error {
+ /// Create a new proc macro evaluation error.
+ pub fn new(text: &str) -> Self {
+ Self {
+ text: text.to_string(),
+ }
+ }
+}
+
+/// An alias for a Result with an ``Error``
+pub type Result<T> = core::result::Result<T, Error>;
+
+/// Style in which to display the an integer.
+///
+/// In order to maintain compatibility with `printf` style systems, sign
+/// and base are combined.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum IntegerDisplayType {
+ /// Signed integer
+ Signed,
+ /// Unsigned integer
+ Unsigned,
+ /// Unsigned octal
+ Octal,
+ /// Unsigned hex with lower case letters
+ Hex,
+ /// Unsigned hex with upper case letters
+ UpperHex,
+}
+
+impl TryFrom<crate::Specifier> for IntegerDisplayType {
+ type Error = Error;
+
+ fn try_from(value: Specifier) -> Result<Self> {
+ match value {
+ Specifier::Decimal | Specifier::Integer => Ok(Self::Signed),
+ Specifier::Unsigned => Ok(Self::Unsigned),
+ Specifier::Octal => Ok(Self::Octal),
+ Specifier::Hex => Ok(Self::Hex),
+ Specifier::UpperHex => Ok(Self::UpperHex),
+ _ => Err(Error::new("No valid IntegerDisplayType for {value:?}.")),
+ }
+ }
+}
+
+/// Implemented for testing through the pw_format_test_macros crate.
+impl ToTokens for IntegerDisplayType {
+ fn to_tokens(&self, tokens: &mut TokenStream2) {
+ let new_tokens = match self {
+ IntegerDisplayType::Signed => quote!(pw_format::macros::IntegerDisplayType::Signed),
+ IntegerDisplayType::Unsigned => {
+ quote!(pw_format::macros::IntegerDisplayType::Unsigned)
+ }
+ IntegerDisplayType::Octal => quote!(pw_format::macros::IntegerDisplayType::Octal),
+ IntegerDisplayType::Hex => quote!(pw_format::macros::IntegerDisplayType::Hex),
+ IntegerDisplayType::UpperHex => {
+ quote!(pw_format::macros::IntegerDisplayType::UpperHex)
+ }
+ };
+ new_tokens.to_tokens(tokens);
+ }
+}
+
+/// A code generator for implementing a `pw_format` style macro.
+///
+/// This trait serves as the primary interface between `pw_format` and a
+/// proc macro using it to implement format string and argument parsing. When
+/// evaluating the proc macro and generating code, [`generate`] will make
+/// repeated calls to [`string_fragment`](FormatMacroGenerator::string_fragment)
+/// and the conversion functions. These calls will be made in the order they
+/// appear in the format string. After all fragments and conversions are
+/// processed, [`generate`] will call
+/// [`finalize`](FormatMacroGenerator::finalize).
+///
+/// For an example of implementing a `FormatMacroGenerator` see the
+/// [pw_format_example_macro crate](https://pigweed.googlesource.com/pigweed/pigweed/+/refs/heads/main/pw_format/rust/pw_format_example_macro.rs).
+pub trait FormatMacroGenerator {
+ /// Called by [`generate`] at the end of code generation.
+ ///
+ /// Consumes `self` and returns the code to be emitted by the proc macro of
+ /// and [`Error`].
+ fn finalize(self) -> Result<TokenStream2>;
+
+ /// Process a string fragment.
+ ///
+ /// A string fragment is a string of characters that appear in a format
+ /// string. This is different than a
+ /// [`string_conversion`](FormatMacroGenerator::string_conversion) which is
+ /// a string provided through a conversion specifier (i.e. `"%s"`).
+ fn string_fragment(&mut self, string: &str) -> Result<()>;
+
+ /// Process an integer conversion.
+ fn integer_conversion(
+ &mut self,
+ display: IntegerDisplayType,
+ type_width: u8, // This should probably be an enum
+ expression: Expr,
+ ) -> Result<()>;
+
+ /// Process a string conversion.
+ ///
+ /// See [`string_fragment`](FormatMacroGenerator::string_fragment) for a
+ /// disambiguation between that function and this one.
+ fn string_conversion(&mut self, expression: Expr) -> Result<()>;
+
+ /// Process a character conversion.
+ fn char_conversion(&mut self, expression: Expr) -> Result<()>;
+}
+
+/// A parsed format string and it's arguments.
+///
+/// `FormatAndArgs` implements [`syn::parse::Parse`] and can be used to parse
+/// arguments to proc maros that take format strings. Arguments are parsed
+/// according to the pattern: `($format_string:literal, $($args:expr),*)`
+#[derive(Debug)]
+pub struct FormatAndArgs {
+ format_string: LitStr,
+ parsed: FormatString,
+ args: VecDeque<Expr>,
+}
+
+impl Parse for FormatAndArgs {
+ fn parse(input: ParseStream) -> syn::parse::Result<Self> {
+ let format_string = input.parse::<LitStr>()?;
+
+ let args = if input.is_empty() {
+ // If there are no more tokens, no arguments were specified.
+ VecDeque::new()
+ } else {
+ // Eat the `,` following the format string.
+ input.parse::<Token![,]>()?;
+
+ let punctuated = Punctuated::<Expr, Token![,]>::parse_terminated(input)?;
+ punctuated.into_iter().collect()
+ };
+
+ let parsed = FormatString::parse(&format_string.value()).map_err(|e| {
+ syn::Error::new_spanned(
+ format_string.to_token_stream(),
+ format!("Error parsing format string {e}"),
+ )
+ })?;
+
+ Ok(FormatAndArgs {
+ format_string,
+ parsed,
+ args,
+ })
+ }
+}
+
+// Grab the next argument returning a descriptive error if no more args are left.
+fn next_arg(spec: &ConversionSpec, args: &mut VecDeque<Expr>) -> Result<Expr> {
+ args.pop_front()
+ .ok_or_else(|| Error::new(&format!("No argument given for {spec:?}")))
+}
+
+// Handle a single format conversion specifier (i.e. `%08x`). Grabs the
+// necessary arguments for the specifier from `args` and generates code
+// to marshal the arguments into the buffer declared in `_tokenize_to_buffer`.
+// Returns an error if args is too short of if a format specifier is unsupported.
+fn handle_conversion(
+ generator: &mut dyn FormatMacroGenerator,
+ spec: &ConversionSpec,
+ args: &mut VecDeque<Expr>,
+) -> Result<()> {
+ match spec.specifier {
+ Specifier::Decimal
+ | Specifier::Integer
+ | Specifier::Octal
+ | Specifier::Unsigned
+ | Specifier::Hex
+ | Specifier::UpperHex => {
+ // TODO: b/281862660 - Support Width::Variable and Precision::Variable.
+ if spec.min_field_width == MinFieldWidth::Variable {
+ return Err(Error::new(
+ "Variable width '*' integer formats are not supported.",
+ ));
+ }
+
+ if spec.precision == Precision::Variable {
+ return Err(Error::new(
+ "Variable precision '*' integer formats are not supported.",
+ ));
+ }
+
+ let arg = next_arg(spec, args)?;
+ let bits = match spec.length.unwrap_or(Length::Long) {
+ Length::Char => 8,
+ Length::Short => 16,
+ Length::Long => 32,
+ Length::LongLong => 64,
+ Length::IntMax => 64,
+ Length::Size => 32,
+ Length::PointerDiff => 32,
+ Length::LongDouble => {
+ return Err(Error::new(
+ "Long double length parameter invalid for integer formats",
+ ))
+ }
+ };
+
+ let display: IntegerDisplayType =
+ spec.specifier.clone().try_into().expect(
+ "Specifier is guaranteed to convert display type but enclosing match arm.",
+ );
+ generator.integer_conversion(display, bits, arg)
+ }
+ Specifier::String => {
+ // TODO: b/281862660 - Support Width::Variable and Precision::Variable.
+ if spec.min_field_width == MinFieldWidth::Variable {
+ return Err(Error::new(
+ "Variable width '*' string formats are not supported.",
+ ));
+ }
+
+ if spec.precision == Precision::Variable {
+ return Err(Error::new(
+ "Variable precision '*' string formats are not supported.",
+ ));
+ }
+
+ let arg = next_arg(spec, args)?;
+ generator.string_conversion(arg)
+ }
+ Specifier::Char => {
+ let arg = next_arg(spec, args)?;
+ generator.char_conversion(arg)
+ }
+
+ Specifier::Double
+ | Specifier::UpperDouble
+ | Specifier::Exponential
+ | Specifier::UpperExponential
+ | Specifier::SmallDouble
+ | Specifier::UpperSmallDouble => {
+ // TODO: b/281862328 - Support floating point numbers.
+ Err(Error::new("Floating point numbers are not supported."))
+ }
+
+ // TODO: b/281862333 - Support pointers.
+ Specifier::Pointer => Err(Error::new("Pointer types are not supported.")),
+ }
+}
+
+/// Generate code for a `pw_format` style proc macro.
+///
+/// `generate` takes a [`FormatMacroGenerator`] and a [`FormatAndArgs`] struct
+/// and uses them to produce the code output for a proc macro.
+pub fn generate(
+ mut generator: impl FormatMacroGenerator,
+ format_and_args: FormatAndArgs,
+) -> core::result::Result<TokenStream2, syn::Error> {
+ let mut args = format_and_args.args;
+ let mut errors = Vec::new();
+
+ for fragment in format_and_args.parsed.fragments {
+ let result = match fragment {
+ FormatFragment::Conversion(spec) => handle_conversion(&mut generator, &spec, &mut args),
+ FormatFragment::Literal(string) => generator.string_fragment(&string),
+ FormatFragment::Percent => generator.string_fragment("%"),
+ };
+ if let Err(e) = result {
+ errors.push(syn::Error::new_spanned(
+ format_and_args.format_string.to_token_stream(),
+ e.text,
+ ));
+ }
+ }
+
+ if !errors.is_empty() {
+ return Err(errors
+ .into_iter()
+ .reduce(|mut accumulated_errors, error| {
+ accumulated_errors.combine(error);
+ accumulated_errors
+ })
+ .expect("errors should not be empty"));
+ }
+
+ generator.finalize().map_err(|e| {
+ syn::Error::new_spanned(format_and_args.format_string.to_token_stream(), e.text)
+ })
+}
+
+/// A specialized generator for proc macros that produce `printf` style format strings.
+///
+/// For proc macros that need to translate a `pw_format` invocation into a
+/// `printf` style format string, `PrintfFormatMacroGenerator` offer a
+/// specialized form of [`FormatMacroGenerator`] that builds the format string
+/// and provides it as an argument to
+/// [`finalize`](PrintfFormatMacroGenerator::finalize).
+///
+/// In cases where a generator needs to override the conversion specifier it
+/// can return it from its appropriate conversion method. An example of using
+/// this would be wanting to pass a Rust string directly to a `printf` call
+/// over FFI. In that case,
+/// [`string_conversion`](PrintfFormatMacroGenerator::string_conversion) could
+/// return `Ok(Some("%.*s".to_string()))` to allow both the length and string
+/// pointer to be passed to `printf`.
+pub trait PrintfFormatMacroGenerator {
+ /// Called by [`generate_printf`] at the end of code generation.
+ ///
+ /// Works like [`FormatMacroGenerator::finalize`] with the addition of
+ /// being provided a `printf_style` format string.
+ fn finalize(self, format_string: String) -> Result<TokenStream2>;
+
+ /// Process a string fragment.
+ ///
+ /// **NOTE**: This string may contain unescaped `%` characters.
+ ///
+ /// See [`FormatMacroGenerator::string_fragment`] for a disambiguation
+ /// between a string fragment and string conversion.
+ fn string_fragment(&mut self, string: &str) -> Result<()>;
+
+ /// Process an integer conversion.
+ ///
+ /// May optionally return a printf format string (i.e. "%d") to override the
+ /// default.
+ fn integer_conversion(&mut self, ty: Ident, expression: Expr) -> Result<Option<String>>;
+
+ /// Process a string conversion.
+ ///
+ /// May optionally return a printf format string (i.e. "%s") to override the
+ /// default.
+ ///
+ /// See [`FormatMacroGenerator::string_fragment`] for a disambiguation
+ /// between a string fragment and string conversion.
+ /// FIXME: docs
+ fn string_conversion(&mut self, expression: Expr) -> Result<Option<String>>;
+
+ /// Process a character conversion.
+ ///
+ /// May optionally return a printf format string (i.e. "%c") to override the
+ /// default.
+ fn char_conversion(&mut self, expression: Expr) -> Result<Option<String>>;
+}
+
+// Wraps a `PrintfFormatMacroGenerator` in a `FormatMacroGenerator` that
+// generates the format string as it goes.
+struct PrintfGenerator<GENERATOR: PrintfFormatMacroGenerator> {
+ inner: GENERATOR,
+ format_string: String,
+}
+
+impl<GENERATOR: PrintfFormatMacroGenerator> FormatMacroGenerator for PrintfGenerator<GENERATOR> {
+ fn finalize(self) -> Result<TokenStream2> {
+ self.inner.finalize(self.format_string)
+ }
+
+ fn string_fragment(&mut self, string: &str) -> Result<()> {
+ // Escape '%' characters.
+ let format_string = string.replace("%", "%%");
+
+ self.format_string.push_str(&format_string);
+ self.inner.string_fragment(string)
+ }
+
+ fn integer_conversion(
+ &mut self,
+ display: IntegerDisplayType,
+ type_width: u8, // in bits
+ expression: Expr,
+ ) -> Result<()> {
+ let length_modifer = match type_width {
+ 8 => "hh",
+ 16 => "h",
+ 32 => "",
+ 64 => "ll",
+ _ => {
+ return Err(Error::new(&format!(
+ "printf backend does not support {} bit field width",
+ type_width
+ )))
+ }
+ };
+
+ let (conversion, ty) = match display {
+ IntegerDisplayType::Signed => ("d", format_ident!("i{type_width}")),
+ IntegerDisplayType::Unsigned => ("u", format_ident!("u{type_width}")),
+ IntegerDisplayType::Octal => ("o", format_ident!("u{type_width}")),
+ IntegerDisplayType::Hex => ("x", format_ident!("u{type_width}")),
+ IntegerDisplayType::UpperHex => ("X", format_ident!("u{type_width}")),
+ };
+
+ match self.inner.integer_conversion(ty, expression)? {
+ Some(s) => self.format_string.push_str(&s),
+ None => self
+ .format_string
+ .push_str(&format!("%{}{}", length_modifer, conversion)),
+ }
+
+ Ok(())
+ }
+
+ fn string_conversion(&mut self, expression: Expr) -> Result<()> {
+ match self.inner.string_conversion(expression)? {
+ Some(s) => self.format_string.push_str(&s),
+ None => self.format_string.push_str("%s"),
+ }
+ Ok(())
+ }
+
+ fn char_conversion(&mut self, expression: Expr) -> Result<()> {
+ match self.inner.char_conversion(expression)? {
+ Some(s) => self.format_string.push_str(&s),
+ None => self.format_string.push_str("%c"),
+ }
+ Ok(())
+ }
+}
+
+/// Generate code for a `pw_format` style proc macro that needs a `printf` format string.
+///
+/// `generate_printf` is a specialized version of [`generate`] which works with
+/// [`PrintfFormatMacroGenerator`]
+pub fn generate_printf(
+ generator: impl PrintfFormatMacroGenerator,
+ format_and_args: FormatAndArgs,
+) -> core::result::Result<TokenStream2, syn::Error> {
+ let generator = PrintfGenerator {
+ inner: generator,
+ format_string: "".into(),
+ };
+ generate(generator, format_and_args)
+}
diff --git a/pw_format/rust/pw_format/tests.rs b/pw_format/rust/pw_format/tests.rs
new file mode 100644
index 000000000..9a5cb7496
--- /dev/null
+++ b/pw_format/rust/pw_format/tests.rs
@@ -0,0 +1,1472 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use super::*;
+
+#[test]
+fn test_specifier() {
+ assert_eq!(specifier("d"), Ok(("", Specifier::Decimal)));
+ assert_eq!(specifier("i"), Ok(("", Specifier::Integer)));
+ assert_eq!(specifier("o"), Ok(("", Specifier::Octal)));
+ assert_eq!(specifier("u"), Ok(("", Specifier::Unsigned)));
+ assert_eq!(specifier("x"), Ok(("", Specifier::Hex)));
+ assert_eq!(specifier("X"), Ok(("", Specifier::UpperHex)));
+ assert_eq!(specifier("f"), Ok(("", Specifier::Double)));
+ assert_eq!(specifier("F"), Ok(("", Specifier::UpperDouble)));
+ assert_eq!(specifier("e"), Ok(("", Specifier::Exponential)));
+ assert_eq!(specifier("E"), Ok(("", Specifier::UpperExponential)));
+ assert_eq!(specifier("g"), Ok(("", Specifier::SmallDouble)));
+ assert_eq!(specifier("G"), Ok(("", Specifier::UpperSmallDouble)));
+ assert_eq!(specifier("c"), Ok(("", Specifier::Char)));
+ assert_eq!(specifier("s"), Ok(("", Specifier::String)));
+ assert_eq!(specifier("p"), Ok(("", Specifier::Pointer)));
+
+ assert_eq!(
+ specifier("z"),
+ Err(nom::Err::Error(nom::error::Error {
+ input: "z",
+ code: nom::error::ErrorKind::MapRes
+ }))
+ );
+}
+
+#[test]
+fn test_flags() {
+ // Parse all the flags
+ assert_eq!(
+ flags("-+ #0"),
+ Ok((
+ "",
+ vec![
+ Flag::LeftJustify,
+ Flag::ForceSign,
+ Flag::SpaceSign,
+ Flag::AlternateSyntax,
+ Flag::LeadingZeros,
+ ]
+ .into_iter()
+ .collect()
+ ))
+ );
+
+ // Parse all the flags but reversed. Should produce the same set.
+ assert_eq!(
+ flags("0# +-"),
+ Ok((
+ "",
+ vec![
+ Flag::LeftJustify,
+ Flag::ForceSign,
+ Flag::SpaceSign,
+ Flag::AlternateSyntax,
+ Flag::LeadingZeros,
+ ]
+ .into_iter()
+ .collect()
+ ))
+ );
+
+ // Non-flag characters after flags are not parsed.
+ assert_eq!(
+ flags("0d"),
+ Ok(("d", vec![Flag::LeadingZeros,].into_iter().collect()))
+ );
+
+ // No flag characters returns empty set.
+ assert_eq!(flags("d"), Ok(("d", vec![].into_iter().collect())));
+}
+
+#[test]
+fn test_width() {
+ assert_eq!(
+ width("1234567890d"),
+ Ok(("d", MinFieldWidth::Fixed(1234567890)))
+ );
+ assert_eq!(width("*d"), Ok(("d", MinFieldWidth::Variable)));
+ assert_eq!(width("d"), Ok(("d", MinFieldWidth::None)));
+}
+
+#[test]
+fn test_precision() {
+ assert_eq!(
+ precision(".1234567890f"),
+ Ok(("f", Precision::Fixed(1234567890)))
+ );
+ assert_eq!(precision(".*f"), Ok(("f", Precision::Variable)));
+ assert_eq!(precision("f"), Ok(("f", Precision::None)));
+}
+#[test]
+fn test_length() {
+ assert_eq!(length("hhd"), Ok(("d", Some(Length::Char))));
+ assert_eq!(length("hd"), Ok(("d", Some(Length::Short))));
+ assert_eq!(length("ld"), Ok(("d", Some(Length::Long))));
+ assert_eq!(length("lld"), Ok(("d", Some(Length::LongLong))));
+ assert_eq!(length("Lf"), Ok(("f", Some(Length::LongDouble))));
+ assert_eq!(length("jd"), Ok(("d", Some(Length::IntMax))));
+ assert_eq!(length("zd"), Ok(("d", Some(Length::Size))));
+ assert_eq!(length("td"), Ok(("d", Some(Length::PointerDiff))));
+ assert_eq!(length("d"), Ok(("d", None)));
+}
+
+#[test]
+fn test_conversion_spec() {
+ assert_eq!(
+ conversion_spec("%d"),
+ Ok((
+ "",
+ ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::Decimal
+ }
+ ))
+ );
+
+ assert_eq!(
+ conversion_spec("%04ld"),
+ Ok((
+ "",
+ ConversionSpec {
+ flags: [Flag::LeadingZeros].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(4),
+ precision: Precision::None,
+ length: Some(Length::Long),
+ specifier: Specifier::Decimal
+ }
+ ))
+ );
+
+ assert_eq!(
+ conversion_spec("%- 4.2Lg"),
+ Ok((
+ "",
+ ConversionSpec {
+ flags: [Flag::LeftJustify, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(4),
+ precision: Precision::Fixed(2),
+ length: Some(Length::LongDouble),
+ specifier: Specifier::SmallDouble
+ }
+ ))
+ );
+}
+
+#[test]
+fn test_format_string() {
+ assert_eq!(
+ format_string("long double %+ 4.2Lg is %-03hd%%."),
+ Ok((
+ "",
+ FormatString {
+ fragments: vec![
+ FormatFragment::Literal("long double ".to_string()),
+ FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(4),
+ precision: Precision::Fixed(2),
+ length: Some(Length::LongDouble),
+ specifier: Specifier::SmallDouble
+ }),
+ FormatFragment::Literal(" is ".to_string()),
+ FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeftJustify, Flag::LeadingZeros]
+ .into_iter()
+ .collect(),
+ min_field_width: MinFieldWidth::Fixed(3),
+ precision: Precision::None,
+ length: Some(Length::Short),
+ specifier: Specifier::Decimal
+ }),
+ FormatFragment::Percent,
+ FormatFragment::Literal(".".to_string()),
+ ]
+ }
+ ))
+ );
+}
+
+#[test]
+fn test_parse() {
+ assert_eq!(
+ FormatString::parse("long double %+ 4.2Lg is %-03hd%%."),
+ Ok(FormatString {
+ fragments: vec![
+ FormatFragment::Literal("long double ".to_string()),
+ FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(4),
+ precision: Precision::Fixed(2),
+ length: Some(Length::LongDouble),
+ specifier: Specifier::SmallDouble
+ }),
+ FormatFragment::Literal(" is ".to_string()),
+ FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeftJustify, Flag::LeadingZeros]
+ .into_iter()
+ .collect(),
+ min_field_width: MinFieldWidth::Fixed(3),
+ precision: Precision::None,
+ length: Some(Length::Short),
+ specifier: Specifier::Decimal
+ }),
+ FormatFragment::Percent,
+ FormatFragment::Literal(".".to_string()),
+ ]
+ })
+ );
+}
+
+//
+// The following test cases are from //pw_tokenizer/py/decode_test.py
+//
+
+#[test]
+fn test_percent() {
+ assert_eq!(
+ FormatString::parse("%%"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Percent],
+ }),
+ );
+}
+
+#[test]
+fn test_percent_with_leading_plus_fails() {
+ assert!(FormatString::parse("%+%").is_err());
+}
+
+#[test]
+fn test_percent_with_leading_negative_fails() {
+ assert!(FormatString::parse("%-%").is_err());
+}
+
+#[test]
+fn test_percent_with_leading_space_fails() {
+ assert!(FormatString::parse("% %").is_err());
+}
+
+#[test]
+fn test_percent_with_leading_hash_fails() {
+ assert!(FormatString::parse("%#%").is_err());
+}
+
+#[test]
+fn test_percent_with_leading_zero_fails() {
+ assert!(FormatString::parse("%0%").is_err());
+}
+
+#[test]
+fn test_percent_with_length_fails() {
+ assert!(FormatString::parse("%hh%").is_err());
+ assert!(FormatString::parse("%h%").is_err());
+ assert!(FormatString::parse("%l%").is_err());
+ assert!(FormatString::parse("%L%").is_err());
+ assert!(FormatString::parse("%j%").is_err());
+ assert!(FormatString::parse("%z%").is_err());
+ assert!(FormatString::parse("%t%").is_err());
+}
+
+#[test]
+fn test_percent_with_width_fails() {
+ assert!(FormatString::parse("%9%").is_err());
+}
+
+#[test]
+fn test_percent_with_multidigit_width_fails() {
+ assert!(FormatString::parse("%10%").is_err());
+}
+
+#[test]
+fn test_percent_with_star_width_fails() {
+ assert!(FormatString::parse("%*%").is_err());
+}
+
+#[test]
+fn test_percent_with_precision_fails() {
+ assert!(FormatString::parse("%.5%").is_err());
+}
+
+#[test]
+fn test_percent_with_multidigit_precision_fails() {
+ assert!(FormatString::parse("%.10%").is_err());
+}
+
+#[test]
+fn test_percent_with_star_precision_fails() {
+ assert!(FormatString::parse("%*%").is_err());
+}
+
+const INTEGERS: &'static [(&'static str, Specifier)] = &[
+ ("d", Specifier::Decimal),
+ ("i", Specifier::Integer),
+ ("o", Specifier::Octal),
+ ("u", Specifier::Unsigned),
+ ("x", Specifier::Hex),
+ ("X", Specifier::UpperHex),
+ // While not strictly an integer pointers take the same args as integers.
+ ("p", Specifier::Pointer),
+];
+
+#[test]
+fn test_integer() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_minus() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%-5{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeftJustify].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(5),
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_plus() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%+{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_blank_space() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("% {format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_plus_and_blank_space_ignores_blank_space() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%+ {format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("% +{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_hash() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%#{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::AlternateSyntax].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone(),
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_zero() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%0{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeadingZeros].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_integer_with_length() {
+ for (format_char, specifier) in INTEGERS {
+ assert_eq!(
+ FormatString::parse(&format!("%hh{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Char),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%h{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Short),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%l{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Long),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%ll{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongLong),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%j{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::IntMax),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%z{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Size),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%t{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::PointerDiff),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+const FLOATS: &'static [(&'static str, Specifier)] = &[
+ ("f", Specifier::Double),
+ ("F", Specifier::UpperDouble),
+ ("e", Specifier::Exponential),
+ ("E", Specifier::UpperExponential),
+ ("g", Specifier::SmallDouble),
+ ("G", Specifier::UpperSmallDouble),
+];
+
+#[test]
+fn test_float() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_minus() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%-10{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeftJustify].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(10),
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_plus() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%+{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_blank_space() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("% {format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_plus_and_blank_space_ignores_blank_space() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%+ {format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("% +{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::ForceSign, Flag::SpaceSign].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_hash() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%.0{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Fixed(0),
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%#.0{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::AlternateSyntax].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Fixed(0),
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_zero() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%010{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeadingZeros].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(10),
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_length() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%hh{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Char),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%h{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Short),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%l{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Long),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%ll{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongLong),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%j{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::IntMax),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%z{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Size),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%t{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::PointerDiff),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%L{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongDouble),
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_width() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%9{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(9),
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_multidigit_width() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%10{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(10),
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_star_width() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%*{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Variable,
+ precision: Precision::None,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_precision() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%.4{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Fixed(4),
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_multidigit_precision() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%.10{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Fixed(10),
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_star_precision() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%.*{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Variable,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_float_with_star_width_and_star_precision() {
+ for (format_char, specifier) in FLOATS {
+ assert_eq!(
+ FormatString::parse(&format!("%*.*{format_char}")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Variable,
+ precision: Precision::Variable,
+ length: None,
+ specifier: specifier.clone()
+ })]
+ })
+ );
+ }
+}
+
+#[test]
+fn test_char() {
+ assert_eq!(
+ FormatString::parse("%c"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::Char
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_char_with_minus() {
+ assert_eq!(
+ FormatString::parse("%-5c"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeftJustify].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(5),
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::Char
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_char_with_plus() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%+c").is_ok());
+}
+
+#[test]
+fn test_char_with_blank_space() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("% c").is_ok());
+}
+
+#[test]
+fn test_char_with_hash() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%#c").is_ok());
+}
+
+#[test]
+fn test_char_with_zero() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%0c").is_ok());
+}
+
+#[test]
+fn test_char_with_length() {
+ // Length modifiers are ignored by %c but are still returned by the
+ // parser.
+ assert_eq!(
+ FormatString::parse("%hhc"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Char),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse("%hc"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Short),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%lc")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Long),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%llc")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongLong),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%jc")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::IntMax),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%zc")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Size),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%tc")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::PointerDiff),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%Lc")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongDouble),
+ specifier: Specifier::Char
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_char_with_width() {
+ assert_eq!(
+ FormatString::parse("%5c"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(5),
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::Char
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_char_with_multidigit_width() {
+ assert_eq!(
+ FormatString::parse("%10c"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(10),
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::Char
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_char_with_star_width() {
+ assert_eq!(
+ FormatString::parse("%*c"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Variable,
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::Char
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_char_with_precision() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%.4c").is_ok());
+}
+
+#[test]
+fn test_long_char_with_hash() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%#lc").is_ok());
+}
+
+#[test]
+fn test_long_char_with_zero() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%0lc").is_ok());
+}
+
+#[test]
+fn test_string() {
+ assert_eq!(
+ FormatString::parse("%s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_minus() {
+ assert_eq!(
+ FormatString::parse("%-6s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [Flag::LeftJustify].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(6),
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_plus() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%+s").is_ok());
+}
+
+#[test]
+fn test_string_with_blank_space() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("% s").is_ok());
+}
+
+#[test]
+fn test_string_with_hash() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%#s").is_ok());
+}
+
+#[test]
+fn test_string_with_zero() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%0s").is_ok());
+}
+
+#[test]
+fn test_string_with_length() {
+ // Length modifiers are ignored by %s but are still returned by the
+ // parser.
+ assert_eq!(
+ FormatString::parse("%hhs"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Char),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse("%hs"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Short),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%ls")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Long),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%lls")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongLong),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%js")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::IntMax),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%zs")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::Size),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%ts")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::PointerDiff),
+ specifier: Specifier::String
+ })]
+ })
+ );
+
+ assert_eq!(
+ FormatString::parse(&format!("%Ls")),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: HashSet::new(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::None,
+ length: Some(Length::LongDouble),
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_width() {
+ assert_eq!(
+ FormatString::parse("%6s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(6),
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_multidigit_width() {
+ assert_eq!(
+ FormatString::parse("%10s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(10),
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_star_width() {
+ assert_eq!(
+ FormatString::parse("%*s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Variable,
+ precision: Precision::None,
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_star_precision() {
+ assert_eq!(
+ FormatString::parse("%.3s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Fixed(3),
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_multidigit_precision() {
+ assert_eq!(
+ FormatString::parse("%.10s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::None,
+ precision: Precision::Fixed(10),
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_width_and_precision() {
+ assert_eq!(
+ FormatString::parse("%10.3s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Fixed(10),
+ precision: Precision::Fixed(3),
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_string_with_star_width_and_star_precision() {
+ assert_eq!(
+ FormatString::parse("%*.*s"),
+ Ok(FormatString {
+ fragments: vec![FormatFragment::Conversion(ConversionSpec {
+ flags: [].into_iter().collect(),
+ min_field_width: MinFieldWidth::Variable,
+ precision: Precision::Variable,
+ length: None,
+ specifier: Specifier::String
+ })]
+ })
+ );
+}
+
+#[test]
+fn test_long_string_with_hash() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%#ls").is_ok());
+}
+
+#[test]
+fn test_long_string_with_zero() {
+ // TODO: b/281750433 - This test should fail.
+ assert!(FormatString::parse("%0ls").is_ok());
+}
diff --git a/pw_format/rust/pw_format_example_macro.rs b/pw_format/rust/pw_format_example_macro.rs
new file mode 100644
index 000000000..3c9806f66
--- /dev/null
+++ b/pw_format/rust/pw_format_example_macro.rs
@@ -0,0 +1,142 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use proc_macro::TokenStream;
+use pw_format::macros::{
+ generate, FormatAndArgs, FormatMacroGenerator, IntegerDisplayType, Result,
+};
+use quote::quote;
+use syn::{
+ parse::{Parse, ParseStream},
+ parse_macro_input, Expr, Token,
+};
+
+type TokenStream2 = proc_macro2::TokenStream;
+
+// Declare a struct to hold our proc macro arguments.
+#[derive(Debug)]
+struct MacroArgs {
+ prefix: Expr,
+ format_and_args: FormatAndArgs,
+}
+
+// Implement `Parse` for our argument struct.
+impl Parse for MacroArgs {
+ fn parse(input: ParseStream) -> syn::parse::Result<Self> {
+ // Our prefix argument comes first argument and ends with a `,`.
+ let prefix: Expr = input.parse()?;
+ input.parse::<Token![,]>()?;
+
+ // Prase the remaining arguments as a format string and arguments.
+ let format_and_args: FormatAndArgs = input.parse()?;
+
+ Ok(MacroArgs {
+ prefix,
+ format_and_args,
+ })
+ }
+}
+
+// Our generator struct needs to track the prefix as well as the code
+// fragments we've generated.
+struct Generator {
+ prefix: Expr,
+ code_fragments: Vec<TokenStream2>,
+}
+
+impl Generator {
+ pub fn new(prefix: Expr) -> Self {
+ Self {
+ prefix,
+ code_fragments: Vec::new(),
+ }
+ }
+}
+
+// This toy example implements the generator by calling `format!()` at runtime.
+impl FormatMacroGenerator for Generator {
+ // Wrap all our fragments in boilerplate and return the code.
+ fn finalize(self) -> Result<TokenStream2> {
+ // Create locally scoped alias so we can refer to them in `quote!()`.
+ let prefix = self.prefix;
+ let code_fragments = self.code_fragments;
+
+ Ok(quote! {
+ {
+ // Initialize the result string with our prefix.
+ let mut result = String::new();
+ result.push_str(#prefix);
+
+ // Enumerate all our code fragments.
+ #(#code_fragments);*;
+
+ // Return the resulting string
+ result
+ }
+ })
+ }
+
+ // Handles a string embedded in the format string. This is different from
+ // `string_conversion` which is used to handle a string referenced by the
+ // format string (i.e. "%s'").
+ fn string_fragment(&mut self, string: &str) -> Result<()> {
+ self.code_fragments.push(quote! {
+ result.push_str(#string);
+ });
+ Ok(())
+ }
+
+ // This example ignores display type and width.
+ fn integer_conversion(
+ &mut self,
+ _display: IntegerDisplayType,
+ _type_width: u8, // in bits
+ expression: Expr,
+ ) -> Result<()> {
+ self.code_fragments.push(quote! {
+ result.push_str(&format!("{}", #expression));
+ });
+ Ok(())
+ }
+
+ fn string_conversion(&mut self, expression: Expr) -> Result<()> {
+ self.code_fragments.push(quote! {
+ result.push_str(&format!("{}", #expression));
+ });
+ Ok(())
+ }
+
+ fn char_conversion(&mut self, expression: Expr) -> Result<()> {
+ self.code_fragments.push(quote! {
+ result.push_str(&format!("{}", #expression));
+ });
+ Ok(())
+ }
+}
+
+// Lastly we declare our proc macro.
+#[proc_macro]
+pub fn example_macro(tokens: TokenStream) -> TokenStream {
+ // Parse our proc macro arguments.
+ let input = parse_macro_input!(tokens as MacroArgs);
+
+ // Create our generator.
+ let generator = Generator::new(input.prefix);
+
+ // Call into `generate()` to handle the code generation.
+ match generate(generator, input.format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
diff --git a/pw_format/rust/pw_format_example_macro_test.rs b/pw_format/rust/pw_format_example_macro_test.rs
new file mode 100644
index 000000000..f1c8dab05
--- /dev/null
+++ b/pw_format/rust/pw_format_example_macro_test.rs
@@ -0,0 +1,24 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#[cfg(test)]
+mod tests {
+ use pw_format_example_macro::example_macro;
+
+ #[test]
+ fn test() {
+ let string = example_macro!("the answer: ", "%d", 42);
+ assert_eq!(&string, "the answer: 42");
+ }
+}
diff --git a/pw_format/rust/pw_format_test_macros.rs b/pw_format/rust/pw_format_test_macros.rs
new file mode 100644
index 000000000..3c1c6643e
--- /dev/null
+++ b/pw_format/rust/pw_format_test_macros.rs
@@ -0,0 +1,227 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use proc_macro::TokenStream;
+use proc_macro2::Ident;
+use pw_format::macros::{
+ generate, generate_printf, FormatAndArgs, FormatMacroGenerator, IntegerDisplayType,
+ PrintfFormatMacroGenerator, Result,
+};
+use quote::quote;
+use syn::{
+ parse::{Parse, ParseStream},
+ parse_macro_input, Expr, Token,
+};
+
+type TokenStream2 = proc_macro2::TokenStream;
+
+// Generator for testing `generate()`.
+struct TestGenerator {
+ code_fragments: Vec<TokenStream2>,
+}
+
+impl TestGenerator {
+ pub fn new() -> Self {
+ Self {
+ code_fragments: Vec::new(),
+ }
+ }
+}
+
+impl FormatMacroGenerator for TestGenerator {
+ fn finalize(self) -> Result<TokenStream2> {
+ let code_fragments = self.code_fragments;
+
+ Ok(quote! {
+ {
+ use pw_format_test_macros_test::TestGeneratorOps;
+ let mut ops = Vec::new();
+ #(#code_fragments);*;
+ ops.push(TestGeneratorOps::Finalize);
+ ops
+ }
+ })
+ }
+
+ fn string_fragment(&mut self, string: &str) -> Result<()> {
+ self.code_fragments.push(quote! {
+ ops.push(TestGeneratorOps::StringFragment(#string.to_string()));
+ });
+ Ok(())
+ }
+
+ // This example ignores display type and width.
+ fn integer_conversion(
+ &mut self,
+ display_type: IntegerDisplayType,
+ type_width: u8, // in bits
+ _expression: Expr,
+ ) -> Result<()> {
+ self.code_fragments.push(quote! {
+ ops.push(TestGeneratorOps::IntegerConversion{
+ display_type: #display_type,
+ type_width: #type_width
+ });
+ });
+ Ok(())
+ }
+
+ fn string_conversion(&mut self, expression: Expr) -> Result<()> {
+ self.code_fragments.push(quote! {
+ ops.push(TestGeneratorOps::StringConversion);
+ });
+ Ok(())
+ }
+
+ fn char_conversion(&mut self, expression: Expr) -> Result<()> {
+ self.code_fragments.push(quote! {
+ ops.push(TestGeneratorOps::CharConversion);
+ });
+ Ok(())
+ }
+}
+
+#[proc_macro]
+pub fn generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = TestGenerator::new();
+ match generate(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Generator for testing `generate_printf()`. Allows control over the return
+// value of the conversion functions.
+struct PrintfTestGenerator {
+ format_string: Option<String>,
+ code_fragments: Vec<TokenStream2>,
+ integer_specifier_override: Option<String>,
+ string_specifier_override: Option<String>,
+ char_specifier_override: Option<String>,
+}
+
+impl PrintfTestGenerator {
+ pub fn new() -> Self {
+ Self {
+ format_string: Some(String::new()),
+ code_fragments: Vec::new(),
+ integer_specifier_override: None,
+ string_specifier_override: None,
+ char_specifier_override: None,
+ }
+ }
+
+ pub fn with_integer_specifier(mut self, specifier: &str) -> Self {
+ self.integer_specifier_override = Some(specifier.to_string());
+ self
+ }
+
+ pub fn with_string_specifier(mut self, specifier: &str) -> Self {
+ self.string_specifier_override = Some(specifier.to_string());
+ self
+ }
+
+ pub fn with_char_specifier(mut self, specifier: &str) -> Self {
+ self.char_specifier_override = Some(specifier.to_string());
+ self
+ }
+}
+
+impl PrintfFormatMacroGenerator for PrintfTestGenerator {
+ fn finalize(self, format_string: String) -> Result<TokenStream2> {
+ // Create locally scoped alias so we can refer to them in `quote!()`.
+ let code_fragments = self.code_fragments;
+
+ Ok(quote! {
+ {
+ use pw_format_test_macros_test::PrintfTestGeneratorOps;
+ let mut ops = Vec::new();
+ #(#code_fragments);*;
+ ops.push(PrintfTestGeneratorOps::Finalize);
+ (#format_string, ops)
+ }
+ })
+ }
+ fn string_fragment(&mut self, string: &str) -> Result<()> {
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::StringFragment(#string.to_string()));
+ });
+ Ok(())
+ }
+ fn integer_conversion(&mut self, ty: Ident, _expression: Expr) -> Result<Option<String>> {
+ let ty_str = format!("{ty}");
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::IntegerConversion{ ty: #ty_str.to_string() });
+ });
+ Ok(self.integer_specifier_override.clone())
+ }
+
+ fn string_conversion(&mut self, _expression: Expr) -> Result<Option<String>> {
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::StringConversion);
+ });
+ Ok(self.string_specifier_override.clone())
+ }
+
+ fn char_conversion(&mut self, _expression: Expr) -> Result<Option<String>> {
+ self.code_fragments.push(quote! {
+ ops.push(PrintfTestGeneratorOps::CharConversion);
+ });
+ Ok(self.char_specifier_override.clone())
+ }
+}
+
+#[proc_macro]
+pub fn printf_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = PrintfTestGenerator::new();
+ match generate_printf(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Causes the generator to substitute %d with %K.
+#[proc_macro]
+pub fn integer_sub_printf_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = PrintfTestGenerator::new().with_integer_specifier("%K");
+ match generate_printf(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Causes the generator to substitute %s with %K.
+#[proc_macro]
+pub fn string_sub_printf_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = PrintfTestGenerator::new().with_string_specifier("%K");
+ match generate_printf(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Causes the generator to substitute %c with %K.
+#[proc_macro]
+pub fn char_sub_printf_generator_test_macro(tokens: TokenStream) -> TokenStream {
+ let format_and_args = parse_macro_input!(tokens as FormatAndArgs);
+ let generator = PrintfTestGenerator::new().with_char_specifier("%K");
+ match generate_printf(generator, format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
diff --git a/pw_format/rust/pw_format_test_macros_test.rs b/pw_format/rust/pw_format_test_macros_test.rs
new file mode 100644
index 000000000..3ae1f546d
--- /dev/null
+++ b/pw_format/rust/pw_format_test_macros_test.rs
@@ -0,0 +1,168 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use pw_format::macros::IntegerDisplayType;
+
+// Used to record calls into the test generator from `generator_test_macro!`.
+#[derive(Debug, PartialEq)]
+pub enum TestGeneratorOps {
+ Finalize,
+ StringFragment(String),
+ IntegerConversion {
+ display_type: IntegerDisplayType,
+ type_width: u8,
+ },
+ StringConversion,
+ CharConversion,
+}
+
+// Used to record calls into the test generator from `printf_generator_test_macro!` and friends.
+#[derive(Debug, PartialEq)]
+pub enum PrintfTestGeneratorOps {
+ Finalize,
+ StringFragment(String),
+ IntegerConversion { ty: String },
+ StringConversion,
+ CharConversion,
+}
+
+#[cfg(test)]
+mod tests {
+ use pw_format::macros::IntegerDisplayType;
+ use pw_format_test_macros::{
+ char_sub_printf_generator_test_macro, generator_test_macro,
+ integer_sub_printf_generator_test_macro, printf_generator_test_macro,
+ string_sub_printf_generator_test_macro,
+ };
+
+ // Create an alias to ourselves so that the proc macro can name our crate.
+ use crate as pw_format_test_macros_test;
+
+ use super::*;
+
+ #[test]
+ fn generate_calls_generator_correctly() {
+ assert_eq!(
+ generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ vec![
+ TestGeneratorOps::StringFragment("test ".to_string()),
+ TestGeneratorOps::IntegerConversion {
+ display_type: IntegerDisplayType::Signed,
+ type_width: 32,
+ },
+ TestGeneratorOps::StringFragment(" ".to_string()),
+ TestGeneratorOps::StringConversion,
+ TestGeneratorOps::StringFragment(" ".to_string()),
+ TestGeneratorOps::CharConversion,
+ TestGeneratorOps::Finalize
+ ]
+ );
+ }
+
+ #[test]
+ fn generate_printf_calls_generator_correctly() {
+ assert_eq!(
+ printf_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ // %ld gets converted to %d because they are equivalent for 32 bit
+ // systems.
+ "test %d %s %c",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+
+ // Test that a generator returning an overridden integer conversion specifier
+ // changes that and only that conversion specifier in the format string.
+ #[test]
+ fn generate_printf_substitutes_integer_conversion() {
+ assert_eq!(
+ integer_sub_printf_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ "test %K %s %c",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+
+ // Test that a generator returning an overridden string conversion specifier
+ // changes that and only that conversion specifier in the format string.
+ #[test]
+ fn generate_printf_substitutes_string_conversion() {
+ assert_eq!(
+ string_sub_printf_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ // %ld gets converted to %d because they are equivalent for 32 bit
+ // systems.
+ "test %d %K %c",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+
+ // Test that a generator returning an overridden character conversion specifier
+ // changes that and only that conversion specifier in the format string.
+ #[test]
+ fn generate_printf_substitutes_char_conversion() {
+ assert_eq!(
+ char_sub_printf_generator_test_macro!("test %ld %s %c", 5, "test", 'c'),
+ (
+ // %ld gets converted to %d because they are equivalent for 32 bit
+ // systems.
+ "test %d %s %K",
+ vec![
+ PrintfTestGeneratorOps::StringFragment("test ".to_string()),
+ PrintfTestGeneratorOps::IntegerConversion {
+ ty: "i32".to_string(),
+ },
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::StringConversion,
+ PrintfTestGeneratorOps::StringFragment(" ".to_string()),
+ PrintfTestGeneratorOps::CharConversion,
+ PrintfTestGeneratorOps::Finalize
+ ]
+ )
+ );
+ }
+}
diff --git a/pw_function/Android.bp b/pw_function/Android.bp
index 70e985087..3ad39a059 100644
--- a/pw_function/Android.bp
+++ b/pw_function/Android.bp
@@ -23,5 +23,11 @@ cc_library_headers {
export_include_dirs: [
"public",
],
+ header_libs: [
+ "fuchsia_sdk_lib_fit",
+ ],
+ export_header_lib_headers: [
+ "fuchsia_sdk_lib_fit",
+ ],
host_supported: true,
}
diff --git a/pw_function/BUILD.gn b/pw_function/BUILD.gn
index efea1cc8d..e3c16cb27 100644
--- a/pw_function/BUILD.gn
+++ b/pw_function/BUILD.gn
@@ -18,15 +18,9 @@ import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_function/function.gni")
import("$dir_pw_unit_test/test.gni")
-declare_args() {
- # The build target that overrides the default configuration options for this
- # module. This should point to a source set that provides defines through a
- # public config (which may -include a file or add defines directly).
- pw_function_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
-}
-
config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
@@ -35,7 +29,10 @@ config("public_include_path") {
pw_source_set("config") {
public = [ "public/pw_function/config.h" ]
public_configs = [ ":public_include_path" ]
- public_deps = [ pw_function_CONFIG ]
+ public_deps = [
+ "$dir_pw_third_party/fuchsia:fit",
+ pw_function_CONFIG,
+ ]
visibility = [ ":*" ]
}
@@ -50,6 +47,16 @@ pw_source_set("pw_function") {
public = [ "public/pw_function/function.h" ]
}
+config("enable_dynamic_allocation_config") {
+ defines = [ "PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION=1" ]
+ visibility = [ ":*" ]
+}
+
+# Use this for pw_function_CONFIG to enable dynamic allocation.
+pw_source_set("enable_dynamic_allocation") {
+ public_configs = [ ":enable_dynamic_allocation_config" ]
+}
+
pw_source_set("pointer") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_function/pointer.h" ]
diff --git a/pw_function/CMakeLists.txt b/pw_function/CMakeLists.txt
index 8b32db534..981178cfb 100644
--- a/pw_function/CMakeLists.txt
+++ b/pw_function/CMakeLists.txt
@@ -36,9 +36,6 @@ pw_add_library(pw_function INTERFACE
pw_preprocessor
pw_third_party.fuchsia.fit
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_FUNCTION)
- zephyr_link_libraries(pw_function)
-endif()
pw_add_test(pw_function.function_test
SOURCES
diff --git a/pw_function/Kconfig b/pw_function/Kconfig
index e9ff1ef3e..01e56bd93 100644
--- a/pw_function/Kconfig
+++ b/pw_function/Kconfig
@@ -12,7 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_function"
+
config PIGWEED_FUNCTION
- bool "Enable the Pigweed function library (pw_function)"
+ bool "Link pw_function library"
select PIGWEED_ASSERT
select PIGWEED_PREPROCESSOR
+ help
+ See :ref:`module-pw_function` for module details.
+
+endmenu
diff --git a/pw_function/docs.rst b/pw_function/docs.rst
index 18041ef51..b8d9b7ed8 100644
--- a/pw_function/docs.rst
+++ b/pw_function/docs.rst
@@ -23,36 +23,36 @@ the stored callable.
.. code-block:: c++
- int Add(int a, int b) { return a + b; }
+ int Add(int a, int b) { return a + b; }
- // Construct a Function object from a function pointer.
- pw::Function<int(int, int)> add_function(Add);
+ // Construct a Function object from a function pointer.
+ pw::Function<int(int, int)> add_function(Add);
- // Invoke the function object.
- int result = add_function(3, 5);
- EXPECT_EQ(result, 8);
+ // Invoke the function object.
+ int result = add_function(3, 5);
+ EXPECT_EQ(result, 8);
- // Construct a function from a lambda.
- pw::Function<int(int)> negate([](int value) { return -value; });
- EXPECT_EQ(negate(27), -27);
+ // Construct a function from a lambda.
+ pw::Function<int(int)> negate([](int value) { return -value; });
+ EXPECT_EQ(negate(27), -27);
Functions are nullable. Invoking a null function triggers a runtime assert.
.. code-block:: c++
- // A function initialized without a callable is implicitly null.
- pw::Function<void()> null_function;
+ // A function initialized without a callable is implicitly null.
+ pw::Function<void()> null_function;
- // Null functions may also be explicitly created or set.
- pw::Function<void()> explicit_null_function(nullptr);
+ // Null functions may also be explicitly created or set.
+ pw::Function<void()> explicit_null_function(nullptr);
- pw::Function<void()> function([]() {}); // Valid (non-null) function.
- function = nullptr; // Set to null, clearing the stored callable.
+ pw::Function<void()> function([]() {}); // Valid (non-null) function.
+ function = nullptr; // Set to null, clearing the stored callable.
- // Functions are comparable to nullptr.
- if (function != nullptr) {
- function();
- }
+ // Functions are comparable to nullptr.
+ if (function != nullptr) {
+ function();
+ }
:cpp:type:`pw::Function`'s default constructor is ``constexpr``, so
default-constructed functions may be used in classes with ``constexpr``
@@ -60,23 +60,22 @@ constructors and in ``constinit`` expressions.
.. code-block:: c++
- class MyClass {
- public:
- // Default construction of a pw::Function is constexpr.
- constexpr MyClass() { ... }
+ class MyClass {
+ public:
+ // Default construction of a pw::Function is constexpr.
+ constexpr MyClass() { ... }
- pw::Function<void(int)> my_function;
- };
+ pw::Function<void(int)> my_function;
+ };
- // pw::Function and classes that use it may be constant initialized.
- constinit MyClass instance;
+ // pw::Function and classes that use it may be constant initialized.
+ constinit MyClass instance;
Storage
=======
By default, a ``Function`` stores its callable inline within the object. The
-inline storage size defaults to the size of two pointers, but is configurable
-through the build system. The size of a ``Function`` object is equivalent to its
-inline storage size.
+inline storage size defaults to the size of one pointer, but is configurable
+through the build system.
The :cpp:type:`pw::InlineFunction` alias is similar to :cpp:type:`pw::Function`,
but is always inlined. That is, even if dynamic allocation is enabled for
@@ -88,36 +87,41 @@ is a compile-time error unless dynamic allocation is enabled.
.. admonition:: Inline storage size
- The default inline size of two pointers is sufficient to store most common
- callable objects, including function pointers, simple non-capturing and
- capturing lambdas, and lightweight custom classes.
+ The default inline size of one pointer is sufficient to store most common
+ callable objects, including function pointers, simple non-capturing and
+ capturing lambdas, and lightweight custom classes.
.. code-block:: c++
- // The lambda is moved into the function's internal storage.
- pw::Function<int(int, int)> subtract([](int a, int b) { return a - b; });
+ // The lambda is moved into the function's internal storage.
+ pw::Function<int(int, int)> subtract([](int a, int b) { return a - b; });
- // Functions can be also be constructed from custom classes that implement
- // operator(). This particular object is large (8 ints of space).
- class MyCallable {
- public:
- int operator()(int value);
+ // Functions can be also be constructed from custom classes that implement
+ // operator(). This particular object is large (8 ints of space).
+ class MyCallable {
+ public:
+ int operator()(int value);
- private:
- int data_[8];
- };
+ private:
+ int data_[8];
+ };
- // Compiler error: sizeof(MyCallable) exceeds function's inline storage size.
- pw::Function<int(int)> function((MyCallable()));
+ // Compiler error: sizeof(MyCallable) exceeds function's inline storage size.
+ pw::Function<int(int)> function((MyCallable()));
.. admonition:: Dynamic allocation
- When ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled, a ``Function``
- will use dynamic allocation to store callables that exceed the inline size.
- When it is enabled but a compile-time check for the inlining is still required
- ``pw::InlineFunction`` can be used.
+ When ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled, a ``Function``
+ will use dynamic allocation to store callables that exceed the inline size.
+ An Allocator type can be optionally supplied as a template argument. The
+ default Allocator type can also be changed by overriding
+ ``PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE`` (the ``value_type`` of the Allocator
+ is irrelevant, since it must support rebinding). When dynamic allocation is
+ enabled but a compile-time check for the inlining is still required
+ ``pw::InlineFunction`` can be used.
.. warning::
+
If ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled then attempts to cast
from `:cpp:type:`pw::InlineFunction` to a regular :cpp:type:`pw::Function`
will **ALWAYS** allocate memory.
@@ -132,6 +136,7 @@ Reference
.. doxygentypedef:: pw::InlineFunction
.. doxygentypedef:: pw::Callback
.. doxygentypedef:: pw::InlineCallback
+.. doxygenfunction:: pw::bind_member
``pw::Function`` as a function parameter
========================================
@@ -140,12 +145,12 @@ place of a function pointer or equivalent callable.
.. code-block:: c++
- // Before:
- void DoTheThing(int arg, void (*callback)(int result));
+ // Before:
+ void DoTheThing(int arg, void (*callback)(int result));
- // After. Note that it is possible to have parameter names within the function
- // signature template for clarity.
- void DoTheThing(int arg, const pw::Function<void(int result)>& callback);
+ // After. Note that it is possible to have parameter names within the function
+ // signature template for clarity.
+ void DoTheThing(int arg, const pw::Function<void(int result)>& callback);
:cpp:type:`pw::Function` is movable, but not copyable, so APIs must accept
:cpp:type:`pw::Function` objects either by const reference (``const
@@ -157,29 +162,30 @@ should be passed as an rvalue reference and moved into a
.. code-block:: c++
- // This function calls a pw::Function but doesn't store it, so it takes a
- // const reference.
- void CallTheCallback(const pw::Function<void(int)>& callback) {
- callback(123);
- }
+ // This function calls a pw::Function but doesn't store it, so it takes a
+ // const reference.
+ void CallTheCallback(const pw::Function<void(int)>& callback) {
+ callback(123);
+ }
- // This function move-assigns a pw::Function to another variable, so it takes
- // an rvalue reference.
- void StoreTheCallback(pw::Function<void(int)>&& callback) {
- stored_callback_ = std::move(callback);
- }
+ // This function move-assigns a pw::Function to another variable, so it takes
+ // an rvalue reference.
+ void StoreTheCallback(pw::Function<void(int)>&& callback) {
+ stored_callback_ = std::move(callback);
+ }
.. admonition:: Rules of thumb for passing a :cpp:type:`pw::Function` to a function
* **Pass by value**: Never.
-
This results in unnecessary :cpp:type:`pw::Function` instances and move
operations.
+
* **Pass by const reference** (``const pw::Function&``): When the
:cpp:type:`pw::Function` is only invoked.
When a :cpp:type:`pw::Function` is called or inspected, but not moved, take
a const reference to avoid copies and support temporaries.
+
* **Pass by rvalue reference** (``pw::Function&&``): When the
:cpp:type:`pw::Function` is moved.
@@ -190,6 +196,7 @@ should be passed as an rvalue reference and moved into a
:cpp:type:`pw::Function` variable, which makes the transfer of ownership
explicit. It is possible to move-assign from an lvalue reference, but this
fails to make it obvious to the caller that the object is no longer valid.
+
* **Pass by non-const reference** (``pw::Function&``): Rarely, when modifying
a variable.
@@ -206,11 +213,11 @@ the callable object. There is no need to create an intermediate
.. code-block:: c++
- // Implicitly creates a pw::Function from a capturing lambda and calls it.
- CallTheCallback([this](int result) { result_ = result; });
+ // Implicitly creates a pw::Function from a capturing lambda and calls it.
+ CallTheCallback([this](int result) { result_ = result; });
- // Implicitly creates a pw::Function from a capturing lambda and stores it.
- StoreTheCallback([this](int result) { result_ = result; });
+ // Implicitly creates a pw::Function from a capturing lambda and stores it.
+ StoreTheCallback([this](int result) { result_ = result; });
When working with an existing :cpp:type:`pw::Function` variable, the variable
can be passed directly to functions that take a const reference. If the function
@@ -219,11 +226,11 @@ takes ownership of the :cpp:type:`pw::Function`, move the
.. code-block:: c++
- // Accepts the pw::Function by const reference.
- CallTheCallback(my_function_);
+ // Accepts the pw::Function by const reference.
+ CallTheCallback(my_function_);
- // Takes ownership of the pw::Function.
- void StoreTheCallback(std::move(my_function));
+ // Takes ownership of the pw::Function.
+ void StoreTheCallback(std::move(my_function));
``pw::Callback`` for one-shot functions
=======================================
@@ -235,6 +242,11 @@ nullptr.
Invoking ``pw::Function`` from a C-style API
============================================
+.. _trampoline layers: https://en.wikipedia.org/wiki/Trampoline_(computing)
+
+One use case for invoking ``pw_function`` from a C-style API is to automate
+the generation of `trampoline layers`_.
+
.. doxygenfile:: pw_function/pointer.h
:sections: detaileddescription
@@ -247,7 +259,7 @@ Invoking ``pw::Function`` from a C-style API
ScopeGuard
----------
.. doxygenclass:: pw::ScopeGuard
- :members:
+ :members:
------------
Size reports
@@ -271,10 +283,25 @@ be used as a reference when sizing external buffers for ``Function`` objects.
Design
------
:cpp:type:`pw::Function` is an alias of
-`fit::function <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_.
+`fit::function_impl <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h>`_.
:cpp:type:`pw::Callback` is an alias of
-`fit::callback <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_.
+`fit::callback_impl <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h>`_.
+
+.. _module-pw_function-non-literal:
+
+Why pw::Function is not a literal
+=================================
+The default constructor for ``pw::Function`` is ``constexpr`` but
+``pw::Function`` is not a literal type. Instances can be declared ``constinit``
+but can't be used in ``constexpr`` contexts. There are a few reasons for this:
+
+* ``pw::Function`` supports wrapping any callable type, and the wrapped type
+ might not be a literal type.
+* ``pw::Function`` stores inline callables in a bytes array, which is not
+ ``constexpr``-friendly.
+* ``pw::Function`` optionally uses dynamic allocation, which doesn't work in
+ ``constexpr`` contexts (at least before C++20).
------
Zephyr
diff --git a/pw_function/function.gni b/pw_function/function.gni
new file mode 100644
index 000000000..1d0ce14ba
--- /dev/null
+++ b/pw_function/function.gni
@@ -0,0 +1,23 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/module_config.gni")
+
+declare_args() {
+ # The build target that overrides the default configuration options for this
+ # module. This should point to a source set that provides defines through a
+ # public config (which may -include a file or add defines directly).
+ pw_function_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+}
diff --git a/pw_function/public/pw_function/config.h b/pw_function/public/pw_function/config.h
index b83489adc..e62f38d92 100644
--- a/pw_function/public/pw_function/config.h
+++ b/pw_function/public/pw_function/config.h
@@ -17,12 +17,15 @@
#include <cstddef>
-// The maximum size of a callable that can be inlined within a function. This is
-// also the size of the Function object itself. Callables larger than this are
-// stored externally to the function.
+#include "lib/fit/function.h"
+
+// The maximum size of a callable that can be inlined within a function.
+// Callables larger than this are stored externally to the function (if dynamic
+// allocation is enabled).
//
-// This defaults to 1 pointer, which is capable of storing common callables
-// such as function pointers and lambdas with a single capture.
+// This defaults to the size of 1 pointer, which is capable of storing common
+// callables such as function pointers and lambdas with a single pointer's size
+// of captured data.
#ifndef PW_FUNCTION_INLINE_CALLABLE_SIZE
#define PW_FUNCTION_INLINE_CALLABLE_SIZE (sizeof(void*))
#endif // PW_FUNCTION_INLINE_CALLABLE_SIZE
@@ -30,12 +33,30 @@
static_assert(PW_FUNCTION_INLINE_CALLABLE_SIZE > 0 &&
PW_FUNCTION_INLINE_CALLABLE_SIZE % alignof(void*) == 0);
-// Whether functions should allocate memory dynamically (using operator new) if
-// a callable is larger than the inline size.
+// Whether functions should allocate memory dynamically if a callable is larger
+// than the inline size.
+//
+// Enabling this allows functions to support callables larger than
+// `PW_FUNCTION_INLINE_CALLABLE_SIZE` by dynamically allocating storage space
+// for them. The Allocator type used can be provided as a template argument, and
+// the default type can be specified by overriding
+// `PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE`. The Allocator must satisfy the C++
+// named requirements for Allocator:
+// https://en.cppreference.com/w/cpp/named_req/Allocator
#ifndef PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION
#define PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION 0
#endif // PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION
+// The default Allocator used to dynamically allocate the callable, if dynamic
+// allocation is enabled. Its `value_type` is irrelevant, since it must support
+// rebinding.
+//
+// This definition is useful to ensure that a project can specify an Allocator
+// that will be used even by Pigweed source code.
+#ifndef PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE
+#define PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE fit::default_callable_allocator
+#endif // PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE
+
namespace pw::function_internal::config {
inline constexpr size_t kInlineCallableSize = PW_FUNCTION_INLINE_CALLABLE_SIZE;
diff --git a/pw_function/public/pw_function/function.h b/pw_function/public/pw_function/function.h
index 99842b348..956a17291 100644
--- a/pw_function/public/pw_function/function.h
+++ b/pw_function/public/pw_function/function.h
@@ -13,6 +13,8 @@
// the License.
#pragma once
+#include <cstddef>
+
#include "lib/fit/function.h"
#include "pw_function/config.h"
@@ -26,7 +28,7 @@ namespace pw {
///
/// template <typename T>
/// bool All(const pw::Vector<T>& items,
-/// pw::Function<bool(const T& item)> predicate) {
+/// const pw::Function<bool(const T& item)>& predicate) {
/// for (const T& item : items) {
/// if (!predicate(item)) {
/// return false;
@@ -46,13 +48,19 @@ namespace pw {
/// }
///
/// @endcode
-template <typename Callable,
- size_t inline_target_size =
- function_internal::config::kInlineCallableSize>
+///
+/// @tparam Allocator The Allocator used to dynamically allocate the callable,
+/// if it exceeds `inline_target_size` and dynamic allocation is enabled. Its
+/// `value_type` is irrelevant, since it must support rebinding.
+template <typename FunctionType,
+ std::size_t inline_target_size =
+ function_internal::config::kInlineCallableSize,
+ typename Allocator = PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE>
using Function = fit::function_impl<
inline_target_size,
/*require_inline=*/!function_internal::config::kEnableDynamicAllocation,
- Callable>;
+ FunctionType,
+ Allocator>;
/// Version of `pw::Function` that exclusively uses inline storage.
///
@@ -60,12 +68,12 @@ using Function = fit::function_impl<
/// any attempt to convert `pw::InlineFunction` to `pw::Function` will ALWAYS
/// allocate.
///
-// TODO(b/252852651): Remove warning above when conversion from
+// TODO: b/252852651 - Remove warning above when conversion from
// `fit::inline_function` to `fit::function` doesn't allocate anymore.
-template <typename Callable,
- size_t inline_target_size =
+template <typename FunctionType,
+ std::size_t inline_target_size =
function_internal::config::kInlineCallableSize>
-using InlineFunction = fit::inline_function<Callable, inline_target_size>;
+using InlineFunction = fit::inline_function<FunctionType, inline_target_size>;
using Closure = Function<void()>;
@@ -79,18 +87,33 @@ using Closure = Function<void()>;
///
/// A `pw::Callback` in the "already called" state has the same state as a
/// `pw::Callback` that has been assigned to `nullptr`.
-template <typename Callable,
- size_t inline_target_size =
- function_internal::config::kInlineCallableSize>
+template <typename FunctionType,
+ std::size_t inline_target_size =
+ function_internal::config::kInlineCallableSize,
+ typename Allocator = PW_FUNCTION_DEFAULT_ALLOCATOR_TYPE>
using Callback = fit::callback_impl<
inline_target_size,
/*require_inline=*/!function_internal::config::kEnableDynamicAllocation,
- Callable>;
+ FunctionType,
+ Allocator>;
/// Version of `pw::Callback` that exclusively uses inline storage.
-template <typename Callable,
- size_t inline_target_size =
+template <typename FunctionType,
+ std::size_t inline_target_size =
function_internal::config::kInlineCallableSize>
-using InlineCallback = fit::inline_callback<Callable, inline_target_size>;
+using InlineCallback = fit::inline_callback<FunctionType, inline_target_size>;
+
+/// Returns a `Callable` which, when called, invokes `method` on `instance`
+/// using the arguments provided.
+///
+/// This is useful for binding the `this` argument of a callable.
+///
+/// `pw::bind_member<&T::MethodName>(instance)` is roughly equivalent to
+/// `[instance](Arg arg1, ...) { instance->MethodName(arg1, ...) }`, albeit with
+/// proper support for overloads and argument forwarding.
+template <auto method, typename T>
+auto bind_member(T* instance) {
+ return fit::bind_member<method, T>(instance);
+}
} // namespace pw
diff --git a/pw_function/size_report/callable_size.cc b/pw_function/size_report/callable_size.cc
index 3b00b2447..ba5617ba1 100644
--- a/pw_function/size_report/callable_size.cc
+++ b/pw_function/size_report/callable_size.cc
@@ -37,7 +37,7 @@ class CustomCallableClass {
void operator()() {}
private:
- std::array<std::byte, 16> data_;
+ [[maybe_unused]] std::array<std::byte, 16> data_;
};
} // namespace
@@ -61,9 +61,14 @@ int main() {
#elif defined(_SIMPLE_LAMBDA)
static CallableSize callable_size([]() {});
#elif defined(_CAPTURING_LAMBDA)
- static CallableSize callable_size([a]() {});
+ static CallableSize callable_size([a]() { static_cast<void>(a); });
#elif defined(_MULTI_CAPTURING_LAMBDA)
- static CallableSize callable_size([a, b, c, d]() {});
+ static CallableSize callable_size([a, b, c, d]() {
+ static_cast<void>(a);
+ static_cast<void>(b);
+ static_cast<void>(c);
+ static_cast<void>(d);
+ });
#elif defined(_CUSTOM_CLASS)
static CallableSize callable_size((CustomCallableClass()));
#endif
diff --git a/pw_fuzzer/BUILD.bazel b/pw_fuzzer/BUILD.bazel
index 4fdfcaba0..0a2db9ed8 100644
--- a/pw_fuzzer/BUILD.bazel
+++ b/pw_fuzzer/BUILD.bazel
@@ -21,8 +21,62 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+################################################################################
+# FuzzTest support
+#
+# Create FuzzTest-style fuzzers by adding a dep on //pw_fuzzer:fuzztest
+
+# Identifies when upstream FuzzTest is being used.
+config_setting(
+ name = "use_fuzztest",
+ testonly = True,
+ flag_values = {
+ "@pigweed//targets:pw_fuzzer_fuzztest_backend": "@com_google_fuzztest//fuzztest",
+ },
+)
+
+pw_cc_library(
+ name = "fuzztest",
+ testonly = True,
+ hdrs = ["public/pw_fuzzer/fuzztest.h"] + select({
+ ":use_fuzztest": ["private/pw_fuzzer/internal/fuzztest.h"],
+ "//conditions:default": [],
+ }),
+ includes = ["public"] + select({
+ ":use_fuzztest": ["private"],
+ "//conditions:default": [],
+ }),
+ deps = [
+ "//pw_containers",
+ "//pw_result",
+ "//pw_status",
+ "//pw_string",
+ "@pigweed//targets:pw_fuzzer_fuzztest_backend",
+ ],
+)
+
+pw_cc_library(
+ name = "fuzztest_stub",
+ testonly = True,
+ hdrs = [
+ "private_overrides/pw_fuzzer/internal/fuzztest.h",
+ "public_overrides/fuzztest/fuzztest.h",
+ ],
+ includes = [
+ "private_overrides",
+ "public_overrides",
+ ],
+ deps = ["//pw_unit_test"],
+)
+
+################################################################################
+# libFuzzer support
+#
+# Create libFuzzer-style fuzzers by using the `pw_fuzzer` template from
+# fuzzer.gni.
+
pw_cc_library(
- name = "pw_fuzzer",
+ name = "libfuzzer",
hdrs = [
"public/pw_fuzzer/asan_interface.h",
"public/pw_fuzzer/fuzzed_data_provider.h",
diff --git a/pw_fuzzer/BUILD.gn b/pw_fuzzer/BUILD.gn
index 142b7617d..5a1e49f97 100644
--- a/pw_fuzzer/BUILD.gn
+++ b/pw_fuzzer/BUILD.gn
@@ -14,89 +14,261 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/error.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
-import("$dir_pw_fuzzer/fuzzer.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
}
-# Add flags for adding LLVM sanitizer coverage for fuzzing. This is added by
-# the host_clang_fuzz toolchains.
-config("instrumentation") {
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- # OSS-Fuzz manipulates compiler flags directly. See
- # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
- cflags_c = string_split(getenv("CFLAGS"))
- cflags_cc = string_split(getenv("CXXFLAGS"))
-
- # OSS-Fuzz sets "-stdlib=libc++", which conflicts with the "-nostdinc++" set
- # by `pw_minimal_cpp_stdlib`.
- cflags_cc += [ "-Wno-unused-command-line-argument" ]
+config("private_include_path") {
+ include_dirs = [ "private" ]
+ visibility = [ ":*" ]
+}
+
+config("overrides_include_path") {
+ include_dirs = [
+ "public_overrides",
+ "private_overrides",
+ ]
+ visibility = [ ":*" ]
+}
+
+# See https://llvm.org/docs/LibFuzzer.html#fuzzer-friendly-build-mode
+config("fuzzing_build_mode_unsafe_for_production") {
+ defines = [ "FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" ]
+}
+
+pw_doc_group("docs") {
+ sources = [
+ "concepts.rst",
+ "docs.rst",
+ "guides/fuzztest.rst",
+ "guides/index.rst",
+ "guides/libfuzzer.rst",
+ "guides/reproducing_oss_fuzz_bugs.rst",
+ ]
+ inputs = [
+ "doc_resources/pw_fuzzer_coverage_guided.png",
+ "examples/fuzztest/BUILD.gn",
+ "examples/fuzztest/BUILD.bazel",
+ "examples/fuzztest/CMakeLists.txt",
+ "examples/fuzztest/metrics.h",
+ "examples/fuzztest/metrics_unittest.cc",
+ "examples/fuzztest/metrics_fuzztest.cc",
+ ]
+}
+
+pw_test_group("tests") {
+ group_deps = [
+ ":fuzztest_tests",
+ "examples/fuzztest:tests",
+ "examples/libfuzzer:tests",
+ ]
+}
+
+################################################################################
+# FuzzTest support
+#
+# Create FuzzTest-style fuzzers by adding a dep on dir_pw_fuzzer:fuzztest
+
+group("fuzztest") {
+ if (dir_pw_third_party_fuzztest != "" && pw_toolchain_FUZZING_ENABLED) {
+ public_deps = [ ":fuzztest.enabled" ]
} else {
- cflags = [ "-fsanitize=fuzzer-no-link" ]
+ public_deps = [ ":fuzztest.disabled" ]
}
}
-# Add flags for linking against compiler-rt's libFuzzer. This is added
-# automatically by `pw_fuzzer`.
-config("engine") {
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- # OSS-Fuzz manipulates linker flags directly. See
- # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
- ldflags = string_split(getenv("LDFLAGS")) + [ getenv("LIB_FUZZING_ENGINE") ]
- } else {
- ldflags = [ "-fsanitize=fuzzer" ]
+# Used by fuzzable unit tests when fuzzing is enabled. Includes headers and deps
+# that provide a Pigweed-compatible subset of FuzzTest, as well as extensions to
+# support common Pigweed types.
+if (dir_pw_third_party_fuzztest != "" && pw_toolchain_FUZZING_ENABLED) {
+ pw_source_set("fuzztest.enabled") {
+ public = [
+ "private/pw_fuzzer/internal/fuzztest.h",
+ "public/pw_fuzzer/fuzztest.h",
+ ]
+ public_configs = [
+ ":public_include_path",
+ ":private_include_path",
+ "$dir_pw_third_party/abseil-cpp/configs:disabled_warnings",
+ "$dir_pw_third_party/abseil-cpp/configs:public_include_path",
+ "$dir_pw_third_party/fuzztest/configs:disabled_warnings",
+ "$dir_pw_third_party/fuzztest/configs:public_include_path",
+ "$dir_pw_third_party/re2/configs:disabled_warnings",
+ "$dir_pw_third_party/re2/configs:public_include_path",
+ ]
+ public_deps = [
+ "$dir_pw_third_party/fuzztest/fuzztest",
+ dir_pw_containers,
+ dir_pw_result,
+ dir_pw_status,
+ dir_pw_string,
+ ]
}
}
-pw_source_set("pw_fuzzer") {
- public_configs = [ ":public_include_path" ]
+# Used by fuzzable unit tests when fuzzing is disabled. Includes stubs of the
+# Pigweed-compatible subset of FuzzTest's interface, as well as extensions to
+# support common Pigweed types.
+pw_source_set("fuzztest.disabled") {
public = [
- "public/pw_fuzzer/asan_interface.h",
- "public/pw_fuzzer/fuzzed_data_provider.h",
+ "private_overrides/pw_fuzzer/internal/fuzztest.h",
+ "public/pw_fuzzer/fuzztest.h",
+ "public_overrides/fuzztest/fuzztest.h",
+ ]
+ public_configs = [
+ ":public_include_path",
+ ":overrides_include_path",
+ ]
+ public_deps = [
+ dir_pw_containers,
+ dir_pw_result,
+ dir_pw_status,
+ dir_pw_string,
]
- public_deps = [ "$dir_pw_log" ]
}
-pw_source_set("run_as_unit_test") {
- configs = [ ":public_include_path" ]
- sources = [ "pw_fuzzer_disabled.cc" ]
- deps = [
- dir_pw_log,
- dir_pw_unit_test,
- ]
+pw_test("fuzztest_tests") {
+ sources = [ "domain_test.cc" ]
+ deps = [ ":fuzztest" ]
}
-# See https://llvm.org/docs/LibFuzzer.html#fuzzer-friendly-build-mode
-config("fuzzing_build_mode_unsafe_for_production") {
- defines = [ "FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" ]
+# This target should only be used when defining a fuzzing toolchain, e.g. to set
+# `pw_unit_test_GOOGLETEST_BACKEND = "$dir_pw_fuzzer:gtest"
+# TODO: b/295961502 - Support running FuzzTest-based fuzzers on OSS-Fuzz.
+if (dir_pw_third_party_googletest == "") {
+ pw_error("gtest") {
+ message_lines = [
+ "pw_unit_test_GOOGLETEST_BACKEND is set to dir_pw_fuzzer:gtest, ",
+ "but dir_pw_third_party_googletest is not set.",
+ ]
+ }
+} else if (!pw_toolchain_FUZZING_ENABLED) {
+ pw_error("gtest") {
+ message_lines = [
+ "pw_unit_test_GOOGLETEST_BACKEND is set to dir_pw_fuzzer:gtest, ",
+ "but $current_toolchain does not support fuzzing.",
+ ]
+ }
+} else {
+ group("gtest") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ public_deps = [ "$dir_pw_unit_test:light" ]
+ } else {
+ public_deps = [ "$dir_pw_third_party/googletest" ]
+ }
+ }
}
-config("fuzzing_verbose_logging") {
- defines = [ "FUZZING_VERBOSE_LOGGING" ]
+# This target should only be used when defining a fuzzing toolchain, e.g. to set
+# `pw_unit_test_MAIN = "$dir_pw_fuzzer:fuzztest_main"
+# TODO: b/295961502 - Support running FuzzTest-based fuzzers on OSS-Fuzz.
+if (dir_pw_third_party_fuzztest == "") {
+ pw_error("fuzztest_main") {
+ message_lines = [
+ "pw_unit_test_MAIN is set to dir_pw_fuzzer:fuzztest_main, ",
+ "but dir_pw_third_party_fuzztest is not set.",
+ ]
+ }
+} else if (!pw_toolchain_FUZZING_ENABLED) {
+ pw_error("fuzztest_main") {
+ message_lines = [
+ "pw_unit_test_MAIN is set to dir_pw_fuzzer:fuzztest_main, ",
+ "but $current_toolchain does not support fuzzing.",
+ ]
+ }
+} else {
+ group("fuzztest_main") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ deps = [ "$dir_pw_unit_test:simple_printing_main" ]
+ } else {
+ deps = [ "$dir_pw_third_party/fuzztest/fuzztest:fuzztest_gtest_main" ]
+ }
+ }
}
-pw_doc_group("docs") {
- inputs = [ "doc_resources/pw_fuzzer_coverage_guided.png" ]
- sources = [ "docs.rst" ]
+################################################################################
+# libFuzzer support
+#
+# Create libFuzzer-style fuzzers by using the `pw_fuzzer` template from
+# fuzzer.gni.
+
+# Add flags for linking against compiler-rt's libFuzzer. This is added
+# automatically by `pw_fuzzer`.
+config("libfuzzer_config") {
+ ldflags = [ "-fsanitize=fuzzer" ]
}
-# Sample fuzzer
-pw_fuzzer("toy_fuzzer") {
- sources = [ "examples/toy_fuzzer.cc" ]
- deps = [
- ":pw_fuzzer",
- dir_pw_status,
+# Includes wrapper's for LLVM's libFuzzer compiler runtime library.
+pw_source_set("libfuzzer") {
+ public = [
+ "public/pw_fuzzer/asan_interface.h",
+ "public/pw_fuzzer/fuzzed_data_provider.h",
]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ dir_pw_log ]
}
-pw_test_group("tests") {
- tests = [ ":toy_fuzzer_test" ]
+# This can be linked against fuzz target functions to create unit tests for
+# them.
+pw_source_set("libfuzzer_test") {
+ testonly = pw_unit_test_TESTONLY
+ sources = [ "pw_fuzzer_disabled.cc" ]
+ public_deps = [ ":libfuzzer" ]
+ deps = [ dir_pw_unit_test ]
}
+# libFuzzer-based fuzzers have a distinct dep graph.
group("fuzzers") {
- deps = [ ":toy_fuzzer" ]
+ deps = [ "examples/libfuzzer:fuzzers" ]
+}
+
+################################################################################
+# Local fuzzing support
+
+# Add flags for adding LLVM sanitizer coverage for fuzzing. This is added by
+# the host_clang_fuzz toolchains.
+config("instrumentation") {
+ cflags = [ "-fsanitize=fuzzer-no-link" ]
+}
+
+################################################################################
+# OSS-Fuzz support
+#
+# OSS-Fuzz manipulates compiler and linker flags directly. See
+# google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+#
+# WARNING: This is not hermetic by design. It never can be, and never will be.
+
+config("oss_fuzz_instrumentation") {
+ cflags_c = string_split(getenv("CFLAGS"))
+ cflags_cc = string_split(getenv("CXXFLAGS"))
+
+ # OSS-Fuzz sets "-stdlib=libc++", which conflicts with the "-nostdinc++" set
+ # by `pw_minimal_cpp_stdlib`.
+ if (cflags_cc + [ "-stdlib=libc++" ] - [ "-stdlib=libc++" ] != cflags_cc) {
+ cflags_cc += [ "-Wno-unused-command-line-argument" ]
+ }
+
+ # Disable UBSan vptr when the target is built with -fno-rtti.
+ if (cflags_cc + [ "-fno-rtti" ] - [ "-fno-rtti" ] != cflags_cc) {
+ cflags_cc += [ " -fno-sanitize=vptr" ]
+ }
+ cflags_cc += [ "-fcoverage-compilation-dir=" + getenv("PW_ROOT") ]
+
+ ldflags = cflags_cc + [ "-fuse-ld=lld" ]
+}
+
+config("libfuzzer_oss_fuzz_config") {
+ engine = getenv("LIB_FUZZING_ENGINE")
+ if (engine == "") {
+ engine = "-fsanitize=fuzzer"
+ }
+ ldflags = [ engine ]
}
diff --git a/pw_fuzzer/CMakeLists.txt b/pw_fuzzer/CMakeLists.txt
new file mode 100644
index 000000000..f293aa448
--- /dev/null
+++ b/pw_fuzzer/CMakeLists.txt
@@ -0,0 +1,57 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+################################################################################
+# FuzzTest support
+#
+# Create FuzzTest-style fuzzers by adding a dep on pw_fuzzer.fuzztest
+
+# If a project configure `pw_unit_test_GOOGLETEST_BACKEND` to use FuzzTest, this
+# target pulls in the "real" FuzzTest. Otherwise, it uses a "stub" which
+# converts invocations of `FUZZ_TEST` to disabled empty unit tests.
+if(pw_unit_test_GOOGLETEST_BACKEND STREQUAL "pw_third_party.fuzztest")
+ pw_add_library(pw_fuzzer.fuzztest INTERFACE
+ HEADERS
+ public/pw_fuzzer/fuzztest.h
+ private/pw_fuzzer/internal/fuzztest.h
+ PUBLIC_DEPS
+ pw_containers
+ pw_result
+ pw_status
+ pw_string
+ pw_third_party.fuzztest
+ PUBLIC_INCLUDES
+ public
+ private
+ )
+else()
+ pw_add_library(pw_fuzzer.fuzztest INTERFACE
+ HEADERS
+ public_overrides/fuzztest/fuzztest.h
+ private_overrides/pw_fuzzer/internal/fuzztest.h
+ PUBLIC_DEPS
+ pw_containers
+ pw_result
+ pw_status
+ pw_string
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ private_overrides
+ )
+endif()
+
+add_subdirectory("examples/fuzztest")
diff --git a/pw_fuzzer/OWNERS b/pw_fuzzer/OWNERS
index 8819b2c20..ab481b4c2 100644
--- a/pw_fuzzer/OWNERS
+++ b/pw_fuzzer/OWNERS
@@ -1 +1,2 @@
+aarongreen@google.com
keir@google.com
diff --git a/pw_fuzzer/concepts.rst b/pw_fuzzer/concepts.rst
new file mode 100644
index 000000000..9662a1b88
--- /dev/null
+++ b/pw_fuzzer/concepts.rst
@@ -0,0 +1,53 @@
+.. _module-pw_fuzzer-concepts:
+
+===================
+pw_fuzzer: Concepts
+===================
+.. pigweed-module-subpage::
+ :name: pw_fuzzer
+ :tagline: Better C++ code through easier fuzzing
+
+Fuzzing is an approach to testing software with generated data. Guided fuzzing
+uses feedback from the code being tested, such as code coverage, to direct the
+generation of additional inputs. This feedback loop typically has three steps
+that it executes repeatedly:
+
+#. The `fuzzing engine`_ generates a new `test input`_. The details of the
+ test input depend on the engine. For example, `libFuzzer`_ generates
+ sequences of bytes of arbitrary length, while `FuzzTest`_ generates
+ parameters to match a function signature.
+
+#. The `test input`_ is used to exercise the `fuzz target`_. This is targeted
+ interface to the code being tested.
+
+#. The code under test is monitored for feedback or any abnormal conditions.
+ The feedback is commonly code coverage information generated by
+ compiler-added `instrumentation`_.
+
+The loop ends when a configured limit is reached, such as a specific duration or
+number of iterations, or when an abnormal condition is detected. These can be
+failed assertions, bug detections by `sanitizers`_, unhandled signals, etc.
+When a loop terminates due to one of these errors, the fuzzer will typically
+create a `reproducer`_ that developers can use to reproduce the fault.
+
+.. image:: doc_resources/pw_fuzzer_coverage_guided.png
+ :alt: Coverage Guided Fuzzing
+ :align: left
+
+.. Diagram created using Google Drawings:
+ https://docs.google.com/drawings/d/1nGHCNp6iOiz_Qee9XCoIhMH01E_bB6tg3mipC-HJ0bo/edit
+
+To learn more about how effective fuzzing can be or explore some of fuzzing's
+"trophy lists", see `Why fuzz?`_.
+
+.. inclusive-language: disable
+.. _fuzz target: https://github.com/google/fuzzing/blob/master/docs/glossary.md#fuzz-target
+.. _fuzzing engine: https://github.com/google/fuzzing/blob/master/docs/glossary.md#fuzzing-engine
+.. _FuzzTest: https://github.com/google/fuzztest
+.. _instrumentation: https://clang.llvm.org/docs/SanitizerCoverage.html
+.. _libFuzzer: https://llvm.org/docs/LibFuzzer.html
+.. _reproducer: https://github.com/google/fuzzing/blob/master/docs/glossary.md#reproducer
+.. _sanitizers: https://github.com/google/fuzzing/blob/master/docs/glossary.md#sanitizer
+.. _test input: https://github.com/google/fuzzing/blob/master/docs/glossary.md#test-input
+.. _Why fuzz?: https://github.com/google/fuzzing/blob/master/docs/why-fuzz.md
+.. inclusive-language: enable
diff --git a/pw_fuzzer/doc_resources/pw_fuzzer_coverage_guided.png b/pw_fuzzer/doc_resources/pw_fuzzer_coverage_guided.png
index 02554728b..36f8dbdf5 100644
--- a/pw_fuzzer/doc_resources/pw_fuzzer_coverage_guided.png
+++ b/pw_fuzzer/doc_resources/pw_fuzzer_coverage_guided.png
Binary files differ
diff --git a/pw_fuzzer/docs.rst b/pw_fuzzer/docs.rst
index 02bf05f45..e50038df3 100644
--- a/pw_fuzzer/docs.rst
+++ b/pw_fuzzer/docs.rst
@@ -1,322 +1,146 @@
.. _module-pw_fuzzer:
----------
+=========
pw_fuzzer
----------
-``pw_fuzzer`` provides developers with tools to write `libFuzzer`_ based
-fuzzers.
+=========
+.. pigweed-module::
+ :name: pw_fuzzer
+ :tagline: Better C++ code through easier fuzzing
+ :status: unstable
+ :languages: C++17, C++20
-Fuzzing or fuzz testing is style of testing that stochastically generates inputs
-to targeted interfaces in order to automatically find defects and/or
-vulnerabilities. In other words, fuzzing is simply an automated way of testing
-APIs with generated data.
-
-A fuzzer is a program that is used to fuzz a interface. It typically has three
-steps that it executes repeatedly:
-
-#. Generate a new, context-free input. This is the *fuzzing engine*. For
- ``pw_fuzzer``, this is `libFuzzer`_.
-#. Use the input to exercise the targeted interface, or code being tested. This
- is the *fuzz target function*. For ``pw_fuzzer``, these are the GN
- ``sources`` and/or ``deps`` that define `LLVMFuzzerTestOneInput`_.
-#. Monitor the code being tested for any abnormal conditions. This is the
- *instrumentation*. For ``pw_fuzzer``, these are sanitizer runtimes from
- LLVM's `compiler_rt`_.
-
-.. note::
+Use state of the art tools to automatically find bugs in your C++ code with 5
+lines of code or less!
- ``pw_fuzzer`` is currently only supported on Linux and MacOS using clang.
-
-.. image:: doc_resources/pw_fuzzer_coverage_guided.png
- :alt: Coverage Guided Fuzzing with libFuzzer
- :align: left
-Writing fuzzers
-===============
+.. code-block:: cpp
-To write a fuzzer, a developer needs to write a fuzz target function follwing
-the `fuzz target function`__ guidelines given by libFuzzer:
+ FUZZ_TEST(MyTestSuite, TestMyInterestingFunction)
+ .WithDomains(Arbitrary<size_t>(), AsciiString());
-.. code:: cpp
+----------
+Background
+----------
+You've written some code. You've written unit tests for that code. The unit
+tests pass. But could there be bugs in inputs or code paths the unit tests do
+not cover? `Fuzzing`_ can help!
- extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
- DoSomethingInterestingWithMyAPI(data, size);
- return 0; // Non-zero return values are reserved for future use.
- }
+However, fuzzing requires some complex interactions between compiler-added
+`instrumentation`_, `compiler runtimes`_, code under test, and the fuzzer code
+itself. And to be reliably useful, fuzzers need to be part of a continuous
+fuzzing infrastructure, adding even more complexity.
-.. __: LLVMFuzzerTestOneInput_
+See :ref:`module-pw_fuzzer-concepts` to learn more about the different
+components of a fuzzer and how they work together to discover hard-to-find bugs.
-When writing you fuzz target function, you may want to consider:
+------------
+Our solution
+------------
+``pw_fuzzer`` makes it easier to write, build, run, and deploy fuzzers. It
+provides convenient integration with two fuzzing `engines`_:
+
+* `libFuzzer`_, allowing integration of legacy fuzzers.
+
+* `FuzzTest`_, allowing easy creation of fuzzers from unit tests in around 5
+ lines of code.
+
+Additionally, it produces artifacts for continuous fuzzing infrastructures such
+as `ClusterFuzz`_ and `OSS-Fuzz`_, and provides the artifacts those systems need.
+
+---------------
+Who this is for
+---------------
+``pw_fuzzer`` is useful for authors of `wide range`_ of C++ code who have
+written unit tests and are interested in finding actionable bugs in their code.
+
+In particular, coverage-guided is effective for testing and finding bugs in code
+that:
+
+* Receives inputs from **untrusted sources** and must be secure.
+
+* Has **complex algorithms** with some equivalence, e.g. compress and
+ decompress, and must be correct.
+
+* Handles **high volumes** of inputs and/or unreliable dependencies and must be
+ stable.
+
+Fuzzing works best when code handles inputs deterministically, that is, given
+the same input it behaves the same way. Fuzzing will be less effective with code
+that modifies global state or has some randomness, e.g. depends on how multiple
+threads are scheduled. Simply put, good fuzzers typically resemble unit tests in
+terms of scope and isolation.
+
+--------------------
+Is it right for you?
+--------------------
+Currently, ``pw_fuzzer`` only provides support for fuzzers that:
+
+* Run on **host**. Sanitizer runtimes such as `AddressSanitizer`_ add
+ significant memory overhead and are not suitable for running on device.
+ Additionally, the currently supported engines assume the presence of certain
+ POSIX features.
+
+* Are built with **Clang**. The `instrumentation`_ used in fuzzing is added by
+ ``clang``.
+
+.. _module-pw_fuzzer-get-started:
+
+---------------
+Getting started
+---------------
+The first step in adding a fuzzer is to determine what fuzzing engine should you
+use. Pigweed currently supports two fuzzing engines:
+
+* **FuzzTest** is the recommended engine. It makes it easy to create fuzzers
+ from your existing unit tests, but it does requires additional third party
+ dependencies and at least C++17. See
+ :ref:`module-pw_fuzzer-guides-using_fuzztest` for details on how to set up a
+ project to use FuzzTest and on how to create and run fuzzers with it.
+
+* **libFuzzer** is a mature, proven engine. It is a part of LLVM and requires
+ code authors to implement a specific function, ``LLVMFuzzerTestOneInput``. See
+ :ref:`module-pw_fuzzer-guides-using_libfuzzer` for details on how to write
+ fuzzers with it.
+
+-------
+Roadmap
+-------
+``pw_fuzzer`` is under active development. There are a number of improvements we
+would like to add, including:
-- It is acceptable to return early if the input doesn't meet some constraints,
- e.g. it is too short.
-- If your fuzzer accepts data with a well-defined format, you can bootstrap
- coverage by crafting examples and adding them to a `corpus`_.
-- There are tools to `split a fuzzing input`_ into multiple fields if needed;
- the `FuzzedDataProvider`_ is particularly easy to use.
-- If your code acts on "transformed" inputs, such as encoded or compressed
- inputs, you may want to try `structure aware fuzzing`.
-- You can do `startup initialization`_ if you need to.
-- If your code is non-deterministic or uses checksums, you may want to disable
- those **only** when fuzzing by using LLVM's
- `FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION`_
+* `b/282560789`_ - Document workflows for analyzing coverage and improving
+ fuzzers.
-.. _build:
-
-Building fuzzers with GN
-========================
-
-To build a fuzzer, do the following:
-
-1. Add the GN target to the module using ``pw_fuzzer`` GN template. If you wish
- to limit when the generated unit test is run, you can set `enable_test_if` in
- the same manner as `enable_if` for `pw_test`:
-
-.. code::
-
- # In $dir_my_module/BUILD.gn
- import("$dir_pw_fuzzer/fuzzer.gni")
-
- pw_fuzzer("my_fuzzer") {
- sources = [ "my_fuzzer.cc" ]
- deps = [ ":my_lib" ]
- enable_test_if = device_has_1m_flash
- }
-
-2. Add the generated unit test to the module's test group. This test verifies
- the fuzzer can build and run, even when not being built in a fuzzing
- toolchain.
-
-.. code::
-
- # In $dir_my_module/BUILD.gn
- pw_test_group("tests") {
- tests = [
- ...
- ":my_fuzzer_test",
- ]
- }
-
-3. If your module does not already have a group of fuzzers, add it and include
- it in the top level fuzzers target. Depending on your project, the specific
- toolchain may differ. Fuzzer toolchains are those with
- ``pw_toolchain_FUZZING_ENABLED`` set to true. Examples include
- ``host_clang_fuzz`` and any toolchains that extend it.
-
-.. code::
-
- # In //BUILD.gn
- group("fuzzers") {
- deps = [
- ...
- "$dir_my_module:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
- ]
- }
-
-4. Add your fuzzer to the module's group of fuzzers.
-
-.. code::
-
- group("fuzzers") {
- deps = [
- ...
- ":my_fuzzer",
- ]
- }
-
-5. If desired, select a sanitizer runtime. By default,
- `//targets/host:host_clang_fuzz` uses "address" if no sanitizer is specified.
- See LLVM for `valid options`_.
-
-.. code:: sh
-
- $ gn gen out --args='pw_toolchain_SANITIZERS=["address"]'
-
-6. Build the fuzzers!
-
-.. code:: sh
-
- $ ninja -C out fuzzers
-
-.. _bazel:
-
-Building and running fuzzers with Bazel
-=======================================
-To build a fuzzer, do the following:
-
-1. Add the Bazel target using ``pw_cc_fuzz_test`` macro.
-
-.. code:: py
-
- load("@pigweed//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
-
- pw_cc_fuzz_test(
- name = "my_fuzz_test",
- srcs = ["my_fuzzer.cc"],
- deps = [
- "@pigweed//pw_fuzzer",
- ":my_lib",
- ],
- )
-
-2. Build and run the fuzzer.
-
-.. code:: sh
-
- bazel test //my_module:my_fuzz_test
-
-3. Swap fuzzer backend to use ASAN fuzzing engine.
-
-.. code::
-
- # .bazelrc
- # Define the --config=asan-libfuzzer configuration.
- build:asan-libfuzzer \
- --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
- build:asan-libfuzzer \
- --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
- build:asan-libfuzzer --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
-
-4. Re-run fuzz tests.
-
-.. code::
-
- bazel test //my_module:my_fuzz_test --config asan-libfuzzer
-
-.. _run:
-
-Running fuzzers locally
-=======================
-
-Based on the example above, the fuzzer output will be at
-``out/host/obj/my_module/my_fuzzer``. It can be invoked using the normal
-`libFuzzer options`_ and `sanitizer runtime flags`_. For even more details, see
-the libFuzzer section on `running a fuzzer`_.
-
-For example, the following invocation disables "one definition rule" detection,
-saves failing inputs to ``artifacts/``, treats any input that takes longer than
-10 seconds as a failure, and stores the working corpus in ``corpus/``.
-
-.. code::
-
- $ mkdir -p corpus
- $ ASAN_OPTIONS=detect_odr_violation=0 \
- out/host_clang_fuzz/obj/pw_fuzzer/bin/toy_fuzzer \
- -artifact_prefix=artifacts/ \
- -timeout=10 \
- corpus
- INFO: Seed: 305325345
- INFO: Loaded 1 modules (46 inline 8-bit counters): 46 [0x38dfc0, 0x38dfee),
- INFO: Loaded 1 PC tables (46 PCs): 46 [0x23aaf0,0x23add0),
- INFO: 0 files found in corpus
- INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
- INFO: A corpus is not provided, starting from an empty corpus
- #2 INITED cov: 2 ft: 3 corp: 1/1b exec/s: 0 rss: 27Mb
- #4 NEW cov: 3 ft: 4 corp: 2/3b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 2 ShuffleBytes-InsertByte-
- #11 NEW cov: 7 ft: 8 corp: 3/7b lim: 4 exec/s: 0 rss: 27Mb L: 4/4 MS: 2 EraseBytes-CrossOver-
- #27 REDUCE cov: 7 ft: 8 corp: 3/6b lim: 4 exec/s: 0 rss: 27Mb L: 3/3 MS: 1 EraseBytes-
- #29 REDUCE cov: 7 ft: 8 corp: 3/5b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 2 ChangeBit-EraseBytes-
- #445 REDUCE cov: 9 ft: 10 corp: 4/13b lim: 8 exec/s: 0 rss: 27Mb L: 8/8 MS: 1 InsertRepeatedBytes-
- #12104 NEW cov: 11 ft: 12 corp: 5/24b lim: 122 exec/s: 0 rss: 28Mb L: 11/11 MS: 4 CMP-InsertByte-ShuffleBytes-ChangeByte- DE: "\xff\xff"-
- #12321 NEW cov: 12 ft: 13 corp: 6/31b lim: 122 exec/s: 0 rss: 28Mb L: 7/11 MS: 2 CopyPart-EraseBytes-
- #12459 REDUCE cov: 12 ft: 13 corp: 6/28b lim: 122 exec/s: 0 rss: 28Mb L: 8/8 MS: 3 CMP-InsertByte-EraseBytes- DE: "\x00\x00"-
- #12826 REDUCE cov: 12 ft: 13 corp: 6/26b lim: 122 exec/s: 0 rss: 28Mb L: 5/8 MS: 2 ShuffleBytes-EraseBytes-
- #14824 REDUCE cov: 12 ft: 13 corp: 6/25b lim: 135 exec/s: 0 rss: 28Mb L: 4/8 MS: 3 PersAutoDict-ShuffleBytes-EraseBytes- DE: "\x00\x00"-
- #15106 REDUCE cov: 12 ft: 13 corp: 6/24b lim: 135 exec/s: 0 rss: 28Mb L: 3/8 MS: 2 ChangeByte-EraseBytes-
- ...
- #197809 REDUCE cov: 35 ft: 36 corp: 22/129b lim: 1800 exec/s: 0 rss: 79Mb L: 9/9 MS: 1 InsertByte-
- #216250 REDUCE cov: 35 ft: 36 corp: 22/128b lim: 1980 exec/s: 0 rss: 87Mb L: 8/8 MS: 1 EraseBytes-
- #242761 REDUCE cov: 35 ft: 36 corp: 22/127b lim: 2237 exec/s: 0 rss: 101Mb L: 7/8 MS: 1 EraseBytes-
- ==126148== ERROR: libFuzzer: deadly signal
- #0 0x35b981 in __sanitizer_print_stack_trace ../recipe_cleanup/clangFu99hg/llvm_build_dir/tools/clang/stage2-bins/runtimes/runtimes-x86_64-unknown-linux-gnu-bins/compiler-rt/lib/asan/asan_stack.cpp:86:3
- #1 0x2bcdb5 in fuzzer::PrintStackTrace() (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x2bcdb5)
- #2 0x2a2ac9 in fuzzer::Fuzzer::CrashCallback() (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x2a2ac9)
- #3 0x7f866684151f (/lib/x86_64-linux-gnu/libpthread.so.0+0x1351f)
- #4 0x3831df in (anonymous namespace)::toy_example(char const*, char const*) /home/aarongreen/src/pigweed/out/host/../../pw_fuzzer/examples/toy_fuzzer.cc:49:15
- #5 0x3831df in LLVMFuzzerTestOneInput /home/aarongreen/src/pigweed/out/host/../../pw_fuzzer/examples/toy_fuzzer.cc:80:3
- #6 0x2a4025 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x2a4025)
- #7 0x2a3774 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x2a3774)
- #8 0x2a5769 in fuzzer::Fuzzer::MutateAndTestOne() (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x2a5769)
- #9 0x2a6185 in fuzzer::Fuzzer::Loop(std::__Fuzzer::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x2a6185)
- #10 0x294c8a in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x294c8a)
- #11 0x2bd422 in main ../recipe_cleanup/clangFu99hg/llvm_build_dir/tools/clang/stage2-bins/runtimes/runtimes-x86_64-unknown-linux-gnu-bins/compiler-rt/lib/fuzzer/FuzzerMain.cpp:19:10
- #12 0x7f8666684bba in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x26bba)
- #13 0x26ae19 in _start (/home/aarongreen/src/pigweed/out/host/obj/pw_fuzzer/toy_fuzzer+0x26ae19)
-
- NOTE: libFuzzer has rudimentary signal handlers.
- Combine libFuzzer with AddressSanitizer or similar for better crash reports.
- SUMMARY: libFuzzer: deadly signal
- MS: 1 CrossOver-; base unit: 9f479f7a6e3a21363397a25da3168218ba182a16
- 0x68,0x65,0x6c,0x6c,0x6f,0x0,0x77,0x6f,0x72,0x6c,0x64,0x0,0x0,0x0,
- hello\x00world\x00\x00\x00
- artifact_prefix='artifacts'; Test unit written to artifacts/crash-6e4fdc7ffd04131ea15dd243a0890b1b606f4831
- Base64: aGVsbG8Ad29ybGQAAAA=
-
-Running fuzzers on OSS-Fuzz
-===========================
-
-Pigweed is integrated with `OSS-Fuzz`_, a continuous fuzzing infrastructure for
-open source software. Fuzzers listed in in ``pw_test_groups`` will automatically
-start being run within a day or so of appearing in the git repository.
-
-Bugs produced by OSS-Fuzz can be found in its `Monorail instance`_. These bugs
-include:
-
-* A detailed report, including a symbolized backtrace.
-* A revision range indicating when the bug has been detected.
-* A minimized testcase, which is a fuzzer input that can be used to reproduce
- the bug.
-
-To reproduce a bug:
-
-#. Build_ the fuzzers as described above.
-#. Download the minimized testcase.
-#. Run_ the fuzzer with the testcase as an argument.
-
-For example, if the testcase is saved as "~/Downloads/testcase"
-and the fuzzer is the same as in the examples above, you could run:
-
-.. code::
-
- $ ./out/host/obj/pw_fuzzer/toy_fuzzer ~/Downloads/testcase
-
-If you need to recreate the OSS-Fuzz environment locally, you can use its
-documentation on `reproducing`_ issues.
-
-In particular, you can recreate the OSS-Fuzz environment using:
-
-.. code::
-
- $ python infra/helper.py pull_images
- $ python infra/helper.py build_image pigweed
- $ python infra/helper.py build_fuzzers --sanitizer <address/undefined> pigweed
-
-With that environment, you can run the reproduce bugs using:
-
-.. code::
-
- python infra/helper.py reproduce pigweed <pw_module>_<fuzzer_name> ~/Downloads/testcase
-
-You can even verify fixes in your local source checkout:
-
-.. code::
-
- $ python infra/helper.py build_fuzzers --sanitizer <address/undefined> pigweed $PW_ROOT
- $ python infra/helper.py reproduce pigweed <pw_module>_<fuzzer_name> ~/Downloads/testcase
-
-.. _compiler_rt: https://compiler-rt.llvm.org/
-.. _corpus: https://llvm.org/docs/LibFuzzer.html#corpus
-.. _FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION: https://llvm.org/docs/LibFuzzer.html#fuzzer-friendly-build-mode
-.. _FuzzedDataProvider: https://github.com/llvm/llvm-project/blob/HEAD/compiler-rt/include/fuzzer/FuzzedDataProvider.h
+* `b/280457542`_ - Add CMake support for FuzzTest.
+
+* `b/281138993`_ - Add a ``pw_cli`` plugin for fuzzing.
+
+* `b/281139237`_ - Develop OSS-Fuzz and ClusterFuzz workflow templates for
+ downtream projects.
+
+.. _b/282560789: https://issues.pigweed.dev/issues/282560789
+.. _b/280457542: https://issues.pigweed.dev/issues/280457542
+.. _b/281138993: https://issues.pigweed.dev/issues/281138993
+.. _b/281139237: https://issues.pigweed.dev/issues/281139237
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ concepts
+ guides/fuzztest
+ guides/libfuzzer
+ guides/reproducing_oss_fuzz_bugs
+
+.. inclusive-language: disable
+.. _AddressSanitizer: https://github.com/google/sanitizers/wiki/AddressSanitizer
+.. _ClusterFuzz: https://github.com/google/clusterfuzz/
+.. _compiler runtimes: https://compiler-rt.llvm.org/
+.. _engines: https://github.com/google/fuzzing/blob/master/docs/glossary.md#fuzzing-engine
+.. _Fuzzing: https://github.com/google/fuzzing/blob/master/docs/intro-to-fuzzing.md
+.. _FuzzTest: https://github.com/google/fuzztest
+.. _instrumentation: https://clang.llvm.org/docs/SanitizerCoverage.html#instrumentation-points
.. _libFuzzer: https://llvm.org/docs/LibFuzzer.html
-.. _libFuzzer options: https://llvm.org/docs/LibFuzzer.html#options
-.. _LLVMFuzzerTestOneInput: https://llvm.org/docs/LibFuzzer.html#fuzz-target
-.. _monorail instance: https://bugs.chromium.org/p/oss-fuzz
-.. _oss-fuzz: https://github.com/google/oss-fuzz
-.. _reproducing: https://google.github.io/oss-fuzz/advanced-topics/reproducing/
-.. _running a fuzzer: https://llvm.org/docs/LibFuzzer.html#running
-.. _sanitizer runtime flags: https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
-.. _split a fuzzing input: https://github.com/google/fuzzing/blob/HEAD/docs/split-inputs.md
-.. _startup initialization: https://llvm.org/docs/LibFuzzer.html#startup-initialization
-.. _structure aware fuzzing: https://github.com/google/fuzzing/blob/HEAD/docs/structure-aware-fuzzing.md
-.. _valid options: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
+.. _OSS-Fuzz: https://github.com/google/oss-fuzz
+.. _wide range: https://github.com/google/fuzzing/blob/master/docs/why-fuzz.md
+.. inclusive-language: enable
diff --git a/pw_fuzzer/domain_test.cc b/pw_fuzzer/domain_test.cc
new file mode 100644
index 000000000..342bf7d57
--- /dev/null
+++ b/pw_fuzzer/domain_test.cc
@@ -0,0 +1,508 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <cctype>
+
+#include "gtest/gtest.h"
+#include "pw_fuzzer/fuzztest.h"
+#include "pw_result/result.h"
+
+// Most of the tests in this file only validate that the domains provided by
+// Pigweed build, both with and without FuzzTest. Each domain comprises one or
+// more FuzzTest domains, so the validation of the distribution of values
+// produced by domains is left to and assumed from FuzzTest's own domain tests.
+
+namespace pw::fuzzer {
+namespace {
+
+////////////////////////////////////////////////////////////////
+// Constants, macros, and types used by the domain tests below.
+
+constexpr size_t kSize = 8;
+constexpr char kMin = 4;
+constexpr char kMax = 16;
+
+/// Generates a target function and fuzz test for a specific type.
+///
+/// The generated test simply checks that the provided domain can produce values
+/// of the appropriate type. Generally, explicitly providing a target function
+/// is preferred for readability, but this macro can be useful for templated
+/// domains that are tested for many repetitive numerical types.
+///
+/// This macro should not be used directly. Instead, use
+/// `FUZZ_TEST_FOR_INTEGRAL`, `FUZZ_TEST_FOR_FLOATING_POINT`, or
+/// `FUZZ_TEST_FOR_ARITHMETIC`.
+#define FUZZ_TEST_FOR_TYPE(Suite, Target, Domain, Type, ...) \
+ void Target(Type t) { Take##Domain<Type>(t); } \
+ FUZZ_TEST(Suite, Target).WithDomains(Domain<Type>(__VA_ARGS__))
+
+/// Generates target functions and fuzz tests for a integral types.
+#define FUZZ_TEST_FOR_INTEGRAL(Suite, Target, Domain, ...) \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_Char, Domain, char, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE( \
+ Suite, Target##_UChar, Domain, unsigned char, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_Short, Domain, short, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE( \
+ Suite, Target##_UShort, Domain, unsigned short, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_Int, Domain, int, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_UInt, Domain, unsigned int, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_Long, Domain, long, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_ULong, Domain, unsigned long, __VA_ARGS__)
+
+/// Generates target functions and fuzz tests for a floating point types.
+#define FUZZ_TEST_FOR_FLOATING_POINT(Suite, Target, Domain, ...) \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_Float, Domain, float, __VA_ARGS__); \
+ FUZZ_TEST_FOR_TYPE(Suite, Target##_Double, Domain, double, __VA_ARGS__)
+
+/// Generates target functions and fuzz tests for a all arithmetic types.
+#define FUZZ_TEST_FOR_ARITHMETIC(Suite, Target, Domain, ...) \
+ FUZZ_TEST_FOR_INTEGRAL(Suite, Target, Domain, __VA_ARGS__); \
+ FUZZ_TEST_FOR_FLOATING_POINT(Suite, Target, Domain, __VA_ARGS__)
+
+// Test struct that can be produced by FuzzTest.
+struct StructForTesting {
+ int a;
+ long b;
+};
+
+// Test class that can be produced by FuzzTest.
+class ClassForTesting {
+ public:
+ ClassForTesting(unsigned char c, short d) : c_(c), d_(d) {}
+ unsigned char c() const { return c_; }
+ short d() const { return d_; }
+
+ private:
+ unsigned char c_;
+ short d_;
+};
+
+////////////////////////////////////////////////////////////////
+// Arbitrary domains forwarded or stubbed from FuzzTest
+
+template <typename T>
+void TakeArbitrary(T) {}
+
+void TakeArbitraryBool(bool b) { TakeArbitrary<bool>(b); }
+FUZZ_TEST(ArbitraryTest, TakeArbitraryBool).WithDomains(Arbitrary<bool>());
+
+FUZZ_TEST_FOR_ARITHMETIC(ArbitraryTest, TakeArbitrary, Arbitrary);
+
+void TakeArbitraryStruct(const StructForTesting& s) {
+ TakeArbitrary<StructForTesting>(s);
+}
+FUZZ_TEST(ArbitraryTest, TakeArbitraryStruct)
+ .WithDomains(Arbitrary<StructForTesting>());
+
+void TakeArbitraryTuple(const std::tuple<int, long>& t) {
+ TakeArbitrary<std::tuple<int, long>>(t);
+}
+FUZZ_TEST(ArbitraryTest, TakeArbitraryTuple)
+ .WithDomains(Arbitrary<std::tuple<int, long>>());
+
+void TakeArbitraryOptional(const std::optional<int>& o) {
+ TakeArbitrary<std::optional<int>>(o);
+}
+FUZZ_TEST(ArbitraryTest, TakeArbitraryOptional)
+ .WithDomains(Arbitrary<std::optional<int>>());
+
+////////////////////////////////////////////////////////////////
+// Numerical domains forwarded or stubbed from FuzzTest
+
+template <typename Arithmetic>
+void TakeInRange(Arithmetic x) {
+ EXPECT_GE(x, Arithmetic(kMin));
+ EXPECT_LE(x, Arithmetic(kMax));
+}
+FUZZ_TEST_FOR_ARITHMETIC(DomainTest, TakeInRange, InRange, kMin, kMax);
+
+template <typename Arithmetic>
+void TakeNonZero(Arithmetic x) {
+ EXPECT_NE(x, Arithmetic(0));
+}
+FUZZ_TEST_FOR_ARITHMETIC(DomainTest, TakeNonZero, NonZero);
+
+template <typename Arithmetic>
+void TakePositive(Arithmetic x) {
+ EXPECT_GT(x, Arithmetic(0));
+}
+FUZZ_TEST_FOR_ARITHMETIC(DomainTest, TakePositive, Positive);
+
+template <typename Arithmetic>
+void TakeNonNegative(Arithmetic x) {
+ EXPECT_GE(x, Arithmetic(0));
+}
+FUZZ_TEST_FOR_ARITHMETIC(DomainTest, TakeNonNegative, NonNegative);
+
+template <typename Arithmetic>
+void TakeNegative(Arithmetic x) {
+ EXPECT_LT(x, Arithmetic(0));
+}
+FUZZ_TEST_FOR_ARITHMETIC(DomainTest, TakeNegative, Positive);
+
+template <typename Arithmetic>
+void TakeNonPositive(Arithmetic x) {
+ EXPECT_LE(x, Arithmetic(0));
+}
+FUZZ_TEST_FOR_ARITHMETIC(DomainTest, TakeNonPositive, NonNegative);
+
+template <typename FloatingPoint>
+void TakeFinite(FloatingPoint f) {
+ EXPECT_TRUE(std::isfinite(f));
+}
+FUZZ_TEST_FOR_FLOATING_POINT(DomainTest, TakeFinite, Finite);
+
+////////////////////////////////////////////////////////////////
+// Character domains forwarded or stubbed from FuzzTest
+
+void TakeNonZeroChar(char c) { EXPECT_NE(c, 0); }
+FUZZ_TEST(DomainTest, TakeNonZeroChar).WithDomains(NonZeroChar());
+
+void TakeNumericChar(char c) { EXPECT_TRUE(std::isdigit(c)); }
+FUZZ_TEST(DomainTest, TakeNumericChar).WithDomains(NumericChar());
+
+void TakeLowerChar(char c) { EXPECT_TRUE(std::islower(c)); }
+FUZZ_TEST(DomainTest, TakeLowerChar).WithDomains(LowerChar());
+
+void TakeUpperChar(char c) { EXPECT_TRUE(std::isupper(c)); }
+FUZZ_TEST(DomainTest, TakeUpperChar).WithDomains(UpperChar());
+
+void TakeAlphaChar(char c) { EXPECT_TRUE(std::isalpha(c)); }
+FUZZ_TEST(DomainTest, TakeAlphaChar).WithDomains(AlphaChar());
+
+void TakeAlphaNumericChar(char c) { EXPECT_TRUE(std::isalnum(c)); }
+FUZZ_TEST(DomainTest, TakeAlphaNumericChar).WithDomains(AlphaNumericChar());
+
+void TakePrintableAsciiChar(char c) { EXPECT_TRUE(std::isprint(c)); }
+FUZZ_TEST(DomainTest, TakePrintableAsciiChar).WithDomains(PrintableAsciiChar());
+
+void TakeAsciiChar(char c) {
+ EXPECT_GE(c, 0);
+ EXPECT_LE(c, 127);
+}
+FUZZ_TEST(DomainTest, TakeAsciiChar).WithDomains(AsciiChar());
+
+////////////////////////////////////////////////////////////////
+// Regular expression domains forwarded or stubbed from FuzzTest
+
+// TODO: b/285775246 - Add support for `fuzztest::InRegexp`.
+// void TakeMatch(std::string_view sv) {
+// ASSERT_EQ(sv.size(), 3U);
+// EXPECT_EQ(sv[0], 'a');
+// EXPECT_EQ(sv[2], 'c');
+// }
+// FUZZ_TEST(DomainTest, TakeMatch).WithDomains(InRegexp("a.c"));
+
+////////////////////////////////////////////////////////////////
+// Enumerated domains forwarded or stubbed from FuzzTest
+
+void TakeSingleDigitEvenNumber(int n) {
+ EXPECT_LT(n, 10);
+ EXPECT_EQ(n % 2, 0);
+}
+FUZZ_TEST(DomainTest, TakeSingleDigitEvenNumber)
+ .WithDomains(ElementOf({0, 2, 4, 6, 8}));
+
+enum Flags : uint8_t {
+ kFlag1 = 1 << 0,
+ kFlag2 = 1 << 1,
+ kFlag3 = 1 << 2,
+};
+void TakeFlagCombination(uint8_t flags) { EXPECT_FALSE(flags & Flags::kFlag2); }
+FUZZ_TEST(DomainTest, TakeFlagCombination)
+ .WithDomains(BitFlagCombinationOf({Flags::kFlag1, Flags::kFlag3}));
+
+////////////////////////////////////////////////////////////////
+// Aggregate domains forwarded or stubbed from FuzzTest
+
+void TakeStructForTesting(const StructForTesting& obj) {
+ EXPECT_NE(obj.a, 0);
+ EXPECT_LT(obj.b, 0);
+}
+FUZZ_TEST(DomainTest, TakeStructForTesting)
+ .WithDomains(StructOf<StructForTesting>(NonZero<int>(), Negative<long>()));
+
+void TakeClassForTesting(const ClassForTesting& obj) {
+ EXPECT_GE(obj.c(), kMin);
+ EXPECT_LE(obj.c(), kMax);
+ EXPECT_GE(obj.d(), 0);
+}
+FUZZ_TEST(DomainTest, TakeClassForTesting)
+ .WithDomains(ConstructorOf<ClassForTesting>(InRange<unsigned char>(kMin,
+ kMax),
+ NonNegative<short>()));
+
+void TakePair(const std::pair<char, float>& p) {
+ EXPECT_TRUE(std::islower(p.first));
+ EXPECT_TRUE(std::isfinite(p.second));
+}
+FUZZ_TEST(DomainTest, TakePair)
+ .WithDomains(PairOf(LowerChar(), Finite<float>()));
+
+void TakeTuple(std::tuple<short, int> a, long b) {
+ EXPECT_NE(std::get<short>(a), 0);
+ EXPECT_NE(std::get<int>(a), 0);
+ EXPECT_NE(b, 0);
+}
+FUZZ_TEST(DomainTest, TakeTuple)
+ .WithDomains(TupleOf(NonZero<short>(), NonZero<int>()), NonZero<long>());
+
+void TakeVariant(const std::variant<int, long>&) {}
+FUZZ_TEST(DomainTest, TakeVariant)
+ .WithDomains(VariantOf(Arbitrary<int>(), Arbitrary<long>()));
+
+void TakeOptional(const std::optional<int>&) {}
+FUZZ_TEST(DomainTest, TakeOptional).WithDomains(OptionalOf(Arbitrary<int>()));
+
+void TakeNullOpt(const std::optional<int>& option) { EXPECT_FALSE(option); }
+FUZZ_TEST(DomainTest, TakeNullOpt).WithDomains(NullOpt<int>());
+
+void TakeNonNull(const std::optional<int>& option) { EXPECT_TRUE(option); }
+FUZZ_TEST(DomainTest, TakeNonNull)
+ .WithDomains(NonNull(OptionalOf(Arbitrary<int>())));
+
+////////////////////////////////////////////////////////////////
+// Other miscellaneous domains forwarded or stubbed from FuzzTest
+
+void TakePositiveOrMinusOne(int n) {
+ if (n != -1) {
+ EXPECT_GT(n, 0);
+ }
+}
+FUZZ_TEST(DomainTest, TakePositiveOrMinusOne)
+ .WithDomains(OneOf(Just(-1), Positive<int>()));
+
+void TakePackedValue(uint32_t value) {
+ EXPECT_GE(value & 0xFFFF, 1000U);
+ EXPECT_LT(value >> 16, 2048U);
+}
+FUZZ_TEST(DomainTest, TakePackedValue)
+ .WithDomains(
+ Map([](uint16_t lower,
+ uint16_t upper) { return (uint32_t(upper) << 16) | lower; },
+ InRange<uint16_t>(1000U, std::numeric_limits<uint16_t>::max()),
+ InRange<uint16_t>(0U, 2047U)));
+
+void TakeOrdered(size_t x, size_t y) { EXPECT_LT(x, y); }
+void FlatMapAdapter(const std::pair<size_t, size_t>& p) {
+ TakeOrdered(p.first, p.second);
+}
+FUZZ_TEST(DomainTest, FlatMapAdapter)
+ .WithDomains(FlatMap(
+ [](size_t x) {
+ return PairOf(
+ Just(x),
+ InRange<size_t>(x + 1, std::numeric_limits<size_t>::max()));
+ },
+ InRange<size_t>(0, std::numeric_limits<size_t>::max() - 1)));
+
+void TakeEven(unsigned int n) { EXPECT_EQ(n % 2, 0U); }
+FUZZ_TEST(DomainTest, TakeEven)
+ .WithDomains(Filter([](unsigned int n) { return n % 2 == 0; },
+ Arbitrary<unsigned int>()));
+
+////////////////////////////////////////////////////////////////
+// pw_status-related types
+
+void TakeStatus(const Status&) {}
+FUZZ_TEST(ArbitraryTest, TakeStatus).WithDomains(Arbitrary<Status>());
+
+void TakeStatusWithSize(const StatusWithSize&) {}
+FUZZ_TEST(ArbitraryTest, TakeStatusWithSize)
+ .WithDomains(Arbitrary<StatusWithSize>());
+
+void TakeNonOkStatus(const Status& status) { EXPECT_FALSE(status.ok()); }
+FUZZ_TEST(FilterTest, TakeNonOkStatus).WithDomains(NonOkStatus());
+
+////////////////////////////////////////////////////////////////
+// pw_result-related types
+
+void TakeResult(const Result<int>&) {}
+FUZZ_TEST(DomainTest, TakeResult).WithDomains(ResultOf(Arbitrary<int>()));
+FUZZ_TEST(ArbitraryTest, TakeResult).WithDomains(Arbitrary<Result<int>>());
+
+////////////////////////////////////////////////////////////////
+// pw_containers-related types
+
+void TakeVector(const Vector<int>& vector) {
+ EXPECT_EQ(vector.max_size(), kSize);
+}
+FUZZ_TEST(DomainTest, TakeVector)
+ .WithDomains(VectorOf<kSize>(Arbitrary<int>()));
+FUZZ_TEST(ArbitraryTest, TakeVector)
+ .WithDomains(Arbitrary<Vector<int, kSize>>());
+
+void TakeVectorAsContainer(const Vector<int>&) {}
+FUZZ_TEST(ContainerTest, TakeVectorAsContainer)
+ .WithDomains(ContainerOf<Vector<int, kSize>>(Arbitrary<int>()));
+
+void TakeVectorNonEmpty(const Vector<int>& vector) {
+ EXPECT_FALSE(vector.empty());
+}
+FUZZ_TEST(ContainerTest, TakeVectorNonEmpty)
+ .WithDomains(NonEmpty(ContainerOf<Vector<int, kSize>>(Arbitrary<int>())));
+
+void TakeVectorLessThan3(const Vector<int>& vector) {
+ EXPECT_LT(vector.size(), 3U);
+}
+FUZZ_TEST(ContainerTest, TakeVectorLessThan3)
+ .WithDomains(
+ ContainerOf<Vector<int, kSize>>(Arbitrary<int>()).WithMaxSize(2));
+
+void TakeVectorAtLeast3(const Vector<int>& vector) {
+ EXPECT_GE(vector.size(), 3U);
+}
+FUZZ_TEST(ContainerTest, TakeVectorAtLeast3)
+ .WithDomains(
+ ContainerOf<Vector<int, kSize>>(Arbitrary<int>()).WithMinSize(3));
+
+void TakeVectorExactly3(const Vector<int>& vector) {
+ EXPECT_EQ(vector.size(), 3U);
+}
+FUZZ_TEST(ContainerTest, TakeVectorExactly3)
+ .WithDomains(ContainerOf<Vector<int, kSize>>(Arbitrary<int>()).WithSize(3));
+
+void TakeVectorUnique(const Vector<int>& vector) {
+ for (auto i = vector.begin(); i != vector.end(); ++i) {
+ for (auto j = i + 1; j != vector.end(); ++j) {
+ EXPECT_NE(*i, *j);
+ }
+ }
+}
+FUZZ_TEST(ContainerTest, TakeVectorUnique)
+ .WithDomains(
+ UniqueElementsContainerOf<Vector<int, kSize>>(Arbitrary<int>()));
+FUZZ_TEST(DomainTest, TakeVectorUnique)
+ .WithDomains(UniqueElementsVectorOf<kSize>(Arbitrary<int>()));
+
+void TakeFlatMap(const containers::FlatMap<int, size_t, kSize>&) {}
+FUZZ_TEST(DomainTest, TakeFlatMap)
+ .WithDomains(FlatMapOf<kSize>(Arbitrary<int>(), Arbitrary<size_t>()));
+FUZZ_TEST(ArbitraryTest, TakeFlatMap)
+ .WithDomains(Arbitrary<containers::FlatMap<int, size_t, kSize>>());
+FUZZ_TEST(ContainerTest, TakeFlatMap)
+ .WithDomains(ContainerOf<containers::FlatMap<int, size_t, kSize>>(
+ FlatMapPairOf(Arbitrary<int>(), Arbitrary<size_t>())));
+FUZZ_TEST(MapToTest, TakeFlatMap)
+ .WithDomains(MapToFlatMap<kSize>(
+ UniqueElementsVectorOf<kSize>(Arbitrary<int>()).WithSize(kSize),
+ ArrayOf<kSize>(Arbitrary<size_t>())));
+
+void TakeDeque(const InlineDeque<int, kSize>& deque) {
+ EXPECT_EQ(deque.max_size(), kSize);
+}
+FUZZ_TEST(DomainTest, TakeDeque).WithDomains(DequeOf<kSize>(Arbitrary<int>()));
+FUZZ_TEST(ArbitraryTest, TakeDeque)
+ .WithDomains(Arbitrary<InlineDeque<int, kSize>>());
+
+void TakeBasicDeque(const BasicInlineDeque<int, unsigned short, kSize>& deque) {
+ EXPECT_EQ(deque.max_size(), kSize);
+}
+FUZZ_TEST(DomainTest, TakeBasicDeque)
+ .WithDomains(BasicDequeOf<unsigned short, kSize>(Arbitrary<int>()));
+FUZZ_TEST(ArbitraryTest, TakeBasicDeque)
+ .WithDomains(Arbitrary<BasicInlineDeque<int, unsigned short, kSize>>());
+
+void TakeQueue(const InlineQueue<int, kSize>& queue) {
+ EXPECT_EQ(queue.max_size(), kSize);
+}
+FUZZ_TEST(DomainTest, TakeQueue).WithDomains(QueueOf<kSize>(Arbitrary<int>()));
+FUZZ_TEST(ArbitraryTest, TakeQueue)
+ .WithDomains(Arbitrary<InlineQueue<int, kSize>>());
+
+void TakeBasicQueue(const BasicInlineQueue<int, unsigned short, kSize>& queue) {
+ EXPECT_EQ(queue.max_size(), kSize);
+}
+FUZZ_TEST(DomainTest, TakeBasicQueue)
+ .WithDomains(BasicQueueOf<unsigned short, kSize>(Arbitrary<int>()));
+FUZZ_TEST(ArbitraryTest, TakeBasicQueue)
+ .WithDomains(Arbitrary<BasicInlineQueue<int, unsigned short, kSize>>());
+
+// Test item that can be added to an intrusive list.
+class TestItem : public IntrusiveList<TestItem>::Item {
+ public:
+ constexpr explicit TestItem(long value) : value_(value) {}
+ long value() const { return value_; }
+
+ private:
+ long value_;
+};
+
+// IntrusiveLists cannot be generated directly, but ScopedLists can.
+void TakeIntrusiveList(const IntrusiveList<TestItem>& list) {
+ EXPECT_LE(list.size(), kSize);
+}
+void ScopedListAdapter(const ScopedList<TestItem, kSize>& scoped) {
+ TakeIntrusiveList(scoped.list());
+}
+FUZZ_TEST(DomainTest, ScopedListAdapter)
+ .WithDomains(ScopedListOf<TestItem, kSize>(Arbitrary<long>()));
+
+////////////////////////////////////////////////////////////////
+// pw_string-related types
+
+void TakeString(const InlineString<>& string) {
+ EXPECT_EQ(string.max_size(), kSize);
+}
+FUZZ_TEST(DomainTest, TakeString)
+ .WithDomains(StringOf<kSize>(Arbitrary<char>()));
+FUZZ_TEST(ArbitraryTest, TakeString)
+ .WithDomains(Arbitrary<InlineString<kSize>>());
+FUZZ_TEST(FilterTest, TakeString).WithDomains(String<kSize>());
+
+void TakeStringAsContainer(const InlineString<>&) {}
+FUZZ_TEST(ContainerTest, TakeStringAsContainer)
+ .WithDomains(ContainerOf<InlineString<kSize>>(Arbitrary<char>()));
+
+void TakeStringNonEmpty(const InlineString<>& string) {
+ EXPECT_FALSE(string.empty());
+}
+FUZZ_TEST(ContainerTest, TakeStringNonEmpty)
+ .WithDomains(NonEmpty(ContainerOf<InlineString<kSize>>(Arbitrary<char>())));
+
+void TakeStringLessThan3(const InlineString<>& string) {
+ EXPECT_LT(string.size(), 3U);
+}
+FUZZ_TEST(ContainerTest, TakeStringLessThan3)
+ .WithDomains(
+ ContainerOf<InlineString<kSize>>(Arbitrary<char>()).WithMaxSize(2));
+
+void TakeStringAtLeast3(const InlineString<>& string) {
+ EXPECT_GE(string.size(), 3U);
+}
+FUZZ_TEST(ContainerTest, TakeStringAtLeast3)
+ .WithDomains(
+ ContainerOf<InlineString<kSize>>(Arbitrary<char>()).WithMinSize(3));
+
+void TakeStringExactly3(const InlineString<>& string) {
+ EXPECT_EQ(string.size(), 3U);
+}
+FUZZ_TEST(ContainerTest, TakeStringExactly3)
+ .WithDomains(
+ ContainerOf<InlineString<kSize>>(Arbitrary<char>()).WithSize(3));
+
+void TakeAsciiString(const InlineString<>& string) {
+ EXPECT_TRUE(std::all_of(
+ string.begin(), string.end(), [](int c) { return c < 0x80; }));
+}
+FUZZ_TEST(FilterTest, TakeAsciiString).WithDomains(AsciiString<kSize>());
+
+void TakePrintableAsciiString(const InlineString<>& string) {
+ EXPECT_TRUE(std::all_of(string.begin(), string.end(), isprint));
+}
+FUZZ_TEST(FilterTest, TakePrintableAsciiString)
+ .WithDomains(PrintableAsciiString<kSize>());
+
+} // namespace
+} // namespace pw::fuzzer
diff --git a/pw_fuzzer/examples/fuzztest/BUILD.bazel b/pw_fuzzer/examples/fuzztest/BUILD.bazel
new file mode 100644
index 000000000..b1ed906d2
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/BUILD.bazel
@@ -0,0 +1,54 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+pw_cc_library(
+ name = "metrics_lib",
+ srcs = [
+ "metrics.cc",
+ ],
+ hdrs = [
+ "metrics.h",
+ ],
+ includes = ["."],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_containers",
+ "//pw_status",
+ "//pw_string",
+ ],
+)
+
+pw_cc_test(
+ name = "metrics_unittest",
+ srcs = ["metrics_unittest.cc"],
+ deps = [":metrics_lib"],
+)
+
+# DOCSTAG: [pwfuzzer_examples_fuzztest-bazel]
+pw_cc_test(
+ name = "metrics_fuzztest",
+ srcs = ["metrics_fuzztest.cc"],
+ deps = [
+ ":metrics_lib",
+ "//pw_fuzzer:fuzztest", # <- Added!
+ ],
+)
+# DOCSTAG: [pwfuzzer_examples_fuzztest-bazel]
diff --git a/pw_fuzzer/examples/fuzztest/BUILD.gn b/pw_fuzzer/examples/fuzztest/BUILD.gn
new file mode 100644
index 000000000..ddc4b46bf
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/BUILD.gn
@@ -0,0 +1,62 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("includes") {
+ visibility = [ ":*" ]
+ include_dirs = [ "." ]
+}
+
+pw_source_set("metrics_lib") {
+ visibility = [ ":*" ]
+ public = [ "metrics.h" ]
+ sources = [ "metrics.cc" ]
+ public_configs = [ ":includes" ]
+ public_deps = [
+ dir_pw_bytes,
+ dir_pw_containers,
+ dir_pw_status,
+ dir_pw_string,
+ ]
+ deps = [ "$dir_pw_assert:check" ]
+}
+
+pw_test("metrics_unittest") {
+ sources = [ "metrics_unittest.cc" ]
+ deps = [ ":metrics_lib" ]
+}
+
+# DOCSTAG: [pwfuzzer_examples_fuzztest-gn]
+pw_test("metrics_fuzztest") {
+ sources = [ "metrics_fuzztest.cc" ]
+ deps = [
+ ":metrics_lib",
+ "$dir_pw_fuzzer:fuzztest", # <- Added!
+ ]
+
+ # TODO: b/283156908 - Re-enable with a fixed seed.
+ enable_if = false
+}
+
+# DOCSTAG: [pwfuzzer_examples_fuzztest-gn]
+pw_test_group("tests") {
+ tests = [
+ ":metrics_unittest",
+ ":metrics_fuzztest",
+ ]
+}
diff --git a/pw_fuzzer/examples/fuzztest/CMakeLists.txt b/pw_fuzzer/examples/fuzztest/CMakeLists.txt
new file mode 100644
index 000000000..3d0d29f92
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/CMakeLists.txt
@@ -0,0 +1,52 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_unit_test/test.cmake)
+
+pw_add_library(pw_fuzzer.examples.fuzztest.metrics_lib STATIC
+ HEADERS
+ metrics.h
+ PUBLIC_INCLUDES
+ .
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
+ pw_string
+ SOURCES
+ metrics.cc
+)
+
+pw_add_test(pw_fuzzer.examples.fuzztest.metrics_unittest
+ SOURCES
+ metrics_unittest.cc
+ PRIVATE_DEPS
+ pw_fuzzer.examples.fuzztest.metrics_lib
+ GROUPS
+ modules
+ pw_fuzzer
+)
+
+# DOCSTAG: [pwfuzzer_examples_fuzztest-cmake]
+pw_add_test(pw_fuzzer.examples.fuzztest.metrics_fuzztest
+ SOURCES
+ metrics_fuzztest.cc
+ PRIVATE_DEPS
+ pw_fuzzer.fuzztest # <- Added!
+ pw_fuzzer.examples.fuzztest.metrics_lib
+ GROUPS
+ modules
+ pw_fuzzer
+)
+# DOCSTAG: [pwfuzzer_examples_fuzztest-cmake]
diff --git a/pw_fuzzer/examples/fuzztest/metrics.cc b/pw_fuzzer/examples/fuzztest/metrics.cc
new file mode 100644
index 000000000..75ae3ee34
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/metrics.cc
@@ -0,0 +1,131 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "metrics.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstring>
+
+#include "pw_assert/check.h"
+#include "pw_status/try.h"
+
+namespace pw::fuzzer::examples {
+namespace {
+Metric::Key Hash(std::string_view str) {
+ PW_CHECK(std::all_of(str.begin(), str.end(), isprint));
+ return static_cast<Metric::Key>(std::hash<std::string_view>{}(str));
+}
+
+template <typename T>
+using IsCopyable =
+ std::enable_if_t<std::is_unsigned_v<T> && !std::is_same_v<T, bool>>;
+
+template <typename T, typename = IsCopyable<T>>
+Status CopyTo(pw::ByteSpan dst, size_t& offset, const T& src) {
+ size_t len = sizeof(src);
+ if (offset + len > dst.size()) {
+ return Status::ResourceExhausted();
+ }
+ memcpy(&dst[offset], &src, len);
+ offset += len;
+ return OkStatus();
+}
+
+template <typename T, typename = IsCopyable<T>>
+Status CopyFrom(pw::ConstByteSpan src, size_t& offset, T& dst) {
+ size_t len = sizeof(dst);
+ if (offset + len > src.size()) {
+ return Status::ResourceExhausted();
+ }
+ memcpy(&dst, &src[offset], len);
+ offset += len;
+ return OkStatus();
+}
+
+} // namespace
+
+Metric::Metric(std::string_view name_, Value value_)
+ : name(name_), value(value_) {
+ key = Hash(name_);
+}
+
+std::optional<Metric::Value> Metrics::GetValue(std::string_view name) const {
+ for (const auto& metric : metrics_) {
+ if (metric.name == name) {
+ return metric.value;
+ }
+ }
+ return std::optional<Metric::Value>();
+}
+
+Status Metrics::SetValue(std::string_view name, Metric::Value value) {
+ for (auto& metric : metrics_) {
+ if (metric.name == name) {
+ metric.value = value;
+ return OkStatus();
+ }
+ }
+ if (metrics_.full()) {
+ return Status::ResourceExhausted();
+ }
+ metrics_.emplace_back(name, value);
+ return OkStatus();
+}
+
+const Vector<Metric>& Metrics::GetMetrics() const { return metrics_; }
+
+Status Metrics::SetMetrics(const Vector<Metric>& metrics) {
+ if (metrics_.capacity() < metrics.size()) {
+ return Status::ResourceExhausted();
+ }
+ metrics_.assign(metrics.begin(), metrics.end());
+ return OkStatus();
+}
+
+StatusWithSize Metrics::Serialize(pw::ByteSpan buffer) const {
+ size_t offset = 0;
+ PW_TRY_WITH_SIZE(CopyTo(buffer, offset, metrics_.size()));
+ for (const auto& metric : metrics_) {
+ PW_TRY_WITH_SIZE(CopyTo(buffer, offset, metric.key));
+ PW_TRY_WITH_SIZE(CopyTo(buffer, offset, metric.value));
+ }
+ return StatusWithSize(offset);
+}
+
+Status Metrics::Deserialize(pw::ConstByteSpan buffer) {
+ size_t offset = 0;
+ size_t num_values = 0;
+ PW_TRY(CopyFrom(buffer, offset, num_values));
+ for (size_t i = 0; i < num_values; ++i) {
+ Metric::Key key;
+ PW_TRY(CopyFrom(buffer, offset, key));
+ Metric::Value value;
+ PW_TRY(CopyFrom(buffer, offset, value));
+ bool found = false;
+ for (auto& metric : metrics_) {
+ if (metric.key == key) {
+ metric.value = value;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return Status::InvalidArgument();
+ }
+ }
+ return OkStatus();
+}
+
+} // namespace pw::fuzzer::examples
diff --git a/pw_fuzzer/examples/fuzztest/metrics.h b/pw_fuzzer/examples/fuzztest/metrics.h
new file mode 100644
index 000000000..9026ffa71
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/metrics.h
@@ -0,0 +1,90 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <optional>
+#include <span>
+#include <string_view>
+
+#include "pw_bytes/span.h"
+#include "pw_containers/vector.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_string/string.h"
+
+namespace pw::fuzzer::examples {
+
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_h]
+// Represent a named value. In order to transmit these values efficiently, they
+// can be referenced by fixed length, generated keys instead of names.
+struct Metric {
+ using Key = uint16_t;
+ using Value = uint32_t;
+
+ static constexpr size_t kMaxNameLen = 32;
+
+ Metric() = default;
+ Metric(std::string_view name_, Value value_);
+
+ InlineString<kMaxNameLen> name;
+ Key key = 0;
+ Value value = 0;
+};
+
+// Represents a set of measurements from a particular source.
+//
+// In order to transmit metrics efficiently, the names of metrics are hashed
+// internally into fixed length keys. The names can be shared once via `GetKeys`
+// and `SetKeys`, after which metrics can be efficiently shared via `Serialize`
+// and `Deserialize`.
+class Metrics {
+ public:
+ static constexpr size_t kMaxMetrics = 32;
+ static constexpr size_t kMaxSerializedSize =
+ sizeof(size_t) +
+ kMaxMetrics * (sizeof(Metric::Key) + sizeof(Metric::Value));
+
+ // Retrieves the value of a named metric and stores it in `out_value`. The
+ // name must consist of printable ASCII characters. Returns false if the named
+ // metric was not `Set` or `Import`ed.
+ std::optional<Metric::Value> GetValue(std::string_view name) const;
+
+ // Sets the value of a named metric. The name must consist of printable ASCII
+ // characters, and will be added to the mapping of names to keys.
+ Status SetValue(std::string_view name, Metric::Value value);
+
+ // Returns the current mapping of names to keys.
+ const Vector<Metric>& GetMetrics() const;
+
+ // Replaces the current mapping of names to keys.
+ Status SetMetrics(const Vector<Metric>& metrics);
+
+ // Serializes this object to the given `buffer`. Does not write more bytes
+ // than `buffer.size()`. Returns the number of number of bytes written or an
+ // error if insufficient space.
+ StatusWithSize Serialize(pw::ByteSpan buffer) const;
+
+ // Populates this object from the data in the given `buffer`.
+ // Returns whether this buffer could be deserialized.
+ Status Deserialize(pw::ConstByteSpan buffer);
+
+ private:
+ Vector<Metric, kMaxMetrics> metrics_;
+};
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_h]
+
+} // namespace pw::fuzzer::examples
diff --git a/pw_fuzzer/examples/fuzztest/metrics_fuzztest.cc b/pw_fuzzer/examples/fuzztest/metrics_fuzztest.cc
new file mode 100644
index 000000000..1549fcd04
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/metrics_fuzztest.cc
@@ -0,0 +1,87 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <algorithm>
+
+#include "gtest/gtest.h"
+#include "metrics.h"
+#include "pw_fuzzer/fuzztest.h"
+
+namespace pw::fuzzer::examples {
+
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest1]
+void ArbitrarySerializeAndDeserialize(const Vector<Metric>& metrics) {
+ std::array<std::byte, Metrics::kMaxSerializedSize> buffer;
+
+ // Add and copy the names only.
+ Metrics src, dst;
+ for (const auto& metric : metrics) {
+ EXPECT_TRUE(src.SetValue(metric.name, 0).ok());
+ }
+ EXPECT_TRUE(dst.SetMetrics(src.GetMetrics()).ok());
+
+ // Modify the values.
+ for (const auto& metric : metrics) {
+ EXPECT_TRUE(src.SetValue(metric.name, metric.value).ok());
+ }
+
+ // Transfer the data and check.
+ EXPECT_TRUE(src.Serialize(buffer).ok());
+ EXPECT_TRUE(dst.Deserialize(buffer).ok());
+ for (const auto& metric : metrics) {
+ EXPECT_EQ(dst.GetValue(metric.name).value_or(0), metric.value);
+ }
+}
+
+// This unit test will run on host and may run on target devices (if supported).
+TEST(MetricsTest, SerializeAndDeserialize) {
+ Vector<Metric, 3> metrics;
+ metrics.emplace_back("one", 1);
+ metrics.emplace_back("two", 2);
+ metrics.emplace_back("three", 3);
+ ArbitrarySerializeAndDeserialize(metrics);
+}
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest1]
+
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest2]
+auto ArbitraryMetric() {
+ return ConstructorOf<Metric>(PrintableAsciiString<Metric::kMaxNameLen>(),
+ Arbitrary<uint32_t>());
+}
+
+// This fuzz test will only run on host.
+FUZZ_TEST(MetricsTest, ArbitrarySerializeAndDeserialize)
+ .WithDomains(VectorOf<Metrics::kMaxMetrics>(ArbitraryMetric()));
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest2]
+
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest3]
+void ArbitraryDeserialize(pw::ConstByteSpan buffer) {
+ // Just make sure this does not crash.
+ Metrics dst;
+ dst.Deserialize(buffer).IgnoreError();
+}
+
+// This unit test will run on host and may run on target devices (if supported).
+TEST(MetricsTest, DeserializeDoesNotCrash) {
+ ArbitraryDeserialize(std::vector<std::byte>(100, std::byte(0x5C)));
+}
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest3]
+
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest4]
+// This fuzz test will only run on host.
+FUZZ_TEST(MetricsTest, ArbitraryDeserialize)
+ .WithDomains(VectorOf<Metrics::kMaxSerializedSize>(Arbitrary<std::byte>()));
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_fuzztest4]
+
+} // namespace pw::fuzzer::examples
diff --git a/pw_fuzzer/examples/fuzztest/metrics_unittest.cc b/pw_fuzzer/examples/fuzztest/metrics_unittest.cc
new file mode 100644
index 000000000..329474d19
--- /dev/null
+++ b/pw_fuzzer/examples/fuzztest/metrics_unittest.cc
@@ -0,0 +1,57 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "metrics.h"
+
+#include <algorithm>
+
+#include "gtest/gtest.h"
+
+namespace pw::fuzzer::examples {
+
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_unittest]
+TEST(MetricsTest, SerializeAndDeserialize) {
+ std::array<std::byte, Metrics::kMaxSerializedSize> buffer;
+
+ // Add and copy the names only.
+ Metrics src, dst;
+ EXPECT_TRUE(src.SetValue("one", 0).ok());
+ EXPECT_TRUE(src.SetValue("two", 0).ok());
+ EXPECT_TRUE(src.SetValue("three", 0).ok());
+ EXPECT_TRUE(dst.SetMetrics(src.GetMetrics()).ok());
+
+ // Modify the values.
+ EXPECT_TRUE(src.SetValue("one", 1).ok());
+ EXPECT_TRUE(src.SetValue("two", 2).ok());
+ EXPECT_TRUE(src.SetValue("three", 3).ok());
+
+ // Transfer the data and check.
+ EXPECT_TRUE(src.Serialize(buffer).ok());
+ EXPECT_TRUE(dst.Deserialize(buffer).ok());
+ EXPECT_EQ(dst.GetValue("one").value_or(0), 1U);
+ EXPECT_EQ(dst.GetValue("two").value_or(0), 2U);
+ EXPECT_EQ(dst.GetValue("three").value_or(0), 3U);
+}
+
+TEST(MetricsTest, DeserializeDoesNotCrash) {
+ std::array<std::byte, Metrics::kMaxSerializedSize> buffer;
+ std::fill(buffer.begin(), buffer.end(), std::byte(0x5C));
+
+ // Just make sure this does not crash.
+ Metrics dst;
+ dst.Deserialize(buffer).IgnoreError();
+}
+// DOCSTAG: [pwfuzzer_examples_fuzztest-metrics_unittest]
+
+} // namespace pw::fuzzer::examples
diff --git a/pw_bloat/py/setup.py b/pw_fuzzer/examples/libfuzzer/BUILD.bazel
index 7fef9f77f..f4a02ad9c 100644
--- a/pw_bloat/py/setup.py
+++ b/pw_fuzzer/examples/libfuzzer/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,8 +11,12 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_bloat"""
-import setuptools # type: ignore
+load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
-setuptools.setup() # Package definition in setup.cfg
+pw_cc_fuzz_test(
+ name = "toy_fuzzer",
+ srcs = ["toy_fuzzer.cc"],
+ tags = ["no-oss-fuzz"],
+ deps = ["//pw_status"],
+)
diff --git a/pw_fuzzer/examples/libfuzzer/BUILD.gn b/pw_fuzzer/examples/libfuzzer/BUILD.gn
new file mode 100644
index 000000000..da1d3594d
--- /dev/null
+++ b/pw_fuzzer/examples/libfuzzer/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_fuzzer/fuzzer.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_fuzzer("toy_fuzzer") {
+ sources = [ "toy_fuzzer.cc" ]
+ deps = [ dir_pw_status ]
+}
+
+group("fuzzers") {
+ deps = [ ":toy_fuzzer" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":toy_fuzzer_test" ]
+}
diff --git a/pw_fuzzer/examples/toy_fuzzer.cc b/pw_fuzzer/examples/libfuzzer/toy_fuzzer.cc
index 99b04df67..99b04df67 100644
--- a/pw_fuzzer/examples/toy_fuzzer.cc
+++ b/pw_fuzzer/examples/libfuzzer/toy_fuzzer.cc
diff --git a/pw_fuzzer/fuzz_test.gni b/pw_fuzzer/fuzz_test.gni
new file mode 100644
index 000000000..ecaa46aa1
--- /dev/null
+++ b/pw_fuzzer/fuzz_test.gni
@@ -0,0 +1,42 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_unit_test/test.gni")
+
+# Creates a unit test that may also include fuzzing test cases.
+#
+# This template merely updates test metadata and adds a dependency on FuzzTest
+# before forwarding to `pw_test`. The resulting test can always be run as a unit
+# test. If `dir_pw_third_party_fuzztest` is set and the toolchain used to build
+# the test supports fuzzing, it can also be run as a fuzzer.
+#
+# As with `pw_test`, targets defined using this template will produce test
+# metadata with a `test_type` of "unit_test" and an additional `test_directory`
+# value describing the location of the test binary within the build output.
+# These tests can be distinguished from other unit tests by additionally having
+# a "fuzztest" tag.
+#
+# Args:
+# - All of the `pw_test` args are accepted.
+template("pw_fuzz_test") {
+ pw_test(target_name) {
+ deps = []
+ tags = []
+ forward_variables_from(invoker, "*")
+ deps += [ "$dir_pw_fuzzer:fuzztest" ]
+ tags += [ "fuzztest" ]
+ }
+}
diff --git a/pw_fuzzer/fuzzer.bzl b/pw_fuzzer/fuzzer.bzl
index 3d158f155..df9e103b5 100644
--- a/pw_fuzzer/fuzzer.bzl
+++ b/pw_fuzzer/fuzzer.bzl
@@ -14,15 +14,20 @@
"""Utilities for fuzzing."""
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
-load(
- "//pw_build/bazel_internal:pigweed_internal.bzl",
- _add_defaults = "add_defaults",
-)
def pw_cc_fuzz_test(**kwargs):
- # TODO(b/234877642): Remove this implicit dependency once we have a better
+ """Wrapper for cc_fuzz_test that adds required Pigweed dependencies.
+
+ Args:
+ **kwargs: Arguments to be augmented.
+ """
+ kwargs["deps"].append("//pw_fuzzer:libfuzzer")
+
+ # TODO: b/234877642 - Remove this implicit dependency once we have a better
# way to handle the facades without introducing a circular dependency into
# the build.
- kwargs["deps"].append("@pigweed_config//:pw_assert_backend")
- _add_defaults(kwargs)
+ kwargs["deps"].append("@pigweed//targets:pw_assert_backend_impl")
+
+ # TODO: b/292628774 - Only linux is supported for now.
+ kwargs["target_compatible_with"] = ["@platforms//os:linux"]
cc_fuzz_test(**kwargs)
diff --git a/pw_fuzzer/fuzzer.gni b/pw_fuzzer/fuzzer.gni
index a43a2d9cb..512bc8a51 100644
--- a/pw_fuzzer/fuzzer.gni
+++ b/pw_fuzzer/fuzzer.gni
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/error.gni")
+import("$dir_pw_build/test_info.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
import("$dir_pw_unit_test/test.gni")
@@ -33,8 +34,7 @@ import("$dir_pw_unit_test/test.gni")
#
# Args:
# - enable_test_if: (optional) Passed as `enable_if` to the unit test.
-# Remaining arguments are the same as `pw_executable`.
-#
+# - All of the `pw_executable` args are accepted.
template("pw_fuzzer") {
if (!pw_toolchain_FUZZING_ENABLED) {
pw_error(target_name) {
@@ -47,6 +47,30 @@ template("pw_fuzzer") {
}
not_needed(invoker, "*")
} else {
+ # Metadata for this test when used as part of a pw_test_group target.
+ _fuzzer_target_name = target_name
+ _fuzzer_output_dir = "${target_out_dir}/bin"
+ if (defined(invoker.output_dir)) {
+ _fuzzer_output_dir = invoker.output_dir
+ }
+
+ _tags = [ "libfuzzer" ]
+ if (defined(invoker.tags)) {
+ _tags += invoker.tags
+ }
+
+ _test_metadata = "${target_name}.metadata"
+ _extra_metadata = {
+ forward_variables_from(invoker, [ "extra_metadata" ])
+ test_directory = rebase_path(_fuzzer_output_dir, root_build_dir)
+ }
+ pw_test_info(_test_metadata) {
+ test_type = "fuzz_test"
+ test_name = _fuzzer_target_name
+ tags = _tags
+ extra_metadata = _extra_metadata
+ }
+
pw_executable(target_name) {
configs = []
deps = []
@@ -57,16 +81,70 @@ template("pw_fuzzer") {
"visibility",
])
forward_variables_from(invoker, [ "visibility" ])
- configs += [ "$dir_pw_fuzzer:engine" ]
- deps += [ dir_pw_fuzzer ]
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ configs += [ "$dir_pw_fuzzer:libfuzzer_oss_fuzz_config" ]
+ } else {
+ configs += [ "$dir_pw_fuzzer:libfuzzer_config" ]
+ }
+ deps += [
+ ":$_test_metadata",
+ "$dir_pw_fuzzer:libfuzzer",
+ ]
+ output_dir = _fuzzer_output_dir
+ metadata = {
+ test_barrier = [ ":$_test_metadata" ]
+ }
}
}
+ group(target_name + ".run") {
+ }
+
pw_test("${target_name}_test") {
deps = []
forward_variables_from(invoker, "*", [ "visibility" ])
forward_variables_from(invoker, [ "visibility" ])
- deps += [ "$dir_pw_fuzzer:run_as_unit_test" ]
+ deps += [ "$dir_pw_fuzzer:libfuzzer_test" ]
enable_if = !defined(enable_test_if) || enable_test_if
}
}
+
+# Defines a related collection of fuzzers.
+#
+# This template wraps `pw_test_group` to collect a set of libFuzzer-based fuzzer
+# tests. These unit tests do not perform fuzzing. Instead, they execute the fuzz
+# target function with a set of fixed inputs to verify the fuzzer can be built
+# and run.
+#
+# If and only if the current toolchain supports fuzzing, this template will also
+# include the fuzzers themselves.
+#
+# As with `pw_test_group`, targets defined using this template will produce test
+# metadata with a `test_type` of "test_group" and an additional `deps` list
+# describing the tests collected by this target.
+#
+# Args:
+# - fuzzers: List of `pw_fuzzer` targets for each of the fuzzers in the group.
+#
+# - The following args have the same meaning as for `pw_python_action`:
+# group_deps
+# enable_if
+# output_metadata
+template("pw_fuzzer_group") {
+ _with_fuzzers = pw_toolchain_FUZZING_ENABLED && pw_toolchain_SANITIZERS != []
+ pw_test_group(target_name) {
+ forward_variables_from(invoker,
+ "*",
+ [
+ "fuzzers",
+ "tests",
+ ])
+ tests = []
+ foreach(fuzzer, invoker.fuzzers) {
+ if (_with_fuzzers) {
+ tests += [ fuzzer ]
+ }
+ tests += [ fuzzer + "_test" ]
+ }
+ }
+}
diff --git a/pw_fuzzer/guides/fuzztest.rst b/pw_fuzzer/guides/fuzztest.rst
new file mode 100644
index 000000000..1586ef9b5
--- /dev/null
+++ b/pw_fuzzer/guides/fuzztest.rst
@@ -0,0 +1,394 @@
+.. _module-pw_fuzzer-guides-using_fuzztest:
+
+========================================
+pw_fuzzer: Adding Fuzzers Using FuzzTest
+========================================
+.. pigweed-module-subpage::
+ :name: pw_fuzzer
+ :tagline: Better C++ code through easier fuzzing
+
+.. note::
+
+ `FuzzTest`_ is currently only supported on Linux and MacOS using Clang.
+
+.. _module-pw_fuzzer-guides-using_fuzztest-toolchain:
+
+----------------------------------------
+Step 0: Set up FuzzTest for your project
+----------------------------------------
+.. note::
+
+ This workflow only needs to be done once for a project.
+
+FuzzTest and its dependencies are not included in Pigweed and need to be added.
+
+See the following:
+
+* :ref:`module-pw_third_party_abseil_cpp-using_upstream`
+* :ref:`module-pw_third_party_fuzztest-using_upstream`
+* :ref:`module-pw_third_party_googletest-using_upstream`
+* :ref:`module-pw_third_party_re2-using_upstream`
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ You may not want to use upstream GoogleTest all the time. For example, it
+ may not be supported on your target device. In this case, you can limit it
+ to a specific toolchain used for fuzzing. For example:
+
+ .. code-block::
+
+ import("$dir_pw_toolchain/host/target_toolchains.gni")
+
+ my_toolchains = {
+ ...
+ clang_fuzz = {
+ name = "my_clang_fuzz"
+ forward_variables_from(pw_toolchain_host.clang_fuzz, "*", ["name"])
+ pw_unit_test_MAIN = "$dir_pw_fuzzer:fuzztest_main"
+ pw_unit_test_GOOGLETEST_BACKEND = "$dir_pw_fuzzer:gtest"
+ }
+ ...
+ }
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ FuzzTest is enabled by setting several CMake variables. The easiest way to
+ set these is to extend your ``toolchain.cmake`` file.
+
+ For example:
+
+ .. code-block::
+
+ include(my_project_toolchain.cmake)
+
+ set(dir_pw_third_party_fuzztest
+ "path/to/fuzztest"
+ CACHE STRING "" FORCE
+ )
+ set(dir_pw_third_party_googletest
+ "path/to/googletest"
+ CACHE STRING "" FORCE
+ )
+ set(pw_unit_test_GOOGLETEST_BACKEND
+ "pw_third_party.fuzztest"
+ CACHE STRING "" FORCE
+ )
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ FuzzTest provides a build configuration that can be imported into your
+ ``.bazelrc`` file. Add the following:
+
+ .. code-block::
+
+ # Include FuzzTest build configurations.
+ try-import %workspace%/third_party/fuzztest/fuzztest.bazelrc
+ build:fuzztest --@pigweed//targets:fuzztest_config=//pw_fuzzer:fuzztest
+
+----------------------------------------
+Step 1: Write a unit test for the target
+----------------------------------------
+
+As noted previously, the very first step is to identify one or more target
+behavior that would benefit from testing. See `FuzzTest Use Cases`_ for more
+details on how to identify this code.
+
+Once identified, it is useful to start from a unit test. You may already have a
+unit test writtern, but if not it is likely still be helpful to write one first.
+Many developers are more familiar with writing unit tests, and there are
+detailed guides available. See for example the `GoogleTest documentation`_.
+
+This guide will use code from ``//pw_fuzzer/examples/fuzztest/``. This code
+includes the following object as an example of code that would benefit from
+fuzzing for undefined behavior and from roundtrip fuzzing.
+
+.. note::
+
+ To keep the example simple, this code uses the standard library. As a result,
+ this code may not work with certain devices.
+
+.. literalinclude:: ../examples/fuzztest/metrics.h
+ :language: cpp
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-metrics_h]
+ :end-before: [pwfuzzer_examples_fuzztest-metrics_h]
+
+Unit tests for this class might attempt to deserialize previously serialized
+objects and to deserialize invalid data:
+
+.. literalinclude:: ../examples/fuzztest/metrics_unittest.cc
+ :language: cpp
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-metrics_unittest]
+ :end-before: [pwfuzzer_examples_fuzztest-metrics_unittest]
+
+--------------------------------------------
+Step 2: Convert your unit test to a function
+--------------------------------------------
+
+Examine your unit tests and identify any places you have fixed values that could
+vary. Turn your unit test into a function that takes those values as parameters.
+Since fuzzing may not occur on all targets, you should preserve your unit test
+by calling the new function with the previously fixed values.
+
+.. literalinclude:: ../examples/fuzztest/metrics_fuzztest.cc
+ :language: cpp
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-metrics_fuzztest1]
+ :end-before: [pwfuzzer_examples_fuzztest-metrics_fuzztest1]
+
+.. literalinclude:: ../examples/fuzztest/metrics_fuzztest.cc
+ :language: cpp
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-metrics_fuzztest3]
+ :end-before: [pwfuzzer_examples_fuzztest-metrics_fuzztest3]
+
+Note that in ``ArbitrarySerializeAndDeserialize`` we no longer assume the
+marshalling will always be successful, and exit early if it is not. You may need
+to make similar modifications to your unit tests if constraints on parameters
+are not expressed by `domains`__ as described below.
+
+.. __: `FuzzTest Domain Reference`_
+
+--------------------------------------------
+Step 3: Add a FUZZ_TEST macro invocation
+--------------------------------------------
+
+Now, include ``"fuzztest/fuzztest.h"`` and pass the test suite name and your
+function name to the ``FUZZ_TEST`` macro. Call ``WithDomains`` on the returned
+object to specify the input domain for each parameter of the function. For
+example:
+
+.. literalinclude:: ../examples/fuzztest/metrics_fuzztest.cc
+ :language: cpp
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-metrics_fuzztest2]
+ :end-before: [pwfuzzer_examples_fuzztest-metrics_fuzztest2]
+
+.. literalinclude:: ../examples/fuzztest/metrics_fuzztest.cc
+ :language: cpp
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-metrics_fuzztest4]
+ :end-before: [pwfuzzer_examples_fuzztest-metrics_fuzztest4]
+
+You may know of specific values that are "interesting", i.e. that represent
+boundary conditions, involve, special handling, etc. To guide the fuzzer towards
+these code paths, you can include them as `seeds`_. However, as noted in
+the comments of the examples, it is recommended to include a unit test with the
+original parameters to ensure the code is tested on target devices.
+
+FuzzTest provides more detailed documentation on these topics. For example:
+
+* Refer to `The FUZZ_TEST Macro`_ reference for more details on how to use this
+ macro.
+
+* Refer to the `FuzzTest Domain Reference`_ for details on all the different
+ types of domains supported by FuzzTest and how they can be combined.
+
+* Refer to the `Test Fixtures`_ reference for how to create fuzz tests from unit
+ tests that use GoogleTest fixtures.
+
+------------------------------------
+Step 4: Add the fuzzer to your build
+------------------------------------
+Next, indicate that the unit test includes one or more fuzz tests.
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ The ``pw_fuzz_test`` template can be used to add the necessary FuzzTest
+ dependency and generate test metadata.
+
+ For example, consider the following ``BUILD.gn``:
+
+ .. literalinclude:: ../examples/fuzztest/BUILD.gn
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-gn]
+ :end-before: [pwfuzzer_examples_fuzztest-gn]
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ Unit tests can support fuzz tests by simply adding a dependency on
+ FuzzTest.
+
+ For example, consider the following ``CMakeLists.txt``:
+
+ .. literalinclude:: ../examples/fuzztest/CMakeLists.txt
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-cmake]
+ :end-before: [pwfuzzer_examples_fuzztest-cmake]
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ Unit tests can support fuzz tests by simply adding a dependency on
+ FuzzTest.
+
+ For example, consider the following ``BUILD.bazel``:
+
+ .. literalinclude:: ../examples/fuzztest/BUILD.bazel
+ :linenos:
+ :start-after: [pwfuzzer_examples_fuzztest-bazel]
+ :end-before: [pwfuzzer_examples_fuzztest-bazel]
+
+------------------------
+Step 5: Build the fuzzer
+------------------------
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ Build using ``ninja`` on a target that includes your fuzzer with a
+ :ref:`fuzzing toolchain<module-pw_fuzzer-guides-using_fuzztest-toolchain>`.
+
+ Pigweed includes a ``//:fuzzers`` target that builds all tests, including
+ those with fuzzers, using a fuzzing toolchain. You may wish to add a
+ similar top-level to your project. For example:
+
+ .. code-block::
+
+ group("fuzzers") {
+ deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_fuzz)" ]
+ }
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ Build using ``cmake`` with the FuzzTest and GoogleTest variables set. For
+ example:
+
+ .. code-block::
+
+ cmake ... \
+ -Ddir_pw_third_party_fuzztest=path/to/fuzztest \
+ -Ddir_pw_third_party_googletest=path/to/googletest \
+ -Dpw_unit_test_GOOGLETEST_BACKEND=pw_third_party.fuzztest
+
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ By default, ``bazel`` will simply omit the fuzz tests and build unit
+ tests. To build these tests as fuzz tests, specify the ``fuzztest``
+ config. For example:
+
+ .. code-block:: sh
+
+ bazel build //... --config=fuzztest
+
+----------------------------------
+Step 6: Running the fuzzer locally
+----------------------------------
+.. TODO: b/281138993 - Add tooling to make it easier to find and run fuzzers.
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ When building. Most toolchains will simply omit the fuzz tests and build
+ and run unit tests. A
+ :ref:`fuzzing toolchain<module-pw_fuzzer-guides-using_fuzztest-toolchain>`
+ will include the fuzzers, but only run them for a limited time. This makes
+ them suitable for automated testing as in CQ.
+
+ If you used the top-level ``//:fuzzers`` described in the previous
+ section, you can find available fuzzers using the generated JSON test
+ metadata file:
+
+ .. code-block:: sh
+
+ jq '.[] | select(contains({tags: ["fuzztest"]}))' \
+ out/host_clang_fuzz/obj/pw_module_tests.testinfo.json
+
+ To run a fuzz with different options, you can pass additional flags to the
+ fuzzer binary. This binary will be in a subdirectory related to the
+ toolchain. For example:
+
+ .. code-block:: sh
+
+ out/host_clang_fuzz/obj/my_module/test/metrics_test \
+ --fuzz=MetricsTest.Roundtrip
+
+ Additional `sanitizer flags`_ may be passed uisng environment variables.
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ When built with FuzzTest and GoogleTest, the fuzzer binaries can be run
+ directly from the CMake build directory. By default, the fuzzers will only
+ run for a limited time. This makes them suitable for automated testing as
+ in CQ. To run a fuzz with different options, you can pass additional flags
+ to the fuzzer binary.
+
+ For example:
+
+ .. code-block:: sh
+
+ build/my_module/metrics_test --fuzz=MetricsTest.Roundtrip
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ By default, ``bazel`` will simply omit the fuzz tests and build and run
+ unit tests. To build these tests as fuzz tests, specify the "fuzztest"
+ config. For example:
+
+ .. code-block:: sh
+
+ bazel test //... --config=fuzztest
+
+ This will build the tests as fuzz tests, but only run them for a limited
+ time. This makes them suitable for automated testing as in CQ.
+
+ To run a fuzz with different options, you can use ``run`` and pass
+ additional flags to the fuzzer binary. For example:
+
+ .. code-block:: sh
+
+ bazel run //my_module:metrics_test --config=fuzztest \
+ --fuzz=MetricsTest.Roundtrip
+
+Running the fuzzer should produce output similar to the following:
+
+.. code-block::
+
+ [.] Sanitizer coverage enabled. Counter map size: 21290, Cmp map size: 262144
+ Note: Google Test filter = MetricsTest.Roundtrip
+ [==========] Running 1 test from 1 test suite.
+ [----------] Global test environment set-up.
+ [----------] 1 test from MetricsTest
+ [ RUN ] MetricsTest.Roundtrip
+ [*] Corpus size: 1 | Edges covered: 131 | Fuzzing time: 504.798us | Total runs: 1.00e+00 | Runs/secs: 1
+ [*] Corpus size: 2 | Edges covered: 133 | Fuzzing time: 934.176us | Total runs: 3.00e+00 | Runs/secs: 3
+ [*] Corpus size: 3 | Edges covered: 134 | Fuzzing time: 2.384383ms | Total runs: 5.30e+01 | Runs/secs: 53
+ [*] Corpus size: 4 | Edges covered: 137 | Fuzzing time: 2.732274ms | Total runs: 5.40e+01 | Runs/secs: 54
+ [*] Corpus size: 5 | Edges covered: 137 | Fuzzing time: 7.275553ms | Total runs: 2.48e+02 | Runs/secs: 248
+
+.. TODO: b/282560789 - Add guides/improve_fuzzers.rst
+.. TODO: b/281139237 - Add guides/continuous_fuzzing.rst
+.. ----------
+.. Next steps
+.. ----------
+.. Once you have a working fuzzer, the next steps are to:
+
+.. * `Run it continuously on a fuzzing infrastructure <continuous_fuzzing>`_.
+.. * `Measure its code coverage and improve it <improve_fuzzers>`_.
+
+.. _FuzzTest: https://github.com/google/fuzztest
+.. _FuzzTest Domain Reference: https://github.com/google/fuzztest/blob/main/doc/domains-reference.md
+.. _FuzzTest Use Cases: https://github.com/google/fuzztest/blob/main/doc/use-cases.md
+.. _GoogleTest documentation: https://google.github.io/googletest/
+.. _Test Fixtures: https://github.com/google/fuzztest/blob/main/doc/fixtures.md
+.. _The FUZZ_TEST Macro: https://github.com/google/fuzztest/blob/main/doc/fuzz-test-macro.md
+.. _sanitizer flags: https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
+.. _seeds: https://github.com/google/fuzztest/blob/main/doc/fuzz-test-macro.md#initial-seeds
diff --git a/pw_fuzzer/guides/index.rst b/pw_fuzzer/guides/index.rst
new file mode 100644
index 000000000..f3ef415af
--- /dev/null
+++ b/pw_fuzzer/guides/index.rst
@@ -0,0 +1,18 @@
+:orphan:
+
+.. This file is included via `pigweed-module`.
+
+.. _module-pw_fuzzer-guides:
+
+=================
+pw_fuzzer: Guides
+=================
+.. pigweed-module-subpage::
+ :name: pw_fuzzer
+ :tagline: Better C++ code through easier fuzzing
+
+``pw_fuzzer`` contains detailed guides on the following topics:
+
+* :ref:`module-pw_fuzzer-guides-using_fuzztest`
+* :ref:`module-pw_fuzzer-guides-using_libfuzzer`
+* :ref:`module-pw_fuzzer-guides-reproducing_oss_fuzz_bugs`
diff --git a/pw_fuzzer/guides/libfuzzer.rst b/pw_fuzzer/guides/libfuzzer.rst
new file mode 100644
index 000000000..a0c51a418
--- /dev/null
+++ b/pw_fuzzer/guides/libfuzzer.rst
@@ -0,0 +1,359 @@
+.. _module-pw_fuzzer-guides-using_libfuzzer:
+
+=========================================
+pw_fuzzer: Adding Fuzzers Using LibFuzzer
+=========================================
+.. pigweed-module-subpage::
+ :name: pw_fuzzer
+ :tagline: Better C++ code through easier fuzzing
+
+.. note::
+
+ `libFuzzer`_ is currently only supported on Linux and MacOS using clang.
+
+.. _module-pw_fuzzer-guides-using_libfuzzer-toolchain:
+
+-----------------------------------------
+Step 0: Set up libFuzzer for your project
+-----------------------------------------
+.. note::
+
+ This workflow only needs to be done once for a project.
+
+`libFuzzer`_ is a LLVM compiler runtime and should included with your ``clang``
+installation. In order to use it, you only need to define a suitable toolchain.
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ Use ``pw_toolchain_host_clang``, or derive a new toolchain from it.
+ For example:
+
+ .. code-block::
+
+ import("$dir_pw_toolchain/host/target_toolchains.gni")
+
+ my_toolchains = {
+ ...
+ clang_fuzz = {
+ name = "my_clang_fuzz"
+ forward_variables_from(pw_toolchain_host.clang_fuzz, "*", ["name"])
+ }
+ ...
+ }
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ LibFuzzer-style fuzzers are not currently supported by Pigweed when using
+ CMake.
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ Include ``rules_fuzzing`` and its Abseil C++ dependency in your
+ ``WORKSPACE`` file. For example:
+
+ .. code-block::
+
+ # Required by: rules_fuzzing.
+ http_archive(
+ name = "com_google_absl",
+ sha256 = "3ea49a7d97421b88a8c48a0de16c16048e17725c7ec0f1d3ea2683a2a75adc21",
+ strip_prefix = "abseil-cpp-20230125.0",
+ urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20230125.0.tar.gz"],
+ )
+
+ # Set up rules for fuzz testing.
+ http_archive(
+ name = "rules_fuzzing",
+ sha256 = "d9002dd3cd6437017f08593124fdd1b13b3473c7b929ceb0e60d317cb9346118",
+ strip_prefix = "rules_fuzzing-0.3.2",
+ urls = ["https://github.com/bazelbuild/rules_fuzzing/archive/v0.3.2.zip"],
+ )
+
+ load("@rules_fuzzing//fuzzing:repositories.bzl", "rules_fuzzing_dependencies")
+
+ rules_fuzzing_dependencies()
+
+ load("@rules_fuzzing//fuzzing:init.bzl", "rules_fuzzing_init")
+
+ rules_fuzzing_init()
+
+ Then, define the following build configuration in your ``.bazelrc`` file:
+
+ .. code-block::
+
+ build:asan-libfuzzer \
+ --@rules_fuzzing//fuzzing:cc_engine=@rules_fuzzing//fuzzing/engines:libfuzzer
+ build:asan-libfuzzer \
+ --@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
+ build:asan-libfuzzer --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
+
+------------------------------------
+Step 1: Write a fuzz target function
+------------------------------------
+To write a fuzzer, a developer needs to write a `fuzz target function`_
+following the guidelines given by libFuzzer:
+
+.. code-block:: cpp
+
+ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ DoSomethingInterestingWithMyAPI(data, size);
+ return 0; // Non-zero return values are reserved for future use.
+ }
+
+When writing your fuzz target function, you may want to consider:
+
+- It is acceptable to return early if the input doesn't meet some constraints,
+ e.g. it is too short.
+- If your fuzzer accepts data with a well-defined format, you can bootstrap
+ coverage by crafting examples and adding them to a `corpus`_.
+- There are tools to `split a fuzzing input`_ into multiple fields if needed;
+ the `FuzzedDataProvider`_ is particularly easy to use.
+- If your code acts on "transformed" inputs, such as encoded or compressed
+ inputs, you may want to try `structure aware fuzzing`.
+- You can do `startup initialization`_ if you need to.
+- If your code is non-deterministic or uses checksums, you may want to disable
+ those **only** when fuzzing by using LLVM's
+ `FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION`_
+
+------------------------------------
+Step 2: Add the fuzzer to your build
+------------------------------------
+To build a fuzzer, do the following:
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ Add the GN target to the module using ``pw_fuzzer`` GN template. If you
+ wish to limit when the generated unit test is run, you can set
+ ``enable_test_if`` in the same manner as ``enable_if`` for `pw_test`:
+
+ .. code-block::
+
+ # In $dir_my_module/BUILD.gn
+ import("$dir_pw_fuzzer/fuzzer.gni")
+
+ pw_fuzzer("my_fuzzer") {
+ sources = [ "my_fuzzer.cc" ]
+ deps = [ ":my_lib" ]
+ enable_test_if = device_has_1m_flash
+ }
+
+ Add the fuzzer GN target to the module's group of fuzzers. Create this
+ group if it does not exist.
+
+ .. code-block::
+
+ # In $dir_my_module/BUILD.gn
+ group("fuzzers") {
+ deps = [
+ ...
+ ":my_fuzzer",
+ ]
+ }
+
+ Make sure this group is referenced from a top-level ``fuzzers`` target in
+ your project, with the appropriate
+ :ref:`fuzzing toolchain<module-pw_fuzzer-guides-using_libfuzzer-toolchain>`.
+ For example:
+
+ .. code-block::
+
+ # In //BUILD.gn
+ group("fuzzers") {
+ deps = [
+ ...
+ "$dir_my_module:fuzzers(//my_toolchains:host_clang_fuzz)",
+ ]
+ }
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ LibFuzzer-style fuzzers are not currently supported by Pigweed when using
+ CMake.
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ Add a Bazel target to the module using the ``pw_cc_fuzz_test`` rule. For
+ example:
+
+ .. code-block::
+
+ # In $dir_my_module/BUILD.bazel
+ pw_cc_fuzz_test(
+ name = "my_fuzzer",
+ srcs = ["my_fuzzer.cc"],
+ deps = [":my_lib"]
+ )
+
+----------------------------------------------
+Step 3: Add the fuzzer unit test to your build
+----------------------------------------------
+Pigweed automatically generates unit tests for libFuzzer-based fuzzers in some
+build systems.
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ The generated unit test will be suffixed by ``_test`` and needs to be
+ added to the module's test group. This test verifies the fuzzer can build
+ and run, even when not being built in a
+ :ref:`fuzzing toolchain<module-pw_fuzzer-guides-using_libfuzzer-toolchain>`.
+ For example, for a fuzzer called ``my_fuzzer``, add the following:
+
+ .. code-block::
+
+ # In $dir_my_module/BUILD.gn
+ pw_test_group("tests") {
+ tests = [
+ ...
+ ":my_fuzzer_test",
+ ]
+ }
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ LibFuzzer-style fuzzers are not currently supported by Pigweed when using
+ CMake.
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ Fuzzer unit tests are not generated for Pigweed's Bazel build.
+
+------------------------
+Step 4: Build the fuzzer
+------------------------
+LibFuzzer-style fuzzers require the compiler to add instrumentation and
+runtimes when building.
+
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ Select a sanitizer runtime. See LLVM for `valid options`_.
+
+ .. code-block:: sh
+
+ $ gn gen out --args='pw_toolchain_SANITIZERS=["address"]'
+
+ Some toolchains may set a default for fuzzers if none is specified. For
+ example, `//targets/host:host_clang_fuzz` defaults to "address".
+
+ Build the fuzzers using ``ninja`` directly.
+
+ .. code-block:: sh
+
+ $ ninja -C out fuzzers
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ LibFuzzer-style fuzzers are not currently supported by Pigweed when using
+ CMake.
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ Specify the `AddressSanitizer`_
+ :ref:`fuzzing toolchain<module-pw_fuzzer-guides-using_libfuzzer-toolchain>`
+ via a ``--config`` when building fuzzers.
+
+ .. code-block:: sh
+
+ $ bazel build //my_module:my_fuzzer --config=asan-libfuzzer
+
+----------------------------------
+Step 5: Running the fuzzer locally
+----------------------------------
+.. tab-set::
+
+ .. tab-item:: GN
+ :sync: gn
+
+ The fuzzer binary will be in a subdirectory related to the toolchain.
+ Additional `libFuzzer options`_ and `corpus`_ arguments can be passed on
+ the command line. For example:
+
+ .. code-block:: sh
+
+ $ out/host_clang_fuzz/obj/my_module/bin/my_fuzzer -seed=1 path/to/corpus
+
+ Additional `sanitizer flags`_ may be passed uisng environment variables.
+
+ .. tab-item:: CMake
+ :sync: cmake
+
+ LibFuzzer-style fuzzers are not currently supported by Pigweed when using
+ CMake.
+
+ .. tab-item:: Bazel
+ :sync: bazel
+
+ Specify the `AddressSanitizer`_
+ :ref:`fuzzing toolchain<module-pw_fuzzer-guides-using_libfuzzer-toolchain>`
+ via a ``--config`` when building and running fuzzers. Additional
+ `libFuzzer options`_ and `corpus`_ arguments can be passed on the command
+ line. For example:
+
+ .. code-block:: sh
+
+ $ bazel run //my_module:my_fuzzer --config=asan-libfuzzer -- \
+ -seed=1 path/to/corpus
+
+Running the fuzzer should produce output similar to the following:
+
+.. code-block::
+
+ INFO: Seed: 305325345
+ INFO: Loaded 1 modules (46 inline 8-bit counters): 46 [0x38dfc0, 0x38dfee),
+ INFO: Loaded 1 PC tables (46 PCs): 46 [0x23aaf0,0x23add0),
+ INFO: 0 files found in corpus
+ INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
+ INFO: A corpus is not provided, starting from an empty corpus
+ #2 INITED cov: 2 ft: 3 corp: 1/1b exec/s: 0 rss: 27Mb
+ #4 NEW cov: 3 ft: 4 corp: 2/3b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 2 ShuffleBytes-InsertByte-
+ #11 NEW cov: 7 ft: 8 corp: 3/7b lim: 4 exec/s: 0 rss: 27Mb L: 4/4 MS: 2 EraseBytes-CrossOver-
+ #27 REDUCE cov: 7 ft: 8 corp: 3/6b lim: 4 exec/s: 0 rss: 27Mb L: 3/3 MS: 1 EraseBytes-
+ #29 REDUCE cov: 7 ft: 8 corp: 3/5b lim: 4 exec/s: 0 rss: 27Mb L: 2/2 MS: 2 ChangeBit-EraseBytes-
+ #445 REDUCE cov: 9 ft: 10 corp: 4/13b lim: 8 exec/s: 0 rss: 27Mb L: 8/8 MS: 1 InsertRepeatedBytes-
+ ...
+
+.. TODO: b/282560789 - Add guides/improve_fuzzers.rst
+.. TODO: b/281139237 - Add guides/continuous_fuzzing.rst
+.. ----------
+.. Next steps
+.. ----------
+.. Once you have created a fuzzer, you may want to:
+
+.. * `Run it continuously on a fuzzing infrastructure <continuous_fuzzing>`_.
+.. * `Measure its code coverage and improve it <improve_a_fuzzer>`_.
+
+
+.. _AddressSanitizer: https://github.com/google/sanitizers/wiki/AddressSanitizer
+.. _continuous_fuzzing: :ref:`module-pw_fuzzer-guides-continuous_fuzzing`
+.. _corpus: https://llvm.org/docs/LibFuzzer.html#corpus
+.. _fuzz target function: https://llvm.org/docs/LibFuzzer.html#fuzz-target
+.. _FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION: https://llvm.org/docs/LibFuzzer.html#fuzzer-friendly-build-mode
+.. _FuzzedDataProvider: https://github.com/llvm/llvm-project/blob/HEAD/compiler-rt/include/fuzzer/FuzzedDataProvider.h
+.. _improve_fuzzers: :ref:`module-pw_fuzzer-guides-improve_fuzzers
+.. _libFuzzer: https://llvm.org/docs/LibFuzzer.html
+.. _libFuzzer options: https://llvm.org/docs/LibFuzzer.html#options
+.. _sanitizer flags: https://github.com/google/sanitizers/wiki/SanitizerCommonFlags
+.. _split a fuzzing input: https://github.com/google/fuzzing/blob/HEAD/docs/split-inputs.md
+.. _startup initialization: https://llvm.org/docs/LibFuzzer.html#startup-initialization
+.. _structure aware fuzzing: https://github.com/google/fuzzing/blob/HEAD/docs/structure-aware-fuzzing.md
+.. _valid options: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
+
diff --git a/pw_fuzzer/guides/reproducing_oss_fuzz_bugs.rst b/pw_fuzzer/guides/reproducing_oss_fuzz_bugs.rst
new file mode 100644
index 000000000..bec831cb3
--- /dev/null
+++ b/pw_fuzzer/guides/reproducing_oss_fuzz_bugs.rst
@@ -0,0 +1,116 @@
+.. _module-pw_fuzzer-guides-reproducing_oss_fuzz_bugs:
+
+=============================================
+pw_fuzzer: Using OSS-Fuzz
+=============================================
+.. pigweed-module-subpage::
+ :name: pw_fuzzer
+ :tagline: Better C++ code through easier fuzzing
+
+.. TODO: b/281139237 - Update with better instructions for downstream projects.
+
+Core Pigweed is integrated with `OSS-Fuzz`_, a continuous fuzzing infrastructure
+for open source software. Fuzzers listed in in ``pw_test_groups`` will
+automatically start being run within a day or so of appearing in the git
+repository.
+
+-------------------------
+Reproducing Bugs Directly
+-------------------------
+
+Bugs produced by OSS-Fuzz can be found in its `Monorail instance`_. These bugs
+include:
+
+* A detailed report, including a symbolized backtrace.
+* A revision range indicating when the bug has been detected.
+* A minimized testcase, which is a fuzzer input that can be used to reproduce
+ the bug.
+
+To reproduce a bug:
+
+#. Build the fuzzers in a local source checkout using one of the
+ :ref:`module-pw_fuzzer-guides`.
+#. Download the minimized testcase from the OSS-Fuzz bug.
+#. Run the fuzzer with the testcase as an argument.
+
+For example, if the testcase is saved as ``~/Downloads/testcase``
+and the fuzzer is the same as in the examples above, you could run:
+
+.. code-block::
+
+ $ ./out/host/obj/pw_fuzzer/toy_fuzzer ~/Downloads/testcase
+
+As noted in OSS-Fuzz's documentation on `timeouts and OOMs`_, you may want to
+add a `-timeout=25` or `-rss_limit_mb=2560` argument to reproduce timeouts or
+OOMs, respectively.
+
+--------------------------------
+Using a OSS-Fuzz Docker Instance
+--------------------------------
+
+If Pigweed fails to build for OSS-Fuzz, or if a fuzzer only triggers a bug in
+OSS-Fuzz and not when run directly, you may want to recreate the OSS-Fuzz
+environment locally using Docker. You can do so using OSS-Fuzz's documentation
+on `reproducing`_ issues.
+
+In particular, you can recreate the OSS-Fuzz environment using:
+
+.. code-block::
+
+ $ python infra/helper.py pull_images
+ $ python infra/helper.py build_image pigweed
+ $ python infra/helper.py build_fuzzers --sanitizer <address/undefined> pigweed
+
+Using a Local Source Checkout
+=============================
+
+When addressing build failures or issues related to specific fuzzers, it is
+very useful to have an OSS-Fuzz instance use a local source checkout with edits
+rather than pull from a public repo. Unfortunately, the normal workflow for
+`using a local source checkout`_ **does not work** for Pigweed. Pigweed provides
+an embedded development environment along with source code for individual
+modules, and this environment includes checks that conflict with the way
+OSS-Fuzz tries to remap and change ownership of the source code.
+
+To work around this, a helper script is provided as part of the ``pigweed``
+project on OSS-Fuzz that wraps the usual ``infra/helper.py``. For commands that
+take a local source path, the wrapper instead provides a ``--local`` flag. This
+flag will use the ``PW_ROOT`` environment variable to find the source checkout,
+and attempt to mount it in the correct location and set specific environment
+variables in order to present a working development environment to the OSS-Fuzz
+instance. Also, the ``pigweed`` project is implied:
+
+.. code-block::
+
+ $ python project/pigweed/helper.py build_fuzzers --sanitizer <sanitizer> --local
+
+The ``sanitizer`` value is one of the usual values based to Clang via
+``-fsanitize=...``, e.g. "address" or "undefined".
+
+After building with a local source checkout, you can verify an issue previously
+found by a fuzzer is fixed:
+
+.. code-block::
+
+ $ python project/pigweed/helper.py reproduce <fuzzer> ~/Downloads/testcase
+
+For libFuzzer-based fuzzers, ``fuzzer`` will be of the form
+``{module_name}_{fuzzer_name}``, e.g. ``pw_protobuf_encoder_fuzzer``.
+
+For FuzzTest-based fuzzers, ``fuzzer`` will additionally include the test case
+and be of the form ``{module_name}_{fuzzer_name}@{test_case}``, e.g.
+``pw_hdlc_decoder_test@Decoder.ProcessNeverCrashes``.
+
+The helper script attempts to restore proper ownership of the source checkout to
+the current user on completion. This can also be triggered manually using:
+
+.. code-block::
+
+ $ python project/pigweed/helper.py reset_local
+
+
+.. _Monorail instance: https://bugs.chromium.org/p/oss-fuzz/issues/list?q=pigweed
+.. _OSS-Fuzz: https://github.com/google/oss-fuzz
+.. _reproducing: https://google.github.io/oss-fuzz/advanced-topics/reproducing/
+.. _timeouts and OOMs: https://google.github.io/oss-fuzz/faq/#how-do-you-handle-timeouts-and-ooms
+.. _using a local source checkout: https://google.github.io/oss-fuzz/advanced-topics/reproducing/#reproduce-using-local-source-checkout
diff --git a/pw_polyfill/iterator_public_overrides/iterator b/pw_fuzzer/private/pw_fuzzer/internal/fuzztest.h
index 80394d868..adee09fd7 100644
--- a/pw_polyfill/iterator_public_overrides/iterator
+++ b/pw_fuzzer/private/pw_fuzzer/internal/fuzztest.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -13,10 +13,13 @@
// the License.
#pragma once
-static_assert(__cplusplus < 201703L,
- "The <iterator> polyfill header is only intended for C++14. "
- "It cannot be used when building with C++17 or newer.");
+/// @file fuzztest.h
+/// Replaceable interface to FuzzTest
+///
+/// @rst
+/// When FuzzTest is enabled, this header includes the FuzzTest interface. It
+/// exists only so that it can be replaced by stubs when FuzzTest is *not*
+/// enabled.
+/// @endrst
-#include_next <iterator>
-
-#include "pw_polyfill/standard_library/iterator.h"
+#include "fuzztest/fuzztest.h"
diff --git a/pw_fuzzer/private_overrides/pw_fuzzer/internal/fuzztest.h b/pw_fuzzer/private_overrides/pw_fuzzer/internal/fuzztest.h
new file mode 100644
index 000000000..de3b7f382
--- /dev/null
+++ b/pw_fuzzer/private_overrides/pw_fuzzer/internal/fuzztest.h
@@ -0,0 +1,399 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+/// @file
+/// Stubs for the Pigweed-compatible subset of the FuzzTest interface
+///
+/// @rst
+/// This header provides stubs for the portion of the FuzzTest interface that
+/// only depends on permitted C++ standard library `headers`_, including
+/// `macros`_ and `domains`_.
+///
+/// This header is included when FuzzTest is disabled, e.g. for GN, when
+/// ``dir_pw_third_party_fuzztest`` or ``pw_toolchain_FUZZING_ENABLED`` are not
+/// set. Otherwise, ``//pw_fuzzer/public/pw_fuzzer/internal/fuzztest.h`` is used
+/// instead.
+///
+/// .. _domains:
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md
+/// .. _headers: https://pigweed.dev/docs/style_guide.html#permitted-headers
+/// .. _macros:
+/// https://github.com/google/fuzztest/blob/main/doc/fuzz-test-macro.md
+/// @endrst
+
+#include <array>
+#include <cmath>
+#include <initializer_list>
+#include <limits>
+#include <optional>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include <variant>
+
+#define FUZZ_TEST(test_suite_name, test_name) \
+ TEST(test_suite_name, DISABLED_##test_name) {} \
+ auto _pw_fuzzer_##test_suite_name##_##test_name##_FUZZTEST_NOT_PRESENT = \
+ fuzztest::internal::TypeCheckFuzzTest(test_name).IgnoreFunction()
+
+#define FUZZ_TEST_F(test_fixture, test_name) \
+ TEST_F(test_fixture, DISABLED_##test_name) {} \
+ auto _pw_fuzzer_##test_fixture##_##test_name##_FUZZTEST_NOT_PRESENT = \
+ fuzztest::internal::TypeCheckFuzzTest(test_name).IgnoreFunction()
+
+namespace fuzztest {
+
+/// Stub for a FuzzTest domain that produces values.
+///
+/// In FuzzTest, domains are used to provide values of specific types when
+/// fuzzing. However, FuzzTest is only optionally supported on host with Clang.
+/// For other build configurations, this struct provides a FuzzTest-compatible
+/// stub that can be used to perform limited type-checking at build time.
+///
+/// Fuzzer authors must not invoke this type directly. Instead, use the factory
+/// methods for domains such as `Arbitrary`, `VectorOf`, `Map`, etc.
+template <typename T>
+struct Domain {
+ using value_type = T;
+};
+
+namespace internal {
+
+/// Stub for a FuzzTest domain that produces containers of values.
+///
+/// This struct is an extension of `Domain` that add stubs for the methods that
+/// control container size.
+template <typename T>
+struct ContainerDomain : public Domain<T> {
+ template <typename U, typename = std::enable_if_t<std::is_integral_v<U>>>
+ ContainerDomain<T>& WithSize(U) {
+ return *this;
+ }
+
+ template <typename U, typename = std::enable_if_t<std::is_integral_v<U>>>
+ ContainerDomain<T>& WithMinSize(U) {
+ return *this;
+ }
+
+ template <typename U, typename = std::enable_if_t<std::is_integral_v<U>>>
+ ContainerDomain<T>& WithMaxSize(U) {
+ return *this;
+ }
+};
+
+/// Stub for a FuzzTest domain that produces optional values.
+///
+/// This struct is an extension of `Domain` that add stubs for the methods that
+/// control nullability.
+template <typename T>
+struct OptionalDomain : public Domain<T> {
+ OptionalDomain<T>& SetAlwaysNull() { return *this; }
+ OptionalDomain<T>& SetWithoutNull() { return *this; }
+};
+
+/// Register a FuzzTest stub.
+///
+/// FuzzTest is only optionally supported on host with Clang. For other build
+/// configurations, this struct provides a FuzzTest-compatible stub of a test
+/// registration that only performs limited type-checking at build time.
+///
+/// Fuzzer authors must not invoke this type directly. Instead, use the
+/// `FUZZ_TEST` and/or `FUZZ_TEST_F` macros.
+template <typename TargetFunction>
+struct TypeCheckFuzzTest {
+ TypeCheckFuzzTest(TargetFunction) {}
+
+ TypeCheckFuzzTest<TargetFunction>& IgnoreFunction() { return *this; }
+
+ template <int&... ExplicitArgumentBarrier,
+ typename... Domains,
+ typename T = std::decay_t<
+ std::invoke_result_t<TargetFunction,
+ const typename Domains::value_type&...>>>
+ TypeCheckFuzzTest<TargetFunction>& WithDomains(Domains...) {
+ return *this;
+ }
+
+ template <int&... ExplicitArgumentBarrier,
+ typename... Seeds,
+ typename T = std::decay_t<
+ std::invoke_result_t<TargetFunction, const Seeds&...>>>
+ TypeCheckFuzzTest<TargetFunction>& WithSeeds(Seeds...) {
+ return *this;
+ }
+};
+
+} // namespace internal
+
+// The remaining functions match those defined by fuzztest/fuzztest.h.
+//
+// This namespace is here only as a way to disable ADL (argument-dependent
+// lookup). Names should be used from the fuzztest:: namespace.
+namespace internal_no_adl {
+
+////////////////////////////////////////////////////////////////
+// Arbitrary domains
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#arbitrary-domains
+
+template <typename T>
+auto Arbitrary() {
+ return Domain<T>{};
+}
+
+////////////////////////////////////////////////////////////////
+// Other miscellaneous domains
+// These typically appear later in docs and tests. They are placed early in this
+// file to allow other domains to be defined using them.
+
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#oneof
+template <int&... ExplicitArgumentBarrier, typename T, typename... Domains>
+auto OneOf(Domain<T>, Domains...) {
+ return Domain<T>{};
+}
+
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#oneof
+template <typename T>
+auto Just(T) {
+ return Domain<T>{};
+}
+
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#map
+template <int&... ExplicitArgumentBarrier, typename Mapper, typename... Inner>
+auto Map(Mapper, Inner...) {
+ return Domain<std::decay_t<
+ std::invoke_result_t<Mapper, typename Inner::value_type&...>>>{};
+}
+
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#flatmap
+template <typename FlatMapper, typename... Inner>
+using FlatMapOutputDomain = std::decay_t<
+ std::invoke_result_t<FlatMapper, typename Inner::value_type&...>>;
+template <int&... ExplicitArgumentBarrier,
+ typename FlatMapper,
+ typename... Inner>
+auto FlatMap(FlatMapper, Inner...) {
+ return Domain<
+ typename FlatMapOutputDomain<FlatMapper, Inner...>::value_type>{};
+}
+
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#filter
+template <int&... ExplicitArgumentBarrier, typename T, typename Pred>
+auto Filter(Pred, Domain<T>) {
+ return Domain<T>{};
+}
+
+////////////////////////////////////////////////////////////////
+// Numerical domains
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+
+template <typename T>
+auto InRange(T min, T max) {
+ return Filter([min, max](T t) { return min <= t && t <= max; },
+ Arbitrary<T>());
+}
+
+template <typename T>
+auto NonZero() {
+ return Filter([](T t) { return t != 0; }, Arbitrary<T>());
+}
+
+template <typename T>
+auto Positive() {
+ if constexpr (std::is_floating_point_v<T>) {
+ return InRange<T>(std::numeric_limits<T>::denorm_min(),
+ std::numeric_limits<T>::max());
+ } else {
+ return InRange<T>(T{1}, std::numeric_limits<T>::max());
+ }
+}
+
+template <typename T>
+auto NonNegative() {
+ return InRange<T>(T{}, std::numeric_limits<T>::max());
+}
+
+template <typename T>
+auto Negative() {
+ static_assert(!std::is_unsigned_v<T>,
+ "Negative<T>() can only be used with with signed T-s! "
+ "For char, consider using signed char.");
+ if constexpr (std::is_floating_point_v<T>) {
+ return InRange<T>(std::numeric_limits<T>::lowest(),
+ -std::numeric_limits<T>::denorm_min());
+ } else {
+ return InRange<T>(std::numeric_limits<T>::min(), T{-1});
+ }
+}
+
+template <typename T>
+auto NonPositive() {
+ static_assert(!std::is_unsigned_v<T>,
+ "NonPositive<T>() can only be used with with signed T-s! "
+ "For char, consider using signed char.");
+ return InRange<T>(std::numeric_limits<T>::lowest(), T{});
+}
+
+template <typename T>
+auto Finite() {
+ static_assert(std::is_floating_point_v<T>,
+ "Finite<T>() can only be used with floating point types!");
+ return Filter([](T f) { return std::isfinite(f); }, Arbitrary<T>());
+}
+
+////////////////////////////////////////////////////////////////
+// Character domains
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+
+inline auto NonZeroChar() { return Positive<char>(); }
+inline auto NumericChar() { return InRange<char>('0', '9'); }
+inline auto LowerChar() { return InRange<char>('a', 'z'); }
+inline auto UpperChar() { return InRange<char>('A', 'Z'); }
+inline auto AlphaChar() { return OneOf(LowerChar(), UpperChar()); }
+inline auto AlphaNumericChar() { return OneOf(AlphaChar(), NumericChar()); }
+inline auto AsciiChar() { return InRange<char>(0, 127); }
+inline auto PrintableAsciiChar() { return InRange<char>(32, 126); }
+
+////////////////////////////////////////////////////////////////
+// Regular expression domains
+
+// TODO: b/285775246 - Add support for `fuzztest::InRegexp`.
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#inregexp-domains
+// inline auto InRegexp(std::string_view) {
+// return Domain<std::string_view>{};
+// }
+
+////////////////////////////////////////////////////////////////
+// Enumerated domains
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#elementof-domains
+
+template <typename T>
+auto ElementOf(std::initializer_list<T>) {
+ return Domain<T>{};
+}
+
+template <typename T>
+auto BitFlagCombinationOf(std::initializer_list<T>) {
+ return Domain<T>{};
+}
+
+////////////////////////////////////////////////////////////////
+// Container domains
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#container-combinators
+
+template <typename T, int&... ExplicitArgumentBarrier, typename U>
+auto ContainerOf(Domain<U>) {
+ return internal::ContainerDomain<T>{};
+}
+
+template <template <typename, typename...> class T,
+ int&... ExplicitArgumentBarrier,
+ typename U>
+auto ContainerOf(Domain<U>) {
+ return internal::ContainerDomain<T<U>>{};
+}
+
+template <typename T, int&... ExplicitArgumentBarrier, typename U>
+auto UniqueElementsContainerOf(Domain<U>) {
+ return internal::ContainerDomain<T>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto NonEmpty(internal::ContainerDomain<T> inner) {
+ return inner.WithMinSize(1);
+}
+
+////////////////////////////////////////////////////////////////
+// Aggregate domains
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#container-combinators
+
+template <int&... ExplicitArgumentBarrier, typename T, typename... Domains>
+auto ArrayOf(Domain<T>, Domains... others) {
+ return Domain<std::array<T, 1 + sizeof...(others)>>{};
+}
+
+template <int N, int&... ExplicitArgumentBarrier, typename T>
+auto ArrayOf(const Domain<T>&) {
+ return Domain<std::array<T, N>>{};
+}
+
+template <typename T, int&... ExplicitArgumentBarrier, typename... Inner>
+auto StructOf(Inner...) {
+ return Domain<T>{};
+}
+
+template <typename T, int&... ExplicitArgumentBarrier>
+auto ConstructorOf() {
+ return Domain<T>{};
+}
+
+template <typename T,
+ int&... ExplicitArgumentBarrier,
+ typename U,
+ typename... Inner>
+auto ConstructorOf(Domain<U>, Inner... inner) {
+ return ConstructorOf<T>(inner...);
+}
+
+template <int&... ExplicitArgumentBarrier, typename T1, typename T2>
+auto PairOf(Domain<T1>, Domain<T2>) {
+ return Domain<std::pair<T1, T2>>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename... Inner>
+auto TupleOf(Inner...) {
+ return Domain<std::tuple<typename Inner::value_type...>>{};
+}
+
+template <typename T, int&... ExplicitArgumentBarrier, typename... Inner>
+auto VariantOf(Inner...) {
+ return Domain<T>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename... Inner>
+auto VariantOf(Inner...) {
+ return Domain<std::variant<typename Inner::value_type...>>{};
+}
+
+template <template <typename> class Optional,
+ int&... ExplicitArgumentBarrier,
+ typename T>
+auto OptionalOf(Domain<T>) {
+ return internal::OptionalDomain<Optional<T>>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto OptionalOf(Domain<T> inner) {
+ return OptionalOf<std::optional>(inner);
+}
+
+template <typename T>
+auto NullOpt() {
+ return internal::OptionalDomain<std::optional<T>>{}.SetAlwaysNull();
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto NonNull(internal::OptionalDomain<T> inner) {
+ return inner.SetWithoutNull();
+}
+
+} // namespace internal_no_adl
+
+// Inject the names from internal_no_adl into fuzztest, without allowing for
+// ADL. Note that an `inline` namespace would not have this effect (ie it would
+// still allow ADL to trigger).
+using namespace internal_no_adl; // NOLINT
+
+} // namespace fuzztest
diff --git a/pw_fuzzer/public/pw_fuzzer/fuzztest.h b/pw_fuzzer/public/pw_fuzzer/fuzztest.h
new file mode 100644
index 000000000..9e72ec274
--- /dev/null
+++ b/pw_fuzzer/public/pw_fuzzer/fuzztest.h
@@ -0,0 +1,1017 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+/// @file fuzztest.h
+/// Pigweed interface to FuzzTest
+///
+/// @rst
+/// This header exposes the portion of the FuzzTest interface that only depends
+/// on permitted C++ standard library `headers`_, including `macros`_ and
+/// `domains`_.
+///
+/// It also extends the interface to provide domains for common Pigweed types,
+/// such as those from the following modules:
+///
+/// * :ref:`module-pw_containers`
+/// * :ref:`module-pw_result`
+/// * :ref:`module-pw_status`
+/// * :ref:`module-pw_string`
+///
+/// .. _domains:
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md
+/// .. _headers: https://pigweed.dev/docs/style_guide.html#permitted-headers
+/// .. _macros:
+/// https://github.com/google/fuzztest/blob/main/doc/fuzz-test-macro.md
+/// @endrst
+
+#include "pw_containers/flat_map.h"
+#include "pw_containers/inline_deque.h"
+#include "pw_containers/inline_queue.h"
+#include "pw_containers/intrusive_list.h"
+#include "pw_containers/vector.h"
+#include "pw_fuzzer/internal/fuzztest.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_string/string.h"
+
+namespace pw::fuzzer {
+
+template <typename T>
+using Domain = fuzztest::Domain<T>;
+
+////////////////////////////////////////////////////////////////
+// Arbitrary domains
+// Additional specializations are provided with the Pigweed domains.
+
+/// @struct ArbitraryImpl
+/// @fn Arbitrary
+///
+/// Produces values for fuzz target function parameters.
+///
+/// This defines a new template rather than using the `fuzztest` one directly in
+/// order to facilitate specializations for Pigweed types.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#arbitrary-domains
+template <typename T, typename = void>
+struct ArbitraryImpl {
+ auto operator()() { return fuzztest::Arbitrary<T>(); }
+};
+template <typename T>
+auto Arbitrary() {
+ return ArbitraryImpl<T>()();
+}
+
+////////////////////////////////////////////////////////////////
+// Numerical domains
+
+/// Produces values from a closed interval.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::InRange;
+
+/// Like Arbitrary<T>(), but does not produce the zero value.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::NonZero;
+
+/// Produces numbers greater than zero.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::Positive;
+
+/// Produces the zero value and numbers greater than zero.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::NonNegative;
+
+/// Produces numbers less than zero.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::Negative;
+
+/// Produces the zero value and numbers less than zero.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::NonPositive;
+
+/// Produces floating points numbers that are neither infinity nor NaN.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#numerical-domains
+using fuzztest::Finite;
+
+////////////////////////////////////////////////////////////////
+// Character domains
+
+/// Produces any char except '\0'.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::NonZeroChar;
+
+/// Alias for `InRange('0', '9')`.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::NumericChar;
+
+/// Alias for `InRange('a', 'z')`.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::LowerChar;
+
+/// Alias for `InRange('A', 'Z')`.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::UpperChar;
+
+/// Alias for `OneOf(LowerChar(), UpperChar())`.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::AlphaChar;
+
+/// Alias for `OneOf(AlphaChar(), NumericChar())`.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::AlphaNumericChar;
+
+/// Produces printable characters (`InRange<char>(32, 126)`).
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::PrintableAsciiChar;
+
+/// Produces ASCII characters (`InRange<char>(0, 127)`).
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#character-domains
+using fuzztest::AsciiChar;
+
+////////////////////////////////////////////////////////////////
+// Regular expression domains
+
+// TODO: b/285775246 - Add support for `fuzztest::InRegexp`.
+// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#inregexp-domains
+// using fuzztest::InRegexp;
+
+////////////////////////////////////////////////////////////////
+// Enumerated domains
+
+/// Produces values from an enumerated set.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#elementof-domains
+using fuzztest::ElementOf;
+
+/// Produces combinations of binary flags from a provided list.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#bitflagcombinationof-domains
+using fuzztest::BitFlagCombinationOf;
+
+////////////////////////////////////////////////////////////////
+// Container domains
+
+/// @struct ContainerOfImpl
+/// @fn ContainerOf
+///
+/// Produces containers of elements provided by inner domains.
+///
+/// This defines a new template rather than using the `fuzztest` one directly in
+/// order to specify the static container capacity as part of the container
+/// type.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#container-combinators
+template <typename Container, typename = void>
+struct ContainerOfImpl {
+ template <int&... ExplicitArgumentBarrier, typename Inner>
+ auto operator()(Inner inner) {
+ return fuzztest::ContainerOf<Container>(std::move(inner))
+ .WithMaxSize(Container{}.max_size());
+ }
+};
+template <typename Container, int&... ExplicitArgumentBarrier, typename Inner>
+auto ContainerOf(Inner inner) {
+ return ContainerOfImpl<Container>()(std::move(inner));
+}
+
+namespace internal {
+
+template <typename T, typename = void>
+struct IsContainer : std::false_type {};
+
+template <typename T>
+struct IsContainer<T, std::void_t<decltype(T().begin(), T().end())>>
+ : std::true_type {};
+
+} // namespace internal
+
+/// Produces containers of at least one element provided by inner domains.
+///
+/// The container type is given by a template parameter.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#container-combinators
+using fuzztest::NonEmpty;
+
+/// @struct UniqueElementsContainerOfImpl
+/// @fn UniqueElementsContainerOf
+///
+/// Produces containers of unique elements provided by inner domains.
+///
+/// This defines a new template rather than using the `fuzztest` one directly in
+/// order to specify the static container capacity as part of the container
+/// type.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#container-combinators
+template <typename Container, typename = void>
+struct UniqueElementsContainerOfImpl {
+ template <int&... ExplicitArgumentBarrier, typename Inner>
+ auto operator()(Inner inner) {
+ return fuzztest::UniqueElementsContainerOf<Container>(std::move(inner))
+ .WithMaxSize(Container{}.max_size());
+ }
+};
+template <typename Container, int&... ExplicitArgumentBarrier, typename Inner>
+auto UniqueElementsContainerOf(Inner inner) {
+ return UniqueElementsContainerOfImpl<Container>()(std::move(inner));
+}
+
+////////////////////////////////////////////////////////////////
+// Aggregate domains
+
+/// Produces std::array<T>s of elements provided by inner domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#container-combinators
+using fuzztest::ArrayOf;
+
+/// Specializes @cpp_class{pw::fuzzer::ContainerOfImpl} for arrays, which do not
+/// need a maximum size applied.
+///
+/// @param[in] inner Domain the produces values of type `T`.
+///
+/// @retval Domain that produces `std::array<T, kArraySize>`s.
+template <typename T, size_t N>
+struct ContainerOfImpl<std::array<T, N>> {
+ template <int&... ExplicitArgumentBarrier, typename Inner>
+ auto operator()(Inner inner) {
+ return fuzztest::ContainerOf<std::array<T, N>>(std::move(inner));
+ }
+};
+
+/// Produces user-defined structs.
+///
+/// The struct type is given by a template parameter.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#structof
+using fuzztest::StructOf;
+
+/// Produces user-defined objects.
+///
+/// The class type is given by a template parameter.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#constructorof
+using fuzztest::ConstructorOf;
+
+/// Produces `std::pair<T1, T2>`s from provided inner domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#pairof
+using fuzztest::PairOf;
+
+/// Produces `std::tuple<T...>`s from provided inner domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#tupleof
+using fuzztest::TupleOf;
+
+/// Produces `std::variant<T...>` from provided inner domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#variantof
+using fuzztest::VariantOf;
+
+/// Produces `std::optional<T>` from provided inner domain.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#optionalof
+using fuzztest::OptionalOf;
+
+/// Produces `std::optional<T>` from provided inner domain that is null.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#optionalof
+using fuzztest::NullOpt;
+
+/// Produces `std::optional<T>` from provided inner domain that is not null.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#optionalof
+using fuzztest::NonNull;
+
+////////////////////////////////////////////////////////////////
+// Other miscellaneous domains
+
+/// Produces values by choosing between provided inner domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#oneof
+using fuzztest::OneOf;
+
+/// Produces values equal to the provided value.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#oneof
+using fuzztest::Just;
+
+/// Produces values by applying a function to values from provided domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#map
+using fuzztest::Map;
+
+/// Creates a domain by applying a function to values from provided domains.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#flatmap
+using fuzztest::FlatMap;
+
+/// Produces values from a provided domain that will cause a provided predicate
+/// to return true.
+///
+/// See
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md#filter
+using fuzztest::Filter;
+
+////////////////////////////////////////////////////////////////
+// pw_status-related types
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::Status}.
+template <>
+struct ArbitraryImpl<Status> {
+ auto operator()() {
+ return ConstructorOf<Status>(
+ Map([](int code) { return static_cast<pw_Status>(code); },
+ InRange<int>(PW_STATUS_OK, PW_STATUS_LAST)));
+ }
+};
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::StatusWithSize}.
+template <>
+struct ArbitraryImpl<StatusWithSize> {
+ auto operator()() {
+ return ConstructorOf<StatusWithSize>(Arbitrary<Status>(),
+ Arbitrary<size_t>());
+ }
+};
+
+/// Like @cpp_func{pw::fuzzer::Arbitrary<Status>}, except that
+/// @cpp_func{pw::OkStatus} is filtered out.
+inline auto NonOkStatus() {
+ return ConstructorOf<pw::Status>(
+ Map([](int code) { return static_cast<pw_Status>(code); },
+ InRange<int>(PW_STATUS_CANCELLED, PW_STATUS_UNAUTHENTICATED)));
+}
+
+////////////////////////////////////////////////////////////////
+// pw_result-related types
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::Result}s.
+///
+/// The value produced may either be value produced by the given domain, or a
+/// @cpp_class{pw::Status} indicating an error.
+///
+/// Alternatively, you can use `Arbitrary<Result<T>>`.
+///
+/// @param[in] inner Domain that produces values of type `T`.
+///
+/// @retval Domain that produces `Result<T>`s.
+template <int&... ExplicitArgumentBarrier, typename Inner>
+auto ResultOf(Inner inner) {
+ return Map(
+ [](std::optional<typename Inner::value_type> value, Status status) {
+ if (value) {
+ return Result<typename Inner::value_type>(*value);
+ }
+ return Result<typename Inner::value_type>(status);
+ },
+ OptionalOf(std::move(inner)),
+ NonOkStatus());
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::Result}.
+template <typename T>
+struct ArbitraryImpl<Result<T>> {
+ auto operator()() { return ResultOf(Arbitrary<T>()); }
+};
+
+////////////////////////////////////////////////////////////////
+// pw_containers-related types
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::Vector}s.
+///
+/// Use this in place of `fuzztest::VectorOf`. The vector's maximum size is set
+/// by the template parameter.
+///
+/// Alternatively, you can use `Arbitrary<Vector<T, kMaxSize>>`.
+///
+/// @param[in] inner Domain that produces values of type `T`.
+///
+/// @retval Domain that produces `Vector<T>`s.
+template <size_t kMaxSize, int&... ExplicitArgumentBarrier, typename Inner>
+auto VectorOf(Inner inner) {
+ return ContainerOf<Vector<typename Inner::value_type, kMaxSize>>(inner)
+ .WithMaxSize(kMaxSize);
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::Vector}.
+template <typename T, size_t kMaxSize>
+struct ArbitraryImpl<Vector<T, kMaxSize>> {
+ auto operator()() { return VectorOf<kMaxSize>(Arbitrary<T>()); }
+};
+
+/// Like `fuzztest::UniqueElementsVectorOf`, but uses a @cpp_class{pw::Vector}
+/// in place of a `std::vector`.
+///
+/// @param[in] inner Domain the produces values for the vector.
+///
+/// @retval Domain that produces `@cpp_class{pw::Vector}`s.
+template <size_t kMaxSize, int&... ExplicitArgumentBarrier, typename Inner>
+auto UniqueElementsVectorOf(Inner inner) {
+ return UniqueElementsContainerOf<
+ Vector<typename Inner::value_type, kMaxSize>>(std::move(inner));
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::containers::Pair}s.
+///
+/// Use this in place of `fuzztest::PairOf` when working with
+/// @cpp_class{pw::containers::FlatMap}s.
+///
+/// Alternatively, you can use `Arbitrary<pw::containers::Pair<K, V>>`.
+///
+/// @param[in] keys Domain that produces values of type `K`.
+/// @param[in] values Domain that produces values of type `V`.
+///
+/// @retval Domain that produces `containers::Pair<T>`s.
+template <int&... ExplicitArgumentBarrier,
+ typename KeyDomain,
+ typename ValueDomain>
+auto FlatMapPairOf(KeyDomain keys, ValueDomain values) {
+ return StructOf<containers::Pair<typename KeyDomain::value_type,
+ typename ValueDomain::value_type>>(
+ std::move(keys), std::move(values));
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::containers::Pair}.
+template <typename K, typename V>
+struct ArbitraryImpl<containers::Pair<K, V>> {
+ auto operator()() { return FlatMapPairOf(Arbitrary<K>(), Arbitrary<V>()); }
+};
+
+/// Transforms a domain that produces containers of keys and values into a
+/// domain that produces @cpp_class{pw::containers::FlatMap}s
+///
+/// This method can be used to apply additional constraints to the set of keys
+/// and/or values overall, e.g. by requiring all keys to be unique.
+///
+/// @param[in] keys Domain that produces containers of keys.
+/// @param[in] values Domain that produces containers of values.
+///
+/// @retval Domain that produces `containers::Pair<T>`s.
+template <size_t kArraySize,
+ int&... ExplicitArgumentBarrier,
+ typename KeyDomain,
+ typename ValueDomain>
+auto MapToFlatMap(KeyDomain keys, ValueDomain values) {
+ using ContainerK = typename KeyDomain::value_type;
+ using ContainerV = typename ValueDomain::value_type;
+ static_assert(internal::IsContainer<ContainerK>::value);
+ static_assert(internal::IsContainer<ContainerV>::value);
+ using K = typename ContainerK::value_type;
+ using V = typename ContainerV::value_type;
+ return Map(
+ [](const ContainerK& keys_c, const ContainerV& vals_c) {
+ auto key = keys_c.begin();
+ auto val = vals_c.begin();
+ std::array<containers::Pair<K, V>, kArraySize> pairs;
+ for (auto& item : pairs) {
+ PW_ASSERT(key != keys_c.end());
+ PW_ASSERT(val != vals_c.end());
+ item.first = *key++;
+ item.second = *val++;
+ }
+ return pairs;
+ },
+ std::move(keys),
+ std::move(values));
+}
+
+/// Returns a FuzzTest domain that produces
+/// @cpp_class{pw::containers::FlatMap}s.
+///
+/// Use this in place of `fuzztest::MapOf` and/or `fuzztest::UnorderedMapOf`.
+/// The map's size is set by the template parameter. The map is populated by
+/// pairs of values from the given `KeyDomain` and `ValueDomain`.
+///
+/// Alternatively, you can use `Arbitrary<FlatMap<K, V, kArraySize>>`.
+///
+/// Note that neither approach returns a domain that produces `FlatMap<K< V>`s.
+/// Such a domain is infeasible, since `FlatMap<K, V>`s are not movable or
+/// copyable. Instead, these functions return domains that produce arrays of
+/// `Pair<K, V>`s that can be implicitly converted to `FlatMap<K, V>`s.
+///
+/// @param[in] keys Domain that produces map keys.
+/// @param[in] values Domain that produces map values.
+///
+/// @retval Domain that produces `std::array<T, kArraySize>`s.
+template <size_t kArraySize,
+ int&... ExplicitArgumentBarrier,
+ typename KeyDomain,
+ typename ValueDomain>
+auto FlatMapOf(KeyDomain keys, ValueDomain values) {
+ return ArrayOf<kArraySize>(FlatMapPairOf(std::move(keys), std::move(values)));
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::containers::FlatMap}.
+template <typename K, typename V, size_t kArraySize>
+struct ArbitraryImpl<containers::FlatMap<K, V, kArraySize>> {
+ auto operator()() {
+ return FlatMapOf<kArraySize>(Arbitrary<K>(), Arbitrary<V>());
+ }
+};
+
+/// Implementation of @cpp_func{pw::fuzzer::ContainerOf} for
+/// @cpp_class{pw::containers::FlatMap}.
+///
+/// Since flat maps have a static capacity, the returned domains do not produce
+/// FuzzTest containers, but aggregates. As a result, container methods such as
+/// `WithMaxSize` cannot be applied. Use @cpp_func{pw::fuzzer::MapToFlatMap}
+/// instead to apply constraints to the set of keys and values.
+///
+/// @param[in] inner Domain the produces @cpp_class{pw::containers::Pair}s.
+///
+/// @retval Domain that produces `@cpp_class{pw::containers::FlatMap}`s.
+template <typename K, typename V, size_t kArraySize>
+struct ContainerOfImpl<containers::FlatMap<K, V, kArraySize>> {
+ template <int&... ExplicitArgumentBarrier, typename Inner>
+ auto operator()(Inner inner) {
+ static_assert(
+ std::is_same_v<typename Inner::value_type, containers::Pair<K, V>>,
+ "The domain passed to `pw::fuzzer::ContainerOf<FlatMap<K, V>>` must "
+ "produce `pw::containers::Pair<K, V>`s. An example of a valid domain is"
+ "`pw::fuzzer::FlatMapPairOf(FooDomain<K>(), BarDomain<V>())`");
+ return ArrayOf<kArraySize>(std::move(inner));
+ }
+};
+
+/// Transforms a domain that produces containers into a domain that produces
+/// @cpp_class{pw::BasicInlineDeque}s.
+///
+/// The domains returned by @cpp_func{pw::fuzzer::BasicDequeOf} and
+/// `Arbitrary<BasicInlineDeque>` do not create FuzzTest containers. This method
+/// can be used to apply container methods such as `WithMinSize` or
+/// `UniqueElementsContainerOf` before building a deque from that container.
+///
+/// @param[in] inner Domain that produces containers.
+///
+/// @retval Domain that produces `@cpp_class{pw::BasicInlineDeque}`s.
+template <typename SizeType,
+ size_t kCapacity,
+ int&... ExplicitArgumentBarrier,
+ typename Inner>
+auto MapToBasicDeque(Inner inner) {
+ using Container = typename Inner::value_type;
+ static_assert(internal::IsContainer<Container>::value);
+ using T = typename Container::value_type;
+ return Map(
+ [](const Container& items) {
+ return BasicInlineDeque<T, SizeType, kCapacity>(items.begin(),
+ items.end());
+ },
+ std::move(inner));
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::BasicInlineDeque}s.
+///
+/// Use this or @cpp_func{pw::fuzzer::DequeOf} in place of `fuzztest::DequeOf`.
+/// The deque's maximum size is set by the template parameter.
+///
+/// Alternatively, you can use `Arbitrary<BasicInlineDeque<T, kCapacity>>`.
+///
+/// @param[in] inner Domain that produces values of type `T`.
+///
+/// @retval Domain that produces values of type
+/// `BasicInlineDeque<T, SizeType, kCapacity>`.
+template <typename SizeType,
+ size_t kCapacity,
+ int&... ExplicitArgumentBarrier,
+ typename Inner>
+auto BasicDequeOf(Inner inner) {
+ return MapToBasicDeque<SizeType, kCapacity>(
+ VectorOf<kCapacity>(std::move(inner)));
+}
+
+// BasicDequeFrom(VectorOf<kCapacity>(Arbitrary<int>()))
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::BasicInlineDeque}.
+template <typename T, typename SizeType, size_t kCapacity>
+struct ArbitraryImpl<BasicInlineDeque<T, SizeType, kCapacity>> {
+ auto operator()() {
+ return BasicDequeOf<SizeType, kCapacity>(Arbitrary<T>());
+ }
+};
+
+/// Implementation of @cpp_func{pw::fuzzer::ContainerOf} for
+/// @cpp_class{pw::containers::BasicInlineDeque}.
+///
+/// Since inline deques have a static capacity, the returned domains do not
+/// produce FuzzTest containers, but aggregates. As a result, container methods
+/// such as `WithMaxSize` cannot be applied. Instead, use
+/// @cpp_func{pw::fuzzer::MapToDeque} or @cpp_func{pw::fuzzer::MapToBasicDeque}
+/// to apply constraints to the set of keys and values.
+///
+/// @param[in] inner Domain the produces values of type `T`.
+///
+/// @retval Domain that produces `@cpp_class{pw::BasicInlineDeque}`s.
+template <typename T, typename SizeType, size_t kCapacity>
+struct ContainerOfImpl<BasicInlineDeque<T, SizeType, kCapacity>> {
+ template <int&... ExplicitArgumentBarrier, typename Inner>
+ auto operator()(Inner inner) {
+ return BasicDequeOf<SizeType, kCapacity>(std::move(inner));
+ }
+};
+
+/// Transforms a domain that produces containers into a domain that produces
+/// @cpp_class{pw::InlineDeque}s.
+///
+/// The domains returned by @cpp_func{pw::fuzzer::equeOf} and
+/// `Arbitrary<InlineDeque>` do not create FuzzTest containers. This method
+/// can be used to apply container methods such as `WithMinSize` or
+/// `UniqueElementsContainerOf` before building a deque from that container.
+///
+/// @param[in] inner Domain that produces containers.
+///
+/// @retval Domain that produces `@cpp_class{pw::InlineDeque}`s.
+template <size_t kCapacity, int&... ExplicitArgumentBarrier, typename Inner>
+auto MapToDeque(Inner inner) {
+ return MapToBasicDeque<uint16_t, kCapacity>(std::move(inner));
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::InlineDeque}s.
+///
+/// Use this or @cpp_func{pw::fuzzer::BasicDequeOf} in place of
+/// `fuzztest::DequeOf`. The deque's maximum size is set by the template
+/// parameter.
+///
+/// Alternatively, you can use `Arbitrary<InlineDeque<T, kCapacity>>`.
+///
+/// @param[in] inner Domain that produces values of type `T`.
+///
+/// @retval Domain that produces values of type `InlineDeque<T, kCapacity>`.
+template <size_t kCapacity, int&... ExplicitArgumentBarrier, typename Inner>
+auto DequeOf(Inner inner) {
+ return BasicDequeOf<uint16_t, kCapacity>(std::move(inner));
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::InlineDeque}.
+template <typename T, size_t kCapacity>
+struct ArbitraryImpl<InlineDeque<T, kCapacity>> {
+ auto operator()() { return DequeOf<kCapacity>(Arbitrary<T>()); }
+};
+
+/// Transforms a domain that produces containers into a domain that produces
+/// @cpp_class{pw::BasicInlineQueue}s.
+///
+/// The domains returned by @cpp_func{pw::fuzzer::BasicQueueOf} and
+/// `Arbitrary<BasicInlineQueue>` do not create FuzzTest containers. This method
+/// can be used to apply container methods such as `WithMinSize` or
+/// `UniqueElementsContainerOf` before building a queue from that container.
+///
+/// @param[in] inner Domain that produces containers.
+///
+/// @retval Domain that produces `@cpp_class{pw::BasicInlineQueue}`s.
+template <typename SizeType,
+ size_t kCapacity,
+ int&... ExplicitArgumentBarrier,
+ typename Inner>
+auto MapToBasicQueue(Inner inner) {
+ using Container = typename Inner::value_type;
+ static_assert(internal::IsContainer<Container>::value);
+ using T = typename Container::value_type;
+ return Map(
+ [](const Container& items) {
+ return BasicInlineQueue<T, SizeType, kCapacity>(items.begin(),
+ items.end());
+ },
+ std::move(inner));
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::BasicInlineQueue}s.
+///
+/// Use this, @cpp_func{pw::fuzzer::QueueOf}, or
+/// @cpp_func{pw::fuzzer::ScopedListOf} in place of `fuzztest::ListOf`. The
+/// queue's maximum size is set by the template parameter.
+///
+/// Alternatively, you can use `Arbitrary<BasicInlineQueue<T, kCapacity>>`.
+///
+/// @param[in] inner Domain that produces values of type `T`.
+///
+/// @retval Domain that produces values of type
+/// `BasicInlineQueue<T, SizeType, kCapacity>`.
+template <typename SizeType,
+ size_t kCapacity,
+ int&... ExplicitArgumentBarrier,
+ typename Inner>
+auto BasicQueueOf(Inner inner) {
+ return MapToBasicQueue<SizeType, kCapacity>(
+ VectorOf<kCapacity>(std::move(inner)));
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::BasicInlineQueue}.
+template <typename T, typename SizeType, size_t kCapacity>
+struct ArbitraryImpl<BasicInlineQueue<T, SizeType, kCapacity>> {
+ auto operator()() {
+ return BasicQueueOf<SizeType, kCapacity>(Arbitrary<T>());
+ }
+};
+
+/// Implementation of @cpp_func{pw::fuzzer::ContainerOf} for
+/// @cpp_class{pw::containers::BasicInlineQueue}.
+///
+/// Since inline queues have a static capacity, the returned domains do not
+/// produce FuzzTest containers, but aggregates. As a result, container methods
+/// such as `WithMaxSize` cannot be applied. Instead, use
+/// @cpp_func{pw::fuzzer::MapToQueue} or @cpp_func{pw::fuzzer::MapToBasicQueue}
+/// to apply constraints to the set of keys and values.
+///
+/// @param[in] inner Domain the produces values of type `T`.
+///
+/// @retval Domain that produces `@cpp_class{pw::BasicInlineQueue}`s.
+template <typename T, typename SizeType, size_t kCapacity>
+struct ContainerOfImpl<BasicInlineQueue<T, SizeType, kCapacity>> {
+ template <int&... ExplicitArgumentBarrier, typename Inner>
+ auto operator()(Inner inner) {
+ return BasicQueueOf<SizeType, kCapacity>(std::move(inner));
+ }
+};
+
+/// Transforms a domain that produces containers into a domain that produces
+/// @cpp_class{pw::InlineQueue}s.
+///
+/// The domains returned by @cpp_func{pw::fuzzer::QueueOf} and
+/// `Arbitrary<InlineQueue>` do not create FuzzTest containers. This method
+/// can be used to apply container methods such as `WithMinSize` or
+/// `UniqueElementsContainerOf` before building a queue from that container.
+///
+/// @param[in] inner Domain that produces containers.
+///
+/// @retval Domain that produces `@cpp_class{pw::InlineQueue}`s.
+template <size_t kCapacity, int&... ExplicitArgumentBarrier, typename Inner>
+auto MapToQueue(Inner inner) {
+ return MapToBasicQueue<uint16_t, kCapacity>(std::move(inner));
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::InlineQueue}s.
+///
+/// Use this, @cpp_func{pw::fuzzer::BasicQueueOf}, or
+/// @cpp_func{pw::fuzzer::ScopedListOf} in place of `fuzztest::ListOf`. The
+/// queue's maximum size is set by the template parameter.
+///
+/// Alternatively, you can use `Arbitrary<InlineQueue<T, kCapacity>>`.
+///
+/// @param[in] inner Domain that produces values of type `T`.
+///
+/// @retval Domain that produces values of type `InlineQueue<T, kCapacity>`.
+template <size_t kCapacity, int&... ExplicitArgumentBarrier, typename Inner>
+auto QueueOf(Inner inner) {
+ return BasicQueueOf<uint16_t, kCapacity>(std::move(inner));
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::InlineQueue}.
+template <typename T, size_t kCapacity>
+struct ArbitraryImpl<InlineQueue<T, kCapacity>> {
+ auto operator()() { return QueueOf<kCapacity>(Arbitrary<T>()); }
+};
+
+// Supporting types and functions for creating `IntrusiveList<T>`s.
+namespace internal {
+
+/// Construct an `Item` and emplace it in the given `Vector`.
+///
+/// The `Item` is constructed using arguments passed as a tuple.
+/// This should only be called by the overload that generates the index sequence
+/// used to expand the tuple.
+///
+/// @param[out] vector The vector to add the item to.
+/// @param[in] args A tuple of arguments to pass to the constructor of `T`.
+/// @param[in] (n/a) An sequence used to index the tuple.
+template <int&... ExplicitArgumentBarrier,
+ typename T,
+ typename Args,
+ size_t... Index>
+void EmplaceItem(Vector<T>& vector,
+ const Args& args,
+ std::index_sequence<Index...>) {
+ vector.emplace_back(std::get<Index>(args)...);
+}
+
+/// Construct an `Item` and emplace it in the given `Vector`.
+///
+/// The `Item` is constructed using arguments passed as a tuple.
+///
+/// @param[out] vector The vector to add the item to.
+/// @param[in] args A tuple of arguments to pass to the constructor of `T`.
+template <int&... ExplicitArgumentBarrier, typename T, typename Args>
+void EmplaceItem(Vector<T>& vector, const Args& args) {
+ EmplaceItem(
+ vector,
+ args,
+ std::make_index_sequence<std::tuple_size<std::decay_t<Args>>::value>{});
+}
+
+} // namespace internal
+
+/// Associates an `IntrusiveList<T>` with a `Vector<T>` that stores its `Item`s.
+///
+/// The `Item`s are constructed from a sequence of argument tuples passed to
+// constructor.
+template <typename T, size_t kMaxSize>
+class ScopedList {
+ public:
+ ~ScopedList() { list_.clear(); }
+
+ template <int&... ExplicitArgumentBarrier, typename Tuple>
+ explicit ScopedList(const Vector<Tuple>& arg_tuples) {
+ for (const auto& arg_tuple : arg_tuples) {
+ items_.emplace_back(std::make_from_tuple<T>(arg_tuple));
+ // internal::EmplaceItem(items_, arg);
+ list_.push_back(items_.back());
+ }
+ }
+
+ ScopedList(const ScopedList& other) = delete;
+ ScopedList& operator=(const ScopedList& other) = delete;
+
+ ScopedList(ScopedList&& other) { *this = std::move(other); }
+ ScopedList& operator=(ScopedList&& other) {
+ list_.clear();
+ other.list_.clear();
+ items_ = std::move(other.items_);
+ list_.assign(items_.begin(), items_.end());
+ return *this;
+ }
+
+ const IntrusiveList<T>& list() const { return list_; }
+
+ private:
+ IntrusiveList<T> list_;
+ Vector<T, kMaxSize> items_;
+};
+
+/// Transforms a domain that produces containers into a domain that produces
+/// @cpp_class{pw::fuzzer::ScopedList}s.
+///
+/// The domains returned by @cpp_func{pw::fuzzer::ScopedListOf} do not create
+/// FuzzTest containers. This method can be used to apply container methods such
+/// as `WithMinSize` or `UniqueElementsContainerOf` before building an intrusive
+/// list from that container.
+///
+/// @param[in] inner Domain that produces containers.
+///
+/// @retval Domain that produces `@cpp_class{pw::fuzzer::ScopedList}`s.
+template <typename T,
+ size_t kMaxSize,
+ int&... ExplicitArgumentBarrier,
+ typename Inner>
+auto MapToScopedList(Inner inner) {
+ using Container = typename Inner::value_type;
+ static_assert(internal::IsContainer<Container>::value);
+ using Tuple = typename Container::value_type;
+ static_assert(
+ std::is_same_v<T, decltype(std::make_from_tuple<T>(Tuple()))>,
+ "The domain passed to `pw::fuzzer::MapToScopedList<T, kMaxSize>>` must "
+ "produce `std::tuple`s of constructor arguments for `T`, e.g. using "
+ "`pw::fuzzer::TupleOf`.");
+ return ConstructorOf<ScopedList<T, kMaxSize>>(std::move(inner));
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::fuzzer::ScopedList}s.
+///
+/// Use this, @cpp_func{pw::fuzzer::BasicQueueOf}, or
+/// @cpp_func{pw::fuzzer::QueueOf} in place of `fuzztest::ListOf`. The list's
+/// maximum size is set by the template parameter.
+///
+/// @param[in] inner... Domains that produces `IntrusiveList<T>::Item`s.
+///
+/// @retval Domain that produces `ScopedList<T, kMaxSize>`s.
+template <typename T,
+ size_t kMaxSize,
+ int&... ExplicitArgumentBarrier,
+ typename... Inner>
+auto ScopedListOf(Inner... inner) {
+ return MapToScopedList<T, kMaxSize>(
+ VectorOf<kMaxSize>(TupleOf(std::move(inner)...)));
+}
+
+////////////////////////////////////////////////////////////////
+// pw_string-related types
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::InlineBasicString}s.
+///
+/// Use this in place of `fuzztest::StringOf`. The characters of the string
+/// are drawn from the given domain. The string capacity is given by the
+/// template parameter.
+///
+/// Alternatively, you can use `Arbitrary<InlineString<kCapacity>>`.
+///
+/// @param[in] inner Domain that produces values of a character type.
+///
+/// @retval Domain that produces `InlineBasicString<kCapacity>`s.
+template <size_t kCapacity, int&... ExplicitArgumentBarrier, typename Inner>
+auto StringOf(Inner inner) {
+ return ContainerOf<InlineBasicString<typename Inner::value_type, kCapacity>>(
+ inner)
+ .WithMaxSize(kCapacity);
+}
+
+/// Implementation of @cpp_func{pw::fuzzer::Arbitrary} for
+/// @cpp_class{pw::InlineBasicString}.
+template <typename T, size_t kCapacity>
+struct ArbitraryImpl<InlineBasicString<T, kCapacity>> {
+ auto operator()() { return StringOf<kCapacity>(Arbitrary<T>()); }
+};
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::InlineString}s.
+///
+/// Use this in place of `fuzztest::String`. The string capacity is given by the
+/// template parameter.
+///
+/// @retval Domain that produces `InlineString<kCapacity>`s.
+template <size_t kCapacity>
+auto String() {
+ return StringOf<kCapacity>(Arbitrary<char>());
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::InlineString}s
+/// containing only ASCII characters.
+///
+/// Use this in place of `fuzztest::AsciiString`. The string capacity is given
+/// by the template parameter.
+///
+/// @retval Domain that produces `InlineString<kCapacity>`s.
+template <size_t kCapacity>
+auto AsciiString() {
+ return StringOf<kCapacity>(AsciiChar());
+}
+
+/// Returns a FuzzTest domain that produces @cpp_class{pw::InlineString}s
+/// containing only printable ASCII characters.
+///
+/// Use this in place of `fuzztest::PrintableAsciiString`. The string capacity
+/// is given by the template parameter.
+///
+/// @retval Domain that produces printable `InlineString<kCapacity>`s.
+template <size_t kCapacity>
+auto PrintableAsciiString() {
+ return StringOf<kCapacity>(PrintableAsciiChar());
+}
+
+} // namespace pw::fuzzer
diff --git a/pw_fuzzer/public_overrides/fuzztest/fuzztest.h b/pw_fuzzer/public_overrides/fuzztest/fuzztest.h
new file mode 100644
index 000000000..a7ddc6e17
--- /dev/null
+++ b/pw_fuzzer/public_overrides/fuzztest/fuzztest.h
@@ -0,0 +1,150 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+/// @file fuzztest.h
+/// Stubs for the FuzzTest interface
+///
+/// @rst
+/// .. warning::
+/// This header depends on portions of the standard library that may not
+/// supported on your device!
+///
+/// This header provides stubs for the full FuzzTest interface, including
+/// `macros`_ and `domains`_ that include C++ standard library `headers`_ that
+/// are not permitted in Pigweed. It should only be used in downstream projects
+/// that support the full standard library on both host and device. It should
+/// never be used in core Pigweed.
+///
+/// If possible, consider including ``pw_fuzzer/fuzztest.h`` instead.
+///
+/// This header is included when FuzzTest is disabled, e.g. for GN, when
+/// ``dir_pw_third_party_fuzztest`` or ``pw_toolchain_FUZZING_ENABLED`` are not
+/// set. Otherwise, ``$dir_pw_third_party_fuzztest/fuzztest.h`` is used instead.
+///
+/// .. _domains:
+/// https://github.com/google/fuzztest/blob/main/doc/domains-reference.md
+/// .. _headers: https://pigweed.dev/docs/style_guide.html#permitted-headers
+/// .. _macros:
+/// https://github.com/google/fuzztest/blob/main/doc/fuzz-test-macro.md
+/// @endrst
+
+#include <deque>
+#include <list>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include "pw_fuzzer/internal/fuzztest.h"
+
+namespace fuzztest {
+
+// This namespace is here only as a way to disable ADL (argument-dependent
+// lookup). Names should be used from the fuzztest:: namespace.
+namespace internal_no_adl {
+
+template <typename T>
+auto ElementOf(std::vector<T>) {
+ return internal::Domain<T>{};
+}
+
+inline auto String() { return Arbitrary<std::string>(); }
+
+template <int&... ExplicitArgumentBarrier, typename T>
+inline auto StringOf(internal::Domain<T> inner) {
+ return ContainerOf<std::string>(std::move(inner));
+}
+
+inline auto AsciiString() { return StringOf(AsciiChar()); }
+
+inline auto PrintableAsciiString() { return StringOf(PrintableAsciiChar()); }
+
+template <template <typename> class Ptr,
+ int&... ExplicitArgumentBarrier,
+ typename T>
+auto SmartPointerOf(internal::Domain<T>) {
+ return internal::Domain<Ptr<T>>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto UniquePtrOf(internal::Domain<T>) {
+ return internal::Domain<std::unique_ptr<T>>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto SharedPtrOf(internal::Domain<T>) {
+ return internal::Domain<std::shared_ptr<T>>{};
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto VectorOf(internal::Domain<T> inner) {
+ return ContainerOf<std::vector<T>>(std::move(inner));
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto DequeOf(internal::Domain<T> inner) {
+ return ContainerOf<std::deque<T>>(std::move(inner));
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto ListOf(internal::Domain<T> inner) {
+ return ContainerOf<std::list<T>>(std::move(inner));
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto SetOf(internal::Domain<T> inner) {
+ return ContainerOf<std::set<T>>(std::move(inner));
+}
+
+template <int&... ExplicitArgumentBarrier, typename K, typename V>
+auto MapOf(internal::Domain<K> keys, internal::Domain<V> values) {
+ return ContainerOf<std::map<K, V>>(
+ PairOf(std::move(keys), std::move(values)));
+}
+
+template <int&... ExplicitArgumentBarrier, typename T>
+auto UnorderedSetOf(internal::Domain<T> inner) {
+ return ContainerOf<std::unordered_set<T>>(std::move(inner));
+}
+
+template <int&... ExplicitArgumentBarrier, typename K, typename V>
+auto UnorderedMapOf(internal::Domain<K> keys, internal::Domain<V> values) {
+ return ContainerOf<std::unordered_map<K, V>>(
+ PairOf(std::move(keys), std::move(values)));
+}
+
+template <typename T>
+auto UniqueElementsVectorOf(internal::Domain<T> inner) {
+ return VectorOf(std::move(inner));
+}
+
+template <typename P,
+ typename T = std::remove_cv_t<
+ std::remove_pointer_t<decltype(std::declval<P>()())>>>
+auto ProtobufOf(P) {
+ return internal::Domain<std::unique_ptr<T>>{};
+}
+
+} // namespace internal_no_adl
+
+// Inject the names from internal_no_adl into fuzztest, without allowing for
+// ADL. Note that an `inline` namespace would not have this effect (ie it would
+// still allow ADL to trigger).
+using namespace internal_no_adl; // NOLINT
+
+} // namespace fuzztest
diff --git a/pw_fuzzer/pw_fuzzer_disabled.cc b/pw_fuzzer/pw_fuzzer_disabled.cc
index 5b47a1073..766dfa29e 100644
--- a/pw_fuzzer/pw_fuzzer_disabled.cc
+++ b/pw_fuzzer/pw_fuzzer_disabled.cc
@@ -32,4 +32,4 @@ TEST(Fuzzer, EmptyInput) {
EXPECT_EQ(LLVMFuzzerTestOneInput(nullptr, 0), 0);
}
-// TODO(b/234883542): Add support for testing a seed corpus.
+// TODO: b/234883542 - Add support for testing a seed corpus.
diff --git a/pw_hdlc/Android.bp b/pw_hdlc/Android.bp
new file mode 100644
index 000000000..821ade0c4
--- /dev/null
+++ b/pw_hdlc/Android.bp
@@ -0,0 +1,58 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library {
+ name: "pw_hdlc",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_checksum",
+ "pw_status",
+ "pw_stream",
+ "pw_varint",
+ ],
+ export_static_lib_headers: [
+ "pw_bytes",
+ "pw_checksum",
+ "pw_status",
+ "pw_stream",
+ "pw_varint",
+ ],
+ srcs: [
+ "decoder.cc",
+ "encoder.cc",
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
diff --git a/pw_hdlc/BUILD.bazel b/pw_hdlc/BUILD.bazel
index e05925e78..8fb126f23 100644
--- a/pw_hdlc/BUILD.bazel
+++ b/pw_hdlc/BUILD.bazel
@@ -67,6 +67,7 @@ pw_cc_library(
deps = [
":pw_hdlc",
"//pw_rpc",
+ "//pw_sys_io",
],
)
@@ -119,9 +120,9 @@ pw_cc_test(
srcs = ["decoder_test.cc"],
deps = [
":pw_hdlc",
+ "//pw_fuzzer:fuzztest",
"//pw_result",
"//pw_stream",
- "//pw_unit_test",
],
)
diff --git a/pw_hdlc/BUILD.gn b/pw_hdlc/BUILD.gn
index 754a29a98..2e01b248e 100644
--- a/pw_hdlc/BUILD.gn
+++ b/pw_hdlc/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_fuzzer/fuzz_test.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -161,11 +162,17 @@ pw_test("encoded_size_test") {
"$dir_pw_varint",
]
sources = [ "encoded_size_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("encoder_test") {
deps = [ ":pw_hdlc" ]
sources = [ "encoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_python_action("generate_decoder_test") {
@@ -178,12 +185,13 @@ pw_python_action("generate_decoder_test") {
]
}
-pw_test("decoder_test") {
- deps = [
- ":generate_decoder_test",
- ":pw_hdlc",
- ]
- sources = [ "decoder_test.cc" ] + get_target_outputs(":generate_decoder_test")
+pw_fuzz_test("decoder_test") {
+ deps = [ ":pw_hdlc" ]
+ source_gen_deps = [ ":generate_decoder_test" ]
+ sources = [ "decoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("rpc_channel_test") {
@@ -192,6 +200,9 @@ pw_test("rpc_channel_test") {
":rpc_channel_output",
]
sources = [ "rpc_channel_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("wire_packet_parser_test") {
@@ -200,6 +211,9 @@ pw_test("wire_packet_parser_test") {
dir_pw_bytes,
]
sources = [ "wire_packet_parser_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_size_diff("size_report") {
@@ -221,7 +235,10 @@ pw_size_diff("size_report") {
pw_doc_group("docs") {
sources = [
+ "api.rst",
+ "design.rst",
"docs.rst",
+ "guide.rst",
"rpc_example/docs.rst",
]
inputs = [
diff --git a/pw_hdlc/CMakeLists.txt b/pw_hdlc/CMakeLists.txt
index c34a3b97c..342c6c24b 100644
--- a/pw_hdlc/CMakeLists.txt
+++ b/pw_hdlc/CMakeLists.txt
@@ -53,6 +53,7 @@ pw_add_library(pw_hdlc.decoder STATIC
pw_hdlc.common
pw_bytes
pw_checksum
+ pw_checksum.crc32
pw_result
pw_span
pw_status
@@ -71,6 +72,7 @@ pw_add_library(pw_hdlc.encoder STATIC
pw_hdlc.common
pw_bytes
pw_checksum
+ pw_checksum.crc32
pw_span
pw_status
pw_stream
@@ -131,15 +133,12 @@ pw_add_library(pw_hdlc.hdlc_sys_io_system_server STATIC
hdlc_sys_io_system_server.cc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_HDLC_RPC)
- zephyr_link_libraries(pw_hdlc.pw_rpc)
-endif()
-
pw_add_test(pw_hdlc.decoder_test
SOURCES
decoder_test.cc
PRIVATE_DEPS
pw_bytes
+ pw_fuzzer.fuzztest
pw_hdlc
GROUPS
modules
diff --git a/pw_hdlc/Kconfig b/pw_hdlc/Kconfig
index 3fc4c00cc..31829a3d7 100644
--- a/pw_hdlc/Kconfig
+++ b/pw_hdlc/Kconfig
@@ -12,13 +12,10 @@
# License for the specific language governing permissions and limitations under
# the License.
-config PIGWEED_HDLC
- bool "Enable Pigweed HDLC library (pw_hdlc)"
- select DEPRECATED
- select PIGWEED_HDLC_RPC
+menu "pw_hdlc"
config PIGWEED_HDLC_RPC
- bool "Enable Pigweed HDLC library (pw_hdlc.pw_rpc)"
+ bool "Link pw_hdlc.pw_rpc library"
select PIGWEED_ASSERT
select PIGWEED_BYTES
select PIGWEED_CHECKSUM
@@ -29,4 +26,7 @@ config PIGWEED_HDLC_RPC
select PIGWEED_STREAM
select PIGWEED_SYS_IO
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_hdlc` for module details.
+endmenu
diff --git a/pw_hdlc/api.rst b/pw_hdlc/api.rst
new file mode 100644
index 000000000..581d432cb
--- /dev/null
+++ b/pw_hdlc/api.rst
@@ -0,0 +1,266 @@
+.. _module-pw_hdlc-api:
+
+======================
+pw_hdlc: API reference
+======================
+.. pigweed-module-subpage::
+ :name: pw_hdlc
+ :tagline: Lightweight, simple, and easy serial communication
+
+This page describes the :ref:`module-pw_hdlc-api-encoder`, :ref:`module-pw_hdlc-api-decoder`,
+and :ref:`module-pw_hdlc-api-rpc` APIs of ``pw_hdlc``.
+
+.. _module-pw_hdlc-api-encoder:
+
+Encoder
+=======
+The Encoder API provides a single function that encodes data as an HDLC
+unnumbered information frame.
+
+.. tab-set::
+
+ .. tab-item:: C++
+
+ .. doxygenfunction:: pw::hdlc::WriteUIFrame(uint64_t address, ConstByteSpan data, stream::Writer &writer)
+
+ Example:
+
+ .. TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: cpp
+
+ // Writes a span of data to a pw::stream::Writer and returns the status. This
+ // implementation uses the pw_checksum module to compute the CRC-32 frame check
+ // sequence.
+
+ #include "pw_hdlc/encoder.h"
+ #include "pw_hdlc/sys_io_stream.h"
+
+ int main() {
+ pw::stream::SysIoWriter serial_writer;
+ Status status = WriteUIFrame(123 /* address */, data, serial_writer);
+ if (!status.ok()) {
+ PW_LOG_INFO("Writing frame failed! %s", status.str());
+ }
+ }
+
+ .. tab-item:: Python
+
+ .. automodule:: pw_hdlc.encode
+ :members:
+ :noindex:
+
+ Example:
+
+ .. TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: python
+
+ # Read bytes from serial and encode HDLC frames
+
+ import serial
+ from pw_hdlc import encode
+
+ ser = serial.Serial()
+ address = 123
+ ser.write(encode.ui_frame(address, b'your data here!'))
+
+ .. tab-item:: TypeScript
+
+ The Encoder class provides a way to build complete, escaped HDLC UI frames.
+
+ .. js:method:: Encoder.uiFrame(address, data)
+ :noindex:
+
+ :param number address: frame address.
+ :param Uint8Array data: frame data.
+ :returns: Uint8Array containing a complete HDLC frame.
+
+.. _module-pw_hdlc-api-decoder:
+
+Decoder
+=======
+
+
+.. tab-set::
+
+ .. tab-item:: C++
+
+ .. doxygenclass:: pw::hdlc::Decoder
+ :members:
+
+ Example:
+
+ .. TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: cpp
+
+ // Read individual bytes from pw::sys_io and decode HDLC frames.
+
+ #include "pw_hdlc/decoder.h"
+ #include "pw_sys_io/sys_io.h"
+
+ int main() {
+ std::byte data;
+ while (true) {
+ if (!pw::sys_io::ReadByte(&data).ok()) {
+ // Log serial reading error
+ }
+ Result<Frame> decoded_frame = decoder.Process(data);
+
+ if (decoded_frame.ok()) {
+ // Handle the decoded frame
+ }
+ }
+ }
+
+ .. tab-item:: Python
+
+ .. autoclass:: pw_hdlc.decode.FrameDecoder
+ :members:
+ :noindex:
+
+ Example:
+
+ .. TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: python
+
+ # Decode data read from serial
+
+ import serial
+ from pw_hdlc import decode
+
+ ser = serial.Serial()
+ decoder = decode.FrameDecoder()
+
+ while True:
+ for frame in decoder.process_valid_frames(ser.read()):
+ # Handle the decoded frame
+
+ It is possible to decode HDLC frames from a stream using different protocols or
+ unstructured data. This is not recommended, but may be necessary when
+ introducing HDLC to an existing system.
+
+ The ``FrameAndNonFrameDecoder`` Python class supports working with raw data and
+ HDLC frames in the same stream.
+
+ .. autoclass:: pw_hdlc.decode.FrameAndNonFrameDecoder
+ :members:
+ :noindex:
+
+ .. tab-item:: TypeScript
+
+ The decoder class unescapes received bytes and adds them to a buffer. Complete,
+ valid HDLC frames are yielded as they are received.
+
+ .. js:method:: Decoder.process(data)
+ :noindex:
+
+ :param Uint8Array data: bytes to be decoded.
+ :yields: HDLC frames, including corrupt frames.
+ The Frame.ok() method whether the frame is valid.
+
+ .. js:method:: processValidFrames(data)
+ :noindex:
+
+ :param Uint8Array data: bytes to be decoded.
+ :yields: Valid HDLC frames, logging any errors.
+
+.. _module-pw_hdlc-api-rpc:
+
+RPC
+===
+
+.. tab-set::
+
+ .. tab-item:: C++
+
+ The ``RpcChannelOutput`` implements pw_rpc's ``pw::rpc::ChannelOutput``
+ interface, simplifying the process of creating an RPC channel over HDLC. A
+ ``pw::stream::Writer`` must be provided as the underlying transport
+ implementation.
+
+ If your HDLC routing path has a Maximum Transmission Unit (MTU) limitation,
+ using the ``FixedMtuChannelOutput`` is strongly recommended to verify that the
+ currently configured max RPC payload size (dictated by pw_rpc's static encode
+ buffer) will always fit safely within the limits of the fixed HDLC MTU *after*
+ HDLC encoding.
+
+ .. tab-item:: Python
+
+ The ``pw_hdlc`` Python package includes utilities to HDLC-encode and
+ decode RPC packets, with examples of RPC Client implementations in Python.
+ It also provides abstractions for interfaces used to receive RPC Packets.
+
+ The ``pw_hdlc.rpc.CancellableReader`` and ``pw_hdlc.rpc.RpcClient``
+ classes and and derived classes are context-managed to cleanly cancel the
+ read process and stop the reader thread. The ``pw_hdlc.rpc.SocketReader``
+ and ``pw_hdlc.rpc.SerialReader`` also close the provided interface on
+ context exit. It is recommended to use these in a context statement. For
+ example:
+
+ .. code-block:: python
+
+ import serial
+ from pw_hdlc import rpc
+
+ if __name__ == '__main__':
+ serial_device = serial.Serial('/dev/ttyACM0')
+ with rpc.SerialReader(serial_device) as reader:
+ with rpc.HdlcRpcClient(
+ reader,
+ [],
+ rpc.default_channels(serial_device.write)) as rpc_client:
+ # Do something with rpc_client.
+
+ # The serial_device object is closed, and reader thread stopped.
+ return 0
+
+ .. autoclass:: pw_hdlc.rpc.channel_output
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.CancellableReader
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.SelectableReader
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.SocketReader
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.SerialReader
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.DataReaderAndExecutor
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.default_channels
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.RpcClient
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.HdlcRpcClient
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.NoEncodingSingleChannelRpcClient
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.SocketSubprocess
+ :members:
+ :noindex:
+
+ .. autoclass:: pw_hdlc.rpc.HdlcRpcLocalServerAndClient
+ :members:
+ :noindex:
diff --git a/pw_hdlc/decoder.cc b/pw_hdlc/decoder.cc
index 82452b2fb..297fa6037 100644
--- a/pw_hdlc/decoder.cc
+++ b/pw_hdlc/decoder.cc
@@ -27,7 +27,8 @@ namespace pw::hdlc {
Result<Frame> Frame::Parse(ConstByteSpan frame) {
uint64_t address;
size_t address_size = varint::Decode(frame, &address, kAddressFormat);
- int data_size = frame.size() - address_size - kControlSize - kFcsSize;
+ int data_size =
+ static_cast<int>(frame.size() - address_size - kControlSize - kFcsSize);
if (address_size == 0 || data_size < 0) {
return Status::DataLoss();
diff --git a/pw_hdlc/decoder_test.cc b/pw_hdlc/decoder_test.cc
index 358bb26dc..4a90a4d21 100644
--- a/pw_hdlc/decoder_test.cc
+++ b/pw_hdlc/decoder_test.cc
@@ -19,12 +19,14 @@
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
+#include "pw_fuzzer/fuzztest.h"
#include "pw_hdlc/internal/protocol.h"
namespace pw::hdlc {
namespace {
using std::byte;
+using namespace fuzzer;
TEST(Frame, Fields) {
static constexpr auto kFrameData =
@@ -152,5 +154,17 @@ TEST(Decoder, TooLargeForBuffer_DecodesNextFrame) {
EXPECT_EQ(OkStatus(), decoder.Process(kFlag).status());
}
+void ProcessNeverCrashes(ConstByteSpan data) {
+ DecoderBuffer<1024> decoder;
+ for (byte b : data) {
+ if (decoder.Process(b).status() != Status::Unavailable()) {
+ decoder.Clear();
+ }
+ }
+}
+
+FUZZ_TEST(Decoder, ProcessNeverCrashes)
+ .WithDomains(VectorOf<1024>(Arbitrary<byte>()));
+
} // namespace
} // namespace pw::hdlc
diff --git a/pw_hdlc/design.rst b/pw_hdlc/design.rst
new file mode 100644
index 000000000..d20d77493
--- /dev/null
+++ b/pw_hdlc/design.rst
@@ -0,0 +1,65 @@
+.. _module-pw_hdlc-design:
+
+===============
+pw_hdlc: Design
+===============
+.. pigweed-module-subpage::
+ :name: pw_hdlc
+ :tagline: Lightweight, simple, and easy serial communication
+
+``pw_hdlc`` implements a subset of the
+`HDLC <https://en.wikipedia.org/wiki/High-Level_Data_Link_Control>`_
+protocol.
+
+--------------------
+Protocol Description
+--------------------
+
+Frames
+======
+The HDLC implementation in ``pw_hdlc`` supports only HDLC unnumbered
+information frames. These frames are encoded as follows:
+
+.. code-block:: text
+
+ _________________________________________
+ | | | | | | |...
+ | | | | | | |... [More frames]
+ |_|_|_|__________________________|____|_|...
+ F A C Payload FCS F
+
+ F = flag byte (0x7e, the ~ character)
+ A = address field
+ C = control field
+ FCS = frame check sequence (CRC-32)
+
+
+Encoding and sending data
+=========================
+This module first writes an initial frame delimiter byte (0x7E) to indicate the
+beginning of the frame. Before sending any of the payload data through serial,
+the special bytes are escaped:
+
++-------------------------+-----------------------+
+| Unescaped Special Bytes | Escaped Special Bytes |
++=========================+=======================+
+| 7E | 7D 5E |
++-------------------------+-----------------------+
+| 7D | 7D 5D |
++-------------------------+-----------------------+
+
+The bytes of the payload are escaped and written in a single pass. The
+frame check sequence is calculated, escaped, and written after. After this, a
+final frame delimiter byte (0x7E) is written to mark the end of the frame.
+
+Decoding received bytes
+=======================
+Frames may be received in multiple parts, so we need to store the received data
+in a buffer until the ending frame delimiter (0x7E) is read. When the
+``pw_hdlc`` decoder receives data, it unescapes it and adds it to a buffer.
+When the frame is complete, it calculates and verifies the frame check sequence
+and does the following:
+
+* If correctly verified, the decoder returns the decoded frame.
+* If the checksum verification fails, the frame is discarded and an error is
+ reported.
diff --git a/pw_hdlc/docs.rst b/pw_hdlc/docs.rst
index 523a0c42f..412e5f032 100644
--- a/pw_hdlc/docs.rst
+++ b/pw_hdlc/docs.rst
@@ -1,323 +1,83 @@
.. _module-pw_hdlc:
+.. rst-class:: with-subtitle
+
=======
pw_hdlc
=======
+.. pigweed-module::
+ :name: pw_hdlc
+ :tagline: Lightweight, simple, and easy serial communication
+ :status: stable
+ :languages: C++17
+ :code-size-impact: 1400 to 2600 bytes
+
+ - Transmit RPCs and other data between devices over serial
+ - Detect corruption and data loss
+ - Stream to transport without buffering
+
+----------
+Background
+----------
+
`High-Level Data Link Control (HDLC)
<https://en.wikipedia.org/wiki/High-Level_Data_Link_Control>`_ is a data link
layer protocol intended for serial communication between devices. HDLC is
standardized as `ISO/IEC 13239:2002 <https://www.iso.org/standard/37010.html>`_.
+------------
+Our solution
+------------
+
The ``pw_hdlc`` module provides a simple, robust frame-oriented transport that
uses a subset of the HDLC protocol. ``pw_hdlc`` supports sending between
embedded devices or the host. It can be used with :ref:`module-pw_rpc` to enable
remote procedure calls (RPCs) on embedded on devices.
-**Why use the pw_hdlc module?**
-
- * Enables the transmission of RPCs and other data between devices over serial.
- * Detects corruption and data loss.
- * Light-weight, simple, and easy to use.
- * Supports streaming to transport without buffering, since the length is not
- encoded.
-
-.. admonition:: Try it out!
-
- For an example of how to use HDLC with :ref:`module-pw_rpc`, see the
- :ref:`module-pw_hdlc-rpc-example`.
-
-.. toctree::
- :maxdepth: 1
- :hidden:
-
- rpc_example/docs
-
---------------------
-Protocol Description
---------------------
-
-Frames
-======
-The HDLC implementation in ``pw_hdlc`` supports only HDLC unnumbered
-information frames. These frames are encoded as follows:
-
-.. code-block:: text
-
- _________________________________________
- | | | | | | |...
- | | | | | | |... [More frames]
- |_|_|_|__________________________|____|_|...
- F A C Payload FCS F
+``pw_hdlc`` has two primary functions:
- F = flag byte (0x7e, the ~ character)
- A = address field
- C = control field
- FCS = frame check sequence (CRC-32)
+* **Encoding** data by constructing a frame with the escaped payload bytes and
+ frame check sequence.
+* **Decoding** data by unescaping the received bytes, verifying the frame
+ check sequence, and returning successfully decoded frames.
+Design considerations
+=====================
-Encoding and sending data
-=========================
-This module first writes an initial frame delimiter byte (0x7E) to indicate the
-beginning of the frame. Before sending any of the payload data through serial,
-the special bytes are escaped:
+* ``pw_hdlc`` only supports unnumbered information frames.
+* It uses special escaped bytes to mark the beginning and end of a frame.
+* Frame data is stored in a buffer until the end-of-frame delimiter byte is read.
- +-------------------------+-----------------------+
- | Unescaped Special Bytes | Escaped Special Bytes |
- +=========================+=======================+
- | 7E | 7D 5E |
- +-------------------------+-----------------------+
- | 7D | 7D 5D |
- +-------------------------+-----------------------+
-
-The bytes of the payload are escaped and written in a single pass. The
-frame check sequence is calculated, escaped, and written after. After this, a
-final frame delimiter byte (0x7E) is written to mark the end of the frame.
-
-Decoding received bytes
-=======================
-Frames may be received in multiple parts, so we need to store the received data
-in a buffer until the ending frame delimiter (0x7E) is read. When the
-``pw_hdlc`` decoder receives data, it unescapes it and adds it to a buffer.
-When the frame is complete, it calculates and verifies the frame check sequence
-and does the following:
-
-* If correctly verified, the decoder returns the decoded frame.
-* If the checksum verification fails, the frame is discarded and an error is
- reported.
-
----------
-API Usage
----------
-There are two primary functions of the ``pw_hdlc`` module:
-
- * **Encoding** data by constructing a frame with the escaped payload bytes and
- frame check sequence.
- * **Decoding** data by unescaping the received bytes, verifying the frame
- check sequence, and returning successfully decoded frames.
-
-Encoder
-=======
-The Encoder API provides a single function that encodes data as an HDLC
-unnumbered information frame.
+See :ref:`module-pw_hdlc-design` for more information.
-C++
----
-.. cpp:namespace:: pw
-
-.. cpp:function:: Status hdlc::WriteUIFrame(uint64_t address, ConstByteSpan data, stream::Writer& writer)
-
- Writes a span of data to a :ref:`pw::stream::Writer <module-pw_stream>` and
- returns the status. This implementation uses the :ref:`module-pw_checksum`
- module to compute the CRC-32 frame check sequence.
-
-.. code-block:: cpp
-
- #include "pw_hdlc/encoder.h"
- #include "pw_hdlc/sys_io_stream.h"
-
- int main() {
- pw::stream::SysIoWriter serial_writer;
- Status status = WriteUIFrame(123 /* address */,
- data,
- serial_writer);
- if (!status.ok()) {
- PW_LOG_INFO("Writing frame failed! %s", status.str());
- }
- }
-
-Python
-------
-.. automodule:: pw_hdlc.encode
- :members:
-
-.. code-block:: python
-
- import serial
- from pw_hdlc import encode
-
- ser = serial.Serial()
- address = 123
- ser.write(encode.ui_frame(address, b'your data here!'))
-
-Typescript
-----------
-
-Encoder
-=======
-The Encoder class provides a way to build complete, escaped HDLC UI frames.
+Size report
+===========
+``pw_hdlc`` currently optimizes for robustness and flexibility instead of
+binary size or performance.
-.. js:method:: Encoder.uiFrame(address, data)
+There are two size reports: the first shows the cost of everything needed to
+use HDLC, including the dependencies on common modules like CRC32 from
+:ref:`module-pw_checksum` and variable-length integer handling from
+:ref:`module-pw_varint`. The other is the cost if your application is already
+linking those functions. ``pw_varint`` is commonly used since it's necessary
+for protocol buffer handling, so is often already present.
- :param number address: frame address.
- :param Uint8Array data: frame data.
- :returns: Uint8Array containing a complete HDLC frame.
+.. include:: size_report
-Decoder
+Roadmap
=======
-The decoder class unescapes received bytes and adds them to a buffer. Complete,
-valid HDLC frames are yielded as they are received.
-
-.. js:method:: Decoder.process(bytes)
-
- :param Uint8Array bytes: bytes received from the medium.
- :yields: Frame complete frames.
-
-C++
----
-.. cpp:class:: pw::hdlc::Decoder
-
- .. cpp:function:: pw::Result<Frame> Process(std::byte b)
-
- Parses a single byte of an HDLC stream. Returns a Result with the complete
- frame if the byte completes a frame. The status is the following:
-
- - OK - A frame was successfully decoded. The Result contains the Frame,
- which is invalidated by the next Process call.
- - UNAVAILABLE - No frame is available.
- - RESOURCE_EXHAUSTED - A frame completed, but it was too large to fit in
- the decoder's buffer.
- - DATA_LOSS - A frame completed, but it was invalid. The frame was
- incomplete or the frame check sequence verification failed.
-
- .. cpp:function:: void Process(pw::ConstByteSpan data, F&& callback, Args&&... args)
-
- Processes a span of data and calls the provided callback with each frame or
- error.
-
-This example demonstrates reading individual bytes from ``pw::sys_io`` and
-decoding HDLC frames:
-
-.. code-block:: cpp
-
- #include "pw_hdlc/decoder.h"
- #include "pw_sys_io/sys_io.h"
-
- int main() {
- std::byte data;
- while (true) {
- if (!pw::sys_io::ReadByte(&data).ok()) {
- // Log serial reading error
- }
- Result<Frame> decoded_frame = decoder.Process(data);
-
- if (decoded_frame.ok()) {
- // Handle the decoded frame
- }
- }
- }
-
-Python
-------
-.. autoclass:: pw_hdlc.decode.FrameDecoder
- :members:
-
-Below is an example using the decoder class to decode data read from serial:
-
-.. code-block:: python
-
- import serial
- from pw_hdlc import decode
-
- ser = serial.Serial()
- decoder = decode.FrameDecoder()
-
- while True:
- for frame in decoder.process_valid_frames(ser.read()):
- # Handle the decoded frame
-
-Typescript
-----------
-Decodes one or more HDLC frames from a stream of data.
-
-.. js:method:: process(data)
-
- :param Uint8Array data: bytes to be decoded.
- :yields: HDLC frames, including corrupt frames.
- The Frame.ok() method whether the frame is valid.
-
-.. js:method:: processValidFrames(data)
-
- :param Uint8Array data: bytes to be decoded.
- :yields: Valid HDLC frames, logging any errors.
-
-Allocating buffers
-------------------
-Since HDLC's encoding overhead changes with payload size and what data is being
-encoded, this module provides helper functions that are useful for determining
-the size of buffers by providing worst-case sizes of frames given a certain
-payload size and vice-versa.
-
-.. code-block:: cpp
-
- #include "pw_assert/check.h"
- #include "pw_bytes/span.h"
- #include "pw_hdlc/encoder"
- #include "pw_hdlc/encoded_size.h"
- #include "pw_status/status.h"
-
- // The max on-the-wire size in bytes of a single HDLC frame after encoding.
- constexpr size_t kMtu = 512;
- constexpr size_t kRpcEncodeBufferSize = pw::hdlc::MaxSafePayloadSize(kMtu);
- std::array<std::byte, kRpcEncodeBufferSize> rpc_encode_buffer;
-
- // Any data encoded to this buffer is guaranteed to fit in the MTU after
- // HDLC encoding.
- pw::ConstByteSpan GetRpcEncodeBuffer() {
- return rpc_encode_buffer;
- }
-
-The HDLC ``Decoder`` has its own helper for allocating a buffer since it doesn't
-need the entire escaped frame in-memory to decode, and therefore has slightly
-lower overhead.
-
-.. code-block:: cpp
-
- #include "pw_hdlc/decoder.h"
-
- // The max on-the-wire size in bytes of a single HDLC frame after encoding.
- constexpr size_t kMtu = 512;
-
- // Create a decoder given the MTU constraint.
- constexpr size_t kDecoderBufferSize =
- pw::hdlc::Decoder::RequiredBufferSizeForFrameSize(kMtu);
- pw::hdlc::DecoderBuffer<kDecoderBufferSize> decoder;
-
--------------------
-Additional features
--------------------
-
-Interleaving unstructured data with HDLC
-========================================
-It is possible to decode HDLC frames from a stream using different protocols or
-unstructured data. This is not recommended, but may be necessary when
-introducing HDLC to an existing system.
-
-The ``FrameAndNonFrameDecoder`` Python class supports working with raw data and
-HDLC frames in the same stream.
-
-.. autoclass:: pw_hdlc.decode.FrameAndNonFrameDecoder
- :members:
-
-RpcChannelOutput
-================
-The ``RpcChannelOutput`` implements pw_rpc's ``pw::rpc::ChannelOutput``
-interface, simplifying the process of creating an RPC channel over HDLC. A
-``pw::stream::Writer`` must be provided as the underlying transport
-implementation.
+- **Expanded protocol support** - ``pw_hdlc`` currently only supports
+ unnumbered information frames. Support for different frame types and
+ extended control fields may be added in the future.
-If your HDLC routing path has a Maximum Transmission Unit (MTU) limitation,
-using the ``FixedMtuChannelOutput`` is strongly recommended to verify that the
-currently configured max RPC payload size (dictated by pw_rpc's static encode
-buffer) will always fit safely within the limits of the fixed HDLC MTU *after*
-HDLC encoding.
+.. _module-pw_hdlc-get-started:
-HdlcRpcClient
-=============
-.. autoclass:: pw_hdlc.rpc.HdlcRpcClient
- :members:
+---------------
+Getting started
+---------------
-.. autoclass:: pw_hdlc.rpc.HdlcRpcLocalServerAndClient
- :members:
+For an example of how to use HDLC with :ref:`module-pw_rpc`, see the
+:ref:`module-pw_hdlc-rpc-example`.
Example pw::rpc::system_server backend
======================================
@@ -327,36 +87,16 @@ and has blocking sends/reads, so it is hardly performance-oriented and
unsuitable for performance-sensitive applications. This mostly servers as a
simplistic example for quickly bringing up RPC over HDLC on bare-metal targets.
------------
-Size report
------------
-The HDLC module currently optimizes for robustness and flexibility instead of
-binary size or performance.
-
-There are two size reports: the first shows the cost of everything needed to
-use HDLC, including the dependencies on common modules like CRC32 from
-pw_checksum and variable-length integer handling from pw_varint. The other is
-the cost if your application is already linking those functions. pw_varint is
-commonly used since it's necessary for protocol buffer handling, so is often
-already present.
-
-.. include:: size_report
-
--------
-Roadmap
--------
-- **Expanded protocol support** - ``pw_hdlc`` currently only supports
- unnumbered information frames. Support for different frame types and
- extended control fields may be added in the future.
-
--------------
-Compatibility
--------------
-C++17
-
-------
Zephyr
-------
+======
To enable ``pw_hdlc.pw_rpc`` for Zephyr add ``CONFIG_PIGWEED_HDLC_RPC=y`` to
the project's configuration.
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ design
+ api
+ rpc_example/docs
+ guide
diff --git a/pw_hdlc/guide.rst b/pw_hdlc/guide.rst
new file mode 100644
index 000000000..2f84b93cd
--- /dev/null
+++ b/pw_hdlc/guide.rst
@@ -0,0 +1,156 @@
+.. _module-pw_hdlc-guide:
+
+=====================
+pw_hdlc: How-to guide
+=====================
+.. pigweed-module-subpage::
+ :name: pw_hdlc
+ :tagline: Lightweight, simple, and easy serial communication
+
+This page shows you how to use the :ref:`module-pw_hdlc-api-encoder` and
+:ref:`module-pw_hdlc-api-decoder` APIs of :ref:`module-pw_hdlc`.
+
+--------
+Encoding
+--------
+.. tab-set::
+
+ .. tab-item:: C++
+
+ ..
+ TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: cpp
+
+ // Writes a span of data to a pw::stream::Writer and returns the status. This
+ // implementation uses the pw_checksum module to compute the CRC-32 frame check
+ // sequence.
+
+ #include "pw_hdlc/encoder.h"
+ #include "pw_hdlc/sys_io_stream.h"
+
+ int main() {
+ pw::stream::SysIoWriter serial_writer;
+ Status status = WriteUIFrame(123 /* address */, data, serial_writer);
+ if (!status.ok()) {
+ PW_LOG_INFO("Writing frame failed! %s", status.str());
+ }
+ }
+
+ .. tab-item:: Python
+
+ ..
+ TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: python
+
+ # Read bytes from serial and encode HDLC frames
+
+ import serial
+ from pw_hdlc import encode
+
+ ser = serial.Serial()
+ address = 123
+ ser.write(encode.ui_frame(address, b'your data here!'))
+
+Allocating buffers when encoding
+================================
+.. tab-set::
+
+ .. tab-item:: C++
+
+ Since HDLC's encoding overhead changes with payload size and what data is being
+ encoded, this module provides helper functions that are useful for determining
+ the size of buffers by providing worst-case sizes of frames given a certain
+ payload size and vice-versa.
+
+ .. code-block:: cpp
+
+ #include "pw_assert/check.h"
+ #include "pw_bytes/span.h"
+ #include "pw_hdlc/encoder"
+ #include "pw_hdlc/encoded_size.h"
+ #include "pw_status/status.h"
+
+ // The max on-the-wire size in bytes of a single HDLC frame after encoding.
+ constexpr size_t kMtu = 512;
+ constexpr size_t kRpcEncodeBufferSize = pw::hdlc::MaxSafePayloadSize(kMtu);
+ std::array<std::byte, kRpcEncodeBufferSize> rpc_encode_buffer;
+
+ // Any data encoded to this buffer is guaranteed to fit in the MTU after
+ // HDLC encoding.
+ pw::ConstByteSpan GetRpcEncodeBuffer() {
+ return rpc_encode_buffer;
+ }
+
+--------
+Decoding
+--------
+.. tab-set::
+
+ .. tab-item:: C++
+
+ ..
+ TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: cpp
+
+ // Read individual bytes from pw::sys_io and decode HDLC frames.
+
+ #include "pw_hdlc/decoder.h"
+ #include "pw_sys_io/sys_io.h"
+
+ int main() {
+ std::byte data;
+ while (true) {
+ if (!pw::sys_io::ReadByte(&data).ok()) {
+ // Log serial reading error
+ }
+ Result<Frame> decoded_frame = decoder.Process(data);
+
+ if (decoded_frame.ok()) {
+ // Handle the decoded frame
+ }
+ }
+ }
+
+ .. tab-item:: Python
+
+ ..
+ TODO: b/279648188 - Share this code between api.rst and guide.rst.
+
+ .. code-block:: python
+
+ # Decode data read from serial
+
+ import serial
+ from pw_hdlc import decode
+
+ ser = serial.Serial()
+ decoder = decode.FrameDecoder()
+
+ while True:
+ for frame in decoder.process_valid_frames(ser.read()):
+ # Handle the decoded frame
+
+Allocating buffers when decoding
+================================
+.. tab-set::
+
+ .. tab-item:: C++
+
+ The HDLC ``Decoder`` has its own helper for allocating a buffer since it doesn't
+ need the entire escaped frame in-memory to decode, and therefore has slightly
+ lower overhead.
+
+ .. code-block:: cpp
+
+ #include "pw_hdlc/decoder.h"
+
+ // The max on-the-wire size in bytes of a single HDLC frame after encoding.
+ constexpr size_t kMtu = 512;
+
+ // Create a decoder given the MTU constraint.
+ constexpr size_t kDecoderBufferSize =
+ pw::hdlc::Decoder::RequiredBufferSizeForFrameSize(kMtu);
+ pw::hdlc::DecoderBuffer<kDecoderBufferSize> decoder;
diff --git a/pw_hdlc/public/pw_hdlc/decoder.h b/pw_hdlc/public/pw_hdlc/decoder.h
index 0290a2ad8..8806850a9 100644
--- a/pw_hdlc/public/pw_hdlc/decoder.h
+++ b/pw_hdlc/public/pw_hdlc/decoder.h
@@ -74,18 +74,20 @@ class Decoder {
Decoder(const Decoder&) = delete;
Decoder& operator=(const Decoder&) = delete;
-
- // Parses a single byte of an HDLC stream. Returns a Result with the complete
- // frame if the byte completes a frame. The status is the following:
- //
- // OK - A frame was successfully decoded. The Result contains the Frame,
- // which is invalidated by the next Process call.
- // UNAVAILABLE - No frame is available.
- // RESOURCE_EXHAUSTED - A frame completed, but it was too large to fit in
- // the decoder's buffer.
- // DATA_LOSS - A frame completed, but it was invalid. The frame was
- // incomplete or the frame check sequence verification failed.
- //
+ Decoder(Decoder&&) = default;
+ Decoder& operator=(Decoder&&) = default;
+
+ /// @brief Parses a single byte of an HDLC stream.
+ ///
+ /// @returns A `pw::Result` with the complete frame if the byte completes a
+ /// frame. The status can be one of the following:
+ /// * `OK` - A frame was successfully decoded. The `Result` contains the
+ /// `Frame`, which is invalidated by the next `Process()` call.
+ /// * `UNAVAILABLE` - No frame is available.
+ /// * `RESOURCE_EXHAUSTED` - A frame completed, but it was too large to fit in
+ /// the decoder's buffer.
+ /// * `DATA_LOSS` - A frame completed, but it was invalid. The frame was
+ /// incomplete or the frame check sequence verification failed.
Result<Frame> Process(std::byte new_byte);
// Returns the buffer space required for a `Decoder` to successfully decode a
@@ -99,8 +101,8 @@ class Decoder {
: max_frame_size - 2;
}
- // Processes a span of data and calls the provided callback with each frame or
- // error.
+ /// @brief Processes a span of data and calls the provided callback with each
+ /// frame or error.
template <typename F, typename... Args>
void Process(ConstByteSpan data, F&& callback, Args&&... args) {
for (std::byte b : data) {
@@ -141,7 +143,7 @@ class Decoder {
bool VerifyFrameCheckSequence() const;
- const ByteSpan buffer_;
+ ByteSpan buffer_;
// Ring buffer of the last four bytes read into the current frame, to allow
// calculating the frame's CRC incrementally. As data is evicted from this
diff --git a/pw_hdlc/public/pw_hdlc/encoder.h b/pw_hdlc/public/pw_hdlc/encoder.h
index 8cd58379a..52ca23d57 100644
--- a/pw_hdlc/public/pw_hdlc/encoder.h
+++ b/pw_hdlc/public/pw_hdlc/encoder.h
@@ -19,16 +19,30 @@
namespace pw::hdlc {
-// Writes an HDLC unnumbered information frame (UI-frame) to the provided
-// writer. The complete frame contains the following:
-//
-// - HDLC flag byte (0x7e)
-// - Address (variable length, up to 10 bytes)
-// - UI-frame control (metadata) byte
-// - Payload (0 or more bytes)
-// - Frame check sequence (CRC-32, 4 bytes)
-// - HDLC flag byte (0x7e)
-//
+/// @brief Writes an HDLC unnumbered information frame (UI frame) to the
+/// provided `pw::stream` writer.
+///
+/// @param address The frame address.
+///
+/// @param payload The frame data to encode.
+///
+/// @param writer The `pw::stream` to write the frame to. The frame contains
+/// the following bytes. See [pw_hdlc: Design](/pw_hdlc/design.html) for more
+/// information.
+/// * HDLC flag byte (`0x7e`)
+/// * Address (variable length, up to 10 bytes)
+/// * UI-frame control (metadata) byte
+/// * Payload (0 or more bytes)
+/// * Frame check sequence (CRC-32, 4 bytes)
+/// * HDLC flag byte (`0x7e`)
+///
+/// @returns A [pw::Status](/pw_status/) instance describing the result of the
+/// operation:
+/// * `pw::Status::Ok()`: The write finished successfully.
+/// * `pw::Status::ResourceExhausted()`: The write failed because the size of
+/// the frame would be larger than the writer's conservative limit.
+/// * `pw::Status::InvalidArgument()`: The start of the write failed. Check
+/// for problems in your `address` argument's value.
Status WriteUIFrame(uint64_t address,
ConstByteSpan payload,
stream::Writer& writer);
diff --git a/pw_hdlc/py/BUILD.bazel b/pw_hdlc/py/BUILD.bazel
index a88424d1d..a6460aa40 100644
--- a/pw_hdlc/py/BUILD.bazel
+++ b/pw_hdlc/py/BUILD.bazel
@@ -32,6 +32,7 @@ py_library(
"//pw_protobuf_compiler/py:pw_protobuf_compiler",
"//pw_rpc/py:pw_rpc",
"//pw_status/py:pw_status",
+ "@python_packages_pyserial//:pkg",
],
)
@@ -57,3 +58,14 @@ py_test(
"//pw_build/py:pw_build",
],
)
+
+py_test(
+ name = "rpc_test",
+ size = "small",
+ srcs = [
+ "rpc_test.py",
+ ],
+ deps = [
+ ":pw_hdlc",
+ ],
+)
diff --git a/pw_hdlc/py/BUILD.gn b/pw_hdlc/py/BUILD.gn
index 5e8b4c03c..f584f1d50 100644
--- a/pw_hdlc/py/BUILD.gn
+++ b/pw_hdlc/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_hdlc/__init__.py",
@@ -33,6 +32,7 @@ pw_python_package("py") {
tests = [
"decode_test.py",
"encode_test.py",
+ "rpc_test.py",
]
python_deps = [
"$dir_pw_protobuf_compiler/py",
diff --git a/pw_hdlc/py/pw_hdlc/rpc.py b/pw_hdlc/py/pw_hdlc/rpc.py
index cb0960524..f9949071a 100644
--- a/pw_hdlc/py/pw_hdlc/rpc.py
+++ b/pw_hdlc/py/pw_hdlc/rpc.py
@@ -13,10 +13,14 @@
# the License.
"""Utilities for using HDLC with pw_rpc."""
+from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor
import io
import logging
-from queue import SimpleQueue
+import os
+import platform
+import queue
+import select
import sys
import threading
import time
@@ -29,11 +33,14 @@ from typing import (
Dict,
Iterable,
List,
- NoReturn,
Optional,
Sequence,
+ TypeVar,
Union,
)
+import warnings
+
+import serial
from pw_protobuf_compiler import python_protos
import pw_rpc
@@ -42,10 +49,11 @@ from pw_rpc import callback_client
from pw_hdlc.decode import Frame, FrameDecoder
from pw_hdlc import encode
-_LOG = logging.getLogger(__name__)
+_LOG = logging.getLogger('pw_hdlc.rpc')
STDOUT_ADDRESS = 1
DEFAULT_ADDRESS = ord('R')
+DEFAULT_CHANNEL_ID = 1
_VERBOSE = logging.DEBUG - 1
@@ -74,69 +82,243 @@ def channel_output(
return write_hdlc
-def _handle_error(frame: Frame) -> None:
- _LOG.error('Failed to parse frame: %s', frame.status.value)
- _LOG.debug('%s', frame.data)
+FrameHandlers = Dict[int, Callable[[Frame], Any]]
+FrameTypeT = TypeVar('FrameTypeT')
+
+class CancellableReader(ABC):
+ """Wraps communication interfaces used for reading incoming data with the
+ guarantee that the read request can be cancelled. Derived classes must
+ implement the :py:func:`cancel_read()` method.
-FrameHandlers = Dict[int, Callable[[Frame], Any]]
+ Cancelling a read invalidates ongoing and future reads. The
+ :py:func:`cancel_read()` method can only be called once.
+ """
+
+ def __init__(self, base_obj: Any, *read_args, **read_kwargs):
+ """
+ Args:
+ base_obj: Object that offers a ``read()`` method with optional args
+ and kwargs.
+ read_args: Arguments for ``base_obj.read()`` function.
+ read_kwargs: Keyword arguments for ``base_obj.read()`` function.
+ """
+ self._base_obj = base_obj
+ self._read_args = read_args
+ self._read_kwargs = read_kwargs
+
+ def __enter__(self) -> 'CancellableReader':
+ return self
+
+ def __exit__(self, *exc_info) -> None:
+ self.cancel_read()
+
+ def read(self) -> bytes:
+ """Reads bytes that contain parts of or full RPC packets."""
+ return self._base_obj.read(*self._read_args, **self._read_kwargs)
+ @abstractmethod
+ def cancel_read(self) -> None:
+ """Cancels a blocking read request and all future reads.
+
+ Can only be called once.
+ """
-def read_and_process_data(
- read: Callable[[], bytes],
- on_read_error: Callable[[Exception], Any],
- frame_handlers: FrameHandlers,
- error_handler: Callable[[Frame], Any] = _handle_error,
- handler_threads: Optional[int] = 1,
-) -> NoReturn:
- """Continuously reads and handles HDLC frames.
- Passes frames to an executor that calls frame handler functions in other
- threads.
+class SelectableReader(CancellableReader):
"""
+ Wraps interfaces that work with select() to signal when data is received.
- def handle_frame(frame: Frame):
- try:
- if not frame.ok():
- error_handler(frame)
- return
+ These interfaces must provide a ``fileno()`` method.
+ WINDOWS ONLY: Only sockets that originate from WinSock can be wrapped. File
+ objects are not acceptable.
+ """
- try:
- frame_handlers[frame.address](frame)
- except KeyError:
- _LOG.warning(
- 'Unhandled frame for address %d: %s', frame.address, frame
- )
- except: # pylint: disable=bare-except
- _LOG.exception('Exception in HDLC frame handler thread')
-
- decoder = FrameDecoder()
-
- # Execute callbacks in a ThreadPoolExecutor to decouple reading the input
- # stream from handling the data. That way, if a handler function takes a
- # long time or crashes, this reading thread is not interrupted.
- with ThreadPoolExecutor(max_workers=handler_threads) as executor:
- while True:
- try:
- data = read()
- except Exception as exc: # pylint: disable=broad-except
- on_read_error(exc)
- continue
+ _STOP_CMD = b'STOP'
+
+ def __init__(self, base_obj: Any, *read_args, **read_kwargs):
+ assert hasattr(base_obj, 'fileno')
+ if platform.system() == 'Windows' and not isinstance(
+ base_obj, socket.socket
+ ):
+ raise ValueError('Only socket objects are selectable on Windows')
+ super().__init__(base_obj, *read_args, **read_kwargs)
+ self._cancel_signal_pipe_r_fd, self._cancel_signal_pipe_w_fd = os.pipe()
+ self._waiting_for_read_or_cancel_lock = threading.Lock()
+
+ def __exit__(self, *exc_info) -> None:
+ self.cancel_read()
+ with self._waiting_for_read_or_cancel_lock:
+ if self._cancel_signal_pipe_r_fd > 0:
+ os.close(self._cancel_signal_pipe_r_fd)
+ self._cancel_signal_pipe_r_fd = -1
+
+ def read(self) -> bytes:
+ if self._wait_for_read_or_cancel():
+ return super().read()
+ return b''
+
+ def _wait_for_read_or_cancel(self) -> bool:
+ """Returns True when ready to read."""
+ with self._waiting_for_read_or_cancel_lock:
+ if self._base_obj.fileno() < 0 or self._cancel_signal_pipe_r_fd < 0:
+ # The interface might've been closed already.
+ return False
+ ready_to_read, _, exception_list = select.select(
+ [self._cancel_signal_pipe_r_fd, self._base_obj],
+ [],
+ [self._base_obj],
+ )
+ if self._cancel_signal_pipe_r_fd in ready_to_read:
+ # A signal to stop the reading process was received.
+ os.read(self._cancel_signal_pipe_r_fd, len(self._STOP_CMD))
+ os.close(self._cancel_signal_pipe_r_fd)
+ self._cancel_signal_pipe_r_fd = -1
+ return False
+
+ if exception_list:
+ _LOG.error('Error reading interface')
+ return False
+ return True
+
+ def cancel_read(self) -> None:
+ if self._cancel_signal_pipe_w_fd > 0:
+ os.write(self._cancel_signal_pipe_w_fd, self._STOP_CMD)
+ os.close(self._cancel_signal_pipe_w_fd)
+ self._cancel_signal_pipe_w_fd = -1
+
+
+class SocketReader(SelectableReader):
+ """Wraps a socket ``recv()`` function."""
+
+ def __init__(self, base_obj: socket.socket, *read_args, **read_kwargs):
+ super().__init__(base_obj, *read_args, **read_kwargs)
+
+ def read(self) -> bytes:
+ if self._wait_for_read_or_cancel():
+ return self._base_obj.recv(*self._read_args, **self._read_kwargs)
+ return b''
+
+ def __exit__(self, *exc_info) -> None:
+ self.cancel_read()
+ self._base_obj.close()
+
+
+class SerialReader(CancellableReader):
+ """Wraps a :py:class:`serial.Serial` object."""
+
+ def __init__(self, base_obj: serial.Serial, *read_args, **read_kwargs):
+ super().__init__(base_obj, *read_args, **read_kwargs)
+
+ def cancel_read(self) -> None:
+ self._base_obj.cancel_read()
+
+ def __exit__(self, *exc_info) -> None:
+ self.cancel_read()
+ self._base_obj.close()
+
+
+class DataReaderAndExecutor:
+ """Reads incoming bytes, data processor that delegates frame handling.
+
+ Executing callbacks in a ThreadPoolExecutor decouples reading the input
+ stream from handling the data. That way, if a handler function takes a
+ long time or crashes, this reading thread is not interrupted.
+ """
+
+ def __init__(
+ self,
+ reader: CancellableReader,
+ on_read_error: Callable[[Exception], None],
+ data_processor: Callable[[bytes], Iterable[FrameTypeT]],
+ frame_handler: Callable[[FrameTypeT], None],
+ handler_threads: Optional[int] = 1,
+ ):
+ """Creates the data reader and frame delegator.
+
+ Args:
+ reader: Reads incoming bytes from the given transport, blocks until
+ data is available or an exception is raised. Otherwise the reader
+ will exit.
+ on_read_error: Called when there is an error reading incoming bytes.
+ data_processor: Processes read bytes and returns a frame-like object
+ that the frame_handler can process.
+ frame_handler: Handles a received frame.
+ handler_threads: The number of threads in the executor pool.
+ """
+
+ self._reader = reader
+ self._on_read_error = on_read_error
+ self._data_processor = data_processor
+ self._frame_handler = frame_handler
+ self._handler_threads = handler_threads
+
+ self._reader_thread = threading.Thread(target=self._run)
+ self._reader_thread_stop = threading.Event()
+
+ def start(self) -> None:
+ """Starts the reading process."""
+ _LOG.debug('Starting read process')
+ self._reader_thread_stop.clear()
+ self._reader_thread.start()
+
+ def stop(self) -> None:
+ """Stops the reading process.
+
+ This requests that the reading process stop and waits
+ for the background thread to exit.
+ """
+ _LOG.debug('Stopping read process')
+ self._reader_thread_stop.set()
+ self._reader.cancel_read()
+ self._reader_thread.join(30)
+ if self._reader_thread.is_alive():
+ warnings.warn(
+ 'Timed out waiting for read thread to terminate.\n'
+ 'Tip: Use a `CancellableReader` to cancel reads.'
+ )
+
+ def _run(self) -> None:
+ """Reads raw data in a background thread."""
+ with ThreadPoolExecutor(max_workers=self._handler_threads) as executor:
+ while not self._reader_thread_stop.is_set():
+ try:
+ data = self._reader.read()
+ except Exception as exc: # pylint: disable=broad-except
+ # Don't report the read error if the thread is stopping.
+ # The stream or device backing _read was likely closed,
+ # so errors are expected.
+ if not self._reader_thread_stop.is_set():
+ self._on_read_error(exc)
+ _LOG.debug(
+ 'DataReaderAndExecutor thread exiting due to exception',
+ exc_info=exc,
+ )
+ return
+
+ if not data:
+ continue
- if data:
_LOG.log(_VERBOSE, 'Read %2d B: %s', len(data), data)
- for frame in decoder.process_valid_frames(data):
- executor.submit(handle_frame, frame)
+ for frame in self._data_processor(data):
+ executor.submit(self._frame_handler, frame)
-def write_to_file(data: bytes, output: BinaryIO = sys.stdout.buffer):
+# Writes to stdout by default, but sys.stdout.buffer is not guaranteed to exist
+# (see https://docs.python.org/3/library/io.html#io.TextIOBase.buffer). Defer
+# to sys.__stdout__.buffer if sys.stdout is wrapped with something that does not
+# offer it.
+def write_to_file(
+ data: bytes,
+ output: BinaryIO = getattr(sys.stdout, 'buffer', sys.__stdout__.buffer),
+) -> None:
output.write(data + b'\n')
output.flush()
def default_channels(write: Callable[[bytes], Any]) -> List[pw_rpc.Channel]:
- return [pw_rpc.Channel(1, channel_output(write))]
+ return [pw_rpc.Channel(DEFAULT_CHANNEL_ID, channel_output(write))]
PathsModulesOrProtoLibrary = Union[
@@ -144,28 +326,24 @@ PathsModulesOrProtoLibrary = Union[
]
-class HdlcRpcClient:
- """An RPC client configured to run over HDLC."""
+class RpcClient:
+ """An RPC client with configurable incoming data processing."""
def __init__(
self,
- read: Callable[[], bytes],
+ reader_and_executor: DataReaderAndExecutor,
paths_or_modules: PathsModulesOrProtoLibrary,
channels: Iterable[pw_rpc.Channel],
- output: Callable[[bytes], Any] = write_to_file,
client_impl: Optional[pw_rpc.client.ClientImpl] = None,
- *,
- _incoming_packet_filter_for_testing: Optional[
- pw_rpc.ChannelManipulator
- ] = None,
):
- """Creates an RPC client configured to communicate using HDLC.
+ """Creates an RPC client.
Args:
- read: Function that reads bytes; e.g serial_device.read.
- paths_or_modules: paths to .proto files or proto modules
- channel: RPC channels to use for output
- output: where to write "stdout" output from the device
+ reader_and_executor: DataReaderAndExecutor instance.
+ paths_or_modules: paths to .proto files or proto modules.
+ channels: RPC channels to use for output.
+ client_impl: The RPC Client implementation. Defaults to the callback
+ client implementation if not provided.
"""
if isinstance(paths_or_modules, python_protos.Library):
self.protos = paths_or_modules
@@ -179,22 +357,18 @@ class HdlcRpcClient:
client_impl, channels, self.protos.modules()
)
- rpc_output: Callable[[bytes], Any] = self._handle_rpc_packet
- if _incoming_packet_filter_for_testing is not None:
- _incoming_packet_filter_for_testing.send_packet = rpc_output
- rpc_output = _incoming_packet_filter_for_testing
+ # Start background thread that reads and processes RPC packets.
+ self._reader_and_executor = reader_and_executor
+ self._reader_and_executor.start()
- frame_handlers: FrameHandlers = {
- DEFAULT_ADDRESS: lambda frame: rpc_output(frame.data),
- STDOUT_ADDRESS: lambda frame: output(frame.data),
- }
+ def __enter__(self):
+ return self
- # Start background thread that reads and processes RPC packets.
- threading.Thread(
- target=read_and_process_data,
- daemon=True,
- args=(read, lambda exc: None, frame_handlers),
- ).start()
+ def __exit__(self, *exc_info):
+ self.close()
+
+ def close(self) -> None:
+ self._reader_and_executor.stop()
def rpcs(self, channel_id: Optional[int] = None) -> Any:
"""Returns object for accessing services on the specified channel.
@@ -208,11 +382,132 @@ class HdlcRpcClient:
return self.client.channel(channel_id).rpcs
- def _handle_rpc_packet(self, packet: bytes) -> None:
+ def handle_rpc_packet(self, packet: bytes) -> None:
if not self.client.process_packet(packet):
_LOG.error('Packet not handled by RPC client: %s', packet)
+class HdlcRpcClient(RpcClient):
+ """An RPC client configured to run over HDLC.
+
+ Expects HDLC frames to have addresses that dictate how to parse the HDLC
+ payloads.
+ """
+
+ def __init__(
+ self,
+ reader: CancellableReader,
+ paths_or_modules: PathsModulesOrProtoLibrary,
+ channels: Iterable[pw_rpc.Channel],
+ output: Callable[[bytes], Any] = write_to_file,
+ client_impl: Optional[pw_rpc.client.ClientImpl] = None,
+ *,
+ _incoming_packet_filter_for_testing: Optional[
+ pw_rpc.ChannelManipulator
+ ] = None,
+ rpc_frames_address: int = DEFAULT_ADDRESS,
+ log_frames_address: int = STDOUT_ADDRESS,
+ extra_frame_handlers: Optional[FrameHandlers] = None,
+ ):
+ """Creates an RPC client configured to communicate using HDLC.
+
+ Args:
+ reader: Readable object used to receive RPC packets.
+ paths_or_modules: paths to .proto files or proto modules.
+ channels: RPC channels to use for output.
+ output: where to write "stdout" output from the device.
+ client_impl: The RPC Client implementation. Defaults to the callback
+ client implementation if not provided.
+ rpc_frames_address: the address used in the HDLC frames for RPC
+ packets. This can be the channel ID, or any custom address.
+ log_frames_address: the address used in the HDLC frames for "stdout"
+ output from the device.
+ extra_fram_handlers: Optional mapping of HDLC frame addresses to their
+ callbacks.
+ """
+ # Set up frame handling.
+ rpc_output: Callable[[bytes], Any] = self.handle_rpc_packet
+ if _incoming_packet_filter_for_testing is not None:
+ _incoming_packet_filter_for_testing.send_packet = rpc_output
+ rpc_output = _incoming_packet_filter_for_testing
+
+ frame_handlers: FrameHandlers = {
+ rpc_frames_address: lambda frame: rpc_output(frame.data),
+ log_frames_address: lambda frame: output(frame.data),
+ }
+ if extra_frame_handlers:
+ frame_handlers.update(extra_frame_handlers)
+
+ def handle_frame(frame: Frame) -> None:
+ # Suppress raising any frame errors to avoid crashes on data
+ # processing, which may hide or drop other data.
+ try:
+ if not frame.ok():
+ _LOG.error('Failed to parse frame: %s', frame.status.value)
+ _LOG.debug('%s', frame.data)
+ return
+
+ try:
+ frame_handlers[frame.address](frame)
+ except KeyError:
+ _LOG.warning(
+ 'Unhandled frame for address %d: %s',
+ frame.address,
+ frame,
+ )
+ except: # pylint: disable=bare-except
+ _LOG.exception('Exception in HDLC frame handler thread')
+
+ decoder = FrameDecoder()
+
+ def on_read_error(exc: Exception) -> None:
+ _LOG.error('data reader encountered an error', exc_info=exc)
+
+ reader_and_executor = DataReaderAndExecutor(
+ reader, on_read_error, decoder.process_valid_frames, handle_frame
+ )
+ super().__init__(
+ reader_and_executor, paths_or_modules, channels, client_impl
+ )
+
+
+class NoEncodingSingleChannelRpcClient(RpcClient):
+ """An RPC client without any frame encoding with a single channel output.
+
+ The caveat is that the provided read function must read entire frames.
+ """
+
+ def __init__(
+ self,
+ reader: CancellableReader,
+ paths_or_modules: PathsModulesOrProtoLibrary,
+ channel: pw_rpc.Channel,
+ client_impl: Optional[pw_rpc.client.ClientImpl] = None,
+ ):
+ """Creates an RPC client over a single channel with no frame encoding.
+
+ Args:
+ reader: Readable object used to receive RPC packets.
+ paths_or_modules: paths to .proto files or proto modules.
+ channel: RPC channel to use for output.
+ client_impl: The RPC Client implementation. Defaults to the callback
+ client implementation if not provided.
+ """
+
+ def process_data(data: bytes):
+ yield data
+
+ def on_read_error(exc: Exception) -> None:
+ _LOG.error('data reader encountered an error', exc_info=exc)
+
+ reader_and_executor = DataReaderAndExecutor(
+ reader, on_read_error, process_data, self.handle_rpc_packet
+ )
+ super().__init__(
+ reader_and_executor, paths_or_modules, [channel], client_impl
+ )
+
+
def _try_connect(port: int, attempts: int = 10) -> socket.socket:
"""Tries to connect to the specified port up to the given number of times.
@@ -285,7 +580,7 @@ class HdlcRpcLocalServerAndClient:
self.server = SocketSubprocess(server_command, port)
- self._bytes_queue: 'SimpleQueue[bytes]' = SimpleQueue()
+ self._bytes_queue: 'queue.SimpleQueue[bytes]' = queue.SimpleQueue()
self._read_thread = threading.Thread(target=self._read_from_socket)
self._read_thread.start()
@@ -298,13 +593,24 @@ class HdlcRpcLocalServerAndClient:
outgoing_processor.send_packet = self.channel_output
self.channel_output = outgoing_processor
- self.client = HdlcRpcClient(
- self._bytes_queue.get,
+ class QueueReader(CancellableReader):
+ def read(self) -> bytes:
+ try:
+ return self._base_obj.get(timeout=3)
+ except queue.Empty:
+ return b''
+
+ def cancel_read(self) -> None:
+ pass
+
+ self._rpc_client = HdlcRpcClient(
+ QueueReader(self._bytes_queue),
protos,
default_channels(self.channel_output),
self.output.write,
_incoming_packet_filter_for_testing=incoming_processor,
- ).client
+ )
+ self.client = self._rpc_client.client
def _read_from_socket(self):
while True:
@@ -316,6 +622,7 @@ class HdlcRpcLocalServerAndClient:
def close(self):
self.server.close()
self.output.close()
+ self._rpc_client.close()
self._read_thread.join()
def __enter__(self) -> 'HdlcRpcLocalServerAndClient':
diff --git a/pw_hdlc/py/rpc_test.py b/pw_hdlc/py/rpc_test.py
new file mode 100755
index 000000000..3ed388205
--- /dev/null
+++ b/pw_hdlc/py/rpc_test.py
@@ -0,0 +1,294 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""device module unit tests"""
+
+from contextlib import contextmanager
+import logging
+import queue
+import threading
+import time
+import unittest
+
+from pw_hdlc.rpc import RpcClient, HdlcRpcClient, CancellableReader
+
+
+class QueueFile:
+ """A fake file object backed by a queue for testing."""
+
+ EOF = object()
+
+ def __init__(self):
+ # Operator puts; consumer gets
+ self._q = queue.Queue()
+
+ # Consumer side access only!
+ self._readbuf = b''
+ self._eof = False
+
+ ###############
+ # Consumer side
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *exc_info):
+ self.close()
+
+ def _read_from_buf(self, size: int) -> bytes:
+ data = self._readbuf[:size]
+ self._readbuf = self._readbuf[size:]
+ return data
+
+ def read(self, size: int = 1) -> bytes:
+ """Reads data from the queue"""
+ # First try to get buffered data
+ data = self._read_from_buf(size)
+ assert len(data) <= size
+ size -= len(data)
+
+ # if size == 0:
+ if data:
+ return data
+
+ # No more data in the buffer
+ assert not self._readbuf
+
+ if self._eof:
+ return data # may be empty
+
+ # Not enough in the buffer; block on the queue
+ item = self._q.get()
+
+ # NOTE: We can't call Queue.task_done() here because the reader hasn't
+ # actually *acted* on the read item yet.
+
+ # Queued data
+ if isinstance(item, bytes):
+ self._readbuf = item
+ return self._read_from_buf(size)
+
+ # Queued exception
+ if isinstance(item, Exception):
+ raise item
+
+ # Report EOF
+ if item is self.EOF:
+ self._eof = True
+ return data # may be empty
+
+ raise Exception('unexpected item type')
+
+ def write(self, data: bytes) -> None:
+ pass
+
+ #####################
+ # Weird middle ground
+
+ # It is a violation of most file-like object APIs for one thread to call
+ # close() while another thread is calling read(). The behavior is
+ # undefined.
+ #
+ # - On Linux, close() may wake up a select(), leaving the caller with a bad
+ # file descriptor (which could get reused!)
+ # - Or the read() could continue to block indefinitely.
+ #
+ # We choose to cause a subsequent/parallel read to receive an exception.
+ def close(self) -> None:
+ self.cause_read_exc(Exception('closed'))
+
+ ###############
+ # Operator side
+
+ def put_read_data(self, data: bytes) -> None:
+ self._q.put(data)
+
+ def cause_read_exc(self, exc: Exception) -> None:
+ self._q.put(exc)
+
+ def set_read_eof(self) -> None:
+ self._q.put(self.EOF)
+
+ def wait_for_drain(self, timeout=None) -> None:
+ """Wait for the queue to drain (be fully consumed).
+
+ Args:
+ timeout: The maximum time (in seconds) to wait, or wait forever
+ if None.
+
+ Raises:
+ TimeoutError: If timeout is given and has elapsed.
+ """
+ # It would be great to use Queue.join() here, but that requires the
+ # consumer to call Queue.task_done(), and we can't do that because
+ # the consumer of read() doesn't know anything about it.
+ # Instead, we poll. ¯\_(ツ)_/¯
+ start_time = time.time()
+ while not self._q.empty():
+ if timeout is not None:
+ elapsed = time.time() - start_time
+ if elapsed > timeout:
+ raise TimeoutError(f"Queue not empty after {elapsed} sec")
+ time.sleep(0.1)
+
+
+class QueueFileTest(unittest.TestCase):
+ """Test the QueueFile class"""
+
+ def test_read_data(self) -> None:
+ file = QueueFile()
+ file.put_read_data(b'hello')
+ self.assertEqual(file.read(5), b'hello')
+
+ def test_read_data_multi_read(self) -> None:
+ file = QueueFile()
+ file.put_read_data(b'helloworld')
+ self.assertEqual(file.read(5), b'hello')
+ self.assertEqual(file.read(5), b'world')
+
+ def test_read_data_multi_put(self) -> None:
+ file = QueueFile()
+ file.put_read_data(b'hello')
+ file.put_read_data(b'world')
+ self.assertEqual(file.read(5), b'hello')
+ self.assertEqual(file.read(5), b'world')
+
+ def test_read_eof(self) -> None:
+ file = QueueFile()
+ file.set_read_eof()
+ result = file.read(5)
+ self.assertEqual(result, b'')
+
+ def test_read_exception(self) -> None:
+ file = QueueFile()
+ message = 'test exception'
+ file.cause_read_exc(ValueError(message))
+ with self.assertRaisesRegex(ValueError, message):
+ file.read(5)
+
+ def test_wait_for_drain_works(self) -> None:
+ file = QueueFile()
+ file.put_read_data(b'hello')
+ file.read()
+ try:
+ # Timeout is arbitrary; will return immediately.
+ file.wait_for_drain(0.1)
+ except TimeoutError:
+ self.fail("wait_for_drain raised TimeoutError")
+
+ def test_wait_for_drain_raises(self) -> None:
+ file = QueueFile()
+ file.put_read_data(b'hello')
+ # don't read
+ with self.assertRaises(TimeoutError):
+ # Timeout is arbitrary; it will raise no matter what.
+ file.wait_for_drain(0.1)
+
+
+class Sentinel:
+ def __repr__(self):
+ return 'Sentinel'
+
+
+class _QueueReader(CancellableReader):
+ def cancel_read(self) -> None:
+ self._base_obj.close()
+
+
+def _get_client(file) -> RpcClient:
+ return HdlcRpcClient(
+ _QueueReader(file),
+ paths_or_modules=[],
+ channels=[],
+ )
+
+
+# This should take <10ms but we'll wait up to 1000x longer.
+_QUEUE_DRAIN_TIMEOUT = 10.0
+
+
+class HdlcRpcClientTest(unittest.TestCase):
+ """Tests the pw_hdlc.rpc.HdlcRpcClient class."""
+
+ # NOTE: There is no test here for stream EOF because Serial.read()
+ # can return an empty result if configured with timeout != None.
+ # The reader thread will continue in this case.
+
+ def test_clean_close_after_stream_close(self) -> None:
+ """Assert RpcClient closes cleanly when stream closes."""
+ # See b/293595266.
+ file = QueueFile()
+
+ with self.assert_no_hdlc_rpc_error_logs():
+ with file:
+ with _get_client(file):
+ # We want to make sure the reader thread is blocked on
+ # read() and doesn't exit immediately.
+ file.put_read_data(b'')
+ file.wait_for_drain(_QUEUE_DRAIN_TIMEOUT)
+
+ # RpcClient.__exit__ calls stop() on the reader thread, but
+ # it is blocked on file.read().
+
+ # QueueFile.close() is called, triggering an exception in the
+ # blocking read() (by implementation choice). The reader should
+ # handle it by *not* logging it and exiting immediately.
+
+ self.assert_no_background_threads_running()
+
+ def test_device_handles_read_exception(self) -> None:
+ """Assert RpcClient closes cleanly when read raises an exception."""
+ # See b/293595266.
+ file = QueueFile()
+
+ logger = logging.getLogger('pw_hdlc.rpc')
+ test_exc = Exception('boom')
+ with self.assertLogs(logger, level=logging.ERROR) as ctx:
+ with _get_client(file):
+ # Cause read() to raise an exception. The reader should
+ # handle it by logging it and exiting immediately.
+ file.cause_read_exc(test_exc)
+ file.wait_for_drain(_QUEUE_DRAIN_TIMEOUT)
+
+ # Assert one exception was raised
+ self.assertEqual(len(ctx.records), 1)
+ rec = ctx.records[0]
+ self.assertIsNotNone(rec.exc_info)
+ assert rec.exc_info is not None # for mypy
+ self.assertEqual(rec.exc_info[1], test_exc)
+
+ self.assert_no_background_threads_running()
+
+ @contextmanager
+ def assert_no_hdlc_rpc_error_logs(self):
+ logger = logging.getLogger('pw_hdlc.rpc')
+ sentinel = Sentinel()
+ with self.assertLogs(logger, level=logging.ERROR) as ctx:
+ # TODO: b/294861320 - use assertNoLogs() in Python 3.10+
+ # We actually want to assert there are no errors, but
+ # TestCase.assertNoLogs() is not available until Python 3.10.
+ # So we log one error to keep the test from failing and manually
+ # inspect the list of captured records.
+ logger.error(sentinel)
+
+ yield ctx
+
+ self.assertEqual([record.msg for record in ctx.records], [sentinel])
+
+ def assert_no_background_threads_running(self):
+ self.assertEqual(threading.enumerate(), [threading.current_thread()])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_hdlc/rpc_example/BUILD.bazel b/pw_hdlc/rpc_example/BUILD.bazel
index 29a86f94e..979430c73 100644
--- a/pw_hdlc/rpc_example/BUILD.bazel
+++ b/pw_hdlc/rpc_example/BUILD.bazel
@@ -23,7 +23,6 @@ pw_cc_binary(
"hdlc_rpc_server.cc",
"main.cc",
],
- tags = ["manual"], # TODO(b/241575924): Fix the Bazel build for rpc_example
deps = [
"//pw_hdlc",
"//pw_hdlc:pw_rpc",
diff --git a/pw_hdlc/rpc_example/docs.rst b/pw_hdlc/rpc_example/docs.rst
index b2555594a..e65e247dc 100644
--- a/pw_hdlc/rpc_example/docs.rst
+++ b/pw_hdlc/rpc_example/docs.rst
@@ -1,8 +1,8 @@
.. _module-pw_hdlc-rpc-example:
-=============================
-RPC over HDLC example project
-=============================
+======================================
+pw_hdlc: RPC over HDLC example project
+======================================
The :ref:`module-pw_hdlc` module includes an example of bringing up a
:ref:`module-pw_rpc` server that can be used to invoke RPCs. The example code
is located at ``pw_hdlc/rpc_example``. This section walks through invoking RPCs
@@ -27,9 +27,10 @@ Activate the Pigweed environment and run the default build.
.. code-block:: sh
- source activate.sh
- gn gen out
- ninja -C out
+ . ./activate.sh
+ pw package install nanopb
+ gn gen out --args='dir_pw_third_party_nanopb="//environment/packages/nanopb"'
+ ninja -C out
3. Flash the firmware image
===========================
@@ -41,38 +42,22 @@ you can flash the image with `OpenOCD <http://openocd.org>`_.
.. code-block:: sh
- openocd -f targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg \
- -c "program out/stm32f429i_disc1_debug/obj/pw_hdlc/rpc_example/bin/rpc_example.elf"
+ openocd -f \
+ targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg \
+ -c "program \
+ out/stm32f429i_disc1_debug/obj/pw_hdlc/rpc_example/bin/rpc_example.elf \
+ verify reset exit"
4. Invoke RPCs from in an interactive console
=============================================
-The RPC console uses `IPython <https://ipython.org>`_ to make a rich interactive
+The RPC console uses :ref:`module-pw_console` to make a rich interactive
console for working with pw_rpc. Run the RPC console with the following command,
replacing ``/dev/ttyACM0`` with the correct serial device for your board.
-.. code-block:: text
-
- $ python -m pw_system.console --device /dev/ttyACM0
-
- Console for interacting with pw_rpc over HDLC.
-
- To start the console, provide a serial port as the --device argument and paths
- or globs for .proto files that define the RPC services to support:
-
- python -m pw_system.console --device /dev/ttyUSB0 --proto-globs pw_rpc/echo.proto
-
- This starts an IPython console for communicating with the connected device. A
- few variables are predefined in the interactive console. These include:
-
- rpcs - used to invoke RPCs
- device - the serial device used for communication
- client - the pw_rpc.Client
-
- An example echo RPC command:
-
- rpcs.pw.rpc.EchoService.Echo(msg="hello!")
+.. code-block:: sh
- In [1]:
+ pw-system-console --no-rpc-logging --proto-globs pw_rpc/echo.proto \
+ --device /dev/ttyACM0
RPCs may be accessed through the predefined ``rpcs`` variable. RPCs are
organized by their protocol buffer package and RPC service, as defined in a
@@ -80,10 +65,10 @@ organized by their protocol buffer package and RPC service, as defined in a
is in the ``pw.rpc`` package. To invoke it synchronously, call
``rpcs.pw.rpc.EchoService.Echo``:
-.. code-block:: python
+.. code-block:: pycon
- In [1]: rpcs.pw.rpc.EchoService.Echo(msg="Your message here!")
- Out[1]: (<Status.OK: 0>, msg: "Your message here!")
+ >>> device.rpcs.pw.rpc.EchoService.Echo(msg='Hello, world!')
+ (Status.OK, pw.rpc.EchoMessage(msg='Hello, world!'))
5. Invoke RPCs with a script
============================
@@ -91,20 +76,25 @@ RPCs may also be invoked from Python scripts. Close the RPC console if it is
running, and execute the example script. Set the --device argument to the
serial port for your device.
+.. code-block:: sh
+
+ python pw_hdlc/rpc_example/example_script.py --device /dev/ttyACM0
+
+You should see this output:
+
.. code-block:: text
- $ pw_hdlc/rpc_example/example_script.py --device /dev/ttyACM0
- The status was Status.OK
- The payload was msg: "Hello"
+ The status was Status.OK
+ The payload was msg: "Hello"
- The device says: Goodbye!
+ The device says: Goodbye!
-------------------------
Local RPC example project
-------------------------
-This example is similar to the above example, except it use socket to
-connect server and client running on the host.
+This example is similar to the above example, except it uses a socket to
+connect a server and a client running on the host.
1. Build Pigweed
================
@@ -112,23 +102,55 @@ Activate the Pigweed environment and build the code.
.. code-block:: sh
- source activate.sh
- gn gen out
- pw watch
+ . ./activate.sh
+ pw package install nanopb
+ gn gen out --args='dir_pw_third_party_nanopb="//environment/packages/nanopb"'
+ ninja -C out
2. Start client side and server side
====================================
-Run pw_rpc client (i.e. use echo.proto)
+Run pw_rpc server in one terminal window.
.. code-block:: sh
- python -m pw_system.console path/to/echo.proto -s localhost:33000
+ ./out/pw_strict_host_clang_debug/obj/pw_hdlc/rpc_example/bin/rpc_example
-Run pw_rpc server
+In a separate activated terminal, run the ``pw-system-console`` RPC client with
+``--proto-globs`` set to ``pw_rpc/echo.proto``. Additional protos can be added
+if needed.
.. code-block:: sh
- out/pw_strict_host_clang_debug/obj/pw_hdlc/rpc_example/bin/rpc_example
+ pw-system-console --no-rpc-logging --proto-globs pw_rpc/echo.proto \
+ --socket-addr default
+
+.. tip::
+
+ The ``--socket-addr`` may be replaced with IP and port separated by a colon,
+ for example: ``127.0.0.1:33000``; or, if using a unix socket, the path to the
+ file follows "file:", for example ``file:/path/to/unix/socket``. Unix socket
+ Python support is pending `<https://bugs.python.org/issue33408>`_.
+
+.. tip::
+
+ The default RPC Channel ID (1) can be overriden with ``--channel-id``.
Then you can invoke RPCs from the interactive console on the client side.
+
+.. code-block:: pycon
+
+ >>> device.rpcs.pw.rpc.EchoService.Echo(msg='Hello, world!')
+ (Status.OK, pw.rpc.EchoMessage(msg='Hello, world!'))
+
+.. seealso::
+
+ - The :ref:`module-pw_console`
+ :bdg-ref-primary-line:`module-pw_console-user_guide` for more info on using
+ the the pw_console UI.
+
+ - The target docs for other RPC enabled application examples:
+
+ - :bdg-ref-primary-line:`target-host-device-simulator`
+ - :bdg-ref-primary-line:`target-raspberry-pi-pico-pw-system`
+ - :bdg-ref-primary-line:`target-stm32f429i-disc1-stm32cube`
diff --git a/pw_hdlc/rpc_example/example_script.py b/pw_hdlc/rpc_example/example_script.py
index 36a3cf85d..ec0bb898e 100755
--- a/pw_hdlc/rpc_example/example_script.py
+++ b/pw_hdlc/rpc_example/example_script.py
@@ -20,7 +20,11 @@ from pathlib import Path
import serial
-from pw_hdlc.rpc import HdlcRpcClient, default_channels
+from pw_hdlc.rpc import (
+ HdlcRpcClient,
+ default_channels,
+ SerialReader,
+)
# Point the script to the .proto file with our RPC services.
PROTO = Path(os.environ['PW_ROOT'], 'pw_rpc/echo.proto')
@@ -29,25 +33,25 @@ PROTO = Path(os.environ['PW_ROOT'], 'pw_rpc/echo.proto')
def script(device: str, baud: int) -> None:
# Set up a pw_rpc client that uses HDLC.
ser = serial.Serial(device, baud, timeout=0.01)
- client = HdlcRpcClient(
- lambda: ser.read(4096), [PROTO], default_channels(ser.write)
- )
-
- # Make a shortcut to the EchoService.
- echo_service = client.rpcs().pw.rpc.EchoService
+ reader = SerialReader(ser, 4096)
+ with reader:
+ client = HdlcRpcClient(reader, [PROTO], default_channels(ser.write))
+ with client:
+ # Make a shortcut to the EchoService.
+ echo_service = client.rpcs().pw.rpc.EchoService
- # Call some RPCs and check the results.
- status, payload = echo_service.Echo(msg='Hello')
+ # Call some RPCs and check the results.
+ status, payload = echo_service.Echo(msg='Hello')
- if status.ok():
- print('The status was', status)
- print('The payload was', payload)
- else:
- print('Uh oh, this RPC returned', status)
+ if status.ok():
+ print('The status was', status)
+ print('The payload was', payload)
+ else:
+ print('Uh oh, this RPC returned', status)
- status, payload = echo_service.Echo(msg='Goodbye!')
+ status, payload = echo_service.Echo(msg='Goodbye!')
- print('The device says:', payload.msg)
+ print('The device says:', payload.msg)
def main():
diff --git a/pw_hdlc/rpc_packets.cc b/pw_hdlc/rpc_packets.cc
index 6241dc4f2..bb3180353 100644
--- a/pw_hdlc/rpc_packets.cc
+++ b/pw_hdlc/rpc_packets.cc
@@ -32,7 +32,7 @@ Status ReadAndProcessPackets(rpc::Server& server,
Frame& frame = result.value();
if (frame.address() == rpc_address) {
server.ProcessPacket(frame.data())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
}
diff --git a/pw_hdlc/ts/crc32.ts b/pw_hdlc/ts/crc32.ts
new file mode 100644
index 000000000..6b20c17f3
--- /dev/null
+++ b/pw_hdlc/ts/crc32.ts
@@ -0,0 +1,79 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+/** Calculate CRC32 unsigned. Browser and NodeJS compatible version. */
+
+// Generated by pycrc (https://github.com/tpircher/pycrc) using
+// `./pycrc.py --algorithm=table-driven --model=crc-32 --generate=c`
+let TABLE: Array<number> | Int32Array = [
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
+ 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
+ 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
+ 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
+ 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
+ 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
+ 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
+ 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
+ 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
+ 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
+ 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
+ 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
+ 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
+ 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
+ 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
+ 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
+ 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
+ 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
+ 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
+ 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
+ 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
+ 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
+];
+
+if (typeof Int32Array !== 'undefined') {
+ TABLE = new Int32Array(TABLE);
+}
+
+export function crc32(bytes: Uint8Array): number {
+ let crc = 0xffffffff;
+ for (const byte of bytes) {
+ const tableIndex = (crc ^ byte) & 0xff;
+ const tableVal = TABLE[tableIndex];
+ if (tableVal === undefined)
+ throw new Error('tableIndex out of range 0-255');
+ crc = (crc >>> 8) ^ tableVal;
+ }
+ return crc ^ 0xffffffff;
+}
diff --git a/pw_hdlc/ts/decoder.ts b/pw_hdlc/ts/decoder.ts
index 881d2cb06..e0e302669 100644
--- a/pw_hdlc/ts/decoder.ts
+++ b/pw_hdlc/ts/decoder.ts
@@ -36,13 +36,13 @@ export class Frame {
status: FrameStatus;
address = -1;
- control: Uint8Array = new Uint8Array();
- data: Uint8Array = new Uint8Array();
+ control: Uint8Array = new Uint8Array(0);
+ data: Uint8Array = new Uint8Array(0);
constructor(
rawEncoded: Uint8Array,
rawDecoded: Uint8Array,
- status: FrameStatus = FrameStatus.OK
+ status: FrameStatus = FrameStatus.OK,
) {
this.rawEncoded = rawEncoded;
this.rawDecoded = rawDecoded;
@@ -80,8 +80,8 @@ enum DecoderState {
/** Decodes one or more HDLC frames from a stream of data. */
export class Decoder {
- private decodedData = new Uint8Array();
- private rawData = new Uint8Array();
+ private decodedData = new Uint8Array(0);
+ private rawData = new Uint8Array(0);
private state = DecoderState.INTERFRAME;
/**
@@ -115,7 +115,7 @@ export class Decoder {
console.warn(
'Failed to decode frame: %s; discarded %d bytes',
frame.status,
- frame.rawEncoded.length
+ frame.rawEncoded.length,
);
console.debug('Discarded data: %s', frame.rawEncoded);
}
@@ -128,7 +128,7 @@ export class Decoder {
}
const frameCrc = new DataView(data.slice(-4).buffer).getInt8(0);
const crc = new DataView(
- protocol.frameCheckSequence(data.slice(0, -4)).buffer
+ protocol.frameCheckSequence(data.slice(0, -4)).buffer,
).getInt8(0);
if (crc !== frameCrc) {
return FrameStatus.FCS_MISMATCH;
@@ -140,10 +140,10 @@ export class Decoder {
const frame = new Frame(
new Uint8Array(this.rawData),
new Uint8Array(this.decodedData),
- status
+ status,
);
- this.rawData = new Uint8Array();
- this.decodedData = new Uint8Array();
+ this.rawData = new Uint8Array(0);
+ this.decodedData = new Uint8Array(0);
return frame;
}
@@ -175,7 +175,7 @@ export class Decoder {
} else {
this.decodedData = util.concatenate(
this.decodedData,
- Uint8Array.of(byte)
+ Uint8Array.of(byte),
);
}
break;
@@ -188,7 +188,7 @@ export class Decoder {
this.state = DecoderState.FRAME;
this.decodedData = util.concatenate(
this.decodedData,
- Uint8Array.of(protocol.escape(byte))
+ Uint8Array.of(protocol.escape(byte)),
);
} else {
this.state = DecoderState.INTERFRAME;
diff --git a/pw_hdlc/ts/decoder_test.ts b/pw_hdlc/ts/decoder_test.ts
index 4a1996441..0e7a75b5e 100644
--- a/pw_hdlc/ts/decoder_test.ts
+++ b/pw_hdlc/ts/decoder_test.ts
@@ -14,7 +14,7 @@
/* eslint-env browser */
-import {Decoder} from './decoder';
+import { Decoder } from './decoder';
import * as protocol from './protocol';
import * as util from './util';
@@ -42,7 +42,7 @@ describe('Decoder', () => {
const expectedAddress = 128;
const frameData = withFlags(
- withFCS(textEncoder.encode('\x00\x03\x03123456789'))
+ withFCS(textEncoder.encode('\x00\x03\x03123456789')),
);
const frames = Array.from(decoder.process(frameData));
diff --git a/pw_hdlc/ts/encoder.ts b/pw_hdlc/ts/encoder.ts
index 1423356a3..68ac8baf0 100644
--- a/pw_hdlc/ts/encoder.ts
+++ b/pw_hdlc/ts/encoder.ts
@@ -18,7 +18,9 @@ import * as util from './util';
const FLAG_BYTE = Uint8Array.from([protocol.FLAG]);
export class Encoder {
- constructor() {}
+ constructor() {
+ // Do nothing.
+ }
/** Encodes an HDLC UI-frame with a CRC-32 frame check sequence. */
uiFrame(address: number, data: Uint8Array): Uint8Array {
diff --git a/pw_hdlc/ts/encoder_test.ts b/pw_hdlc/ts/encoder_test.ts
index 2780fa93e..23451bcfb 100644
--- a/pw_hdlc/ts/encoder_test.ts
+++ b/pw_hdlc/ts/encoder_test.ts
@@ -14,7 +14,7 @@
/* eslint-env browser */
-import {Encoder} from './encoder';
+import { Encoder } from './encoder';
import * as protocol from './protocol';
import * as util from './util';
@@ -40,34 +40,34 @@ describe('Encoder', () => {
it('creates frame for empty data', () => {
const data = textEncoder.encode('');
expect(encoder.uiFrame(0, data)).toEqual(
- withFlags(withFCS(new Uint8Array([0x01, 0x03])))
+ withFlags(withFCS(new Uint8Array([0x01, 0x03]))),
);
expect(encoder.uiFrame(0x1a, data)).toEqual(
- withFlags(withFCS(new Uint8Array([0x35, 0x03])))
+ withFlags(withFCS(new Uint8Array([0x35, 0x03]))),
);
expect(encoder.uiFrame(0x1a, data)).toEqual(
- withFlags(withFCS(textEncoder.encode('\x35\x03')))
+ withFlags(withFCS(textEncoder.encode('\x35\x03'))),
);
});
it('creates frame for one byte', () => {
const data = textEncoder.encode('A');
expect(encoder.uiFrame(0, data)).toEqual(
- withFlags(withFCS(textEncoder.encode('\x01\x03A')))
+ withFlags(withFCS(textEncoder.encode('\x01\x03A'))),
);
});
it('creates frame for multibyte data', () => {
const data = textEncoder.encode('123456789');
expect(encoder.uiFrame(0, data)).toEqual(
- withFlags(withFCS(textEncoder.encode('\x01\x03123456789')))
+ withFlags(withFCS(textEncoder.encode('\x01\x03123456789'))),
);
});
it('creates frame for multibyte data with address', () => {
const data = textEncoder.encode('123456789');
expect(encoder.uiFrame(128, data)).toEqual(
- withFlags(withFCS(textEncoder.encode('\x00\x03\x03123456789')))
+ withFlags(withFCS(textEncoder.encode('\x00\x03\x03123456789'))),
);
});
@@ -75,15 +75,30 @@ describe('Encoder', () => {
const data = textEncoder.encode('\x7d');
const expectedContent = util.concatenate(
textEncoder.encode('\x7d\x5d\x03\x7d\x5d'),
- protocol.frameCheckSequence(textEncoder.encode('\x7d\x03\x7d'))
+ protocol.frameCheckSequence(textEncoder.encode('\x7d\x03\x7d')),
);
expect(encoder.uiFrame(0x3e, data)).toEqual(withFlags(expectedContent));
const data2 = textEncoder.encode('A\x7e\x7dBC');
const expectedContent2 = util.concatenate(
textEncoder.encode('\x7d\x5d\x03A\x7d\x5e\x7d\x5dBC'),
- protocol.frameCheckSequence(textEncoder.encode('\x7d\x03A\x7e\x7dBC'))
+ protocol.frameCheckSequence(textEncoder.encode('\x7d\x03A\x7e\x7dBC')),
);
expect(encoder.uiFrame(0x3e, data2)).toEqual(withFlags(expectedContent2));
});
+
+ it('Computes frameCheckSequence correctly', () => {
+ expect(
+ protocol.frameCheckSequence(textEncoder.encode('\x7d\x03A\x7e\x7dBC')),
+ ).toEqual(new Uint8Array([195, 124, 135, 9]));
+ expect(
+ protocol.frameCheckSequence(textEncoder.encode('\x7d\x5d\x03\x7d\x5d')),
+ ).toEqual(new Uint8Array([183, 144, 10, 115]));
+ expect(
+ protocol.frameCheckSequence(textEncoder.encode('\x7d\x03\x7d')),
+ ).toEqual(new Uint8Array([83, 124, 241, 166]));
+ expect(
+ protocol.frameCheckSequence(textEncoder.encode('hello pigweed')),
+ ).toEqual(new Uint8Array([34, 22, 236, 2]));
+ });
});
diff --git a/pw_hdlc/ts/index.ts b/pw_hdlc/ts/index.ts
index 2a629a411..d0e77cc7f 100644
--- a/pw_hdlc/ts/index.ts
+++ b/pw_hdlc/ts/index.ts
@@ -12,5 +12,5 @@
// License for the specific language governing permissions and limitations under
// the License.
-export {Decoder, Frame, FrameStatus} from './decoder';
-export {Encoder} from './encoder';
+export { Decoder, Frame, FrameStatus } from './decoder';
+export { Encoder } from './encoder';
diff --git a/pw_hdlc/ts/protocol.ts b/pw_hdlc/ts/protocol.ts
index ef357a0d8..5b41dcfcd 100644
--- a/pw_hdlc/ts/protocol.ts
+++ b/pw_hdlc/ts/protocol.ts
@@ -13,8 +13,7 @@
// the License.
/** Low-level HDLC protocol features. */
-import {Buffer} from 'buffer';
-import {crc32} from 'crc';
+import { crc32 } from './crc32';
/** Special flag character for delimiting HDLC frames. */
export const FLAG = 0x7e;
@@ -47,9 +46,16 @@ function bitwiseOr(x: number, y: number) {
return highOr * highMask + lowOr;
}
+function getUint8ArraySubset(array: Uint8Array, start: number, end: number) {
+ const subset = new Uint8Array(array.buffer, start, end - start);
+ return subset;
+}
+
/** Calculates the CRC32 of |data| */
export function frameCheckSequence(data: Uint8Array): Uint8Array {
- const crc = crc32(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
+ const crc = crc32(
+ getUint8ArraySubset(data, data.byteOffset, data.byteLength),
+ );
const arr = new ArrayBuffer(4);
const view = new DataView(arr);
view.setUint32(0, crc, true); // litteEndian = true
@@ -64,6 +70,7 @@ export function escape(byte: number): number {
/** Encodes an HDLC address as a one-terminated LSB varint. */
export function encodeAddress(address: number): Uint8Array {
const byteList = [];
+ // eslint-disable-next-line no-constant-condition
while (true) {
byteList.push((address & 0x7f) << 1);
address >>= 7;
diff --git a/pw_hdlc/ts/util.ts b/pw_hdlc/ts/util.ts
index 34cb6a072..db991afa8 100644
--- a/pw_hdlc/ts/util.ts
+++ b/pw_hdlc/ts/util.ts
@@ -21,10 +21,10 @@
export function replace(
data: Uint8Array,
target: number,
- substitute: number[]
+ substitute: number[],
): Uint8Array {
const result: number[] = [];
- data.forEach(value => {
+ data.forEach((value) => {
if (value === target) {
result.push(...substitute);
} else {
@@ -38,11 +38,11 @@ export function replace(
export function concatenate(...byteList: Uint8Array[]): Uint8Array {
const length = byteList.reduce(
(accumulator, bytes) => accumulator + bytes.length,
- 0
+ 0,
);
const result = new Uint8Array(length);
let offset = 0;
- byteList.forEach(value => {
+ byteList.forEach((value) => {
result.set(value, offset);
offset += value.length;
});
diff --git a/pw_hex_dump/public/pw_hex_dump/hex_dump.h b/pw_hex_dump/public/pw_hex_dump/hex_dump.h
index b08ce0414..f8b76291a 100644
--- a/pw_hex_dump/public/pw_hex_dump/hex_dump.h
+++ b/pw_hex_dump/public/pw_hex_dump/hex_dump.h
@@ -99,15 +99,15 @@ class FormattedHexDumper {
FormattedHexDumper() = default;
FormattedHexDumper(span<char> dest) {
SetLineBuffer(dest)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
FormattedHexDumper(span<char> dest, Flags config_flags)
: flags(config_flags) {
SetLineBuffer(dest)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
- // TODO(b/234892215): Add iterator support.
+ // TODO: b/234892215 - Add iterator support.
// Set the destination buffer that the hex dumper will write to line-by-line.
//
diff --git a/pw_i2c/Android.bp b/pw_i2c/Android.bp
new file mode 100644
index 000000000..b10ae240e
--- /dev/null
+++ b/pw_i2c/Android.bp
@@ -0,0 +1,59 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+genrule {
+ name: "pw_i2c_proto_with_prefix",
+ defaults: ["pw_rpc_add_prefix_to_proto"],
+ srcs: [
+ "i2c.options",
+ "i2c.proto",
+ ],
+ out: [
+ "pw_i2c/i2c.options",
+ "pw_i2c/i2c.proto",
+ ],
+}
+
+genrule {
+ name: "pw_i2c_pwpb_rpc_header",
+ defaults: ["pw_rpc_generate_pwpb_rpc_header_with_prefix"],
+ srcs: [":pw_i2c_proto_with_prefix"],
+ out: ["pw_i2c/i2c.rpc.pwpb.h"],
+}
+
+genrule {
+ name: "pw_i2c_pwpb_proto_header",
+ defaults: ["pw_rpc_generate_pwpb_proto_with_prefix"],
+ srcs: [":pw_i2c_proto_with_prefix"],
+ out: ["pw_i2c/i2c.pwpb.h"],
+}
+
+cc_library_headers {
+ name: "pw_i2c_service_pwpb_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ generated_headers: [
+ "pw_i2c_pwpb_proto_header",
+ "pw_i2c_pwpb_rpc_header",
+ ],
+ export_generated_headers: [
+ "pw_i2c_pwpb_proto_header",
+ "pw_i2c_pwpb_rpc_header",
+ ],
+}
diff --git a/pw_i2c/BUILD.bazel b/pw_i2c/BUILD.bazel
index 98afc2230..a9d0af354 100644
--- a/pw_i2c/BUILD.bazel
+++ b/pw_i2c/BUILD.bazel
@@ -12,11 +12,17 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
"pw_cc_test",
)
+load(
+ "//pw_protobuf_compiler:pw_proto_library.bzl",
+ "pw_proto_filegroup",
+ "pw_proto_library",
+)
package(default_visibility = ["//visibility:public"])
@@ -111,6 +117,7 @@ pw_cc_library(
pw_cc_library(
name = "initiator_gmock",
+ testonly = True,
hdrs = [
"public/pw_i2c/initiator_gmock.h",
],
@@ -157,3 +164,46 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+pw_proto_filegroup(
+ name = "i2c_proto_and_options",
+ srcs = ["i2c.proto"],
+ options_files = ["i2c.options"],
+)
+
+proto_library(
+ name = "i2c_proto",
+ srcs = [":i2c_proto_and_options"],
+)
+
+pw_proto_library(
+ name = "i2c_cc",
+ deps = [":i2c_proto"],
+)
+
+pw_cc_library(
+ name = "i2c_service",
+ srcs = ["i2c_service.cc"],
+ hdrs = ["public/pw_i2c/i2c_service.h"],
+ includes = ["public"],
+ deps = [
+ ":address",
+ ":i2c_cc.pwpb_rpc",
+ ":initiator",
+ "//pw_chrono:system_clock",
+ "//pw_containers:vector",
+ "//pw_status",
+ ],
+)
+
+pw_cc_test(
+ name = "i2c_service_test",
+ srcs = ["i2c_service_test.cc"],
+ deps = [
+ ":i2c_service",
+ ":initiator_mock",
+ "//pw_containers:vector",
+ "//pw_rpc/pwpb:test_method_context",
+ "//pw_status",
+ ],
+)
diff --git a/pw_i2c/BUILD.gn b/pw_i2c/BUILD.gn
index 700f8edad..4eed45af1 100644
--- a/pw_i2c/BUILD.gn
+++ b/pw_i2c/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
@@ -70,6 +71,27 @@ pw_source_set("register_device") {
deps = [ "$dir_pw_assert" ]
}
+pw_proto_library("protos") {
+ sources = [ "i2c.proto" ]
+ inputs = [ "i2c.options" ]
+ prefix = "pw_i2c"
+}
+
+pw_source_set("i2c_service") {
+ public = [ "public/pw_i2c/i2c_service.h" ]
+ sources = [ "i2c_service.cc" ]
+ public_deps = [
+ ":protos.pwpb_rpc",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_i2c:initiator",
+ ]
+ deps = [
+ "$dir_pw_containers:vector",
+ "$dir_pw_i2c:address",
+ "$dir_pw_status",
+ ]
+}
+
pw_source_set("mock") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_i2c/initiator_mock.h" ]
@@ -102,6 +124,7 @@ pw_test_group("tests") {
":device_test",
":initiator_mock_test",
":register_device_test",
+ ":i2c_service_test",
]
}
@@ -138,6 +161,18 @@ pw_test("initiator_mock_test") {
]
}
+pw_test("i2c_service_test") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ sources = [ "i2c_service_test.cc" ]
+ deps = [
+ ":i2c_service",
+ "$dir_pw_containers:vector",
+ "$dir_pw_i2c:mock",
+ "$dir_pw_rpc/pwpb:test_method_context",
+ "$dir_pw_status",
+ ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_i2c/CMakeLists.txt b/pw_i2c/CMakeLists.txt
new file mode 100644
index 000000000..39e0c51a0
--- /dev/null
+++ b/pw_i2c/CMakeLists.txt
@@ -0,0 +1,185 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
+
+pw_add_library(pw_i2c.address STATIC
+ HEADERS
+ public/pw_i2c/address.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ SOURCES
+ address.cc
+)
+
+pw_add_library(pw_i2c.initiator INTERFACE
+ HEADERS
+ public/pw_i2c/initiator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_chrono.system_clock
+ pw_i2c.address
+ pw_status
+)
+
+pw_add_library(pw_i2c.device INTERFACE
+ HEADERS
+ public/pw_i2c/device.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_chrono.system_clock
+ pw_i2c.address
+ pw_i2c.initiator
+ pw_span
+ pw_status
+)
+
+pw_add_library(pw_i2c.register_device STATIC
+ HEADERS
+ public/pw_i2c/register_device.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_chrono.system_clock
+ pw_i2c.address
+ pw_i2c.device
+ pw_i2c.initiator
+ pw_result
+ pw_status
+ PRIVATE_DEPS
+ pw_assert
+ SOURCES
+ register_device.cc
+)
+
+pw_proto_library(pw_i2c.protos
+ SOURCES
+ i2c.proto
+ INPUTS
+ i2c.options
+ PREFIX
+ pw_i2c
+)
+
+pw_add_library(pw_i2c.i2c_service STATIC
+ HEADERS
+ public/pw_i2c/i2c_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_i2c.initiator
+ pw_i2c.protos.pwpb_rpc
+ PRIVATE_DEPS
+ pw_containers.vector
+ pw_i2c.address
+ pw_status
+ SOURCES
+ i2c_service.cc
+)
+
+pw_add_library(pw_i2c.mock STATIC
+ HEADERS
+ public/pw_i2c/initiator_mock.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_containers
+ pw_containers.to_array
+ pw_i2c.initiator
+ PRIVATE_DEPS
+ pw_assert
+ pw_unit_test
+ SOURCES
+ initiator_mock.cc
+)
+
+pw_add_library(pw_i2c.gmock INTERFACE
+ HEADERS
+ public/pw_i2c/initiator_gmock.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_i2c.initiator
+ pw_third_party.googletest
+)
+
+pw_add_test(pw_i2c.address_test
+ SOURCES
+ address_test.cc
+ PRIVATE_DEPS
+ pw_i2c.address
+ GROUPS
+ modules
+ pw_i2c
+)
+
+if(NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "")
+pw_add_test(pw_i2c.device_test
+ SOURCES
+ device_test.cc
+ PRIVATE_DEPS
+ pw_containers
+ pw_i2c.device
+ pw_i2c.mock
+ GROUPS
+ modules
+ pw_i2c
+)
+
+pw_add_test(pw_i2c.register_device_test
+ SOURCES
+ register_device_test.cc
+ PRIVATE_DEPS
+ pw_assert
+ pw_i2c.register_device
+ GROUPS
+ modules
+ pw_i2c
+)
+
+pw_add_test(pw_i2c.initiator_mock_test
+ SOURCES
+ initiator_mock_test.cc
+ PUBLIC_DEPS
+ pw_containers
+ pw_i2c.mock
+ GROUPS
+ modules
+ pw_i2c
+)
+
+pw_add_test(pw_i2c.i2c_service_test
+ SOURCES
+ i2c_service_test.cc
+ PUBLIC_DEPS
+ pw_containers.vector
+ pw_i2c.i2c_service
+ pw_i2c.mock
+ pw_rpc.test_utils
+ pw_status
+ GROUPS
+ modules
+ pw_i2c
+)
+endif()
diff --git a/pw_i2c/address_test.cc b/pw_i2c/address_test.cc
index 0d3a90e83..ce34bffb6 100644
--- a/pw_i2c/address_test.cc
+++ b/pw_i2c/address_test.cc
@@ -39,10 +39,10 @@ TEST(Address, TenBitRuntimeChecked) {
EXPECT_EQ(ten_bit.GetTenBit(), Address::kMaxTenBitAddress);
}
-// TODO(b/235289499): Verify assert behaviour when trying to get a 7bit address
+// TODO: b/235289499 - Verify assert behaviour when trying to get a 7bit address
// out of a 10bit address.
-// TODO(b/234882063): Add tests to ensure the constexpr constructors fail to
+// TODO: b/234882063 - Add tests to ensure the constexpr constructors fail to
// compile with invalid addresses once no-copmile tests are set up in Pigweed.
} // namespace pw::i2c
diff --git a/pw_i2c/docs.rst b/pw_i2c/docs.rst
index 24369689f..8af329c85 100644
--- a/pw_i2c/docs.rst
+++ b/pw_i2c/docs.rst
@@ -3,10 +3,9 @@
------
pw_i2c
------
-
.. warning::
- This module is under construction, not ready for use, and the documentation
- is incomplete.
+ This module is under construction, not ready for use, and the documentation
+ is incomplete.
pw_i2c contains interfaces and utility functions for using I2C.
@@ -15,13 +14,8 @@ Features
pw::i2c::Initiator
------------------
-.. inclusive-language: disable
-
-The common interface for initiating transactions with devices on an I2C bus.
-Other documentation sources may call this style of interface an I2C "master",
-"central" or "controller".
-
-.. inclusive-language: enable
+.. doxygenclass:: pw::i2c::Initiator
+ :members:
pw::i2c::Device
---------------
@@ -30,6 +24,13 @@ contains ``pw::i2c::Address`` and wraps the ``pw::i2c::Initiator`` API.
Common use case includes streaming arbitrary data (Read/Write). Only works
with devices with a single device address.
+.. note::
+ ``Device`` is intended to represent ownership of a specific responder.
+ Individual transactions are atomic (as described under ``Initiator``), but
+ there is no synchronization for sequences of transactions. Therefore, shared
+ access should be faciliated with higher level application abstractions. To
+ help enforce this, the ``Device`` object is only movable and not copyable.
+
pw::i2c::RegisterDevice
-----------------------
The common interface for interfacing with register devices. Contains methods
@@ -50,7 +51,7 @@ list. An example of this is shown below:
.. code-block:: cpp
using pw::i2c::Address;
- using pw::i2c::MakeExpectedTransactionlist;
+ using pw::i2c::MakeExpectedTransactionArray;
using pw::i2c::MockInitiator;
using pw::i2c::WriteTransaction;
using std::literals::chrono_literals::ms;
@@ -85,3 +86,42 @@ list. An example of this is shown below:
pw::i2c::GmockInitiator
-----------------------
gMock of Initiator used for testing and mocking out the Initiator.
+
+I2c Debug Service
+=================
+This module implements an I2C register access service for debugging and bringup.
+To use, provide it with a callback function that returns an ``Initiator`` for
+the specified ``bus_index``.
+
+Example invocations
+-------------------
+Using the pigweed console, you can invoke the service to perform an I2C read:
+
+.. code-block:: python
+
+ device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=0x22, register_address=b'\x0e', read_size=1)
+
+The above shows reading register 0x0e on a device located at
+I2C address 0x22.
+
+For responders that support 4 byte register width, you can specify as:
+
+.. code-block:: python
+
+ device.rpcs.pw.i2c.I2c.I2cRead(bus_index=0, target_address=<address>, register_address=b'\x00\x00\x00\x00', read_size=4)
+
+
+And similarly, for performing I2C write:
+
+.. code-block:: python
+
+ device.rpcs.pw.i2c.I2c.I2cWrite(bus_index=0, target_address=0x22,register_address=b'\x0e', value=b'\xbc')
+
+
+Similarly, multi-byte writes can also be specified with the bytes fields for
+`register_address` and `value`.
+
+I2C responders that require multi-byte access may expect a specific endianness.
+The order of bytes specified in the bytes field will match the order of bytes
+sent/received on the bus. Maximum supported value for multi-byte access is
+4 bytes.
diff --git a/pw_i2c/i2c.options b/pw_i2c/i2c.options
new file mode 100644
index 000000000..09e71ff60
--- /dev/null
+++ b/pw_i2c/i2c.options
@@ -0,0 +1,19 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+pw.i2c.I2cWriteRequest.register_address max_size:4
+pw.i2c.I2cWriteRequest.value max_size:32
+pw.i2c.I2cReadRequest.register_address max_size:4
+pw.i2c.I2cReadResponse.value max_size:32
+
diff --git a/pw_i2c/i2c.proto b/pw_i2c/i2c.proto
new file mode 100644
index 000000000..d68654b72
--- /dev/null
+++ b/pw_i2c/i2c.proto
@@ -0,0 +1,52 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+syntax = "proto3";
+
+package pw.i2c;
+
+message I2cWriteRequest {
+ // Which I2C initiator bus to communicate on.
+ uint32 bus_index = 1;
+ // 7-bit I2C target address to write to.
+ uint32 target_address = 2;
+ // Register address to write. Follow the endianness required by the
+ // responder for multi-byte address.
+ bytes register_address = 3;
+ // Value to write. Follow the endianness required by the responder.
+ bytes value = 4;
+}
+
+message I2cWriteResponse {}
+
+message I2cReadRequest {
+ // Which I2C initiator bus to communicate on.
+ uint32 bus_index = 1;
+ // 7-bit I2C target address to read from.
+ uint32 target_address = 2;
+ // Register address to write. Follow the endianness required by the
+ // responder for multi-byte address.
+ bytes register_address = 3;
+ // Expected number of bytes from the responder.
+ uint32 read_size = 4;
+}
+
+message I2cReadResponse {
+ bytes value = 1;
+}
+
+service I2c {
+ // Enable access to I2C devices implementing register read/writes.
+ rpc I2cWrite(I2cWriteRequest) returns (I2cWriteResponse) {}
+ rpc I2cRead(I2cReadRequest) returns (I2cReadResponse) {}
+}
diff --git a/pw_i2c/i2c_service.cc b/pw_i2c/i2c_service.cc
new file mode 100644
index 000000000..0dad6193b
--- /dev/null
+++ b/pw_i2c/i2c_service.cc
@@ -0,0 +1,84 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_i2c/i2c_service.h"
+
+#include <algorithm>
+#include <chrono>
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/vector.h"
+#include "pw_i2c/address.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_status/status.h"
+
+namespace pw::i2c {
+namespace {
+
+constexpr auto kI2cTimeout =
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(100));
+
+} // namespace
+
+void I2cService::I2cWrite(
+ const pwpb::I2cWriteRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::I2cWriteResponse::Message>& responder) {
+ Initiator* initiator = initiator_selector_(request.bus_index);
+ if (initiator == nullptr) {
+ responder.Finish({}, Status::InvalidArgument()).IgnoreError();
+ return;
+ }
+
+ constexpr auto kMaxWriteSize =
+ pwpb::I2cWriteRequest::kRegisterAddressMaxSize +
+ pwpb::I2cWriteRequest::kValueMaxSize;
+ Vector<std::byte, kMaxWriteSize> write_buffer{};
+ write_buffer.assign(std::begin(request.register_address),
+ std::end(request.register_address));
+ std::copy(std::begin(request.value),
+ std::end(request.value),
+ std::back_inserter(write_buffer));
+ auto result = initiator->WriteFor(
+ Address{static_cast<uint16_t>(request.target_address)},
+ write_buffer,
+ kI2cTimeout);
+ responder.Finish({}, result).IgnoreError();
+}
+
+void I2cService::I2cRead(
+ const pwpb::I2cReadRequest::Message& request,
+ rpc::PwpbUnaryResponder<pwpb::I2cReadResponse::Message>& responder) {
+ constexpr auto kMaxReadSize = pwpb::I2cReadResponse::kValueMaxSize;
+
+ Initiator* initiator = initiator_selector_(request.bus_index);
+ if (initiator == nullptr || request.read_size > kMaxReadSize) {
+ responder.Finish({}, Status::InvalidArgument()).IgnoreError();
+ return;
+ }
+ Vector<std::byte, kMaxReadSize> value{};
+ value.resize(request.read_size);
+ auto result = initiator->WriteReadFor(
+ Address{static_cast<uint16_t>(request.target_address)},
+ request.register_address,
+ {value.data(), value.size()},
+ kI2cTimeout);
+
+ if (result.ok()) {
+ responder.Finish({value}, OkStatus()).IgnoreError();
+ } else {
+ responder.Finish({}, result).IgnoreError();
+ }
+}
+
+} // namespace pw::i2c
diff --git a/pw_i2c/i2c_service_test.cc b/pw_i2c/i2c_service_test.cc
new file mode 100644
index 000000000..7f069da7a
--- /dev/null
+++ b/pw_i2c/i2c_service_test.cc
@@ -0,0 +1,244 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_i2c/i2c_service.h"
+
+#include <algorithm>
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_i2c/address.h"
+#include "pw_i2c/initiator.h"
+#include "pw_i2c/initiator_mock.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_status/status.h"
+
+namespace pw::i2c {
+namespace {
+
+auto MakeSingletonSelector(Initiator* initiator) {
+ return [initiator](size_t pos) { return pos == 0 ? initiator : nullptr; };
+}
+
+TEST(I2cServiceTest, I2cWriteSingleByteOk) {
+ Vector<std::byte, 4> register_addr{};
+ Vector<std::byte, 4> register_value{};
+ constexpr auto kExpectWrite = bytes::Array<0x02, 0x03>();
+ register_addr.push_back(kExpectWrite[0]);
+ register_value.push_back(kExpectWrite[1]);
+ auto transactions = MakeExpectedTransactionArray(
+ {Transaction(OkStatus(),
+ Address{0x01},
+ kExpectWrite,
+ {},
+ std::chrono::milliseconds(100))});
+ MockInitiator i2c_initiator(transactions);
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 0,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .value = register_value});
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), OkStatus());
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cWriteMultiByteOk) {
+ constexpr int kWriteSize = 4;
+ Vector<std::byte, kWriteSize> register_addr{};
+ Vector<std::byte, kWriteSize> register_value{};
+ constexpr auto kExpectWrite =
+ bytes::Array<0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09>();
+ std::copy(kExpectWrite.begin(),
+ kExpectWrite.begin() + kWriteSize,
+ std::back_inserter(register_addr));
+ std::copy(kExpectWrite.begin() + kWriteSize,
+ kExpectWrite.end(),
+ std::back_inserter(register_value));
+ auto transactions = MakeExpectedTransactionArray(
+ {Transaction(OkStatus(),
+ Address{0x01},
+ kExpectWrite,
+ {},
+ std::chrono::milliseconds(100))});
+ MockInitiator i2c_initiator(transactions);
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 0,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .value = register_value});
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), OkStatus());
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cWriteInvalidBusIndex) {
+ Vector<std::byte, 4> register_addr{};
+ Vector<std::byte, 4> register_value{};
+
+ MockInitiator i2c_initiator({});
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cWrite)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 1,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .value = register_value});
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), Status::InvalidArgument());
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadSingleByteOk) {
+ constexpr auto kExpectWrite = bytes::Array<0x02>();
+ constexpr auto kExpectRead = bytes::Array<0x03>();
+ Vector<std::byte, 4> register_addr{};
+ register_addr.push_back(kExpectWrite[0]);
+
+ auto transactions = MakeExpectedTransactionArray(
+ {Transaction(OkStatus(),
+ Address{0x01},
+ kExpectWrite,
+ kExpectRead,
+ std::chrono::milliseconds(100))});
+ MockInitiator i2c_initiator(transactions);
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 0,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .read_size = static_cast<uint32_t>(kExpectRead.size())});
+
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), OkStatus());
+ for (size_t i = 0; i < kExpectRead.size(); ++i) {
+ EXPECT_EQ(kExpectRead[i], context.response().value[i]);
+ }
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadMultiByteOk) {
+ constexpr auto kExpectWrite = bytes::Array<0x02, 0x04, 0x06, 0x08>();
+ constexpr auto kExpectRead = bytes::Array<0x03, 0x05, 0x07, 0x09>();
+ Vector<std::byte, 4> register_addr{};
+ std::copy(kExpectWrite.begin(),
+ kExpectWrite.end(),
+ std::back_inserter(register_addr));
+ auto transactions = MakeExpectedTransactionArray(
+ {Transaction(OkStatus(),
+ Address{0x01},
+ kExpectWrite,
+ kExpectRead,
+ std::chrono::milliseconds(100))});
+ MockInitiator i2c_initiator(transactions);
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 0,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .read_size = static_cast<uint32_t>(kExpectRead.size())});
+
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), OkStatus());
+ for (size_t i = 0; i < kExpectRead.size(); ++i) {
+ EXPECT_EQ(kExpectRead[i], context.response().value[i]);
+ }
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadMaxByteOk) {
+ constexpr auto kExpectWrite = bytes::Array<0x02, 0x04, 0x06, 0x08>();
+ constexpr auto kExpectRead = bytes::Array<0x03, 0x05, 0x07, 0x09>();
+ static_assert(sizeof(kExpectRead) <= pwpb::I2cReadResponse::kValueMaxSize);
+
+ Vector<std::byte, 4> register_addr{};
+ std::copy(kExpectWrite.begin(),
+ kExpectWrite.end(),
+ std::back_inserter(register_addr));
+ auto transactions = MakeExpectedTransactionArray(
+ {Transaction(OkStatus(),
+ Address{0x01},
+ kExpectWrite,
+ kExpectRead,
+ std::chrono::milliseconds(100))});
+ MockInitiator i2c_initiator(transactions);
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({
+ .bus_index = 0,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .read_size = sizeof(kExpectRead),
+ });
+
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), OkStatus());
+ // EXPECT_EQ(kExpectRead, context.response().value);
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadMultiByteOutOfBounds) {
+ pwpb::I2cReadResponse::Message response_message;
+ constexpr auto kMaxReadSize = response_message.value.max_size();
+ constexpr auto kRegisterAddr = bytes::Array<0x02, 0x04, 0x06, 0x08>();
+ Vector<std::byte, 4> register_addr{};
+ std::copy(kRegisterAddr.begin(),
+ kRegisterAddr.end(),
+ std::back_inserter(register_addr));
+ MockInitiator i2c_initiator({});
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 0,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .read_size = kMaxReadSize + 1});
+
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), Status::InvalidArgument());
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+TEST(I2cServiceTest, I2cReadInvalidBusIndex) {
+ Vector<std::byte, 4> register_addr{};
+ MockInitiator i2c_initiator({});
+
+ PW_PWPB_TEST_METHOD_CONTEXT(I2cService, I2cRead)
+ context{MakeSingletonSelector(&i2c_initiator)};
+
+ context.call({.bus_index = 1,
+ .target_address = 0x01,
+ .register_address = register_addr,
+ .read_size = 1});
+
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.status(), Status::InvalidArgument());
+ EXPECT_EQ(i2c_initiator.Finalize(), OkStatus());
+}
+
+} // namespace
+} // namespace pw::i2c
diff --git a/pw_i2c/public/pw_i2c/device.h b/pw_i2c/public/pw_i2c/device.h
index 41129e399..4a76ba57a 100644
--- a/pw_i2c/public/pw_i2c/device.h
+++ b/pw_i2c/public/pw_i2c/device.h
@@ -32,6 +32,7 @@ class Device {
: initiator_(initiator), device_address_(device_address) {}
Device(const Device&) = delete;
+ Device(Device&&) = default;
~Device() = default;
// Write bytes and then read bytes as either one atomic or two independent I2C
diff --git a/pw_i2c/public/pw_i2c/i2c_service.h b/pw_i2c/public/pw_i2c/i2c_service.h
new file mode 100644
index 000000000..7b029b1fd
--- /dev/null
+++ b/pw_i2c/public/pw_i2c/i2c_service.h
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <memory>
+#include <utility>
+
+#include "pw_function/function.h"
+#include "pw_i2c/i2c.pwpb.h"
+#include "pw_i2c/i2c.rpc.pwpb.h"
+#include "pw_i2c/initiator.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+
+namespace pw::i2c {
+
+// RPC service to perform I2C transactions.
+class I2cService final : public pw_rpc::pwpb::I2c::Service<I2cService> {
+ public:
+ // Callback which returns an initiator for the given position or nullptr if
+ // the position not valid on this device.
+ using InitiatorSelector = pw::Function<Initiator*(size_t pos)>;
+
+ explicit I2cService(InitiatorSelector&& initiator_selector)
+ : initiator_selector_(std::move(initiator_selector)) {}
+
+ void I2cWrite(
+ const pwpb::I2cWriteRequest::Message& request,
+ pw::rpc::PwpbUnaryResponder<pwpb::I2cWriteResponse::Message>& responder);
+ void I2cRead(
+ const pwpb::I2cReadRequest::Message& request,
+ pw::rpc::PwpbUnaryResponder<pwpb::I2cReadResponse::Message>& responder);
+
+ private:
+ InitiatorSelector initiator_selector_;
+};
+
+} // namespace pw::i2c
diff --git a/pw_i2c/public/pw_i2c/initiator.h b/pw_i2c/public/pw_i2c/initiator.h
index 8b09a67e0..7d9d6ddde 100644
--- a/pw_i2c/public/pw_i2c/initiator.h
+++ b/pw_i2c/public/pw_i2c/initiator.h
@@ -22,18 +22,26 @@
namespace pw::i2c {
-// Base driver interface for I2C initiating I2C transactions in a thread safe
-// manner. Other documentation sources may call this style of interface an I2C
-// "master", "central" or "controller". // inclusive-language: ignore
-//
-// The Initiator is not required to support 10bit addressing. If only 7bit
-// addressing is supported, the Initiator will assert when given an address
-// which is out of 7bit address range.
-//
-// The implementer of this pure virtual interface is responsible for ensuring
-// thread safety and enabling functionality such as initialization,
-// configuration, enabling/disabling, unsticking SDA, and detecting device
-// address registration collisions.
+/// @brief The common, base driver interface for initiating thread-safe
+/// transactions with devices on an I2C bus. Other documentation may call this
+/// style of interface an I2C "master", <!-- inclusive-language: disable -->
+/// "central", or "controller".
+///
+/// `Initiator` isn't required to support 10-bit addressing. If only 7-bit
+/// addressing is supported, `Initiator` asserts when given an address
+/// that is out of 7-bit address range.
+///
+/// The implementer of this pure virtual interface is responsible for ensuring
+/// thread safety and enabling functionality such as initialization,
+/// configuration, enabling/disabling, unsticking SDA, and detecting device
+/// address registration collisions.
+///
+/// @note `Initiator` uses internal synchronization, so it's safe to
+/// initiate transactions from multiple threads. However, write+read
+/// transactions may not be atomic with multiple controllers on the bus.
+/// Furthermore, devices may require specific sequences of transactions, and
+/// application logic must provide the synchronization to execute these
+/// sequences correctly.
class Initiator {
public:
virtual ~Initiator() = default;
diff --git a/pw_i2c/public/pw_i2c/register_device.h b/pw_i2c/public/pw_i2c/register_device.h
index 0f9b4df39..b71683c83 100644
--- a/pw_i2c/public/pw_i2c/register_device.h
+++ b/pw_i2c/public/pw_i2c/register_device.h
@@ -378,7 +378,7 @@ inline Status RegisterDevice::ReadRegisters32(
PW_TRY(
ReadRegisters(register_address, as_writable_bytes(return_data), timeout));
- // TODO(b/185952662): Extend endian in pw_byte to support this conversion
+ // TODO: b/185952662 - Extend endian in pw_byte to support this conversion
// as optimization.
// Post process endian information.
for (uint32_t& register_value : return_data) {
diff --git a/pw_i2c/register_device.cc b/pw_i2c/register_device.cc
index 9c3b15e29..055ddcc3b 100644
--- a/pw_i2c/register_device.cc
+++ b/pw_i2c/register_device.cc
@@ -28,7 +28,7 @@ void PutRegisterAddressInByteBuilder(
const uint32_t register_address,
const endian order,
RegisterAddressSize register_address_size) {
- // TODO(b/185952662): Simplify the call site by extending the byte builder
+ // TODO: b/185952662 - Simplify the call site by extending the byte builder
// and endian API.
switch (register_address_size) {
case RegisterAddressSize::k1Byte:
@@ -112,7 +112,7 @@ Status RegisterDevice::WriteRegisters(const uint32_t register_address,
case 4:
PutRegisterData32InByteBuilder(builder, register_data, data_order_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
break;
default:
diff --git a/pw_i2c_linux/BUILD.bazel b/pw_i2c_linux/BUILD.bazel
new file mode 100644
index 000000000..2cd7cf96a
--- /dev/null
+++ b/pw_i2c_linux/BUILD.bazel
@@ -0,0 +1,57 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "initiator",
+ srcs = [
+ "initiator.cc",
+ ],
+ hdrs = [
+ "public/pw_i2c_linux/initiator.h",
+ ],
+ includes = ["public"],
+ target_compatible_with = ["@platforms//os:linux"],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_chrono:system_clock",
+ "//pw_i2c:address",
+ "//pw_i2c:initiator",
+ "//pw_log",
+ "//pw_status",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:timed_mutex",
+ ],
+)
+
+pw_cc_test(
+ name = "initiator_test",
+ srcs = [
+ "initiator_test.cc",
+ ],
+ deps = [
+ ":initiator",
+ "//pw_i2c:initiator",
+ ],
+)
diff --git a/pw_i2c_linux/BUILD.gn b/pw_i2c_linux/BUILD.gn
new file mode 100644
index 000000000..573a2e002
--- /dev/null
+++ b/pw_i2c_linux/BUILD.gn
@@ -0,0 +1,61 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+}
+
+pw_source_set("initiator") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_i2c_linux/initiator.h" ]
+ public_deps = [
+ "$dir_pw_bytes",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_i2c:address",
+ "$dir_pw_i2c:initiator",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:timed_mutex",
+ ]
+ sources = [ "initiator.cc" ]
+ deps = [
+ "$dir_pw_assert",
+ "$dir_pw_log",
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":initiator_test" ]
+}
+
+pw_test("initiator_test") {
+ enable_if = current_os == "linux"
+ sources = [ "initiator_test.cc" ]
+ deps = [
+ ":initiator",
+ "$dir_pw_i2c:initiator",
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_i2c_linux/OWNERS b/pw_i2c_linux/OWNERS
new file mode 100644
index 000000000..dcdb6bdf8
--- /dev/null
+++ b/pw_i2c_linux/OWNERS
@@ -0,0 +1 @@
+amarkov@google.com
diff --git a/pw_i2c_linux/docs.rst b/pw_i2c_linux/docs.rst
new file mode 100644
index 000000000..9659d9bb6
--- /dev/null
+++ b/pw_i2c_linux/docs.rst
@@ -0,0 +1,84 @@
+.. _module-pw_i2c_linux:
+
+---------------------
+pw_i2c_linux
+---------------------
+``pw_i2c_linux`` implements the ``pw_i2c`` interface using the Linux userspace
+``i2c-dev`` driver. Transfers are executed using blocking ``ioctl`` calls.
+Write+read transactions are implemented atomically using a single system call,
+and a retry mechanism is used to support bus arbitration between multiple
+controllers.
+
+C++
+===
+.. doxygenclass:: pw::i2c::LinuxInitiator
+ :members:
+
+Examples
+========
+A simple example illustrating the usage:
+
+.. code-block:: C++
+
+ #include "pw_i2c/address.h"
+ #include "pw_i2c/device.h"
+ #include "pw_i2c_linux/initiator.h"
+ #include "pw_log/log.h"
+ #include "pw_result/result.h"
+
+ constexpr auto kBusPath = "/dev/i2c-0";
+ constexpr auto kAddress = pw::i2c::Address::SevenBit<0x42>();
+
+ pw::Result<int> result = pw::i2c::LinuxInitiator::OpenI2cBus(kBusPath);
+ if (!result.ok()) {
+ PW_LOG_ERROR("Failed to open I2C bus [%s]", kBusPath);
+ return result.status();
+ }
+ pw::i2c::LinuxInitiator initiator(*result);
+ pw::i2c::Device device(initiator, address);
+ // Use device to talk to address.
+
+In real-world use cases, you may want to create an initiator singleton. This
+can be done by initializing a function-local static variable with a lambda:
+
+.. code-block:: C++
+
+ #include <functional>
+
+ #include "pw_i2c/address.h"
+ #include "pw_i2c/device.h"
+ #include "pw_i2c/initiator.h"
+ #include "pw_i2c_linux/initiator.h"
+ #include "pw_log/log.h"
+ #include "pw_result/result.h"
+ #include "pw_status/status.h"
+
+ // Open the I2C bus and return an initiator singleton.
+ pw::i2c::Initiator* GetInitiator() {
+ static constexpr auto kBusPath = "/dev/i2c-0";
+ static auto* initiator = std::invoke([]() -> pw::i2c::Initiator* {
+ pw::Result<int> result = pw::i2c::LinuxInitiator::OpenI2cBus(kBusPath);
+ if (!result.ok()) {
+ PW_LOG_ERROR("Failed to open I2C bus [%s]", kBusPath);
+ return nullptr;
+ }
+ return new pw::i2c::Initiator(*result);
+ });
+ return initiator;
+ }
+
+ // Use the initiator from anywhere.
+ constexpr auto kAddress = pw::i2c::Address::SevenBit<0x42>();
+ auto* initiator = GetInitiator();
+ if (initiator == nullptr) {
+ PW_LOG_ERROR("I2C initiator unavailable");
+ return pw::Status::Internal();
+ }
+ pw::i2c::Device device(*initiator, address);
+ // Use device to talk to address.
+
+Caveats
+=======
+Only 7-bit addresses are supported right now, but it should be possible to add
+support for 10-bit addresses with minimal changes - as long as the Linux driver
+supports 10-bit addresses.
diff --git a/pw_i2c_linux/initiator.cc b/pw_i2c_linux/initiator.cc
new file mode 100644
index 000000000..ce92bf963
--- /dev/null
+++ b/pw_i2c_linux/initiator.cc
@@ -0,0 +1,235 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#define PW_LOG_MODULE_NAME "I2C"
+
+#include "pw_i2c_linux/initiator.h"
+
+#include <fcntl.h>
+#include <linux/i2c-dev.h>
+#include <linux/i2c.h>
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <array>
+#include <cerrno>
+#include <cinttypes>
+#include <mutex>
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_i2c/address.h"
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+
+namespace pw::i2c {
+namespace {
+
+using ::pw::chrono::SystemClock;
+
+// Returns an appropriate status code for the given fault_code (i.e. `errno`).
+// For unexpected fault codes, logs messages to aid in debugging.
+// Reference: https://www.kernel.org/doc/html/latest/i2c/fault-codes.html
+Status PwStatusAndLog(int i2c_errno, uint8_t device_address) {
+ switch (i2c_errno) {
+ case EAGAIN:
+ // Lost arbitration on a multi-controller bus.
+ // This is a normal condition on multi-controller busses.
+ return Status::Aborted();
+ case ENOENT:
+ case ENODEV:
+ case ENXIO:
+ case EREMOTEIO:
+ // Generally indicates the device is unavailable or faulty.
+ // This is a normal condition when incorrect address is specified.
+ //
+ // Return Unavailable instead of NotFound as per the requirements of
+ // pw::i2c::Initiator.
+ PW_LOG_INFO("I2C device unavailable at address 0x%" PRIx8,
+ device_address);
+ return Status::Unavailable();
+ case ESHUTDOWN:
+ // It's not really clear what would cause a bus to be "suspended".
+ PW_LOG_WARN("I2C bus is suspended");
+ return Status::FailedPrecondition();
+ default:
+ // All other errors are unexpected and don't have a well-defined code.
+ PW_LOG_ERROR("I2C transaction failed for address 0x%" PRIx8 ": errno=%d",
+ device_address,
+ i2c_errno);
+ return Status::Unknown();
+ }
+}
+
+} // namespace
+
+// Open the file at the given path and validate that it is a valid bus device.
+Result<int> LinuxInitiator::OpenI2cBus(const char* bus_path) {
+ int fd = open(bus_path, O_RDWR);
+ if (fd < 0) {
+ PW_LOG_ERROR(
+ "Unable to open I2C bus device [%s]: errno=%d", bus_path, errno);
+ return Status::InvalidArgument();
+ }
+
+ // Verify that the bus supports full I2C functionality.
+ unsigned long functionality = 0;
+ if (ioctl(fd, I2C_FUNCS, &functionality) != 0) {
+ PW_LOG_ERROR("Unable to read I2C functionality for bus [%s]: errno=%d",
+ bus_path,
+ errno);
+ close(fd);
+ return Status::InvalidArgument();
+ }
+
+ if ((functionality & I2C_FUNC_I2C) == 0) {
+ PW_LOG_ERROR("I2C bus [%s] does not support full I2C functionality",
+ bus_path);
+ close(fd);
+ return Status::InvalidArgument();
+ }
+ return fd;
+}
+
+LinuxInitiator::LinuxInitiator(int fd) : fd_(fd) { PW_DCHECK(fd_ >= 0); }
+
+LinuxInitiator::~LinuxInitiator() {
+ PW_DCHECK(fd_ >= 0);
+ close(fd_);
+}
+
+Status LinuxInitiator::DoWriteReadFor(Address device_address,
+ ConstByteSpan tx_buffer,
+ ByteSpan rx_buffer,
+ SystemClock::duration timeout) {
+ auto start_time = SystemClock::now();
+
+ // Validate arguments.
+ const auto address = device_address.GetSevenBit();
+ if (tx_buffer.empty() && rx_buffer.empty()) {
+ PW_LOG_ERROR("At least one of tx_buffer or rx_buffer must be not empty");
+ return Status::InvalidArgument();
+ }
+
+ // Try to acquire access to the bus.
+ if (!mutex_.try_lock_for(timeout)) {
+ return Status::DeadlineExceeded();
+ }
+ std::lock_guard lock(mutex_, std::adopt_lock);
+ const auto elapsed = SystemClock::now() - start_time;
+ return DoWriteReadForLocked(address, tx_buffer, rx_buffer, timeout - elapsed);
+}
+
+// Perform an I2C write, read, or combined write+read transaction.
+//
+// Preconditions:
+// - `this->mutex_` is acquired
+// - `this->fd_` is open for read/write and supports full I2C functionality.
+// - `address` is a 7-bit device address
+// - At least one of `tx_buffer` or `rx_buffer` is not empty.
+//
+// The transaction will be retried if we can't get access to the bus, until
+// the timeout is reached. There will be no retries if `timeout` is zero or
+// negative.
+Status LinuxInitiator::DoWriteReadForLocked(
+ uint8_t address,
+ ConstByteSpan tx_buffer,
+ ByteSpan rx_buffer,
+ chrono::SystemClock::duration timeout) {
+ const auto start_time = SystemClock::now();
+
+ // Prepare messages for either a read, write, or combined transaction.
+ // Populate `ioctl_data` with either one or two `i2c_msg` operations.
+ // Use the `messages` buffer to store the operations.
+ i2c_rdwr_ioctl_data ioctl_data{};
+ std::array<i2c_msg, 2> messages{};
+ if (!tx_buffer.empty() && rx_buffer.empty()) {
+ messages[0] = i2c_msg{
+ .addr = address,
+ .flags = 0, // Read transaction
+ .len = static_cast<uint16_t>(tx_buffer.size()),
+ .buf = reinterpret_cast<uint8_t*>(
+ const_cast<std::byte*>(tx_buffer.data())), // NOLINT: read-only
+ };
+ ioctl_data = {
+ .msgs = messages.data(),
+ .nmsgs = 1,
+ };
+ } else if (!rx_buffer.empty() && tx_buffer.empty()) {
+ messages[0] = i2c_msg{
+ .addr = address,
+ .flags = I2C_M_RD,
+ .len = static_cast<uint16_t>(rx_buffer.size()),
+ .buf = reinterpret_cast<uint8_t*>(rx_buffer.data()),
+ };
+ ioctl_data = {
+ .msgs = messages.data(),
+ .nmsgs = 1,
+ };
+ } else {
+ // DoWriteReadFor already checks that at least one buffer has data.
+ // This is just an internal consistency check.
+ PW_DCHECK(!rx_buffer.empty() && !tx_buffer.empty());
+ messages[0] = i2c_msg{
+ .addr = address,
+ .flags = 0, // Read transaction
+ .len = static_cast<uint16_t>(tx_buffer.size()),
+ .buf = reinterpret_cast<uint8_t*>(
+ const_cast<std::byte*>(tx_buffer.data())), // NOLINT: read-only
+ };
+ messages[1] = i2c_msg{
+ .addr = address,
+ .flags = I2C_M_RD,
+ .len = static_cast<uint16_t>(rx_buffer.size()),
+ .buf = reinterpret_cast<uint8_t*>(rx_buffer.data()),
+ };
+ ioctl_data = {
+ .msgs = messages.data(),
+ .nmsgs = 2,
+ };
+ }
+ PW_LOG_DEBUG("Attempting I2C transaction with %" PRIu32 " operations",
+ ioctl_data.nmsgs);
+
+ // Attempt the transaction. If we can't get exclusive access to the bus,
+ // then keep trying until we run out of time.
+ do {
+ if (ioctl(fd_, I2C_RDWR, &ioctl_data) < 0) {
+ Status status = PwStatusAndLog(errno, address);
+ if (status == Status::Aborted()) {
+ // Lost arbitration and need to try again.
+ PW_LOG_DEBUG("Retrying I2C transaction");
+ continue;
+ }
+ return status;
+ }
+ return OkStatus();
+ } while (SystemClock::now() - start_time < timeout);
+
+ // Attempt transaction one last time. This thread may have been suspended
+ // after the last attempt, but before the timeout actually expired. The
+ // timeout is meant to be a minimum time period.
+ if (ioctl(fd_, I2C_RDWR, &ioctl_data) < 0) {
+ Status status = PwStatusAndLog(errno, address);
+ if (status == Status::Aborted()) {
+ PW_LOG_INFO("Timeout waiting for I2C bus access");
+ return Status::DeadlineExceeded();
+ }
+ return status;
+ }
+ return OkStatus();
+}
+
+} // namespace pw::i2c
diff --git a/pw_i2c_linux/initiator_test.cc b/pw_i2c_linux/initiator_test.cc
new file mode 100644
index 000000000..fe6dac260
--- /dev/null
+++ b/pw_i2c_linux/initiator_test.cc
@@ -0,0 +1,44 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_i2c_linux/initiator.h"
+
+#include <type_traits>
+
+#include "gtest/gtest.h"
+#include "pw_i2c/initiator.h"
+
+namespace pw::i2c {
+namespace {
+
+// A bus path that doesn't exist. Linux only supports 255 minor numbers.
+constexpr auto kBusPathMissing = "/dev/i2c-256";
+// A bus path that points at something that is not an I2C bus.
+constexpr auto kBusPathInvalid = "/dev/null";
+
+TEST(LinuxInitiatorTest, TestOpenMissingFileFails) {
+ EXPECT_EQ(LinuxInitiator::OpenI2cBus(kBusPathMissing).status(),
+ Status::InvalidArgument());
+}
+
+TEST(LinuxInitiatorTest, TestOpenInvalidFileFails) {
+ EXPECT_EQ(LinuxInitiator::OpenI2cBus(kBusPathInvalid).status(),
+ Status::InvalidArgument());
+}
+
+// Check that LinuxInitiator implements Initiator.
+static_assert(std::is_assignable_v<Initiator&, LinuxInitiator&>);
+
+} // namespace
+} // namespace pw::i2c \ No newline at end of file
diff --git a/pw_i2c_linux/public/pw_i2c_linux/initiator.h b/pw_i2c_linux/public/pw_i2c_linux/initiator.h
new file mode 100644
index 000000000..2fe5d6607
--- /dev/null
+++ b/pw_i2c_linux/public/pw_i2c_linux/initiator.h
@@ -0,0 +1,96 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_bytes/span.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_i2c/address.h"
+#include "pw_i2c/initiator.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/timed_mutex.h"
+
+namespace pw::i2c {
+
+/// Initiator interface implementation using the Linux userspace i2c-dev driver.
+///
+/// Takes exclusive control of an I2C bus device (ex. "/dev/i2c-0"). The user is
+/// responsible to open the device node prior to creating the initiator. The
+/// file descriptor is closed when the initiator object is destroyed.
+///
+/// The bus device must support the full I2C functionality. Users of the class
+/// are encouraged to use the `OpenI2cBus` helper to ensure the bus is valid.
+///
+/// Access to the bus is guarded by an internal mutex, so this initiator can be
+/// safely used from multiple threads.
+///
+class LinuxInitiator final : public Initiator {
+ public:
+ /// Open an I2C bus and validate that full I2C functionality is supported.
+ ///
+ /// @param[in] bus_path Path to the I2C bus device node.
+ ///
+ /// @retval OK The device node was opened successfully.
+ /// @retval InvalidArgument Failed to open the device node or to validate I2C
+ /// functionality.
+ ///
+ /// @return The open file descriptor on success.
+ ///
+ static Result<int> OpenI2cBus(const char* bus_path);
+
+ /// Construct an instantiator using an open file descriptor.
+ /// The file descriptor is closed during destruction.
+ ///
+ /// @param[in] fd Valid file descriptor for an I2C device node.
+ ///
+ LinuxInitiator(int fd);
+
+ LinuxInitiator(const LinuxInitiator&) = delete;
+ LinuxInitiator& operator=(const LinuxInitiator&) = delete;
+
+ ~LinuxInitiator() override;
+
+ private:
+ /// Implement pw::i2c::Initiator with the following additional requriements:
+ /// - Asserts that `device_address` is a 7-bit address.
+ /// - At least one of `tx_buffer` or `rx_buffer` must be not empty.
+ /// Otherwise, returns InvalidArgument.
+ ///
+ /// @note
+ /// The timeout is used both for getting an exclusive lock on the initiator
+ /// and for getting exclusive use of a multi-controller bus. If the timeout
+ /// is zero or negative, the transaction will only execute if there is no
+ /// contention at either level.
+ ///
+ Status DoWriteReadFor(Address device_address,
+ ConstByteSpan tx_buffer,
+ ByteSpan rx_buffer,
+ chrono::SystemClock::duration timeout) override
+ PW_LOCKS_EXCLUDED(mutex_);
+
+ Status DoWriteReadForLocked(uint8_t address,
+ ConstByteSpan tx_buffer,
+ ByteSpan rx_buffer,
+ chrono::SystemClock::duration timeout)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// The file descriptor for the i2c-dev device representing this bus.
+ const int fd_;
+
+ /// This mutex is used to synchronize access across multiple retries.
+ sync::TimedMutex mutex_;
+};
+
+} // namespace pw::i2c
diff --git a/pw_i2c_mcuxpresso/BUILD.bazel b/pw_i2c_mcuxpresso/BUILD.bazel
index 9f82586f4..f71be2d04 100644
--- a/pw_i2c_mcuxpresso/BUILD.bazel
+++ b/pw_i2c_mcuxpresso/BUILD.bazel
@@ -14,6 +14,7 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
+ "pw_cc_test",
)
package(default_visibility = ["//visibility:public"])
@@ -25,15 +26,21 @@ pw_cc_library(
srcs = ["initiator.cc"],
hdrs = ["public/pw_i2c_mcuxpresso/initiator.h"],
includes = ["public"],
- # TODO(b/259153338): Get this to build.
- tags = ["manual"],
deps = [
"//pw_chrono:system_clock",
+ "//pw_i2c:address",
"//pw_i2c:initiator",
"//pw_status",
"//pw_sync:interrupt_spin_lock",
"//pw_sync:lock_annotations",
"//pw_sync:mutex",
"//pw_sync:timed_thread_notification",
+ "@pigweed//targets:mcuxpresso_sdk",
],
)
+
+pw_cc_test(
+ name = "initiator_test",
+ srcs = ["initiator_test.cc"],
+ deps = [":pw_i2c_mcuxpresso"],
+)
diff --git a/pw_i2c_mcuxpresso/BUILD.gn b/pw_i2c_mcuxpresso/BUILD.gn
index 3ff44c6bb..59b361759 100644
--- a/pw_i2c_mcuxpresso/BUILD.gn
+++ b/pw_i2c_mcuxpresso/BUILD.gn
@@ -40,9 +40,23 @@ if (pw_third_party_mcuxpresso_SDK != "") {
}
}
+pw_test("initiator_test") {
+ enable_if =
+ pw_third_party_mcuxpresso_SDK == "//targets/mimxrt595_evk_freertos:sdk" &&
+ (pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_debug" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_size_optimized" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_speed_optimized")
+ sources = [ "initiator_test.cc" ]
+ deps = [
+ ":pw_i2c_mcuxpresso",
+ "//targets/mimxrt595_evk_freertos:sdk",
+ ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
pw_test_group("tests") {
+ tests = [ ":initiator_test" ]
}
diff --git a/pw_i2c_mcuxpresso/docs.rst b/pw_i2c_mcuxpresso/docs.rst
index 449f6e266..2211258f0 100644
--- a/pw_i2c_mcuxpresso/docs.rst
+++ b/pw_i2c_mcuxpresso/docs.rst
@@ -12,15 +12,27 @@ non-blocking driver API.
Setup
=====
-
This module requires following setup:
- 1. Use ``pw_build_mcuxpresso`` to create a ``pw_source_set`` for an
- MCUXpresso SDK.
- 2. Include the i2c driver component in this SDK definition.
- 3. Specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
- the name of this source set.
- 4. Use ``pw::i2c::McuxpressoInitiator`` implementation of
- ``pw::i2c::Initiator`` while creating ``pw::i2c::Device`` or
- ``pw::i2c::RegisterDevice`` interface to access the I2C devices connected to
- target. \ No newline at end of file
+1. Use ``pw_build_mcuxpresso`` to create a ``pw_source_set`` for an
+ MCUXpresso SDK.
+2. Include the i2c driver component in this SDK definition.
+3. Specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
+ the name of this source set.
+4. Use ``pw::i2c::McuxpressoInitiator`` implementation of
+ ``pw::i2c::Initiator`` while creating ``pw::i2c::Device`` or
+ ``pw::i2c::RegisterDevice`` interface to access the I2C devices connected to
+ target.
+
+Usage
+=====
+.. code-block:: cpp
+
+ constexpr uint32_t kI2CBaudRate = 100000;
+ constexpr McuxpressoInitiator::Config kConfig = {
+ .flexcomm_address = I2C11_BASE,
+ .clock_name = kCLOCK_Flexcomm11Clk,
+ .baud_rate_bps = kI2CBaudRate,
+ };
+ McuxpressoInitiator initiator{kConfig};
+ initiator.Enable();
diff --git a/pw_i2c_mcuxpresso/initiator.cc b/pw_i2c_mcuxpresso/initiator.cc
index 55b5717a6..ba4bce41b 100644
--- a/pw_i2c_mcuxpresso/initiator.cc
+++ b/pw_i2c_mcuxpresso/initiator.cc
@@ -41,24 +41,31 @@ Status HalStatusToPwStatus(status_t status) {
} // namespace
// inclusive-language: disable
-McuxpressoInitiator::McuxpressoInitiator(I2C_Type* base,
- uint32_t baud_rate_bps,
- uint32_t src_clock_hz)
- : base_(base) {
+void McuxpressoInitiator::Enable() {
+ std::lock_guard lock(mutex_);
+
i2c_master_config_t master_config;
I2C_MasterGetDefaultConfig(&master_config);
- master_config.baudRate_Bps = baud_rate_bps;
- I2C_MasterInit(base_, &master_config, src_clock_hz);
+ master_config.baudRate_Bps = config_.baud_rate_bps;
+ I2C_MasterInit(base_, &master_config, CLOCK_GetFreq(config_.clock_name));
// Create the handle for the non-blocking transfer and register callback.
I2C_MasterTransferCreateHandle(
base_, &handle_, McuxpressoInitiator::TransferCompleteCallback, this);
+
+ enabled_ = true;
+}
+
+void McuxpressoInitiator::Disable() {
+ std::lock_guard lock(mutex_);
+ I2C_MasterDeinit(base_);
+ enabled_ = false;
}
-McuxpressoInitiator::~McuxpressoInitiator() { I2C_MasterDeinit(base_); }
+McuxpressoInitiator::~McuxpressoInitiator() { Disable(); }
-void McuxpressoInitiator::TransferCompleteCallback(I2C_Type* base,
- i2c_master_handle_t* handle,
+void McuxpressoInitiator::TransferCompleteCallback(I2C_Type*,
+ i2c_master_handle_t*,
status_t status,
void* initiator_ptr) {
McuxpressoInitiator& initiator =
@@ -103,6 +110,10 @@ Status McuxpressoInitiator::DoWriteReadFor(
const uint8_t address = device_address.GetSevenBit();
std::lock_guard lock(mutex_);
+ if (!enabled_) {
+ return Status::FailedPrecondition();
+ }
+
if (!tx_buffer.empty() && rx_buffer.empty()) {
i2c_master_transfer_t transfer{kI2C_TransferDefaultFlag,
address,
diff --git a/pw_i2c_mcuxpresso/initiator_test.cc b/pw_i2c_mcuxpresso/initiator_test.cc
new file mode 100644
index 000000000..4848b3253
--- /dev/null
+++ b/pw_i2c_mcuxpresso/initiator_test.cc
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_i2c_mcuxpresso/initiator.h"
+
+#include <gtest/gtest.h>
+
+#include <cstdint>
+
+#include "fsl_clock.h"
+
+namespace pw::i2c {
+namespace {
+
+constexpr uint32_t kI2CBaudRate = 100000;
+constexpr McuxpressoInitiator::Config kConfig = {
+ .flexcomm_address = I2C11_BASE,
+ .clock_name = kCLOCK_Flexcomm11Clk,
+ .baud_rate_bps = kI2CBaudRate,
+};
+
+TEST(InitiatorTest, Init) {
+ // Simple test only meant to ensure module is compiled.
+ McuxpressoInitiator initiator{kConfig};
+ initiator.Enable();
+}
+
+} // namespace
+} // namespace pw::i2c
diff --git a/pw_i2c_mcuxpresso/public/pw_i2c_mcuxpresso/initiator.h b/pw_i2c_mcuxpresso/public/pw_i2c_mcuxpresso/initiator.h
index ab32beb90..f96c7626b 100644
--- a/pw_i2c_mcuxpresso/public/pw_i2c_mcuxpresso/initiator.h
+++ b/pw_i2c_mcuxpresso/public/pw_i2c_mcuxpresso/initiator.h
@@ -13,6 +13,7 @@
// the License.
#pragma once
+#include "fsl_clock.h"
#include "fsl_i2c.h"
#include "pw_i2c/initiator.h"
#include "pw_sync/interrupt_spin_lock.h"
@@ -26,11 +27,21 @@ namespace pw::i2c {
// Currently supports only devices with 7 bit adresses.
class McuxpressoInitiator final : public Initiator {
public:
- McuxpressoInitiator(I2C_Type* base,
- uint32_t baud_rate_bps,
- uint32_t src_clock_hz);
+ struct Config {
+ uint32_t flexcomm_address;
+ clock_name_t clock_name;
+ uint32_t baud_rate_bps;
+ };
- ~McuxpressoInitiator();
+ McuxpressoInitiator(const Config& config)
+ : config_(config),
+ base_(reinterpret_cast<I2C_Type*>(config_.flexcomm_address)) {}
+
+ // Should be called before attempting any transfers.
+ void Enable() PW_LOCKS_EXCLUDED(mutex_);
+ void Disable() PW_LOCKS_EXCLUDED(mutex_);
+
+ ~McuxpressoInitiator() final;
private:
Status DoWriteReadFor(Address device_address,
@@ -53,7 +64,9 @@ class McuxpressoInitiator final : public Initiator {
// inclusive-language: enable
sync::Mutex mutex_;
- I2C_Type* base_ PW_GUARDED_BY(mutex_);
+ Config const config_;
+ I2C_Type* const base_;
+ bool enabled_ PW_GUARDED_BY(mutex_);
// Transfer completion status for non-blocking I2C transfer.
sync::TimedThreadNotification callback_complete_notification_;
@@ -61,8 +74,8 @@ class McuxpressoInitiator final : public Initiator {
status_t transfer_status_ PW_GUARDED_BY(callback_isl_);
// inclusive-language: disable
- i2c_master_handle_t handle_;
+ i2c_master_handle_t handle_ PW_GUARDED_BY(mutex_);
// inclusive-language: enable
};
-} // namespace pw::i2c \ No newline at end of file
+} // namespace pw::i2c
diff --git a/pw_ide/docs.rst b/pw_ide/docs.rst
index d28d93e86..674108af8 100644
--- a/pw_ide/docs.rst
+++ b/pw_ide/docs.rst
@@ -11,7 +11,9 @@ Usage
Setup
-----
-Most of the time, ``pw ide setup`` is all you need to get started.
+Most of the time, ``pw ide sync`` is all you need to get started.
+
+.. _module-pw_ide-configuration:
Configuration
-------------
@@ -26,12 +28,12 @@ source control, and applies only to that checkout of the project. All of these
files have the same schema, in which these options can be configured:
.. autoproperty:: pw_ide.settings.PigweedIdeSettings.working_dir
-.. autoproperty:: pw_ide.settings.PigweedIdeSettings.build_dir
-.. autoproperty:: pw_ide.settings.PigweedIdeSettings.compdb_paths
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.compdb_search_paths
.. autoproperty:: pw_ide.settings.PigweedIdeSettings.targets
.. autoproperty:: pw_ide.settings.PigweedIdeSettings.target_inference
.. autoproperty:: pw_ide.settings.PigweedIdeSettings.default_target
-.. autoproperty:: pw_ide.settings.PigweedIdeSettings.setup
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.cascade_targets
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.sync
.. autoproperty:: pw_ide.settings.PigweedIdeSettings.clangd_additional_query_drivers
.. autoproperty:: pw_ide.settings.PigweedIdeSettings.editors
@@ -43,41 +45,48 @@ protocol (LSP). It uses a
`compilation database <https://clang.llvm.org/docs/JSONCompilationDatabase.html>`_,
a JSON file containing the compile commands for the project. Projects that have
multiple targets and/or use multiple toolchains need separate compilation
-databases for each target/toolchain. ``pw_ide`` provides tools for managing
+databases for each target toolchain. ``pw_ide`` provides tools for managing
those databases.
-Assuming you have a compilation database output from a build system, start with:
+Assuming you have one or more compilation databases that have been generated by
+your build system, start with:
.. code-block:: bash
- pw ide cpp --process <path or glob to your compile_commands.json file(s)>
+ pw ide sync
+
+This command will:
+
+- Find every compilation database in your build directory
+
+- Analyze each database
+
+ - If a database is internally consistent (i.e., it only contains valid
+ compile commands for a single target), it will use that database as-is for
+ the target toolchain that database pertains to. This is the typical case for
+ CMake builds.
-The ``pw_ide`` working directory will now contain one or more compilation
-database files, each for a separate target among the targets defined in
-``.pw_ide.yaml``. If you're using GN, you can generate the initial compilation
-database and process it in a single command by adding the ``--gn`` flag.
+ - Otherwise, if a database contains commands for multiple target toolchains
+ and/or contains invalid compile commands, the database will be processed,
+ yielding one new compilation database for each target toolchain. Those
+ databases will be used instead of the original.
-List the available targets with:
+- Link each target to its respective compilation database
+
+Now, you can list the available target toolchains with:
.. code-block:: bash
pw ide cpp --list
-Then set the target that ``clangd`` should use with:
+Then set the target toolchain that ``clangd`` should use with:
.. code-block:: bash
pw ide cpp --set <selected target name>
-``clangd`` can now be configured to point to the ``compile_commands.json`` file
-in the ``pw_ide`` working directory and provide code intelligence for the
-selected target. If you select a new target, ``clangd`` *does not* need to be
-reconfigured to look at a new file (in other words, ``clangd`` can always be
-pointed at the same, stable ``compile_commands.json`` file). However,
-``clangd`` may need to be restarted when the target changes.
-
-``clangd`` must be run within the activated Pigweed environment in order for
-``clangd.sh`` instead of directly using the ``clangd`` binary.
+``clangd`` will now work as designed since it is configured to use a compilation
+database that is consistent to just a single target toolchain.
``clangd`` must be run with arguments that provide the Pigweed environment paths
to the correct toolchains and sysroots. One way to do this is to launch your
@@ -143,14 +152,14 @@ out-of-the-box with embedded projects:
To deal with these challenges, ``pw_ide`` processes the compilation database you
provide, yielding one or more compilation databases that are valid, consistent,
-and specific to a particular build target. This enables code intelligence and
-navigation features that reflect that build.
-
-After processing a compilation database, ``pw_ide`` knows what targets are
-available and provides tools for selecting which target is active. These tools
-can be integrated into code editors, but are ultimately CLI-driven and
-editor-agnostic. Enabling code intelligence in your editor may be as simple as
-configuring its language server protocol client to use the ``clangd`` command
+and specific to a particular target toolchain. This enables code intelligence
+and navigation features that reflect that build.
+
+After processing a compilation database, ``pw_ide`` knows what target toolchains
+are available and provides tools for selecting which target toolchain is active.
+These tools can be integrated into code editors, but are ultimately CLI-driven
+and editor-agnostic. Enabling code intelligence in your editor may be as simple
+as configuring its language server protocol client to use the ``clangd`` command
that ``pw_ide`` can generate for you.
When to provide additional configuration to support your use cases
@@ -174,10 +183,15 @@ the compiler to ``clangd_additional_query_drivers`` in your project's
use the query driver globs to find your compiler and configure ``clangd`` to
use it.
+Compiler wrappers
+^^^^^^^^^^^^^^^^^
+If you're using ``ccache`` or any other wrapper command that is configured
+using ``ccache``'s' ``KEY=VALUE`` pattern, it will work out of the box.
+
Selected API Reference
^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: pw_ide.cpp
- :members: CppCompileCommand, CppCompilationDatabase, CppCompilationDatabasesMap, CppIdeFeaturesState, path_to_executable, target_is_enabled, ClangdSettings
+ :members: CppCompileCommand, CppCompilationDatabase, CppCompilationDatabasesMap, CppIdeFeaturesState, path_to_executable, ClangdSettings
Automated Support for Code Editors & IDEs
-----------------------------------------
@@ -186,6 +200,34 @@ for code editors, where default settings can be defined within ``pw_ide``,
which can be overridden by project settings, which in turn can be overridden
by individual user settings.
+.. _module-pw_ide-vscode:
+
+Visual Studio Code
+^^^^^^^^^^^^^^^^^^
+Running ``pw ide sync`` will automatically generate settings for Visual Studio
+Code. ``pw_ide`` comes with sensible defaults for Pigweed projects, but those
+can be augmented or overridden at the project level or the user level using
+``pw_project_settings.json`` and ``pw_user_settings.json`` respectively. The
+generated ``settings.json`` file is essentially a build artifact and shouldn't
+be committed to source control.
+
+The same pattern applies to ``tasks.json``, which provides Visual Studio Code
+tasks for ``pw_ide`` commands. Access these by opening the command palette
+(Ctrl/Cmd-Shift-P), selecting ``Tasks: Run Task``, then selecting the desired
+task.
+
+The same pattern also applies to ``launch.json``, which is used to define
+configurations for running and debugging your project. Create a
+``pw_project_launch.json`` with configurations that conform to the Visual Studio
+Code `debugger configuration format <https://code.visualstudio.com/docs/editor/debugging>`_.
+
+.. tip::
+
+ What's the difference between "Change C++ Code Analysis Target" and "Set C++
+ Code Analyis Target"? "Set" will automatically restart the ``clangd``
+ language server for you to pick up the changed target immediately, while
+ "Change" will not.
+
Selected API Reference
^^^^^^^^^^^^^^^^^^^^^^
.. automodule:: pw_ide.editors
diff --git a/pw_ide/py/BUILD.gn b/pw_ide/py/BUILD.gn
index 7438cc234..607f3516c 100644
--- a/pw_ide/py/BUILD.gn
+++ b/pw_ide/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_ide/__init__.py",
@@ -33,6 +32,7 @@ pw_python_package("py") {
"pw_ide/exceptions.py",
"pw_ide/python.py",
"pw_ide/settings.py",
+ "pw_ide/status_reporter.py",
"pw_ide/symlinks.py",
"pw_ide/vscode.py",
]
diff --git a/pw_ide/py/commands_test.py b/pw_ide/py/commands_test.py
index 3bc90ac3e..a55e3654a 100644
--- a/pw_ide/py/commands_test.py
+++ b/pw_ide/py/commands_test.py
@@ -17,8 +17,9 @@ import logging
import os
import unittest
-from pw_ide.commands import _make_working_dir, LoggingStatusReporter
+from pw_ide.commands import _make_working_dir
from pw_ide.settings import PW_IDE_DIR_NAME
+from pw_ide.status_reporter import LoggingStatusReporter
from test_cases import PwIdeTestCase
diff --git a/pw_ide/py/cpp_test.py b/pw_ide/py/cpp_test.py
index 87d57f3e0..ee5abc423 100644
--- a/pw_ide/py/cpp_test.py
+++ b/pw_ide/py/cpp_test.py
@@ -15,118 +15,27 @@
import json
from pathlib import Path
-from typing import cast, Dict, List, Optional, Tuple, TypedDict, Union
+from typing import cast, Dict, List, Optional
import unittest
-from unittest.mock import ANY, Mock, patch
# pylint: disable=protected-access
from pw_ide.cpp import (
- _COMPDB_FILE_PREFIX,
- _COMPDB_FILE_SEPARATOR,
- _COMPDB_FILE_EXTENSION,
- _COMPDB_CACHE_DIR_PREFIX,
- _COMPDB_CACHE_DIR_SEPARATOR,
+ COMPDB_FILE_NAME,
+ command_parts,
path_to_executable,
- aggregate_compilation_database_targets,
- compdb_generate_cache_path,
- compdb_generate_file_path,
- compdb_target_from_path,
CppCompilationDatabase,
CppCompilationDatabasesMap,
CppCompileCommand,
CppCompileCommandDict,
- CppIdeFeaturesState,
infer_target,
_infer_target_pos,
- InvalidTargetException,
)
from pw_ide.exceptions import UnresolvablePathException
-from pw_ide.symlinks import set_symlink
from test_cases import PwIdeTestCase
-class TestCompDbGenerateFilePath(unittest.TestCase):
- """Tests compdb_generate_file_path"""
-
- def test_with_target_includes_target(self) -> None:
- name = 'foo'
- actual = str(compdb_generate_file_path('foo'))
- expected = (
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
- f'{name}{_COMPDB_FILE_EXTENSION}'
- )
- self.assertEqual(actual, expected)
-
- def test_without_target_omits_target(self) -> None:
- actual = str(compdb_generate_file_path())
- expected = f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}'
- self.assertEqual(actual, expected)
-
-
-class TestCompDbGenerateCachePath(unittest.TestCase):
- """Tests compdb_generate_cache_path"""
-
- def test_with_target_includes_target(self) -> None:
- name = 'foo'
- actual = str(compdb_generate_cache_path('foo'))
- expected = (
- f'{_COMPDB_CACHE_DIR_PREFIX}' f'{_COMPDB_CACHE_DIR_SEPARATOR}{name}'
- )
- self.assertEqual(actual, expected)
-
- def test_without_target_omits_target(self) -> None:
- actual = str(compdb_generate_cache_path())
- expected = f'{_COMPDB_CACHE_DIR_PREFIX}'
- self.assertEqual(actual, expected)
-
-
-class _CompDbTargetFromPathTestCase(TypedDict):
- path: str
- target: Optional[str]
-
-
-class TestCompDbTargetFromPath(unittest.TestCase):
- """Tests compdb_target_from_path"""
-
- def run_test(self, path: Path, expected_target: Optional[str]) -> None:
- target = compdb_target_from_path(path)
- self.assertEqual(target, expected_target)
-
- def test_correct_target_from_path(self) -> None:
- """Test that the expected target is extracted from the file path."""
- cases: List[_CompDbTargetFromPathTestCase] = [
- {
- 'path': (
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
- f'pw_strict_host_clang_debug{_COMPDB_FILE_EXTENSION}'
- ),
- 'target': 'pw_strict_host_clang_debug',
- },
- {
- 'path': (
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
- f'stm32f429i_disc1_debug{_COMPDB_FILE_EXTENSION}'
- ),
- 'target': 'stm32f429i_disc1_debug',
- },
- {
- 'path': (
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
- f'{_COMPDB_FILE_EXTENSION}'
- ),
- 'target': None,
- },
- {'path': 'foompile_barmmands.json', 'target': None},
- {'path': 'foompile_barmmands_target_x.json', 'target': None},
- {'path': '', 'target': None},
- ]
-
- for case in cases:
- self.run_test(Path(case['path']), case['target'])
-
-
class TestPathToExecutable(PwIdeTestCase):
"""Tests path_to_executable"""
@@ -169,7 +78,7 @@ class TestPathToExecutable(PwIdeTestCase):
self.assertEqual(str(result), str(expected_path))
def test_invalid_absolute_path_in_env_returns_same_path(self):
- executable_path = self.temp_dir_path / 'exe'
+ executable_path = self.temp_dir_path / '_pw_invalid_exe'
query_drivers = [f'{self.temp_dir_path}/*']
result = path_to_executable(
str(executable_path.absolute()), path_globs=query_drivers
@@ -177,7 +86,7 @@ class TestPathToExecutable(PwIdeTestCase):
self.assertIsNone(result)
def test_invalid_absolute_path_outside_env_returns_same_path(self):
- executable_path = Path('/usr/bin/exe')
+ executable_path = Path('/usr/bin/_pw_invalid_exe')
query_drivers = ['/**/*']
result = path_to_executable(
str(executable_path.absolute()), path_globs=query_drivers
@@ -185,7 +94,7 @@ class TestPathToExecutable(PwIdeTestCase):
self.assertIsNone(result)
def test_invalid_relative_path_returns_same_path(self):
- executable_path = Path('../exe')
+ executable_path = Path('../_pw_invalid_exe')
query_drivers = [f'{self.temp_dir_path}/*']
result = path_to_executable(
str(executable_path), path_globs=query_drivers
@@ -193,7 +102,7 @@ class TestPathToExecutable(PwIdeTestCase):
self.assertIsNone(result)
def test_invalid_no_path_returns_new_path(self):
- executable_path = Path('exe')
+ executable_path = Path('_pw_invalid_exe')
query_drivers = [f'{self.temp_dir_path}/*']
expected_path = self.temp_dir_path / executable_path
self.touch_temp_file(expected_path)
@@ -342,11 +251,726 @@ class TestCppCompileCommand(PwIdeTestCase):
self.assertIsNone(result)
+class TestCommandParts(unittest.TestCase):
+ """Test command_parts"""
+
+ def test_command_parts(self):
+ """Test command_parts"""
+
+ # pylint: disable=line-too-long
+ test_cases = [
+ "python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/block.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o",
+ 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ '../environment/cipd/packages/pigweed/bin/isosceles-clang++ -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ 'ccache arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ 'ccache ../environment/cipd/packages/pigweed/bin/isosceles-clang++ -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ 'ccache ../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ 'ccache debug=true max_size=10G arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ 'ccache debug=true max_size=10G ../environment/cipd/packages/pigweed/bin/isosceles-clang++ -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ 'ccache debug=true max_size=10G ../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ "ccache python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/block.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o",
+ ]
+
+ expected_results = [
+ (
+ None,
+ 'python',
+ [
+ '../pw_toolchain/py/pw_toolchain/clang_tidy.py',
+ '--source-exclude',
+ "'third_party/.*'",
+ '--source-exclude',
+ "'.*packages/mbedtls.*'",
+ '--source-exclude',
+ "'.*packages/boringssl.*'",
+ '--skip-include-path',
+ "'mbedtls/include'",
+ '--skip-include-path',
+ "'mbedtls'",
+ '--skip-include-path',
+ "'boringssl/src/include'",
+ '--skip-include-path',
+ "'boringssl'",
+ '--skip-include-path',
+ "'pw_tls_client/generate_test_data'",
+ '--source-file',
+ '../pw_allocator/block.cc',
+ '--source-root',
+ "'../'",
+ '--export-fixes',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml',
+ '--',
+ '../environment/cipd/packages/pigweed/bin/clang++',
+ 'END_OF_INVOKER',
+ '-MMD',
+ '-MF',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o',
+ '&&',
+ 'touch',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ None,
+ 'arm-none-eabi-g++',
+ [
+ '-MMD',
+ '-MF',
+ 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-Wno-psabi',
+ '-mabi=aapcs',
+ '-mthumb',
+ '--sysroot=../environment/cipd/packages/arm',
+ '-specs=nano.specs',
+ '-specs=nosys.specs',
+ '-mcpu=cortex-m4',
+ '-mfloat-abi=hard',
+ '-mfpu=fpv4-sp-d16',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-u_printf_float',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-DPW_ARMV7M_ENABLE_FPU=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/assert_compatibility_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ None,
+ '../environment/cipd/packages/pigweed/bin/isosceles-clang++',
+ [
+ '-MMD',
+ '-MF',
+ 'isosceles_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ None,
+ '../environment/cipd/packages/pigweed/bin/clang++',
+ [
+ '-MMD',
+ '-MF',
+ 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache',
+ 'arm-none-eabi-g++',
+ [
+ '-MMD',
+ '-MF',
+ 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-Wno-psabi',
+ '-mabi=aapcs',
+ '-mthumb',
+ '--sysroot=../environment/cipd/packages/arm',
+ '-specs=nano.specs',
+ '-specs=nosys.specs',
+ '-mcpu=cortex-m4',
+ '-mfloat-abi=hard',
+ '-mfpu=fpv4-sp-d16',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-u_printf_float',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-DPW_ARMV7M_ENABLE_FPU=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/assert_compatibility_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache',
+ '../environment/cipd/packages/pigweed/bin/isosceles-clang++',
+ [
+ '-MMD',
+ '-MF',
+ 'isosceles_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache',
+ '../environment/cipd/packages/pigweed/bin/clang++',
+ [
+ '-MMD',
+ '-MF',
+ 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache debug=true max_size=10G',
+ 'arm-none-eabi-g++',
+ [
+ '-MMD',
+ '-MF',
+ 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-Wno-psabi',
+ '-mabi=aapcs',
+ '-mthumb',
+ '--sysroot=../environment/cipd/packages/arm',
+ '-specs=nano.specs',
+ '-specs=nosys.specs',
+ '-mcpu=cortex-m4',
+ '-mfloat-abi=hard',
+ '-mfpu=fpv4-sp-d16',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-u_printf_float',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-DPW_ARMV7M_ENABLE_FPU=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/assert_compatibility_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache debug=true max_size=10G',
+ '../environment/cipd/packages/pigweed/bin/isosceles-clang++',
+ [
+ '-MMD',
+ '-MF',
+ 'isosceles_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache debug=true max_size=10G',
+ '../environment/cipd/packages/pigweed/bin/clang++',
+ [
+ '-MMD',
+ '-MF',
+ 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ (
+ 'ccache',
+ 'python',
+ [
+ '../pw_toolchain/py/pw_toolchain/clang_tidy.py',
+ '--source-exclude',
+ "'third_party/.*'",
+ '--source-exclude',
+ "'.*packages/mbedtls.*'",
+ '--source-exclude',
+ "'.*packages/boringssl.*'",
+ '--skip-include-path',
+ "'mbedtls/include'",
+ '--skip-include-path',
+ "'mbedtls'",
+ '--skip-include-path',
+ "'boringssl/src/include'",
+ '--skip-include-path',
+ "'boringssl'",
+ '--skip-include-path',
+ "'pw_tls_client/generate_test_data'",
+ '--source-file',
+ '../pw_allocator/block.cc',
+ '--source-root',
+ "'../'",
+ '--export-fixes',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml',
+ '--',
+ '../environment/cipd/packages/pigweed/bin/clang++',
+ 'END_OF_INVOKER',
+ '-MMD',
+ '-MF',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d',
+ '-g3',
+ '--sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk',
+ '-Og',
+ '-Wshadow',
+ '-Wredundant-decls',
+ '-Wthread-safety',
+ '-Wswitch-enum',
+ '-fdiagnostics-color',
+ '-g',
+ '-fno-common',
+ '-fno-exceptions',
+ '-ffunction-sections',
+ '-fdata-sections',
+ '-Wall',
+ '-Wextra',
+ '-Wimplicit-fallthrough',
+ '-Wcast-qual',
+ '-Wundef',
+ '-Wpointer-arith',
+ '-Werror',
+ '-Wno-error=cpp',
+ '-Wno-error=deprecated-declarations',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-ffile-prefix-map=/pigweed/pigweed/=',
+ '-ffile-prefix-map=../=',
+ '-ffile-prefix-map=/pigweed/pigweed/out=out',
+ '-Wextra-semi',
+ '-fno-rtti',
+ '-Wnon-virtual-dtor',
+ '-std=c++17',
+ '-Wno-register',
+ '-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1',
+ '-DPW_STATUS_CFG_CHECK_IF_USED=1',
+ '-I../pw_allocator/public',
+ '-I../pw_assert/public',
+ '-I../pw_assert/print_and_abort_assert_public_overrides',
+ '-I../pw_preprocessor/public',
+ '-I../pw_assert_basic/public_overrides',
+ '-I../pw_assert_basic/public',
+ '-I../pw_span/public',
+ '-I../pw_polyfill/public',
+ '-I../pw_polyfill/standard_library_public',
+ '-I../pw_status/public',
+ '-c',
+ '../pw_allocator/block.cc',
+ '-o',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o',
+ '&&',
+ 'touch',
+ 'pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o',
+ ],
+ ),
+ ]
+ # pylint: enable=line-too-long
+
+ for test_case, expected in zip(test_cases, expected_results):
+ self.assertEqual(command_parts(test_case), expected)
+
+
class TestCppCompilationDatabase(PwIdeTestCase):
"""Tests CppCompilationDatabase"""
def setUp(self):
- self.build_dir = Path('/pigweed/pigweed/out')
+ self.root_dir = Path('/pigweed/pigweed/out')
self.fixture: List[CppCompileCommandDict] = [
{
@@ -422,10 +1046,10 @@ class TestCppCompilationDatabase(PwIdeTestCase):
def test_merge(self):
compdb1 = CppCompilationDatabase.load(
- self.fixture_merge_1, self.build_dir
+ self.fixture_merge_1, self.root_dir
)
compdb2 = CppCompilationDatabase.load(
- self.fixture_merge_2, self.build_dir
+ self.fixture_merge_2, self.root_dir
)
compdb1.merge(compdb2)
result = [compile_command.as_dict() for compile_command in compdb1]
@@ -434,41 +1058,41 @@ class TestCppCompilationDatabase(PwIdeTestCase):
def test_merge_no_dupes(self):
compdb1 = CppCompilationDatabase.load(
- self.fixture_merge_1, self.build_dir
+ self.fixture_merge_1, self.root_dir
)
fixture_combo = [*self.fixture_merge_1, *self.fixture_merge_2]
- compdb2 = CppCompilationDatabase.load(fixture_combo, self.build_dir)
+ compdb2 = CppCompilationDatabase.load(fixture_combo, self.root_dir)
compdb1.merge(compdb2)
result = [compile_command.as_dict() for compile_command in compdb1]
expected = [*self.fixture_merge_1, *self.fixture_merge_2]
self.assertCountEqual(result, expected)
def test_load_from_dicts(self):
- compdb = CppCompilationDatabase.load(self.fixture, self.build_dir)
+ compdb = CppCompilationDatabase.load(self.fixture, self.root_dir)
self.assertCountEqual(compdb.as_dicts(), self.expected)
def test_load_from_json(self):
compdb = CppCompilationDatabase.load(
- json.dumps(self.fixture), self.build_dir
+ json.dumps(self.fixture), self.root_dir
)
self.assertCountEqual(compdb.as_dicts(), self.expected)
def test_load_from_path(self):
with self.make_temp_file(
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}',
+ COMPDB_FILE_NAME,
json.dumps(self.fixture),
) as (_, file_path):
path = file_path
- compdb = CppCompilationDatabase.load(path, self.build_dir)
+ compdb = CppCompilationDatabase.load(path, self.root_dir)
self.assertCountEqual(compdb.as_dicts(), self.expected)
def test_load_from_file_handle(self):
with self.make_temp_file(
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}',
+ COMPDB_FILE_NAME,
json.dumps(self.fixture),
) as (file, _):
- compdb = CppCompilationDatabase.load(file, self.build_dir)
+ compdb = CppCompilationDatabase.load(file, self.root_dir)
self.assertCountEqual(compdb.as_dicts(), self.expected)
@@ -487,22 +1111,22 @@ class TestCppCompilationDatabase(PwIdeTestCase):
raw_db: List[CppCompileCommandDict] = [
{
'command': 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
- 'directory': str(self.build_dir),
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
},
{
'command': '../environment/cipd/packages/pigweed/bin/isosceles-clang++ -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
- 'directory': str(self.build_dir),
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
},
{
- 'command': '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
- 'directory': str(self.build_dir),
+ 'command': 'ccache ../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
},
{
'command': "python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/block.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o",
- 'directory': str(self.build_dir),
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
},
]
@@ -513,7 +1137,7 @@ class TestCppCompilationDatabase(PwIdeTestCase):
'command':
# Ensures path format matches OS (e.g. Windows)
f'{Path("../environment/cipd/packages/pigweed/bin/isosceles-clang++")} -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
- 'directory': str(self.build_dir),
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
'output': 'isosceles_debug/obj/pw_allocator/block.block.cc.o',
},
@@ -522,8 +1146,8 @@ class TestCppCompilationDatabase(PwIdeTestCase):
{
'command':
# Ensures path format matches OS (e.g. Windows)
- f'{Path("../environment/cipd/packages/pigweed/bin/clang++")} -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
- 'directory': str(self.build_dir),
+ f'ccache {Path("../environment/cipd/packages/pigweed/bin/clang++")} -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
'output': 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
},
@@ -533,7 +1157,7 @@ class TestCppCompilationDatabase(PwIdeTestCase):
'command':
# Ensures this test avoids the unpathed compiler search
f'{self.temp_dir_path / "arm-none-eabi-g++"} -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
- 'directory': str(self.build_dir),
+ 'directory': str(self.root_dir),
'file': '../pw_allocator/block.cc',
'output': 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
},
@@ -542,7 +1166,7 @@ class TestCppCompilationDatabase(PwIdeTestCase):
# pylint: enable=line-too-long
compdbs = CppCompilationDatabase.load(
- raw_db, build_dir=self.build_dir
+ raw_db, root_dir=self.root_dir
).process(settings, default_path=self.temp_dir_path)
compdbs_as_dicts = {
target: compdb.as_dicts() for target, compdb in compdbs.items()
@@ -632,351 +1256,29 @@ class TestCppCompilationDatabasesMap(PwIdeTestCase):
self.assertEqual(len(result), 2)
self.assertCountEqual(result._dbs, {**db_set1._dbs, **db_set2._dbs})
-
-class TestCppIdeFeaturesState(PwIdeTestCase):
- """Tests CppIdeFeaturesState"""
-
- def test_finds_all_compdbs(self) -> None:
- """Test that it finds all compilation databases on initalization."""
-
- targets = [
- 'pw_strict_host_clang_debug',
- 'stm32f429i_disc1_debug',
- ]
-
- # Simulate a dir with n compilation databases and m < n cache dirs.
- files_data: List[Tuple[Union[Path, str], str]] = [
- (compdb_generate_file_path(target), '') for target in targets
- ]
-
- files_data.append((compdb_generate_cache_path(targets[0]), ''))
-
- # Symlinks
- files_data.append((compdb_generate_file_path(), ''))
- files_data.append((compdb_generate_cache_path(), ''))
-
- expected = [
- (
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
- f'{targets[0]}{_COMPDB_FILE_EXTENSION}',
- f'{_COMPDB_CACHE_DIR_PREFIX}{_COMPDB_CACHE_DIR_SEPARATOR}'
- f'{targets[0]}',
- ),
- (
- f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
- f'{targets[1]}{_COMPDB_FILE_EXTENSION}',
- None,
- ),
- ]
-
- settings = self.make_ide_settings(targets=targets)
-
- with self.make_temp_files(files_data):
- found_compdbs = CppIdeFeaturesState(settings)
-
- # Strip out the temp dir path data.
- get_name = lambda p: p.name if p is not None else None
- found_compdbs_str = [
- (
- get_name(target.compdb_file_path),
- get_name(target.compdb_cache_path),
- )
- for target in found_compdbs
- ]
-
- self.assertCountEqual(found_compdbs_str, expected)
-
- def test_finds_all_targets(self) -> None:
- """Test that it finds all targets on initalization."""
- targets = [
- 'pw_strict_host_clang_debug',
- 'stm32f429i_disc1_debug',
- ]
-
- settings = self.make_ide_settings(targets=targets)
-
- with self.make_temp_file(
- compdb_generate_file_path(targets[0])
- ), self.make_temp_file(compdb_generate_file_path(targets[1])):
- found_targets = CppIdeFeaturesState(settings).targets
-
- self.assertCountEqual(found_targets, targets)
-
- def test_get_target_finds_valid_target(self) -> None:
- target = 'pw_strict_host_clang_debug'
- settings = self.make_ide_settings(targets=[target])
-
- compdb_filename = (
- f'{_COMPDB_FILE_PREFIX}'
- f'{_COMPDB_FILE_SEPARATOR}'
- f'{target}{_COMPDB_FILE_EXTENSION}'
- )
-
- symlink = self.temp_dir_path / compdb_generate_file_path()
-
- with self.make_temp_file(Path(compdb_filename)) as (_, compdb):
- set_symlink(compdb, symlink)
-
- found_target = CppIdeFeaturesState(settings).current_target
- self.assertEqual(found_target, target)
-
- def test_get_target_returns_none_with_no_symlink(self) -> None:
- target = 'pw_strict_host_clang_debug'
- settings = self.make_ide_settings(targets=[target])
- found_target = CppIdeFeaturesState(settings).current_target
- self.assertIsNone(found_target)
-
- @patch('os.readlink')
- def test_get_target_returns_none_with_bad_symlink(
- self, mock_readlink: Mock
- ) -> None:
- target = 'pw_strict_host_clang_debug'
- settings = self.make_ide_settings(targets=[target])
-
- mock_readlink.return_value = (
- f'{_COMPDB_FILE_PREFIX}'
- f'{_COMPDB_FILE_SEPARATOR}'
- f'{target}{_COMPDB_FILE_EXTENSION}'
+ def test_cascade_disabled(self):
+ settings = self.make_ide_settings(
+ cascade_targets=False, targets=['test_target_1', 'test_target_2']
)
-
- found_target = CppIdeFeaturesState(settings).current_target
- self.assertIsNone(found_target)
-
- @patch('os.remove')
- @patch('os.mkdir')
- @patch('os.symlink')
- def test_set_target_sets_valid_target_when_no_target_set(
- self, mock_symlink: Mock, mock_mkdir: Mock, mock_remove: Mock
- ) -> None:
- """Test the case where no symlinks have been set."""
-
- target = 'pw_strict_host_clang_debug'
- settings = self.make_ide_settings(targets=[target])
- compdb_symlink_path = compdb_generate_file_path()
- cache_symlink_path = compdb_generate_cache_path()
-
- with self.make_temp_file(compdb_generate_file_path(target)):
- CppIdeFeaturesState(settings).current_target = target
-
- mock_mkdir.assert_any_call(
- self.path_in_temp_dir(compdb_generate_cache_path(target))
- )
-
- target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
- compdb_generate_file_path(target), compdb_symlink_path
- )
- mock_symlink.assert_any_call(*target_and_symlink_in_temp_dir, ANY)
-
- target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
- compdb_generate_cache_path(target), cache_symlink_path
- )
- mock_symlink.assert_any_call(*target_and_symlink_in_temp_dir, ANY)
-
- mock_remove.assert_not_called()
-
- @patch('os.remove')
- @patch('os.mkdir')
- @patch('os.symlink')
- def test_set_target_sets_valid_target_when_target_already_set(
- self, mock_symlink: Mock, mock_mkdir: Mock, mock_remove: Mock
- ) -> None:
- """Test the case where symlinks have been set, and now we're setting
- them to a different target."""
-
- targets = [
- 'pw_strict_host_clang_debug',
- 'stm32f429i_disc1_debug',
- ]
-
- settings = self.make_ide_settings(targets=targets)
- compdb_symlink_path = compdb_generate_file_path()
- cache_symlink_path = compdb_generate_cache_path()
-
- # Set the first target, which should initalize the symlinks.
- with self.make_temp_file(
- compdb_generate_file_path(targets[0])
- ), self.make_temp_file(compdb_generate_file_path(targets[1])):
- CppIdeFeaturesState(settings).current_target = targets[0]
-
- mock_mkdir.assert_any_call(
- self.path_in_temp_dir(compdb_generate_cache_path(targets[0]))
- )
-
- mock_remove.assert_not_called()
-
- # Simulate symlink creation
- with self.make_temp_file(compdb_symlink_path), self.make_temp_file(
- cache_symlink_path
- ):
- # Set the second target, which should replace the symlinks
- CppIdeFeaturesState(settings).current_target = targets[1]
-
- mock_mkdir.assert_any_call(
- self.path_in_temp_dir(
- compdb_generate_cache_path(targets[1])
- )
- )
-
- mock_remove.assert_any_call(
- self.path_in_temp_dir(compdb_symlink_path)
- )
-
- mock_remove.assert_any_call(
- self.path_in_temp_dir(cache_symlink_path)
- )
-
- target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
- compdb_generate_file_path(targets[1]), compdb_symlink_path
- )
- mock_symlink.assert_any_call(
- *target_and_symlink_in_temp_dir, ANY
- )
-
- target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
- compdb_generate_cache_path(targets[1]), cache_symlink_path
- )
- mock_symlink.assert_any_call(
- *target_and_symlink_in_temp_dir, ANY
- )
-
- @patch('os.remove')
- @patch('os.mkdir')
- @patch('os.symlink')
- def test_set_target_sets_valid_target_back_and_forth(
- self, mock_symlink: Mock, mock_mkdir: Mock, mock_remove: Mock
- ) -> None:
- """Test the case where symlinks have been set, we set them to a second
- target, and now we're setting them back to the first target."""
-
- targets = [
- 'pw_strict_host_clang_debug',
- 'stm32f429i_disc1_debug',
- ]
-
- settings = self.make_ide_settings(targets=targets)
- compdb_symlink_path = compdb_generate_file_path()
- cache_symlink_path = compdb_generate_cache_path()
-
- # Set the first target, which should initalize the symlinks
- with self.make_temp_file(
- compdb_generate_file_path(targets[0])
- ), self.make_temp_file(compdb_generate_file_path(targets[1])):
- CppIdeFeaturesState(settings).current_target = targets[0]
-
- # Simulate symlink creation
- with self.make_temp_file(compdb_symlink_path), self.make_temp_file(
- cache_symlink_path
- ):
- # Set the second target, which should replace the symlinks
- CppIdeFeaturesState(settings).current_target = targets[1]
-
- # Reset mocks to clear events prior to those under test
- mock_symlink.reset_mock()
- mock_mkdir.reset_mock()
- mock_remove.reset_mock()
-
- # Set the first target again, which should also replace the
- # symlinks and reuse the existing cache folder
- CppIdeFeaturesState(settings).current_target = targets[0]
-
- mock_mkdir.assert_any_call(
- self.path_in_temp_dir(
- compdb_generate_cache_path(targets[0])
- )
- )
-
- mock_remove.assert_any_call(
- self.path_in_temp_dir(compdb_symlink_path)
- )
-
- mock_remove.assert_any_call(
- self.path_in_temp_dir(cache_symlink_path)
- )
-
- target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
- compdb_generate_file_path(targets[0]), compdb_symlink_path
- )
- mock_symlink.assert_any_call(
- *target_and_symlink_in_temp_dir, ANY
- )
-
- target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
- compdb_generate_cache_path(targets[0]), cache_symlink_path
- )
- mock_symlink.assert_any_call(
- *target_and_symlink_in_temp_dir, ANY
- )
-
- @patch('os.symlink')
- def test_set_target_invalid_target_not_in_enabled_targets_raises(
- self, mock_symlink: Mock
- ):
- target = 'pw_strict_host_clang_debug'
- settings = self.make_ide_settings(targets=[target])
-
- with self.make_temp_file(
- compdb_generate_file_path(target)
- ), self.assertRaises(InvalidTargetException):
- CppIdeFeaturesState(settings).current_target = 'foo'
- mock_symlink.assert_not_called()
-
- @patch('os.symlink')
- def test_set_target_invalid_target_not_in_available_targets_raises(
- self, mock_symlink: Mock
- ):
- target = 'pw_strict_host_clang_debug'
- settings = self.make_ide_settings(targets=[target])
-
- with self.assertRaises(InvalidTargetException):
- CppIdeFeaturesState(settings).current_target = target
- mock_symlink.assert_not_called()
-
-
-class TestAggregateCompilationDatabaseTargets(PwIdeTestCase):
- """Tests aggregate_compilation_database_targets"""
-
- def test_gets_all_legitimate_targets(self):
- """Test compilation target aggregation against a typical sample of raw
- output from GN."""
-
- targets = [
- 'pw_strict_host_clang_debug',
- 'stm32f429i_disc1_debug',
- ]
-
- settings = self.make_ide_settings(targets=targets)
-
- raw_db: List[CppCompileCommandDict] = [
- {
- # pylint: disable=line-too-long
- 'command': 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
- # pylint: enable=line-too-long
- 'directory': '/pigweed/pigweed/out',
- 'file': '../pw_allocator/block.cc',
- },
- {
- # pylint: disable=line-too-long
- 'command': '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
- # pylint: enable=line-too-long
- 'directory': '/pigweed/pigweed/out',
- 'file': '../pw_allocator/block.cc',
- },
- {
- # pylint: disable=line-too-long
- 'command': "python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/block.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o",
- # pylint: enable=line-too-long
- 'directory': '/pigweed/pigweed/out',
- 'file': '../pw_allocator/block.cc',
- },
- ]
-
- root = Path('/pigweed/pigweed/out')
-
- aggregated_targets = aggregate_compilation_database_targets(
- raw_db, settings, root, default_path=self.temp_dir_path
+ target1 = 'test_target_1'
+ target2 = 'test_target_2'
+ db_set = CppCompilationDatabasesMap(settings)
+ db_set[target1] = self.fixture_1(target1)
+ db_set[target2] = self.fixture_2(target2)
+ result = db_set._compdb_to_write(target1)
+ self.assertCountEqual(result._db, [*db_set[target1]])
+
+ def test_cascade_enabled(self):
+ settings = self.make_ide_settings(
+ cascade_targets=True, targets=['test_target_1', 'test_target_2']
)
-
- self.assertCountEqual(aggregated_targets, targets)
+ target1 = 'test_target_1'
+ target2 = 'test_target_2'
+ db_set = CppCompilationDatabasesMap(settings)
+ db_set[target1] = self.fixture_1(target1)
+ db_set[target2] = self.fixture_2(target2)
+ result = db_set._compdb_to_write(target1)
+ self.assertCountEqual(result._db, [*db_set[target1], *db_set[target2]])
if __name__ == '__main__':
diff --git a/pw_ide/py/editors_test.py b/pw_ide/py/editors_test.py
index cce3722ff..1391fe990 100644
--- a/pw_ide/py/editors_test.py
+++ b/pw_ide/py/editors_test.py
@@ -19,9 +19,13 @@ import unittest
from pw_ide.editors import (
dict_deep_merge,
+ dict_swap_type,
EditorSettingsFile,
EditorSettingsManager,
JsonFileFormat,
+ Json5FileFormat,
+ YamlFileFormat,
+ _StructuredFileFormat,
)
from test_cases import PwIdeTestCase
@@ -30,8 +34,8 @@ from test_cases import PwIdeTestCase
class TestDictDeepMerge(unittest.TestCase):
"""Tests dict_deep_merge"""
+ # pylint: disable=unnecessary-lambda
def test_invariants_with_dict_success(self):
- # pylint: disable=unnecessary-lambda
dict_a = {'hello': 'world'}
dict_b = {'foo': 'bar'}
@@ -44,7 +48,6 @@ class TestDictDeepMerge(unittest.TestCase):
self.assertEqual(result, expected)
def test_invariants_with_dict_implicit_ctor_success(self):
- # pylint: disable=unnecessary-lambda
dict_a = {'hello': 'world'}
dict_b = {'foo': 'bar'}
@@ -57,7 +60,6 @@ class TestDictDeepMerge(unittest.TestCase):
self.assertEqual(result, expected)
def test_invariants_with_dict_fails_wrong_ctor_type(self):
- # pylint: disable=unnecessary-lambda
dict_a = {'hello': 'world'}
dict_b = {'foo': 'bar'}
@@ -65,7 +67,6 @@ class TestDictDeepMerge(unittest.TestCase):
dict_deep_merge(dict_b, dict_a, lambda: OrderedDict())
def test_invariants_with_ordered_dict_success(self):
- # pylint: disable=unnecessary-lambda
dict_a = OrderedDict({'hello': 'world'})
dict_b = OrderedDict({'foo': 'bar'})
@@ -80,7 +81,6 @@ class TestDictDeepMerge(unittest.TestCase):
self.assertEqual(result, expected)
def test_invariants_with_ordered_dict_implicit_ctor_success(self):
- # pylint: disable=unnecessary-lambda
dict_a = OrderedDict({'hello': 'world'})
dict_b = OrderedDict({'foo': 'bar'})
@@ -95,247 +95,312 @@ class TestDictDeepMerge(unittest.TestCase):
self.assertEqual(result, expected)
def test_invariants_with_ordered_dict_fails_wrong_ctor_type(self):
- # pylint: disable=unnecessary-lambda
dict_a = OrderedDict({'hello': 'world'})
dict_b = OrderedDict({'foo': 'bar'})
with self.assertRaises(TypeError):
dict_deep_merge(dict_b, dict_a, lambda: dict())
+ # pylint: enable=unnecessary-lambda
-class TestEditorSettingsFile(PwIdeTestCase):
- """Tests EditorSettingsFile"""
+ def test_merge_basic(self):
+ dict_a = {'hello': 'world'}
+ dict_b = {'hello': 'bar'}
- def test_open_new_file_and_write(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ expected = {'hello': 'bar'}
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ def test_merge_nested_dict(self):
+ dict_a = {'hello': {'a': 'foo'}}
+ dict_b = {'hello': {'b': 'bar'}}
- with open(self.temp_dir_path / f'{name}.{json_fmt.ext}') as file:
- settings_dict = json_fmt.load(file)
+ expected = {'hello': {'a': 'foo', 'b': 'bar'}}
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
- self.assertEqual(settings_dict['hello'], 'world')
+ def test_merge_list(self):
+ dict_a = {'hello': ['world']}
+ dict_b = {'hello': ['bar']}
- def test_open_new_file_and_get(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ expected = {'hello': ['world', 'bar']}
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ def test_merge_list_no_duplicates(self):
+ dict_a = {'hello': ['world']}
+ dict_b = {'hello': ['world']}
- settings_dict = settings_file.get()
- self.assertEqual(settings_dict['hello'], 'world')
+ expected = {'hello': ['world']}
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
- def test_open_new_file_no_backup(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ def test_merge_nested_dict_with_lists(self):
+ dict_a = {'hello': {'a': 'foo', 'c': ['lorem']}}
+ dict_b = {'hello': {'b': 'bar', 'c': ['ipsum']}}
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ expected = {'hello': {'a': 'foo', 'b': 'bar', 'c': ['lorem', 'ipsum']}}
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
- backup_files = [
- path
- for path in self.temp_dir_path.iterdir()
- if path.name != f'{name}.{json_fmt.ext}'
- ]
+ def test_merge_object_fails(self):
+ class Strawman:
+ pass
- self.assertEqual(len(backup_files), 0)
+ dict_a = {'hello': 'world'}
+ dict_b = {'foo': Strawman()}
- def test_open_existing_file_and_backup(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ with self.assertRaises(TypeError):
+ dict_deep_merge(dict_b, dict_a)
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ def test_merge_copies_string(self):
+ test_str = 'bar'
+ dict_a = {'hello': {'a': 'foo'}}
+ dict_b = {'hello': {'b': test_str}}
- with settings_file.modify() as settings:
- settings['hello'] = 'mundo'
+ result = dict_deep_merge(dict_b, dict_a)
+ test_str = 'something else'
- settings_dict = settings_file.get()
- self.assertEqual(settings_dict['hello'], 'mundo')
+ self.assertEqual(result['hello']['b'], 'bar')
- backup_files = [
- path
- for path in self.temp_dir_path.iterdir()
- if path.name != f'{name}.{json_fmt.ext}'
- ]
- self.assertEqual(len(backup_files), 1)
+class TestDictSwapType(unittest.TestCase):
+ """Tests dict_swap_type"""
- with open(backup_files[0]) as file:
- settings_dict = json_fmt.load(file)
+ def test_ordereddict_to_dict(self):
+ """Test converting an OrderedDict to a plain dict"""
- self.assertEqual(settings_dict['hello'], 'world')
+ ordered_dict = OrderedDict(
+ {
+ 'hello': 'world',
+ 'foo': 'bar',
+ 'nested': OrderedDict(
+ {
+ 'lorem': 'ipsum',
+ 'dolor': 'sit amet',
+ }
+ ),
+ }
+ )
- def test_open_existing_file_with_reinit_and_backup(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ plain_dict = dict_swap_type(ordered_dict, dict)
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ expected_plain_dict = {
+ 'hello': 'world',
+ 'foo': 'bar',
+ 'nested': {
+ 'lorem': 'ipsum',
+ 'dolor': 'sit amet',
+ },
+ }
- with settings_file.modify(reinit=True) as settings:
- settings['hello'] = 'mundo'
+ # The returned dict has the content and type we expect
+ self.assertDictEqual(plain_dict, expected_plain_dict)
+ self.assertIsInstance(plain_dict, dict)
+ self.assertIsInstance(plain_dict['nested'], dict)
- settings_dict = settings_file.get()
- self.assertEqual(settings_dict['hello'], 'mundo')
+ # The original OrderedDict is unchanged
+ self.assertIsInstance(ordered_dict, OrderedDict)
+ self.assertIsInstance(ordered_dict['nested'], OrderedDict)
- backup_files = [
- path
- for path in self.temp_dir_path.iterdir()
- if path.name != f'{name}.{json_fmt.ext}'
- ]
- self.assertEqual(len(backup_files), 1)
+class EditorSettingsTestType(Enum):
+ SETTINGS = 'settings'
- with open(backup_files[0]) as file:
- settings_dict = json_fmt.load(file)
- self.assertEqual(settings_dict['hello'], 'world')
+class TestCasesGenericOnFileFormat:
+ """Container for tests generic on FileFormat.
- def open_existing_file_no_change_no_backup(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ This misdirection is needed to prevent the base test class cases from being
+ run as actual tests.
+ """
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ class EditorSettingsFileTestCase(PwIdeTestCase):
+ """Test case for EditorSettingsFile with a provided FileFormat"""
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ def setUp(self):
+ if not hasattr(self, 'file_format'):
+ self.file_format = _StructuredFileFormat()
+ return super().setUp()
- settings_dict = settings_file.get()
- self.assertEqual(settings_dict['hello'], 'world')
+ def test_open_new_file_and_write(self):
+ name = 'settings'
+ settings_file = EditorSettingsFile(
+ self.temp_dir_path, name, self.file_format
+ )
- backup_files = [
- path
- for path in self.temp_dir_path.iterdir()
- if path.name != f'{name}.{json_fmt.ext}'
- ]
+ with settings_file.build() as settings:
+ settings['hello'] = 'world'
- self.assertEqual(len(backup_files), 0)
+ with open(
+ self.temp_dir_path / f'{name}.{self.file_format.ext}'
+ ) as file:
+ settings_dict = self.file_format.load(file)
- with open(backup_files[0]) as file:
- settings_dict = json_fmt.load(file)
+ self.assertEqual(settings_dict['hello'], 'world')
- self.assertEqual(settings_dict['hello'], 'world')
+ def test_open_new_file_and_get(self):
+ name = 'settings'
+ settings_file = EditorSettingsFile(
+ self.temp_dir_path, name, self.file_format
+ )
- def test_write_bad_file_restore_backup(self):
- name = 'settings'
- json_fmt = JsonFileFormat()
- settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+ with settings_file.build() as settings:
+ settings['hello'] = 'world'
+
+ settings_dict = settings_file.get()
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ class EditorSettingsManagerTestCase(PwIdeTestCase):
+ """Test case for EditorSettingsManager with a provided FileFormat"""
+
+ def setUp(self):
+ if not hasattr(self, 'file_format'):
+ self.file_format = _StructuredFileFormat()
+ return super().setUp()
+
+ def test_settings_merge(self):
+ """Test that settings merge as expected in isolation."""
+ default_settings = OrderedDict(
+ {
+ 'foo': 'bar',
+ 'baz': 'qux',
+ 'lorem': OrderedDict(
+ {
+ 'ipsum': 'dolor',
+ }
+ ),
+ }
+ )
- with settings_file.modify() as settings:
- settings['hello'] = 'world'
+ types_with_defaults = {
+ EditorSettingsTestType.SETTINGS: lambda _: default_settings
+ }
- with self.assertRaises(TypeError):
- with settings_file.modify() as settings:
- settings['hello'] = object()
+ ide_settings = self.make_ide_settings()
+ manager = EditorSettingsManager(
+ ide_settings,
+ self.temp_dir_path,
+ self.file_format,
+ types_with_defaults,
+ )
- settings_dict = settings_file.get()
- self.assertEqual(settings_dict['hello'], 'world')
+ project_settings = OrderedDict(
+ {
+ 'alpha': 'beta',
+ 'baz': 'xuq',
+ 'foo': 'rab',
+ }
+ )
- backup_files = [
- path
- for path in self.temp_dir_path.iterdir()
- if path.name != f'{name}.{json_fmt.ext}'
- ]
+ with manager.project(
+ EditorSettingsTestType.SETTINGS
+ ).build() as settings:
+ dict_deep_merge(project_settings, settings)
+
+ user_settings = OrderedDict(
+ {
+ 'baz': 'xqu',
+ 'lorem': OrderedDict(
+ {
+ 'ipsum': 'sit amet',
+ 'consectetur': 'adipiscing',
+ }
+ ),
+ }
+ )
- self.assertEqual(len(backup_files), 0)
+ with manager.user(
+ EditorSettingsTestType.SETTINGS
+ ).build() as settings:
+ dict_deep_merge(user_settings, settings)
+ expected = {
+ 'alpha': 'beta',
+ 'foo': 'rab',
+ 'baz': 'xqu',
+ 'lorem': {
+ 'ipsum': 'sit amet',
+ 'consectetur': 'adipiscing',
+ },
+ }
-class EditorSettingsTestType(Enum):
- SETTINGS = 'settings'
+ with manager.active(
+ EditorSettingsTestType.SETTINGS
+ ).build() as active_settings:
+ manager.default(EditorSettingsTestType.SETTINGS).sync_to(
+ active_settings
+ )
+ manager.project(EditorSettingsTestType.SETTINGS).sync_to(
+ active_settings
+ )
+ manager.user(EditorSettingsTestType.SETTINGS).sync_to(
+ active_settings
+ )
+
+ self.assertCountEqual(
+ manager.active(EditorSettingsTestType.SETTINGS).get(), expected
+ )
-class TestEditorSettingsManager(PwIdeTestCase):
- """Tests EditorSettingsManager"""
+class TestEditorSettingsFileJsonFormat(
+ TestCasesGenericOnFileFormat.EditorSettingsFileTestCase
+):
+ """Test EditorSettingsFile with JsonFormat"""
- def test_settings_merge(self):
- """Test that settings merge as expected in isolation."""
- default_settings = OrderedDict(
- {
- 'foo': 'bar',
- 'baz': 'qux',
- 'lorem': OrderedDict(
- {
- 'ipsum': 'dolor',
- }
- ),
- }
- )
+ def setUp(self):
+ self.file_format = JsonFileFormat()
+ return super().setUp()
- types_with_defaults = {
- EditorSettingsTestType.SETTINGS: lambda _: default_settings
- }
- ide_settings = self.make_ide_settings()
- json_fmt = JsonFileFormat()
- manager = EditorSettingsManager(
- ide_settings, self.temp_dir_path, json_fmt, types_with_defaults
- )
+class TestEditorSettingsManagerJsonFormat(
+ TestCasesGenericOnFileFormat.EditorSettingsManagerTestCase
+):
+ """Test EditorSettingsManager with JsonFormat"""
- project_settings = OrderedDict(
- {
- 'alpha': 'beta',
- 'baz': 'xuq',
- 'foo': 'rab',
- }
- )
+ def setUp(self):
+ self.file_format = JsonFileFormat()
+ return super().setUp()
- with manager.project(
- EditorSettingsTestType.SETTINGS
- ).modify() as settings:
- dict_deep_merge(project_settings, settings)
- user_settings = OrderedDict(
- {
- 'baz': 'xqu',
- 'lorem': OrderedDict(
- {
- 'ipsum': 'sit amet',
- 'consectetur': 'adipiscing',
- }
- ),
- }
- )
+class TestEditorSettingsFileJson5Format(
+ TestCasesGenericOnFileFormat.EditorSettingsFileTestCase
+):
+ """Test EditorSettingsFile with Json5Format"""
- with manager.user(EditorSettingsTestType.SETTINGS).modify() as settings:
- dict_deep_merge(user_settings, settings)
+ def setUp(self):
+ self.file_format = Json5FileFormat()
+ return super().setUp()
- expected = {
- 'alpha': 'beta',
- 'foo': 'rab',
- 'baz': 'xqu',
- 'lorem': {
- 'ipsum': 'sit amet',
- 'consectetur': 'adipiscing',
- },
- }
- with manager.active(
- EditorSettingsTestType.SETTINGS
- ).modify() as active_settings:
- manager.default(EditorSettingsTestType.SETTINGS).sync_to(
- active_settings
- )
- manager.project(EditorSettingsTestType.SETTINGS).sync_to(
- active_settings
- )
- manager.user(EditorSettingsTestType.SETTINGS).sync_to(
- active_settings
- )
+class TestEditorSettingsManagerJson5Format(
+ TestCasesGenericOnFileFormat.EditorSettingsManagerTestCase
+):
+ """Test EditorSettingsManager with Json5Format"""
- self.assertCountEqual(
- manager.active(EditorSettingsTestType.SETTINGS).get(), expected
- )
+ def setUp(self):
+ self.file_format = Json5FileFormat()
+ return super().setUp()
+
+
+class TestEditorSettingsFileYamlFormat(
+ TestCasesGenericOnFileFormat.EditorSettingsFileTestCase
+):
+ """Test EditorSettingsFile with YamlFormat"""
+
+ def setUp(self):
+ self.file_format = YamlFileFormat()
+ return super().setUp()
+
+
+class TestEditorSettingsManagerYamlFormat(
+ TestCasesGenericOnFileFormat.EditorSettingsManagerTestCase
+):
+ """Test EditorSettingsManager with YamlFormat"""
+
+ def setUp(self):
+ self.file_format = YamlFileFormat()
+ return super().setUp()
if __name__ == '__main__':
diff --git a/pw_ide/py/pw_ide/activate.py b/pw_ide/py/pw_ide/activate.py
index 082543bde..79d874205 100644
--- a/pw_ide/py/pw_ide/activate.py
+++ b/pw_ide/py/pw_ide/activate.py
@@ -146,7 +146,7 @@ def _sanitize_path(
user_home = Path.home().resolve()
resolved_path = Path(path).resolve()
- # TODO(b/248257406) Remove once we drop support for Python 3.8.
+ # TODO: b/248257406 - Remove once we drop support for Python 3.8.
def is_relative_to(path: Path, other: Path) -> bool:
try:
path.relative_to(other)
@@ -458,9 +458,15 @@ def main() -> int:
return 0
if args.exec is not None:
+ # Ensure that the command is always dequoted.
+ # When executed directly from the shell, this is already done by
+ # default. But in other contexts, the command may be passed more
+ # literally with whitespace and quotes, which won't work.
+ exec_cmd = args.exec.strip(" '")
+
# We're executing a command in a subprocess with the modified env.
return subprocess.run(
- args.exec, env=modified_env.env, shell=True
+ exec_cmd, env=modified_env.env, shell=True
).returncode
# If we got here, we're trying to modify the current shell's env.
diff --git a/pw_ide/py/pw_ide/cli.py b/pw_ide/py/pw_ide/cli.py
index dd3928b6d..7db4c6143 100644
--- a/pw_ide/py/pw_ide/cli.py
+++ b/pw_ide/py/pw_ide/cli.py
@@ -16,16 +16,14 @@
import argparse
import enum
from inspect import cleandoc
-from pathlib import Path
import re
from typing import Any, Callable, Dict, List, Optional, Protocol
from pw_ide.commands import (
- cmd_clear,
cmd_cpp,
cmd_python,
- cmd_reset,
cmd_setup,
+ cmd_sync,
cmd_vscode,
)
@@ -264,86 +262,65 @@ def _build_argument_parser() -> argparse.ArgumentParser:
func=lambda *_args, **_kwargs: parser_root.print_help()
)
+ parser_root.add_argument(
+ '-o',
+ '--output',
+ choices=['stdout', 'log'],
+ default='pretty',
+ help='where program output should go',
+ )
+
subcommand_parser = parser_root.add_subparsers(help='Subcommands')
add_parser = _parser_adder(subcommand_parser)
+ add_parser(cmd_sync, 'sync')
add_parser(cmd_setup, 'setup')
- parser_reset = add_parser(cmd_reset, 'reset')
- parser_reset.add_argument(
- '--hard',
- action='store_true',
- help='completely remove the .pw_ide working '
- 'dir and supported editor files',
- )
-
parser_cpp = add_parser(cmd_cpp, 'cpp')
parser_cpp.add_argument(
'-l',
'--list',
dest='should_list_targets',
action='store_true',
- help='list the targets available for C/C++ ' 'language analysis',
+ help='list the target toolchains available for C/C++ language analysis',
)
parser_cpp.add_argument(
'-g',
'--get',
dest='should_get_target',
action='store_true',
- help='print the current target used for C/C++ ' 'language analysis',
+ help=(
+ 'print the current target toolchain '
+ 'used for C/C++ language analysis'
+ ),
)
parser_cpp.add_argument(
'-s',
'--set',
dest='target_to_set',
metavar='TARGET',
- help='set the target to use for C/C++ language ' 'server analysis',
+ help=(
+ 'set the target toolchain to '
+ 'use for C/C++ language server analysis'
+ ),
)
parser_cpp.add_argument(
'--set-default',
dest='use_default_target',
action='store_true',
- help='set the C/C++ analysis target to the default '
- 'defined in pw_ide settings',
- )
- parser_cpp.add_argument(
- '--no-override',
- dest='override_current_target',
- action='store_const',
- const=False,
- default=True,
- help='if called with --set, don\'t override the '
- 'current target if one is already set',
- )
- parser_cpp.add_argument(
- '--ninja',
- dest='should_run_ninja',
- action='store_true',
- help='use Ninja to generate a compilation database',
- )
- parser_cpp.add_argument(
- '--gn',
- dest='should_run_gn',
- action='store_true',
- help='run gn gen {out} --export-compile-commands, '
- 'along with any other arguments defined in args.gn',
+ help=(
+ 'set the C/C++ analysis target toolchain to the default '
+ 'defined in pw_ide settings'
+ ),
)
parser_cpp.add_argument(
'-p',
'--process',
- dest='compdb_file_paths',
- metavar='COMPILATION_DATABASE_FILES',
- type=Path,
- nargs='*',
+ action='store_true',
help='process a file or several files matching '
'the clang compilation database format',
)
parser_cpp.add_argument(
- '--build-dir',
- type=Path,
- help='override the build directory defined in ' 'pw_ide settings',
- )
- parser_cpp.add_argument(
'--clangd-command',
action='store_true',
help='print the command for your system that runs '
@@ -363,7 +340,12 @@ def _build_argument_parser() -> argparse.ArgumentParser:
'--venv',
dest='should_print_venv',
action='store_true',
- help='print the path to the Pigweed Python ' 'virtual environment',
+ help='print the path to the Pigweed Python virtual environment',
+ )
+ parser_python.add_argument(
+ '--install-editable',
+ metavar='MODULE',
+ help='install a Pigweed Python module in editable mode',
)
parser_vscode = add_parser(cmd_vscode, 'vscode')
@@ -382,34 +364,10 @@ def _build_argument_parser() -> argparse.ArgumentParser:
help='do not update these settings types',
)
parser_vscode.add_argument(
- '--no-override',
- action='store_true',
- help='don\'t overwrite existing active ' 'settings files',
- )
-
- parser_clear = add_parser(cmd_clear, 'clear')
- parser_clear.add_argument(
- '--compdb',
- action='store_true',
- help='delete all compilation database from ' 'the working directory',
- )
- parser_clear.add_argument(
- '--cache',
+ '--install-extension',
+ dest='should_install_extension',
action='store_true',
- help='delete all compilation database caches '
- 'from the working directory',
- )
- parser_clear.add_argument(
- '--editor',
- metavar='EDITOR',
- help='delete the active settings file for '
- 'the provided supported editor',
- )
- parser_clear.add_argument(
- '--editor-backups',
- metavar='EDITOR',
- help='delete backup settings files for '
- 'the provided supported editor',
+ help='install the experimental extension',
)
return parser_root
diff --git a/pw_ide/py/pw_ide/commands.py b/pw_ide/py/pw_ide/commands.py
index 09785f5a5..393acfaed 100644
--- a/pw_ide/py/pw_ide/commands.py
+++ b/pw_ide/py/pw_ide/commands.py
@@ -19,19 +19,19 @@ import shlex
import shutil
import subprocess
import sys
-from typing import cast, Callable, List, Optional, Set, Tuple, Union
+from typing import cast, Dict, List, Optional, Set, Tuple
-from pw_cli.color import colors
+from pw_cli.env import pigweed_environment
from pw_ide.cpp import (
+ COMPDB_FILE_NAME,
ClangdSettings,
- compdb_generate_file_path,
CppCompilationDatabase,
+ CppCompilationDatabaseFileHashes,
+ CppCompilationDatabaseFileTargets,
CppCompilationDatabasesMap,
CppIdeFeaturesState,
- delete_compilation_databases,
- delete_compilation_database_caches,
- MAX_COMMANDS_TARGET_FILENAME,
+ CppIdeFeaturesTarget,
)
from pw_ide.exceptions import (
@@ -45,147 +45,35 @@ from pw_ide.python import PythonPaths
from pw_ide.settings import (
PigweedIdeSettings,
SupportedEditor,
- SupportedEditorName,
)
-from pw_ide import vscode
-from pw_ide.vscode import VscSettingsManager, VscSettingsType
-
-
-def _no_color(msg: str) -> str:
- return msg
-
-
-def _split_lines(msg: Union[str, List[str]]) -> Tuple[str, List[str]]:
- """Turn a list of strings into a tuple of the first and list of rest."""
- if isinstance(msg, str):
- return (msg, [])
-
- return (msg[0], msg[1:])
-
-
-class StatusReporter:
- """Print user-friendly status reports to the terminal for CLI tools.
-
- The output of ``demo()`` looks something like this, but more colorful:
-
- .. code-block:: none
-
- • FYI, here's some information:
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- Donec condimentum metus molestie metus maximus ultricies ac id dolor.
- ✓ This is okay, no changes needed.
- ✓ We changed some things successfully!
- ⚠ Uh oh, you might want to be aware of this.
- ❌ This is bad! Things might be broken!
-
- You can instead redirect these lines to logs without formatting by
- substituting ``LoggingStatusReporter``. Consumers of this should be
- designed to take any subclass and not make assumptions about where the
- output will go. But the reason you would choose this over plain logging is
- because you want to support pretty-printing to the terminal.
-
- This is also "themable" in the sense that you can subclass this, override
- the methods with whatever formatting you want, and supply the subclass to
- anything that expects an instance of this.
-
- Key:
-
- - info: Plain ol' informational status.
- - ok: Something was checked and it was okay.
- - new: Something needed to be changed/updated and it was successfully.
- - wrn: Warning, non-critical.
- - err: Error, critical.
-
- This doesn't expose the %-style string formatting that is used in idiomatic
- Python logging, but this shouldn't be used for performance-critical logging
- situations anyway.
- """
+from pw_ide.status_reporter import LoggingStatusReporter, StatusReporter
- def _report( # pylint: disable=no-self-use
- self,
- msg: Union[str, List[str]],
- color: Callable[[str], str],
- char: str,
- func: Callable,
- silent: bool,
- ) -> None:
- """Actually print/log/whatever the status lines."""
- first_line, rest_lines = _split_lines(msg)
- first_line = color(f'{char} {first_line}')
- spaces = ' ' * len(char)
- rest_lines = [color(f'{spaces} {line}') for line in rest_lines]
-
- if not silent:
- for line in [first_line, *rest_lines]:
- func(line)
-
- def demo(self):
- """Run this to see what your status reporter output looks like."""
- self.info(
- [
- 'FYI, here\'s some information:',
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
- 'Donec condimentum metus molestie metus maximus ultricies '
- 'ac id dolor.',
- ]
- )
- self.ok('This is okay, no changes needed.')
- self.new('We changed some things successfully!')
- self.wrn('Uh oh, you might want to be aware of this.')
- self.err('This is bad! Things might be broken!')
-
- def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, _no_color, '\u2022', print, silent)
-
- def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, colors().blue, '\u2713', print, silent)
-
- def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, colors().green, '\u2713', print, silent)
-
- def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, colors().yellow, '\u26A0', print, silent)
-
- def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, colors().red, '\u274C', print, silent)
-
-
-class LoggingStatusReporter(StatusReporter):
- """Print status lines to logs instead of to the terminal."""
-
- def __init__(self, logger: logging.Logger) -> None:
- self.logger = logger
- super().__init__()
+from pw_ide import vscode
+from pw_ide.vscode import (
+ install_extension_from_vsix,
+ VscSettingsManager,
+ VscSettingsType,
+)
- def _report(
- self,
- msg: Union[str, List[str]],
- color: Callable[[str], str],
- char: str,
- func: Callable,
- silent: bool,
- ) -> None:
- first_line, rest_lines = _split_lines(msg)
+_LOG = logging.getLogger(__package__)
+env = pigweed_environment()
- if not silent:
- for line in [first_line, *rest_lines]:
- func(line)
- def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, _no_color, '', self.logger.info, silent)
+def _inject_reporter(func):
+ """Inject a status reporter instance based on selected output type."""
- def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, _no_color, '', self.logger.info, silent)
+ def wrapped(*args, **kwargs):
+ output = kwargs.pop('output', 'stdout')
+ reporter = StatusReporter()
- def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, _no_color, '', self.logger.info, silent)
+ if output == 'log':
+ reporter = LoggingStatusReporter(_LOG)
- def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, _no_color, '', self.logger.warning, silent)
+ kwargs['reporter'] = reporter
+ return func(*args, **kwargs)
- def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
- self._report(msg, _no_color, '', self.logger.error, silent)
+ return wrapped
def _make_working_dir(
@@ -193,14 +81,9 @@ def _make_working_dir(
) -> None:
if not settings.working_dir.exists():
settings.working_dir.mkdir()
- reporter.new(
- 'Initialized the Pigweed IDE working directory at '
- f'{settings.working_dir}'
- )
- else:
if not quiet:
- reporter.ok(
- 'Pigweed IDE working directory already present at '
+ reporter.new(
+ 'Initialized the Pigweed IDE working directory at '
f'{settings.working_dir}'
)
@@ -212,101 +95,12 @@ def _report_unrecognized_editor(reporter: StatusReporter, editor: str) -> None:
reporter.wrn(f'Automatically-supported editors: {supported_editors}')
-def cmd_clear(
- compdb: bool,
- cache: bool,
- editor: Optional[SupportedEditorName],
- editor_backups: Optional[SupportedEditorName],
- silent: bool = False,
- reporter: StatusReporter = StatusReporter(),
- pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
-) -> None:
- """Clear components of the IDE features.
-
- In contrast to the ``reset`` subcommand, ``clear`` allows you to specify
- components to delete. You will not need this command under normal
- circumstances.
- """
- if compdb:
- delete_compilation_databases(pw_ide_settings)
- reporter.wrn('Cleared compilation databases', silent)
-
- if cache:
- delete_compilation_database_caches(pw_ide_settings)
- reporter.wrn('Cleared compilation database caches', silent)
-
- if editor is not None:
- try:
- validated_editor = SupportedEditor(editor)
- except ValueError:
- _report_unrecognized_editor(reporter, cast(str, editor))
- sys.exit(1)
-
- if validated_editor == SupportedEditor.VSCODE:
- vsc_settings_manager = VscSettingsManager(pw_ide_settings)
- vsc_settings_manager.delete_all_active_settings()
-
- reporter.wrn(
- f'Cleared active settings for {validated_editor.value}', silent
- )
-
- if editor_backups is not None:
- try:
- validated_editor = SupportedEditor(editor_backups)
- except ValueError:
- _report_unrecognized_editor(reporter, cast(str, editor))
- sys.exit(1)
-
- if validated_editor == SupportedEditor.VSCODE:
- vsc_settings_manager = VscSettingsManager(pw_ide_settings)
- vsc_settings_manager.delete_all_backups()
-
- reporter.wrn(
- f'Cleared backup settings for {validated_editor.value}',
- silent=silent,
- )
-
-
-def cmd_reset(
- hard: bool = False,
- reporter: StatusReporter = StatusReporter(),
- pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
-) -> None:
- """Reset IDE settings.
-
- This will clear your .pw_ide working directory and active settings for
- supported editors, restoring your repository to a pre-"pw ide setup" state.
- Any clangd caches in the working directory will not be removed, so that they
- don't need to be generated again later. All backed up supported editor
- settings will also be left in place.
-
- Adding the --hard flag will completely delete the .pw_ide directory and all
- supported editor backup settings, restoring your repository to a
- pre-`pw ide setup` state.
-
- This command does not affect this project's pw_ide and editor settings or
- your own pw_ide and editor override settings.
- """
- delete_compilation_databases(pw_ide_settings)
- vsc_settings_manager = VscSettingsManager(pw_ide_settings)
- vsc_settings_manager.delete_all_active_settings()
-
- if hard:
- try:
- shutil.rmtree(pw_ide_settings.working_dir)
- except FileNotFoundError:
- pass
-
- vsc_settings_manager.delete_all_backups()
-
- reporter.wrn('Pigweed IDE settings were reset!')
-
-
-def cmd_setup(
+@_inject_reporter
+def cmd_sync(
reporter: StatusReporter = StatusReporter(),
pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
- """Set up or update your Pigweed project IDE features.
+ """Setup or sync your Pigweed project IDE features.
This will automatically set up your development environment with all the
features that Pigweed IDE supports, with sensible defaults.
@@ -319,19 +113,36 @@ def cmd_setup(
your downstream project), you can re-run this command to set up the new
features. It will not overwrite or break any of your existing configuration.
"""
+ reporter.info('Syncing pw_ide...')
_make_working_dir(reporter, pw_ide_settings)
+ for command in pw_ide_settings.sync:
+ _LOG.debug("Running: %s", command)
+ subprocess.run(shlex.split(command))
+
if pw_ide_settings.editor_enabled('vscode'):
cmd_vscode()
- for command in pw_ide_settings.setup:
- subprocess.run(shlex.split(command))
+ reporter.info('Done')
+@_inject_reporter
+def cmd_setup(
+ reporter: StatusReporter = StatusReporter(),
+ pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
+) -> None:
+ """Deprecated! Please use `pw ide sync`."""
+ reporter.wrn(
+ "The `setup` command is now `sync`. Next time, run `pw ide sync`."
+ )
+ cmd_sync(reporter, pw_ide_settings)
+
+
+@_inject_reporter
def cmd_vscode(
include: Optional[List[VscSettingsType]] = None,
exclude: Optional[List[VscSettingsType]] = None,
- no_override: bool = False,
+ should_install_extension: bool = False,
reporter: StatusReporter = StatusReporter(),
pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
) -> None:
@@ -391,7 +202,12 @@ def cmd_vscode(
default.
"""
if not pw_ide_settings.editor_enabled('vscode'):
- reporter.wrn('Visual Studio Code support is disabled in settings!')
+ reporter.wrn(
+ 'Visual Studio Code support is disabled in settings! If this is '
+ 'unexpected, see this page for information on enabling support: '
+ 'https://pigweed.dev/pw_ide/'
+ '#pw_ide.settings.PigweedIdeSettings.editors'
+ )
sys.exit(1)
if not vscode.DEFAULT_SETTINGS_PATH.exists():
@@ -399,55 +215,313 @@ def cmd_vscode(
vsc_manager = VscSettingsManager(pw_ide_settings)
- if include is None:
+ if include is None and exclude is None:
include_set = set(VscSettingsType.all())
- else:
- include_set = set(include)
-
- if exclude is None:
exclude_set: Set[VscSettingsType] = set()
+
+ elif include is None:
+ include_set = set(VscSettingsType.all())
+ exclude_set = set(exclude if exclude is not None else [])
+
+ elif exclude is None:
+ include_set = set(include if include is not None else [])
+ exclude_set = set()
+
else:
- exclude_set = set(exclude)
+ include_set = set(include if include is not None else [])
+ exclude_set = set(exclude if exclude is not None else [])
types_to_update = cast(
List[VscSettingsType], tuple(include_set - exclude_set)
)
for settings_type in types_to_update:
+ prev_settings_hash = ''
active_settings_existed = vsc_manager.active(settings_type).is_present()
- if no_override and active_settings_existed:
- reporter.ok(
- f'Visual Studio Code active {settings_type.value} '
- 'already present; will not overwrite'
- )
+ if active_settings_existed:
+ prev_settings_hash = vsc_manager.active(settings_type).hash()
- else:
- with vsc_manager.active(settings_type).modify(
- reinit=True
- ) as active_settings:
- vsc_manager.default(settings_type).sync_to(active_settings)
- vsc_manager.project(settings_type).sync_to(active_settings)
- vsc_manager.user(settings_type).sync_to(active_settings)
+ with vsc_manager.active(settings_type).build() as active_settings:
+ vsc_manager.default(settings_type).sync_to(active_settings)
+ vsc_manager.project(settings_type).sync_to(active_settings)
+ vsc_manager.user(settings_type).sync_to(active_settings)
+ new_settings_hash = vsc_manager.active(settings_type).hash()
+ settings_changed = new_settings_hash != prev_settings_hash
+
+ _LOG.debug(
+ 'VS Code %s prev hash: %s',
+ settings_type.name.lower(),
+ prev_settings_hash,
+ )
+ _LOG.debug(
+ 'VS Code %s curr hash: %s',
+ settings_type.name.lower(),
+ new_settings_hash,
+ )
+
+ if settings_changed:
verb = 'Updated' if active_settings_existed else 'Created'
reporter.new(
f'{verb} Visual Studio Code active ' f'{settings_type.value}'
)
+ if should_install_extension:
+ reporter.new("Installing Visual Studio Code extension")
+
+ try:
+ install_extension_from_vsix(reporter)
+ except FileNotFoundError:
+ reporter.err("Could not find Visual Studio Code")
+ sys.exit(1)
+ except subprocess.CalledProcessError:
+ reporter.err("Failed to install extension!")
+ sys.exit(1)
+
+
+def _process_compdbs( # pylint: disable=too-many-locals
+ reporter: StatusReporter,
+ pw_ide_settings: PigweedIdeSettings,
+ always_output_new: bool = False,
+):
+ """Find and process compilation databases in the project.
+
+ This essentially does four things:
+ - Find all the compilation databases it can in the build directory
+ - For any databases we've seen before and are unchanged, skip them
+ - For any we haven't seed before or are changed, process them
+ - Save the state to disk so that other commands can examine/change targets
+ """
+
+ state = CppIdeFeaturesState(pw_ide_settings)
+
+ # If a compilation database was seen before and is unchanged, or if it's new
+ # and we process it, it will end up in the new hashes dict. If we saw it
+ # in the past but it no longer exists, it will not move over to the new
+ # hashes dict.
+ prev_compdb_hashes = state.compdb_hashes
+ new_compdb_hashes: CppCompilationDatabaseFileHashes = {}
+ prev_compdb_targets = state.compdb_targets
+ new_compdb_targets: CppCompilationDatabaseFileTargets = {}
+
+ targets: List[CppIdeFeaturesTarget] = []
+ num_new_unprocessed_targets = 0
+ num_new_processed_targets = 0
+ num_carried_over_targets = 0
+ num_removed_targets = len(state.targets.values())
+
+ unprocessed_compdb_files: List[Path] = []
+ processed_compdb_files: List[Path] = []
+
+ # Associate processed compilation databases with their original sources
+ all_processed_compdbs: Dict[Path, CppCompilationDatabasesMap] = {}
+
+ # Get a list of paths to search for compilation databases.
+ compdb_search_paths: List[
+ Tuple[Path, str]
+ ] = pw_ide_settings.compdb_search_paths
+ # Get the list of files for each search path, tupled with the search path.
+ compdb_file_path_groups = [
+ (search_path, list(search_path[0].rglob(str(COMPDB_FILE_NAME))))
+ for search_path in compdb_search_paths
+ ]
+ # Flatten that list.
+ compdb_file_paths: List[Tuple[Path, Path, str]] = [
+ (search_path, file_path, target_inference)
+ for (
+ (search_path, target_inference),
+ file_path_group,
+ ) in compdb_file_path_groups
+ for file_path in file_path_group
+ ]
+
+ for (
+ compdb_root_dir,
+ compdb_file_path,
+ target_inference,
+ ) in compdb_file_paths:
+ # Load the compilation database
+ try:
+ compdb = CppCompilationDatabase.load(
+ compdb_to_load=compdb_file_path,
+ root_dir=compdb_root_dir,
+ target_inference=target_inference,
+ )
+ except MissingCompDbException:
+ reporter.err(f'File not found: {str(compdb_file_path)}')
+ sys.exit(1)
+ # TODO(chadnorvell): Recover more gracefully from errors.
+ except BadCompDbException:
+ reporter.err(
+ 'File does not match compilation database format: '
+ f'{str(compdb_file_path)}'
+ )
+ sys.exit(1)
+
+ # Check the hash of the compilation database against our cache of
+ # database hashes. Have we see this before and is the hash the same?
+ # Then we can skip this database.
+ if (
+ compdb_file_path in prev_compdb_hashes
+ and compdb.file_hash == prev_compdb_hashes[compdb_file_path]
+ ):
+ # Store this hash in the new hash registry.
+ new_compdb_hashes[compdb_file_path] = compdb.file_hash
+ # Copy the targets associated with this file...
+ new_compdb_targets[compdb_file_path] = prev_compdb_targets[
+ compdb_file_path
+ ]
+ # ... and add them to the targets list.
+ targets.extend(new_compdb_targets[compdb_file_path])
+ num_carried_over_targets += len(
+ new_compdb_targets[compdb_file_path]
+ )
+ num_removed_targets -= len(new_compdb_targets[compdb_file_path])
+ continue
+
+ # We haven't seen this database before. Process it.
+ processed_compdbs = compdb.process(
+ settings=pw_ide_settings,
+ path_globs=pw_ide_settings.clangd_query_drivers(),
+ always_output_new=always_output_new,
+ )
+
+ # The source database doesn't actually need processing, so use it as is.
+ if processed_compdbs is None:
+ # Infer the name of the target from the path
+ name = '_'.join(
+ compdb_file_path.relative_to(compdb_root_dir).parent.parts
+ )
+
+ target = CppIdeFeaturesTarget(
+ name=name,
+ compdb_file_path=compdb_file_path,
+ num_commands=len(
+ CppCompilationDatabase.load(
+ compdb_file_path, compdb_root_dir
+ )
+ ),
+ )
+
+ # An unprocessed database will have only one target.
+ new_compdb_targets[compdb_file_path] = [target]
+ unprocessed_compdb_files.append(compdb_file_path)
+ targets.append(target)
+ num_new_unprocessed_targets += 1
+
+ # Remember that we've seen this database.
+ new_compdb_hashes[compdb_file_path] = compdb.file_hash
+
+ else:
+ # We need to use the processed databases, so store them for writing.
+ # We'll add the targets associated with the processed databases
+ # later.
+ all_processed_compdbs[compdb_file_path] = processed_compdbs
+ processed_compdb_files.append(compdb_file_path)
+
+ if len(all_processed_compdbs) > 0:
+ # Merge into one map of target names to compilation database.
+ merged_compdbs = CppCompilationDatabasesMap.merge(
+ *all_processed_compdbs.values()
+ )
+
+ # Write processed databases to files.
+ try:
+ merged_compdbs.write()
+ except TypeError:
+ reporter.err('Could not serialize file to JSON!')
+ reporter.wrn('pw_ide state will not be persisted.')
+ return False
+
+ # Grab the target and file info from the processed databases.
+ for target_name, compdb in merged_compdbs.items():
+ target = CppIdeFeaturesTarget(
+ name=target_name,
+ compdb_file_path=cast(Path, compdb.file_path),
+ num_commands=len(compdb),
+ )
+
+ targets.append(target)
+ num_new_processed_targets += 1
+
+ if (
+ source := cast(Path, compdb.source_file_path)
+ ) not in new_compdb_targets:
+ new_compdb_targets[source] = [target]
+ new_compdb_hashes[source] = cast(str, compdb.source_file_hash)
+ else:
+ new_compdb_targets[source].append(target)
+
+ # Write out state.
+ targets_dict = {target_data.name: target_data for target_data in targets}
+ state.targets = targets_dict
+ state.compdb_hashes = new_compdb_hashes
+ state.compdb_targets = new_compdb_targets
+
+ # If the current target is no longer valid, unset it.
+ if (
+ state.current_target is not None
+ and state.current_target.name not in targets_dict
+ ):
+ state.current_target = None
+
+ num_total_targets = len(targets)
+ num_new_targets = num_new_processed_targets + num_new_unprocessed_targets
+
+ # Report the results.
+ # Return True if anything meaningful changed as a result of the processing.
+ # If the new state is essentially identical to the old state, return False
+ # so the caller can avoid needlessly updating anything else.
+ if num_new_targets > 0 or num_removed_targets > 0:
+ found_compdb_text = (
+ f'Found {len(compdb_file_paths)} compilation database'
+ )
+
+ if len(compdb_file_paths) > 1:
+ found_compdb_text += 's'
+
+ reporter.ok(found_compdb_text)
+
+ reporter_lines = []
+
+ if len(unprocessed_compdb_files) > 0:
+ reporter_lines.append(
+ f'Linked {len(unprocessed_compdb_files)} '
+ 'unmodified compilation databases'
+ )
+
+ if len(processed_compdb_files) > 0:
+ working_dir_path = pw_ide_settings.working_dir.relative_to(
+ Path(env.PW_PROJECT_ROOT)
+ )
+ reporter_lines.append(
+ f'Processed {len(processed_compdb_files)} to working dir at '
+ f'{working_dir_path}'
+ )
-# TODO(chadnorvell): Break up this function.
-# The linting errors are a nuisance but they're beginning to have a point.
+ if len(reporter_lines) > 0:
+ reporter_lines.extend(
+ [
+ f'{num_total_targets} targets are now available '
+ f'({num_new_targets} are new, '
+ f'{num_removed_targets} were removed)',
+ ]
+ )
+
+ reporter.new(reporter_lines)
+
+ return True
+ return False
+
+
+@_inject_reporter
def cmd_cpp( # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
should_list_targets: bool,
should_get_target: bool,
target_to_set: Optional[str],
- compdb_file_paths: Optional[List[Path]],
- build_dir: Optional[Path],
+ process: bool = True,
use_default_target: bool = False,
- should_run_ninja: bool = False,
- should_run_gn: bool = False,
- override_current_target: bool = True,
clangd_command: bool = False,
clangd_command_system: Optional[str] = None,
reporter: StatusReporter = StatusReporter(),
@@ -459,18 +533,28 @@ def cmd_cpp( # pylint: disable=too-many-arguments, too-many-locals, too-many-br
use the clangd compilation database format, defined at:
https://clang.llvm.org/docs/JSONCompilationDatabase.html
- This command helps you use clangd with Pigweed projects, which use multiple
- toolchains within a distinct environment, and often define multiple targets.
- This means compilation units are likely have multiple compile commands, and
- clangd is not equipped to deal with this out of the box. We handle this by:
+ Pigweed projects define their build configuration(s) via a build system,
+ usually GN, Bazel, or CMake. Based on build configurations, the build
+ system generates commands to compile each translation unit in the project.
+ clangd uses those commands to parse the build graph and provide rich code
+ intelligence.
+
+ Pigweed projects often target multiple devices & architectures, and use
+ multiple compiler toolchains. As a result, there may be more than one way
+ to compile each translation unit. Your build system ensures that it only
+ invokes a single compiler command for each translation unit which is
+ consistent with the toolchain and target appropriate to that build, which
+ we refer to as a "target toolchain".
+
+ We need to do the same thing with the compilation database that clangd uses.
+ We handle this by:
- Processing the compilation database produced the build system into
- multiple internally-consistent compilation databases, one for each target
- (where a "target" is a particular build for a particular system using a
- particular toolchain).
+ multiple internally-consistent compilation databases, one for each
+ target toolchain.
- - Providing commands to select which target you want to use for code
- analysis.
+ - Providing commands to select which target toolchain you want to use for
+ code analysis.
Refer to the Pigweed documentation or your build system's documentation to
learn how to produce a clangd compilation database. Once you have one, run
@@ -480,49 +564,39 @@ def cmd_cpp( # pylint: disable=too-many-arguments, too-many-locals, too-many-br
pw ide cpp --process {path to compile_commands.json}
- If you're using GN to generate the compilation database, you can do that and
- process it in a single command:
-
- .. code-block:: bash
-
- pw ide cpp --gn
-
- You can do the same for a Ninja build (whether it was generated by GN or
- another way):
-
- .. code-block:: bash
-
- pw ide cpp --ninja
-
- You can now examine the targets that are available to you:
+ You can now examine the target toolchains that are available to you:
.. code-block:: bash
pw ide cpp --list
- ... and select the target you want to use:
+ ... and select the target toolchain you want to use:
.. code-block:: bash
pw ide cpp --set host_clang
As long as your editor or language server plugin is properly configured, you
- will now get code intelligence features relevant to that particular target.
+ will now get code intelligence features relevant to that particular target
+ toolchain.
- You can see what target is selected by running:
+ You can see what target toolchain is selected by running:
.. code-block:: bash
pw ide cpp
- Whenever you switch to a target you haven't used before, clangd will need to
- index the build, which may take several minutes. These indexes are cached,
- so you can switch between targets without re-indexing each time.
+ Whenever you switch to a target toolchain you haven't used before, clangd
+ will index the build, which may take several minutes. This process is not
+ blocking, so you can take advantage of code analysis immediately even while
+ the indexing is in progress. These indexes are cached, so you can switch
+ between targets without re-indexing each time.
If your build configuration changes significantly (e.g. you add a new file
to the project), you will need to re-process the compilation database for
- that change to be recognized. Your target selection will not change, and
- your index will only need to be incrementally updated.
+ that change to be recognized and manifested in the target toolchain. Your
+ target toolchain selection will not change, and your index will only need to
+ be incrementally updated.
You can generate the clangd command your editor needs to run with:
@@ -542,244 +616,84 @@ def cmd_cpp( # pylint: disable=too-many-arguments, too-many-locals, too-many-br
# If true, no arguments were provided so we do the default behavior.
default = True
- build_dir = (
- build_dir if build_dir is not None else pw_ide_settings.build_dir
- )
-
- if compdb_file_paths is not None:
- should_process = True
+ state = CppIdeFeaturesState(pw_ide_settings)
- if len(compdb_file_paths) == 0:
- compdb_file_paths = pw_ide_settings.compdb_paths_expanded
- else:
- should_process = False
- # This simplifies typing in the rest of this method. We rely on
- # `should_process` instead of the status of this variable.
- compdb_file_paths = []
-
- # Order of operations matters from here on. It should be possible to run
- # a build system command to generate a compilation database, then process
- # the compilation database, then successfully set the target in a single
- # command.
-
- # Use Ninja to generate the initial compile_commands.json
- if should_run_ninja:
+ if process:
default = False
+ _process_compdbs(reporter, pw_ide_settings)
- ninja_commands = ['ninja', '-t', 'compdb']
- reporter.info(f'Running Ninja: {" ".join(ninja_commands)}')
-
- output_compdb_file_path = build_dir / compdb_generate_file_path()
-
- try:
- # Ninja writes to STDOUT, so we capture to a file.
- with open(output_compdb_file_path, 'w') as compdb_file:
- result = subprocess.run(
- ninja_commands,
- cwd=build_dir,
- stdout=compdb_file,
- stderr=subprocess.PIPE,
- )
- except FileNotFoundError:
- reporter.err(f'Could not open path! {str(output_compdb_file_path)}')
-
- if result.returncode == 0:
- reporter.info('Ran Ninja successfully!')
- should_process = True
- compdb_file_paths.append(output_compdb_file_path)
- else:
- reporter.err('Something went wrong!')
- # Convert from bytes and remove trailing newline
- err = result.stderr.decode().split('\n')[:-1]
-
- for line in err:
- reporter.err(line)
-
- sys.exit(1)
-
- # Use GN to generate the initial compile_commands.json
- if should_run_gn:
- default = False
-
- gn_commands = ['gn', 'gen', str(build_dir), '--export-compile-commands']
-
- try:
- with open(build_dir / 'args.gn') as args_file:
- gn_args = [
- line
- for line in args_file.readlines()
- if not line.startswith('#')
- ]
- except FileNotFoundError:
- gn_args = []
-
- gn_args_string = 'none' if len(gn_args) == 0 else ', '.join(gn_args)
-
- reporter.info(
- [f'Running GN: {" ".join(gn_commands)} (args: {gn_args_string})']
- )
-
- result = subprocess.run(gn_commands, capture_output=True)
- gn_status_lines = ['Ran GN successfully!']
-
- if result.returncode == 0:
- # Convert from bytes and remove trailing newline
- out = result.stdout.decode().split('\n')[:-1]
-
- for line in out:
- gn_status_lines.append(line)
-
- reporter.info(gn_status_lines)
- should_process = True
- output_compdb_file_path = build_dir / compdb_generate_file_path()
- compdb_file_paths.append(output_compdb_file_path)
- else:
- reporter.err('Something went wrong!')
- # Convert from bytes and remove trailing newline
- err = result.stderr.decode().split('\n')[:-1]
-
- for line in err:
- reporter.err(line)
-
- sys.exit(1)
-
- if should_process:
- default = False
- prev_targets = len(CppIdeFeaturesState(pw_ide_settings))
- compdb_databases: List[CppCompilationDatabasesMap] = []
- last_processed_path = Path()
-
- for compdb_file_path in compdb_file_paths:
- # If the path is a dir, append the default compile commands
- # file name.
- if compdb_file_path.is_dir():
- compdb_file_path /= compdb_generate_file_path()
-
- try:
- compdb_databases.append(
- CppCompilationDatabase.load(
- Path(compdb_file_path), build_dir
- ).process(
- settings=pw_ide_settings,
- path_globs=pw_ide_settings.clangd_query_drivers(),
- )
- )
- except MissingCompDbException:
- reporter.err(f'File not found: {str(compdb_file_path)}')
-
- if '*' in str(compdb_file_path):
- reporter.wrn(
- 'It looks like you provided a glob that '
- 'did not match any files.'
- )
-
- sys.exit(1)
- # TODO(chadnorvell): Recover more gracefully from errors.
- except BadCompDbException:
- reporter.err(
- 'File does not match compilation database format: '
- f'{str(compdb_file_path)}'
- )
- sys.exit(1)
-
- last_processed_path = compdb_file_path
-
- if len(compdb_databases) == 0:
- reporter.err(
- 'No compilation databases found in: '
- f'{str(compdb_file_paths)}'
- )
- sys.exit(1)
-
- try:
- CppCompilationDatabasesMap.merge(*compdb_databases).write()
- except TypeError:
- reporter.err('Could not serialize file to JSON!')
-
- total_targets = len(CppIdeFeaturesState(pw_ide_settings))
- new_targets = total_targets - prev_targets
-
- if len(compdb_file_paths) == 1:
- processed_text = str(last_processed_path)
- else:
- processed_text = f'{len(compdb_file_paths)} compilation databases'
-
- reporter.new(
- [
- f'Processed {processed_text} '
- f'to {pw_ide_settings.working_dir}',
- f'{total_targets} targets are now available '
- f'({new_targets} are new)',
- ]
- )
+ if state.current_target is None:
+ use_default_target = True
if use_default_target:
defined_default = pw_ide_settings.default_target
- max_commands_target: Optional[str] = None
- try:
- with open(
- pw_ide_settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
- ) as max_commands_target_file:
- max_commands_target = max_commands_target_file.readline()
- except FileNotFoundError:
- pass
-
- if defined_default is None and max_commands_target is None:
- reporter.err('Can\'t use default target because none is defined!')
+ if defined_default is None and state.max_commands_target is None:
+ reporter.err(
+ 'Can\'t use default target toolchain because none is defined!'
+ )
reporter.wrn('Have you processed a compilation database yet?')
sys.exit(1)
+ else:
+ max_commands_target = cast(
+ CppIdeFeaturesTarget, state.max_commands_target
+ )
- target_to_set = (
+ default_target = (
defined_default
if defined_default is not None
- else max_commands_target
+ else max_commands_target.name
)
+ if state.current_target != default:
+ target_to_set = default_target
+
if target_to_set is not None:
default = False
-
- # Always set the target if it's not already set, but if it is,
- # respect the --no-override flag.
- should_set_target = (
- CppIdeFeaturesState(pw_ide_settings).current_target is None
- or override_current_target
+ reporter.info(
+ f'Setting C/C++ analysis target toolchain to: {target_to_set}'
)
- if should_set_target:
- try:
- CppIdeFeaturesState(
- pw_ide_settings
- ).current_target = target_to_set
- except InvalidTargetException:
+ try:
+ CppIdeFeaturesState(
+ pw_ide_settings
+ ).current_target = state.targets.get(target_to_set, None)
+
+ if str(CppIdeFeaturesState(pw_ide_settings).current_target) != str(
+ target_to_set
+ ):
reporter.err(
- [
- f'Invalid target! {target_to_set} not among the '
- 'defined targets.',
- 'Check .pw_ide.yaml or .pw_ide.user.yaml for defined '
- 'targets.',
- ]
+ f'Failed to set target toolchain to {target_to_set}!'
)
- sys.exit(1)
- except MissingCompDbException:
- reporter.err(
+ reporter.wrn(
[
- f'File not found for target! {target_to_set}',
- 'Did you run pw ide cpp --process '
- '{path to compile_commands.json}?',
+ 'You have tried to set a target toolchain '
+ 'that is not available.',
+ 'Run `pw ide cpp --list` to show available '
+ 'target toolchains.',
+ f'If you expected {target_to_set} to be in that list',
+ 'and it is not, you may need to use your build system',
+ 'generate a compilation database.',
]
)
sys.exit(1)
- reporter.new(
- 'Set C/C++ language server analysis target to: '
- f'{target_to_set}'
+ except InvalidTargetException:
+ reporter.err(
+ f'Invalid target toolchain! {target_to_set} not among the '
+ 'defined target toolchains.'
)
- else:
- reporter.ok(
- 'Target already is set and will not be overridden: '
- f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
+ sys.exit(1)
+ except MissingCompDbException:
+ reporter.err(
+ f'File not found for target toolchain! {target_to_set}'
)
+ sys.exit(1)
+
+ reporter.new(
+ 'Set C/C++ language server analysis target toolchain to: '
+ f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
+ )
if clangd_command:
default = False
@@ -803,25 +717,70 @@ def cmd_cpp( # pylint: disable=too-many-arguments, too-many-locals, too-many-br
if should_list_targets:
default = False
targets_list_status = [
- 'C/C++ targets available for language server analysis:'
+ 'C/C++ target toolchains available for language server analysis:'
]
- for target in sorted(
- CppIdeFeaturesState(pw_ide_settings).enabled_available_targets
- ):
+ for target in sorted(CppIdeFeaturesState(pw_ide_settings).targets):
targets_list_status.append(f'\t{target}')
reporter.info(targets_list_status)
if should_get_target or default:
+ current_target = CppIdeFeaturesState(pw_ide_settings).current_target
+ name = 'None' if current_target is None else current_target.name
+
reporter.info(
- 'Current C/C++ language server analysis target: '
- f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
+ 'Current C/C++ language server analysis '
+ f'target toolchain: {name}'
)
+def install_py_module_as_editable(
+ module_name: str,
+ reporter: StatusReporter,
+) -> None:
+ """Install a Pigweed Python module in editable mode."""
+ reporter.info(f'Installing {module_name} as an editable module')
+ try:
+ site_packages_path = [
+ path for path in sys.path if 'site-packages' in path
+ ][0]
+ except IndexError:
+ reporter.err(f'Could not find {module_name} in the Python path!')
+ sys.exit(1)
+
+ reporter.info(f'Found {module_name} at: {site_packages_path}')
+ shutil.rmtree(Path(site_packages_path) / module_name)
+
+ try:
+ subprocess.run(
+ [
+ 'pip',
+ 'install',
+ '--no-deps',
+ '-e',
+ f'{module_name}/py',
+ ],
+ check=True,
+ stdout=subprocess.PIPE,
+ )
+ except subprocess.CalledProcessError:
+ reporter.err(
+ [
+ f'Failed to install {module_name}!',
+ 'You may need to re-bootstrap',
+ ]
+ )
+
+ reporter.new('Success!')
+ reporter.wrn('Note that running bootstrap or building will reverse this.')
+
+
+@_inject_reporter
def cmd_python(
- should_print_venv: bool, reporter: StatusReporter = StatusReporter()
+ should_print_venv: bool,
+ install_editable: Optional[str] = None,
+ reporter: StatusReporter = StatusReporter(),
) -> None:
"""Configure Python code intelligence support.
@@ -831,15 +790,29 @@ def cmd_python(
.. code-block:: bash
pw ide python --venv
+
+ When working on Pigweed's Python modules, it can be convenient to install
+ them in editable mode to instantly realize code changes. You can do this by
+ running:
+
+ .. code-block:: bash
+
+ pw ide python --install-editable pw_{module name}
+
+ Just note that running bootstrap or building will override this.
"""
# If true, no arguments were provided and we should do the default
# behavior.
default = True
+ if install_editable is not None:
+ default = False
+ install_py_module_as_editable(install_editable, reporter)
+
if should_print_venv or default:
reporter.info(
[
'Location of the Pigweed Python virtual environment:',
- PythonPaths().interpreter,
+ str(PythonPaths().interpreter),
]
)
diff --git a/pw_ide/py/pw_ide/cpp.py b/pw_ide/py/pw_ide/cpp.py
index e58d66d74..b8cea99de 100644
--- a/pw_ide/py/pw_ide/cpp.py
+++ b/pw_ide/py/pw_ide/cpp.py
@@ -43,20 +43,24 @@ point at the symlink and is set up with the right paths, you'll get code
intelligence.
"""
-from collections import defaultdict
-from dataclasses import dataclass
+from contextlib import contextmanager
+from dataclasses import asdict, dataclass, field
import glob
+from hashlib import sha1
from io import TextIOBase
import json
-import os
+import logging
from pathlib import Path
import platform
+import random
+import re
+import sys
from typing import (
Any,
cast,
- Callable,
Dict,
Generator,
+ Iterator,
List,
Optional,
Tuple,
@@ -64,6 +68,8 @@ from typing import (
Union,
)
+from pw_cli.env import pigweed_environment
+
from pw_ide.exceptions import (
BadCompDbException,
InvalidTargetException,
@@ -71,105 +77,234 @@ from pw_ide.exceptions import (
UnresolvablePathException,
)
-from pw_ide.settings import PigweedIdeSettings, PW_PIGWEED_CIPD_INSTALL_DIR
+from pw_ide.settings import PigweedIdeSettings
from pw_ide.symlinks import set_symlink
-_COMPDB_FILE_PREFIX = 'compile_commands'
-_COMPDB_FILE_SEPARATOR = '_'
-_COMPDB_FILE_EXTENSION = '.json'
-
-_COMPDB_CACHE_DIR_PREFIX = '.cache'
-_COMPDB_CACHE_DIR_SEPARATOR = '_'
+_LOG = logging.getLogger(__package__)
+env = pigweed_environment()
-COMPDB_FILE_GLOB = f'{_COMPDB_FILE_PREFIX}*{_COMPDB_FILE_EXTENSION}'
-COMPDB_CACHE_DIR_GLOB = f'{_COMPDB_CACHE_DIR_PREFIX}*'
+COMPDB_FILE_NAME = 'compile_commands.json'
+STABLE_CLANGD_DIR_NAME = '.stable'
+_CPP_IDE_FEATURES_DATA_FILE = 'pw_ide_state.json'
+_UNSUPPORTED_TOOLCHAIN_EXECUTABLES = ('_pw_invalid', 'python')
+_SUPPORTED_WRAPPER_EXECUTABLES = ('ccache',)
-MAX_COMMANDS_TARGET_FILENAME = 'max_commands_target'
-_SUPPORTED_TOOLCHAIN_EXECUTABLES = ('clang', 'gcc', 'g++')
+@dataclass(frozen=True)
+class CppIdeFeaturesTarget:
+ """Data pertaining to a C++ code analysis target."""
+ name: str
+ compdb_file_path: Path
+ num_commands: int
+ is_enabled: bool = True
+
+ def __str__(self) -> str:
+ return self.name
+
+ def serialized(self) -> Dict[str, Any]:
+ return {
+ **asdict(self),
+ **{
+ 'compdb_file_path': str(self.compdb_file_path),
+ },
+ }
-def compdb_generate_file_path(target: str = '') -> Path:
- """Generate a compilation database file path."""
+ @classmethod
+ def deserialize(cls, **data) -> 'CppIdeFeaturesTarget':
+ return cls(
+ **{
+ **data,
+ **{
+ 'compdb_file_path': Path(data['compdb_file_path']),
+ },
+ }
+ )
- path = Path(f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}')
- if target:
- path = path.with_name(
- f'{_COMPDB_FILE_PREFIX}'
- f'{_COMPDB_FILE_SEPARATOR}{target}'
- f'{_COMPDB_FILE_EXTENSION}'
- )
+CppCompilationDatabaseFileHashes = Dict[Path, str]
+CppCompilationDatabaseFileTargets = Dict[Path, List[CppIdeFeaturesTarget]]
- return path
+@dataclass
+class CppIdeFeaturesData:
+ """State data about C++ code analysis features."""
-def compdb_generate_cache_path(target: str = '') -> Path:
- """Generate a compilation database cache directory path."""
+ targets: Dict[str, CppIdeFeaturesTarget] = field(default_factory=dict)
+ current_target: Optional[CppIdeFeaturesTarget] = None
+ compdb_hashes: CppCompilationDatabaseFileHashes = field(
+ default_factory=dict
+ )
+ compdb_targets: CppCompilationDatabaseFileTargets = field(
+ default_factory=dict
+ )
- path = Path(f'{_COMPDB_CACHE_DIR_PREFIX}')
+ def serialized(self) -> Dict[str, Any]:
+ return {
+ 'current_target': self.current_target.serialized()
+ if self.current_target is not None
+ else None,
+ 'targets': {
+ name: target_data.serialized()
+ for name, target_data in self.targets.items()
+ },
+ 'compdb_hashes': {
+ str(path): hash_str
+ for path, hash_str in self.compdb_hashes.items()
+ },
+ 'compdb_targets': {
+ str(path): [
+ target_data.serialized() for target_data in target_data_list
+ ]
+ for path, target_data_list in self.compdb_targets.items()
+ },
+ }
- if target:
- path = path.with_name(
- f'{_COMPDB_CACHE_DIR_PREFIX}'
- f'{_COMPDB_CACHE_DIR_SEPARATOR}{target}'
+ @classmethod
+ def deserialize(cls, **data) -> 'CppIdeFeaturesData':
+ return cls(
+ current_target=CppIdeFeaturesTarget.deserialize(
+ **data['current_target']
+ )
+ if data['current_target'] is not None
+ else None,
+ targets={
+ name: CppIdeFeaturesTarget.deserialize(**target_data)
+ for name, target_data in data['targets'].items()
+ },
+ compdb_hashes={
+ Path(path_str): hash_str
+ for path_str, hash_str in data['compdb_hashes'].items()
+ },
+ compdb_targets={
+ Path(path_str): [
+ CppIdeFeaturesTarget.deserialize(**target_data)
+ for target_data in target_data_list
+ ]
+ for path_str, target_data_list in data['compdb_targets'].items()
+ },
)
- return path
+class CppIdeFeaturesState:
+ """Container for IDE features state data."""
-def compdb_target_from_path(filename: Path) -> Optional[str]:
- """Get a target name from a compilation database path."""
+ def __init__(self, pw_ide_settings: PigweedIdeSettings) -> None:
+ self.settings = pw_ide_settings
- # The length of the common compilation database file name prefix
- prefix_length = len(_COMPDB_FILE_PREFIX) + len(_COMPDB_FILE_SEPARATOR)
+ def __len__(self) -> int:
+ return len(self.targets)
- if len(filename.stem) <= prefix_length:
- # This will return None for the symlink filename, and any filename that
- # is too short to be a compilation database.
- return None
+ def __getitem__(self, index: str) -> CppIdeFeaturesTarget:
+ return self.targets[index]
- if filename.stem[:prefix_length] != (
- _COMPDB_FILE_PREFIX + _COMPDB_FILE_SEPARATOR
- ):
- # This will return None for any files that don't have the common prefix.
- return None
+ def __iter__(self) -> Generator[CppIdeFeaturesTarget, None, None]:
+ return (target for target in self.targets.values())
- return filename.stem[prefix_length:]
+ @property
+ def stable_target_link(self) -> Path:
+ return self.settings.working_dir / STABLE_CLANGD_DIR_NAME
+ @contextmanager
+ def _file(self) -> Generator[CppIdeFeaturesData, None, None]:
+ """A simple key-value store for state data."""
+ file_path = self.settings.working_dir / _CPP_IDE_FEATURES_DATA_FILE
-def _none_to_empty_str(value: Optional[str]) -> str:
- return value if value is not None else ''
+ try:
+ with open(file_path) as file:
+ data = CppIdeFeaturesData.deserialize(**json.load(file))
+ except (FileNotFoundError, json.decoder.JSONDecodeError):
+ data = CppIdeFeaturesData()
+ yield data
-def _none_if_not_exists(path: Path) -> Optional[Path]:
- return path if path.exists() else None
+ with open(file_path, 'w') as file:
+ json.dump(data.serialized(), file, indent=2)
+ @property
+ def targets(self) -> Dict[str, CppIdeFeaturesTarget]:
+ with self._file() as state:
+ return state.targets
-def compdb_cache_path_if_exists(
- working_dir: Path, target: Optional[str]
-) -> Optional[Path]:
- return _none_if_not_exists(
- working_dir / compdb_generate_cache_path(_none_to_empty_str(target))
- )
+ @targets.setter
+ def targets(self, new_targets: Dict[str, CppIdeFeaturesTarget]) -> None:
+ with self._file() as state:
+ state.targets = new_targets
+ @property
+ def current_target(self) -> Optional[CppIdeFeaturesTarget]:
+ with self._file() as state:
+ return state.current_target
-def target_is_enabled(
- target: Optional[str], settings: PigweedIdeSettings
-) -> bool:
- """Determine if a target is enabled.
+ @current_target.setter
+ def current_target(
+ self, new_current_target: Optional[Union[str, CppIdeFeaturesTarget]]
+ ) -> None:
+ with self._file() as state:
+ if new_current_target is None:
+ state.current_target = None
+ else:
+ if isinstance(new_current_target, CppIdeFeaturesTarget):
+ name = new_current_target.name
+ new_current_target_inst = new_current_target
+ else:
+ name = new_current_target
+
+ try:
+ new_current_target_inst = state.targets[name]
+ except KeyError:
+ raise InvalidTargetException
+
+ if not new_current_target_inst.compdb_file_path.exists():
+ raise MissingCompDbException
+
+ set_symlink(
+ new_current_target_inst.compdb_file_path.parent,
+ self.stable_target_link,
+ )
- By default, all targets are enabled. If specific targets are defined in a
- settings file, only those targets will be enabled.
- """
+ state.current_target = state.targets[name]
+
+ @property
+ def max_commands_target(self) -> Optional[CppIdeFeaturesTarget]:
+ with self._file() as state:
+ if len(state.targets) == 0:
+ return None
+
+ max_commands_target_name = sorted(
+ [
+ (name, target.num_commands)
+ for name, target in state.targets.items()
+ ],
+ key=lambda x: x[1],
+ reverse=True,
+ )[0][0]
+
+ return state.targets[max_commands_target_name]
+
+ @property
+ def compdb_hashes(self) -> CppCompilationDatabaseFileHashes:
+ with self._file() as state:
+ return state.compdb_hashes
- if target is None:
- return False
+ @compdb_hashes.setter
+ def compdb_hashes(
+ self, new_compdb_hashes: CppCompilationDatabaseFileHashes
+ ) -> None:
+ with self._file() as state:
+ state.compdb_hashes = new_compdb_hashes
- if len(settings.targets) == 0:
- return True
+ @property
+ def compdb_targets(self) -> CppCompilationDatabaseFileTargets:
+ with self._file() as state:
+ return state.compdb_targets
- return target in settings.targets
+ @compdb_targets.setter
+ def compdb_targets(
+ self, new_compdb_targets: CppCompilationDatabaseFileTargets
+ ) -> None:
+ with self._file() as state:
+ state.compdb_targets = new_compdb_targets
def path_to_executable(
@@ -236,16 +371,20 @@ def path_to_executable(
# We were give an empty string, not a path. Not a valid command.
if len(maybe_path.parts) == 0:
+ _LOG.debug("Invalid executable path. The path was an empty string.")
return None
- # Determine if the executable name matches supported drivers.
- is_supported_driver = False
+ # Determine if the executable name matches unsupported drivers.
+ is_supported_driver = True
- for supported_executable in _SUPPORTED_TOOLCHAIN_EXECUTABLES:
- if supported_executable in maybe_path.name:
- is_supported_driver = True
+ for unsupported_executable in _UNSUPPORTED_TOOLCHAIN_EXECUTABLES:
+ if unsupported_executable in maybe_path.name:
+ is_supported_driver = False
if not is_supported_driver:
+ _LOG.debug(
+ "Invalid executable path. This is not a supported driver: %s", exe
+ )
return None
# Now, ensure the executable has a path.
@@ -282,16 +421,40 @@ def path_to_executable(
return maybe_path
-def command_parts(command: str) -> Tuple[str, List[str]]:
- """Return the executable string and the rest of the command tokens."""
+def command_parts(command: str) -> Tuple[Optional[str], str, List[str]]:
+ """Return the executable string and the rest of the command tokens.
+
+ If the command contains a prefixed wrapper like `ccache`, it will be
+ extracted separately. So the return value contains:
+ (wrapper, compiler executable, all other tokens)
+ """
parts = command.split()
- head = parts[0] if len(parts) > 0 else ''
- tail = parts[1:] if len(parts) > 1 else []
- return head, tail
+ curr = ''
+ wrapper = None
+
+ try:
+ curr = parts.pop(0)
+ except IndexError:
+ return (None, curr, [])
+
+ if curr in _SUPPORTED_WRAPPER_EXECUTABLES:
+ wrapper = curr
+
+ while curr := parts.pop(0):
+ # This is very `ccache`-centric. It will work for other wrappers
+ # that use KEY=VALUE-style options or no options at all, but will
+ # not work for other cases.
+ if re.fullmatch(r'(.*)=(.*)', curr):
+ wrapper = f'{wrapper} {curr}'
+ else:
+ break
+
+ return (wrapper, curr, parts)
# This is a clumsy way to express optional keys, which is not directly
# supported in TypedDicts right now.
+# TODO(chadnorvell): Use `NotRequired` when we support Python 3.11.
class BaseCppCompileCommandDict(TypedDict):
file: str
directory: str
@@ -343,7 +506,7 @@ class CppCompileCommand:
self._file = file
self._directory = directory
- executable, tokens = command_parts(command)
+ _, executable, tokens = command_parts(command)
self._executable_path = Path(executable)
self._inferred_output: Optional[str] = None
@@ -454,7 +617,7 @@ class CppCompileCommand:
'Compile commands without \'command\' ' 'are not supported yet.'
)
- executable_str, tokens = command_parts(self.command)
+ wrapper, executable_str, tokens = command_parts(self.command)
executable_path = path_to_executable(
executable_str,
default_path=default_path,
@@ -462,13 +625,27 @@ class CppCompileCommand:
strict=strict,
)
- if executable_path is None or self.output is None:
+ if executable_path is None:
+ _LOG.debug(
+ "Compile command rejected due to bad executable path: %s",
+ self.command,
+ )
+ return None
+
+ if self.output is None:
+ _LOG.debug(
+ "Compile command rejected due to no output property: %s",
+ self.command,
+ )
return None
# TODO(chadnorvell): Some commands include the executable multiple
# times. It's not clear if that affects clangd.
new_command = f'{str(executable_path)} {" ".join(tokens)}'
+ if wrapper is not None:
+ new_command = f'{wrapper} {new_command}'
+
return self.__class__(
file=self.file,
directory=self.directory,
@@ -504,6 +681,27 @@ class CppCompileCommand:
return compile_command_dict
+def _path_nearest_parent(path1: Path, path2: Path) -> Path:
+ """Get the closest common parent of two paths."""
+ # This is the Python < 3.9 version of: if path2.is_relative_to(path1)
+ try:
+ path2.relative_to(path1)
+ return path1
+ except ValueError:
+ pass
+
+ if path1 == path2:
+ return path1
+
+ if len(path1.parts) > len(path2.parts):
+ return _path_nearest_parent(path1.parent, path2)
+
+ if len(path1.parts) < len(path2.parts):
+ return _path_nearest_parent(path1, path2.parent)
+
+ return _path_nearest_parent(path1.parent, path2.parent)
+
+
def _infer_target_pos(target_glob: str) -> List[int]:
"""Infer the position of the target in a compilation unit artifact path."""
tokens = Path(target_glob).parts
@@ -535,7 +733,17 @@ def infer_target(
# may be in the "directory" or the "output" of the compile command. So we
# need to construct the full path that combines both and use that to search
# for the target.
- subpath = output_path.relative_to(root)
+ try:
+ # The path used for target inference is the path relative to the root
+ # dir. If this artifact is a direct child of the root, this just
+ # truncates the root off of its path.
+ subpath = output_path.relative_to(root)
+ except ValueError:
+ # If the output path isn't a child path of the root dir, find the
+ # closest shared parent dir and use that as the root for truncation.
+ common_parent = _path_nearest_parent(root, output_path)
+ subpath = output_path.relative_to(common_parent)
+
return '_'.join([subpath.parts[pos] for pos in target_pos])
@@ -550,14 +758,28 @@ class CppCompilationDatabase:
See: https://clang.llvm.org/docs/JSONCompilationDatabase.html
"""
- def __init__(self, build_dir: Optional[Path] = None) -> None:
+ def __init__(
+ self,
+ root_dir: Optional[Path] = None,
+ file_path: Optional[Path] = None,
+ source_file_path: Optional[Path] = None,
+ target_inference: Optional[str] = None,
+ ) -> None:
self._db: List[CppCompileCommand] = []
+ self.file_path: Optional[Path] = file_path
+ self.source_file_path: Optional[Path] = source_file_path
+ self.source_file_hash: Optional[str] = None
+
+ if target_inference is None:
+ self.target_inference = PigweedIdeSettings().target_inference
+ else:
+ self.target_inference = target_inference
# Only compilation databases that are loaded will have this, and it
# contains the root directory of the build that the compilation
# database is based on. Processed compilation databases will not have
# a value here.
- self._build_dir = build_dir
+ self._root_dir = root_dir
def __len__(self) -> int:
return len(self._db)
@@ -568,6 +790,17 @@ class CppCompilationDatabase:
def __iter__(self) -> Generator[CppCompileCommand, None, None]:
return (compile_command for compile_command in self._db)
+ @property
+ def file_hash(self) -> str:
+ # If this compilation database did not originate from a file, return a
+ # hash that is almost certainly not going to match any other hash; these
+ # sources are not persistent, so they cannot be compared.
+ if self.file_path is None:
+ return '%032x' % random.getrandbits(160)
+
+ data = self.file_path.read_text().encode('utf-8')
+ return sha1(data).hexdigest()
+
def add(self, *commands: CppCompileCommand):
"""Add compile commands to the compilation database."""
self._db.extend(commands)
@@ -596,13 +829,17 @@ class CppCompilationDatabase:
def to_file(self, path: Path):
"""Write the compilation database to a JSON file."""
+ path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w') as file:
json.dump(self.as_dicts(), file, indent=2, sort_keys=True)
@classmethod
def load(
- cls, compdb_to_load: LoadableToCppCompilationDatabase, build_dir: Path
+ cls,
+ compdb_to_load: LoadableToCppCompilationDatabase,
+ root_dir: Path,
+ target_inference: Optional[str] = None,
) -> 'CppCompilationDatabase':
"""Load a compilation database.
@@ -610,6 +847,7 @@ class CppCompilationDatabase:
Python data structure that matches the format (list of dicts).
"""
db_as_dicts: List[Dict[str, Any]]
+ file_path = None
if isinstance(compdb_to_load, list):
# The provided data is already in the format we want it to be in,
@@ -620,11 +858,13 @@ class CppCompilationDatabase:
if isinstance(compdb_to_load, Path):
# The provided data is a path to a file, presumably JSON.
try:
+ file_path = compdb_to_load
compdb_data = compdb_to_load.read_text()
except FileNotFoundError:
raise MissingCompDbException()
elif isinstance(compdb_to_load, TextIOBase):
# The provided data is a file handle, presumably JSON.
+ file_path = Path(compdb_to_load.name) # type: ignore
compdb_data = compdb_to_load.read()
elif isinstance(compdb_to_load, str):
# The provided data is a a string, presumably JSON.
@@ -632,7 +872,11 @@ class CppCompilationDatabase:
db_as_dicts = json.loads(compdb_data)
- compdb = cls(build_dir=build_dir)
+ compdb = cls(
+ root_dir=root_dir,
+ file_path=file_path,
+ target_inference=target_inference,
+ )
try:
compdb.add(
@@ -660,14 +904,24 @@ class CppCompilationDatabase:
default_path: Optional[Path] = None,
path_globs: Optional[List[str]] = None,
strict: bool = False,
- ) -> 'CppCompilationDatabasesMap':
+ always_output_new: bool = False,
+ ) -> Optional['CppCompilationDatabasesMap']:
"""Process a ``clangd`` compilation database file.
Given a clang compilation database that may have commands for multiple
valid or invalid targets/toolchains, keep only the valid compile
commands and store them in target-specific compilation databases.
+
+ If this finds that the processed file is functionally identical to the
+ input file (meaning that the input file did not require processing to
+ be used successfully with ``clangd``), then it will return ``None``,
+ indicating that the original file should be used. This behavior can be
+ overridden by setting ``always_output_new``, which will ensure that a
+ new compilation database is always written to the working directory and
+ original compilation databases outside the working directory are never
+ made available for code intelligence.
"""
- if self._build_dir is None:
+ if self._root_dir is None:
raise ValueError(
'Can only process a compilation database that '
'contains a root build directory, usually '
@@ -678,6 +932,8 @@ class CppCompilationDatabase:
clean_compdbs = CppCompilationDatabasesMap(settings)
+ # Do processing, segregate processed commands into separate databases
+ # for each target.
for compile_command in self:
processed_command = compile_command.process(
default_path=default_path, path_globs=path_globs, strict=strict
@@ -688,16 +944,39 @@ class CppCompilationDatabase:
and processed_command.output_path is not None
):
target = infer_target(
- settings.target_inference,
- self._build_dir,
+ self.target_inference,
+ self._root_dir,
processed_command.output_path,
)
- if target_is_enabled(target, settings):
- # This invariant is satisfied by target_is_enabled
- target = cast(str, target)
- processed_command.target = target
- clean_compdbs[target].add(processed_command)
+ target = cast(str, target)
+ processed_command.target = target
+ clean_compdbs[target].add(processed_command)
+
+ if clean_compdbs[target].source_file_path is None:
+ clean_compdbs[target].source_file_path = self.file_path
+ clean_compdbs[target].source_file_hash = self.file_hash
+
+ # TODO(chadnorvell): Handle len(clean_compdbs) == 0
+
+ # Determine if the processed database is functionally identical to the
+ # original, unless configured to always output the new databases.
+ # The criteria for "functionally identical" are:
+ #
+ # - The original file only contained commands for a single target
+ # - The number of compile commands in the processed database is equal to
+ # that of the original database.
+ #
+ # This is a little bit crude. For example, it doesn't account for the
+ # (rare) edge case of multiple databases having commands for the same
+ # target. However, if you know that you have that kind of situation, you
+ # should use `always_output_new` and not rely on this.
+ if (
+ not always_output_new
+ and len(clean_compdbs) == 1
+ and len(clean_compdbs[0]) == len(self)
+ ):
+ return None
return clean_compdbs
@@ -707,19 +986,36 @@ class CppCompilationDatabasesMap:
def __init__(self, settings: PigweedIdeSettings):
self.settings = settings
- self._dbs: Dict[str, CppCompilationDatabase] = defaultdict(
- CppCompilationDatabase
- )
+ self._dbs: Dict[str, CppCompilationDatabase] = dict()
def __len__(self) -> int:
return len(self._dbs)
- def __getitem__(self, key: str) -> CppCompilationDatabase:
+ def _default(self, key: Union[str, int]):
+ # This is like `defaultdict` except that we can use the provided key
+ # (i.e. the target name) in the constructor.
+ if isinstance(key, str) and key not in self._dbs:
+ file_path = self.settings.working_dir / key / COMPDB_FILE_NAME
+ self._dbs[key] = CppCompilationDatabase(file_path=file_path)
+
+ def __getitem__(self, key: Union[str, int]) -> CppCompilationDatabase:
+ self._default(key)
+
+ # Support list-based indexing...
+ if isinstance(key, int):
+ return list(self._dbs.values())[key]
+
+ # ... and key-based indexing.
return self._dbs[key]
def __setitem__(self, key: str, item: CppCompilationDatabase) -> None:
+ self._default(key)
self._dbs[key] = item
+ def __iter__(self) -> Iterator[str]:
+ for target, _ in self.items():
+ yield target
+
@property
def targets(self) -> List[str]:
return list(self._dbs.keys())
@@ -729,36 +1025,65 @@ class CppCompilationDatabasesMap:
) -> Generator[Tuple[str, CppCompilationDatabase], None, None]:
return ((key, value) for (key, value) in self._dbs.items())
- def write(self) -> None:
- """Write compilation databases to target-specific JSON files."""
- # This also writes out a file with the name of the target that has the
- # largest number of commands, i.e., the target with the broadest
- # compilation unit coverage. We can use this as a default target of
- # last resort.
- max_commands = 0
- max_commands_target = None
-
- for target, compdb in self.items():
- if max_commands_target is None or len(compdb) > max_commands:
- max_commands_target = target
- max_commands = len(compdb)
-
- compdb.to_file(
- self.settings.working_dir / compdb_generate_file_path(target)
- )
-
- max_commands_target_path = (
- self.settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
+ def _sort_by_commands(self) -> List[str]:
+ """Sort targets by the number of compile commands they have."""
+ enumerated_targets = sorted(
+ [(len(db), target) for target, db in self._dbs.items()],
+ key=lambda x: x[0],
+ reverse=True,
)
- if max_commands_target is not None:
- if max_commands_target_path.exists():
- max_commands_target_path.unlink()
+ return [target for (_, target) in enumerated_targets]
+
+ def _sort_with_target_priority(self, target: str) -> List[str]:
+ """Sorted targets, but with the provided target first."""
+ sorted_targets = self._sort_by_commands()
+ # This will raise a ValueError if the target is not in the list, but
+ # we have ensured that that will never happen by the time we get here.
+ sorted_targets.remove(target)
+ return [target, *sorted_targets]
- with open(
- max_commands_target_path, 'x'
- ) as max_commands_target_file:
- max_commands_target_file.write(max_commands_target)
+ def _targets_to_write(self, target: str) -> List[str]:
+ """Return the list of targets whose comp. commands should be written.
+
+ Under most conditions, this will return a list with just the provided
+ target; essentially it's a no-op. But if ``cascade_targets`` is
+ enabled, this returns a list of all targets with the provided target
+ at the head of the list.
+ """
+ if not self.settings.cascade_targets:
+ return [target]
+
+ return self._sort_with_target_priority(target)
+
+ def _compdb_to_write(self, target: str) -> CppCompilationDatabase:
+ """The compilation database to write to file for this target.
+
+ Under most conditions, this will return the compilation database
+ associated with the provided target. But if ``cascade_targets`` is
+ enabled, this returns a compilation database with commands from all
+ targets, ordered per ``_sort_with_target_priority``.
+ """
+ targets = self._targets_to_write(target)
+ compdb = CppCompilationDatabase()
+
+ for iter_target in targets:
+ compdb.add(*self[iter_target])
+
+ return compdb
+
+ def test_write(self) -> None:
+ """Test writing to file.
+
+ This will raise an exception if the file is not JSON-serializable."""
+ for _, compdb in self.items():
+ compdb.to_json()
+
+ def write(self) -> None:
+ """Write compilation databases to target-specific JSON files."""
+ for target in self:
+ path = self.settings.working_dir / target / COMPDB_FILE_NAME
+ self._compdb_to_write(target).to_file(path)
@classmethod
def merge(
@@ -789,7 +1114,7 @@ class CppCompilationDatabasesMap:
"""
if len(db_sets) == 0:
raise ValueError(
- 'At least one set of compilation databases is ' 'required.'
+ 'At least one set of compilation databases is required.'
)
# Shortcut for the most common case.
@@ -805,248 +1130,28 @@ class CppCompilationDatabasesMap:
return merged
-@dataclass(frozen=True)
-class CppIdeFeaturesTarget:
- """Data pertaining to a C++ code analysis target."""
-
- name: str
- compdb_file_path: Path
- compdb_cache_path: Optional[Path]
- is_enabled: bool
-
-
-class CppIdeFeaturesState:
- """The state of the C++ analysis targets in the working directory.
-
- Targets can be:
-
- - **Available**: A compilation database is present for this target.
- - **Enabled**: Any targets are enabled by default, but a subset can be
- enabled instead in the pw_ide settings. Enabled targets need
- not be available if they haven't had a compilation database
- created through processing yet.
- - **Valid**: Is both available and enabled.
- - **Current**: The one currently activated target that is exposed to clangd.
- """
-
- def __init__(self, settings: PigweedIdeSettings) -> None:
- self.settings = settings
-
- # We filter out Nones below, so we can assume its a str
- target: Callable[[Path], str] = lambda path: cast(
- str, compdb_target_from_path(path)
- )
-
- # Contains every compilation database that's present in the working dir.
- # This dict comprehension looks monstrous, but it just finds targets and
- # associates the target names with their CppIdeFeaturesTarget objects.
- self.targets: Dict[str, CppIdeFeaturesTarget] = {
- target(file_path): CppIdeFeaturesTarget(
- name=target(file_path),
- compdb_file_path=file_path,
- compdb_cache_path=compdb_cache_path_if_exists(
- settings.working_dir, compdb_target_from_path(file_path)
- ),
- is_enabled=target_is_enabled(target(file_path), settings),
- )
- for file_path in settings.working_dir.iterdir()
- if file_path.match(
- f'{_COMPDB_FILE_PREFIX}*{_COMPDB_FILE_EXTENSION}'
- )
- # This filters out the symlink
- and compdb_target_from_path(file_path) is not None
- }
-
- # Contains the currently selected target.
- self._current_target: Optional[CppIdeFeaturesTarget] = None
-
- # This is diagnostic data; it tells us what the current target should
- # be, even if the state of the working directory is corrupted and the
- # compilation database for the target isn't actually present. Anything
- # that requires a compilation database to be definitely present should
- # use `current_target` instead of these values.
- self.current_target_name: Optional[str] = None
- self.current_target_file_path: Optional[Path] = None
- self.current_target_exists: Optional[bool] = None
-
- # Contains the name of the target that has the most compile commands,
- # i.e., the target with the most file coverage in the project.
- self._max_commands_target: Optional[str] = None
-
- try:
- src_file = Path(
- os.readlink(
- (settings.working_dir / compdb_generate_file_path())
- )
- )
-
- self.current_target_file_path = src_file
- self.current_target_name = compdb_target_from_path(src_file)
-
- if not self.current_target_file_path.exists():
- self.current_target_exists = False
-
- else:
- self.current_target_exists = True
- self._current_target = CppIdeFeaturesTarget(
- name=target(src_file),
- compdb_file_path=src_file,
- compdb_cache_path=compdb_cache_path_if_exists(
- settings.working_dir, target(src_file)
- ),
- is_enabled=target_is_enabled(target(src_file), settings),
- )
- except (FileNotFoundError, OSError):
- # If the symlink doesn't exist, there is no current target.
- pass
-
- try:
- with open(
- settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
- ) as max_commands_target_file:
- self._max_commands_target = max_commands_target_file.readline()
- except FileNotFoundError:
- # If the file doesn't exist, a compilation database probably
- # hasn't been processed yet.
- pass
-
- def __len__(self) -> int:
- return len(self.targets)
-
- def __getitem__(self, index: str) -> CppIdeFeaturesTarget:
- return self.targets[index]
-
- def __iter__(self) -> Generator[CppIdeFeaturesTarget, None, None]:
- return (target for target in self.targets.values())
-
- @property
- def current_target(self) -> Optional[str]:
- """The name of current target used for code analysis.
-
- The presence of a symlink with the expected filename pointing to a
- compilation database matching the expected filename format is the source
- of truth on what the current target is.
- """
- return (
- self._current_target.name
- if self._current_target is not None
- else None
- )
-
- @current_target.setter
- def current_target(self, target: Optional[str]) -> None:
- settings = self.settings
-
- if not self.is_valid_target(target):
- raise InvalidTargetException()
-
- # The check above rules out None.
- target = cast(str, target)
-
- compdb_symlink_path = settings.working_dir / compdb_generate_file_path()
-
- compdb_target_path = settings.working_dir / compdb_generate_file_path(
- target
- )
-
- if not compdb_target_path.exists():
- raise MissingCompDbException()
-
- set_symlink(compdb_target_path, compdb_symlink_path)
-
- cache_symlink_path = settings.working_dir / compdb_generate_cache_path()
-
- cache_target_path = settings.working_dir / compdb_generate_cache_path(
- target
- )
-
- if not cache_target_path.exists():
- os.mkdir(cache_target_path)
-
- set_symlink(cache_target_path, cache_symlink_path)
+class ClangdSettings:
+ """Makes system-specific settings for running ``clangd`` with Pigweed."""
- @property
- def max_commands_target(self) -> Optional[str]:
- """The target with the most compile commands.
+ def __init__(self, settings: PigweedIdeSettings):
+ state = CppIdeFeaturesState(settings)
- The return value is the name of the target with the largest number of
- compile commands (i.e., the largest coverage across the files in the
- project). This can be a useful "default target of last resort".
- """
- return self._max_commands_target
+ clangd_bin = "clangd"
- @property
- def available_targets(self) -> List[str]:
- return list(self.targets.keys())
+ if sys.platform.lower() == "windows":
+ clangd_bin += ".exe"
- @property
- def enabled_available_targets(self) -> Generator[str, None, None]:
- return (
- name for name, target in self.targets.items() if target.is_enabled
+ self.clangd_path: Path = (
+ Path(env.PW_PIGWEED_CIPD_INSTALL_DIR) / 'bin' / clangd_bin
)
- def is_valid_target(self, target: Optional[str]) -> bool:
- if target is None or (data := self.targets.get(target, None)) is None:
- return False
-
- return data.is_enabled
-
+ compile_commands_dir = env.PW_PROJECT_ROOT
-def aggregate_compilation_database_targets(
- compdb_file: LoadableToCppCompilationDatabase,
- settings: PigweedIdeSettings,
- build_dir: Path,
- *,
- default_path: Optional[Path] = None,
- path_globs: Optional[List[str]] = None,
-) -> List[str]:
- """Return all valid unique targets from a ``clang`` compilation database."""
- compdbs_map = CppCompilationDatabase.load(compdb_file, build_dir).process(
- settings, default_path=default_path, path_globs=path_globs
- )
-
- return compdbs_map.targets
-
-
-def delete_compilation_databases(settings: PigweedIdeSettings) -> None:
- """Delete all compilation databases in the working directory.
-
- This leaves cache directories in place.
- """
- if settings.working_dir.exists():
- for path in settings.working_dir.iterdir():
- if path.name.startswith(_COMPDB_FILE_PREFIX):
- try:
- path.unlink()
- except FileNotFoundError:
- pass
-
-
-def delete_compilation_database_caches(settings: PigweedIdeSettings) -> None:
- """Delete all compilation database caches in the working directory.
-
- This leaves all compilation databases in place.
- """
- if settings.working_dir.exists():
- for path in settings.working_dir.iterdir():
- if path.name.startswith(_COMPDB_CACHE_DIR_PREFIX):
- try:
- path.unlink()
- except FileNotFoundError:
- pass
-
-
-class ClangdSettings:
- """Makes system-specific settings for running ``clangd`` with Pigweed."""
-
- def __init__(self, settings: PigweedIdeSettings):
- self.compile_commands_dir: Path = PigweedIdeSettings().working_dir
- self.clangd_path: Path = (
- Path(PW_PIGWEED_CIPD_INSTALL_DIR) / 'bin' / 'clangd'
- )
+ if state.current_target is not None:
+ compile_commands_dir = str(state.stable_target_link)
self.arguments: List[str] = [
- f'--compile-commands-dir={self.compile_commands_dir}',
+ f'--compile-commands-dir={compile_commands_dir}',
f'--query-driver={settings.clangd_query_driver_str()}',
'--background-index',
'--clang-tidy',
diff --git a/pw_ide/py/pw_ide/editors.py b/pw_ide/py/pw_ide/editors.py
index f59e35346..24d185b89 100644
--- a/pw_ide/py/pw_ide/editors.py
+++ b/pw_ide/py/pw_ide/editors.py
@@ -45,9 +45,9 @@ from collections import defaultdict
from contextlib import contextmanager
from dataclasses import dataclass
import enum
+from hashlib import sha1
import json
from pathlib import Path
-import time
from typing import (
Any,
Callable,
@@ -57,8 +57,10 @@ from typing import (
Literal,
Optional,
OrderedDict,
+ Type,
TypeVar,
)
+import yaml
import json5 # type: ignore
@@ -70,8 +72,18 @@ class _StructuredFileFormat:
@property
def ext(self) -> str:
+ """The file extension for this file format."""
return 'null'
+ @property
+ def unserializable_error(self) -> Type[Exception]:
+ """The error class that will be raised when writing unserializable data.
+
+ This allows us to generically catch serialization errors without needing
+ to know which file format we're using.
+ """
+ return TypeError
+
def load(self, *args, **kwargs) -> OrderedDict:
raise ValueError(
f'Cannot load from file with {self.__class__.__name__}!'
@@ -90,11 +102,13 @@ class JsonFileFormat(_StructuredFileFormat):
def load(self, *args, **kwargs) -> OrderedDict:
"""Load JSON into an ordered dict."""
+ # Load into an OrderedDict instead of a plain dict
kwargs['object_pairs_hook'] = OrderedDict
return json.load(*args, **kwargs)
def dump(self, data: OrderedDict, *args, **kwargs) -> None:
"""Dump JSON in a readable format."""
+ # Ensure the output is human-readable
kwargs['indent'] = 2
json.dump(data, *args, **kwargs)
@@ -111,20 +125,58 @@ class Json5FileFormat(_StructuredFileFormat):
def load(self, *args, **kwargs) -> OrderedDict:
"""Load JSON into an ordered dict."""
+ # Load into an OrderedDict instead of a plain dict
kwargs['object_pairs_hook'] = OrderedDict
return json5.load(*args, **kwargs)
def dump(self, data: OrderedDict, *args, **kwargs) -> None:
"""Dump JSON in a readable format."""
+ # Ensure the output is human-readable
kwargs['indent'] = 2
+ # Prevent unquoting keys that don't strictly need to be quoted
kwargs['quote_keys'] = True
json5.dump(data, *args, **kwargs)
+class YamlFileFormat(_StructuredFileFormat):
+ """YAML file format."""
+
+ @property
+ def ext(self) -> str:
+ return 'yaml'
+
+ @property
+ def unserializable_error(self) -> Type[Exception]:
+ return yaml.representer.RepresenterError
+
+ def load(self, *args, **kwargs) -> OrderedDict:
+ """Load YAML into an ordered dict."""
+ # This relies on the fact that in Python 3.6+, dicts are stored in
+ # order, as an implementation detail rather than by design contract.
+ data = yaml.safe_load(*args, **kwargs)
+ return dict_swap_type(data, OrderedDict)
+
+ def dump(self, data: OrderedDict, *args, **kwargs) -> None:
+ """Dump YAML in a readable format."""
+ # Ensure the output is human-readable
+ kwargs['indent'] = 2
+ # Always use the "block" style (i.e. the dict-like style)
+ kwargs['default_flow_style'] = False
+ # Don't infere with ordering
+ kwargs['sort_keys'] = False
+ # The yaml module doesn't understand OrderedDicts
+ data_to_dump = dict_swap_type(data, dict)
+ yaml.safe_dump(data_to_dump, *args, **kwargs)
+
+
# Allows constraining to dicts and dict subclasses, while also constraining to
# the *same* dict subclass.
_DictLike = TypeVar('_DictLike', bound=Dict)
+# Likewise, constrain to a specific dict subclass, but one that can be different
+# from that of _DictLike.
+_AnotherDictLike = TypeVar('_AnotherDictLike', bound=Dict)
+
def dict_deep_merge(
src: _DictLike,
@@ -138,6 +190,10 @@ def dict_deep_merge(
`src` and `dest` need to be the same subclass of dict. If they're anything
other than basic dicts, you need to also provide a constructor that returns
an empty dict of the same subclass.
+
+ This is only intended to support dicts of JSON-serializable values, i.e.,
+ numbers, booleans, strings, lists, and dicts, all of which will be copied.
+ All other object types will be rejected with an exception.
"""
# Ensure that src and dest are the same type of dict.
# These kinds of direct class comparisons are un-Pythonic, but the invariant
@@ -183,9 +239,41 @@ def dict_deep_merge(
if isinstance(value, src.__class__):
node = dest.setdefault(key, empty_dict)
dict_deep_merge(value, node, ctor)
+ # The value is a list; merge if the corresponding dest value is a list.
+ elif isinstance(value, list) and isinstance(dest.get(key, []), list):
+ # Disallow duplicates arising from the same value appearing in both.
+ try:
+ dest[key] += [x for x in value if x not in dest[key]]
+ except KeyError:
+ dest[key] = list(value)
+ # The value is a string; copy the value.
+ elif isinstance(value, str):
+ dest[key] = f'{value}'
+ # The value is scalar (int, float, bool); copy it over.
+ elif isinstance(value, (int, float, bool)):
+ dest[key] = value
+ # The value is some other object type; it's not supported.
+ else:
+ raise TypeError(f'Cannot merge value of type {type(value)}')
+
+ return dest
+
+
+def dict_swap_type(
+ src: _DictLike,
+ ctor: Callable[[], _AnotherDictLike],
+) -> _AnotherDictLike:
+ """Change the dict subclass of all dicts in a nested dict-like structure.
+
+ This returns new data and does not mutate the original data structure.
+ """
+ dest = ctor()
+
+ for key, value in src.items():
+ # The value is a nested dict; recursively construct.
+ if isinstance(value, src.__class__):
+ dest[key] = dict_swap_type(value, ctor)
# The value is something else; copy it over.
- # TODO(chadnorvell): This doesn't deep merge other data structures, e.g.
- # lists, lists of dicts, dicts of lists, etc.
else:
dest[key] = value
@@ -242,15 +330,27 @@ class EditorSettingsDefinition:
"""Return the settings as an ordered dict."""
return self._data
+ def hash(self) -> str:
+ return sha1(json.dumps(self.get()).encode('utf-8')).hexdigest()
+
@contextmanager
- def modify(self, reinit: bool = False):
- """Modify a settings file via an ordered dict."""
- if reinit:
- new_data: OrderedDict[str, Any] = OrderedDict()
- yield new_data
- self._data = new_data
- else:
- yield self._data
+ def build(self) -> Generator[OrderedDict[str, Any], None, None]:
+ """Expose a settings file builder.
+
+ You get an empty dict when entering the content, then you can build
+ up settings by using ``sync_to`` to merge other settings dicts into this
+ one, as long as everything is JSON-serializable. Example:
+
+ .. code-block:: python
+
+ with settings_definition.modify() as settings:
+ some_other_settings.sync_to(settings)
+
+ This data is not persisted to disk.
+ """
+ new_data: OrderedDict[str, Any] = OrderedDict()
+ yield new_data
+ self._data = new_data
def sync_to(self, settings: EditorSettingsDict) -> None:
"""Merge this set of settings on top of the provided settings."""
@@ -290,18 +390,6 @@ class EditorSettingsFile(EditorSettingsDefinition):
def __repr__(self) -> str:
return f'<{self.__class__.__name__}: {str(self._path)}>'
- def _backup_filename(self, glob=False):
- timestamp = time.strftime('%Y%m%d_%H%M%S')
- timestamp = '*' if glob else timestamp
- backup_str = f'.{timestamp}.bak'
- return f'{self._name}{backup_str}.{self._format.ext}'
-
- def _make_backup(self) -> Path:
- return self._path.replace(self._path.with_name(self._backup_filename()))
-
- def _restore_backup(self, backup: Path) -> Path:
- return backup.replace(self._path)
-
def get(self) -> EditorSettingsDict:
"""Read a settings file into an ordered dict.
@@ -317,81 +405,36 @@ class EditorSettingsFile(EditorSettingsDefinition):
return settings
@contextmanager
- def modify(self, reinit: bool = False):
- """Modify a settings file via an ordered dict.
+ def build(self) -> Generator[OrderedDict[str, Any], None, None]:
+ """Expose a settings file builder.
- Get the dict when entering the context, then modify it like any
- other dict, with the caveat that whatever goes into it needs to be
- JSON-serializable. Example:
+ You get an empty dict when entering the content, then you can build
+ up settings by using ``sync_to`` to merge other settings dicts into this
+ one, as long as everything is JSON-serializable. Example:
.. code-block:: python
with settings_file.modify() as settings:
- settings[foo] = bar
+ some_other_settings.sync_to(settings)
After modifying the settings and leaving this context, the file will
- be written. If the file already exists, a backup will be made. If a
- failure occurs while writing the new file, it will be deleted and the
- backup will be restored.
-
- If the ``reinit`` argument is set, a new, empty file will be created
- instead of modifying any existing file. If there is an existing file,
- it will still be backed up.
+ be written. If a failure occurs while writing the new file, it will be
+ deleted.
"""
- if self._path.exists():
- should_load_existing = True
- should_backup = True
- else:
- should_load_existing = False
- should_backup = False
-
- if reinit:
- should_load_existing = False
-
- if should_load_existing:
- with self._path.open() as file:
- settings: OrderedDict = self._format.load(file)
- else:
- settings = OrderedDict()
-
- prev_settings = settings.copy()
-
- # TODO(chadnorvell): There's a subtle bug here where you can't assign
- # to this var and have it take effect. You have to modify it in place.
- # But you won't notice until things don't get written to disk.
- yield settings
-
- # If the settings haven't changed, don't create a backup.
- if should_load_existing:
- if settings == prev_settings:
- should_backup = False
-
- if should_backup:
- # Move the current file to a new backup file. This frees the main
- # file for open('x').
- backup = self._make_backup()
- else:
- backup = None
- # If the file exists and we didn't move it to a backup file, delete
- # it so we can open('x') it again.
- if self._path.exists():
- self._path.unlink()
-
- file = self._path.open('x')
+ new_data: OrderedDict[str, Any] = OrderedDict()
+ yield new_data
+ file = self._path.open('w')
try:
- self._format.dump(settings, file)
- except TypeError:
+ self._format.dump(new_data, file)
+ except self._format.unserializable_error:
# We'll get this error if we try to sneak something in that's
- # not JSON-serializable. Unless we handle this, we'll end up
+ # not serializable. Unless we handle this, we may end up
# with a partially-written file that can't be parsed. So we
# delete that and restore the backup.
file.close()
self._path.unlink()
- if backup is not None:
- self._restore_backup(backup)
-
raise
finally:
if not file.closed:
@@ -406,12 +449,6 @@ class EditorSettingsFile(EditorSettingsDefinition):
except FileNotFoundError:
pass
- def delete_backups(self) -> None:
- glob = self._backup_filename(glob=True)
-
- for path in self._path.glob(glob):
- path.unlink()
-
_SettingsLevelName = Literal['default', 'active', 'project', 'user']
diff --git a/pw_ide/py/pw_ide/settings.py b/pw_ide/py/pw_ide/settings.py
index 549bf8c8f..fbb63babb 100644
--- a/pw_ide/py/pw_ide/settings.py
+++ b/pw_ide/py/pw_ide/settings.py
@@ -15,31 +15,23 @@
import enum
from inspect import cleandoc
-import glob
import os
from pathlib import Path
-from typing import Any, cast, Dict, List, Literal, Optional, Union
+from typing import Any, cast, Dict, List, Literal, Optional, Tuple, Union
import yaml
+from pw_cli.env import pigweed_environment
from pw_cli.yaml_config_loader_mixin import YamlConfigLoaderMixin
-PW_IDE_DIR_NAME = '.pw_ide'
-PW_IDE_DEFAULT_DIR = (
- Path(os.path.expandvars('$PW_PROJECT_ROOT')) / PW_IDE_DIR_NAME
-)
-
-PW_PIGWEED_CIPD_INSTALL_DIR = Path(
- os.path.expandvars('$PW_PIGWEED_CIPD_INSTALL_DIR')
-)
+env = pigweed_environment()
+env_vars = vars(env)
-PW_ARM_CIPD_INSTALL_DIR = Path(os.path.expandvars('$PW_ARM_CIPD_INSTALL_DIR'))
+PW_IDE_DIR_NAME = '.pw_ide'
+PW_IDE_DEFAULT_DIR = Path(env.PW_PROJECT_ROOT) / PW_IDE_DIR_NAME
_DEFAULT_BUILD_DIR_NAME = 'out'
-_DEFAULT_BUILD_DIR = (
- Path(os.path.expandvars('$PW_PROJECT_ROOT')) / _DEFAULT_BUILD_DIR_NAME
-)
+_DEFAULT_BUILD_DIR = env.PW_PROJECT_ROOT / _DEFAULT_BUILD_DIR_NAME
-_DEFAULT_COMPDB_PATHS = [_DEFAULT_BUILD_DIR]
_DEFAULT_TARGET_INFERENCE = '?'
SupportedEditorName = Literal['vscode']
@@ -54,12 +46,12 @@ _DEFAULT_SUPPORTED_EDITORS: Dict[SupportedEditorName, bool] = {
}
_DEFAULT_CONFIG: Dict[str, Any] = {
+ 'cascade_targets': False,
'clangd_additional_query_drivers': [],
- 'build_dir': _DEFAULT_BUILD_DIR,
- 'compdb_paths': _DEFAULT_BUILD_DIR_NAME,
+ 'compdb_search_paths': [_DEFAULT_BUILD_DIR_NAME],
'default_target': None,
'editors': _DEFAULT_SUPPORTED_EDITORS,
- 'setup': ['pw --no-banner ide cpp --gn --set-default --no-override'],
+ 'sync': ['pw --no-banner ide cpp --process'],
'targets': [],
'target_inference': _DEFAULT_TARGET_INFERENCE,
'working_dir': PW_IDE_DEFAULT_DIR,
@@ -70,6 +62,49 @@ _DEFAULT_PROJECT_USER_FILE = Path('$PW_PROJECT_ROOT/.pw_ide.user.yaml')
_DEFAULT_USER_FILE = Path('$HOME/.pw_ide.yaml')
+def _expand_any_vars(input_path: Path) -> Path:
+ """Expand any environment variables in a path.
+
+ Python's ``os.path.expandvars`` will only work on an isolated environment
+ variable name. In shell, you can expand variables within a larger command
+ or path. We replicate that functionality here.
+ """
+ outputs = []
+
+ for token in input_path.parts:
+ expanded_var = os.path.expandvars(token)
+
+ if expanded_var == token:
+ outputs.append(token)
+ else:
+ outputs.append(expanded_var)
+
+ # pylint: disable=no-value-for-parameter
+ return Path(os.path.join(*outputs))
+ # pylint: enable=no-value-for-parameter
+
+
+def _expand_any_vars_str(input_path: str) -> str:
+ """`_expand_any_vars`, except takes and returns a string instead of path."""
+ return str(_expand_any_vars(Path(input_path)))
+
+
+def _parse_dir_path(input_path_str: str) -> Path:
+ if (path := Path(input_path_str)).is_absolute():
+ return path
+
+ return Path.cwd() / path
+
+
+def _parse_compdb_search_path(
+ input_data: Union[str, Tuple[str, str]], default_inference: str
+) -> Tuple[Path, str]:
+ if isinstance(input_data, (tuple, list)):
+ return _parse_dir_path(input_data[0]), input_data[1]
+
+ return _parse_dir_path(input_data), default_inference
+
+
class PigweedIdeSettings(YamlConfigLoaderMixin):
"""Pigweed IDE features settings storage class."""
@@ -103,30 +138,25 @@ class PigweedIdeSettings(YamlConfigLoaderMixin):
return Path(self._config.get('working_dir', PW_IDE_DEFAULT_DIR))
@property
- def build_dir(self) -> Path:
- """The build system's root output directory.
-
- We will use this as the output directory when automatically running
- build system commands, and will use it to resolve target names using
- target name inference when processing compilation databases. This can
- be the same build directory used for general-purpose builds, but it
- does not have to be.
+ def compdb_search_paths(self) -> List[Tuple[Path, str]]:
+ """Paths to directories to search for compilation databases.
+
+ If you're using a build system to generate compilation databases, this
+ may simply be your build output directory. However, you can add
+ additional directories to accommodate compilation databases from other
+ sources.
+
+ Entries can be just directories, in which case the default target
+ inference pattern will be used. Or entries can be tuples of a directory
+ and a target inference pattern. See the documentation for
+ ``target_inference`` for more information.
"""
- return Path(self._config.get('build_dir', _DEFAULT_BUILD_DIR))
-
- @property
- def compdb_paths(self) -> str:
- """A path glob to search for compilation databases.
-
- These paths can be to files or to directories. Paths that are
- directories will be appended with the default file name for
- ``clangd`` compilation databases, ``compile_commands.json``.
- """
- return self._config.get('compdb_paths', _DEFAULT_BUILD_DIR_NAME)
-
- @property
- def compdb_paths_expanded(self) -> List[Path]:
- return [Path(node) for node in glob.iglob(self.compdb_paths)]
+ return [
+ _parse_compdb_search_path(search_path, self.target_inference)
+ for search_path in self._config.get(
+ 'compdb_search_paths', _DEFAULT_BUILD_DIR
+ )
+ ]
@property
def targets(self) -> List[str]:
@@ -184,6 +214,14 @@ class PigweedIdeSettings(YamlConfigLoaderMixin):
ignored. For example, a glob indicating that the directory two levels
down from the build directory root has the target name would be
expressed with ``*/*/?``.
+
+ Note that the build artifact path is relative to the compilation
+ database search path that found the file. For example, for a compilation
+ database search path of ``{project dir}/out``, for the purposes of
+ target inference, the build artifact path is relative to the ``{project
+ dir}/out`` directory. Target inference patterns can be defined for each
+ compilation database search path. See the documentation for
+ ``compdb_search_paths`` for more information.
"""
return self._config.get('target_inference', _DEFAULT_TARGET_INFERENCE)
@@ -200,10 +238,10 @@ class PigweedIdeSettings(YamlConfigLoaderMixin):
return self._config.get('default_target', None)
@property
- def setup(self) -> List[str]:
+ def sync(self) -> List[str]:
"""A sequence of commands to automate IDE features setup.
- ``pw ide setup`` should do everything necessary to get the project from
+ ``pw ide sync`` should do everything necessary to get the project from
a fresh checkout to a working default IDE experience. This defines the
list of commands that makes that happen, which will be executed
sequentially in subprocesses. These commands should be idempotent, so
@@ -211,7 +249,7 @@ class PigweedIdeSettings(YamlConfigLoaderMixin):
configuration without the risk of putting those features in a bad or
unexpected state.
"""
- return self._config.get('setup', list())
+ return self._config.get('sync', list())
@property
def clangd_additional_query_drivers(self) -> List[str]:
@@ -225,12 +263,21 @@ class PigweedIdeSettings(YamlConfigLoaderMixin):
return self._config.get('clangd_additional_query_drivers', list())
def clangd_query_drivers(self) -> List[str]:
- return [
- *[str(Path(p)) for p in self.clangd_additional_query_drivers],
- str(PW_PIGWEED_CIPD_INSTALL_DIR / 'bin' / '*'),
- str(PW_ARM_CIPD_INSTALL_DIR / 'bin' / '*'),
+ drivers = [
+ *[
+ _expand_any_vars_str(p)
+ for p in self.clangd_additional_query_drivers
+ ],
]
+ if (env_var := env_vars.get('PW_PIGWEED_CIPD_INSTALL_DIR')) is not None:
+ drivers.append(str(Path(env_var) / 'bin' / '*'))
+
+ if (env_var := env_vars.get('PW_ARM_CIPD_INSTALL_DIR')) is not None:
+ drivers.append(str(Path(env_var) / 'bin' / '*'))
+
+ return drivers
+
def clangd_query_driver_str(self) -> str:
return ','.join(self.clangd_query_drivers())
@@ -254,6 +301,37 @@ class PigweedIdeSettings(YamlConfigLoaderMixin):
"""
return self._config.get('editors', {}).get(editor, False)
+ @property
+ def cascade_targets(self) -> bool:
+ """Mix compile commands for multiple targets to maximize code coverage.
+
+ By default (with this set to ``False``), the compilation database for
+ each target is consistent in the sense that it only contains compile
+ commands for one build target, so the code intelligence that database
+ provides is related to a single, known compilation artifact. However,
+ this means that code intelligence may not be provided for every source
+ file in a project, because some source files may be relevant to targets
+ other than the one you have currently set. Those source files won't
+ have compile commands for the current target, and no code intelligence
+ will appear in your editor.
+
+ If this is set to ``True``, compilation databases will still be
+ separated by target, but compile commands for *all other targets* will
+ be appended to the list of compile commands for *that* target. This
+ will maximize code coverage, ensuring that you have code intelligence
+ for every file that is built for any target, at the cost of
+ consistency—the code intelligence for some files may show information
+ that is incorrect or irrelevant to the currently selected build target.
+
+ The currently set target's compile commands will take priority at the
+ top of the combined file, then all other targets' commands will come
+ after in order of the number of commands they have (i.e. in the order of
+ their code coverage). This relies on the fact that ``clangd`` parses the
+ compilation database from the top down, using the first compile command
+ it encounters for each compilation unit.
+ """
+ return self._config.get('cascade_targets', False)
+
def _docstring_set_default(
obj: Any, default: Any, literal: bool = False
@@ -288,11 +366,8 @@ _docstring_set_default(
PigweedIdeSettings.working_dir, PW_IDE_DIR_NAME, literal=True
)
_docstring_set_default(
- PigweedIdeSettings.build_dir, _DEFAULT_BUILD_DIR_NAME, literal=True
-)
-_docstring_set_default(
- PigweedIdeSettings.compdb_paths,
- _DEFAULT_CONFIG['compdb_paths'],
+ PigweedIdeSettings.compdb_search_paths,
+ [_DEFAULT_BUILD_DIR_NAME],
literal=True,
)
_docstring_set_default(
@@ -304,12 +379,17 @@ _docstring_set_default(
literal=True,
)
_docstring_set_default(
+ PigweedIdeSettings.cascade_targets,
+ _DEFAULT_CONFIG['cascade_targets'],
+ literal=True,
+)
+_docstring_set_default(
PigweedIdeSettings.target_inference,
_DEFAULT_CONFIG['target_inference'],
literal=True,
)
_docstring_set_default(
- PigweedIdeSettings.setup, _DEFAULT_CONFIG['setup'], literal=True
+ PigweedIdeSettings.sync, _DEFAULT_CONFIG['sync'], literal=True
)
_docstring_set_default(
PigweedIdeSettings.clangd_additional_query_drivers,
diff --git a/pw_ide/py/pw_ide/status_reporter.py b/pw_ide/py/pw_ide/status_reporter.py
new file mode 100644
index 000000000..f0e25788c
--- /dev/null
+++ b/pw_ide/py/pw_ide/status_reporter.py
@@ -0,0 +1,143 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Attractive status output to the terminal (and other places if you want)."""
+
+import logging
+from typing import Callable, List, Tuple, Union
+
+from pw_cli.color import colors
+
+
+def _no_color(msg: str) -> str:
+ return msg
+
+
+def _split_lines(msg: Union[str, List[str]]) -> Tuple[str, List[str]]:
+ """Turn a list of strings into a tuple of the first and list of rest."""
+ if isinstance(msg, str):
+ return (msg, [])
+
+ return (msg[0], msg[1:])
+
+
+class StatusReporter:
+ """Print user-friendly status reports to the terminal for CLI tools.
+
+ You can instead redirect these lines to logs without formatting by
+ substituting ``LoggingStatusReporter``. Consumers of this should be
+ designed to take any subclass and not make assumptions about where the
+ output will go. But the reason you would choose this over plain logging is
+ because you want to support pretty-printing to the terminal.
+
+ This is also "themable" in the sense that you can subclass this, override
+ the methods with whatever formatting you want, and supply the subclass to
+ anything that expects an instance of this.
+
+ Key:
+
+ - info: Plain ol' informational status.
+ - ok: Something was checked and it was okay.
+ - new: Something needed to be changed/updated and it was successfully.
+ - wrn: Warning, non-critical.
+ - err: Error, critical.
+
+ This doesn't expose the %-style string formatting that is used in idiomatic
+ Python logging, but this shouldn't be used for performance-critical logging
+ situations anyway.
+ """
+
+ def _report( # pylint: disable=no-self-use
+ self,
+ msg: Union[str, List[str]],
+ color: Callable[[str], str],
+ char: str,
+ func: Callable,
+ silent: bool,
+ ) -> None:
+ """Actually print/log/whatever the status lines."""
+ first_line, rest_lines = _split_lines(msg)
+ first_line = color(f'{char} {first_line}')
+ spaces = ' ' * len(char)
+ rest_lines = [color(f'{spaces} {line}') for line in rest_lines]
+
+ if not silent:
+ for line in [first_line, *rest_lines]:
+ func(line)
+
+ def demo(self):
+ """Run this to see what your status reporter output looks like."""
+ self.info(
+ [
+ 'FYI, here\'s some information:',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ 'Donec condimentum metus molestie metus maximus ultricies '
+ 'ac id dolor.',
+ ]
+ )
+ self.ok('This is okay, no changes needed.')
+ self.new('We changed some things successfully!')
+ self.wrn('Uh oh, you might want to be aware of this.')
+ self.err('This is bad! Things might be broken!')
+
+ def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '\u2022', print, silent)
+
+ def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().blue, '\u2713', print, silent)
+
+ def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().green, '\u2713', print, silent)
+
+ def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().yellow, '\u26A0\uFE0F ', print, silent)
+
+ def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().red, '\U0001F525', print, silent)
+
+
+class LoggingStatusReporter(StatusReporter):
+ """Print status lines to logs instead of to the terminal."""
+
+ def __init__(self, logger: logging.Logger) -> None:
+ self.logger = logger
+ super().__init__()
+
+ def _report(
+ self,
+ msg: Union[str, List[str]],
+ color: Callable[[str], str],
+ char: str,
+ func: Callable,
+ silent: bool,
+ ) -> None:
+ first_line, rest_lines = _split_lines(msg)
+
+ if not silent:
+ for line in [first_line, *rest_lines]:
+ func(line)
+
+ def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.info, silent)
+
+ def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.info, silent)
+
+ def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.info, silent)
+
+ def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.warning, silent)
+
+ def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.error, silent)
diff --git a/pw_ide/py/pw_ide/vscode.py b/pw_ide/py/pw_ide/vscode.py
index c564fe0af..76f0f4288 100644
--- a/pw_ide/py/pw_ide/vscode.py
+++ b/pw_ide/py/pw_ide/vscode.py
@@ -65,10 +65,14 @@ import json
import os
from pathlib import Path
import platform
-from typing import Any, Dict, List, OrderedDict
+import shutil
+import subprocess
+from typing import Any, Dict, List, Optional, OrderedDict
+
+from pw_cli.env import pigweed_environment
from pw_ide.activate import BashShellModifier
-from pw_ide.cpp import ClangdSettings
+from pw_ide.cpp import ClangdSettings, CppIdeFeaturesState
from pw_ide.editors import (
EditorSettingsDict,
@@ -79,6 +83,11 @@ from pw_ide.editors import (
from pw_ide.python import PythonPaths
from pw_ide.settings import PigweedIdeSettings
+from pw_ide.status_reporter import StatusReporter
+
+env = pigweed_environment()
+
+_VSIX_DIR = Path(env.PW_ROOT) / 'pw_ide' / 'vscode'
def _vsc_os(system: str = platform.system()):
@@ -99,15 +108,15 @@ def _activated_env() -> OrderedDict[str, Any]:
# Not all environments have an actions.json, which this ultimately relies
# on (e.g. tests in CI). No problem, just return an empty dict instead.
try:
- env = (
+ activated_env = (
BashShellModifier(env_only=True, path_var='${env:PATH}')
.modify_env()
.env_mod
)
except (FileNotFoundError, json.JSONDecodeError):
- env = dict()
+ activated_env = dict()
- return OrderedDict(env)
+ return OrderedDict(activated_env)
def _local_terminal_integrated_env() -> Dict[str, Any]:
@@ -159,7 +168,6 @@ _DEFAULT_SETTINGS: EditorSettingsDict = OrderedDict(
"bazel-out": True,
"bazel-pigweed": True,
"bazel-testlogs": True,
- "build": True,
"environment": True,
"node_modules": True,
"out": True,
@@ -172,94 +180,153 @@ _DEFAULT_SETTINGS: EditorSettingsDict = OrderedDict(
"gulp.autoDetect": "off",
"jake.autoDetect": "off",
"npm.autoDetect": "off",
- "clangd.onConfigChanged": "restart",
"C_Cpp.intelliSenseEngine": "Disabled",
"[cpp]": OrderedDict(
{"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"}
),
+ "python.analysis.diagnosticSeverityOverrides": OrderedDict(
+ # Due to our project structure, the linter spuriously thinks we're
+ # shadowing system modules any time we import them. This disables
+ # that check.
+ {"reportShadowedImports": "none"}
+ ),
+ # The "strict" mode is much more strict than what we currently enforce.
+ "python.analysis.typeCheckingMode": "basic",
"python.formatting.provider": "yapf",
+ "python.linting.pylintEnabled": True,
+ "python.linting.mypyEnabled": True,
+ "python.testing.unittestEnabled": True,
"[python]": OrderedDict({"editor.tabSize": 4}),
"typescript.tsc.autoDetect": "off",
"[gn]": OrderedDict({"editor.defaultFormatter": "msedge-dev.gnls"}),
"[proto3]": OrderedDict(
{"editor.defaultFormatter": "zxh404.vscode-proto3"}
),
+ "[restructuredtext]": OrderedDict({"editor.tabSize": 3}),
}
)
-# pylint: disable=line-too-long
_DEFAULT_TASKS: EditorSettingsDict = OrderedDict(
{
"version": "2.0.0",
"tasks": [
{
- "type": "shell",
- "label": "Pigweed IDE: Format",
- "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw format --fix'",
+ "type": "process",
+ "label": "Pigweed: Format",
+ "command": "${config:python.defaultInterpreterPath}",
+ "args": [
+ "-m",
+ "pw_ide.activate",
+ "-x 'pw format --fix'",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
{
- "type": "shell",
- "label": "Pigweed IDE: Presubmit",
- "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw presubmit'",
+ "type": "process",
+ "label": "Pigweed: Presubmit",
+ "command": "${config:python.defaultInterpreterPath}",
+ "args": [
+ "-m",
+ "pw_ide.activate",
+ "-x 'pw presubmit'",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
{
- "label": "Pigweed IDE: Set Python Virtual Environment",
+ "label": "Pigweed: Set Python Virtual Environment",
"command": "${command:python.setInterpreter}",
"problemMatcher": [],
},
{
- "label": "Pigweed IDE: Restart Python Language Server",
+ "label": "Pigweed: Restart Python Language Server",
"command": "${command:python.analysis.restartLanguageServer}",
"problemMatcher": [],
},
{
- "label": "Pigweed IDE: Restart C++ Language Server",
+ "label": "Pigweed: Restart C++ Language Server",
"command": "${command:clangd.restart}",
"problemMatcher": [],
},
{
- "type": "shell",
- "label": "Pigweed IDE: Process C++ Compilation Database from GN",
- "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --gn --process out/compile_commands.json'",
+ "type": "process",
+ "label": "Pigweed: Sync IDE",
+ "command": "${config:python.defaultInterpreterPath}",
+ "args": [
+ "-m",
+ "pw_ide.activate",
+ "-x 'pw ide sync'",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
{
- "type": "shell",
- "label": "Pigweed IDE: Setup",
- "command": "python3 ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide setup'",
+ "type": "process",
+ "label": "Pigweed: Current C++ Target Toolchain",
+ "command": "${config:python.defaultInterpreterPath}",
+ "args": [
+ "-m",
+ "pw_ide.activate",
+ "-x 'pw ide cpp'",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
{
- "type": "shell",
- "label": "Pigweed IDE: Current C++ Code Analysis Target",
- "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp'",
+ "type": "process",
+ "label": "Pigweed: List C++ Target Toolchains",
+ "command": "${config:python.defaultInterpreterPath}",
+ "args": [
+ "-m",
+ "pw_ide.activate",
+ "-x 'pw ide cpp --list'",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
{
- "type": "shell",
- "label": "Pigweed IDE: List C++ Code Analysis Targets",
- "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --list'",
+ "type": "process",
+ "label": (
+ "Pigweed: Change C++ Target Toolchain "
+ "without LSP restart"
+ ),
+ "command": "${config:python.defaultInterpreterPath}",
+ "args": [
+ "-m",
+ "pw_ide.activate",
+ "-x 'pw ide cpp --set ${input:availableTargetToolchains}'",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
{
- "type": "shell",
- "label": "Pigweed IDE: Set C++ Code Analysis Target",
- "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --set ${input:target}'",
+ "label": "Pigweed: Set C++ Target Toolchain",
+ "dependsOrder": "sequence",
+ "dependsOn": [
+ "Pigweed: Change C++ Target Toolchain without LSP restart",
+ "Pigweed: Restart C++ Language Server",
+ ],
+ "presentation": {
+ "focus": True,
+ },
"problemMatcher": [],
},
],
- "inputs": [
- {
- "id": "target",
- "type": "promptString",
- "description": "C++ code analysis target",
- }
- ],
}
)
-# pylint: enable=line-too-long
_DEFAULT_EXTENSIONS: EditorSettingsDict = OrderedDict(
{
@@ -270,17 +337,22 @@ _DEFAULT_EXTENSIONS: EditorSettingsDict = OrderedDict(
"msedge-dev.gnls",
"zxh404.vscode-proto3",
"josetr.cmake-language-support-vscode",
- "swyddfa.esbonio",
],
"unwantedRecommendations": [
"ms-vscode.cpptools",
"persidskiy.vscode-gnformat",
"lextudio.restructuredtext",
- "trond-snekvik.simple-rst",
],
}
)
+_DEFAULT_LAUNCH: EditorSettingsDict = OrderedDict(
+ {
+ "version": "0.2.0",
+ "configurations": [],
+ }
+)
+
def _default_settings(
pw_ide_settings: PigweedIdeSettings,
@@ -295,8 +367,23 @@ def _default_settings(
)
-def _default_tasks(_pw_ide_settings: PigweedIdeSettings) -> EditorSettingsDict:
- return _DEFAULT_TASKS
+def _default_tasks(
+ pw_ide_settings: PigweedIdeSettings,
+ state: Optional[CppIdeFeaturesState] = None,
+) -> EditorSettingsDict:
+ if state is None:
+ state = CppIdeFeaturesState(pw_ide_settings)
+
+ inputs = [
+ {
+ "type": "pickString",
+ "id": "availableTargetToolchains",
+ "description": "Available target toolchains",
+ "options": list(state.targets),
+ }
+ ]
+
+ return OrderedDict(**_DEFAULT_TASKS, inputs=inputs)
def _default_extensions(
@@ -305,6 +392,12 @@ def _default_extensions(
return _DEFAULT_EXTENSIONS
+def _default_launch(
+ _pw_ide_settings: PigweedIdeSettings,
+) -> EditorSettingsDict:
+ return _DEFAULT_LAUNCH
+
+
DEFAULT_SETTINGS_PATH = Path(os.path.expandvars('$PW_PROJECT_ROOT')) / '.vscode'
@@ -312,12 +405,14 @@ class VscSettingsType(Enum):
"""Visual Studio Code settings files.
VSC supports editor settings (``settings.json``), recommended
- extensions (``extensions.json``), and tasks (``tasks.json``).
+ extensions (``extensions.json``), tasks (``tasks.json``), and
+ launch/debug configurations (``launch.json``).
"""
SETTINGS = 'settings'
TASKS = 'tasks'
EXTENSIONS = 'extensions'
+ LAUNCH = 'launch'
@classmethod
def all(cls) -> List['VscSettingsType']:
@@ -334,4 +429,124 @@ class VscSettingsManager(EditorSettingsManager[VscSettingsType]):
VscSettingsType.SETTINGS: _default_settings,
VscSettingsType.TASKS: _default_tasks,
VscSettingsType.EXTENSIONS: _default_extensions,
+ VscSettingsType.LAUNCH: _default_launch,
}
+
+
+def _prompt_for_path(reporter: StatusReporter) -> Path:
+ reporter.info(
+ [
+ "Hmmm... I can't seem to find your Visual Studio Code binary path!",
+ "You can provide it manually here, or Ctrl-C to cancel.",
+ ]
+ )
+
+ path = Path(input("> "))
+
+ if path.exists():
+ return path
+
+ reporter.err("Nothing found there!")
+ raise FileNotFoundError()
+
+
+# TODO(chadnorvell): Replace this when we support Python 3.11 with:
+# _PathData = Tuple[Optional[str], *Tuple[str]]
+_PathData = List[Optional[str]]
+
+
+def _try_code_path(path_list: _PathData) -> Optional[Path]:
+ root, *rest = path_list
+
+ if root is None:
+ return None
+
+ path = Path(root)
+
+ for part in rest:
+ if part is None:
+ return None
+
+ path /= part
+
+ return path
+
+
+def _try_each_code_path(
+ reporter: StatusReporter, *path_lists: _PathData
+) -> Path:
+ for path_list in path_lists:
+ if (path := _try_code_path(path_list)) is not None:
+ return path
+
+ if (path_str := shutil.which('code')) is not None:
+ return Path(path_str)
+
+ return _prompt_for_path(reporter)
+
+
+def _get_vscode_exe_path(
+ reporter: StatusReporter, system: str = platform.system()
+) -> Path:
+ if system == 'Darwin':
+ return _try_each_code_path(
+ reporter,
+ [
+ '/Applications',
+ 'Visual Studio Code.app',
+ 'Contents',
+ 'Resources',
+ 'app',
+ 'bin',
+ 'code',
+ ],
+ )
+
+ if system == 'Windows':
+ return _try_each_code_path(
+ reporter,
+ [
+ os.getenv('APPDATA'),
+ 'Local',
+ 'Programs',
+ 'Microsoft VS Code',
+ 'bin',
+ 'code.exe',
+ ],
+ [
+ os.getenv('LOCALAPPDATA'),
+ 'Local',
+ 'Programs',
+ 'Microsoft VS Code',
+ 'bin',
+ 'code.exe',
+ ],
+ )
+
+ if system == 'Linux':
+ return _try_each_code_path(
+ reporter,
+ ['/usr', 'bin', 'code'],
+ ['/usr', 'local', 'bin', 'code'],
+ )
+
+ return _prompt_for_path(reporter)
+
+
+def _get_latest_extension_vsix() -> Path:
+ return sorted(_VSIX_DIR.glob('*.vsix'), reverse=True)[0]
+
+
+def install_extension_from_vsix(reporter: StatusReporter) -> None:
+ """Install the latest pre-built VSC extension from its VSIX file.
+
+ Normally, extensions are installed from the VSC extension marketplace.
+ This will instead install the extension directly from a file.
+ """
+ extension_path = _get_latest_extension_vsix()
+ vscode_exe_path = _get_vscode_exe_path(reporter)
+
+ reporter.info(f"Path: {vscode_exe_path}")
+ subprocess.run(
+ [vscode_exe_path, '--install-extension', extension_path], check=True
+ )
diff --git a/pw_ide/py/test_cases.py b/pw_ide/py/test_cases.py
index 25df41162..77609d827 100644
--- a/pw_ide/py/test_cases.py
+++ b/pw_ide/py/test_cases.py
@@ -153,6 +153,7 @@ class PwIdeTestCase(TempDirTestCase):
self,
working_dir: Optional[Union[str, Path]] = None,
targets: Optional[List[str]] = None,
+ cascade_targets: bool = False,
) -> PigweedIdeSettings:
"""Make settings that wrap provided paths in the temp path."""
@@ -171,5 +172,6 @@ class PwIdeTestCase(TempDirTestCase):
default_config={
'working_dir': str(working_dir_path),
'targets': targets,
+ 'cascade_targets': cascade_targets,
},
)
diff --git a/pw_ide/py/vscode_test.py b/pw_ide/py/vscode_test.py
index 33e4f4216..8191d6e27 100644
--- a/pw_ide/py/vscode_test.py
+++ b/pw_ide/py/vscode_test.py
@@ -30,19 +30,19 @@ class TestVscSettingsManager(PwIdeTestCase):
with manager.active(
VscSettingsType.SETTINGS
- ).modify() as active_settings:
+ ).build() as active_settings:
manager.default(VscSettingsType.SETTINGS).sync_to(active_settings)
manager.project(VscSettingsType.SETTINGS).sync_to(active_settings)
manager.user(VscSettingsType.SETTINGS).sync_to(active_settings)
- with manager.active(VscSettingsType.TASKS).modify() as active_settings:
+ with manager.active(VscSettingsType.TASKS).build() as active_settings:
manager.default(VscSettingsType.TASKS).sync_to(active_settings)
manager.project(VscSettingsType.TASKS).sync_to(active_settings)
manager.user(VscSettingsType.TASKS).sync_to(active_settings)
with manager.active(
VscSettingsType.EXTENSIONS
- ).modify() as active_settings:
+ ).build() as active_settings:
manager.default(VscSettingsType.EXTENSIONS).sync_to(active_settings)
manager.project(VscSettingsType.EXTENSIONS).sync_to(active_settings)
manager.user(VscSettingsType.EXTENSIONS).sync_to(active_settings)
@@ -65,7 +65,7 @@ class TestVscSettingsManager(PwIdeTestCase):
with manager.active(
VscSettingsType.SETTINGS
- ).modify() as active_settings:
+ ).build() as active_settings:
manager.default(VscSettingsType.SETTINGS).sync_to(active_settings)
manager.project(VscSettingsType.SETTINGS).sync_to(active_settings)
manager.user(VscSettingsType.SETTINGS).sync_to(active_settings)
diff --git a/pw_ide/ts/pigweed-vscode/.vscode/launch.json b/pw_ide/ts/pigweed-vscode/.vscode/launch.json
new file mode 100644
index 000000000..20f8408ef
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/.vscode/launch.json
@@ -0,0 +1,30 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Run Extension",
+ "type": "extensionHost",
+ "request": "launch",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/out/**/*.js"
+ ],
+ "preLaunchTask": "${defaultBuildTask}"
+ },
+ {
+ "name": "Extension Tests",
+ "type": "extensionHost",
+ "request": "launch",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}",
+ "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/out/test/**/*.js"
+ ],
+ "preLaunchTask": "${defaultBuildTask}"
+ }
+ ]
+}
diff --git a/pw_ide/ts/pigweed-vscode/.vscode/settings.json b/pw_ide/ts/pigweed-vscode/.vscode/settings.json
new file mode 100644
index 000000000..4eaa030a9
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/.vscode/settings.json
@@ -0,0 +1,12 @@
+{
+ "files.exclude": {
+ "out": false
+ },
+ "search.exclude": {
+ "out": true
+ },
+ "typescript.tsc.autoDetect": "off",
+ "editor.rulers": [
+ 80
+ ]
+}
diff --git a/pw_ide/ts/pigweed-vscode/.vscode/tasks.json b/pw_ide/ts/pigweed-vscode/.vscode/tasks.json
new file mode 100644
index 000000000..34edf970f
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/.vscode/tasks.json
@@ -0,0 +1,18 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "watch",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "presentation": {
+ "reveal": "never"
+ },
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ }
+ }
+ ]
+}
diff --git a/pw_ide/ts/pigweed-vscode/CHANGELOG.md b/pw_ide/ts/pigweed-vscode/CHANGELOG.md
new file mode 100644
index 000000000..f1b145a48
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/CHANGELOG.md
@@ -0,0 +1,13 @@
+# Change Log
+
+## [0.1.1]
+
+- Fixes cases where "Pigweed: Check Extensions" was not running on startup.
+
+## [0.1.0]
+
+- Adds the "Pigweed: Check Extensions" command, which prompts the user to
+ install all recommended extensions and disable all unwanted extensions, as
+ defined by the project's `extensions.json`. This makes "recommended"
+ extensions required, and "unwanted" extensions forbidden, allowing Pigweed
+ projects to define more consistent development environments for their teams.
diff --git a/pw_ide/ts/pigweed-vscode/README.md b/pw_ide/ts/pigweed-vscode/README.md
new file mode 100644
index 000000000..434603d7a
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/README.md
@@ -0,0 +1,25 @@
+# Pigweed Extension for Visual Studio Code
+
+This is highly experimental!
+
+## Developing
+
+- Ensure that you have `npm` installed globally; this doesn't use the
+ distribution provided by Pigweed yet.
+
+- Open the `pigweed/pw_ide/vscode` directory directly in Visual Studio Code.
+
+- Run `npm install` to add all dependencies.
+
+- Run "Run Extension" in the "Run and Debug" sidebar, or simply hit F5. A new
+ Visual Studio Code window will open with the extension installed.
+
+- Make changes. The build will update automatically. Click the little green
+ circle-with-an-arrow icon at the top of your development window to update
+ the extension development host with the new build.
+
+## Building
+
+- Install the build tool: `npm install -g @vscode/vsce`
+
+- Build the VSIX: `vsce package`
diff --git a/pw_ide/ts/pigweed-vscode/package-lock.json b/pw_ide/ts/pigweed-vscode/package-lock.json
new file mode 100644
index 000000000..be6ff320a
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/package-lock.json
@@ -0,0 +1,4452 @@
+{
+ "name": "pigweed-vscode",
+ "version": "0.1.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "pigweed-vscode",
+ "version": "0.1.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "glob": "^8.1.0",
+ "hjson": "^3.2.2",
+ "node-polyfill-webpack-plugin": "^2.0.1"
+ },
+ "devDependencies": {
+ "@types/glob": "^8.1.0",
+ "@types/hjson": "2.4.3",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "20.2.5",
+ "@types/vscode": "^1.64.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.8",
+ "@typescript-eslint/parser": "^5.59.8",
+ "@vscode/test-electron": "^2.3.2",
+ "eslint": "^8.41.0",
+ "mocha": "^10.2.0",
+ "ts-loader": "^9.4.4",
+ "typescript": "^5.1.3",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ },
+ "engines": {
+ "vscode": "^1.64.0"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@discoveryjs/json-ext": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
+ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz",
+ "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+ "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz",
+ "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.11",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz",
+ "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
+ "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
+ "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
+ "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.19",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz",
+ "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
+ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/@types/eslint": {
+ "version": "8.44.2",
+ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz",
+ "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==",
+ "dependencies": {
+ "@types/estree": "*",
+ "@types/json-schema": "*"
+ }
+ },
+ "node_modules/@types/eslint-scope": {
+ "version": "3.7.4",
+ "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
+ "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
+ "dependencies": {
+ "@types/eslint": "*",
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz",
+ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA=="
+ },
+ "node_modules/@types/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==",
+ "dev": true,
+ "dependencies": {
+ "@types/minimatch": "^5.1.2",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/hjson": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/@types/hjson/-/hjson-2.4.3.tgz",
+ "integrity": "sha512-EDixutNn3FQwa5HdET+Tx0h7TirP2knJa9TB21ySqr8XVqT/VsvvOwnanTLHUOOpNofcUhkRKhOk0Wh4YD9RSA==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.12",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz",
+ "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA=="
+ },
+ "node_modules/@types/minimatch": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
+ "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==",
+ "dev": true
+ },
+ "node_modules/@types/mocha": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz",
+ "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "20.2.5",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.5.tgz",
+ "integrity": "sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ=="
+ },
+ "node_modules/@types/semver": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==",
+ "dev": true
+ },
+ "node_modules/@types/vscode": {
+ "version": "1.82.0",
+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz",
+ "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
+ "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/type-utils": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
+ "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz",
+ "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "@typescript-eslint/utils": "5.62.0",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vscode/test-electron": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.3.4.tgz",
+ "integrity": "sha512-eWzIqXMhvlcoXfEFNWrVu/yYT5w6De+WZXR/bafUQhAp8+8GkQo95Oe14phwiRUPv8L+geAKl/QM2+PoT3YW3g==",
+ "dev": true,
+ "dependencies": {
+ "http-proxy-agent": "^4.0.1",
+ "https-proxy-agent": "^5.0.0",
+ "jszip": "^3.10.1",
+ "semver": "^7.5.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@webassemblyjs/ast": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
+ "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==",
+ "dependencies": {
+ "@webassemblyjs/helper-numbers": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/floating-point-hex-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz",
+ "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw=="
+ },
+ "node_modules/@webassemblyjs/helper-api-error": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz",
+ "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q=="
+ },
+ "node_modules/@webassemblyjs/helper-buffer": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz",
+ "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA=="
+ },
+ "node_modules/@webassemblyjs/helper-numbers": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz",
+ "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==",
+ "dependencies": {
+ "@webassemblyjs/floating-point-hex-parser": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz",
+ "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA=="
+ },
+ "node_modules/@webassemblyjs/helper-wasm-section": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz",
+ "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/ieee754": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz",
+ "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==",
+ "dependencies": {
+ "@xtuc/ieee754": "^1.2.0"
+ }
+ },
+ "node_modules/@webassemblyjs/leb128": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz",
+ "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==",
+ "dependencies": {
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webassemblyjs/utf8": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz",
+ "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA=="
+ },
+ "node_modules/@webassemblyjs/wasm-edit": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz",
+ "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/helper-wasm-section": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6",
+ "@webassemblyjs/wasm-opt": "1.11.6",
+ "@webassemblyjs/wasm-parser": "1.11.6",
+ "@webassemblyjs/wast-printer": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-gen": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz",
+ "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-opt": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz",
+ "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-buffer": "1.11.6",
+ "@webassemblyjs/wasm-gen": "1.11.6",
+ "@webassemblyjs/wasm-parser": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/wasm-parser": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz",
+ "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@webassemblyjs/helper-api-error": "1.11.6",
+ "@webassemblyjs/helper-wasm-bytecode": "1.11.6",
+ "@webassemblyjs/ieee754": "1.11.6",
+ "@webassemblyjs/leb128": "1.11.6",
+ "@webassemblyjs/utf8": "1.11.6"
+ }
+ },
+ "node_modules/@webassemblyjs/wast-printer": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz",
+ "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==",
+ "dependencies": {
+ "@webassemblyjs/ast": "1.11.6",
+ "@xtuc/long": "4.2.2"
+ }
+ },
+ "node_modules/@webpack-cli/configtest": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
+ "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/info": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
+ "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ }
+ },
+ "node_modules/@webpack-cli/serve": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
+ "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x",
+ "webpack-cli": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@xtuc/ieee754": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+ "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
+ },
+ "node_modules/@xtuc/long": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.10.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+ "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-assertions": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
+ "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "dev": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-keywords": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
+ "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
+ "peerDependencies": {
+ "ajv": "^6.9.1"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
+ "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "node_modules/asn1.js/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
+ "node_modules/assert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+ "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-nan": "^1.3.2",
+ "object-is": "^1.1.5",
+ "object.assign": "^4.1.4",
+ "util": "^0.12.5"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bn.js": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
+ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true
+ },
+ "node_modules/browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "dependencies": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "dependencies": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "node_modules/browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/browserify-rsa": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz",
+ "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==",
+ "dependencies": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "node_modules/browserify-sign": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz",
+ "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==",
+ "dependencies": {
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.3",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.5",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.10",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz",
+ "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001517",
+ "electron-to-chromium": "^1.4.477",
+ "node-releases": "^2.0.13",
+ "update-browserslist-db": "^1.0.11"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
+ },
+ "node_modules/buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="
+ },
+ "node_modules/builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ=="
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001534",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001534.tgz",
+ "integrity": "sha512-vlPVrhsCS7XaSh2VvWluIQEzVhefrUQcEsQWSS5A5V+dM07uv1qHeQzAOTGIMy9i3e9bH15+muvI/UHojVgS/Q==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+ "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/chrome-trace-event": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
+ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/clone-deep": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+ "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+ "dev": true,
+ "dependencies": {
+ "is-plain-object": "^2.0.4",
+ "kind-of": "^6.0.2",
+ "shallow-clone": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/colorette": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/console-browserify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="
+ },
+ "node_modules/constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ=="
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "node_modules/create-ecdh": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
+ "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "node_modules/create-ecdh/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dependencies": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz",
+ "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.1",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/des.js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "node_modules/diffie-hellman/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/domain-browser": {
+ "version": "4.22.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
+ "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://bevry.me/fund"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.520",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.520.tgz",
+ "integrity": "sha512-Frfus2VpYADsrh1lB3v/ft/WVFlVzOIm+Q0p7U7VqHI6qr7NWHYKe+Wif3W50n7JAFoBsWVsoU0+qDks6WQ60g=="
+ },
+ "node_modules/elliptic": {
+ "version": "6.5.4",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
+ "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
+ "dependencies": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/elliptic/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.15.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz",
+ "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/envinfo": {
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz",
+ "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==",
+ "dev": true,
+ "bin": {
+ "envinfo": "dist/cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz",
+ "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q=="
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.49.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz",
+ "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.2",
+ "@eslint/js": "8.49.0",
+ "@humanwhocodes/config-array": "^0.11.11",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esquery/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esrecurse/node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "dependencies": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
+ "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/filter-obj": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-2.0.2.tgz",
+ "integrity": "sha512-lO3ttPjHZRfjMcxWKb1j1eDhTFsu4meeR3lnMcnBFhk6RuLhvEiuALu2TlfL310ph4lCYYwgF/ElIjdP739tdg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz",
+ "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.7",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
+ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^5.0.1",
+ "once": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
+ },
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.21.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+ "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hash-base": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
+ "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/hash-base/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/hash-base/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hjson": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/hjson/-/hjson-3.2.2.tgz",
+ "integrity": "sha512-MkUeB0cTIlppeSsndgESkfFD21T2nXPRaBStLtf3cAYA2bVEFdXlodZB0TukwZiobPD1Ksax5DK4RTZeaXCI3Q==",
+ "bin": {
+ "hjson": "bin/hjson"
+ }
+ },
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz",
+ "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==",
+ "dev": true,
+ "dependencies": {
+ "@tootallnate/once": "1",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/https-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+ "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg=="
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "dev": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "dev": true
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+ "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+ "dev": true,
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/interpret": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+ "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+ "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-nan": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+ "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+ "dev": true,
+ "dependencies": {
+ "isobject": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz",
+ "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==",
+ "dependencies": {
+ "which-typed-array": "^1.1.11"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dev": true,
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.3",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz",
+ "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/loader-runner": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
+ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
+ "engines": {
+ "node": ">=6.11.5"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "bin": {
+ "miller-rabin": "bin/miller-rabin"
+ }
+ },
+ "node_modules/miller-rabin/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
+ "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.3",
+ "debug": "4.3.4",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.2.0",
+ "he": "1.2.0",
+ "js-yaml": "4.1.0",
+ "log-symbols": "4.1.0",
+ "minimatch": "5.0.1",
+ "ms": "2.1.3",
+ "nanoid": "3.3.3",
+ "serialize-javascript": "6.0.0",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "workerpool": "6.2.1",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": ">= 14.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/mocha/node_modules/glob": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+ "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/mocha/node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz",
+ "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+ "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/mocha/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz",
+ "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==",
+ "dev": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+ },
+ "node_modules/node-polyfill-webpack-plugin": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz",
+ "integrity": "sha512-ZUMiCnZkP1LF0Th2caY6J/eKKoA0TefpoVa68m/LQU1I/mE8rGt4fNYGgNuCcK+aG8P8P43nbeJ2RqJMOL/Y1A==",
+ "dependencies": {
+ "assert": "^2.0.0",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^6.0.3",
+ "console-browserify": "^1.2.0",
+ "constants-browserify": "^1.0.0",
+ "crypto-browserify": "^3.12.0",
+ "domain-browser": "^4.22.0",
+ "events": "^3.3.0",
+ "filter-obj": "^2.0.2",
+ "https-browserify": "^1.0.0",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "^1.0.1",
+ "process": "^0.11.10",
+ "punycode": "^2.1.1",
+ "querystring-es3": "^0.2.1",
+ "readable-stream": "^4.0.0",
+ "stream-browserify": "^3.0.0",
+ "stream-http": "^3.2.0",
+ "string_decoder": "^1.3.0",
+ "timers-browserify": "^2.0.12",
+ "tty-browserify": "^0.0.1",
+ "type-fest": "^2.14.0",
+ "url": "^0.11.0",
+ "util": "^0.12.4",
+ "vm-browserify": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "webpack": ">=5"
+ }
+ },
+ "node_modules/node-polyfill-webpack-plugin/node_modules/readable-stream": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
+ "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/node-polyfill-webpack-plugin/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/node-polyfill-webpack-plugin/node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/node-polyfill-webpack-plugin/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
+ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz",
+ "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A=="
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-asn1": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz",
+ "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==",
+ "dependencies": {
+ "asn1.js": "^5.2.0",
+ "browserify-aes": "^1.0.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pbkdf2": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz",
+ "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==",
+ "dependencies": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "node_modules/public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/public-encrypt/node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.11.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
+ "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystring-es3": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+ "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "dependencies": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/rechoir": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+ "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.20.0"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.4",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
+ "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/schema-utils": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
+ "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
+ "dependencies": {
+ "@types/json-schema": "^7.0.8",
+ "ajv": "^6.12.5",
+ "ajv-keywords": "^3.5.2"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.5.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
+ "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
+ "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
+ "dev": true,
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
+ "node_modules/shallow-clone": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+ "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+ "dev": true,
+ "dependencies": {
+ "kind-of": "^6.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/stream-browserify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
+ "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
+ "dependencies": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
+ "node_modules/stream-browserify/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/stream-http": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz",
+ "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==",
+ "dependencies": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "node_modules/stream-http/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.19.4",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz",
+ "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.8.2",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terser-webpack-plugin": {
+ "version": "5.3.9",
+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz",
+ "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.17",
+ "jest-worker": "^27.4.5",
+ "schema-utils": "^3.1.1",
+ "serialize-javascript": "^6.0.1",
+ "terser": "^5.16.8"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "^5.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "uglify-js": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
+ "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/timers-browserify": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
+ "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
+ "dependencies": {
+ "setimmediate": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-loader": {
+ "version": "9.4.4",
+ "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.4.4.tgz",
+ "integrity": "sha512-MLukxDHBl8OJ5Dk3y69IsKVFRA/6MwzEqBgh+OXMPB/OD01KQuWPFd1WAQP8a5PeSCAxfnkhiuWqfmFJzJQt9w==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "enhanced-resolve": "^5.0.0",
+ "micromatch": "^4.0.0",
+ "semver": "^7.3.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "typescript": "*",
+ "webpack": "^5.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tty-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
+ "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
+ "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.2.tgz",
+ "integrity": "sha512-7yIgNnrST44S7PJ5+jXbdIupfU1nWUdQJBFBeJRclPXiWgCvrSq5Frw8lr/i//n5sqDfzoKmBymMS81l4U/7cg==",
+ "dependencies": {
+ "punycode": "^1.4.1",
+ "qs": "^6.11.2"
+ }
+ },
+ "node_modules/url/node_modules/punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
+ },
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="
+ },
+ "node_modules/watchpack": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
+ "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
+ "dependencies": {
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/webpack": {
+ "version": "5.88.2",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz",
+ "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==",
+ "dependencies": {
+ "@types/eslint-scope": "^3.7.3",
+ "@types/estree": "^1.0.0",
+ "@webassemblyjs/ast": "^1.11.5",
+ "@webassemblyjs/wasm-edit": "^1.11.5",
+ "@webassemblyjs/wasm-parser": "^1.11.5",
+ "acorn": "^8.7.1",
+ "acorn-import-assertions": "^1.9.0",
+ "browserslist": "^4.14.5",
+ "chrome-trace-event": "^1.0.2",
+ "enhanced-resolve": "^5.15.0",
+ "es-module-lexer": "^1.2.1",
+ "eslint-scope": "5.1.1",
+ "events": "^3.2.0",
+ "glob-to-regexp": "^0.4.1",
+ "graceful-fs": "^4.2.9",
+ "json-parse-even-better-errors": "^2.3.1",
+ "loader-runner": "^4.2.0",
+ "mime-types": "^2.1.27",
+ "neo-async": "^2.6.2",
+ "schema-utils": "^3.2.0",
+ "tapable": "^2.1.1",
+ "terser-webpack-plugin": "^5.3.7",
+ "watchpack": "^2.4.0",
+ "webpack-sources": "^3.2.3"
+ },
+ "bin": {
+ "webpack": "bin/webpack.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependenciesMeta": {
+ "webpack-cli": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli": {
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
+ "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
+ "dev": true,
+ "dependencies": {
+ "@discoveryjs/json-ext": "^0.5.0",
+ "@webpack-cli/configtest": "^2.1.1",
+ "@webpack-cli/info": "^2.0.2",
+ "@webpack-cli/serve": "^2.0.5",
+ "colorette": "^2.0.14",
+ "commander": "^10.0.1",
+ "cross-spawn": "^7.0.3",
+ "envinfo": "^7.7.3",
+ "fastest-levenshtein": "^1.0.12",
+ "import-local": "^3.0.2",
+ "interpret": "^3.1.1",
+ "rechoir": "^0.8.0",
+ "webpack-merge": "^5.7.3"
+ },
+ "bin": {
+ "webpack-cli": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "webpack": "5.x.x"
+ },
+ "peerDependenciesMeta": {
+ "@webpack-cli/generators": {
+ "optional": true
+ },
+ "webpack-bundle-analyzer": {
+ "optional": true
+ },
+ "webpack-dev-server": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-cli/node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/webpack-merge": {
+ "version": "5.9.0",
+ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz",
+ "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==",
+ "dev": true,
+ "dependencies": {
+ "clone-deep": "^4.0.1",
+ "wildcard": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/webpack-sources": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
+ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz",
+ "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wildcard": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+ "dev": true
+ },
+ "node_modules/workerpool": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz",
+ "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==",
+ "dev": true
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/pw_ide/ts/pigweed-vscode/package.json b/pw_ide/ts/pigweed-vscode/package.json
new file mode 100644
index 000000000..fb5104714
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/package.json
@@ -0,0 +1,67 @@
+{
+ "publisher": "pigweed",
+ "name": "pigweed-vscode",
+ "displayName": "Pigweed Visual Studio Code Extension",
+ "description": "Visual Studio Code support for Pigweed projects",
+ "repository": {
+ "type": "git",
+ "url": "https://pigweed.googlesource.com/pigweed/pigweed"
+ },
+ "version": "0.1.1",
+ "license": "Apache-2.0",
+ "engines": {
+ "vscode": "^1.64.0"
+ },
+ "categories": [
+ "Debuggers",
+ "Formatters",
+ "Linters",
+ "Programming Languages",
+ "Other",
+ "Testing"
+ ],
+ "activationEvents": [
+ "workspaceContains:**/pigweed.json"
+ ],
+ "main": "./out/extension.js",
+ "contributes": {
+ "commands": [
+ {
+ "command": "pigweed.check-extensions",
+ "title": "Pigweed: Check Extensions"
+ }
+ ]
+ },
+ "scripts": {
+ "vscode:prepublish": "npm run package",
+ "webpack": "webpack --mode development",
+ "webpack-dev": "webpack --mode development --watch",
+ "package": "webpack --mode production --devtool hidden-source-map",
+ "compile": "tsc -p ./",
+ "watch": "tsc -watch -p ./",
+ "pretest": "npm run compile && npm run lint",
+ "lint": "eslint src --ext ts",
+ "test": "node ./out/test/runTest.js"
+ },
+ "dependencies": {
+ "glob": "^8.1.0",
+ "hjson": "^3.2.2"
+ },
+ "devDependencies": {
+ "@types/glob": "^8.1.0",
+ "@types/hjson": "2.4.3",
+ "@types/mocha": "^10.0.1",
+ "@types/node": "20.2.5",
+ "@types/vscode": "^1.64.0",
+ "@typescript-eslint/eslint-plugin": "^5.59.8",
+ "@typescript-eslint/parser": "^5.59.8",
+ "@vscode/test-electron": "^2.3.2",
+ "eslint": "^8.41.0",
+ "mocha": "^10.2.0",
+ "node-polyfill-webpack-plugin": "^2.0.1",
+ "ts-loader": "^9.4.4",
+ "typescript": "^5.1.3",
+ "webpack": "^5.88.2",
+ "webpack-cli": "^5.1.4"
+ }
+}
diff --git a/pw_ide/ts/pigweed-vscode/src/config.ts b/pw_ide/ts/pigweed-vscode/src/config.ts
new file mode 100644
index 000000000..c4879fba6
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/src/config.ts
@@ -0,0 +1,94 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import * as hjson from 'hjson';
+import * as vscode from 'vscode';
+
+/**
+ * Schema for extensions.json
+ */
+export interface ExtensionsJson {
+ recommendations?: string[];
+ unwantedRecommendations?: string[];
+}
+
+/**
+ * Partial schema for the workspace config file
+ */
+interface WorkspaceConfig {
+ extensions?: ExtensionsJson;
+}
+
+// When the project is opened directly (i.e., by opening the repo directory),
+// we have direct access to extensions.json. But if the project is part of a
+// workspace (https://code.visualstudio.com/docs/editor/workspaces), we'll get
+// a combined config that includes the equivalent of extensions.json associated
+// with the "extensions" key. This is taken into consideration only for the sake
+// of completeness; Pigweed doesn't currently support the use of workspaces.
+type LoadableConfig = ExtensionsJson & WorkspaceConfig;
+
+/**
+ * Load a config file that contains extensions.json data. This could be
+ * extensions.json itself, or a workspace file that contains the equivalent.
+ * @param uri - A file path to load
+ * @returns - The extensions.json file data
+ */
+export async function loadExtensionsJson(
+ uri: vscode.Uri,
+): Promise<ExtensionsJson> {
+ const buffer = await vscode.workspace.fs.readFile(uri);
+ const config: LoadableConfig = hjson.parse(buffer.toString());
+
+ if (config.extensions) {
+ return config.extensions;
+ }
+
+ return config as ExtensionsJson;
+}
+
+/**
+ * Find and return the extensions.json data for the project.
+ * @param includeWorkspace - Also search workspace files
+ * @returns The extensions.json file data
+ */
+export async function getExtensionsJson(
+ includeWorkspace = false,
+): Promise<ExtensionsJson> {
+ const files = await vscode.workspace.findFiles(
+ '.vscode/extensions.json',
+ '**/node_modules/**',
+ );
+
+ if (includeWorkspace) {
+ const workspaceFile = vscode.workspace.workspaceFile;
+
+ if (workspaceFile) {
+ files.push(workspaceFile);
+ }
+ }
+
+ if (files.length == 0) {
+ // TODO(chadnorvell): Improve this
+ vscode.window.showErrorMessage('extensions.json is missing!');
+ throw new Error('extensions.json is missing!');
+ } else {
+ if (files.length > 1) {
+ vscode.window.showWarningMessage(
+ 'Found multiple extensions.json! Will only use the first.',
+ );
+ }
+
+ return await loadExtensionsJson(files[0]);
+ }
+}
diff --git a/pw_ide/ts/pigweed-vscode/src/extension.ts b/pw_ide/ts/pigweed-vscode/src/extension.ts
new file mode 100644
index 000000000..16e1ca51f
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/src/extension.ts
@@ -0,0 +1,242 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import * as vscode from 'vscode';
+
+import { getExtensionsJson } from './config';
+
+/**
+ * Open the extensions sidebar and show the provided extensions.
+ * @param extensions - A list of extension IDs
+ */
+function showExtensions(extensions: string[]) {
+ vscode.commands.executeCommand(
+ 'workbench.extensions.search',
+ '@id:' + extensions.join(', @id:'),
+ );
+}
+
+/**
+ * Given a list of extensions, return the subset that are not installed or are
+ * disabled.
+ * @param extensions - A list of extension IDs
+ * @returns A list of extension IDs
+ */
+function getUnavailableExtensions(extensions: string[]): string[] {
+ const unavailableExtensions: string[] = [];
+ const available = vscode.extensions.all;
+
+ // TODO(chadnorvell): Verify that this includes disabled extensions
+ extensions.map(async (extId) => {
+ const ext = available.find((ext) => ext.id == extId);
+
+ if (!ext) {
+ unavailableExtensions.push(extId);
+ }
+ });
+
+ return unavailableExtensions;
+}
+
+/**
+ * If there are recommended extensions that are not installed or enabled in the
+ * current workspace, prompt the user to install them. This is "sticky" in the
+ * sense that it will keep bugging the user to enable those extensions until
+ * they enable them all, or until they explicitly cancel.
+ * @param recs - A list of extension IDs
+ */
+async function installRecommendedExtensions(recs: string[]): Promise<void> {
+ let unavailableRecs = getUnavailableExtensions(recs);
+ const totalNumUnavailableRecs = unavailableRecs.length;
+ let numUnavailableRecs = totalNumUnavailableRecs;
+
+ const update = () => {
+ unavailableRecs = getUnavailableExtensions(recs);
+ numUnavailableRecs = unavailableRecs.length;
+ };
+
+ const wait = async () => new Promise((resolve) => setTimeout(resolve, 2500));
+
+ const progressIncrement = (num: number) =>
+ 1 - (num / totalNumUnavailableRecs) * 100;
+
+ // All recommendations are installed; we're done.
+ if (totalNumUnavailableRecs == 0) {
+ console.log('User has all recommended extensions');
+
+ return;
+ }
+
+ showExtensions(unavailableRecs);
+
+ vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ // TODO(chadnorvell): Make this look better
+ title:
+ 'Install these extensions! This Pigweed project needs these recommended extensions to be installed.',
+ cancellable: true,
+ },
+ async (progress, token) => {
+ while (numUnavailableRecs > 0) {
+ // TODO(chadnorvell): Wait for vscode.extensions.onDidChange
+ await wait();
+ update();
+
+ progress.report({
+ increment: progressIncrement(numUnavailableRecs),
+ });
+
+ if (numUnavailableRecs > 0) {
+ console.log(
+ `User lacks ${numUnavailableRecs} recommended extensions`,
+ );
+
+ showExtensions(unavailableRecs);
+ }
+
+ if (token.isCancellationRequested) {
+ console.log('User cancelled recommended extensions check');
+
+ break;
+ }
+ }
+
+ console.log('All recommended extensions are enabled');
+ progress.report({ increment: 100 });
+ },
+ );
+}
+
+/**
+ * Given a list of extensions, return the subset that are enabled.
+ * @param extensions - A list of extension IDs
+ * @returns A list of extension IDs
+ */
+function getEnabledExtensions(extensions: string[]): string[] {
+ const enabledExtensions: string[] = [];
+ const available = vscode.extensions.all;
+
+ // TODO(chadnorvell): Verify that this excludes disabled extensions
+ extensions.map(async (extId) => {
+ const ext = available.find((ext) => ext.id == extId);
+
+ if (ext) {
+ enabledExtensions.push(extId);
+ }
+ });
+
+ return enabledExtensions;
+}
+
+/**
+ * If there are unwanted extensions that are enabled in the current workspace,
+ * prompt the user to disable them. This is "sticky" in the sense that it will
+ * keep bugging the user to disable those extensions until they disable them
+ * all, or until they explicitly cancel.
+ * @param recs - A list of extension IDs
+ */
+async function disableUnwantedExtensions(unwanted: string[]) {
+ let enabledUnwanted = getEnabledExtensions(unwanted);
+ const totalNumEnabledUnwanted = enabledUnwanted.length;
+ let numEnabledUnwanted = totalNumEnabledUnwanted;
+
+ const update = () => {
+ enabledUnwanted = getEnabledExtensions(unwanted);
+ numEnabledUnwanted = enabledUnwanted.length;
+ };
+
+ const wait = async () => new Promise((resolve) => setTimeout(resolve, 2500));
+
+ const progressIncrement = (num: number) =>
+ 1 - (num / totalNumEnabledUnwanted) * 100;
+
+ // All unwanted are disabled; we're done.
+ if (totalNumEnabledUnwanted == 0) {
+ console.log('User has no unwanted extensions enabled');
+
+ return;
+ }
+
+ showExtensions(enabledUnwanted);
+
+ vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ // TODO(chadnorvell): Make this look better
+ title:
+ 'Disable these extensions! This Pigweed project needs these extensions to be disabled.',
+ cancellable: true,
+ },
+ async (progress, token) => {
+ while (numEnabledUnwanted > 0) {
+ // TODO(chadnorvell): Wait for vscode.extensions.onDidChange
+ await wait();
+ update();
+
+ progress.report({
+ increment: progressIncrement(numEnabledUnwanted),
+ });
+
+ if (numEnabledUnwanted > 0) {
+ console.log(
+ `User has ${numEnabledUnwanted} unwanted extensions enabled`,
+ );
+
+ showExtensions(enabledUnwanted);
+ }
+
+ if (token.isCancellationRequested) {
+ console.log('User cancelled unwanted extensions check');
+
+ break;
+ }
+ }
+
+ console.log('All unwanted extensions are disabled');
+ progress.report({ increment: 100 });
+ },
+ );
+}
+
+async function checkExtensions() {
+ const extensions = await getExtensionsJson();
+
+ const num_recommendations = extensions.recommendations?.length ?? 0;
+ const num_unwanted = extensions.unwantedRecommendations?.length ?? 0;
+
+ if (num_recommendations > 0) {
+ await installRecommendedExtensions(extensions.recommendations as string[]);
+ }
+
+ if (num_unwanted > 0) {
+ await disableUnwantedExtensions(
+ extensions.unwantedRecommendations as string[],
+ );
+ }
+}
+
+export function activate(context: vscode.ExtensionContext) {
+ const pwCheckExtensions = vscode.commands.registerCommand(
+ 'pigweed.check-extensions',
+ () => checkExtensions(),
+ );
+
+ context.subscriptions.push(pwCheckExtensions);
+ checkExtensions();
+}
+
+export function deactivate() {
+ // Do nothing.
+}
diff --git a/pw_ide/ts/pigweed-vscode/tsconfig.json b/pw_ide/ts/pigweed-vscode/tsconfig.json
new file mode 100644
index 000000000..8a14b3fce
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "ES2020",
+ "outDir": "out",
+ "lib": [
+ "ES2020"
+ ],
+ "exclude": [
+ "*.js"
+ ],
+ "sourceMap": true,
+ "strict": true
+ }
+}
diff --git a/pw_ide/ts/pigweed-vscode/webpack.config.js b/pw_ide/ts/pigweed-vscode/webpack.config.js
new file mode 100644
index 000000000..df60d43f5
--- /dev/null
+++ b/pw_ide/ts/pigweed-vscode/webpack.config.js
@@ -0,0 +1,72 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+'use strict';
+
+const path = require('path');
+const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
+const webpack = require('webpack');
+
+/**@type {import('webpack').Configuration}*/
+const config = {
+ // vscode extensions run in webworker context for VS
+ // Code web 📖 -> https://webpack.js.org/configuration/target/#target
+ target: 'webworker',
+
+ // the entry point of this extension, 📖 ->
+ // https://webpack.js.org/configuration/entry-context/
+ entry: './src/extension.ts',
+
+ output: {
+ // the bundle is stored in the 'dist' folder (check package.json), 📖 ->
+ // https://webpack.js.org/configuration/output/
+ path: path.resolve(__dirname, 'dist'),
+ filename: 'extension.js',
+ libraryTarget: 'commonjs2',
+ devtoolModuleFilenameTemplate: '../[resource-path]',
+ },
+ devtool: 'source-map',
+ plugins: [new NodePolyfillPlugin()],
+ externals: {
+ // the vscode-module is created on-the-fly and must
+ // be excluded. Add other modules that cannot be
+ // webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/
+ vscode: 'commonjs vscode',
+ },
+ resolve: {
+ // support reading TypeScript and JavaScript files, 📖 ->
+ // https://github.com/TypeStrong/ts-loader
+ // look for `browser` entry point in imported node modules
+ mainFields: ['browser', 'module', 'main'],
+ extensions: ['.ts', '.js'],
+ alias: {
+ // provides alternate implementation for node module and source files
+ },
+ fallback: {
+ // Webpack 5 no longer polyfills Node.js core modules automatically.
+ // see https://webpack.js.org/configuration/resolve/#resolvefallback
+ // for the list of Node.js core module polyfills.
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /\.ts$/,
+ exclude: /node_modules/,
+ use: [{ loader: 'ts-loader' }],
+ },
+ ],
+ },
+};
+module.exports = config;
diff --git a/pw_interrupt/BUILD.bazel b/pw_interrupt/BUILD.bazel
index 5379e0628..00b7598c9 100644
--- a/pw_interrupt/BUILD.bazel
+++ b/pw_interrupt/BUILD.bazel
@@ -22,6 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_setting(
+ name = "backend_constraint_setting",
+)
+
pw_cc_facade(
name = "context_facade",
hdrs = [
@@ -32,32 +36,21 @@ pw_cc_facade(
pw_cc_library(
name = "context",
+ hdrs = [
+ "public/pw_interrupt/context.h",
+ ],
+ includes = ["public"],
deps = [
- ":context_facade",
- "@pigweed_config//:pw_interrupt_backend",
+ "@pigweed//targets:pw_interrupt_backend",
],
)
-pw_cc_library(
+alias(
name = "backend_multiplexer",
- # Normally this would be done in the backend packages but because there is
- # no host implementation we have to define this here.
- target_compatible_with = select({
- "@platforms//cpu:armv7-m": [],
- "@platforms//cpu:armv7e-m": [],
- "@platforms//cpu:armv8-m": [],
- "//conditions:default": ["@platforms//:incompatible"],
- }),
- visibility = ["@pigweed_config//:__pkg__"],
- deps = select({
- "@platforms//cpu:armv7-m": ["//pw_interrupt_cortex_m:context_armv7m"],
- "@platforms//cpu:armv7e-m": ["//pw_interrupt_cortex_m:context_armv7m"],
- "@platforms//cpu:armv8-m": ["//pw_interrupt_cortex_m:context_armv8m"],
- # This is required for this to be a valid select when building for the
- # host i.e. 'bazel build //pw_interrupt/...'. The
- # target_compatible_with attribute is used to skip this target when
- # built with a wildcard. If explicitly depended on for a host build
- # the build will fail.
- "//conditions:default": [],
+ actual = select({
+ "//pw_interrupt_cortex_m:backend": "//pw_interrupt_cortex_m:context",
+ "//pw_interrupt_xtensa:backend": "//pw_interrupt_xtensa:context",
+ "//conditions:default": "//pw_build:unspecified_backend",
}),
+ visibility = ["@pigweed//targets:__pkg__"],
)
diff --git a/pw_interrupt/docs.rst b/pw_interrupt/docs.rst
index 1bc7d219f..711dcf1f5 100644
--- a/pw_interrupt/docs.rst
+++ b/pw_interrupt/docs.rst
@@ -3,11 +3,8 @@
------------
pw_interrupt
------------
-Pigweed's interrupt module provides a consistent interface for to determine
-whether one is currently executing in an interrupt context (IRQ or NMI) or not.
-
-.. c:function:: bool InInterruptContext()
-
- Returns true if currently executing within an interrupt service routine
- handling an IRQ or NMI.:w!
+Pigweed's interrupt module provides a consistent interface for determining
+whether your code is currently executing in an interrupt context (IRQ or NMI)
+or not.
+.. doxygenfunction:: pw::interrupt::InInterruptContext()
diff --git a/pw_interrupt/public/pw_interrupt/context.h b/pw_interrupt/public/pw_interrupt/context.h
index 6b8bd123a..477400351 100644
--- a/pw_interrupt/public/pw_interrupt/context.h
+++ b/pw_interrupt/public/pw_interrupt/context.h
@@ -15,8 +15,11 @@
namespace pw::interrupt {
-// Returns true if currently executing within an interrupt service routine
-// handling an IRQ or NMI.
+/// @brief Checks if the currently executing code is within an interrupt service
+/// routine handling an interrupt request (IRQ) or non-maskable interrupt (NMI).
+///
+/// @returns `true` if the the currently executing code is in an interrupt
+/// context. `false` if not.
bool InInterruptContext();
} // namespace pw::interrupt
diff --git a/pw_interrupt_cortex_m/BUILD.bazel b/pw_interrupt_cortex_m/BUILD.bazel
index 153af3924..6ec832130 100644
--- a/pw_interrupt_cortex_m/BUILD.bazel
+++ b/pw_interrupt_cortex_m/BUILD.bazel
@@ -21,8 +21,13 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_interrupt:backend_constraint_setting",
+)
+
pw_cc_library(
- name = "context_headers",
+ name = "context",
hdrs = [
"public/pw_interrupt_cortex_m/context_inline.h",
"public_overrides/pw_interrupt_backend/context_inline.h",
@@ -31,44 +36,9 @@ pw_cc_library(
"public",
"public_overrides",
],
+ target_compatible_with = [":backend"],
deps = [
- "//pw_preprocessor:cortex_m",
- ],
-)
-
-pw_cc_library(
- name = "context",
- deps = [
- ":context_headers",
"//pw_interrupt:context_facade",
- ],
-)
-
-# The following targets are deprecated, depend on ":context" instead.
-pw_cc_library(
- name = "context_armv7m_headers",
- deps = [
- ":context_headers",
- ],
-)
-
-pw_cc_library(
- name = "context_armv7m",
- deps = [
- ":context",
- ],
-)
-
-pw_cc_library(
- name = "context_armv8m_headers",
- deps = [
- ":context_headers",
- ],
-)
-
-pw_cc_library(
- name = "context_armv8m",
- deps = [
- ":context",
+ "//pw_preprocessor:cortex_m",
],
)
diff --git a/pw_interrupt_xtensa/BUILD.bazel b/pw_interrupt_xtensa/BUILD.bazel
new file mode 100644
index 000000000..c3ab8e2aa
--- /dev/null
+++ b/pw_interrupt_xtensa/BUILD.bazel
@@ -0,0 +1,36 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_interrupt:backend_constraint_setting",
+)
+
+pw_cc_library(
+ name = "context",
+ srcs = ["context.cc"],
+ target_compatible_with = [":backend"],
+ deps = [
+ "//pw_interrupt:context_facade",
+ ],
+)
diff --git a/pw_interrupt_xtensa/BUILD.gn b/pw_interrupt_xtensa/BUILD.gn
new file mode 100644
index 000000000..c822cb3cb
--- /dev/null
+++ b/pw_interrupt_xtensa/BUILD.gn
@@ -0,0 +1,31 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_source_set("context") {
+ public_deps = [ "$dir_pw_interrupt:context.facade" ]
+ sources = [ "context.cc" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_interrupt_xtensa/context.cc b/pw_interrupt_xtensa/context.cc
new file mode 100644
index 000000000..9c16fcaf5
--- /dev/null
+++ b/pw_interrupt_xtensa/context.cc
@@ -0,0 +1,34 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_interrupt/context.h"
+
+#include <xtensa/config/core.h>
+#include <xtensa/hal.h>
+
+#include <cstdint>
+
+namespace pw::interrupt {
+
+bool InInterruptContext() {
+ // xthal_intlevel_get returns the current interrupt level of the processor
+ // (value of PS.INTLEVEL register). C based handlers are always dispatched
+ // from an interrupt level below XCHAL_EXCM_LEVEL - handlers running above
+ // this level must be written in assembly. The interrupt level is set to zero
+ // when interrupts are enabled but the core isn't currently procesing one.
+ const uint32_t int_level = xthal_intlevel_get();
+ return (int_level < XCHAL_EXCM_LEVEL) && (int_level > 0);
+}
+
+} // namespace pw::interrupt
diff --git a/pw_interrupt_xtensa/docs.rst b/pw_interrupt_xtensa/docs.rst
new file mode 100644
index 000000000..182bdbd35
--- /dev/null
+++ b/pw_interrupt_xtensa/docs.rst
@@ -0,0 +1,7 @@
+.. _module-pw_interrupt_xtensa:
+
+-------------------
+pw_interrupt_xtensa
+-------------------
+Pigweed's interrupt Xtensa module provides an architecture specific
+backend for ``pw_interrupt``.
diff --git a/pw_interrupt_zephyr/CMakeLists.txt b/pw_interrupt_zephyr/CMakeLists.txt
index 52768a008..e5fc9223a 100644
--- a/pw_interrupt_zephyr/CMakeLists.txt
+++ b/pw_interrupt_zephyr/CMakeLists.txt
@@ -14,10 +14,6 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(NOT CONFIG_PIGWEED_INTERRUPT_CONTEXT)
- return()
-endif()
-
pw_add_library(pw_interrupt_zephyr.context INTERFACE
HEADERS
public/pw_interrupt_zephyr/context_inline.h
@@ -28,5 +24,4 @@ pw_add_library(pw_interrupt_zephyr.context INTERFACE
PUBLIC_DEPS
pw_interrupt.context.facade
)
-zephyr_link_interface(pw_interrupt_zephyr.context)
-zephyr_link_libraries(pw_interrupt_zephyr.context)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_INTERRUPT_CONTEXT pw_interrupt_zephyr.context)
diff --git a/pw_interrupt_zephyr/Kconfig b/pw_interrupt_zephyr/Kconfig
index cf51e3b59..64e8b5f7d 100644
--- a/pw_interrupt_zephyr/Kconfig
+++ b/pw_interrupt_zephyr/Kconfig
@@ -12,5 +12,11 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_interrupt"
+
config PIGWEED_INTERRUPT_CONTEXT
- bool "Enable the Pigweed interrupt library (pw_interrupt.context)"
+ bool "Link pw_interrupt.context library"
+ help
+ See :ref:`module-pw_interrupt` for module details.
+
+endmenu
diff --git a/pw_intrusive_ptr/BUILD.gn b/pw_intrusive_ptr/BUILD.gn
index fee72c0d8..852792785 100644
--- a/pw_intrusive_ptr/BUILD.gn
+++ b/pw_intrusive_ptr/BUILD.gn
@@ -51,7 +51,7 @@ pw_test("intrusive_ptr_test") {
sources = [ "intrusive_ptr_test.cc" ]
deps = [ ":pw_intrusive_ptr" ]
- # TODO(b/260624583): Fix this for //targets/rp2040
+ # TODO: b/260624583 - Fix this for //targets/rp2040
enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "pico_executable"
}
@@ -59,6 +59,6 @@ pw_test("recyclable_test") {
sources = [ "recyclable_test.cc" ]
deps = [ ":pw_intrusive_ptr" ]
- # TODO(b/260624583): Fix this for //targets/rp2040
+ # TODO: b/260624583 - Fix this for //targets/rp2040
enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "pico_executable"
}
diff --git a/pw_kvs/BUILD.bazel b/pw_kvs/BUILD.bazel
index 5b1fbfd1a..80bf3a5bb 100644
--- a/pw_kvs/BUILD.bazel
+++ b/pw_kvs/BUILD.bazel
@@ -104,6 +104,17 @@ pw_cc_library(
)
pw_cc_library(
+ name = "flash_partition_with_logical_sectors",
+ hdrs = [
+ "public/pw_kvs/flash_partition_with_logical_sectors.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":pw_kvs",
+ ],
+)
+
+pw_cc_library(
name = "fake_flash_1_aligned_partition",
srcs = [
"fake_flash_test_partition.cc",
@@ -123,6 +134,27 @@ pw_cc_library(
)
pw_cc_library(
+ name = "fake_flash_1_aligned_4_logical_partition",
+ srcs = [
+ "fake_flash_test_logical_sector_partition.cc",
+ ],
+ hdrs = [
+ "public/pw_kvs/flash_test_partition.h",
+ ],
+ defines = [
+ "PW_FLASH_TEST_SECTORS=24U",
+ "PW_FLASH_TEST_SECTOR_SIZE=4096U",
+ "PW_FLASH_TEST_ALIGNMENT=1U",
+ "PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR=4U",
+ ],
+ deps = [
+ ":fake_flash",
+ ":flash_partition_with_logical_sectors",
+ ":pw_kvs",
+ ],
+)
+
+pw_cc_library(
name = "fake_flash_12_byte_partition",
srcs = ["fake_flash_test_partition.cc"],
hdrs = ["public/pw_kvs/flash_test_partition.h"],
@@ -313,6 +345,21 @@ pw_cc_test(
)
pw_cc_test(
+ name = "flash_partition_1_alignment_4_logical_test",
+ srcs = ["flash_partition_test.cc"],
+ defines = [
+ "PW_FLASH_TEST_ITERATIONS=2",
+ "PW_FLASH_TEST_WRITE_SIZE=1",
+ ],
+ deps = [
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":pw_kvs",
+ "//pw_log",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "flash_partition_16_alignment_test",
srcs = ["flash_partition_test.cc"],
defines = [
@@ -373,6 +420,21 @@ pw_cc_test(
)
pw_cc_test(
+ name = "flash_partition_4_logical_256_write_size_test",
+ srcs = ["flash_partition_test.cc"],
+ defines = [
+ "PW_FLASH_TEST_ITERATIONS=2",
+ "PW_FLASH_TEST_WRITE_SIZE=256",
+ ],
+ deps = [
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":pw_kvs",
+ "//pw_log",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "key_test",
srcs = [
"key_test.cc",
@@ -419,6 +481,23 @@ pw_cc_test(
)
pw_cc_test(
+ name = "key_value_store_1_alignment_4_logical_flash_test",
+ srcs = ["key_value_store_initialized_test.cc"],
+ deps = [
+ ":crc16",
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":pw_kvs",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_log:facade",
+ "//pw_span",
+ "//pw_status",
+ "//pw_string:builder",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "key_value_store_16_alignment_flash_test",
srcs = ["key_value_store_initialized_test.cc"],
deps = [
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index 907769f46..1125e6d69 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -120,6 +120,13 @@ pw_source_set("fake_flash") {
]
}
+pw_source_set("flash_partition_with_logical_sectors") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_kvs/flash_partition_with_logical_sectors.h" ]
+ public_deps = [ dir_pw_kvs ]
+ deps = [ ":config" ]
+}
+
pw_source_set("fake_flash_12_byte_partition") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_kvs/flash_test_partition.h" ]
@@ -200,6 +207,24 @@ pw_source_set("fake_flash_256_aligned_partition") {
]
}
+pw_source_set("fake_flash_1_aligned_4_logical_partition") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_kvs/flash_test_partition.h" ]
+ sources = [ "fake_flash_test_logical_sector_partition.cc" ]
+ public_deps = [ ":flash_test_partition" ]
+ deps = [
+ ":fake_flash",
+ ":flash_partition_with_logical_sectors",
+ dir_pw_kvs,
+ ]
+ defines = [
+ "PW_FLASH_TEST_SECTORS=24U",
+ "PW_FLASH_TEST_SECTOR_SIZE=4096U",
+ "PW_FLASH_TEST_ALIGNMENT=1U",
+ "PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR=4U",
+ ]
+}
+
pw_source_set("fake_flash_test_key_value_store") {
public_configs = [ ":public_include_path" ]
sources = [ "fake_flash_test_key_value_store.cc" ]
@@ -358,7 +383,7 @@ pw_test_group("tests") {
if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
pw_toolchain_SCOPE.is_host_toolchain) {
- # TODO(b/234883746): KVS tests are not compatible with device builds as they
+ # TODO: b/234883746 - KVS tests are not compatible with device builds as they
# use features such as std::map and are computationally expensive. Solving
# this requires a more complex capabilities-based build and configuration
# system which allowing enabling specific tests for targets that support
@@ -368,13 +393,17 @@ pw_test_group("tests") {
":entry_test",
":entry_cache_test",
":flash_partition_1_stream_test",
+ ":flash_partition_4_logical_stream_test",
":flash_partition_1_alignment_test",
+ ":flash_partition_1_alignment_4_logical_test",
":flash_partition_16_alignment_test",
":flash_partition_64_alignment_test",
":flash_partition_256_alignment_test",
":flash_partition_256_write_size_test",
+ ":flash_partition_4_logical_256_write_size_test",
":key_value_store_test",
":key_value_store_1_alignment_flash_test",
+ ":key_value_store_1_alignment_4_logical_flash_test",
":key_value_store_16_alignment_flash_test",
":key_value_store_64_alignment_flash_test",
":key_value_store_256_alignment_flash_test",
@@ -437,6 +466,15 @@ pw_test("flash_partition_1_stream_test") {
]
}
+pw_test("flash_partition_4_logical_stream_test") {
+ deps = [
+ ":fake_flash",
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":flash_partition_stream_test",
+ dir_pw_log,
+ ]
+}
+
pw_test("flash_partition_1_alignment_test") {
deps = [
":fake_flash",
@@ -446,6 +484,15 @@ pw_test("flash_partition_1_alignment_test") {
]
}
+pw_test("flash_partition_1_alignment_4_logical_test") {
+ deps = [
+ ":fake_flash",
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":flash_partition_test_2_iterations",
+ dir_pw_log,
+ ]
+}
+
pw_test("flash_partition_16_alignment_test") {
deps = [
":fake_flash",
@@ -482,6 +529,15 @@ pw_test("flash_partition_256_write_size_test") {
]
}
+pw_test("flash_partition_4_logical_256_write_size_test") {
+ deps = [
+ ":fake_flash",
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":flash_partition_test_2_iterations_256_write",
+ dir_pw_log,
+ ]
+}
+
pw_test("key_value_store_test") {
deps = [
":config",
@@ -503,6 +559,13 @@ pw_test("key_value_store_1_alignment_flash_test") {
]
}
+pw_test("key_value_store_1_alignment_4_logical_flash_test") {
+ deps = [
+ ":fake_flash_1_aligned_4_logical_partition",
+ ":key_value_store_initialized_test",
+ ]
+}
+
pw_test("key_value_store_16_alignment_flash_test") {
deps = [
":fake_flash_16_aligned_partition",
diff --git a/pw_kvs/CMakeLists.txt b/pw_kvs/CMakeLists.txt
index 1d3a2e7e8..188abcf18 100644
--- a/pw_kvs/CMakeLists.txt
+++ b/pw_kvs/CMakeLists.txt
@@ -109,6 +109,15 @@ pw_add_library(pw_kvs.fake_flash STATIC
pw_log
)
+pw_add_library(pw_kvs.flash_partition_with_logical_sectors INTERFACE
+ HEADERS
+ public/pw_kvs/flash_partition_with_logical_sectors.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs
+)
+
pw_add_library(pw_kvs.fake_flash_12_byte_partition STATIC
HEADERS
public/pw_kvs/flash_test_partition.h
@@ -145,6 +154,26 @@ pw_add_library(pw_kvs.fake_flash_1_aligned_partition STATIC
PW_FLASH_TEST_ALIGNMENT=1U
)
+pw_add_library(pw_kvs.fake_flash_1_aligned_4_logical_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs.flash_test_partition
+ SOURCES
+ fake_flash_test_logical_sector_partition.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.flash_partition_with_logical_sectors
+ pw_kvs
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_SECTORS=24U
+ PW_FLASH_TEST_SECTOR_SIZE=4096U
+ PW_FLASH_TEST_ALIGNMENT=1U
+ PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR=4U
+)
+
pw_add_library(pw_kvs.fake_flash_16_aligned_partition STATIC
HEADERS
public/pw_kvs/flash_test_partition.h
@@ -424,6 +453,17 @@ pw_add_test(pw_kvs.flash_partition_1_alignment_test
pw_kvs
)
+pw_add_test(pw_kvs.flash_partition_1_alignment_4_logical_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_1_aligned_4_logical_partition
+ pw_kvs.flash_partition_test_100_iterations
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
pw_add_test(pw_kvs.flash_partition_16_alignment_test
PRIVATE_DEPS
pw_kvs.fake_flash
@@ -468,6 +508,17 @@ pw_add_test(pw_kvs.flash_partition_256_write_size_test
pw_kvs
)
+pw_add_test(pw_kvs.flash_partition_4_logical_256_write_size_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_1_aligned_4_logical_partition
+ pw_kvs.flash_partition_test_2_iterations_256_write
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
pw_add_test(pw_kvs.key_value_store_test
SOURCES
key_value_store_test.cc
@@ -494,6 +545,15 @@ pw_add_test(pw_kvs.key_value_store_1_alignment_flash_test
pw_kvs
)
+pw_add_test(pw_kvs.key_value_store_1_alignment_4_logical_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_1_aligned_4_logical_partition
+ pw_kvs.key_value_store_initialized_test
+ GROUPS
+ modules
+ pw_kvs
+)
+
pw_add_test(pw_kvs.key_value_store_16_alignment_flash_test
PRIVATE_DEPS
pw_kvs.fake_flash_16_aligned_partition
diff --git a/pw_kvs/alignment_test.cc b/pw_kvs/alignment_test.cc
index 81edbb911..dab5d0ae6 100644
--- a/pw_kvs/alignment_test.cc
+++ b/pw_kvs/alignment_test.cc
@@ -26,99 +26,6 @@ namespace {
using namespace std::string_view_literals;
using std::byte;
-TEST(AlignUp, Zero) {
- EXPECT_EQ(0u, AlignUp(0, 1));
- EXPECT_EQ(0u, AlignUp(0, 2));
- EXPECT_EQ(0u, AlignUp(0, 15));
-}
-
-TEST(AlignUp, Aligned) {
- for (size_t i = 1; i < 130; ++i) {
- EXPECT_EQ(i, AlignUp(i, i));
- EXPECT_EQ(2 * i, AlignUp(2 * i, i));
- EXPECT_EQ(3 * i, AlignUp(3 * i, i));
- }
-}
-
-TEST(AlignUp, NonAligned_PowerOf2) {
- EXPECT_EQ(32u, AlignUp(1, 32));
- EXPECT_EQ(32u, AlignUp(31, 32));
- EXPECT_EQ(64u, AlignUp(33, 32));
- EXPECT_EQ(64u, AlignUp(45, 32));
- EXPECT_EQ(64u, AlignUp(63, 32));
- EXPECT_EQ(128u, AlignUp(127, 32));
-}
-
-TEST(AlignUp, NonAligned_NonPowerOf2) {
- EXPECT_EQ(2u, AlignUp(1, 2));
-
- EXPECT_EQ(15u, AlignUp(1, 15));
- EXPECT_EQ(15u, AlignUp(14, 15));
- EXPECT_EQ(30u, AlignUp(16, 15));
-}
-
-TEST(AlignDown, Zero) {
- EXPECT_EQ(0u, AlignDown(0, 1));
- EXPECT_EQ(0u, AlignDown(0, 2));
- EXPECT_EQ(0u, AlignDown(0, 15));
-}
-
-TEST(AlignDown, Aligned) {
- for (size_t i = 1; i < 130; ++i) {
- EXPECT_EQ(i, AlignDown(i, i));
- EXPECT_EQ(2 * i, AlignDown(2 * i, i));
- EXPECT_EQ(3 * i, AlignDown(3 * i, i));
- }
-}
-
-TEST(AlignDown, NonAligned_PowerOf2) {
- EXPECT_EQ(0u, AlignDown(1, 32));
- EXPECT_EQ(0u, AlignDown(31, 32));
- EXPECT_EQ(32u, AlignDown(33, 32));
- EXPECT_EQ(32u, AlignDown(45, 32));
- EXPECT_EQ(32u, AlignDown(63, 32));
- EXPECT_EQ(96u, AlignDown(127, 32));
-}
-
-TEST(AlignDown, NonAligned_NonPowerOf2) {
- EXPECT_EQ(0u, AlignDown(1, 2));
-
- EXPECT_EQ(0u, AlignDown(1, 15));
- EXPECT_EQ(0u, AlignDown(14, 15));
- EXPECT_EQ(15u, AlignDown(16, 15));
-}
-
-TEST(Padding, Zero) {
- EXPECT_EQ(0u, Padding(0, 1));
- EXPECT_EQ(0u, Padding(0, 2));
- EXPECT_EQ(0u, Padding(0, 15));
-}
-
-TEST(Padding, Aligned) {
- for (size_t i = 1; i < 130; ++i) {
- EXPECT_EQ(0u, Padding(i, i));
- EXPECT_EQ(0u, Padding(2 * i, i));
- EXPECT_EQ(0u, Padding(3 * i, i));
- }
-}
-
-TEST(Padding, NonAligned_PowerOf2) {
- EXPECT_EQ(31u, Padding(1, 32));
- EXPECT_EQ(1u, Padding(31, 32));
- EXPECT_EQ(31u, Padding(33, 32));
- EXPECT_EQ(19u, Padding(45, 32));
- EXPECT_EQ(1u, Padding(63, 32));
- EXPECT_EQ(1u, Padding(127, 32));
-}
-
-TEST(Padding, NonAligned_NonPowerOf2) {
- EXPECT_EQ(1u, Padding(1, 2));
-
- EXPECT_EQ(14u, Padding(1, 15));
- EXPECT_EQ(1u, Padding(14, 15));
- EXPECT_EQ(14u, Padding(16, 15));
-}
-
constexpr size_t kAlignment = 10;
constexpr std::string_view kData =
diff --git a/pw_kvs/docs.rst b/pw_kvs/docs.rst
index 4f4452211..8489cb66f 100644
--- a/pw_kvs/docs.rst
+++ b/pw_kvs/docs.rst
@@ -83,7 +83,17 @@ they are part of. Partition logical sectors may be smaller due to partition
overhead (encryption, wear tracking, etc) or larger due to combining raw
sectors into larger logical sectors.
-FlashPartition supports access via NonSeekableWriter and SeekableReader.
+FlashPartition supports access via NonSeekableWriter and SeekableReader. The
+reader defaults to the full size of the partition but can optionally be limited
+to a smaller range.
+
+
+FlashMemory has a variant FakeFlashMemory that uses RAM rather than flash as
+the storage media. This is helpful for unit tests and development without wear
+on the phyisical flash of a device.
+
+FlashPartition has several variants (FlashPartitionWithStats and
+FlashPartitionWithLogicalSectors) that are helpful in some situations.
Size report
===========
diff --git a/pw_kvs/fake_flash_test_key_value_store.cc b/pw_kvs/fake_flash_test_key_value_store.cc
index 840eb722f..7433dca51 100644
--- a/pw_kvs/fake_flash_test_key_value_store.cc
+++ b/pw_kvs/fake_flash_test_key_value_store.cc
@@ -72,7 +72,8 @@ sync::Borrowable<KeyValueStore> borrowable_kvs(test_kvs,
sync::Borrowable<KeyValueStore>& TestKvs() {
if (!test_kvs.initialized()) {
- test_kvs.Init().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ test_kvs.Init()
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
return borrowable_kvs;
diff --git a/pw_kvs/fake_flash_test_logical_sector_partition.cc b/pw_kvs/fake_flash_test_logical_sector_partition.cc
new file mode 100644
index 000000000..6240f3c37
--- /dev/null
+++ b/pw_kvs/fake_flash_test_logical_sector_partition.cc
@@ -0,0 +1,58 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_kvs/fake_flash_memory.h"
+#include "pw_kvs/flash_memory.h"
+#include "pw_kvs/flash_partition_with_logical_sectors.h"
+#include "pw_kvs/flash_test_partition.h"
+
+namespace pw::kvs {
+
+namespace {
+
+#if !defined(PW_FLASH_TEST_SECTORS) || (PW_FLASH_TEST_SECTORS <= 0)
+#error PW_FLASH_TEST_SECTORS must be defined and > 0
+#endif // PW_FLASH_TEST_SECTORS
+
+#if !defined(PW_FLASH_TEST_SECTOR_SIZE) || (PW_FLASH_TEST_SECTOR_SIZE <= 0)
+#error PW_FLASH_TEST_SECTOR_SIZE must be defined and > 0
+#endif // PW_FLASH_TEST_SECTOR_SIZE
+
+#if !defined(PW_FLASH_TEST_ALIGNMENT) || (PW_FLASH_TEST_ALIGNMENT <= 0)
+#error PW_FLASH_TEST_ALIGNMENT must be defined and > 0
+#endif // PW_FLASH_TEST_ALIGNMENT
+
+#if !defined(PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR) || \
+ (PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR <= 0)
+#error PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR must be defined and > 0
+#endif // PW_FLASH_TEST_ALIGNMENT
+
+constexpr size_t kSectors = PW_FLASH_TEST_SECTORS;
+constexpr size_t kSectorSize = PW_FLASH_TEST_SECTOR_SIZE;
+constexpr size_t kAlignment = PW_FLASH_TEST_ALIGNMENT;
+constexpr size_t kSectorsPerLogicalSector =
+ PW_FLASH_TEST_SECTORS_PER_LOGICAL_SECTOR;
+
+// Use PW_FLASH_TEST_SECTORS x PW_FLASH_TEST_SECTOR_SIZE sectors,
+// PW_FLASH_TEST_ALIGNMENT byte alignment.
+FakeFlashMemoryBuffer<kSectorSize, kSectors> test_flash(kAlignment);
+
+FlashPartitionWithLogicalSectors test_partition(&test_flash,
+ kSectorsPerLogicalSector);
+
+} // namespace
+
+FlashPartition& FlashTestPartition() { return test_partition; }
+
+} // namespace pw::kvs
diff --git a/pw_kvs/flash_memory.cc b/pw_kvs/flash_memory.cc
index a564d8f74..1e20a6bbb 100644
--- a/pw_kvs/flash_memory.cc
+++ b/pw_kvs/flash_memory.cc
@@ -52,12 +52,11 @@ Status FlashPartition::Writer::DoWrite(ConstByteSpan data) {
}
StatusWithSize FlashPartition::Reader::DoRead(ByteSpan data) {
- if (position_ >= partition_.size_bytes()) {
+ if (position_ >= read_limit_) {
return StatusWithSize::OutOfRange();
}
- size_t bytes_to_read =
- std::min(data.size_bytes(), partition_.size_bytes() - position_);
+ size_t bytes_to_read = std::min(data.size_bytes(), read_limit_ - position_);
const StatusWithSize sws =
partition_.Read(position_, data.first(bytes_to_read));
diff --git a/pw_kvs/flash_partition_stream_test.cc b/pw_kvs/flash_partition_stream_test.cc
index 09c1376b3..850a9da41 100644
--- a/pw_kvs/flash_partition_stream_test.cc
+++ b/pw_kvs/flash_partition_stream_test.cc
@@ -222,6 +222,49 @@ TEST_F(FlashStreamTest, Read_Multiple_Seeks) {
}
}
+TEST_F(FlashStreamTest, Read_Seeks_With_Limit) {
+ static const size_t kSeekReadSizeBytes = 512;
+ const size_t kPartitionSize = flash_.buffer().size_bytes();
+ ASSERT_GE(flash_.buffer().size_bytes(), (2 * kSeekReadSizeBytes));
+
+ InitBufferToRandom(flash_.buffer(), 0xffde176);
+ FlashPartition::Reader reader(partition_, kSeekReadSizeBytes);
+
+ ASSERT_EQ(reader.ConservativeReadLimit(), kSeekReadSizeBytes);
+
+ reader.SetReadLimit(5u);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 5u);
+ ASSERT_EQ(0u, reader.Tell());
+
+ reader.SetReadLimit(kPartitionSize + 5);
+ ASSERT_EQ(reader.ConservativeReadLimit(), kPartitionSize);
+ ASSERT_EQ(0u, reader.Tell());
+
+ reader.SetReadLimit(kSeekReadSizeBytes);
+ ASSERT_EQ(reader.ConservativeReadLimit(), kSeekReadSizeBytes);
+ ASSERT_EQ(0u, reader.Tell());
+
+ ASSERT_EQ(reader.Seek(1u), OkStatus());
+ ASSERT_EQ(reader.ConservativeReadLimit(), (kSeekReadSizeBytes - 1));
+ ASSERT_EQ(1u, reader.Tell());
+
+ ASSERT_EQ(reader.Seek(kSeekReadSizeBytes), OkStatus());
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+ ASSERT_EQ(kSeekReadSizeBytes, reader.Tell());
+
+ ASSERT_EQ(reader.Seek(kSeekReadSizeBytes + 1), Status::OutOfRange());
+ ASSERT_EQ(reader.Seek(2 * kSeekReadSizeBytes), Status::OutOfRange());
+
+ reader.SetReadLimit(kPartitionSize + 5);
+ ASSERT_EQ(reader.ConservativeReadLimit(),
+ (kPartitionSize - kSeekReadSizeBytes));
+ ASSERT_EQ(kSeekReadSizeBytes, reader.Tell());
+
+ reader.SetReadLimit(5);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+ ASSERT_EQ(5u, reader.Tell());
+}
+
TEST_F(FlashStreamTest, Read_Seek_Forward_and_Back) {
static const size_t kSeekReadSizeBytes = 256;
static const size_t kTotalIterations = 3;
@@ -293,6 +336,76 @@ TEST_F(FlashStreamTest, Read_Past_End) {
VerifyFlashContent(result.value(), read_chunk.size_bytes());
}
+TEST_F(FlashStreamTest, Read_Past_End_of_Limit) {
+ static const size_t kBytesForReadLimit = 128;
+ static const size_t kBytesForFinalRead = 50;
+
+ InitBufferToRandom(flash_.buffer(), 0xcccde176);
+ FlashPartition::Reader reader(partition_, kBytesForReadLimit);
+
+ ASSERT_GE(source_buffer_.size(), kBytesForReadLimit);
+ ASSERT_GT(kBytesForReadLimit, 2 * kBytesForFinalRead);
+
+ ByteSpan read_chunk =
+ span(source_buffer_).first(kBytesForReadLimit - kBytesForFinalRead);
+
+ auto result = reader.Read(read_chunk);
+ ASSERT_EQ(result.status(), OkStatus());
+ ASSERT_EQ(result.value().size_bytes(), read_chunk.size_bytes());
+ ASSERT_EQ(reader.Tell(), read_chunk.size_bytes());
+ ASSERT_EQ(reader.ConservativeReadLimit(), kBytesForFinalRead);
+ ASSERT_EQ(result.value().data(), read_chunk.data());
+ VerifyFlashContent(read_chunk);
+
+ result = reader.Read(read_chunk);
+ ASSERT_EQ(result.status(), OkStatus());
+ ASSERT_EQ(result.value().size_bytes(), kBytesForFinalRead);
+ ASSERT_EQ(reader.Tell(), kBytesForReadLimit);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0U);
+ ASSERT_EQ(result.value().data(), read_chunk.data());
+ VerifyFlashContent(result.value(), read_chunk.size_bytes());
+
+ ASSERT_EQ(reader.Read(read_chunk).status(), Status::OutOfRange());
+}
+
+TEST_F(FlashStreamTest, Read_With_Zero_Byte_Limit) {
+ static const size_t kBytesForReadLimit = 128;
+ static const size_t kBytesForFinalRead = 50;
+
+ InitBufferToRandom(flash_.buffer(), 0xcccde176);
+ FlashPartition::Reader reader(partition_, 0u);
+
+ ASSERT_GE(source_buffer_.size(), kBytesForReadLimit);
+ ASSERT_GT(kBytesForReadLimit, 2 * kBytesForFinalRead);
+
+ ByteSpan read_chunk = span(source_buffer_);
+
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+ ASSERT_EQ(reader.Tell(), 0u);
+
+ auto result = reader.Read(read_chunk);
+ ASSERT_EQ(result.status(), Status::OutOfRange());
+ ASSERT_EQ(reader.Tell(), 0u);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+
+ ASSERT_EQ(reader.Seek(0), OkStatus());
+ ASSERT_EQ(reader.Tell(), 0u);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+
+ ASSERT_EQ(reader.Seek(1), Status::OutOfRange());
+ ASSERT_EQ(reader.Tell(), 0u);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+
+ ASSERT_EQ(reader.Seek(5), Status::OutOfRange());
+ ASSERT_EQ(reader.Tell(), 0u);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+
+ result = reader.Read(read_chunk);
+ ASSERT_EQ(result.status(), Status::OutOfRange());
+ ASSERT_EQ(reader.Tell(), 0u);
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+}
+
TEST_F(FlashStreamTest, Read_Past_End_After_Seek) {
InitBufferToRandom(flash_.buffer(), 0xddcde176);
FlashPartition::Reader reader(partition_);
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
index 6dc570c00..130c9be54 100644
--- a/pw_kvs/key_value_store.cc
+++ b/pw_kvs/key_value_store.cc
@@ -24,7 +24,7 @@
#include "pw_assert/check.h"
#include "pw_kvs_private/config.h"
-#include "pw_log/shorter.h"
+#include "pw_log/log.h"
#include "pw_status/try.h"
namespace pw::kvs {
@@ -61,9 +61,10 @@ Status KeyValueStore::Init() {
error_detected_ = false;
last_transaction_id_ = 0;
- INF("Initializing key value store");
+ PW_LOG_INFO("Initializing key value store");
if (partition_.sector_count() > sectors_.max_size()) {
- ERR("KVS init failed: kMaxUsableSectors (=%u) must be at least as "
+ PW_LOG_ERROR(
+ "KVS init failed: kMaxUsableSectors (=%u) must be at least as "
"large as the number of sectors in the flash partition (=%u)",
unsigned(sectors_.max_size()),
unsigned(partition_.sector_count()));
@@ -71,7 +72,8 @@ Status KeyValueStore::Init() {
}
if (partition_.sector_count() < 2) {
- ERR("KVS init failed: FlashParition sector count (=%u) must be at 2. KVS "
+ PW_LOG_ERROR(
+ "KVS init failed: FlashParition sector count (=%u) must be at 2. KVS "
"requires at least 1 working sector + 1 free/reserved sector",
unsigned(partition_.sector_count()));
return Status::FailedPrecondition();
@@ -82,7 +84,8 @@ Status KeyValueStore::Init() {
// TODO(davidrogers): investigate doing this as a static assert/compile-time
// check.
if (sector_size_bytes > SectorDescriptor::max_sector_size()) {
- ERR("KVS init failed: sector_size_bytes (=%u) is greater than maximum "
+ PW_LOG_ERROR(
+ "KVS init failed: sector_size_bytes (=%u) is greater than maximum "
"allowed sector size (=%u)",
unsigned(sector_size_bytes),
unsigned(SectorDescriptor::max_sector_size()));
@@ -105,22 +108,24 @@ Status KeyValueStore::Init() {
if (metadata_result.IsOutOfRange()) {
internal_stats_.missing_redundant_entries_recovered =
pre_fix_redundancy_errors;
- INF("KVS init: Redundancy level successfully updated");
+ PW_LOG_INFO("KVS init: Redundancy level successfully updated");
} else {
- WRN("KVS init: Corruption detected and fully repaired");
+ PW_LOG_WARN("KVS init: Corruption detected and fully repaired");
}
initialized_ = InitializationState::kReady;
} else if (recovery_status.IsResourceExhausted()) {
- WRN("KVS init: Unable to maintain required free sector");
+ PW_LOG_WARN("KVS init: Unable to maintain required free sector");
} else {
- WRN("KVS init: Corruption detected and unable repair");
+ PW_LOG_WARN("KVS init: Corruption detected and unable repair");
}
} else {
- WRN("KVS init: Corruption detected, no repair attempted due to options");
+ PW_LOG_WARN(
+ "KVS init: Corruption detected, no repair attempted due to options");
}
}
- INF("KeyValueStore init complete: active keys %u, deleted keys %u, sectors "
+ PW_LOG_INFO(
+ "KeyValueStore init complete: active keys %u, deleted keys %u, sectors "
"%u, logical sector size %u bytes",
unsigned(size()),
unsigned(entry_cache_.total_entries() - size()),
@@ -129,7 +134,8 @@ Status KeyValueStore::Init() {
// Report any corruption was not repaired.
if (error_detected_) {
- WRN("KVS init: Corruption found but not repaired, KVS unavailable until "
+ PW_LOG_WARN(
+ "KVS init: Corruption found but not repaired, KVS unavailable until "
"successful maintenance.");
return Status::DataLoss();
}
@@ -143,7 +149,7 @@ Status KeyValueStore::InitializeMetadata() {
sectors_.Reset();
entry_cache_.Reset();
- DBG("First pass: Read all entries from all sectors");
+ PW_LOG_DEBUG("First pass: Read all entries from all sectors");
Address sector_address = 0;
size_t total_corrupt_bytes = 0;
@@ -157,20 +163,21 @@ Status KeyValueStore::InitializeMetadata() {
size_t sector_corrupt_bytes = 0;
for (int num_entries_in_sector = 0; true; num_entries_in_sector++) {
- DBG("Load entry: sector=%u, entry#=%d, address=%u",
- unsigned(sector_address),
- num_entries_in_sector,
- unsigned(entry_address));
+ PW_LOG_DEBUG("Load entry: sector=%u, entry#=%d, address=%u",
+ unsigned(sector_address),
+ num_entries_in_sector,
+ unsigned(entry_address));
if (!sectors_.AddressInSector(sector, entry_address)) {
- DBG("Fell off end of sector; moving to the next sector");
+ PW_LOG_DEBUG("Fell off end of sector; moving to the next sector");
break;
}
Address next_entry_address;
Status status = LoadEntry(entry_address, &next_entry_address);
if (status.IsNotFound()) {
- DBG("Hit un-written data in sector; moving to the next sector");
+ PW_LOG_DEBUG(
+ "Hit un-written data in sector; moving to the next sector");
break;
} else if (!status.ok()) {
// The entry could not be read, indicating likely data corruption within
@@ -211,9 +218,9 @@ Status KeyValueStore::InitializeMetadata() {
sector.mark_corrupt();
error_detected_ = true;
- WRN("Sector %u contains %uB of corrupt data",
- sectors_.Index(sector),
- unsigned(sector_corrupt_bytes));
+ PW_LOG_WARN("Sector %u contains %uB of corrupt data",
+ sectors_.Index(sector),
+ unsigned(sector_corrupt_bytes));
}
if (sector.Empty(sector_size_bytes)) {
@@ -223,7 +230,7 @@ Status KeyValueStore::InitializeMetadata() {
total_corrupt_bytes += sector_corrupt_bytes;
}
- DBG("Second pass: Count valid bytes in each sector");
+ PW_LOG_DEBUG("Second pass: Count valid bytes in each sector");
Address newest_key = 0;
// For every valid entry, for each address, count the valid bytes in that
@@ -232,10 +239,10 @@ Status KeyValueStore::InitializeMetadata() {
// initializing last_new_sector_.
for (EntryMetadata& metadata : entry_cache_) {
if (metadata.addresses().size() < redundancy()) {
- DBG("Key 0x%08x missing copies, has %u, needs %u",
- unsigned(metadata.hash()),
- unsigned(metadata.addresses().size()),
- unsigned(redundancy()));
+ PW_LOG_DEBUG("Key 0x%08x missing copies, has %u, needs %u",
+ unsigned(metadata.hash()),
+ unsigned(metadata.addresses().size()),
+ unsigned(redundancy()));
entry_copies_missing++;
}
size_t index = 0;
@@ -272,7 +279,7 @@ Status KeyValueStore::InitializeMetadata() {
sectors_.set_last_new_sector(newest_key);
if (!empty_sector_found) {
- DBG("No empty sector found");
+ PW_LOG_DEBUG("No empty sector found");
error_detected_ = true;
}
@@ -281,14 +288,16 @@ Status KeyValueStore::InitializeMetadata() {
error_detected_ = true;
if (!other_errors && entry_copies_missing == entry_cache_.total_entries()) {
- INF("KVS configuration changed to redundancy of %u total copies per key",
+ PW_LOG_INFO(
+ "KVS configuration changed to redundancy of %u total copies per key",
unsigned(redundancy()));
return Status::OutOfRange();
}
}
if (error_detected_) {
- WRN("Corruption detected. Found %u corrupt bytes, %u corrupt entries, "
+ PW_LOG_WARN(
+ "Corruption detected. Found %u corrupt bytes, %u corrupt entries, "
"and %u keys missing redundant copies.",
unsigned(total_corrupt_bytes),
unsigned(corrupt_entries),
@@ -374,9 +383,9 @@ Status KeyValueStore::LoadEntry(Address entry_address,
Status KeyValueStore::ScanForEntry(const SectorDescriptor& sector,
Address start_address,
Address* next_entry_address) {
- DBG("Scanning sector %u for entries starting from address %u",
- sectors_.Index(sector),
- unsigned(start_address));
+ PW_LOG_DEBUG("Scanning sector %u for entries starting from address %u",
+ sectors_.Index(sector),
+ unsigned(start_address));
// Entries must start at addresses which are aligned on a multiple of
// Entry::kMinAlignmentBytes. However, that multiple can vary between entries.
@@ -392,7 +401,7 @@ Status KeyValueStore::ScanForEntry(const SectorDescriptor& sector,
continue;
}
if (formats_.KnownMagic(magic)) {
- DBG("Found entry magic at address %u", unsigned(address));
+ PW_LOG_DEBUG("Found entry magic at address %u", unsigned(address));
*next_entry_address = address;
return OkStatus();
}
@@ -448,14 +457,14 @@ StatusWithSize KeyValueStore::Get(Key key,
Status KeyValueStore::PutBytes(Key key, span<const byte> value) {
PW_TRY(CheckWriteOperation(key));
- DBG("Writing key/value; key length=%u, value length=%u",
- unsigned(key.size()),
- unsigned(value.size()));
+ PW_LOG_DEBUG("Writing key/value; key length=%u, value length=%u",
+ unsigned(key.size()),
+ unsigned(value.size()));
if (Entry::size(partition_, key, value) > partition_.sector_size_bytes()) {
- DBG("%u B value with %u B key cannot fit in one sector",
- unsigned(value.size()),
- unsigned(key.size()));
+ PW_LOG_DEBUG("%u B value with %u B key cannot fit in one sector",
+ unsigned(value.size()),
+ unsigned(key.size()));
return Status::InvalidArgument();
}
@@ -464,10 +473,10 @@ Status KeyValueStore::PutBytes(Key key, span<const byte> value) {
if (status.ok()) {
// TODO(davidrogers): figure out logging how to support multiple addresses.
- DBG("Overwriting entry for key 0x%08x in %u sectors including %u",
- unsigned(metadata.hash()),
- unsigned(metadata.addresses().size()),
- sectors_.Index(metadata.first_address()));
+ PW_LOG_DEBUG("Overwriting entry for key 0x%08x in %u sectors including %u",
+ unsigned(metadata.hash()),
+ unsigned(metadata.addresses().size()),
+ sectors_.Index(metadata.first_address()));
return WriteEntryForExistingKey(metadata, EntryState::kValid, key, value);
}
@@ -485,10 +494,10 @@ Status KeyValueStore::Delete(Key key) {
PW_TRY(FindExisting(key, &metadata));
// TODO(davidrogers): figure out logging how to support multiple addresses.
- DBG("Writing tombstone for key 0x%08x in %u sectors including %u",
- unsigned(metadata.hash()),
- unsigned(metadata.addresses().size()),
- sectors_.Index(metadata.first_address()));
+ PW_LOG_DEBUG("Writing tombstone for key 0x%08x in %u sectors including %u",
+ unsigned(metadata.hash()),
+ unsigned(metadata.addresses().size()),
+ sectors_.Index(metadata.first_address()));
return WriteEntryForExistingKey(metadata, EntryState::kDeleted, key, {});
}
@@ -498,7 +507,7 @@ void KeyValueStore::Item::ReadKey() {
Entry entry;
if (kvs_.ReadEntry(*iterator_, entry).ok()) {
entry.ReadKey(key_buffer_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
@@ -544,7 +553,7 @@ Status KeyValueStore::ReadEntry(const EntryMetadata& metadata,
sectors_.FromAddress(address).mark_corrupt();
}
- ERR("No valid entries for key. Data has been lost!");
+ PW_LOG_ERROR("No valid entries for key. Data has been lost!");
return read_result;
}
@@ -612,9 +621,9 @@ Status KeyValueStore::FixedSizeGet(Key key,
PW_TRY_ASSIGN(const size_t actual_size, ValueSize(metadata));
if (actual_size != size_bytes) {
- DBG("Requested %u B read, but value is %u B",
- unsigned(size_bytes),
- unsigned(actual_size));
+ PW_LOG_DEBUG("Requested %u B read, but value is %u B",
+ unsigned(size_bytes),
+ unsigned(actual_size));
return Status::InvalidArgument();
}
@@ -677,14 +686,16 @@ Status KeyValueStore::WriteEntryForNewKey(Key key, span<const byte> value) {
entry_cache_.full()) {
Status maintenance_status = HeavyMaintenance();
if (!maintenance_status.ok()) {
- WRN("KVS Maintenance failed for write: %s", maintenance_status.str());
+ PW_LOG_WARN("KVS Maintenance failed for write: %s",
+ maintenance_status.str());
return maintenance_status;
}
}
#endif // PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
if (entry_cache_.full()) {
- WRN("KVS full: trying to store a new entry, but can't. Have %u entries",
+ PW_LOG_WARN(
+ "KVS full: trying to store a new entry, but can't. Have %u entries",
unsigned(entry_cache_.total_entries()));
return Status::ResourceExhausted();
}
@@ -706,8 +717,8 @@ Status KeyValueStore::WriteEntry(Key key,
prior_entry->ValueMatches(value).ok()) {
// The new value matches the prior value, don't need to write anything. Just
// keep the existing entry.
- DBG("Write for key 0x%08x with matching value skipped",
- unsigned(prior_metadata->hash()));
+ PW_LOG_DEBUG("Write for key 0x%08x with matching value skipped",
+ unsigned(prior_metadata->hash()));
return OkStatus();
}
@@ -773,9 +784,9 @@ Status KeyValueStore::GetAddressesForWrite(Address* write_addresses,
PW_TRY(GetSectorForWrite(&sector, write_size, span(write_addresses, i)));
write_addresses[i] = sectors_.NextWritableAddress(*sector);
- DBG("Found space for entry in sector %u at address %u",
- sectors_.Index(sector),
- unsigned(write_addresses[i]));
+ PW_LOG_DEBUG("Found space for entry in sector %u at address %u",
+ sectors_.Index(sector),
+ unsigned(write_addresses[i]));
}
return OkStatus();
@@ -820,13 +831,13 @@ Status KeyValueStore::GetSectorForWrite(
// moving entries for keys other than the one being worked on in to sectors
// that have copies of the key trying to be written.
if (gc_sector_count > (partition_.sector_count() + 2)) {
- ERR("Did more GC sectors than total sectors!!!!");
+ PW_LOG_ERROR("Did more GC sectors than total sectors!!!!");
return Status::ResourceExhausted();
}
}
if (!result.ok()) {
- WRN("Unable to find sector to write %u B", unsigned(entry_size));
+ PW_LOG_WARN("Unable to find sector to write %u B", unsigned(entry_size));
}
return result;
}
@@ -834,7 +845,7 @@ Status KeyValueStore::GetSectorForWrite(
Status KeyValueStore::MarkSectorCorruptIfNotOk(Status status,
SectorDescriptor* sector) {
if (!status.ok()) {
- DBG(" Sector %u corrupt", sectors_.Index(sector));
+ PW_LOG_DEBUG(" Sector %u corrupt", sectors_.Index(sector));
sector->mark_corrupt();
error_detected_ = true;
}
@@ -849,10 +860,10 @@ Status KeyValueStore::AppendEntry(const Entry& entry,
SectorDescriptor& sector = sectors_.FromAddress(entry.address());
if (!result.ok()) {
- ERR("Failed to write %u bytes at %#x. %u actually written",
- unsigned(entry.size()),
- unsigned(entry.address()),
- unsigned(result.size()));
+ PW_LOG_ERROR("Failed to write %u bytes at %#x. %u actually written",
+ unsigned(entry.size()),
+ unsigned(entry.address()),
+ unsigned(result.size()));
PW_TRY(MarkSectorCorruptIfNotOk(result.status(), &sector));
}
@@ -923,7 +934,7 @@ Status KeyValueStore::FullMaintenanceHelper(MaintenanceType maintenance_type) {
// Full maintenance can be a potentially heavy operation, and should be
// relatively infrequent, so log start/end at INFO level.
- INF("Beginning full maintenance");
+ PW_LOG_INFO("Beginning full maintenance");
CheckForErrors();
// Step 1: Repair errors
@@ -936,7 +947,7 @@ Status KeyValueStore::FullMaintenanceHelper(MaintenanceType maintenance_type) {
Status overall_status = update_status.status();
if (!overall_status.ok()) {
- ERR("Failed to update all entries to the primary format");
+ PW_LOG_ERROR("Failed to update all entries to the primary format");
}
SectorDescriptor* sector = sectors_.last_new();
@@ -965,7 +976,7 @@ Status KeyValueStore::FullMaintenanceHelper(MaintenanceType maintenance_type) {
(force_gc || sector->valid_bytes() == 0)) {
gc_status = GarbageCollectSector(*sector, {});
if (!gc_status.ok()) {
- ERR("Failed to garbage collect all sectors");
+ PW_LOG_ERROR("Failed to garbage collect all sectors");
break;
}
}
@@ -999,9 +1010,9 @@ Status KeyValueStore::FullMaintenanceHelper(MaintenanceType maintenance_type) {
#endif // PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
if (overall_status.ok()) {
- INF("Full maintenance complete");
+ PW_LOG_INFO("Full maintenance complete");
} else {
- ERR("Full maintenance finished with some errors");
+ PW_LOG_ERROR("Full maintenance finished with some errors");
}
return overall_status;
}
@@ -1020,9 +1031,9 @@ Status KeyValueStore::PartialMaintenance() {
}
Status KeyValueStore::GarbageCollect(span<const Address> reserved_addresses) {
- DBG("Garbage Collect a single sector");
+ PW_LOG_DEBUG("Garbage Collect a single sector");
for ([[maybe_unused]] Address address : reserved_addresses) {
- DBG(" Avoid address %u", unsigned(address));
+ PW_LOG_DEBUG(" Avoid address %u", unsigned(address));
}
// Step 1: Find the sector to garbage collect
@@ -1044,9 +1055,9 @@ Status KeyValueStore::RelocateKeyAddressesInSector(
span<const Address> reserved_addresses) {
for (FlashPartition::Address& address : metadata.addresses()) {
if (sectors_.AddressInSector(sector_to_gc, address)) {
- DBG(" Relocate entry for Key 0x%08" PRIx32 ", sector %u",
- metadata.hash(),
- sectors_.Index(sectors_.FromAddress(address)));
+ PW_LOG_DEBUG(" Relocate entry for Key 0x%08" PRIx32 ", sector %u",
+ metadata.hash(),
+ sectors_.Index(sectors_.FromAddress(address)));
PW_TRY(RelocateEntry(metadata, address, reserved_addresses));
}
}
@@ -1056,7 +1067,7 @@ Status KeyValueStore::RelocateKeyAddressesInSector(
Status KeyValueStore::GarbageCollectSector(
SectorDescriptor& sector_to_gc, span<const Address> reserved_addresses) {
- DBG(" Garbage Collect sector %u", sectors_.Index(sector_to_gc));
+ PW_LOG_DEBUG(" Garbage Collect sector %u", sectors_.Index(sector_to_gc));
// Step 1: Move any valid entries in the GC sector to other sectors
if (sector_to_gc.valid_bytes() != 0) {
@@ -1067,7 +1078,8 @@ Status KeyValueStore::GarbageCollectSector(
}
if (sector_to_gc.valid_bytes() != 0) {
- ERR(" Failed to relocate valid entries from sector being garbage "
+ PW_LOG_ERROR(
+ " Failed to relocate valid entries from sector being garbage "
"collected, %u valid bytes remain",
unsigned(sector_to_gc.valid_bytes()));
return Status::Internal();
@@ -1081,7 +1093,8 @@ Status KeyValueStore::GarbageCollectSector(
sector_to_gc.set_writable_bytes(partition_.sector_size_bytes());
}
- DBG(" Garbage Collect sector %u complete", sectors_.Index(sector_to_gc));
+ PW_LOG_DEBUG(" Garbage Collect sector %u complete",
+ sectors_.Index(sector_to_gc));
return OkStatus();
}
@@ -1095,7 +1108,8 @@ StatusWithSize KeyValueStore::UpdateEntriesToPrimaryFormat() {
continue;
}
- DBG("Updating entry 0x%08x from old format [0x%08x] to new format "
+ PW_LOG_DEBUG(
+ "Updating entry 0x%08x from old format [0x%08x] to new format "
"[0x%08x]",
unsigned(prior_metadata.hash()),
unsigned(entry.magic()),
@@ -1171,10 +1185,11 @@ Status KeyValueStore::RepairCorruptSectors() {
repair_status = OkStatus();
}
- DBG(" Pass %u", unsigned(loop_count));
+ PW_LOG_DEBUG(" Pass %u", unsigned(loop_count));
for (SectorDescriptor& sector : sectors_) {
if (sector.corrupt()) {
- DBG(" Found sector %u with corruption", sectors_.Index(sector));
+ PW_LOG_DEBUG(" Found sector %u with corruption",
+ sectors_.Index(sector));
Status sector_status = GarbageCollectSector(sector, {});
if (sector_status.ok()) {
internal_stats_.corrupt_sectors_recovered += 1;
@@ -1183,7 +1198,7 @@ Status KeyValueStore::RepairCorruptSectors() {
}
}
}
- DBG(" Pass %u complete", unsigned(loop_count));
+ PW_LOG_DEBUG(" Pass %u complete", unsigned(loop_count));
} while (!repair_status.ok() && loop_count < 2);
return repair_status;
@@ -1193,19 +1208,19 @@ Status KeyValueStore::EnsureFreeSectorExists() {
Status repair_status = OkStatus();
bool empty_sector_found = false;
- DBG(" Find empty sector");
+ PW_LOG_DEBUG(" Find empty sector");
for (SectorDescriptor& sector : sectors_) {
if (sector.Empty(partition_.sector_size_bytes())) {
empty_sector_found = true;
- DBG(" Empty sector found");
+ PW_LOG_DEBUG(" Empty sector found");
break;
}
}
if (empty_sector_found == false) {
- DBG(" No empty sector found, attempting to GC a free sector");
+ PW_LOG_DEBUG(" No empty sector found, attempting to GC a free sector");
Status sector_status = GarbageCollect(span<const Address, 0>());
if (repair_status.ok() && !sector_status.ok()) {
- DBG(" Unable to free an empty sector");
+ PW_LOG_DEBUG(" Unable to free an empty sector");
repair_status = sector_status;
}
}
@@ -1217,11 +1232,12 @@ Status KeyValueStore::EnsureEntryRedundancy() {
Status repair_status = OkStatus();
if (redundancy() == 1) {
- DBG(" Redundancy not in use, nothting to check");
+ PW_LOG_DEBUG(" Redundancy not in use, nothting to check");
return OkStatus();
}
- DBG(" Write any needed additional duplicate copies of keys to fulfill %u"
+ PW_LOG_DEBUG(
+ " Write any needed additional duplicate copies of keys to fulfill %u"
" redundancy",
unsigned(redundancy()));
for (EntryMetadata& metadata : entry_cache_) {
@@ -1229,15 +1245,15 @@ Status KeyValueStore::EnsureEntryRedundancy() {
continue;
}
- DBG(" Key with %u of %u copies found, adding missing copies",
- unsigned(metadata.addresses().size()),
- unsigned(redundancy()));
+ PW_LOG_DEBUG(" Key with %u of %u copies found, adding missing copies",
+ unsigned(metadata.addresses().size()),
+ unsigned(redundancy()));
Status fill_status = AddRedundantEntries(metadata);
if (fill_status.ok()) {
internal_stats_.missing_redundant_entries_recovered += 1;
- DBG(" Key missing copies added");
+ PW_LOG_DEBUG(" Key missing copies added");
} else {
- DBG(" Failed to add key missing copies");
+ PW_LOG_DEBUG(" Failed to add key missing copies");
if (repair_status.ok()) {
repair_status = fill_status;
}
@@ -1248,7 +1264,7 @@ Status KeyValueStore::EnsureEntryRedundancy() {
}
Status KeyValueStore::FixErrors() {
- DBG("Fixing KVS errors");
+ PW_LOG_DEBUG("Fixing KVS errors");
// Step 1: Garbage collect any sectors marked as corrupt.
Status overall_status = RepairCorruptSectors();
@@ -1279,11 +1295,11 @@ Status KeyValueStore::FixErrors() {
Status KeyValueStore::Repair() {
// If errors have been detected, just reinit the KVS metadata. This does a
// full deep error check and any needed repairs. Then repair any errors.
- INF("Starting KVS repair");
+ PW_LOG_INFO("Starting KVS repair");
- DBG("Reinitialize KVS metadata");
+ PW_LOG_DEBUG("Reinitialize KVS metadata");
InitializeMetadata()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return FixErrors();
}
@@ -1320,83 +1336,88 @@ KeyValueStore::Entry KeyValueStore::CreateEntry(Address address,
void KeyValueStore::LogDebugInfo() const {
const size_t sector_size_bytes = partition_.sector_size_bytes();
- DBG("====================== KEY VALUE STORE DUMP =========================");
- DBG(" ");
- DBG("Flash partition:");
- DBG(" Sector count = %u", unsigned(partition_.sector_count()));
- DBG(" Sector max count = %u", unsigned(sectors_.max_size()));
- DBG(" Sectors in use = %u", unsigned(sectors_.size()));
- DBG(" Sector size = %u", unsigned(sector_size_bytes));
- DBG(" Total size = %u", unsigned(partition_.size_bytes()));
- DBG(" Alignment = %u", unsigned(partition_.alignment_bytes()));
- DBG(" ");
- DBG("Key descriptors:");
- DBG(" Entry count = %u", unsigned(entry_cache_.total_entries()));
- DBG(" Max entry count = %u", unsigned(entry_cache_.max_entries()));
- DBG(" ");
- DBG(" # hash version address address (hex)");
+ PW_LOG_DEBUG(
+ "====================== KEY VALUE STORE DUMP =========================");
+ PW_LOG_DEBUG(" ");
+ PW_LOG_DEBUG("Flash partition:");
+ PW_LOG_DEBUG(" Sector count = %u", unsigned(partition_.sector_count()));
+ PW_LOG_DEBUG(" Sector max count = %u", unsigned(sectors_.max_size()));
+ PW_LOG_DEBUG(" Sectors in use = %u", unsigned(sectors_.size()));
+ PW_LOG_DEBUG(" Sector size = %u", unsigned(sector_size_bytes));
+ PW_LOG_DEBUG(" Total size = %u", unsigned(partition_.size_bytes()));
+ PW_LOG_DEBUG(" Alignment = %u",
+ unsigned(partition_.alignment_bytes()));
+ PW_LOG_DEBUG(" ");
+ PW_LOG_DEBUG("Key descriptors:");
+ PW_LOG_DEBUG(" Entry count = %u",
+ unsigned(entry_cache_.total_entries()));
+ PW_LOG_DEBUG(" Max entry count = %u", unsigned(entry_cache_.max_entries()));
+ PW_LOG_DEBUG(" ");
+ PW_LOG_DEBUG(" # hash version address address (hex)");
size_t count = 0;
for (const EntryMetadata& metadata : entry_cache_) {
- DBG(" |%3zu: | %8zx |%8zu | %8zu | %8zx",
- count++,
- size_t(metadata.hash()),
- size_t(metadata.transaction_id()),
- size_t(metadata.first_address()),
- size_t(metadata.first_address()));
+ PW_LOG_DEBUG(" |%3zu: | %8zx |%8zu | %8zu | %8zx",
+ count++,
+ size_t(metadata.hash()),
+ size_t(metadata.transaction_id()),
+ size_t(metadata.first_address()),
+ size_t(metadata.first_address()));
}
- DBG(" ");
+ PW_LOG_DEBUG(" ");
- DBG("Sector descriptors:");
- DBG(" # tail free valid has_space");
+ PW_LOG_DEBUG("Sector descriptors:");
+ PW_LOG_DEBUG(" # tail free valid has_space");
for (const SectorDescriptor& sd : sectors_) {
- DBG(" |%3u: | %8zu |%8zu | %s",
- sectors_.Index(sd),
- size_t(sd.writable_bytes()),
- sd.valid_bytes(),
- sd.writable_bytes() ? "YES" : "");
+ PW_LOG_DEBUG(" |%3u: | %8zu |%8zu | %s",
+ sectors_.Index(sd),
+ size_t(sd.writable_bytes()),
+ sd.valid_bytes(),
+ sd.writable_bytes() ? "YES" : "");
}
- DBG(" ");
+ PW_LOG_DEBUG(" ");
// TODO(keir): This should stop logging after some threshold.
// size_t dumped_bytes = 0;
- DBG("Sector raw data:");
+ PW_LOG_DEBUG("Sector raw data:");
for (size_t sector_id = 0; sector_id < sectors_.size(); ++sector_id) {
// Read sector data. Yes, this will blow the stack on embedded.
std::array<byte, 500> raw_sector_data; // TODO!!!
[[maybe_unused]] StatusWithSize sws =
partition_.Read(sector_id * sector_size_bytes, raw_sector_data);
- DBG("Read: %u bytes", unsigned(sws.size()));
+ PW_LOG_DEBUG("Read: %u bytes", unsigned(sws.size()));
- DBG(" base addr offs 0 1 2 3 4 5 6 7");
+ PW_LOG_DEBUG(" base addr offs 0 1 2 3 4 5 6 7");
for (size_t i = 0; i < sector_size_bytes; i += 8) {
- DBG(" %3zu %8zx %5zu | %02x %02x %02x %02x %02x %02x %02x %02x",
- sector_id,
- (sector_id * sector_size_bytes) + i,
- i,
- static_cast<unsigned int>(raw_sector_data[i + 0]),
- static_cast<unsigned int>(raw_sector_data[i + 1]),
- static_cast<unsigned int>(raw_sector_data[i + 2]),
- static_cast<unsigned int>(raw_sector_data[i + 3]),
- static_cast<unsigned int>(raw_sector_data[i + 4]),
- static_cast<unsigned int>(raw_sector_data[i + 5]),
- static_cast<unsigned int>(raw_sector_data[i + 6]),
- static_cast<unsigned int>(raw_sector_data[i + 7]));
+ PW_LOG_DEBUG(" %3zu %8zx %5zu | %02x %02x %02x %02x %02x %02x %02x %02x",
+ sector_id,
+ (sector_id * sector_size_bytes) + i,
+ i,
+ static_cast<unsigned int>(raw_sector_data[i + 0]),
+ static_cast<unsigned int>(raw_sector_data[i + 1]),
+ static_cast<unsigned int>(raw_sector_data[i + 2]),
+ static_cast<unsigned int>(raw_sector_data[i + 3]),
+ static_cast<unsigned int>(raw_sector_data[i + 4]),
+ static_cast<unsigned int>(raw_sector_data[i + 5]),
+ static_cast<unsigned int>(raw_sector_data[i + 6]),
+ static_cast<unsigned int>(raw_sector_data[i + 7]));
// TODO(keir): Fix exit condition.
if (i > 128) {
break;
}
}
- DBG(" ");
+ PW_LOG_DEBUG(" ");
}
- DBG("////////////////////// KEY VALUE STORE DUMP END /////////////////////");
+ PW_LOG_DEBUG(
+ "////////////////////// KEY VALUE STORE DUMP END /////////////////////");
}
void KeyValueStore::LogSectors() const {
- DBG("Sector descriptors: count %u", unsigned(sectors_.size()));
+ PW_LOG_DEBUG("Sector descriptors: count %u", unsigned(sectors_.size()));
for (auto& sector : sectors_) {
- DBG(" - Sector %u: valid %u, recoverable %u, free %u",
+ PW_LOG_DEBUG(
+ " - Sector %u: valid %u, recoverable %u, free %u",
sectors_.Index(sector),
unsigned(sector.valid_bytes()),
unsigned(sector.RecoverableBytes(partition_.sector_size_bytes())),
@@ -1405,13 +1426,14 @@ void KeyValueStore::LogSectors() const {
}
void KeyValueStore::LogKeyDescriptor() const {
- DBG("Key descriptors: count %u", unsigned(entry_cache_.total_entries()));
+ PW_LOG_DEBUG("Key descriptors: count %u",
+ unsigned(entry_cache_.total_entries()));
for (const EntryMetadata& metadata : entry_cache_) {
- DBG(" - Key: %s, hash %#x, transaction ID %u, first address %#x",
- metadata.state() == EntryState::kDeleted ? "Deleted" : "Valid",
- unsigned(metadata.hash()),
- unsigned(metadata.transaction_id()),
- unsigned(metadata.first_address()));
+ PW_LOG_DEBUG(" - Key: %s, hash %#x, transaction ID %u, first address %#x",
+ metadata.state() == EntryState::kDeleted ? "Deleted" : "Valid",
+ unsigned(metadata.hash()),
+ unsigned(metadata.transaction_id()),
+ unsigned(metadata.first_address()));
}
}
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index 11e410249..d840bd5a8 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -37,7 +37,6 @@
#include "pw_kvs/internal/entry.h"
#include "pw_kvs_private/config.h"
#include "pw_log/log.h"
-#include "pw_log/shorter.h"
#include "pw_status/status.h"
#include "pw_string/string_builder.h"
@@ -168,11 +167,11 @@ TEST(InMemoryKvs, WriteOneKeyMultipleTimes) {
int num_reloads = 2;
for (int reload = 0; reload < num_reloads; ++reload) {
- DBG("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
- DBG("xxx xxxx");
- DBG("xxx Reload %2d xxxx", reload);
- DBG("xxx xxxx");
- DBG("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+ PW_LOG_DEBUG("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
+ PW_LOG_DEBUG("xxx xxxx");
+ PW_LOG_DEBUG("xxx Reload %2d xxxx", reload);
+ PW_LOG_DEBUG("xxx xxxx");
+ PW_LOG_DEBUG("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
// Create and initialize the KVS. For KVS magic value always use a random 32
// bit integer rather than a human readable 4 bytes. See pw_kvs/format.h for
@@ -188,7 +187,8 @@ TEST(InMemoryKvs, WriteOneKeyMultipleTimes) {
uint32_t written_value;
EXPECT_EQ(kvs.size(), (reload == 0) ? 0 : 1u);
for (uint32_t i = 0; i < num_writes; ++i) {
- DBG("PUT #%zu for key %s with value %zu", size_t(i), key, size_t(i));
+ PW_LOG_DEBUG(
+ "PUT #%zu for key %s with value %zu", size_t(i), key, size_t(i));
written_value = i + 0xfc; // Prevent accidental pass with zero.
EXPECT_OK(kvs.Put(key, written_value));
@@ -196,7 +196,7 @@ TEST(InMemoryKvs, WriteOneKeyMultipleTimes) {
}
// Verify that we can read the value back.
- DBG("GET final value for key: %s", key);
+ PW_LOG_DEBUG("GET final value for key: %s", key);
uint32_t actual_value;
EXPECT_OK(kvs.Get(key, &actual_value));
EXPECT_EQ(actual_value, written_value);
@@ -229,7 +229,7 @@ TEST(InMemoryKvs, WritingMultipleKeysIncreasesSize) {
for (size_t i = 0; i < num_writes; ++i) {
StringBuffer<150> key;
key << "key_" << i;
- DBG("PUT #%zu for key %s with value %zu", i, key.c_str(), i);
+ PW_LOG_DEBUG("PUT #%zu for key %s with value %zu", i, key.c_str(), i);
size_t value = i + 77; // Prevent accidental pass with zero.
EXPECT_OK(kvs.Put(key.view(), value));
@@ -253,12 +253,12 @@ TEST(InMemoryKvs, WriteAndReadOneKey) {
// Add one entry.
const char* key = "Key1";
- DBG("PUT value for key: %s", key);
+ PW_LOG_DEBUG("PUT value for key: %s", key);
uint8_t written_value = 0xDA;
ASSERT_OK(kvs.Put(key, written_value));
EXPECT_EQ(kvs.size(), 1u);
- DBG("GET value for key: %s", key);
+ PW_LOG_DEBUG("GET value for key: %s", key);
uint8_t actual_value;
ASSERT_OK(kvs.Get(key, &actual_value));
EXPECT_EQ(actual_value, written_value);
@@ -280,12 +280,12 @@ TEST(InMemoryKvs, WriteOneKeyValueMultipleTimes) {
const char* key = "Key1";
uint8_t written_value = 0xDA;
for (int i = 0; i < 50; i++) {
- DBG("PUT [%d] value for key: %s", i, key);
+ PW_LOG_DEBUG("PUT [%d] value for key: %s", i, key);
ASSERT_OK(kvs.Put(key, written_value));
EXPECT_EQ(kvs.size(), 1u);
}
- DBG("GET value for key: %s", key);
+ PW_LOG_DEBUG("GET value for key: %s", key);
uint8_t actual_value;
ASSERT_OK(kvs.Get(key, &actual_value));
EXPECT_EQ(actual_value, written_value);
diff --git a/pw_kvs/public/pw_kvs/alignment.h b/pw_kvs/public/pw_kvs/alignment.h
index f8050d4ff..875d35074 100644
--- a/pw_kvs/public/pw_kvs/alignment.h
+++ b/pw_kvs/public/pw_kvs/alignment.h
@@ -19,6 +19,7 @@
#include <initializer_list>
#include <utility>
+#include "pw_bytes/alignment.h"
#include "pw_bytes/span.h"
#include "pw_kvs/io.h"
#include "pw_span/span.h"
@@ -26,21 +27,6 @@
namespace pw {
-// Returns the value rounded down to the nearest multiple of alignment.
-constexpr size_t AlignDown(size_t value, size_t alignment) {
- return (value / alignment) * alignment;
-}
-
-// Returns the value rounded up to the nearest multiple of alignment.
-constexpr size_t AlignUp(size_t value, size_t alignment) {
- return (value + alignment - 1) / alignment * alignment;
-}
-
-// Returns the number of padding bytes required to align the provided length.
-constexpr size_t Padding(size_t length, size_t alignment) {
- return AlignUp(length, alignment) - length;
-}
-
// Class for managing aligned writes. Stores data in an intermediate buffer and
// calls an output function with aligned data as the buffer becomes full. Any
// bytes remaining in the buffer are written to the output when Flush() is
@@ -61,7 +47,7 @@ class AlignedWriter {
AlignedWriter& operator=(const AlignedWriter&) = delete;
~AlignedWriter() {
- Flush().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ Flush().IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
// Writes bytes to the AlignedWriter. The output may be called if the internal
diff --git a/pw_kvs/public/pw_kvs/checksum.h b/pw_kvs/public/pw_kvs/checksum.h
index 9a307ea0e..ba931e00b 100644
--- a/pw_kvs/public/pw_kvs/checksum.h
+++ b/pw_kvs/public/pw_kvs/checksum.h
@@ -91,7 +91,7 @@ class AlignedChecksum : public ChecksumAlgorithm {
public:
void Update(span<const std::byte> data) final {
writer_.Write(data)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
protected:
@@ -106,7 +106,8 @@ class AlignedChecksum : public ChecksumAlgorithm {
static_assert(kBufferSize >= kAlignmentBytes);
void Finalize() final {
- writer_.Flush().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ writer_.Flush()
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
FinalizeAligned();
}
diff --git a/pw_kvs/public/pw_kvs/flash_memory.h b/pw_kvs/public/pw_kvs/flash_memory.h
index 37b5cdc1a..c7c9a6c88 100644
--- a/pw_kvs/public/pw_kvs/flash_memory.h
+++ b/pw_kvs/public/pw_kvs/flash_memory.h
@@ -16,6 +16,7 @@
#include <cstddef>
#include <cstdint>
#include <initializer_list>
+#include <limits>
#include "pw_assert/assert.h"
#include "pw_kvs/alignment.h"
@@ -42,7 +43,7 @@ class FlashMemory {
// The flash address is in the range of: 0 to FlashSize.
typedef uint32_t Address;
- // TODO(b/235149326): This can be constexpr when tokenized asserts are fixed.
+ // TODO: b/235149326 - This can be constexpr when tokenized asserts are fixed.
FlashMemory(size_t sector_size,
size_t sector_count,
size_t alignment,
@@ -167,26 +168,42 @@ class FlashPartition {
class Reader final : public stream::SeekableReader {
public:
- constexpr Reader(kvs::FlashPartition& partition)
- : partition_(partition), position_(0) {}
+ /// @brief Stream seekable reader for FlashPartitions.
+ ///
+ /// @param partition The partiion to read.
+ /// @param read_limit_bytes Optional limit to read less than the full
+ /// FlashPartition. Reader will use the lesser of read_limit_bytes and
+ /// partition size. Situations needing a subset that starts somewhere other
+ /// than 0 can seek to the desired start point.
+ Reader(kvs::FlashPartition& partition,
+ size_t read_limit_bytes = std::numeric_limits<size_t>::max())
+ : partition_(partition),
+ read_limit_(std::min(read_limit_bytes, partition_.size_bytes())),
+ position_(0) {}
Reader(const Reader&) = delete;
Reader& operator=(const Reader&) = delete;
+ void SetReadLimit(size_t read_limit_bytes) {
+ read_limit_ = std::min(read_limit_bytes, partition_.size_bytes());
+ position_ = std::min(position_, read_limit_);
+ }
+
private:
StatusWithSize DoRead(ByteSpan data) override;
size_t DoTell() override { return position_; }
Status DoSeek(ptrdiff_t offset, Whence origin) override {
- return CalculateSeek(offset, origin, partition_.size_bytes(), position_);
+ return CalculateSeek(offset, origin, read_limit_, position_);
}
size_t ConservativeLimit(LimitType type) const override {
- return type == LimitType::kRead ? partition_.size_bytes() - position_ : 0;
+ return type == LimitType::kRead ? read_limit_ - position_ : 0;
}
FlashPartition& partition_;
+ size_t read_limit_;
size_t position_;
};
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
diff --git a/pw_kvs/public/pw_kvs/flash_partition_with_logical_sectors.h b/pw_kvs/public/pw_kvs/flash_partition_with_logical_sectors.h
new file mode 100644
index 000000000..3c7e0fa9a
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/flash_partition_with_logical_sectors.h
@@ -0,0 +1,71 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "pw_kvs/flash_memory.h"
+
+namespace pw::kvs {
+
+// FlashPartition that supports combining multiple FlashMemory sectors in to a
+// single logical FlashPartition sector. The number of FlashMemory sectors per
+// logical sector is specified by flash_sectors_per_logical_sector.
+//
+// If the number of FlashMemory sectors is not a multiple of
+// flash_sectors_per_logical_sector, then the FlashMemory sectors used in the
+// partition is rounded down to the nearest multiple.
+class FlashPartitionWithLogicalSectors : public FlashPartition {
+ public:
+ FlashPartitionWithLogicalSectors(
+ FlashMemory* flash,
+ size_t flash_sectors_per_logical_sector,
+ uint32_t flash_start_sector_index,
+ uint32_t flash_sector_count,
+ uint32_t alignment_bytes = 0, // Defaults to flash alignment
+ PartitionPermission permission = PartitionPermission::kReadAndWrite)
+ : FlashPartition(flash,
+ flash_start_sector_index,
+ flash_sector_count,
+ alignment_bytes,
+ permission),
+ flash_sectors_per_logical_sector_(flash_sectors_per_logical_sector) {}
+
+ FlashPartitionWithLogicalSectors(FlashMemory* flash,
+ size_t flash_sectors_per_logical_sector)
+ : FlashPartitionWithLogicalSectors(flash,
+ flash_sectors_per_logical_sector,
+ 0,
+ flash->sector_count(),
+ flash->alignment_bytes()) {}
+
+ size_t sector_size_bytes() const override {
+ return flash_.sector_size_bytes() * flash_sectors_per_logical_sector_;
+ }
+
+ size_t sector_count() const override {
+ return flash_sector_count_ / flash_sectors_per_logical_sector_;
+ }
+
+ Status Erase(Address address, size_t num_sectors) override {
+ return flash_.Erase(address,
+ num_sectors * flash_sectors_per_logical_sector_);
+ }
+
+ private:
+ size_t flash_sectors_per_logical_sector_;
+};
+
+} // namespace pw::kvs
diff --git a/pw_kvs/sectors.cc b/pw_kvs/sectors.cc
index 25edcf676..50fe99673 100644
--- a/pw_kvs/sectors.cc
+++ b/pw_kvs/sectors.cc
@@ -18,7 +18,7 @@
#include "pw_kvs/internal/sectors.h"
#include "pw_kvs_private/config.h"
-#include "pw_log/shorter.h"
+#include "pw_log/log.h"
namespace pw::kvs::internal {
namespace {
@@ -64,12 +64,13 @@ Status Sectors::Find(FindMode find_mode,
temp_sectors_to_skip_[sectors_to_skip++] = &FromAddress(address);
}
- DBG("Find sector with %u bytes available, starting with sector %u, %s",
+ PW_LOG_DEBUG(
+ "Find sector with %u bytes available, starting with sector %u, %s",
unsigned(size),
Index(last_new_),
(find_mode == kAppendEntry) ? "Append" : "GC");
for (size_t i = 0; i < sectors_to_skip; ++i) {
- DBG(" Skip sector %u", Index(temp_sectors_to_skip_[i]));
+ PW_LOG_DEBUG(" Skip sector %u", Index(temp_sectors_to_skip_[i]));
}
// last_new_ is the sector that was last selected as the "new empty sector" to
@@ -136,7 +137,8 @@ Status Sectors::Find(FindMode find_mode,
// to keep 1 empty sector after the sector found here, but that rule does not
// apply during GC.
if (first_empty_sector != nullptr && at_least_two_empty_sectors) {
- DBG(" Found a usable empty sector; returning the first found (%u)",
+ PW_LOG_DEBUG(
+ " Found a usable empty sector; returning the first found (%u)",
Index(first_empty_sector));
last_new_ = first_empty_sector;
*found_sector = first_empty_sector;
@@ -147,14 +149,15 @@ Status Sectors::Find(FindMode find_mode,
// bytes
if (non_empty_least_reclaimable_sector != nullptr) {
*found_sector = non_empty_least_reclaimable_sector;
- DBG(" Found a usable sector %u, with %u B recoverable, in GC",
+ PW_LOG_DEBUG(
+ " Found a usable sector %u, with %u B recoverable, in GC",
Index(*found_sector),
unsigned((*found_sector)->RecoverableBytes(sector_size_bytes)));
return OkStatus();
}
// No sector was found.
- DBG(" Unable to find a usable sector");
+ PW_LOG_DEBUG(" Unable to find a usable sector");
*found_sector = nullptr;
return Status::ResourceExhausted();
}
@@ -173,7 +176,7 @@ SectorDescriptor* Sectors::FindSectorToGarbageCollect(
// Build a vector of sectors to avoid.
for (size_t i = 0; i < reserved_addresses.size(); ++i) {
temp_sectors_to_skip_[i] = &FromAddress(reserved_addresses[i]);
- DBG(" Skip sector %u", Index(reserved_addresses[i]));
+ PW_LOG_DEBUG(" Skip sector %u", Index(reserved_addresses[i]));
}
const span sectors_to_skip(temp_sectors_to_skip_, reserved_addresses.size());
@@ -216,17 +219,18 @@ SectorDescriptor* Sectors::FindSectorToGarbageCollect(
!Contains(sectors_to_skip, &sector)) {
sector_candidate = &sector;
candidate_bytes = sector.valid_bytes();
- DBG(" Doing GC on sector with no reclaimable bytes!");
+ PW_LOG_DEBUG(" Doing GC on sector with no reclaimable bytes!");
}
}
}
if (sector_candidate != nullptr) {
- DBG("Found sector %u to Garbage Collect, %u recoverable bytes",
+ PW_LOG_DEBUG(
+ "Found sector %u to Garbage Collect, %u recoverable bytes",
Index(sector_candidate),
unsigned(sector_candidate->RecoverableBytes(sector_size_bytes)));
} else {
- DBG("Unable to find sector to garbage collect!");
+ PW_LOG_DEBUG("Unable to find sector to garbage collect!");
}
return sector_candidate;
}
diff --git a/pw_kvs/size_report/base_with_only_flash.cc b/pw_kvs/size_report/base_with_only_flash.cc
index f1fafa931..164171197 100644
--- a/pw_kvs/size_report/base_with_only_flash.cc
+++ b/pw_kvs/size_report/base_with_only_flash.cc
@@ -40,20 +40,20 @@ int main() {
is_set = (result != nullptr);
test_partition.Erase()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
test_partition.Write(0, pw::as_bytes(pw::span(working_buffer)))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
bool tmp_bool;
test_partition.IsErased(&tmp_bool)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
is_erased = tmp_bool;
test_partition.Read(0, as_writable_bytes(pw::span(working_buffer)))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return 0;
}
diff --git a/pw_kvs/size_report/with_kvs.cc b/pw_kvs/size_report/with_kvs.cc
index eed3e4b8a..83b866924 100644
--- a/pw_kvs/size_report/with_kvs.cc
+++ b/pw_kvs/size_report/with_kvs.cc
@@ -49,17 +49,17 @@ int main() {
std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
is_set = (result != nullptr);
- kvs.Init().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ kvs.Init().IgnoreError(); // TODO: b/242598609 - Handle Status properly
unsigned kvs_value = 42;
kvs.Put("example_key", kvs_value)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
kvs_entry_count = kvs.size();
unsigned read_value = 0;
kvs.Get("example_key", &read_value)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return 0;
}
diff --git a/pw_libc/BUILD.gn b/pw_libc/BUILD.gn
index 6b5c01f0e..d9fd80be7 100644
--- a/pw_libc/BUILD.gn
+++ b/pw_libc/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2019 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -16,6 +16,8 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/llvm_libc/llvm_libc.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -23,7 +25,10 @@ config("default_config") {
}
pw_test_group("tests") {
- tests = [ ":memset_test" ]
+ tests = [
+ ":llvm_libc_tests",
+ ":memset_test",
+ ]
}
pw_test("memset_test") {
@@ -31,6 +36,139 @@ pw_test("memset_test") {
deps = [ "$dir_pw_containers" ]
}
+# Clang has __attribute__(("no-builtin")), but gcc doesn't support it so we
+# need this flag instead.
+config("no-builtin") {
+ cflags = [ "-fno-builtin" ]
+}
+
+# Downstream projects sometimes build with -Wshadow, which on gcc also warns
+# about constructor arguments shadowing struct members. This is too pedantic
+# and not reasonable to change upstream llvm-libc.
+config("no-shadow") {
+ cflags = [ "-Wno-shadow" ]
+}
+
+# If dir_pw_third_party_llvm_libc is defined, use that directory to create a
+# pw_libc.a from llvm-libc. Otherwise, we create an empty pw_libc.a.
+if (dir_pw_third_party_llvm_libc != "") {
+ pw_libc_source_set("stdlib") {
+ functions = [
+ "abs",
+ "rand",
+ "srand",
+ ]
+ additional_srcs = [
+ "baremetal/abort.cpp",
+ "rand_util.cpp",
+ ]
+
+ # srand and rand are both tested in rand_test.cpp.
+ no_test_functions = [ "srand" ]
+ }
+
+ pw_libc_source_set("string") {
+ defines = [ "LIBC_COPT_MEMCPY_USE_EMBEDDED_TINY" ]
+ functions = [
+ "strcmp",
+ "strcpy",
+ "strstr",
+ "strnlen",
+ "memcpy",
+ "memset",
+ "memmove",
+ ]
+
+ # memmove tests use gtest matchers which pw_unit_test doesn't support.
+ no_test_functions = [ "memmove" ]
+
+ configs = [
+ ":no-builtin",
+ ":no-shadow",
+ ]
+ }
+
+ pw_libc_source_set("ctype") {
+ functions = [ "isprint" ]
+ }
+
+ pw_libc_source_set("time") {
+ functions = [ "gmtime" ]
+ additional_srcs = [ "time_utils.cpp" ]
+
+ # gmtime requires gtest matchers which pw_unit_test doesn't support.
+ # Moreover, the matches in llvm-libc don't have the same internal API that
+ # gtest does, so it isn't possible to enable this tests when using gtest
+ # either.
+ no_test_functions = [ "gmtime" ]
+ }
+
+ pw_libc_source_set("math") {
+ non_cpu_dir = "generic"
+
+ functions = [
+ "modff",
+ "roundf",
+ ]
+
+ # Math tests require the MPFR library, which is not available.
+ no_test_functions = functions
+ }
+
+ pw_libc_source_set("stdio") {
+ functions = [ "snprintf" ]
+
+ additional_srcs = [
+ "printf_core/printf_main.cpp",
+ "printf_core/writer.cpp",
+ "printf_core/parser.cpp",
+ "printf_core/converter.cpp",
+ ]
+
+ defines = [
+ "LIBC_COPT_PRINTF_DISABLE_FLOAT",
+ "LIBC_COPT_PRINTF_DISABLE_WRITE_INT",
+ "LIBC_COPT_PRINTF_DISABLE_INDEX_MODE",
+ ]
+
+ # This config includes -Wshadow. On gcc, this warns even for constructor
+ # arguments which shadow members. This is too pedantic and shouldn't be
+ # changed upstream.
+ remove_configs = [ "//pw_build:extra_strict_warnings" ]
+ }
+
+ pw_static_library("pw_libc") {
+ complete_static_lib = true
+ add_global_link_deps = false
+ deps = [
+ ":ctype",
+ ":math",
+ ":stdio",
+ ":stdlib",
+ ":string",
+ ":time",
+ ]
+ }
+
+ pw_test_group("llvm_libc_tests") {
+ tests = [
+ ":ctype_tests",
+ ":math_tests",
+ ":stdio_tests",
+ ":stdlib_tests",
+ ":string_tests",
+ ":time_tests",
+ ]
+ }
+} else {
+ pw_static_library("pw_libc") {
+ add_global_link_deps = false
+ }
+
+ pw_test_group("llvm_libc_tests") {
+ }
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_libcxx/BUILD.bazel b/pw_libcxx/BUILD.bazel
new file mode 100644
index 000000000..0ef580840
--- /dev/null
+++ b/pw_libcxx/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+pw_cc_library(
+ name = "pw_libcxx",
+ srcs = [
+ "__cxa_deleted_virtual.cc",
+ "__cxa_pure_virtual.cc",
+ "operator_delete.cc",
+ ],
+)
diff --git a/pw_libcxx/BUILD.gn b/pw_libcxx/BUILD.gn
new file mode 100644
index 000000000..4083abf4c
--- /dev/null
+++ b/pw_libcxx/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/cc_library.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_static_library("pw_libcxx") {
+ sources = [
+ "__cxa_deleted_virtual.cc",
+ "__cxa_pure_virtual.cc",
+ "operator_delete.cc",
+ ]
+}
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_libcxx/__cxa_deleted_virtual.cc b/pw_libcxx/__cxa_deleted_virtual.cc
new file mode 100644
index 000000000..6f02ab1ed
--- /dev/null
+++ b/pw_libcxx/__cxa_deleted_virtual.cc
@@ -0,0 +1,15 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+extern "C" void __cxa_deleted_virtual() { __builtin_trap(); }
diff --git a/pw_libcxx/__cxa_pure_virtual.cc b/pw_libcxx/__cxa_pure_virtual.cc
new file mode 100644
index 000000000..b0c73b510
--- /dev/null
+++ b/pw_libcxx/__cxa_pure_virtual.cc
@@ -0,0 +1,15 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+extern "C" void __cxa_pure_virtual() { __builtin_trap(); }
diff --git a/pw_libcxx/docs.rst b/pw_libcxx/docs.rst
new file mode 100644
index 000000000..4243fee74
--- /dev/null
+++ b/pw_libcxx/docs.rst
@@ -0,0 +1,9 @@
+.. _module-pw_libcxx:
+
+---------
+pw_libcxx
+---------
+The ``pw_libcxx`` module provides libcxx symbols, and will eventually facilitate
+pulling in headers as well. Currently, none of the library is built from
+upstream LLVM libcxx, instead the symbols provided should just crash in
+an embedded context.
diff --git a/pw_libcxx/operator_delete.cc b/pw_libcxx/operator_delete.cc
new file mode 100644
index 000000000..a545f525d
--- /dev/null
+++ b/pw_libcxx/operator_delete.cc
@@ -0,0 +1,37 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <new>
+
+// fxbug.dev/132869: We should look into compiler changes to avoid emitting
+// references to these symbols.
+// These operator delete implementations are provided to satisfy references from
+// the vtable for the deleting destructor. In practice, these will never be
+// reached because users should not be using new/delete.
+
+void operator delete(void*) noexcept { __builtin_trap(); }
+void operator delete[](void*) noexcept { __builtin_trap(); }
+
+void operator delete(void*, std::align_val_t) noexcept { __builtin_trap(); }
+void operator delete[](void*, std::align_val_t) noexcept { __builtin_trap(); }
+
+void operator delete(void*, std::size_t) noexcept { __builtin_trap(); }
+void operator delete[](void*, std::size_t) noexcept { __builtin_trap(); }
+
+void operator delete(void*, std::size_t, std::align_val_t) noexcept {
+ __builtin_trap();
+}
+void operator delete[](void*, std::size_t, std::align_val_t) noexcept {
+ __builtin_trap();
+}
diff --git a/pw_log/Android.bp b/pw_log/Android.bp
index 17138993c..9b38ba320 100644
--- a/pw_log/Android.bp
+++ b/pw_log/Android.bp
@@ -24,6 +24,160 @@ cc_library_headers {
host_supported: true,
}
+cc_library_headers {
+ name: "pw_log_pwpb_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ generated_headers: [
+ "google_protobuf_descriptor_pwpb_h",
+ "pw_log_log_proto_pwpb_h",
+ "pw_log_log_rpc_pwpb_h",
+ "pw_log_log_raw_rpc_h",
+ "pw_protobuf_protos_common_pwpb_h",
+ "pw_tokenizer_proto_options_pwpb_h",
+ ],
+ export_generated_headers: [
+ "google_protobuf_descriptor_pwpb_h",
+ "pw_log_log_proto_pwpb_h",
+ "pw_log_log_rpc_pwpb_h",
+ "pw_log_log_raw_rpc_h",
+ "pw_protobuf_protos_common_pwpb_h",
+ "pw_tokenizer_proto_options_pwpb_h",
+ ],
+}
+
+// Copies the proto files to a prefix directory to add the prefix to the
+// compiled proto. The prefix is taken from the directory name of the first
+// item listen in out.
+genrule_defaults {
+ name: "pw_log_add_prefix_to_proto",
+ cmd: "out_files=($(out)); prefix=$$(dirname $${out_files[0]}); " +
+ "mkdir -p $${prefix}; cp -t $${prefix} $(in);"
+}
+
+genrule {
+ name: "pw_log_log_proto_with_prefix",
+ defaults: ["pw_log_add_prefix_to_proto"],
+ srcs: [
+ "log.proto",
+ ],
+ out: [
+ "pw_log/proto/log.proto",
+ ],
+}
+
+genrule {
+ name: "pw_log_log_proto_pwpb_h",
+ srcs: [
+ ":libprotobuf-internal-protos",
+ ":pw_log_log_proto_with_prefix",
+ ":pw_protobuf_common_proto",
+ ],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--proto-path=external/pigweed/pw_protobuf/ " +
+ // Requires the generated pw_tokenizer/proto/options.proto filepath.
+ "--proto-path=$$(dirname $$(dirname $$(dirname $(location :pw_tokenizer_proto_options_proto)))) " +
+ "--proto-path=$$(dirname $$(dirname $$(dirname $(location :pw_log_log_proto_with_prefix)))) " +
+ "--proto-path=external/protobuf/src/ " +
+ "--out-dir=$(genDir) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$(genDir) " +
+ "--sources $(location :pw_log_log_proto_with_prefix) " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) && " +
+ // TODO(b/308678575) - Ideally the previous command would create the file at the right location,
+ // but following all the layers of pigweed wrappers around protoc is too difficult.
+ "python3 -c \"import os; import shutil; " +
+ "shutil.copy2(os.path.splitext('$(location :pw_log_log_proto_with_prefix)')[0]+'.pwpb.h', '$(out)')\"",
+ out: [
+ "pw_log/proto/log.pwpb.h",
+ ],
+ tool_files: [
+ ":pw_tokenizer_proto_options_proto",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+genrule {
+ name: "pw_log_log_rpc_pwpb_h",
+ srcs: [
+ ":libprotobuf-internal-protos",
+ ":pw_log_log_proto_with_prefix",
+ ":pw_protobuf_common_proto",
+ ],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--proto-path=external/pigweed/pw_protobuf/ " +
+ // Requires the generated pw_tokenizer/proto/options.proto filepath.
+ "--proto-path=$$(dirname $$(dirname $$(dirname $(location :pw_tokenizer_proto_options_proto)))) " +
+ "--proto-path=$$(dirname $$(dirname $$(dirname $(location :pw_log_log_proto_with_prefix)))) " +
+ "--proto-path=external/protobuf/src/ " +
+ "--out-dir=$(genDir) " +
+ "--plugin-path=$(location pw_rpc_plugin_pwpb_py) " +
+ "--compile-dir=$(genDir) " +
+ "--sources $(location :pw_log_log_proto_with_prefix) " +
+ "--language pwpb_rpc " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) && " +
+ // TODO(b/308678575) - Ideally the previous command would create the file at the right location,
+ // but following all the layers of pigweed wrappers around protoc is too difficult.
+ "python3 -c \"import os; import shutil; " +
+ "shutil.copy2(os.path.splitext('$(location :pw_log_log_proto_with_prefix)')[0]+'.rpc.pwpb.h', '$(out)')\"",
+ out: [
+ "pw_log/proto/log.rpc.pwpb.h",
+ ],
+ tool_files: [
+ ":pw_tokenizer_proto_options_proto",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_pwpb_py",
+ ],
+}
+
+genrule {
+ name: "pw_log_log_raw_rpc_h",
+ srcs: [
+ ":libprotobuf-internal-protos",
+ ":pw_log_log_proto_with_prefix",
+ ":pw_protobuf_common_proto",
+ ],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--proto-path=external/pigweed/pw_protobuf/ " +
+ // Requires the generated pw_tokenizer/proto/options.proto filepath.
+ "--proto-path=$$(dirname $$(dirname $$(dirname $(location :pw_tokenizer_proto_options_proto)))) " +
+ "--proto-path=$$(dirname $$(dirname $$(dirname $(location :pw_log_log_proto_with_prefix)))) " +
+ "--proto-path=external/protobuf/src/ " +
+ "--out-dir=$(genDir) " +
+ "--plugin-path=$(location pw_rpc_plugin_rawpb_py) " +
+ "--compile-dir=$(genDir) " +
+ "--sources $(location :pw_log_log_proto_with_prefix) " +
+ "--language raw_rpc " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) && " +
+ // TODO(b/308678575) - Ideally the previous command would create the file at the right location,
+ // but following all the layers of pigweed wrappers around protoc is too difficult.
+ "python3 -c \"import os; import shutil; " +
+ "shutil.copy2(os.path.splitext('$(location :pw_log_log_proto_with_prefix)')[0]+'.raw_rpc.pb.h', '$(out)')\"",
+ out: [
+ "pw_log/proto/log.raw_rpc.pb.h",
+ ],
+ tool_files: [
+ ":pw_tokenizer_proto_options_proto",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_rawpb_py",
+ ],
+}
+
android_library {
name: "pw_log_android_java",
srcs: ["java/android_main/dev/pigweed/pw_log/*.java"],
diff --git a/pw_log/BUILD.bazel b/pw_log/BUILD.bazel
index b1bf3acbf..6a4776f5c 100644
--- a/pw_log/BUILD.bazel
+++ b/pw_log/BUILD.bazel
@@ -18,8 +18,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -43,12 +43,27 @@ pw_cc_facade(
pw_cc_library(
name = "pw_log",
+ hdrs = [
+ "public/pw_log/config.h",
+ "public/pw_log/levels.h",
+ "public/pw_log/log.h",
+ "public/pw_log/options.h",
+ "public/pw_log/short.h",
+ "public/pw_log/shorter.h",
+ "public/pw_log/tokenized_args.h",
+ ],
+ includes = ["public"],
deps = [
- ":facade",
- "@pigweed_config//:pw_log_backend",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_log_backend",
],
)
+label_flag(
+ name = "backend_impl",
+ build_setting_default = "//pw_log_basic:impl",
+)
+
pw_cc_library(
name = "glog_adapter",
hdrs = [
@@ -96,7 +111,7 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "log_proto_py_pb2",
tags = ["manual"],
@@ -115,7 +130,7 @@ pw_proto_library(
pw_cc_library(
name = "backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = ["//pw_log_basic"],
)
diff --git a/pw_log/BUILD.gn b/pw_log/BUILD.gn
index 3ff928ed2..e61a294df 100644
--- a/pw_log/BUILD.gn
+++ b/pw_log/BUILD.gn
@@ -59,6 +59,12 @@ pw_facade("pw_log") {
require_link_deps = [ ":impl" ]
}
+pw_source_set("args") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ dir_pw_tokenizer ]
+ public = [ "public/pw_log/tokenized_args.h" ]
+}
+
pw_source_set("glog_adapter") {
public_configs = [ ":public_include_path" ]
public = [
@@ -162,12 +168,14 @@ pw_proto_library("protos") {
"$dir_pw_protobuf:common_protos",
"$dir_pw_tokenizer:proto",
]
+ python_package = "py"
}
pw_doc_group("docs") {
sources = [
"docs.rst",
"protobuf.rst",
+ "tokenized_args.rst",
]
inputs = [
"example_layer_diagram.svg",
diff --git a/pw_log/CMakeLists.txt b/pw_log/CMakeLists.txt
index 887bbf8dd..0bbd88414 100644
--- a/pw_log/CMakeLists.txt
+++ b/pw_log/CMakeLists.txt
@@ -38,6 +38,7 @@ pw_add_facade(pw_log INTERFACE
public/pw_log/options.h
public/pw_log/short.h
public/pw_log/shorter.h
+ public/pw_log/tokenized_args.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
diff --git a/pw_log/basic_log_test.cc b/pw_log/basic_log_test.cc
index 960f9beef..c851d785f 100644
--- a/pw_log/basic_log_test.cc
+++ b/pw_log/basic_log_test.cc
@@ -16,7 +16,7 @@
// compile the constructs promised by the logging facade; and that when run,
// there is no crash.
//
-// TODO(b/235289499): Add verification of the actually logged statements.
+// TODO: b/235289499 - Add verification of the actually logged statements.
// clang-format off
#define PW_LOG_MODULE_NAME "TST"
@@ -28,7 +28,7 @@
#include "gtest/gtest.h"
-// TODO(b/235291136): Test unsigned integer logging (32 and 64 bit); test
+// TODO: b/235291136 - Test unsigned integer logging (32 and 64 bit); test
// pointer logging.
void LoggingFromFunction() { PW_LOG_INFO("From a function!"); }
diff --git a/pw_log/docs.rst b/pw_log/docs.rst
index 3453c08e1..950a82c3e 100644
--- a/pw_log/docs.rst
+++ b/pw_log/docs.rst
@@ -1,3 +1,5 @@
+:tocdepth: 4
+
.. _module-pw_log:
======
@@ -15,9 +17,10 @@ service for efficiently storing and transmitting log messages. See
:ref:`module-pw_log-protobuf` for details.
.. toctree::
- :hidden:
+ :hidden:
- protobuf
+ protobuf
+ tokenized_args
--------------
Usage examples
@@ -27,39 +30,48 @@ long-form names.
.. code-block:: cpp
- #define PW_LOG_MODULE_NAME "BLE"
+ #define PW_LOG_MODULE_NAME "BLE"
- #include "pw_log/log.h"
+ #include "pw_log/log.h"
- int main() {
- PW_LOG_INFO("Booting...");
- PW_LOG_DEBUG("CPU temp: %.2f", cpu_temperature);
- if (BootFailed()) {
- PW_LOG_CRITICAL("Had trouble booting due to error %d", GetErrorCode());
- ReportErrorsAndHalt();
- }
- PW_LOG_INFO("Successfully booted");
- }
+ int main() {
+ PW_LOG_INFO("Booting...");
+ PW_LOG_DEBUG("CPU temp: %.2f", cpu_temperature);
+ if (BootFailed()) {
+ PW_LOG_CRITICAL("Had trouble booting due to error %d", GetErrorCode());
+ ReportErrorsAndHalt();
+ }
+ PW_LOG_INFO("Successfully booted");
+ }
In ``.cc`` files, it is possible to dispense with the ``PW_`` part of the log
names and go for shorter log macros. Include ``pw_log/short.h`` or
``pw_log/shorter.h`` for shorter versions of the macros.
+.. warning::
+ The shorter log macros collide with `Abseil's logging API
+ <https://abseil.io/docs/cpp/guides/logging>`_. Do not use them in upstream
+ Pigweed modules, or any code that may depend on Abseil.
+
.. code-block:: cpp
- #define PW_LOG_MODULE_NAME "BLE"
+ #define PW_LOG_MODULE_NAME "BLE"
+
+ #include "pw_log/shorter.h"
- #include "pw_log/shorter.h"
+ int main() {
+ INF("Booting...");
+ DBG("CPU temp: %.2f", cpu_temperature);
+ if (BootFailed()) {
+ CRT("Had trouble booting due to error %d", GetErrorCode());
+ ReportErrorsAndHalt();
+ }
+ INF("Successfully booted");
+ }
- int main() {
- INF("Booting...");
- DBG("CPU temp: %.2f", cpu_temperature);
- if (BootFailed()) {
- CRT("Had trouble booting due to error %d", GetErrorCode());
- ReportErrorsAndHalt();
- }
- INF("Successfully booted");
- }
+The ``pw_log`` facade also exposes a handful of macros that only apply
+specifically to tokenized logging. See :ref:`module-pw_log-tokenized-args` for
+details.
Layer diagram example: ``stm32f429i-disc1``
===========================================
@@ -71,6 +83,8 @@ turn outputs to the STM32F429 bare metal backend for ``pw_sys_io``, which is
.. image:: example_layer_diagram.svg
+.. _module-pw_log-macros:
+
Logging macros
==============
These are the primary macros for logging information about the functioning of a
@@ -78,45 +92,45 @@ system, intended to be used directly.
.. c:macro:: PW_LOG(level, module, flags, fmt, ...)
- This is the primary mechanism for logging.
+ This is the primary mechanism for logging.
- *level* - An integer level as defined by ``pw_log/levels.h``.
+ *level* - An integer level as defined by ``pw_log/levels.h``.
- *module* - A string literal for the module name. Defaults to
- :c:macro:`PW_LOG_MODULE_NAME`.
+ *module* - A string literal for the module name. Defaults to
+ :c:macro:`PW_LOG_MODULE_NAME`.
- *flags* - Arbitrary flags the backend can leverage. The semantics of these
- flags are not defined in the facade, but are instead meant as a general
- mechanism for communication bits of information to the logging backend.
- ``pw_log`` reserves 2 flag bits by default, but log backends may provide for
- more or fewer flag bits.
+ *flags* - Arbitrary flags the backend can leverage. The semantics of these
+ flags are not defined in the facade, but are instead meant as a general
+ mechanism for communication bits of information to the logging backend.
+ ``pw_log`` reserves 2 flag bits by default, but log backends may provide for
+ more or fewer flag bits.
- Here are some ideas for what a backend might use flags for:
+ Here are some ideas for what a backend might use flags for:
- - Example: ``HAS_PII`` - A log has personally-identifying data
- - Example: ``HAS_DII`` - A log has device-identifying data
- - Example: ``RELIABLE_DELIVERY`` - Ask the backend to ensure the log is
- delivered; this may entail blocking other logs.
- - Example: ``BEST_EFFORT`` - Don't deliver this log if it would mean blocking
- or dropping important-flagged logs
+ - Example: ``HAS_PII`` - A log has personally-identifying data
+ - Example: ``HAS_DII`` - A log has device-identifying data
+ - Example: ``RELIABLE_DELIVERY`` - Ask the backend to ensure the log is
+ delivered; this may entail blocking other logs.
+ - Example: ``BEST_EFFORT`` - Don't deliver this log if it would mean blocking
+ or dropping important-flagged logs
- *fmt* - The message to log, which may contain format specifiers like ``%d``
- or ``%0.2f``.
+ *fmt* - The message to log, which may contain format specifiers like ``%d``
+ or ``%0.2f``.
- Example:
+ Example:
- .. code-block:: cpp
+ .. code-block:: cpp
- PW_LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Temp is %d degrees", temp);
- PW_LOG(PW_LOG_LEVEL_ERROR, PW_LOG_MODULE_NAME, UNRELIABLE_DELIVERY, "It didn't work!");
+ PW_LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Temp is %d degrees", temp);
+ PW_LOG(PW_LOG_LEVEL_ERROR, PW_LOG_MODULE_NAME, UNRELIABLE_DELIVERY, "It didn't work!");
- .. note::
+ .. note::
- ``PW_LOG()`` should not be used frequently; typically only when adding
- flags to a particular message to mark PII or to indicate delivery
- guarantees. For most cases, prefer to use the direct ``PW_LOG_INFO`` or
- ``PW_LOG_DEBUG`` style macros, which are often implemented more efficiently
- in the backend.
+ ``PW_LOG()`` should not be used frequently; typically only when adding
+ flags to a particular message to mark PII or to indicate delivery
+ guarantees. For most cases, prefer to use the direct ``PW_LOG_INFO`` or
+ ``PW_LOG_DEBUG`` style macros, which are often implemented more efficiently
+ in the backend.
.. c:macro:: PW_LOG_DEBUG(fmt, ...)
@@ -125,7 +139,7 @@ system, intended to be used directly.
.. c:macro:: PW_LOG_ERROR(fmt, ...)
.. c:macro:: PW_LOG_CRITICAL(fmt, ...)
- Shorthand for ``PW_LOG(<level>, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, fmt, ...)``.
+ Shorthand for ``PW_LOG(<level>, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, fmt, ...)``.
--------------------
Module configuration
@@ -137,22 +151,22 @@ more details.
.. c:macro:: PW_LOG_LEVEL_DEFAULT
- Controls the default value of ``PW_LOG_LEVEL``. Setting
- ``PW_LOG_LEVEL_DEFAULT`` will change the behavior of all source files that
- have not explicitly set ``PW_LOG_LEVEL``. Defaults to ``PW_LOG_LEVEL_DEBUG``.
+ Controls the default value of ``PW_LOG_LEVEL``. Setting
+ ``PW_LOG_LEVEL_DEFAULT`` will change the behavior of all source files that
+ have not explicitly set ``PW_LOG_LEVEL``. Defaults to ``PW_LOG_LEVEL_DEBUG``.
.. c:macro:: PW_LOG_FLAGS_DEFAULT
- Controls the default value of ``PW_LOG_FLAGS``. Setting
- ``PW_LOG_FLAGS_DEFAULT`` will change the behavior of all source files that
- have not explicitly set ``PW_LOG_FLAGS``. Defaults to ``0``.
+ Controls the default value of ``PW_LOG_FLAGS``. Setting
+ ``PW_LOG_FLAGS_DEFAULT`` will change the behavior of all source files that
+ have not explicitly set ``PW_LOG_FLAGS``. Defaults to ``0``.
.. c:macro:: PW_LOG_ENABLE_IF_DEFAULT
- Controls the default value of ``PW_LOG_ENABLE_IF``. Setting
- ``PW_LOG_ENABLE_IF_DEFAULT`` will change the behavior of all source files that
- have not explicitly set ``PW_LOG_ENABLE_IF``. Defaults to
- ``((level) >= PW_LOG_LEVEL)``.
+ Controls the default value of ``PW_LOG_ENABLE_IF``. Setting
+ ``PW_LOG_ENABLE_IF_DEFAULT`` will change the behavior of all source files that
+ have not explicitly set ``PW_LOG_ENABLE_IF``. Defaults to
+ ``((level) >= PW_LOG_LEVEL)``.
Per-source file configuration
@@ -166,38 +180,38 @@ it is included. To handle potential transitive includes, place these
``#defines`` before all ``#include`` statements. This should only be done in
source files, not headers. For example:
- .. code-block:: cpp
+.. code-block:: cpp
- // Set the pw_log option macros here, before ALL of the #includes.
- #define PW_LOG_MODULE_NAME "Calibration"
- #define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
+ // Set the pw_log option macros here, before ALL of the #includes.
+ #define PW_LOG_MODULE_NAME "Calibration"
+ #define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
- #include <array>
- #include <random>
+ #include <array>
+ #include <random>
- #include "devices/hal9000.h"
- #include "pw_log/log.h"
- #include "pw_rpc/server.h"
+ #include "devices/hal9000.h"
+ #include "pw_log/log.h"
+ #include "pw_rpc/server.h"
- int MyFunction() {
- PW_LOG_INFO("hello???");
- }
+ int MyFunction() {
+ PW_LOG_INFO("hello???");
+ }
.. c:macro:: PW_LOG_MODULE_NAME
- A string literal module name to use in logs. Log backends may attach this
- name to log messages or use it for runtime filtering. Defaults to ``""``. The
- ``PW_LOG_MODULE_NAME_DEFINED`` macro is set to ``1`` or ``0`` to indicate
- whether ``PW_LOG_MODULE_NAME`` was overridden.
+ A string literal module name to use in logs. Log backends may attach this
+ name to log messages or use it for runtime filtering. Defaults to ``""``. The
+ ``PW_LOG_MODULE_NAME_DEFINED`` macro is set to ``1`` or ``0`` to indicate
+ whether ``PW_LOG_MODULE_NAME`` was overridden.
.. c:macro:: PW_LOG_FLAGS
- Log flags to use for the ``PW_LOG_<level>`` macros. Different flags may be
- applied when using the ``PW_LOG`` macro directly.
+ Log flags to use for the ``PW_LOG_<level>`` macros. Different flags may be
+ applied when using the ``PW_LOG`` macro directly.
- Log backends use flags to change how they handle individual log messages.
- Potential uses include assigning logs priority or marking them as containing
- personal information. Defaults to ``PW_LOG_FLAGS_DEFAULT``.
+ Log backends use flags to change how they handle individual log messages.
+ Potential uses include assigning logs priority or marking them as containing
+ personal information. Defaults to ``PW_LOG_FLAGS_DEFAULT``.
.. c:macro:: PW_LOG_LEVEL
@@ -209,15 +223,15 @@ source files, not headers. For example:
.. code-block:: cpp
- #define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+ #define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
- #include "pw_log/log.h"
+ #include "pw_log/log.h"
- void DoSomething() {
- PW_LOG_DEBUG("This won't be logged at all");
- PW_LOG_INFO("This is INFO level, and will display");
- PW_LOG_WARN("This is above INFO level, and will display");
- }
+ void DoSomething() {
+ PW_LOG_DEBUG("This won't be logged at all");
+ PW_LOG_INFO("This is INFO level, and will display");
+ PW_LOG_WARN("This is above INFO level, and will display");
+ }
.. c:macro:: PW_LOG_ENABLE_IF(level, flags)
@@ -230,37 +244,37 @@ source files, not headers. For example:
.. code-block:: cpp
- // Pigweed's log facade will call this macro to decide to log or not. In
- // this case, it will drop logs with the PII flag set if display of PII is
- // not enabled for the application.
- #define PW_LOG_ENABLE_IF(level, flags) \
- (level >= PW_LOG_LEVEL_INFO && \
- !((flags & MY_PRODUCT_PII_MASK) && MY_PRODUCT_LOG_PII_ENABLED)
+ // Pigweed's log facade will call this macro to decide to log or not. In
+ // this case, it will drop logs with the PII flag set if display of PII is
+ // not enabled for the application.
+ #define PW_LOG_ENABLE_IF(level, flags) \
+ (level >= PW_LOG_LEVEL_INFO && \
+ !((flags & MY_PRODUCT_PII_MASK) && MY_PRODUCT_LOG_PII_ENABLED)
- #include "pw_log/log.h"
+ #include "pw_log/log.h"
- // This define might be supplied by the build system.
- #define MY_PRODUCT_LOG_PII_ENABLED false
+ // This define might be supplied by the build system.
+ #define MY_PRODUCT_LOG_PII_ENABLED false
- // This is the PII mask bit selected by the application.
- #define MY_PRODUCT_PII_MASK (1 << 5)
+ // This is the PII mask bit selected by the application.
+ #define MY_PRODUCT_PII_MASK (1 << 5)
- void DoSomethingWithSensitiveInfo() {
- PW_LOG_DEBUG("This won't be logged at all");
- PW_LOG_INFO("This is INFO level, and will display");
+ void DoSomethingWithSensitiveInfo() {
+ PW_LOG_DEBUG("This won't be logged at all");
+ PW_LOG_INFO("This is INFO level, and will display");
- // In this example, this will not be logged since logging with PII
- // is disabled by the above macros.
- PW_LOG(PW_LOG_LEVEL_INFO,
- MY_PRODUCT_PII_MASK,
- "Sensitive: %d",
- sensitive_info);
- }
+ // In this example, this will not be logged since logging with PII
+ // is disabled by the above macros.
+ PW_LOG(PW_LOG_LEVEL_INFO,
+ MY_PRODUCT_PII_MASK,
+ "Sensitive: %d",
+ sensitive_info);
+ }
.. attention::
- At this time, only compile time filtering is supported. In the future, we
- plan to add support for runtime filtering.
+ At this time, only compile time filtering is supported. In the future, we
+ plan to add support for runtime filtering.
------------------
Logging attributes
@@ -297,6 +311,8 @@ common for the ``pw_log`` backend to cause circular dependencies. Because of
this, log backends may avoid declaring explicit dependencies, instead relying
on include paths to access header files.
+GN
+==
In GN, the ``pw_log`` backend's full implementation with true dependencies is
made available through the ``$dir_pw_log:impl`` group. When ``pw_log_BACKEND``
is set, ``$dir_pw_log:impl`` must be listed in the ``pw_build_LINK_DEPS``
@@ -313,6 +329,18 @@ to directly provide dependencies through include paths only, rather than GN
``public_deps``. In this case, GN header checking can be disabled with
``check_includes = false``.
+.. _module-pw_log-bazel-backend_impl:
+
+Bazel
+=====
+In Bazel, log backends may avoid cyclic dependencies by placing the full
+implementation in an ``impl`` target, like ``//pw_log_tokenized:impl``. The
+``//pw_log:backend_impl`` label flag should be set to the ``impl`` target
+required by the log backend used by the platform.
+
+You must add a dependency on the ``@pigweed//pw_log:backend_impl`` target to
+any binary using ``pw_log``.
+
----------------------
Google Logging Adapter
----------------------
@@ -327,15 +355,15 @@ Configuration
.. c:macro:: PW_LOG_CFG_GLOG_BUFFER_SIZE_BYTES
- The size of the stack-allocated buffer used by the Google Logging (glog)
- macros. This only affects the glog macros provided through pw_log/glog.h.
+ The size of the stack-allocated buffer used by the Google Logging (glog)
+ macros. This only affects the glog macros provided through pw_log/glog.h.
- Pigweed strongly recommends sticking to printf-style logging instead
- of C++ stream-style Google Log logging unless absolutely necessary. The glog
- macros are only provided for compatibility with non-embedded code. See
- :ref:`module-pw_log-design-discussion` for more details.
+ Pigweed strongly recommends sticking to printf-style logging instead
+ of C++ stream-style Google Log logging unless absolutely necessary. The glog
+ macros are only provided for compatibility with non-embedded code. See
+ :ref:`module-pw_log-design-discussion` for more details.
- Undersizing this buffer will result in truncated log messages.
+ Undersizing this buffer will result in truncated log messages.
-----------------
Design discussion
@@ -354,14 +382,14 @@ Consider this example use of Google Log:
.. code-block:: cpp
- LOG(INFO) << "My temperature is " << temperature << ". State: " << state;
+ LOG(INFO) << "My temperature is " << temperature << ". State: " << state;
This log statement has two string literals. It might seem like one could convert
move to tokenization:
.. code-block:: cpp
- LOG(INFO) << TOKEN("My temperature is ") << temperature << TOKEN(". State: ") << state;
+ LOG(INFO) << TOKEN("My temperature is ") << temperature << TOKEN(". State: ") << state;
However, this doesn't work. The key problem is that the tokenization system
needs to allocate the string in a linker section that is excluded from the
@@ -376,8 +404,8 @@ string constant, which can be expanded by the preprocessor (as part of
.. code-block:: cpp
- // Note: LOG_INFO can be tokenized behind the macro; transparent to users.
- PW_LOG_INFO("My temperature is %d. State: %s", temperature, state);
+ // Note: LOG_INFO can be tokenized behind the macro; transparent to users.
+ PW_LOG_INFO("My temperature is %d. State: %s", temperature, state);
Additionally, while Pigweed is mostly C++, it a practical reality that at times
projects using Pigweed will need to log from third-party libraries written in
@@ -426,3 +454,35 @@ implementation in ``pw_log/java/main`` simply wraps a
``com.google.common.flogger.FluentLogger``. An implementation that logs to
Android's ``android.util.Log`` instead is provided in
``pw_log/java/android_main``.
+
+----------------
+pw_log in Python
+----------------
+``pw_log`` provides utilities to decode ``LogEntries`` and the encapsulated
+``LogEntry`` proto messages.
+
+The ``Log`` class represents a decoded ``LogEntry`` in a human-readable and
+consumable fashion.
+
+The ``LogStreamDecoder`` offers APIs to decode ``LogEntries`` and ``LogEntry``
+while tracking and logging log drops. It requires a ``decoded_log_handler`` to
+pass decoded logs to. This class can also be customized to use an optional token
+database if the message, module and thread names are tokenized; a custom
+timestamp parser; and optional message parser for any extra message parsing.
+``pw_log`` includes examples for customizing the ``LogStreamDecoder``:
+``timestamp_parser_ns_since_boot`` parses the timestamp number from nanoseconds
+since boot to an HH:MM::SS string, ``log_decoded_log`` emits a decoded ``Log``
+to the provided logger in a format known to ``pw console``, and
+``pw_status_code_to_name`` searches the decoded log message for a matching
+pattern encapsulating the status code number and replaces it with the status
+name.
+
+Python API
+==========
+
+pw_log.log_decoder
+------------------
+.. automodule:: pw_log.log_decoder
+ :members: Log, LogStreamDecoder
+ :undoc-members:
+ :show-inheritance:
diff --git a/pw_log/glog_adapter_test.cc b/pw_log/glog_adapter_test.cc
index d6691cea0..ce386d43f 100644
--- a/pw_log/glog_adapter_test.cc
+++ b/pw_log/glog_adapter_test.cc
@@ -11,7 +11,7 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-// TODO(b/235289499): Add verification of the actually logged statements.
+// TODO: b/235289499 - Add verification of the actually logged statements.
// clang-format off
#define PW_LOG_MODULE_NAME "TST"
diff --git a/pw_log/public/pw_log/options.h b/pw_log/public/pw_log/options.h
index 87936f6db..f43762aec 100644
--- a/pw_log/public/pw_log/options.h
+++ b/pw_log/public/pw_log/options.h
@@ -68,7 +68,7 @@
#endif // PW_LOG_FLAGS
// DEPRECATED: Use PW_LOG_FLAGS.
-// TODO(b/234876701): Remove this macro after migration.
+// TODO: b/234876701 - Remove this macro after migration.
#ifndef PW_LOG_DEFAULT_FLAGS
#define PW_LOG_DEFAULT_FLAGS PW_LOG_FLAGS
#endif // PW_LOG_DEFAULT_FLAGS
diff --git a/pw_log/public/pw_log/tokenized_args.h b/pw_log/public/pw_log/tokenized_args.h
new file mode 100644
index 000000000..06b0d5a0c
--- /dev/null
+++ b/pw_log/public/pw_log/tokenized_args.h
@@ -0,0 +1,52 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+// pw_log backends that use pw_tokenizer and want to support nested tokenization
+// define this file under their public_overrides/ directory to activate the
+// PW_LOG_TOKEN aliases. If this file does not exist in the log backend,
+// arguments behave as basic strings (const char*).
+#if __has_include("pw_log_backend/log_backend_uses_pw_tokenizer.h")
+
+#include "pw_tokenizer/nested_tokenization.h"
+#include "pw_tokenizer/tokenize.h"
+
+#define PW_LOG_TOKEN_TYPE pw_tokenizer_Token
+#define PW_LOG_TOKEN PW_TOKENIZE_STRING
+#define PW_LOG_TOKEN_EXPR PW_TOKENIZE_STRING_EXPR
+#define PW_LOG_TOKEN_FMT PW_TOKEN_FMT
+
+#else
+
+/// If nested tokenization is supported by the logging backend, this is an
+/// alias for `pw_tokenizer_Token`.
+///
+/// For non-tokenizing backends, defaults to `const char*`.
+#define PW_LOG_TOKEN_TYPE const char*
+
+/// If nested tokenization is supported by the logging backend, this is an
+/// alias for `PW_TOKENIZE_STRING`. No-op otherwise.
+#define PW_LOG_TOKEN(string_literal) string_literal
+
+/// If nested tokenization is supported by the logging backend, this is an
+/// alias for `PW_TOKENIZE_STRING_EXPR`. No-op otherwise.
+#define PW_LOG_TOKEN_EXPR(string_literal) string_literal
+
+/// If nested tokenization is supported by the logging backend, this is an
+/// alias for `PW_TOKEN_FORMAT`.
+///
+/// For non-tokenizing backends, defaults to the string specifier `%s`.
+#define PW_LOG_TOKEN_FMT() "%s"
+
+#endif //__has_include("log_backend/log_backend_uses_pw_tokenizer.h")
diff --git a/pw_log/py/BUILD.bazel b/pw_log/py/BUILD.bazel
new file mode 100644
index 000000000..93ea9296b
--- /dev/null
+++ b/pw_log/py/BUILD.bazel
@@ -0,0 +1,44 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_log",
+ srcs = [
+ "pw_log/__init__.py",
+ "pw_log/log_decoder.py",
+ ],
+ imports = ["."],
+ deps = [
+ # TODO: b/241456982 - Add this dep back in
+ # "//pw_log:log_proto_py_pb2",
+ "//pw_log_tokenized/py:pw_log_tokenized",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_status/py:pw_status",
+ "//pw_tokenizer/py:pw_tokenizer",
+ ],
+)
+
+# TODO: b/241456982 - Not expected to build yet.
+py_test(
+ name = "log_decoder_test",
+ srcs = [
+ "log_decoder_test.py",
+ ],
+ tags = ["manual"],
+ deps = [
+ ":pw_log",
+ ],
+)
diff --git a/pw_log/py/BUILD.gn b/pw_log/py/BUILD.gn
new file mode 100644
index 000000000..44c685c5e
--- /dev/null
+++ b/pw_log/py/BUILD.gn
@@ -0,0 +1,52 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+
+pw_python_package("py") {
+ generate_setup = {
+ metadata = {
+ name = "pw_log"
+ version = "0.0.1"
+ }
+ }
+ sources = [
+ "pw_log/__init__.py",
+ "pw_log/log_decoder.py",
+ ]
+ tests = [ "log_decoder_test.py" ]
+ python_test_deps = []
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_log_tokenized/py",
+ "$dir_pw_status/py",
+ "$dir_pw_tokenizer/py",
+ ]
+ proto_library = "..:protos"
+}
+
+# This setup.py may be used to install pw_tokenizer without GN. It does not
+# include the pw_tokenizer.proto subpackage, since it contains a generated
+# protobuf module.
+pw_python_script("setup") {
+ sources = [ "setup.py" ]
+ inputs = [
+ "setup.cfg",
+ "pyproject.toml",
+ ]
+}
diff --git a/pw_log/py/log_decoder_test.py b/pw_log/py/log_decoder_test.py
new file mode 100644
index 000000000..1629aee22
--- /dev/null
+++ b/pw_log/py/log_decoder_test.py
@@ -0,0 +1,869 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""Log decoder tests."""
+
+from dataclasses import dataclass
+import logging
+from random import randint
+from typing import Any, List
+from unittest import TestCase, main
+
+from pw_log.log_decoder import (
+ Log,
+ LogStreamDecoder,
+ log_decoded_log,
+ pw_status_code_to_name,
+ timestamp_parser_ns_since_boot,
+)
+from pw_log.proto import log_pb2
+import pw_tokenizer
+
+_MESSAGE_NO_FILENAME = 'World'
+_MESSAGE_AND_ARGS_NO_FILENAME = f'■msg♦{_MESSAGE_NO_FILENAME}'
+_MESSAGE_TOKEN_NO_FILENAME = pw_tokenizer.tokens.pw_tokenizer_65599_hash(
+ _MESSAGE_AND_ARGS_NO_FILENAME
+)
+
+# Creating a database with tokenized information for the core to detokenize
+# tokenized log entries.
+_TOKEN_DATABASE = pw_tokenizer.tokens.Database(
+ [
+ pw_tokenizer.tokens.TokenizedStringEntry(0x01148A48, 'total_dropped'),
+ pw_tokenizer.tokens.TokenizedStringEntry(
+ 0x03796798, 'min_queue_remaining'
+ ),
+ pw_tokenizer.tokens.TokenizedStringEntry(0x2E668CD6, 'Jello, world!'),
+ pw_tokenizer.tokens.TokenizedStringEntry(0x329481A2, 'parser_errors'),
+ pw_tokenizer.tokens.TokenizedStringEntry(0x7F35A9A5, 'TestName'),
+ pw_tokenizer.tokens.TokenizedStringEntry(0xCC6D3131, 'Jello?'),
+ pw_tokenizer.tokens.TokenizedStringEntry(
+ 0x144C501D, '■msg♦SampleMessage■module♦MODULE■file♦file/path.cc'
+ ),
+ pw_tokenizer.tokens.TokenizedStringEntry(0x0000106A, 'ModuleOrMessage'),
+ pw_tokenizer.tokens.TokenizedStringEntry(
+ pw_tokenizer.tokens.pw_tokenizer_65599_hash(
+ '■msg♦World■module♦wifi■file♦/path/to/file.cc'
+ ),
+ '■msg♦World■module♦wifi■file♦/path/to/file.cc',
+ ),
+ pw_tokenizer.tokens.TokenizedStringEntry(
+ _MESSAGE_TOKEN_NO_FILENAME, _MESSAGE_AND_ARGS_NO_FILENAME
+ ),
+ ]
+)
+_DETOKENIZER = pw_tokenizer.Detokenizer(_TOKEN_DATABASE)
+
+
+def _create_log_entry_with_tokenized_fields(
+ message: str, module: str, file: str, thread: str, line: int, level: int
+) -> log_pb2.LogEntry:
+ """Tokenizing tokenizable LogEntry fields to become a detoknized log."""
+ tokenized_message = pw_tokenizer.encode.encode_token_and_args(
+ pw_tokenizer.tokens.pw_tokenizer_65599_hash(message)
+ )
+ tokenized_module = pw_tokenizer.encode.encode_token_and_args(
+ pw_tokenizer.tokens.pw_tokenizer_65599_hash(module)
+ )
+ tokenized_file = pw_tokenizer.encode.encode_token_and_args(
+ pw_tokenizer.tokens.pw_tokenizer_65599_hash(file)
+ )
+ tokenized_thread = pw_tokenizer.encode.encode_token_and_args(
+ pw_tokenizer.tokens.pw_tokenizer_65599_hash(thread)
+ )
+
+ return log_pb2.LogEntry(
+ message=tokenized_message,
+ module=tokenized_module,
+ file=tokenized_file,
+ line_level=Log.pack_line_level(line, level),
+ thread=tokenized_thread,
+ )
+
+
+def _create_random_log_entry() -> log_pb2.LogEntry:
+ return log_pb2.LogEntry(
+ message=bytes(f'message {randint(1,100)}'.encode('utf-8')),
+ line_level=Log.pack_line_level(
+ randint(0, 2000), randint(logging.DEBUG, logging.CRITICAL)
+ ),
+ file=b'main.cc',
+ thread=bytes(f'thread {randint(1,5)}'.encode('utf-8')),
+ )
+
+
+def _create_drop_count_message_log_entry(
+ drop_count: int, reason: str = ''
+) -> log_pb2.LogEntry:
+ log_entry = log_pb2.LogEntry(dropped=drop_count)
+ if reason:
+ log_entry.message = bytes(reason.encode('utf-8'))
+ return log_entry
+
+
+class TestLogStreamDecoderBase(TestCase):
+ """Base Test class for LogStreamDecoder."""
+
+ def setUp(self) -> None:
+ """Set up logs decoder."""
+
+ def parse_pw_status(msg: str) -> str:
+ return pw_status_code_to_name(msg)
+
+ self.captured_logs: List[Log] = []
+
+ def decoded_log_handler(log: Log) -> None:
+ self.captured_logs.append(log)
+
+ self.decoder = LogStreamDecoder(
+ decoded_log_handler=decoded_log_handler,
+ detokenizer=_DETOKENIZER,
+ source_name='source',
+ timestamp_parser=timestamp_parser_ns_since_boot,
+ message_parser=parse_pw_status,
+ )
+
+ def _captured_logs_as_str(self) -> str:
+ return '\n'.join(map(str, self.captured_logs))
+
+
+class TestLogStreamDecoderDecodingFunctionality(TestLogStreamDecoderBase):
+ """Tests LogStreamDecoder decoding functionality."""
+
+ def test_parse_log_entry_valid_non_tokenized(self) -> None:
+ """Test that valid LogEntry protos are parsed correctly."""
+ expected_log = Log(
+ message='Hello',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Hello',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(result, expected_log)
+
+ def test_parse_log_entry_valid_packed_message(self) -> None:
+ """Test that valid LogEntry protos are parsed correctly."""
+ log_with_metadata_in_message = Log(
+ message='World',
+ file_and_line='/path/to/file.cc',
+ level=logging.DEBUG,
+ source_name=self.decoder.source_name,
+ module_name='wifi',
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=bytes(
+ '■msg♦World■module♦wifi■file♦/path/to/file.cc'.encode(
+ 'utf-8'
+ )
+ ),
+ line_level=Log.pack_line_level(0, logging.DEBUG),
+ timestamp=100,
+ )
+ )
+ self.assertEqual(result, log_with_metadata_in_message)
+
+ def test_parse_log_entry_valid_logs_drop_message(self) -> None:
+ """Test that valid LogEntry protos are parsed correctly."""
+ dropped_message = Log(
+ message='Dropped 30 logs due to buffer too small',
+ level=logging.WARNING,
+ source_name=self.decoder.source_name,
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(message=b'buffer too small', dropped=30)
+ )
+ self.assertEqual(result, dropped_message)
+
+ def test_parse_log_entry_valid_tokenized(self) -> None:
+ """Test that tokenized LogEntry protos are parsed correctly."""
+ message = 'Jello, world!'
+ module_name = 'TestName'
+ file = 'parser_errors'
+ thread_name = 'Jello?'
+ line = 123
+ level = logging.INFO
+
+ expected_log = Log(
+ message=message,
+ module_name=module_name,
+ file_and_line=file + ':123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ thread_name=thread_name,
+ )
+
+ log_entry = _create_log_entry_with_tokenized_fields(
+ message, module_name, file, thread_name, line, level
+ )
+ result = self.decoder.parse_log_entry_proto(log_entry)
+ self.assertEqual(result, expected_log, msg='Log was not detokenized')
+
+ def test_tokenized_contents_not_detokenized(self):
+ """Test fields with tokens not in the database are not decrypted."""
+ # The following strings do not have tokens in the device token db.
+ message_not_in_db = 'device is shutting down.'
+ module_name_not_in_db = 'Battery'
+ file_not_in_db = 'charger.cc'
+ thread_name_not_in_db = 'BatteryStatus'
+ line = 123
+ level = logging.INFO
+
+ log_entry = _create_log_entry_with_tokenized_fields(
+ message_not_in_db,
+ module_name_not_in_db,
+ file_not_in_db,
+ thread_name_not_in_db,
+ line,
+ level,
+ )
+ message = pw_tokenizer.proto.decode_optionally_tokenized(
+ _DETOKENIZER, log_entry.message
+ )
+ module = pw_tokenizer.proto.decode_optionally_tokenized(
+ _DETOKENIZER, log_entry.module
+ )
+ file = pw_tokenizer.proto.decode_optionally_tokenized(
+ _DETOKENIZER, log_entry.file
+ )
+ thread = pw_tokenizer.proto.decode_optionally_tokenized(
+ _DETOKENIZER, log_entry.thread
+ )
+ expected_log = Log(
+ message=message,
+ module_name=module,
+ file_and_line=file + ':123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ thread_name=thread,
+ )
+ result = self.decoder.parse_log_entry_proto(log_entry)
+ self.assertEqual(
+ result, expected_log, msg='Log was unexpectedly detokenized'
+ )
+
+ def test_extracting_log_entry_fields_from_tokenized_metadata(self):
+ """Test that tokenized metadata can be used to extract other fields."""
+ metadata = '■msg♦World■module♦wifi■file♦/path/to/file.cc'
+ thread_name = 'M0Log'
+
+ log_entry = log_pb2.LogEntry(
+ message=pw_tokenizer.encode.encode_token_and_args(
+ pw_tokenizer.tokens.pw_tokenizer_65599_hash(metadata)
+ ),
+ line_level=Log.pack_line_level(0, logging.DEBUG),
+ thread=bytes(thread_name.encode('utf-8')),
+ )
+
+ log_with_metadata_in_message = Log(
+ message='World',
+ file_and_line='/path/to/file.cc',
+ level=logging.DEBUG,
+ source_name=self.decoder.source_name,
+ module_name='wifi',
+ timestamp='0:00',
+ thread_name=thread_name,
+ )
+
+ result = self.decoder.parse_log_entry_proto(log_entry)
+ self.assertEqual(
+ result, log_with_metadata_in_message, msg='Log was detokenized.'
+ )
+
+ def test_extracting_status_argument_from_log_message(self):
+ """Test extract status from log message."""
+ expected_log = Log(
+ message='Could not start flux capacitor: PERMISSION_DENIED',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Status=7',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ expected_log = Log(
+ message='Error connecting to server: UNAVAILABLE',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Error connecting to server: pw::Status=14',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_extracting_status_with_improper_spacing(self):
+ """Test spaces before pw::Status are ignored."""
+ expected_log = Log(
+ message='Error connecting to server:UNAVAILABLE',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Error connecting to server:pw::Status=14',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ expected_log = Log(
+ message='Error connecting to server: UNAVAILABLE',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Error connecting to server: pw::Status=14',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_not_extracting_status_extra_space_before_code(self):
+ """Test spaces after pw::Status are not allowed."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::Status= 7',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Status= 7',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_not_extracting_status_new_line_before_code(self):
+ """Test new line characters after pw::Status are not allowed."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::Status=\n7',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Status=\n7',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_not_extracting_status_from_log_message_with_improper_format(self):
+ """Test status not extracted from log message with incorrect format."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::Status12',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Status12',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_status_code_in_message_does_not_exist(self):
+ """Test status does not exist in pw_status."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::Status=17',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Status=17',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_status_code_in_message_is_negative(self):
+ """Test status code is negative."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::Status=-1',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Status=-1',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_status_code_is_name(self):
+ """Test if the status code format includes the name instead."""
+ expected_log = Log(
+ message='Cannot use flux capacitor: pw::Status=PERMISSION_DENIED',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=(
+ b'Cannot use flux capacitor: pw::Status=PERMISSION_DENIED'
+ ),
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_spelling_mistakes_with_status_keyword(self):
+ """Test spelling mistakes with status keyword."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::Rtatus=12',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::Rtatus=12',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_spelling_mistakes_with_status_keyword_lowercase_s(self):
+ """Test spelling mistakes with status keyword."""
+ expected_log = Log(
+ message='Could not start flux capacitor: pw::status=13',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Could not start flux capacitor: pw::status=13',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_status_code_at_beginning_of_message(self):
+ """Test embedded status argument is found."""
+ expected_log = Log(
+ message='UNAVAILABLE to connect to server.',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'pw::Status=14 to connect to server.',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_status_code_in_the_middle_of_message(self):
+ """Test embedded status argument is found."""
+ expected_log = Log(
+ message='Connection error: UNAVAILABLE connecting to server.',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=(
+ b'Connection error: pw::Status=14 connecting to server.'
+ ),
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_status_code_with_no_surrounding_spaces(self):
+ """Test embedded status argument is found."""
+ expected_log = Log(
+ message='Connection error:UNAVAILABLEconnecting to server.',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Connection error:pw::Status=14connecting to server.',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_multiple_status_arguments_in_log_message(self):
+ """Test replacement of multiple status arguments into status string."""
+ expected_log = Log(
+ message='Connection error: UNAVAILABLE and PERMISSION_DENIED.',
+ file_and_line='my/path/file.cc:123',
+ level=logging.INFO,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ )
+ result = self.decoder.parse_log_entry_proto(
+ log_pb2.LogEntry(
+ message=b'Connection error: pw::Status=14 and pw::Status=7.',
+ file=b'my/path/file.cc',
+ line_level=Log.pack_line_level(123, logging.INFO),
+ )
+ )
+ self.assertEqual(
+ result,
+ expected_log,
+ msg='Status was not extracted from log message.',
+ )
+
+ def test_no_filename_in_message_parses_successfully(self):
+ """Test that if the file name is not present the log entry is parsed."""
+ thread_name = 'thread'
+
+ log_entry = log_pb2.LogEntry(
+ message=pw_tokenizer.encode.encode_token_and_args(
+ _MESSAGE_TOKEN_NO_FILENAME
+ ),
+ line_level=Log.pack_line_level(123, logging.DEBUG),
+ thread=bytes(thread_name.encode('utf-8')),
+ )
+ expected_log = Log(
+ message=_MESSAGE_NO_FILENAME,
+ file_and_line=':123',
+ level=logging.DEBUG,
+ source_name=self.decoder.source_name,
+ timestamp='0:00',
+ thread_name=thread_name,
+ )
+ result = self.decoder.parse_log_entry_proto(log_entry)
+ self.assertEqual(result, expected_log)
+
+ def test_log_decoded_log(self):
+ """Test that the logger correctly formats a decoded log."""
+ test_log = Log(
+ message="SampleMessage",
+ level=logging.DEBUG,
+ timestamp='1:30',
+ module_name="MyModule",
+ source_name="MySource",
+ thread_name="MyThread",
+ file_and_line='my_file.cc:123',
+ metadata_fields={'field1': 432, 'field2': 'value'},
+ )
+
+ class CapturingLogger(logging.Logger):
+ """Captures values passed to log().
+
+ Tests that calls to log() have the correct level, extra arguments
+ and the message format string and arguments match.
+ """
+
+ @dataclass(frozen=True)
+ class LoggerLog:
+ """Represents a process log() call."""
+
+ level: int
+ message: str
+ kwargs: Any
+
+ def __init__(self):
+ super().__init__(name="CapturingLogger")
+ self.log_calls: List[CapturingLogger.LoggerLog] = []
+
+ def log(self, level, msg, *args, **kwargs) -> None:
+ log = CapturingLogger.LoggerLog(
+ level=level, message=msg % args, kwargs=kwargs
+ )
+ self.log_calls.append(log)
+
+ test_logger = CapturingLogger()
+ log_decoded_log(test_log, test_logger)
+ self.assertEqual(len(test_logger.log_calls), 1)
+ self.assertEqual(test_logger.log_calls[0].level, test_log.level)
+ self.assertEqual(
+ test_logger.log_calls[0].message,
+ '[%s] %s %s %s %s'
+ % (
+ test_log.source_name,
+ test_log.module_name,
+ test_log.timestamp,
+ test_log.message,
+ test_log.file_and_line,
+ ),
+ )
+ self.assertEqual(
+ test_logger.log_calls[0].kwargs['extra']['extra_metadata_fields'],
+ test_log.metadata_fields,
+ )
+
+
+class TestLogStreamDecoderLogDropDetectionFunctionality(
+ TestLogStreamDecoderBase
+):
+ """Tests LogStreamDecoder log drop detection functionality."""
+
+ def test_log_drops_transport_error(self):
+ """Tests log drops at transport."""
+ log_entry_in_log_entries_1 = 3
+ log_entries_1 = log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ _create_random_log_entry()
+ for _ in range(log_entry_in_log_entries_1)
+ ],
+ )
+ self.decoder.parse_log_entries_proto(log_entries_1)
+ # Assume a second LogEntries was dropped with 5 log entries. I.e. a
+ # log_entries_2 with sequence_ie = 4 and 5 log entries.
+ log_entry_in_log_entries_2 = 5
+ log_entry_in_log_entries_3 = 3
+ log_entries_3 = log_pb2.LogEntries(
+ first_entry_sequence_id=log_entry_in_log_entries_2
+ + log_entry_in_log_entries_3,
+ entries=[
+ _create_random_log_entry()
+ for _ in range(log_entry_in_log_entries_3)
+ ],
+ )
+ self.decoder.parse_log_entries_proto(log_entries_3)
+
+ # The drop message is placed where the dropped logs were detected.
+ self.assertEqual(
+ len(self.captured_logs),
+ log_entry_in_log_entries_1 + 1 + log_entry_in_log_entries_3,
+ msg=(
+ 'Unexpected number of messages received: '
+ f'{self._captured_logs_as_str()}'
+ ),
+ )
+ self.assertEqual(
+ (
+ f'Dropped {log_entry_in_log_entries_2} logs due to '
+ f'{LogStreamDecoder.DROP_REASON_LOSS_AT_TRANSPORT}'
+ ),
+ self.captured_logs[log_entry_in_log_entries_1].message,
+ )
+
+ def test_log_drops_source_not_connected(self):
+ """Tests log drops when source of the logs was not connected."""
+ log_entry_in_log_entries = 4
+ drop_count = 7
+ log_entries = log_pb2.LogEntries(
+ first_entry_sequence_id=drop_count,
+ entries=[
+ _create_random_log_entry()
+ for _ in range(log_entry_in_log_entries)
+ ],
+ )
+ self.decoder.parse_log_entries_proto(log_entries)
+
+ # The drop message is placed where the log drops was detected.
+ self.assertEqual(
+ len(self.captured_logs),
+ 1 + log_entry_in_log_entries,
+ msg=(
+ 'Unexpected number of messages received: '
+ f'{self._captured_logs_as_str()}'
+ ),
+ )
+ self.assertEqual(
+ (
+ f'Dropped {drop_count} logs due to '
+ f'{LogStreamDecoder.DROP_REASON_SOURCE_NOT_CONNECTED}'
+ ),
+ self.captured_logs[0].message,
+ )
+
+ def test_log_drops_source_enqueue_failure_no_message(self):
+ """Tests log drops when source reports log drops."""
+ drop_count = 5
+ log_entries = log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ _create_random_log_entry(),
+ _create_random_log_entry(),
+ _create_drop_count_message_log_entry(drop_count),
+ _create_random_log_entry(),
+ ],
+ )
+ self.decoder.parse_log_entries_proto(log_entries)
+
+ # The drop message is placed where the log drops was detected.
+ self.assertEqual(
+ len(self.captured_logs),
+ 4,
+ msg=(
+ 'Unexpected number of messages received: '
+ f'{self._captured_logs_as_str()}'
+ ),
+ )
+ self.assertEqual(
+ (
+ f'Dropped {drop_count} logs due to '
+ f'{LogStreamDecoder.DROP_REASON_SOURCE_ENQUEUE_FAILURE}'
+ ),
+ self.captured_logs[2].message,
+ )
+
+ def test_log_drops_source_enqueue_failure_with_message(self):
+ """Tests log drops when source reports log drops."""
+ drop_count = 8
+ reason = 'Flux Capacitor exploded'
+ log_entries = log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ _create_random_log_entry(),
+ _create_random_log_entry(),
+ _create_drop_count_message_log_entry(drop_count, reason),
+ _create_random_log_entry(),
+ ],
+ )
+ self.decoder.parse_log_entries_proto(log_entries)
+
+ # The drop message is placed where the log drops was detected.
+ self.assertEqual(
+ len(self.captured_logs),
+ 4,
+ msg=(
+ 'Unexpected number of messages received: '
+ f'{self._captured_logs_as_str()}'
+ ),
+ )
+ self.assertEqual(
+ f'Dropped {drop_count} logs due to {reason.lower()}',
+ self.captured_logs[2].message,
+ )
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_log/py/pw_log/__init__.py b/pw_log/py/pw_log/__init__.py
new file mode 100644
index 000000000..c3e1bdbd5
--- /dev/null
+++ b/pw_log/py/pw_log/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
diff --git a/pw_log/py/pw_log/log_decoder.py b/pw_log/py/pw_log/log_decoder.py
new file mode 100644
index 000000000..c50846e50
--- /dev/null
+++ b/pw_log/py/pw_log/log_decoder.py
@@ -0,0 +1,413 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""Utils to decode logs."""
+
+from dataclasses import dataclass
+import datetime
+import logging
+import re
+from typing import Any, Callable, Dict, Optional
+
+from pw_log.proto import log_pb2
+import pw_log_tokenized
+import pw_status
+from pw_tokenizer import Detokenizer
+from pw_tokenizer.proto import decode_optionally_tokenized
+
+_LOG = logging.getLogger(__name__)
+
+
+@dataclass(frozen=True)
+class LogLineLevel:
+ """Tuple of line number and level packed in LogEntry."""
+
+ line: int
+ level: int
+
+
+class Log:
+ """A decoded, human-readable representation of a LogEntry.
+
+ Contains fields to represent a decoded pw_log/log.proto LogEntry message in
+ a human readable way.
+
+ Attributes:
+ message: The log message as a string.
+ level: A integer representing the log level, follows logging levels.
+ flags: An integer with the bit flags.
+ timestamp: A string representation of a timestamp.
+ module_name: The module name as a string.
+ thread_name: The thread name as a string.
+ source_name: The source name as a string.
+ file_and_line: The filepath and line as a string.
+ metadata_fields: Extra fields with string-string mapping.
+ """
+
+ _LOG_LEVEL_NAMES = {
+ logging.DEBUG: 'DBG',
+ logging.INFO: 'INF',
+ logging.WARNING: 'WRN',
+ logging.ERROR: 'ERR',
+ logging.CRITICAL: 'CRT',
+ # The logging module does not have a FATAL level. This module's log
+ # level values are 10 * PW_LOG_LEVEL values. PW_LOG_LEVEL_FATAL is 7.
+ 70: 'FTL',
+ }
+ _LEVEL_MASK = 0x7 # pylint: disable=C0103
+ _LINE_OFFSET = 3 # pylint: disable=C0103
+
+ def __init__(
+ self,
+ message: str = '',
+ level: int = logging.NOTSET,
+ flags: int = 0,
+ timestamp: str = '',
+ module_name: str = '',
+ thread_name: str = '',
+ source_name: str = '',
+ file_and_line: str = '',
+ metadata_fields: Optional[Dict[str, str]] = None,
+ ) -> None:
+ self.message = message
+ self.level = level # Value from logging levels.
+ self.flags = flags
+ self.timestamp = timestamp # A human readable value.
+ self.module_name = module_name
+ self.thread_name = thread_name
+ self.source_name = source_name
+ self.file_and_line = file_and_line
+ self.metadata_fields = dict()
+ if metadata_fields:
+ self.metadata_fields.update(metadata_fields)
+
+ def __eq__(self, other: Any) -> bool:
+ if not isinstance(other, Log):
+ return False
+
+ return (
+ self.message == other.message
+ and self.level == other.level
+ and self.flags == other.flags
+ and self.timestamp == other.timestamp
+ and self.module_name == other.module_name
+ and self.thread_name == other.thread_name
+ and self.source_name == other.source_name
+ and self.file_and_line == other.file_and_line
+ and self.metadata_fields == other.metadata_fields
+ )
+
+ def __repr__(self) -> str:
+ return self.__str__()
+
+ def __str__(self) -> str:
+ level_name = self._LOG_LEVEL_NAMES.get(self.level, '')
+ metadata = ' '.join(map(str, self.metadata_fields.values()))
+ return (
+ f'{level_name} [{self.source_name}] {self.module_name} '
+ f'{self.timestamp} {self.message} {self.file_and_line} '
+ f'{metadata}'
+ ).strip()
+
+ @staticmethod
+ def pack_line_level(line: int, logging_log_level: int) -> int:
+ """Packs the line and level values into an integer as done in LogEntry.
+
+ Args:
+ line: the line number
+ level: the logging level using logging levels.
+ Returns:
+ An integer with the two values bitpacked.
+ """
+ return (line << Log._LINE_OFFSET) | (
+ Log.logging_level_to_pw_log_level(logging_log_level)
+ & Log._LEVEL_MASK
+ )
+
+ @staticmethod
+ def unpack_line_level(packed_line_level: int) -> LogLineLevel:
+ """Unpacks the line and level values packed as done in LogEntry.
+
+ Args:
+ An integer with the two values bitpacked.
+ Returns:
+ line: the line number
+ level: the logging level using logging levels.
+ """
+ line = packed_line_level >> Log._LINE_OFFSET
+ # Convert to logging module level number.
+ level = Log.pw_log_level_to_logging_level(
+ packed_line_level & Log._LEVEL_MASK
+ )
+ return LogLineLevel(line=line, level=level)
+
+ @staticmethod
+ def pw_log_level_to_logging_level(pw_log_level: int) -> int:
+ """Maps a pw_log/levels.h value to Python logging level value."""
+ return pw_log_level * 10
+
+ @staticmethod
+ def logging_level_to_pw_log_level(logging_log_level: int) -> int:
+ """Maps a Python logging level value to a pw_log/levels.h value."""
+ return int(logging_log_level / 10)
+
+
+def pw_status_code_to_name(
+ message: str, status_pattern: str = 'pw::Status=([0-9]+)'
+) -> str:
+ """Replaces the pw::Status code number with the status name.
+
+ Searches for a matching pattern encapsulating the status code number and
+ replaces it with the status name. This is useful when logging the status
+ code instead of the string to save space.
+
+ Args:
+ message: The string to look for the status code.
+ status_pattern: The regex pattern describing the format encapsulating
+ the status code.
+ Returns:
+ The message string with the status name if found, else the original
+ string.
+ """
+ # Min and max values for Status.
+ max_status_value = max(status.value for status in pw_status.Status)
+ min_status_value = min(status.value for status in pw_status.Status)
+
+ def replacement_callback(match: re.Match) -> str:
+ status_code = int(match.group(1))
+ if min_status_value <= status_code <= max_status_value:
+ return pw_status.Status(status_code).name
+ return match.group(0)
+
+ return re.sub(
+ pattern=status_pattern, repl=replacement_callback, string=message
+ )
+
+
+def log_decoded_log(log: Log, logger: logging.Logger) -> None:
+ """Formats and saves the log information in a pw console compatible way.
+
+ Arg:
+ logger: The logger to emit the log information to.
+ """
+ # Fields used for pw console table view.
+ log.metadata_fields['module'] = log.module_name
+ log.metadata_fields['source_name'] = log.source_name
+ log.metadata_fields['timestamp'] = log.timestamp
+ log.metadata_fields['msg'] = log.message
+ log.metadata_fields['file'] = log.file_and_line
+
+ logger.log(
+ log.level,
+ '[%s] %s %s %s %s',
+ log.source_name,
+ log.module_name,
+ log.timestamp,
+ log.message,
+ log.file_and_line,
+ extra=dict(extra_metadata_fields=log.metadata_fields),
+ )
+
+
+def timestamp_parser_ns_since_boot(timestamp: int) -> str:
+ """Decodes timestamp as nanoseconds since boot.
+
+ Args:
+ timestamp: The timestamp as an integer.
+ Returns:
+ A string representation of the timestamp.
+ """
+ return str(datetime.timedelta(seconds=timestamp / 1e9))[:-3]
+
+
+class LogStreamDecoder:
+ """Decodes an RPC stream of LogEntries packets.
+
+ Performs log drop detection on the stream of LogEntries proto messages.
+
+ Args:
+ decoded_log_handler: Callback called on each decoded log.
+ detokenizer: Detokenizes log messages if tokenized when provided.
+ source_name: Optional string to identify the logs source.
+ timestamp_parser: Optional timestamp parser number to a string.
+ message_parser: Optional message parser called after detokenization is
+ attempted on a log message.
+ """
+
+ DROP_REASON_LOSS_AT_TRANSPORT = 'loss at transport'
+ DROP_REASON_SOURCE_NOT_CONNECTED = 'source not connected'
+ DROP_REASON_SOURCE_ENQUEUE_FAILURE = 'enqueue failure at source'
+
+ def __init__(
+ self,
+ decoded_log_handler: Callable[[Log], None],
+ detokenizer: Optional[Detokenizer] = None,
+ source_name: str = '',
+ timestamp_parser: Optional[Callable[[int], str]] = None,
+ message_parser: Optional[Callable[[str], str]] = None,
+ ):
+ self.decoded_log_handler = decoded_log_handler
+ self.detokenizer = detokenizer
+ self.source_name = source_name
+ self.timestamp_parser = timestamp_parser
+ self.message_parser = message_parser
+ self._expected_log_sequence_id = 0
+
+ def parse_log_entries_proto(
+ self, log_entries_proto: log_pb2.LogEntries
+ ) -> None:
+ """Parses each LogEntry in log_entries_proto.
+
+ Args:
+ log_entry_proto: A LogEntry message proto.
+ Returns:
+ A Log object with the decoded log_entry_proto.
+ """
+ has_received_logs = self._expected_log_sequence_id > 0
+ dropped_log_count = self._calculate_dropped_logs(log_entries_proto)
+ if dropped_log_count > 0:
+ reason = (
+ self.DROP_REASON_LOSS_AT_TRANSPORT
+ if has_received_logs
+ else self.DROP_REASON_SOURCE_NOT_CONNECTED
+ )
+ self.decoded_log_handler(
+ self._handle_log_drop_count(dropped_log_count, reason)
+ )
+ elif dropped_log_count < 0:
+ _LOG.error('Log sequence ID is smaller than expected')
+
+ for i, log_entry_proto in enumerate(log_entries_proto.entries):
+ # Handle dropped count first.
+ if log_entry_proto.dropped:
+ # Avoid duplicating drop reports since the device will report
+ # a drop count due to a transmission failure, of the last
+ # attempted transmission only, in the first entry of the next
+ # successful transmission.
+ if i == 0 and dropped_log_count >= log_entry_proto.dropped:
+ continue
+ parsed_log = self.parse_log_entry_proto(log_entry_proto)
+ self.decoded_log_handler(parsed_log)
+
+ def parse_log_entry_proto(self, log_entry_proto: log_pb2.LogEntry) -> Log:
+ """Parses the log_entry_proto contents into a human readable format.
+
+ Args:
+ log_entry_proto: A LogEntry message proto.
+ Returns:
+ A Log object with the decoded log_entry_proto.
+ """
+ detokenized_message = self._decode_optionally_tokenized_field(
+ log_entry_proto.message
+ )
+ # Handle dropped count first.
+ if log_entry_proto.dropped:
+ drop_reason = self.DROP_REASON_SOURCE_ENQUEUE_FAILURE
+ if detokenized_message:
+ drop_reason = detokenized_message.lower()
+ return self._handle_log_drop_count(
+ log_entry_proto.dropped, drop_reason
+ )
+
+ # Parse message and metadata, if any, encoded in a key-value format as
+ # described in pw_log/log.proto LogEntry::message field.
+ message_and_metadata = pw_log_tokenized.FormatStringWithMetadata(
+ detokenized_message
+ )
+ module_name = self._decode_optionally_tokenized_field(
+ log_entry_proto.module
+ )
+ if not module_name:
+ module_name = message_and_metadata.module
+
+ line_level_tuple = Log.unpack_line_level(log_entry_proto.line_level)
+ file_and_line = self._decode_optionally_tokenized_field(
+ log_entry_proto.file
+ )
+ if not file_and_line:
+ # Set file name if found in the metadata.
+ if message_and_metadata.file is not None:
+ file_and_line = message_and_metadata.file
+ else:
+ file_and_line = ''
+
+ # Add line number to filepath if needed.
+ if line_level_tuple.line and ':' not in file_and_line:
+ file_and_line += f':{line_level_tuple.line}'
+ # Add extra log information avoiding duplicated data.
+ metadata_fields = {
+ k: v
+ for k, v in message_and_metadata.fields.items()
+ if k not in ['file', 'module', 'msg']
+ }
+
+ message = message_and_metadata.message
+ if self.message_parser:
+ message = self.message_parser(message)
+ if self.timestamp_parser:
+ timestamp = self.timestamp_parser(log_entry_proto.timestamp)
+ else:
+ timestamp = str(log_entry_proto.timestamp)
+ log = Log(
+ message=message,
+ level=line_level_tuple.level,
+ flags=log_entry_proto.flags,
+ timestamp=timestamp,
+ module_name=module_name,
+ thread_name=self._decode_optionally_tokenized_field(
+ log_entry_proto.thread
+ ),
+ source_name=self.source_name,
+ file_and_line=file_and_line,
+ metadata_fields=metadata_fields,
+ )
+
+ return log
+
+ def _handle_log_drop_count(self, drop_count: int, reason: str) -> Log:
+ log_word = 'logs' if drop_count > 1 else 'log'
+ log = Log(
+ message=f'Dropped {drop_count} {log_word} due to {reason}',
+ level=logging.WARNING,
+ source_name=self.source_name,
+ )
+ return log
+
+ def _calculate_dropped_logs(
+ self, log_entries_proto: log_pb2.LogEntries
+ ) -> int:
+ # Count log messages received that don't use the dropped field.
+ messages_received = sum(
+ 1 if not log_proto.dropped else 0
+ for log_proto in log_entries_proto.entries
+ )
+ dropped_log_count = (
+ log_entries_proto.first_entry_sequence_id
+ - self._expected_log_sequence_id
+ )
+ self._expected_log_sequence_id = (
+ log_entries_proto.first_entry_sequence_id + messages_received
+ )
+ return dropped_log_count
+
+ def _decode_optionally_tokenized_field(self, field: bytes) -> str:
+ """Decodes tokenized field into a printable string.
+ Args:
+ field: Bytes.
+ Returns:
+ A printable string.
+ """
+ if self.detokenizer:
+ return decode_optionally_tokenized(self.detokenizer, field)
+ return field.decode('utf-8')
diff --git a/pw_log/py/pyproject.toml b/pw_log/py/pyproject.toml
new file mode 100644
index 000000000..78668a709
--- /dev/null
+++ b/pw_log/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_log/py/setup.cfg b/pw_log/py/setup.cfg
new file mode 100644
index 000000000..e157974ad
--- /dev/null
+++ b/pw_log/py/setup.cfg
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[metadata]
+name = pw_log
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Tools for working with log protos
+
+[options]
+packages = pw_log
+
+[options.package_data]
+pw_log = py.typed
diff --git a/pw_log/tokenized_args.rst b/pw_log/tokenized_args.rst
new file mode 100644
index 000000000..d88910579
--- /dev/null
+++ b/pw_log/tokenized_args.rst
@@ -0,0 +1,76 @@
+.. _module-pw_log-tokenized-args:
+
+-----------------------
+Tokenized log arguments
+-----------------------
+The ``pw_log`` facade is intended to make the logging backend invisible to the
+user, but some backend features require additional frontend support,
+necessitating a break in the abstraction. One of these features is the logging
+of nested token arguments in tokenized logs, because only the user is able to
+know which log arguments can be tokens (see :ref:`module-pw_tokenizer` for
+context on tokenization, and :ref:`module-pw_log_tokenized` for an example of
+a tokenized logging backend). Arguments that have already been tokenized are
+just unsigned integers, and not all strings can be compile-time constants, so
+users must be provided with a way of manually marking token arguments.
+
+To this end, ``pw_log/tokenized_args.h`` aliases macros from ``pw_tokenizer``
+to enable logging nested tokens when the active backend uses tokenization.
+These alias macros revert to plain string logging otherwise, allowing projects
+to take advantage of nested token logging without breaking readable logs when
+the project is built with a different logging backend. To support logging
+nested token arguments, a ``pw_log`` backend must add an empty file
+``log_backend_uses_pw_tokenizer.h`` under ``public_overrides/pw_log_backend/``.
+
+Although the detokenizing backend accepts several different numeric bases, the
+macros currently only support formatting nested tokens in hexadecimal, which
+affects how the arguments appear in final logs if they cannot be detokenized
+for any reason. Undetokenized tokens will appear inline as hex integers
+prefixed with ``$#``, e.g. ``$#34d16466``.
+
+.. doxygendefine:: PW_LOG_TOKEN_TYPE
+.. doxygendefine:: PW_LOG_TOKEN
+.. doxygendefine:: PW_LOG_TOKEN_EXPR
+.. doxygendefine:: PW_LOG_TOKEN_FMT
+
+Example usage with inline string arguments:
+
+.. code-block:: cpp
+
+ #include "pw_log/log.h"
+ #include "pw_log/tokenized_args.h"
+
+ // bool active_
+ PW_LOG_INFO("Component is " PW_LOG_TOKEN_FMT(),
+ active_ ? PW_LOG_TOKEN_EXPR("active")
+ : PW_LOG_TOKEN_EXPR("idle"));
+
+Example usage with enums:
+
+.. code-block:: cpp
+
+ #include "pw_log/log.h"
+ #include "pw_log/tokenized_args.h"
+
+ namespace foo {
+
+ enum class Color { kRed, kGreen, kBlue };
+
+ PW_LOG_TOKEN_TYPE ColorToToken(Color color) {
+ switch (color) {
+ case Color::kRed:
+ return PW_LOG_TOKEN_EXPR("kRed");
+ case Color::kGreen:
+ return PW_LOG_TOKEN_EXPR("kGreen");
+ case Color::kBlue:
+ return PW_LOG_TOKEN_EXPR("kBlue");
+ default:
+ return PW_LOG_TOKEN_EXPR("kUnknown");
+ }
+ }
+
+ } // namespace foo
+
+ void LogColor(foo::Color color) {
+ PW_LOG("Color: [" PW_LOG_TOKEN_FMT() "]", color)
+ }
+
diff --git a/pw_log_basic/Android.bp b/pw_log_basic/Android.bp
new file mode 100644
index 000000000..fa9bd443d
--- /dev/null
+++ b/pw_log_basic/Android.bp
@@ -0,0 +1,40 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_log_basic_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ "public_overrides",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+cc_defaults {
+ name: "pw_log_basic_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_log_basic_include_dirs",
+ ],
+
+ export_header_lib_headers: [
+ "pw_log_basic_include_dirs",
+ ],
+}
diff --git a/pw_log_basic/BUILD.bazel b/pw_log_basic/BUILD.bazel
index 1b21fc886..89e40fb54 100644
--- a/pw_log_basic/BUILD.bazel
+++ b/pw_log_basic/BUILD.bazel
@@ -49,3 +49,9 @@ pw_cc_library(
"//pw_sys_io",
],
)
+
+# The impl of pw_log_basic is an empty library: it's so basic that there's no
+# risk of circular dependencies.
+cc_library(
+ name = "impl",
+)
diff --git a/pw_log_basic/log_basic.cc b/pw_log_basic/log_basic.cc
index cde9ededc..05bae375b 100644
--- a/pw_log_basic/log_basic.cc
+++ b/pw_log_basic/log_basic.cc
@@ -85,7 +85,7 @@ const char* GetFileBasename(const char* filename) {
void (*write_log)(std::string_view) = [](std::string_view log) {
sys_io::WriteLine(log)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
};
} // namespace
diff --git a/pw_log_basic/public/pw_log_basic/log_basic.h b/pw_log_basic/public/pw_log_basic/log_basic.h
index a2c0e7889..cc3f68d32 100644
--- a/pw_log_basic/public/pw_log_basic/log_basic.h
+++ b/pw_log_basic/public/pw_log_basic/log_basic.h
@@ -38,7 +38,7 @@ PW_EXTERN_C_END
// arguments. Additionally, the use of the __FUNC__ macro adds a static const
// char[] variable inside functions with a log.
//
-// TODO(b/235289435): Reconsider the naming of this module when more is in
+// TODO: b/235289435 - Reconsider the naming of this module when more is in
// place.
#define PW_HANDLE_LOG(level, module, flags, message, ...) \
do { \
diff --git a/pw_log_rpc/BUILD.gn b/pw_log_rpc/BUILD.gn
index 5b62508b3..e23e0f101 100644
--- a/pw_log_rpc/BUILD.gn
+++ b/pw_log_rpc/BUILD.gn
@@ -32,6 +32,12 @@ config("public_include_path") {
visibility = [ ":*" ]
}
+# TODO: b/297280281 - Windows+gcc+mingw-w64 thinks that some variables are not
+# always initialized. This is a toolchain bug, not a real issue.
+config("disable_warning") {
+ cflags = [ "-Wno-maybe-uninitialized" ]
+}
+
pw_source_set("config") {
sources = [ "public/pw_log_rpc/internal/config.h" ]
public_configs = [ ":public_include_path" ]
@@ -139,9 +145,15 @@ pw_source_set("rpc_log_drain_thread") {
"$dir_pw_sync:timed_thread_notification",
"$dir_pw_thread:thread",
]
+ if (current_os == "win") {
+ public_configs += [ ":disable_warning" ]
+ }
}
pw_source_set("test_utils") {
+ # TODO: b/303282642 - Remove this testonly
+ testonly = pw_unit_test_TESTONLY
+
public_deps = [
"$dir_pw_bytes",
"$dir_pw_containers:vector",
diff --git a/pw_log_rpc/docs.rst b/pw_log_rpc/docs.rst
index 0b30356f0..1bb1bc683 100644
--- a/pw_log_rpc/docs.rst
+++ b/pw_log_rpc/docs.rst
@@ -1,5 +1,7 @@
.. _module-pw_log_rpc:
+:tocdepth: 4
+
==========
pw_log_rpc
==========
@@ -88,6 +90,32 @@ also be internal log readers, i.e. ``MultiSink::Drain``\s, attached to the
pw_rpc-->computer[Computer];
pw_rpc-->other_listener[Other log<br>listener];
+Relation to pw_log and pw_log_tokenized
+=======================================
+``pw_log_rpc`` is often used in combination with ``pw_log`` and
+``pw_log_tokenized``. The diagram below shows the order of execution after
+invoking a ``pw_log`` macro.
+
+.. mermaid::
+
+ flowchart TD
+ project["`**your project code**`"]
+ --> pw_log["`**pw_log**
+ *facade*`"]
+ --> token_backend["`**pw_log_tokenized**
+ *backend for pw_log*`"]
+ --> token_facade["`**pw_log_tokenized:handler**
+ *facade*`"]
+ --> custom_backend["`**your custom code**
+ *backend for pw_log_tokenized:handler*`"]
+ --> pw_log_rpc["`**pw_log_rpc**`"];
+
+* See :ref:`docs-module-structure-facades` for an explanation of facades and
+ backends.
+* See ``pw_log_tokenized_HandleLog()`` and ``pw_log_tokenized_HandleMessageVaList()``
+ in ``//pw_system/log_backend.cc`` for an example of how :ref:`module-pw_system`
+ implements ``your custom code (pw_log_tokenized backend)``.
+
Components Overview
===================
LogEntry and LogEntries
@@ -103,11 +131,17 @@ out if logs were dropped during transmission.
RPC log service
---------------
The ``LogService`` class is an RPC service that provides a way to request a log
-stream sent via RPC and configure log filters. Thus, it helps avoid using a
-different protocol for logs and RPCs over the same interface(s).
+stream sent via RPC and configure log filters. Thus, it helps avoid
+using a different protocol for logs and RPCs over the same interface(s).
It requires a ``RpcLogDrainMap`` to assign stream writers and delegate the
log stream flushing to the user's preferred method, as well as a ``FilterMap``
-to retrieve and modify filters.
+to retrieve and modify filters. The client may also stop streaming the logs by
+calling ``Cancel()`` or ``RequestCompletion()`` using the ``RawClientReader``
+interface. Note that ``Cancel()`` may lead to dropped logs. To prevent dropped
+logs use ``RequestCompletion()`` and enable :c:macro:`PW_RPC_REQUEST_COMPLETION_CALLBACK`
+e.g. ``-DPW_RPC_REQUEST_COMPLETION_CALLBACK=1``.
+If ``PW_RPC_REQUEST_COMPLETION_CALLBACK`` is not enabled, RequestCompletion()
+call will not stop the logging stream.
RpcLogDrain
-----------
@@ -449,3 +483,23 @@ Logging in other source files
To defer logging, other source files must simply include ``pw_log/log.h`` and
use the :ref:`module-pw_log` APIs, as long as the source set that includes
``foo/log.cc`` is setup as the log backend.
+
+--------------------
+pw_log_rpc in Python
+--------------------
+``pw_log_rpc`` provides client utilities for dealing with RPC logging.
+
+The ``LogStreamHandler`` offers APIs to start a log stream:``listen_to_logs``,
+to handle RPC stream errors: ``handle_log_stream_error``, and RPC stream
+completed events: ``handle_log_stream_completed``. It uses a provided
+``LogStreamDecoder`` to delegate log parsing to.
+
+Python API
+==========
+
+pw_log_rpc.rpc_log_stream
+-------------------------
+.. automodule:: pw_log_rpc.rpc_log_stream
+ :members: LogStreamHandler
+ :undoc-members:
+ :show-inheritance:
diff --git a/pw_log_rpc/log_service_test.cc b/pw_log_rpc/log_service_test.cc
index e201e2382..4d5feea03 100644
--- a/pw_log_rpc/log_service_test.cc
+++ b/pw_log_rpc/log_service_test.cc
@@ -112,14 +112,13 @@ class LogServiceTest : public ::testing::Test {
}
protected:
- std::array<std::byte, kMultiSinkBufferSize> multisink_buffer_;
+ std::array<std::byte, kMultiSinkBufferSize> multisink_buffer_ = {};
multisink::MultiSink multisink_;
- RpcLogDrainMap drain_map_;
- std::array<std::byte, kMaxLogEntrySize> entry_encode_buffer_;
+ std::array<std::byte, kMaxLogEntrySize> entry_encode_buffer_ = {};
static constexpr size_t kMaxFilterRules = 4;
- std::array<Filter::Rule, kMaxFilterRules> rules1_;
- std::array<Filter::Rule, kMaxFilterRules> rules2_;
- std::array<Filter::Rule, kMaxFilterRules> rules3_;
+ std::array<Filter::Rule, kMaxFilterRules> rules1_ = {};
+ std::array<Filter::Rule, kMaxFilterRules> rules2_ = {};
+ std::array<Filter::Rule, kMaxFilterRules> rules3_ = {};
static constexpr std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id1_{
std::byte(65), std::byte(66), std::byte(67), std::byte(0)};
static constexpr std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id2_{
@@ -133,9 +132,9 @@ class LogServiceTest : public ::testing::Test {
};
// Drain Buffers
- std::array<std::byte, kMaxLogEntrySize> drain_buffer1_;
- std::array<std::byte, kMaxLogEntrySize> drain_buffer2_;
- std::array<std::byte, RpcLogDrain::kMinEntryBufferSize> small_buffer_;
+ std::array<std::byte, kMaxLogEntrySize> drain_buffer1_ = {};
+ std::array<std::byte, kMaxLogEntrySize> drain_buffer2_ = {};
+ std::array<std::byte, RpcLogDrain::kMinEntryBufferSize> small_buffer_ = {};
static constexpr uint32_t kIgnoreWriterErrorsDrainId = 1;
static constexpr uint32_t kCloseWriterOnErrorDrainId = 2;
static constexpr uint32_t kSmallBufferDrainId = 3;
@@ -157,6 +156,7 @@ class LogServiceTest : public ::testing::Test {
RpcLogDrain::LogDrainErrorHandling::kIgnoreWriterErrors,
&filters_[2]),
};
+ RpcLogDrainMap drain_map_;
std::array<std::byte, 128> encoding_buffer_ = {};
};
diff --git a/pw_log_rpc/py/BUILD.bazel b/pw_log_rpc/py/BUILD.bazel
new file mode 100644
index 000000000..5c7f0fbf4
--- /dev/null
+++ b/pw_log_rpc/py/BUILD.bazel
@@ -0,0 +1,43 @@
+# Copyright 2022 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_log_rpc",
+ srcs = [
+ "pw_log_rpc/__init__.py",
+ "pw_log_rpc/rpc_log_stream.py",
+ ],
+ imports = ["."],
+ deps = [
+ # TODO: b/241456982 - Add this dep back in
+ # "//pw_log:log_proto_py_pb2",
+ "//pw_log/py:pw_log",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_status/py:pw_status",
+ ],
+)
+
+# TODO: b/241456982 - Not expected to build yet.
+py_test(
+ name = "rpc_log_stream_test",
+ srcs = [
+ "rpc_log_stream_test.py",
+ ],
+ tags = ["manual"],
+ deps = [
+ ":pw_log_rpc",
+ ],
+)
diff --git a/pw_log_rpc/py/BUILD.gn b/pw_log_rpc/py/BUILD.gn
new file mode 100644
index 000000000..37a2b752c
--- /dev/null
+++ b/pw_log_rpc/py/BUILD.gn
@@ -0,0 +1,52 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+
+pw_python_package("py") {
+ generate_setup = {
+ metadata = {
+ name = "pw_log_rpc"
+ version = "0.0.1"
+ }
+ }
+ sources = [
+ "pw_log_rpc/__init__.py",
+ "pw_log_rpc/rpc_log_stream.py",
+ ]
+ tests = [ "rpc_log_stream_test.py" ]
+ python_test_deps = []
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_log:protos.python",
+ "$dir_pw_log/py",
+ "$dir_pw_rpc/py",
+ "$dir_pw_status/py",
+ ]
+}
+
+# This setup.py may be used to install pw_tokenizer without GN. It does not
+# include the pw_tokenizer.proto subpackage, since it contains a generated
+# protobuf module.
+pw_python_script("setup") {
+ sources = [ "setup.py" ]
+ inputs = [
+ "setup.cfg",
+ "pyproject.toml",
+ ]
+}
diff --git a/pw_log_rpc/py/pw_log_rpc/__init__.py b/pw_log_rpc/py/pw_log_rpc/__init__.py
new file mode 100644
index 000000000..c3e1bdbd5
--- /dev/null
+++ b/pw_log_rpc/py/pw_log_rpc/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
diff --git a/pw_log_rpc/py/pw_log_rpc/rpc_log_stream.py b/pw_log_rpc/py/pw_log_rpc/rpc_log_stream.py
new file mode 100644
index 000000000..78a8e4eb0
--- /dev/null
+++ b/pw_log_rpc/py/pw_log_rpc/rpc_log_stream.py
@@ -0,0 +1,87 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""Utils to decode logs."""
+
+import logging
+
+from pw_log.log_decoder import LogStreamDecoder
+from pw_log.proto import log_pb2
+import pw_rpc
+import pw_status
+
+_LOG = logging.getLogger(__name__)
+
+
+class LogStreamHandler:
+ """Handles an RPC Log Stream.
+
+ Args:
+ rpcs: RPC services to request RPC Log Streams.
+ decoder: LogStreamDecoder
+ """
+
+ def __init__(
+ self, rpcs: pw_rpc.client.Services, decoder: LogStreamDecoder
+ ) -> None:
+ self.rpcs = rpcs
+ self._decoder = decoder
+
+ def listen_to_logs(self) -> None:
+ """Requests Logs streamed over RPC.
+
+ The RPCs remain open until the server cancels or closes them, either
+ with a response or error packet.
+ """
+
+ def on_log_entries(_, log_entries_proto: log_pb2.LogEntries) -> None:
+ self._decoder.parse_log_entries_proto(log_entries_proto)
+
+ self.rpcs.pw.log.Logs.Listen.open(
+ on_next=on_log_entries,
+ on_completed=lambda _, status: self.handle_log_stream_completed(
+ status
+ ),
+ on_error=lambda _, error: self.handle_log_stream_error(error),
+ )
+
+ def handle_log_stream_error(self, error: pw_status.Status) -> None:
+ """Resets the log stream RPC on error to avoid losing logs.
+
+ Override this function to change default behavior.
+ """
+ _LOG.error(
+ 'Log stream error: %s from source %s',
+ error,
+ self.source_name,
+ )
+ # Only re-request logs if the RPC was not cancelled by the client.
+ if error != pw_status.Status.CANCELLED:
+ self.listen_to_logs()
+
+ def handle_log_stream_completed(self, status: pw_status.Status) -> None:
+ """Resets the log stream RPC on completed to avoid losing logs.
+
+ Override this function to change default behavior.
+ """
+ _LOG.debug(
+ 'Log stream completed with status: %s for source: %s',
+ status,
+ self.source_name,
+ )
+ self.listen_to_logs()
+
+ @property
+ def source_name(self) -> str:
+ return self._decoder.source_name
diff --git a/pw_log_rpc/py/pyproject.toml b/pw_log_rpc/py/pyproject.toml
new file mode 100644
index 000000000..78668a709
--- /dev/null
+++ b/pw_log_rpc/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_log_rpc/py/rpc_log_stream_test.py b/pw_log_rpc/py/rpc_log_stream_test.py
new file mode 100644
index 000000000..337d5956c
--- /dev/null
+++ b/pw_log_rpc/py/rpc_log_stream_test.py
@@ -0,0 +1,356 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+"""RPC log stream handler tests."""
+
+from dataclasses import dataclass
+import logging
+from typing import Any, Callable, List
+from unittest import TestCase, main, mock
+
+from google.protobuf import message
+from pw_log.log_decoder import Log, LogStreamDecoder
+from pw_log.proto import log_pb2
+from pw_log_rpc.rpc_log_stream import LogStreamHandler
+from pw_rpc import callback_client, client, packets
+from pw_rpc.internal import packet_pb2
+from pw_status import Status
+
+_LOG = logging.getLogger(__name__)
+
+
+def _encode_server_stream_packet(
+ rpc: packets.RpcIds, payload: message.Message
+) -> bytes:
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_STREAM,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
+ payload=payload.SerializeToString(),
+ ).SerializeToString()
+
+
+def _encode_cancel(rpc: packets.RpcIds) -> bytes:
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_ERROR,
+ status=Status.CANCELLED.value,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
+ ).SerializeToString()
+
+
+def _encode_error(rpc: packets.RpcIds) -> bytes:
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_ERROR,
+ status=Status.UNKNOWN.value,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
+ ).SerializeToString()
+
+
+def _encode_completed(rpc: packets.RpcIds, status: Status) -> bytes:
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.RESPONSE,
+ status=status.value,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
+ ).SerializeToString()
+
+
+class _CallableWithCounter:
+ """Wraps a function and counts how many time it was called."""
+
+ @dataclass
+ class CallParams:
+ args: Any
+ kwargs: Any
+
+ def __init__(self, func: Callable[[Any], Any]):
+ self._func = func
+ self.calls: List[_CallableWithCounter.CallParams] = []
+
+ def call_count(self) -> int:
+ return len(self.calls)
+
+ def __call__(self, *args, **kwargs) -> None:
+ self.calls.append(_CallableWithCounter.CallParams(args, kwargs))
+ self._func(*args, **kwargs)
+
+
+class TestRpcLogStreamHandler(TestCase):
+ """Tests for TestRpcLogStreamHandler."""
+
+ def setUp(self) -> None:
+ """Set up logs decoder."""
+ self._channel_id = 1
+ self.client = client.Client.from_modules(
+ callback_client.Impl(),
+ [client.Channel(self._channel_id, lambda _: None)],
+ [log_pb2],
+ )
+
+ self.captured_logs: List[Log] = []
+
+ def decoded_log_handler(log: Log) -> None:
+ self.captured_logs.append(log)
+
+ log_decoder = LogStreamDecoder(
+ decoded_log_handler=decoded_log_handler,
+ source_name='source',
+ )
+ self.log_stream_handler = LogStreamHandler(
+ self.client.channel(self._channel_id).rpcs, log_decoder
+ )
+
+ def _get_rpc_ids(self) -> packets.RpcIds:
+ service = next(iter(self.client.services))
+ method = next(iter(service.methods))
+
+ # To handle unrequested log streams, packets' call Ids are set to
+ # kOpenCallId.
+ call_id = client.OPEN_CALL_ID
+ return packets.RpcIds(self._channel_id, service.id, method.id, call_id)
+
+ def test_listen_to_logs_subsequent_calls(self):
+ """Test a stream of RPC Logs."""
+ self.log_stream_handler.handle_log_stream_error = mock.Mock()
+ self.log_stream_handler.handle_log_stream_completed = mock.Mock()
+ self.log_stream_handler.listen_to_logs()
+
+ self.assertIs(
+ self.client.process_packet(
+ _encode_server_stream_packet(
+ self._get_rpc_ids(),
+ log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ log_pb2.LogEntry(message=b'message0'),
+ log_pb2.LogEntry(message=b'message1'),
+ ],
+ ),
+ )
+ ),
+ Status.OK,
+ )
+ self.assertFalse(self.log_stream_handler.handle_log_stream_error.called)
+ self.assertFalse(
+ self.log_stream_handler.handle_log_stream_completed.called
+ )
+ self.assertEqual(len(self.captured_logs), 2)
+
+ # A subsequent RPC packet should be handled successfully.
+ self.assertIs(
+ self.client.process_packet(
+ _encode_server_stream_packet(
+ self._get_rpc_ids(),
+ log_pb2.LogEntries(
+ first_entry_sequence_id=2,
+ entries=[
+ log_pb2.LogEntry(message=b'message2'),
+ log_pb2.LogEntry(message=b'message3'),
+ ],
+ ),
+ )
+ ),
+ Status.OK,
+ )
+ self.assertFalse(self.log_stream_handler.handle_log_stream_error.called)
+ self.assertFalse(
+ self.log_stream_handler.handle_log_stream_completed.called
+ )
+ self.assertEqual(len(self.captured_logs), 4)
+
+ def test_log_stream_cancelled(self):
+ """Tests that a cancelled log stream is not restarted."""
+ self.log_stream_handler.handle_log_stream_error = mock.Mock()
+ self.log_stream_handler.handle_log_stream_completed = mock.Mock()
+
+ listen_function = _CallableWithCounter(
+ self.log_stream_handler.listen_to_logs
+ )
+ self.log_stream_handler.listen_to_logs = listen_function
+ self.log_stream_handler.listen_to_logs()
+
+ # Send logs prior to cancellation.
+ self.assertIs(
+ self.client.process_packet(
+ _encode_server_stream_packet(
+ self._get_rpc_ids(),
+ log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ log_pb2.LogEntry(message=b'message0'),
+ log_pb2.LogEntry(message=b'message1'),
+ ],
+ ),
+ )
+ ),
+ Status.OK,
+ )
+ self.assertIs(
+ self.client.process_packet(_encode_cancel(self._get_rpc_ids())),
+ Status.OK,
+ )
+ self.log_stream_handler.handle_log_stream_error.assert_called_once_with(
+ Status.CANCELLED
+ )
+ self.assertFalse(
+ self.log_stream_handler.handle_log_stream_completed.called
+ )
+ self.assertEqual(len(self.captured_logs), 2)
+ self.assertEqual(listen_function.call_count(), 1)
+
+ def test_log_stream_error_stream_restarted(self):
+ """Tests that an error on the log stream restarts the stream."""
+ self.log_stream_handler.handle_log_stream_completed = mock.Mock()
+
+ error_handler = _CallableWithCounter(
+ self.log_stream_handler.handle_log_stream_error
+ )
+ self.log_stream_handler.handle_log_stream_error = error_handler
+
+ listen_function = _CallableWithCounter(
+ self.log_stream_handler.listen_to_logs
+ )
+ self.log_stream_handler.listen_to_logs = listen_function
+ self.log_stream_handler.listen_to_logs()
+
+ # Send logs prior to cancellation.
+ self.assertIs(
+ self.client.process_packet(
+ _encode_server_stream_packet(
+ self._get_rpc_ids(),
+ log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ log_pb2.LogEntry(message=b'message0'),
+ log_pb2.LogEntry(message=b'message1'),
+ ],
+ ),
+ )
+ ),
+ Status.OK,
+ )
+ self.assertIs(
+ self.client.process_packet(_encode_error(self._get_rpc_ids())),
+ Status.OK,
+ )
+
+ self.assertFalse(
+ self.log_stream_handler.handle_log_stream_completed.called
+ )
+ self.assertEqual(len(self.captured_logs), 2)
+ self.assertEqual(listen_function.call_count(), 2)
+ self.assertEqual(error_handler.call_count(), 1)
+ self.assertEqual(error_handler.calls[0].args, (Status.UNKNOWN,))
+
+ def test_log_stream_completed_ok_stream_restarted(self):
+ """Tests that when the log stream completes the stream is restarted."""
+ self.log_stream_handler.handle_log_stream_error = mock.Mock()
+
+ completion_handler = _CallableWithCounter(
+ self.log_stream_handler.handle_log_stream_completed
+ )
+ self.log_stream_handler.handle_log_stream_completed = completion_handler
+
+ listen_function = _CallableWithCounter(
+ self.log_stream_handler.listen_to_logs
+ )
+ self.log_stream_handler.listen_to_logs = listen_function
+ self.log_stream_handler.listen_to_logs()
+
+ # Send logs prior to cancellation.
+ self.assertIs(
+ self.client.process_packet(
+ _encode_server_stream_packet(
+ self._get_rpc_ids(),
+ log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ log_pb2.LogEntry(message=b'message0'),
+ log_pb2.LogEntry(message=b'message1'),
+ ],
+ ),
+ )
+ ),
+ Status.OK,
+ )
+ self.assertIs(
+ self.client.process_packet(
+ _encode_completed(self._get_rpc_ids(), Status.OK)
+ ),
+ Status.OK,
+ )
+
+ self.assertFalse(self.log_stream_handler.handle_log_stream_error.called)
+ self.assertEqual(len(self.captured_logs), 2)
+ self.assertEqual(listen_function.call_count(), 2)
+ self.assertEqual(completion_handler.call_count(), 1)
+ self.assertEqual(completion_handler.calls[0].args, (Status.OK,))
+
+ def test_log_stream_completed_with_error_stream_restarted(self):
+ """Tests that when the log stream completes the stream is restarted."""
+ self.log_stream_handler.handle_log_stream_error = mock.Mock()
+
+ completion_handler = _CallableWithCounter(
+ self.log_stream_handler.handle_log_stream_completed
+ )
+ self.log_stream_handler.handle_log_stream_completed = completion_handler
+
+ listen_function = _CallableWithCounter(
+ self.log_stream_handler.listen_to_logs
+ )
+ self.log_stream_handler.listen_to_logs = listen_function
+ self.log_stream_handler.listen_to_logs()
+
+ # Send logs prior to cancellation.
+ self.assertIs(
+ self.client.process_packet(
+ _encode_server_stream_packet(
+ self._get_rpc_ids(),
+ log_pb2.LogEntries(
+ first_entry_sequence_id=0,
+ entries=[
+ log_pb2.LogEntry(message=b'message0'),
+ log_pb2.LogEntry(message=b'message1'),
+ ],
+ ),
+ )
+ ),
+ Status.OK,
+ )
+ self.assertIs(
+ self.client.process_packet(
+ _encode_completed(self._get_rpc_ids(), Status.UNKNOWN)
+ ),
+ Status.OK,
+ )
+
+ self.assertFalse(self.log_stream_handler.handle_log_stream_error.called)
+ self.assertEqual(len(self.captured_logs), 2)
+ self.assertEqual(listen_function.call_count(), 2)
+ self.assertEqual(completion_handler.call_count(), 1)
+ self.assertEqual(completion_handler.calls[0].args, (Status.UNKNOWN,))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_log_rpc/py/setup.cfg b/pw_log_rpc/py/setup.cfg
new file mode 100644
index 000000000..037fa0de0
--- /dev/null
+++ b/pw_log_rpc/py/setup.cfg
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[metadata]
+name = pw_log_rpc
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Tools for working with RPC Logs
+
+[options]
+packages = pw_log_rpc
+
+[options.package_data]
+pw_log = py.typed
diff --git a/pw_log_rpc/rpc_log_drain.cc b/pw_log_rpc/rpc_log_drain.cc
index 68294f9a5..143dbf5ed 100644
--- a/pw_log_rpc/rpc_log_drain.cc
+++ b/pw_log_rpc/rpc_log_drain.cc
@@ -69,6 +69,15 @@ Status RpcLogDrain::Open(rpc::RawServerWriter& writer) {
return Status::AlreadyExists();
}
server_writer_ = std::move(writer);
+
+ // Set a callback to close the drain when RequestCompletion() is requested by
+ // the reader. This callback is only set and invoked if
+ // PW_RPC_REQUEST_COMPLETION_CALLBACK is enabled.
+ // TODO: b/274936558 - : Add unit tests to check that when this callback is
+ // invoked, the stream is closed gracefully without dropping logs.
+ server_writer_.set_on_completion_requested_if_enabled(
+ [this]() { Close().IgnoreError(); });
+
if (on_open_callback_ != nullptr) {
on_open_callback_();
}
@@ -125,7 +134,7 @@ RpcLogDrain::LogDrainState RpcLogDrain::SendLogs(size_t max_num_bundles,
}
encoder.WriteFirstEntrySequenceId(sequence_id_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
sequence_id_ += packed_entry_count;
const Status status = server_writer_.Write(encoder);
sent_bundle_count++;
diff --git a/pw_log_string/BUILD.bazel b/pw_log_string/BUILD.bazel
index e837734e7..6ba51d52e 100644
--- a/pw_log_string/BUILD.bazel
+++ b/pw_log_string/BUILD.bazel
@@ -25,39 +25,51 @@ licenses(["notice"])
pw_cc_library(
name = "pw_log_string",
hdrs = [
- "public/pw_log_string/log_string.h",
"public_overrides/pw_log_backend/log_backend.h",
],
includes = [
- "public",
"public_overrides",
],
deps = [
- ":handler",
+ ":handler_facade",
"//pw_preprocessor",
],
)
pw_cc_facade(
name = "handler_facade",
- hdrs = ["public/pw_log_string/handler.h"],
+ hdrs = [
+ "public/pw_log_string/config.h",
+ "public/pw_log_string/handler.h",
+ ],
includes = ["public"],
deps = ["//pw_preprocessor"],
)
+alias(
+ name = "impl",
+ actual = ":handler",
+)
+
pw_cc_library(
name = "handler",
srcs = ["handler.cc"],
+ hdrs = [
+ "public/pw_log_string/config.h",
+ "public/pw_log_string/handler.h",
+ ],
+ includes = ["public"],
deps = [
":handler_facade",
- "@pigweed_config//:pw_log_string_handler_backend",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_log_string_handler_backend",
],
)
pw_cc_library(
name = "handler_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
- # TODO(b/257539200): This probably should default to
+ visibility = ["@pigweed//targets:__pkg__"],
+ # TODO: b/257539200 - This probably should default to
# //pw_system:log_backend, but that target does not yet build in Bazel.
# deps = ["//pw_system:log_backend"],
)
diff --git a/pw_log_string/BUILD.gn b/pw_log_string/BUILD.gn
index 130550c0d..c8d65f5e2 100644
--- a/pw_log_string/BUILD.gn
+++ b/pw_log_string/BUILD.gn
@@ -16,11 +16,16 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/error.gni")
import("$dir_pw_build/facade.gni")
+import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
import("backend.gni")
+declare_args() {
+ pw_log_string_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+}
+
config("public_include_path") {
include_dirs = [ "public" ]
}
@@ -29,23 +34,23 @@ config("backend_config") {
include_dirs = [ "public_overrides" ]
}
-# This source set only provides pw_log's backend interface by invoking the
-# :handler facade.
-pw_source_set("pw_log_string") {
- public_configs = [
- ":backend_config",
- ":public_include_path",
- ]
- public = [
- "public/pw_log_string/log_string.h",
- "public_overrides/pw_log_backend/log_backend.h",
- ]
+pw_source_set("config") {
+ public = [ "public/pw_log_string/config.h" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [
- ":handler",
"$dir_pw_preprocessor",
+ pw_log_string_CONFIG,
]
}
+# This source set only provides pw_log's backend interface by invoking the
+# :handler facade.
+pw_source_set("pw_log_string") {
+ public_configs = [ ":backend_config" ]
+ public = [ "public_overrides/pw_log_backend/log_backend.h" ]
+ public_deps = [ ":handler" ]
+}
+
pw_source_set("pw_log_string.impl") {
deps = [ ":handler.impl" ]
}
@@ -56,7 +61,10 @@ pw_facade("handler") {
backend = pw_log_string_HANDLER_BACKEND
public_configs = [ ":public_include_path" ]
public = [ "public/pw_log_string/handler.h" ]
- public_deps = [ "$dir_pw_preprocessor" ]
+ public_deps = [
+ ":config",
+ "$dir_pw_preprocessor",
+ ]
sources = [ "handler.cc" ]
require_link_deps = [ ":handler.impl" ]
diff --git a/pw_log_string/CMakeLists.txt b/pw_log_string/CMakeLists.txt
index fd3741f8e..3cecc20bd 100644
--- a/pw_log_string/CMakeLists.txt
+++ b/pw_log_string/CMakeLists.txt
@@ -15,15 +15,24 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_log_string/backend.cmake)
+pw_add_module_config(pw_log_string_CONFIG)
+
+pw_add_library(pw_log_string.config INTERFACE
+ HEADERS
+ public/pw_log_string/config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ ${pw_log_string_CONFIG}
+ pw_preprocessor
+)
+
pw_add_library(pw_log_string INTERFACE
HEADERS
- public/pw_log_string/log_string.h
public_overrides/pw_log_backend/log_backend.h
PUBLIC_INCLUDES
- public
public_overrides
PUBLIC_DEPS
- pw_preprocessor
pw_log_string.handler
)
@@ -35,6 +44,7 @@ pw_add_facade(pw_log_string.handler STATIC
PUBLIC_INCLUDES
public
PUBLIC_DEPS
+ pw_log_string.config
pw_preprocessor
SOURCES
handler.cc
diff --git a/pw_log_string/docs.rst b/pw_log_string/docs.rst
index d8f440917..b4a67efcf 100644
--- a/pw_log_string/docs.rst
+++ b/pw_log_string/docs.rst
@@ -20,17 +20,155 @@ as the backend for ``pw_log`` via ``pw_log_string``. For example it can be
useful to mix tokenized and string based logging in case you have a C ABI where
tokenization can not be used on the other side.
----------------
-Getting started
----------------
-This module is extremely minimal to set up:
+.. _module-pw_log_string-get-started-gn:
-1. Implement ``pw_log_string_HandleMessageVaList()``
-2. Set ``pw_log_BACKEND`` to ``"$dir_pw_log_string"``
-3. Set ``pw_log_string_HANDLER_BACKEND`` to point to the source set that
- implements ``pw_log_string_HandleMessageVaList()``
+----------------
+Get started (GN)
+----------------
+This section outlines how to implement a ``pw_log_string`` backend in a
+GN-based project.
+
+.. note::
+ The example code was written for a :ref:`host <target-host>` target running
+ on Linux.
+
+Invoke a logging macro
+======================
+Call one of the :ref:`pw_log macros <module-pw_log-macros>` in your project
+code:
+
+.. code-block:: cpp
+ :emphasize-lines: 9
+
+ /* //src/app.cc */
+
+ #include <unistd.h>
+
+ #include "pw_log/log.h"
+
+ int main() {
+ while (true) {
+ PW_LOG_INFO("Hello, world!");
+ sleep(5);
+ }
+ return 0;
+ }
+
+Implement the logging function
+==============================
+Implement :cpp:func:`pw_log_string_HandleMessageVaList()` in C. Macros like
+:cpp:func:`PW_LOG()` hand off the actual logging implementation to this
+function.
+
+The function signature of your implementation must match the one specified by
+Pigweed.
+
+The example code below just logs most of the available information to
+``stdout``:
+
+.. code-block:: c
+
+ /* //src/pw_log_string_backend.c */
+
+ #include <stdio.h>
+ #include <stdarg.h>
+
+ void pw_log_string_HandleMessageVaList(int level,
+ unsigned int flags,
+ const char* module_name,
+ const char* file_name,
+ int line_number,
+ const char* message,
+ va_list args) {
+ printf("Entering custom pw_log_string backend...\n");
+ printf("%d\n", level);
+ printf("%u\n", flags);
+ printf("%s\n", module_name);
+ printf("%s\n", file_name);
+ printf("%d\n", line_number);
+ printf("%s\n", message);
+ if (args) { /* Do something with your args here... */ }
+ printf("Exiting custom pw_log_string backend...\n\n");
+ }
What exactly ``pw_log_string_HandleMessageVaList()`` should do is entirely up to
-the implementation. ``pw_log_basic``'s log handler is one example, but it's also
-possible to encode as protobuf and send over a TCP port, write to a file, or
-blink an LED to log as morse code.
+the implementation. The log handler in ``pw_log_basic`` is one example, but it's
+also possible to encode as protobuf and send over a TCP port, write to a file,
+or even blink an LED to log as morse code.
+
+Create source sets
+==================
+.. _source set: https://gn.googlesource.com/gn/+/main/docs/reference.md#c_language-source_sets
+
+Use ``pw_source_set`` to create a `source set`_ for your logging
+implementation. Do not use GN's built-in ``source_set`` feature.
+
+.. code-block:: python
+
+ # //src/BUILD.gn
+
+ ...
+
+ pw_source_set("pw_log_string_backend") {
+ sources = [ "pw_log_string_backend.c" ]
+ }
+
+ pw_source_set("pw_log_string_backend.impl") {
+ sources = []
+ }
+
+ ...
+
+.. _//pw_log/BUILD.gn: https://cs.opensource.google/pigweed/pigweed/+/main:pw_log/BUILD.gn
+
+The empty ``pw_log_string_backend.impl`` source set prevents circular
+dependencies. See the comment for ``group("impl")`` in `//pw_log/BUILD.gn`_
+for more context.
+
+Configure backends
+==================
+Update your target toolchain configuration file:
+
+* Set ``pw_log_BACKEND`` to ``dir_pw_log_string``
+* Point ``pw_log_string_HANDLER_BACKEND`` to your source set that implements
+ :cpp:func:`pw_log_string_HandleMessageVaList()`
+* Update :ref:`pw_build_LINK_DEPS <module-pw_build-link-deps>` to include
+ ``"$dir_pw_log:impl"`` and ``"$dir_pw_log_string:handler:impl"``
+
+.. code-block:: python
+ :emphasize-lines: 11,12,14,15
+
+ # //targets/my_target/target_toolchains.gni
+
+ ...
+
+ my_target = {
+ ...
+ my_toolchain = {
+ name = "my_toolchain"
+ defaults = {
+ ...
+ pw_log_BACKEND = dir_pw_log_string
+ pw_log_string_HANDLER_BACKEND = "//src:pw_log_string_backend"
+ pw_build_LINK_DEPS = [
+ "$dir_pw_log:impl",
+ "$dir_pw_log_string:handler.impl",
+ ...
+ ]
+ ...
+ }
+ }
+ }
+
+ ...
+
+
+(Optional) Implement message handler
+====================================
+Optionally provide your own implementation of ``PW_LOG_STRING_HANDLE_MESSAGE``
+which invokes ``pw_log_string_HANDLER_BACKEND`` with your selected arguments.
+
+-------------
+API reference
+-------------
+.. doxygenfunction:: pw_log_string_HandleMessageVaList(int level, unsigned int flags, const char* module_name, const char* file_name, int line_number, const char* message, va_list args)
diff --git a/pw_log_string/public/pw_log_string/log_string.h b/pw_log_string/public/pw_log_string/config.h
index 352541dad..78c98d345 100644
--- a/pw_log_string/public/pw_log_string/log_string.h
+++ b/pw_log_string/public/pw_log_string/config.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -13,21 +13,22 @@
// the License.
#pragma once
-#include "pw_log_string/handler.h"
#include "pw_preprocessor/arguments.h"
-// Log a message with many attributes included. This is a backend implementation
-// for the logging facade in pw_log/log.h.
-//
-// This is the log macro frontend that funnels everything into the C-based
-// message hangler facade, i.e. pw_log_string_HandleMessage. It's not efficient
-// at the callsite, since it passes many arguments.
-#define PW_HANDLE_LOG(level, module, flags, message, ...) \
- do { \
- pw_log_string_HandleMessage((level), \
- (flags), \
- (module), \
- __FILE__, \
- __LINE__, \
- message PW_COMMA_ARGS(__VA_ARGS__)); \
+// User-provided header to optionally override options in this file.
+#if defined(PW_LOG_STRING_CONFIG_HEADER)
+#include PW_LOG_STRING_CONFIG_HEADER
+#endif // defined(PW_LOG_STRING_CONFIG_HEADER)
+
+// Default implementation which can be overriden to adjust arguments passed to
+// pw_log_string_HandleMessage.
+#ifndef PW_LOG_STRING_CONFIG_HANDLE_MESSAGE
+#define PW_LOG_STRING_CONFIG_HANDLE_MESSAGE(level, module, flags, ...) \
+ do { \
+ pw_log_string_HandleMessage((level), \
+ (flags), \
+ (module), \
+ __FILE__, \
+ __LINE__ PW_COMMA_ARGS(__VA_ARGS__)); \
} while (0)
+#endif // PW_LOG_STRING_CONFIG_HANDLE_MESSAGE
diff --git a/pw_log_string/public/pw_log_string/handler.h b/pw_log_string/public/pw_log_string/handler.h
index f8c84432f..611735a13 100644
--- a/pw_log_string/public/pw_log_string/handler.h
+++ b/pw_log_string/public/pw_log_string/handler.h
@@ -15,9 +15,21 @@
#include <stdarg.h>
+#include "pw_log_string/config.h"
#include "pw_preprocessor/compiler.h"
#include "pw_preprocessor/util.h"
+// This macro implements PW_LOG using pw_log_string_HandleMessage.
+//
+// This is the log macro frontend that funnels everything into the C-based
+// message hangler facade, i.e. pw_log_string_HandleMessage. It's not efficient
+// at the callsite, since it passes many arguments.
+//
+// Users can configure exactly what is passed to pw_log_string_HandleMessage by
+// providing their own PW_LOG_STRING_CONFIG_HANDLE_MESSAGE implementation.
+//
+#define PW_LOG_STRING_HANDLE_MESSAGE PW_LOG_STRING_CONFIG_HANDLE_MESSAGE
+
PW_EXTERN_C_START
// Invokes pw_log_string_HandleMessageVaList, this is implemented by the facade.
@@ -29,8 +41,8 @@ void pw_log_string_HandleMessage(int level,
const char* message,
...) PW_PRINTF_FORMAT(6, 7);
-// Log a message with the listed attributes, this must be implemented by the
-// backend.
+/// Logs a message with the listed attributes. This must be implemented by the
+/// backend.
void pw_log_string_HandleMessageVaList(int level,
unsigned int flags,
const char* module_name,
diff --git a/pw_log_string/public_overrides/pw_log_backend/log_backend.h b/pw_log_string/public_overrides/pw_log_backend/log_backend.h
index 6bc92fe72..464a965db 100644
--- a/pw_log_string/public_overrides/pw_log_backend/log_backend.h
+++ b/pw_log_string/public_overrides/pw_log_backend/log_backend.h
@@ -13,4 +13,15 @@
// the License.
#pragma once
-#include "pw_log_string/log_string.h"
+#include "pw_log_string/handler.h"
+
+// Log a message with many attributes included. This is a backend implementation
+// for the logging facade in pw_log/log.h.
+//
+// This is the log macro frontend that funnels everything into the C-based
+// message hangler facade, i.e. pw_log_string_HandleMessage. It's not efficient
+// at the callsite, since it passes many arguments.
+//
+// Users can configure exactly what is passed to pw_log_string_HandleMessage by
+// providing their own PW_LOG_STRING_HANDLE_MESSAGE implementation.
+#define PW_HANDLE_LOG PW_LOG_STRING_HANDLE_MESSAGE
diff --git a/pw_log_tokenized/Android.bp b/pw_log_tokenized/Android.bp
new file mode 100644
index 000000000..e3ddfa0bf
--- /dev/null
+++ b/pw_log_tokenized/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_log_tokenized_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
+}
diff --git a/pw_log_tokenized/BUILD.bazel b/pw_log_tokenized/BUILD.bazel
index abcf0f1de..b598ad3cc 100644
--- a/pw_log_tokenized/BUILD.bazel
+++ b/pw_log_tokenized/BUILD.bazel
@@ -14,6 +14,7 @@
load(
"//pw_build:pigweed.bzl",
+ "pw_cc_facade",
"pw_cc_library",
"pw_cc_test",
)
@@ -28,11 +29,9 @@ pw_cc_library(
"public/pw_log_tokenized/config.h",
"public/pw_log_tokenized/log_tokenized.h",
"public/pw_log_tokenized/metadata.h",
- "public_overrides/pw_log_backend/log_backend.h",
],
includes = [
"public",
- "public_overrides",
],
deps = [
"//pw_log:facade",
@@ -43,14 +42,45 @@ pw_cc_library(
pw_cc_library(
name = "pw_log_tokenized",
srcs = ["log_tokenized.cc"],
+ hdrs = [
+ "public_overrides/pw_log_backend/log_backend.h",
+ "public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h",
+ ],
+ includes = [
+ "public_overrides",
+ ],
deps = [
- ":handler",
+ ":handler_facade",
":headers",
"//pw_log:facade",
],
)
pw_cc_library(
+ name = "gcc_partially_tokenized",
+ srcs = ["log_tokenized.cc"],
+ hdrs = [
+ "gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h",
+ "public/pw_log_tokenized/gcc_partially_tokenized.h",
+ ],
+ includes = [
+ "gcc_partially_tokenized_public_overrides",
+ "public",
+ ],
+ deps = [
+ ":handler",
+ ":headers",
+ "//pw_log:facade",
+ "//pw_log_string:handler",
+ ],
+)
+
+alias(
+ name = "impl",
+ actual = ":handler",
+)
+
+pw_cc_facade(
name = "handler_facade",
hdrs = ["public/pw_log_tokenized/handler.h"],
includes = ["public"],
@@ -59,36 +89,35 @@ pw_cc_library(
pw_cc_library(
name = "handler",
+ hdrs = ["public/pw_log_tokenized/handler.h"],
+ includes = ["public"],
deps = [
- ":handler_facade",
- "@pigweed_config//:pw_log_tokenized_handler_backend",
+ "//pw_preprocessor",
+ "//targets:pw_log_tokenized_handler_backend",
],
)
# There is no default backend for now.
pw_cc_library(
name = "backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
)
-# The compatibility library is not needed in Bazel.
pw_cc_library(
- name = "compatibility",
- srcs = ["compatibility.cc"],
- visibility = ["//visibility:private"],
+ name = "base64",
+ hdrs = ["public/pw_log_tokenized/base64.h"],
+ includes = ["public"],
deps = [
- ":handler_facade",
- "//pw_tokenizer",
- "//pw_tokenizer:global_handler_with_payload",
+ ":headers", # Only config.h is needed
+ "//pw_tokenizer:base64",
],
)
pw_cc_library(
name = "base64_over_hdlc",
srcs = ["base64_over_hdlc.cc"],
- hdrs = ["public/pw_log_tokenized/base64_over_hdlc.h"],
- includes = ["public"],
deps = [
+ ":base64",
":handler_facade",
"//pw_hdlc",
"//pw_stream:sys_io_stream",
@@ -104,6 +133,7 @@ pw_cc_test(
"pw_log_tokenized_private/test_utils.h",
],
deps = [
+ ":base64",
":headers",
"//pw_unit_test",
],
diff --git a/pw_log_tokenized/BUILD.gn b/pw_log_tokenized/BUILD.gn
index 3e2b16181..17240f4e9 100644
--- a/pw_log_tokenized/BUILD.gn
+++ b/pw_log_tokenized/BUILD.gn
@@ -20,7 +20,6 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_log/backend.gni")
import("$dir_pw_log_tokenized/backend.gni")
-import("$dir_pw_tokenizer/backend.gni")
import("$dir_pw_unit_test/test.gni")
declare_args() {
@@ -40,6 +39,11 @@ config("backend_config") {
visibility = [ ":*" ]
}
+config("gcc_partially_tokenized_backend_config") {
+ include_dirs = [ "public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw_log.
pw_source_set("pw_log_tokenized") {
public_configs = [ ":backend_config" ]
@@ -47,79 +51,52 @@ pw_source_set("pw_log_tokenized") {
":handler.facade", # Depend on the facade to avoid circular dependencies.
":headers",
]
- public = [ "public_overrides/pw_log_backend/log_backend.h" ]
+ public = [
+ "public_overrides/pw_log_backend/log_backend.h",
+ "public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h",
+ ]
sources = [ "log_tokenized.cc" ]
}
-config("backwards_compatibility_config") {
- defines = [ "_PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT" ]
- visibility = [ ":*" ]
-}
-
pw_source_set("headers") {
visibility = [ ":*" ]
public_configs = [ ":public_include_path" ]
public_deps = [
":config",
":metadata",
-
- # TODO(hepler): Remove this dependency when all projects have migrated to
- # the new pw_log_tokenized handler.
- "$dir_pw_tokenizer:global_handler_with_payload",
dir_pw_preprocessor,
dir_pw_tokenizer,
]
public = [ "public/pw_log_tokenized/log_tokenized.h" ]
}
-# The old pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND backend may still be
-# in use by projects that have not switched to the new pw_log_tokenized facade.
-# Use the old backend as a stand-in for the new backend if it is set.
-_old_backend_is_set = pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND != ""
-_new_backend_is_set = pw_log_tokenized_HANDLER_BACKEND != ""
+# This target provides the backend for pw_log for GCC which tokenizes as much as
+# it can and uses pw_log_string:handler for the rest.
+pw_source_set("gcc_partially_tokenized") {
+ public_configs = [
+ ":gcc_partially_tokenized_backend_config",
+ ":public_include_path",
+ ]
+ public_deps = [
+ ":handler.facade", # Depend on the facade to avoid circular dependencies.
+ ":headers",
+ "$dir_pw_log_string:handler",
+ ]
+ public = [
+ "gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h",
+ "public/pw_log_tokenized/gcc_partially_tokenized.h",
+ ]
+ sources = [ "log_tokenized.cc" ]
+}
pw_facade("handler") {
public_configs = [ ":public_include_path" ]
- public_deps = [
- # TODO(hepler): Remove this dependency when all projects have migrated to
- # the new pw_log_tokenized handler.
- "$dir_pw_tokenizer:global_handler_with_payload",
- dir_pw_preprocessor,
- ]
+ public_deps = [ dir_pw_preprocessor ]
public = [ "public/pw_log_tokenized/handler.h" ]
- # If the global handler backend is set, redirect the new facade to the old
- # facade. If no backend is set, the old facade may still be in use through
- # link deps, so provide the compatibility layer.
- #
- # TODO(hepler): Remove these backwards compatibility workarounds when projects
- # have migrated.
- if (_old_backend_is_set || (!_old_backend_is_set && !_new_backend_is_set)) {
- assert(pw_log_tokenized_HANDLER_BACKEND == "",
- "pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND is deprecated; " +
- "only pw_log_tokenized_HANDLER_BACKEND should be set")
-
- backend = pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND
-
- # There is only one pw_log_tokenized backend in Pigweed, and it has been
- # updated to the new API.
- if (_old_backend_is_set &&
- get_label_info(pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND,
- "label_no_toolchain") ==
- get_label_info(":base64_over_hdlc", "label_no_toolchain")) {
- defines = [ "PW_LOG_TOKENIZED_BACKEND_USES_NEW_API=1" ]
- } else {
- defines = [ "PW_LOG_TOKENIZED_BACKEND_USES_NEW_API=0" ]
- }
-
- public_configs += [ ":backwards_compatibility_config" ]
- deps = [ dir_pw_tokenizer ]
- sources = [ "compatibility.cc" ]
- } else {
- backend = pw_log_tokenized_HANDLER_BACKEND
- }
+ backend = pw_log_tokenized_HANDLER_BACKEND
}
pw_source_set("metadata") {
@@ -132,6 +109,7 @@ pw_source_set("config") {
public_configs = [ ":public_include_path" ]
public_deps = [
"$dir_pw_log:facade",
+ "$dir_pw_tokenizer:config",
pw_log_tokenized_CONFIG,
]
public = [ "public/pw_log_tokenized/config.h" ]
@@ -143,18 +121,26 @@ pw_source_set("config") {
pw_source_set("pw_log_tokenized.impl") {
deps = [ ":pw_log_tokenized" ]
- if (_new_backend_is_set || _old_backend_is_set) {
+ if (pw_log_tokenized_HANDLER_BACKEND != "") {
deps += [ ":handler" ]
}
}
+pw_source_set("base64") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_log_tokenized/base64.h" ]
+ public_deps = [
+ ":config",
+ "$dir_pw_tokenizer:base64",
+ ]
+}
+
# This target provides a backend for pw_tokenizer that encodes tokenized logs as
# Base64, encodes them into HDLC frames, and writes them over sys_io.
pw_source_set("base64_over_hdlc") {
- public_configs = [ ":public_include_path" ]
- public = [ "public/pw_log_tokenized/base64_over_hdlc.h" ]
sources = [ "base64_over_hdlc.cc" ]
deps = [
+ ":base64",
":handler.facade",
"$dir_pw_hdlc:encoder",
"$dir_pw_stream:sys_io_stream",
@@ -177,6 +163,7 @@ pw_test("log_tokenized_test") {
"pw_log_tokenized_private/test_utils.h",
]
deps = [
+ ":base64",
":headers",
dir_pw_preprocessor,
]
diff --git a/pw_log_tokenized/CMakeLists.txt b/pw_log_tokenized/CMakeLists.txt
index 28b3c96a9..d138fa3a1 100644
--- a/pw_log_tokenized/CMakeLists.txt
+++ b/pw_log_tokenized/CMakeLists.txt
@@ -24,21 +24,46 @@ pw_add_library(pw_log_tokenized.config INTERFACE
public
PUBLIC_DEPS
pw_log.facade
+ pw_tokenizer.config
${pw_log_tokenized_CONFIG}
)
pw_add_library(pw_log_tokenized STATIC
HEADERS
- public/pw_log_tokenized/log_tokenized.h
public_overrides/pw_log_backend/log_backend.h
+ public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h
PUBLIC_INCLUDES
- public
public_overrides
PUBLIC_DEPS
- pw_log_tokenized.config
pw_log_tokenized.handler
+ pw_log_tokenized._headers
+ SOURCES
+ log_tokenized.cc
+)
+
+pw_add_library(pw_log_tokenized._headers INTERFACE
+ HEADERS
+ public/pw_log_tokenized/log_tokenized.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_log_tokenized.config
pw_log_tokenized.metadata
+ pw_preprocessor
pw_tokenizer
+)
+
+pw_add_library(pw_log_tokenized.gcc_partially_tokenized STATIC
+ HEADERS
+ public/pw_log_tokenized/gcc_partially_tokenized.h
+ gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h
+ PUBLIC_INCLUDES
+ public
+ gcc_partially_tokenized_public_overrides
+ PUBLIC_DEPS
+ pw_log_string.handler
+ pw_log_tokenized.handler
+ pw_log_tokenized._headers
SOURCES
log_tokenized.cc
)
@@ -63,37 +88,42 @@ pw_add_facade(pw_log_tokenized.handler INTERFACE
pw_preprocessor
)
-# This target provides a backend for pw_tokenizer that encodes tokenized logs as
-# Base64, encodes them into HDLC frames, and writes them over sys_io.
-pw_add_library(pw_log_tokenized.base64_over_hdlc STATIC
+pw_add_library(pw_log_tokenized.base64 INTERFACE
HEADERS
- public/pw_log_tokenized/base64_over_hdlc.h
+ public/pw_log_tokenized/base64.h
PUBLIC_INCLUDES
public
+ PUBLIC_DEPS
+ pw_log_tokenized.config
+ pw_tokenizer.base64
+)
+
+# This target provides a backend for pw_tokenizer that encodes tokenized logs as
+# Base64, encodes them into HDLC frames, and writes them over sys_io.
+pw_add_library(pw_log_tokenized.base64_over_hdlc STATIC
SOURCES
base64_over_hdlc.cc
PRIVATE_DEPS
pw_hdlc.encoder
+ pw_log_tokenized.base64
pw_log_tokenized.handler
pw_span
pw_stream.sys_io_stream
- pw_tokenizer.base64
)
-if(NOT "${pw_tokenizer.global_handler_with_payload_BACKEND}" STREQUAL "")
- pw_add_test(pw_log_tokenized.log_tokenized_test
- SOURCES
- log_tokenized_test.cc
- log_tokenized_test_c.c
- pw_log_tokenized_private/test_utils.h
- PRIVATE_DEPS
- pw_log_tokenized
- pw_preprocessor
- GROUPS
- modules
- pw_log_tokenized
- )
-endif()
+pw_add_test(pw_log_tokenized.log_tokenized_test
+ SOURCES
+ log_tokenized_test.cc
+ log_tokenized_test_c.c
+ pw_log_tokenized_private/test_utils.h
+ PRIVATE_DEPS
+ pw_log_tokenized.base64
+ pw_log_tokenized._headers
+ pw_preprocessor
+ GROUPS
+ modules
+ pw_log_tokenized
+)
pw_add_test(pw_log_tokenized.metadata_test
SOURCES
diff --git a/pw_log_tokenized/base64_over_hdlc.cc b/pw_log_tokenized/base64_over_hdlc.cc
index 29e925ab7..997f6cf55 100644
--- a/pw_log_tokenized/base64_over_hdlc.cc
+++ b/pw_log_tokenized/base64_over_hdlc.cc
@@ -15,17 +15,19 @@
// This function serves as a backend for pw_tokenizer / pw_log_tokenized that
// encodes tokenized logs as Base64 and writes them using HDLC.
-#include "pw_log_tokenized/base64_over_hdlc.h"
-
#include "pw_hdlc/encoder.h"
+#include "pw_log_tokenized/base64.h"
#include "pw_log_tokenized/handler.h"
#include "pw_span/span.h"
#include "pw_stream/sys_io_stream.h"
+#include "pw_string/string.h"
#include "pw_tokenizer/base64.h"
namespace pw::log_tokenized {
namespace {
+inline constexpr int kBase64LogHdlcAddress = 1;
+
stream::SysIoWriter writer;
} // namespace
@@ -36,15 +38,12 @@ extern "C" void pw_log_tokenized_HandleLog(
const uint8_t log_buffer[],
size_t size_bytes) {
// Encode the tokenized message as Base64.
- char base64_buffer[tokenizer::kDefaultBase64EncodedBufferSize];
- const size_t base64_bytes = tokenizer::PrefixedBase64Encode(
- span(log_buffer, size_bytes), base64_buffer);
- base64_buffer[base64_bytes] = '\0';
+ const pw::InlineBasicString base64_string =
+ PrefixedBase64Encode(log_buffer, size_bytes);
// HDLC-encode the Base64 string via a SysIoWriter.
- hdlc::WriteUIFrame(PW_LOG_TOKENIZED_BASE64_LOG_HDLC_ADDRESS,
- as_bytes(span(base64_buffer, base64_bytes)),
- writer);
+ hdlc::WriteUIFrame(
+ kBase64LogHdlcAddress, as_bytes(span(base64_string)), writer);
}
} // namespace pw::log_tokenized
diff --git a/pw_log_tokenized/compatibility.cc b/pw_log_tokenized/compatibility.cc
deleted file mode 100644
index 54060dbc7..000000000
--- a/pw_log_tokenized/compatibility.cc
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright 2023 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-// If the project is still using pw_tokenizer's global handler with payload
-// facade, then define its functions as used by pw_log_tokenized.
-
-#include <cstdarg>
-
-#include "pw_log_tokenized/handler.h"
-#include "pw_preprocessor/compiler.h"
-#include "pw_tokenizer/encode_args.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
-
-// If the new API is in use, define pw_tokenizer_HandleEncodedMessageWithPayload
-// to redirect to it, in case there are any direct calls to it. Only projects
-// that use the base64_over_hdlc backend will have been updated to the new API.
-#if PW_LOG_TOKENIZED_BACKEND_USES_NEW_API
-
-extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- uint32_t metadata, const uint8_t encoded_message[], size_t size_bytes) {
- pw_log_tokenized_HandleLog(metadata, encoded_message, size_bytes);
-}
-
-#else // If the new API is not in use, implement it to redirect to the old API.
-
-extern "C" void pw_log_tokenized_HandleLog(uint32_t metadata,
- const uint8_t encoded_message[],
- size_t size_bytes) {
- pw_tokenizer_HandleEncodedMessageWithPayload(
- metadata, encoded_message, size_bytes);
-}
-
-#endif // PW_LOG_TOKENIZED_BACKEND_USES_NEW_API
-
-// Implement the global tokenized log handler function, which is identical
-// This function is the same as _pw_log_tokenized_EncodeTokenizedLog().
-extern "C" void _pw_tokenizer_ToGlobalHandlerWithPayload(
- uint32_t metadata,
- pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...) {
- va_list args;
- va_start(args, types);
- pw::tokenizer::EncodedMessage<> encoded_message(token, types, args);
- va_end(args);
-
- pw_log_tokenized_HandleLog(
- metadata, encoded_message.data_as_uint8(), encoded_message.size());
-}
diff --git a/pw_log_tokenized/docs.rst b/pw_log_tokenized/docs.rst
index 24a6d8d4a..0e84c5909 100644
--- a/pw_log_tokenized/docs.rst
+++ b/pw_log_tokenized/docs.rst
@@ -4,7 +4,8 @@
pw_log_tokenized
----------------
The ``pw_log_tokenized`` module contains utilities for tokenized logging. It
-connects ``pw_log`` to ``pw_tokenizer``.
+connects ``pw_log`` to ``pw_tokenizer`` and supports
+:ref:`module-pw_log-tokenized-args`.
C++ backend
===========
@@ -55,7 +56,7 @@ letter.
.. code-block::
- "■key1♦contents1■key2♦contents2■key3♦contents3"
+ "■key1♦contents1■key2♦contents2■key3♦contents3"
This format makes the message easily machine parseable and human readable. It is
extremely unlikely to conflict with log message contents due to the characters
@@ -67,7 +68,7 @@ Implementations may add other fields, but they will be ignored by the
.. code-block::
- "■msg♦Hyperdrive %d set to %f■module♦engine■file♦propulsion/hyper.cc"
+ "■msg♦Hyperdrive %d set to %f■module♦engine■file♦propulsion/hyper.cc"
Using key-value pairs allows placing the fields in any order.
``pw_log_tokenized`` places the message first. This is prefered when tokenizing
@@ -175,6 +176,11 @@ object:
token_buffer.size());
}
+The binary tokenized message may be encoded in the :ref:`prefixed Base64 format
+<module-pw_tokenizer-base64-format>` with the following function:
+
+.. doxygenfunction:: PrefixedBase64Encode(span<const std::byte>)
+
Build targets
-------------
The GN build for ``pw_log_tokenized`` has two targets: ``pw_log_tokenized`` and
@@ -184,6 +190,14 @@ implements the backend for the ``pw_log`` facade. ``pw_log_tokenized`` invokes
the ``pw_log_tokenized:handler`` facade, which must be implemented by the user
of ``pw_log_tokenized``.
+GCC has a bug resulting in section attributes of templated functions being
+ignored. This in turn means that log tokenization cannot work for templated
+functions, because the token database entries are lost at build time.
+For more information see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70435.
+If you are using GCC, the ``gcc_partially_tokenized`` target can be used as a
+backend for the ``pw_log`` facade instead which tokenizes as much as possible
+and uses the ``pw_log_string:handler`` for the rest using string logging.
+
Python package
==============
``pw_log_tokenized`` includes a Python package for decoding tokenized logs.
diff --git a/pw_log_tokenized/gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h b/pw_log_tokenized/gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h
new file mode 100644
index 000000000..14139d9eb
--- /dev/null
+++ b/pw_log_tokenized/gcc_partially_tokenized_public_overrides/pw_log_backend/log_backend.h
@@ -0,0 +1,24 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This override header includes the main tokenized logging header and defines
+// the PW_LOG macro as the tokenized logging macro.
+#pragma once
+
+#include "pw_log_tokenized/config.h"
+#include "pw_log_tokenized/log_tokenized.h"
+
+#define PW_LOG_FLAG_BITS PW_LOG_TOKENIZED_FLAG_BITS
+
+#include "pw_log_tokenized/gcc_partially_tokenized.h"
diff --git a/pw_log_tokenized/log_tokenized.cc b/pw_log_tokenized/log_tokenized.cc
index 25c00d90a..6a1ba8ce8 100644
--- a/pw_log_tokenized/log_tokenized.cc
+++ b/pw_log_tokenized/log_tokenized.cc
@@ -16,6 +16,7 @@
#include <cstdarg>
+#include "pw_log_tokenized/config.h"
#include "pw_log_tokenized/handler.h"
#include "pw_tokenizer/encode_args.h"
@@ -26,7 +27,8 @@ extern "C" void _pw_log_tokenized_EncodeTokenizedLog(
...) {
va_list args;
va_start(args, types);
- pw::tokenizer::EncodedMessage<> encoded_message(token, types, args);
+ pw::tokenizer::EncodedMessage<PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES>
+ encoded_message(token, types, args);
va_end(args);
pw_log_tokenized_HandleLog(
diff --git a/pw_log_tokenized/log_tokenized_test.cc b/pw_log_tokenized/log_tokenized_test.cc
index a8d4759b0..51c90e34d 100644
--- a/pw_log_tokenized/log_tokenized_test.cc
+++ b/pw_log_tokenized/log_tokenized_test.cc
@@ -28,6 +28,7 @@
#include "pw_log_tokenized/log_tokenized.h"
#include "gtest/gtest.h"
+#include "pw_log_tokenized/base64.h"
#include "pw_log_tokenized_private/test_utils.h"
namespace pw::log_tokenized {
@@ -44,6 +45,22 @@ constexpr uintptr_t kModuleToken =
PW_TOKENIZER_STRING_TOKEN(PW_LOG_MODULE_NAME) &
((1u << PW_LOG_TOKENIZED_MODULE_BITS) - 1);
+TEST(LogTokenized, Base64) {
+ constexpr uint8_t kBinary[6]{1, 2, 3, 4, 5, 6};
+ constexpr const char* kBase64Expected = "$AQIDBAUG"; // calculated in Python
+
+ InlineBasicString result_1 = PrefixedBase64Encode(as_bytes(span(kBinary)));
+ EXPECT_EQ(result_1, kBase64Expected);
+ EXPECT_EQ(result_1.capacity(), kBase64EncodedBufferSizeBytes);
+
+ InlineBasicString result_2 = PrefixedBase64Encode(kBinary, sizeof(kBinary));
+ EXPECT_EQ(result_2, kBase64Expected);
+
+ InlineBasicString result_3 = PrefixedBase64Encode(
+ reinterpret_cast<const std::byte*>(kBinary), sizeof(kBinary));
+ EXPECT_EQ(result_3, kBase64Expected);
+}
+
TEST(LogTokenized, LogMetadata_LevelTooLarge_Clamps) {
auto check_metadata = [] {
Metadata metadata = Metadata(last_log.metadata);
diff --git a/pw_log_tokenized/log_tokenized_test_c.c b/pw_log_tokenized/log_tokenized_test_c.c
index 81667a7e0..1202ab1c7 100644
--- a/pw_log_tokenized/log_tokenized_test_c.c
+++ b/pw_log_tokenized/log_tokenized_test_c.c
@@ -47,14 +47,14 @@ void pw_log_tokenized_Test_LogMetadata_LevelTooLarge_Clamps(void) {
}
void pw_log_tokenized_Test_LogMetadata_TooManyFlags_Truncates(void) {
-// clang-format off
+ // clang-format off
#line 1100
PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, PW_LOG_MODULE_NAME, 0xFFFFFFFF, "hello");
// clang-format on
}
void pw_log_tokenized_Test_LogMetadata_LogMetadata_VariousValues(void) {
-// clang-format off
+ // clang-format off
#line 1200
PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, PW_LOG_MODULE_NAME, 3, "hello%s", "?");
// clang-format on
diff --git a/pw_log_tokenized/public/pw_log_tokenized/base64.h b/pw_log_tokenized/public/pw_log_tokenized/base64.h
new file mode 100644
index 000000000..89e7a199f
--- /dev/null
+++ b/pw_log_tokenized/public/pw_log_tokenized/base64.h
@@ -0,0 +1,51 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstddef>
+#include <type_traits>
+
+#include "pw_log_tokenized/config.h"
+#include "pw_tokenizer/base64.h"
+
+namespace pw::log_tokenized {
+
+// Minimum capacity for a string that to hold the Base64-encoded version of a
+// PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES tokenized message. This is the
+// capacity needed to encode to a `pw::InlineString` and does not include a null
+// terminator.
+inline constexpr size_t kBase64EncodedBufferSizeBytes =
+ tokenizer::Base64EncodedBufferSize(kEncodingBufferSizeBytes);
+
+/// Encodes a binary tokenized log in the prefixed Base64 format. Calls
+/// @cpp_func{pw::tokenizer::PrefixedBase64Encode} for a string sized to fit a
+/// `kEncodingBufferSizeBytes` tokenized log.
+inline InlineString<kBase64EncodedBufferSizeBytes> PrefixedBase64Encode(
+ span<const std::byte> binary_message) {
+ return tokenizer::PrefixedBase64Encode<kEncodingBufferSizeBytes>(
+ binary_message);
+}
+
+#ifndef PW_EXCLUDE_FROM_DOXYGEN // Doxygen fails to parse this, so skip it.
+
+template <typename T,
+ typename = std::enable_if_t<sizeof(T) == sizeof(std::byte)>>
+inline InlineString<kBase64EncodedBufferSizeBytes> PrefixedBase64Encode(
+ const T* log_buffer, size_t size_bytes) {
+ return PrefixedBase64Encode(as_bytes(span(log_buffer, size_bytes)));
+}
+
+#endif // PW_EXCLUDE_FROM_DOXYGEN
+
+} // namespace pw::log_tokenized
diff --git a/pw_log_tokenized/public/pw_log_tokenized/config.h b/pw_log_tokenized/public/pw_log_tokenized/config.h
index 647598177..c1060b5ff 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/config.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/config.h
@@ -17,6 +17,22 @@
#include "pw_log/levels.h"
#include "pw_log/options.h"
+#include "pw_tokenizer/config.h"
+
+// The size of the stack-allocated argument encoding buffer to use by default.
+// A buffer of this size is allocated and used for the 4-byte token and for
+// encoding all arguments. It must be at least large enough for the token (4
+// bytes).
+//
+// This buffer does not need to be large to accommodate a good number of
+// tokenized string arguments. Integer arguments are usually encoded smaller
+// than their native size (e.g. 1 or 2 bytes for smaller numbers). All floating
+// point types are encoded as four bytes. Null-terminated strings are encoded
+// 1:1 in size, however, and can quickly fill up this buffer.
+#ifndef PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES
+#define PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES \
+ PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES
+#endif // PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES
// This macro takes the PW_LOG format string and optionally transforms it. By
// default, pw_log_tokenized specifies three fields as key-value pairs.
@@ -68,3 +84,17 @@
static_assert((PW_LOG_TOKENIZED_LEVEL_BITS + PW_LOG_TOKENIZED_LINE_BITS +
PW_LOG_TOKENIZED_FLAG_BITS + PW_LOG_TOKENIZED_MODULE_BITS) == 32,
"Log metadata fields must use 32 bits");
+
+#ifdef __cplusplus
+
+#include <cstddef>
+
+namespace pw::log_tokenized {
+
+// C++ constant for the encoding buffer size. Use this instead of the macro.
+inline constexpr size_t kEncodingBufferSizeBytes =
+ PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES;
+
+} // namespace pw::log_tokenized
+
+#endif // __cplusplus
diff --git a/pw_log_tokenized/public/pw_log_tokenized/gcc_partially_tokenized.h b/pw_log_tokenized/public/pw_log_tokenized/gcc_partially_tokenized.h
new file mode 100644
index 000000000..14d7128ac
--- /dev/null
+++ b/pw_log_tokenized/public/pw_log_tokenized/gcc_partially_tokenized.h
@@ -0,0 +1,48 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_log_tokenized/log_tokenized.h"
+
+#if !defined(__cplusplus) || !defined(__GNUC__) || defined(__clang__)
+
+// If we're not compiling C++ or we're not using GCC, then tokenize the log.
+#define PW_HANDLE_LOG PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD
+
+#else // defined(__cplusplus) && defined(__GNUC__) && !defined(__clang__)
+
+#include <string_view>
+
+#include "pw_log_string/handler.h"
+
+// GCC has a bug resulting in section attributes of templated functions being
+// ignored. This in turn means that log tokenization cannot work for templated
+// functions, because the token database entries are lost at build time.
+// For more information see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70435
+//
+// To work around this, we defer to string logging only in templated contexts.
+//
+// __PRETTY_FUNCTION__ is suffixed with ' [with ...]' for templated contexts
+// if the class and/or function is templated. For example:
+// "Foo() [with T = char]".
+#define PW_HANDLE_LOG(...) \
+ do { \
+ if constexpr (std::string_view(__PRETTY_FUNCTION__).back() == ']') { \
+ PW_LOG_STRING_HANDLE_MESSAGE(__VA_ARGS__); \
+ } else { \
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(__VA_ARGS__); \
+ } \
+ } while (0)
+
+#endif // !defined(__cplusplus) || !defined(__GNUC__) || defined(__clang__)
diff --git a/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h b/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
index 61454a741..2ad6b2bbe 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
@@ -24,12 +24,6 @@
#include "pw_log_tokenized/metadata.h"
#endif // __cplusplus
-#ifdef _PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
-#endif // _PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT
-
-#undef _PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT
-
// This macro implements PW_LOG using pw_tokenizer. Users must implement
// pw_log_tokenized_HandleLog(uint32_t metadata, uint8_t* buffer, size_t size).
// The log level, module token, and flags are packed into the metadata argument.
diff --git a/pw_log_tokenized/public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h b/pw_log_tokenized/public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h
new file mode 100644
index 000000000..96c1beda2
--- /dev/null
+++ b/pw_log_tokenized/public_overrides/pw_log_backend/log_backend_uses_pw_tokenizer.h
@@ -0,0 +1,21 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// DO NOT INCLUDE! Empty file.
+// pw_log/tokenized_args.h checks for the existence of this file to switch
+// between logging token arguments and non-token arguments for non-tokenizing
+// logging backends.
+#pragma once
+
+static_assert(false, "Don't include me!");
diff --git a/pw_log_tokenized/py/BUILD.gn b/pw_log_tokenized/py/BUILD.gn
index c9e376eec..de0e98869 100644
--- a/pw_log_tokenized/py/BUILD.gn
+++ b/pw_log_tokenized/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [ "pw_log_tokenized/__init__.py" ]
tests = [
diff --git a/pw_log_tokenized/py/setup.py b/pw_log_tokenized/py/setup.py
deleted file mode 100644
index 686aeb760..000000000
--- a/pw_log_tokenized/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_log_tokenized"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_log_zephyr/CMakeLists.txt b/pw_log_zephyr/CMakeLists.txt
index 3f39df97d..20f1e4440 100644
--- a/pw_log_zephyr/CMakeLists.txt
+++ b/pw_log_zephyr/CMakeLists.txt
@@ -14,34 +14,77 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(CONFIG_PIGWEED_LOG_ZEPHYR)
- pw_add_library(pw_log_zephyr STATIC
- HEADERS
- public/pw_log_zephyr/log_zephyr.h
- public/pw_log_zephyr/config.h
- public_overrides/pw_log_backend/log_backend.h
- PUBLIC_INCLUDES
- public
- public_overrides
- PUBLIC_DEPS
- pw_log.facade
- zephyr_interface
- SOURCES
- log_zephyr.cc
- PRIVATE_DEPS
- pw_preprocessor
- )
- zephyr_link_libraries(pw_log_zephyr)
-endif()
+pw_add_library(pw_log_zephyr STATIC
+ HEADERS
+ public/pw_log_zephyr/log_zephyr.h
+ public/pw_log_zephyr/config.h
+ public_overrides/pw_log_backend/log_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_log.facade
+ SOURCES
+ log_zephyr.cc
+ PRIVATE_DEPS
+ pw_preprocessor
+)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_LOG_ZEPHYR
+ pw_log.facade
+ pw_log_zephyr
+)
+
+pw_add_library(pw_log_zephyr.tokenized_lib INTERFACE
+ HEADERS
+ zephyr_public_overrides/zephyr_custom_log.h
+ PUBLIC_INCLUDES
+ zephyr_public_overrides
+ PUBLIC_DEPS
+ pw_log.facade
+ pw_log_tokenized
+ pw_log_tokenized.base64
+ pw_tokenizer
+)
+pw_add_library(pw_log_zephyr.tokenized_handler STATIC
+ SOURCES
+ pw_log_zephyr_tokenized_handler.cc
+ PUBLIC_DEPS
+ pw_log_tokenized.handler
+ pw_log_zephyr.tokenized_lib
+)
+
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED
+ pw_log.facade
+ pw_log_tokenized
+ pw_log_zephyr.tokenized_handler
+)
if(CONFIG_PIGWEED_LOG_TOKENIZED)
- pw_add_library(pw_log_zephyr.tokenized_handler STATIC
- SOURCES
- pw_log_zephyr_tokenized_handler.cc
- PRIVATE_DEPS
- pw_log_tokenized.handler
- pw_tokenizer
- )
- zephyr_link_libraries(pw_log pw_log_zephyr.tokenized_handler)
- zephyr_include_directories(public_overrides)
+ zephyr_include_directories(zephyr_public_overrides)
endif()
+
+pw_add_library(pw_log_zephyr.tokenized_rpc_handler INTERFACE
+ PUBLIC_DEPS
+ pw_checksum
+ pw_sys_io
+ pw_system.init
+ pw_metric.global
+ pw_system.log_backend
+ HEADERS
+ zephyr_public_overrides/zephyr_custom_log.h
+)
+
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED_RPC
+ pw_log.facade
+ pw_log_tokenized
+ pw_log_zephyr.tokenized_rpc_handler
+)
+if(CONFIG_PIGWEED_LOG_TOKENIZED_RPC)
+ zephyr_include_directories(zephyr_public_overrides)
+endif()
+
+# Map tokenized configuration options
+pw_set_config_from_zephyr(CONFIG_PIGWEED_LOG_TOKENIZED_LEVEL_BITS PW_LOG_TOKENIZED_LEVEL_BITS)
+pw_set_config_from_zephyr(CONFIG_PIGWEED_LOG_TOKENIZED_LINE_BITS PW_LOG_TOKENIZED_LINE_BITS)
+pw_set_config_from_zephyr(CONFIG_PIGWEED_LOG_TOKENIZED_FLAG_BITS PW_LOG_TOKENIZED_FLAG_BITS)
+pw_set_config_from_zephyr(CONFIG_PIGWEED_LOG_TOKENIZED_MODULE_BITS PW_LOG_TOKENIZED_MODULE_BITS) \ No newline at end of file
diff --git a/pw_log_zephyr/Kconfig b/pw_log_zephyr/Kconfig
index 59a971be9..d9eee9ffb 100644
--- a/pw_log_zephyr/Kconfig
+++ b/pw_log_zephyr/Kconfig
@@ -12,10 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_log"
+
choice PIGWEED_LOG
prompt "Logging backend used"
help
- The type of Zephyr pw_log backend to use.
+ The type of Zephyr pw_log backend to use. See :ref:`module-pw_log` for
+ module details
config PIGWEED_LOG_ZEPHYR
bool "Zephyr logging for PW_LOG_* statements"
@@ -23,30 +26,112 @@ config PIGWEED_LOG_ZEPHYR
help
Once the Pigweed logging is enabled, all Pigweed logs via PW_LOG_*() will
be routed to the Zephyr logging system. This means that:
- - PW_LOG_LEVEL_DEBUG maps to Zephyr's LOG_LEVEL_DBG
- - PW_LOG_LEVEL_INFO maps to Zephyr's LOG_LEVEL_INF
- - PW_LOG_LEVEL_WARN maps to Zephyr's LOG_LEVEL_WRN
- - PW_LOG_LEVEL_ERROR maps to Zephyr's LOG_LEVEL_ERR
- - PW_LOG_LEVEL_CRITICAL maps to Zephyr's LOG_LEVEL_ERR
- - PW_LOG_LEVEL_FATAL maps to Zephyr's LOG_LEVEL_ERR
-
-config PIGWEED_LOG_TOKENIZED
+ - :c:macro:`PW_LOG_LEVEL_DEBUG` maps to Zephyr's LOG_LEVEL_DBG
+ - :c:macro:`PW_LOG_LEVEL_INFO` maps to Zephyr's LOG_LEVEL_INF
+ - :c:macro:`PW_LOG_LEVEL_WARN` maps to Zephyr's LOG_LEVEL_WRN
+ - :c:macro:`PW_LOG_LEVEL_ERROR` maps to Zephyr's LOG_LEVEL_ERR
+ - :c:macro:`PW_LOG_LEVEL_CRITICAL` maps to Zephyr's LOG_LEVEL_ERR
+ - :c:macro:`PW_LOG_LEVEL_FATAL` maps to Zephyr's LOG_LEVEL_ERR
+
+menuconfig PIGWEED_LOG_TOKENIZED
bool "Maps all Zephyr log macros to tokenized PW_LOG_* macros"
select PIGWEED_PREPROCESSOR
+ select PIGWEED_SYNC_INTERRUPT_SPIN_LOCK
+ select PIGWEED_SYS_IO
select PIGWEED_TOKENIZER
select LOG_CUSTOM_HEADER
help
Map all the Zephyr log macros to use Pigweed's then use the
- 'pw_log_tokenized' target as the logging backend in order to
+ :ref:`module-pw_log_tokenized` target as the logging backend in order to
automatically tokenize all the logging strings. This means that Pigweed
will also tokenize all of Zephyr's logging statements.
+config PIGWEED_LOG_TOKENIZED_LIB
+ bool "Tokenize logging and implement your own pw_log_tokenized_HandleLog"
+ select PIGWEED_TOKENIZER
+ select LOG_CUSTOM_HEADER
+ help
+ Same as PIGWEED_LOG_TOKENIZED but you'll need to implement
+ pw_log_tokenized_HandleLog. This gives you flexiblity to access handlers
+ outside of pigweed.
+
+config PIGWEED_LOG_NONE
+ bool "Do not use pigweed logging"
+ help
+ Pigweed log macros will not work unless the application manually
+ configures the logging backend.
+
+menuconfig PIGWEED_LOG_TOKENIZED_RPC
+ bool "Enables RPC tokenized logging"
+ select PIGWEED_SYNC_INTERRUPT_SPIN_LOCK
+ select PIGWEED_SYNC_THREAD_NOTIFICATION
+ select PIGWEED_SYNC_TIMED_THREAD_NOTIFICATION
+ select PIGWEED_SYSTEM_HDLC_RPC_SERVER
+ select PIGWEED_SYSTEM_LOG_BACKEND
+ select PIGWEED_SYSTEM_TARGET_HOOKS
+ select PIGWEED_THREAD
+ select PIGWEED_THREAD_ITERATION
+ select PIGWEED_TOKENIZER
+ select LOG_CUSTOM_HEADER
+ depends on CONSOLE_GETCHAR
+ help
+ Enable tokenized logging over RPC by using the system log backend.
+
endchoice
-if PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED
+if PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED || PIGWEED_LOG_TOKENIZED_RPC
+
+choice "PIGWEED_LOG_LEVEL_CHOICE"
+ prompt "Max compiled-in log level for pigweed"
+ default PIGWEED_LOG_LEVEL_DEFAULT
+ depends on LOG
+
+config PIGWEED_LOG_LEVEL_OFF
+ bool "Off"
+ help
+ Turn off all Pigweed logging.
+
+config PIGWEED_LOG_LEVEL_ERR
+ bool "Error"
+ help
+ Only print error level log statements.
+
+config PIGWEED_LOG_LEVEL_WRN
+ bool "Warning"
+ help
+ Only print warning level log statements and above.
+
+config PIGWEED_LOG_LEVEL_INF
+ bool "Info"
+ help
+ Only print info level log statements and above.
+
+config PIGWEED_LOG_LEVEL_DBG
+ bool "Debug"
+ help
+ Print all log statements.
+
+config PIGWEED_LOG_LEVEL_DEFAULT
+ bool "Default"
+ help
+ Use Zephyr's ``LOG_DEFAULT_LEVEL`` as the log level.
+
+endchoice
+
+config PIGWEED_LOG_LEVEL
+ int
+ depends on LOG
+ default 0 if PIGWEED_LOG_LEVEL_OFF
+ default 1 if PIGWEED_LOG_LEVEL_ERR
+ default 2 if PIGWEED_LOG_LEVEL_WRN
+ default 3 if PIGWEED_LOG_LEVEL_INF
+ default 4 if PIGWEED_LOG_LEVEL_DBG
+ default LOG_DEFAULT_LEVEL if PIGWEED_LOG_LEVEL_DEFAULT
+
+endif # PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED || PIGWEED_LOG_TOKENIZED_RPC
-module = PIGWEED
-module-str = "pigweed"
-source "subsys/logging/Kconfig.template.log_config"
+if PIGWEED_LOG_TOKENIZED
+rsource "Kconfig.tokenized"
+endif # PIGWEED_LOG_TOKENIZED
-endif # PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED
+endmenu
diff --git a/pw_log_zephyr/Kconfig.tokenized b/pw_log_zephyr/Kconfig.tokenized
new file mode 100644
index 000000000..64a313890
--- /dev/null
+++ b/pw_log_zephyr/Kconfig.tokenized
@@ -0,0 +1,53 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+config PIGWEED_LOG_TOKENIZED_LEVEL_BITS
+ int "Number of bits required to present the log level"
+ default 3
+ help
+ The total representation size of the metadata is the sum of
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LEVEL_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LINE_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_FLAG_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_MODULE_BITS``
+
+config PIGWEED_LOG_TOKENIZED_LINE_BITS
+ int "Number of bits required to present the log line number"
+ default 11
+ help
+ The total representation size of the metadata is the sum of
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LEVEL_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LINE_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_FLAG_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_MODULE_BITS``
+
+config PIGWEED_LOG_TOKENIZED_FLAG_BITS
+ int "Number of bits required for implementation-defined flags"
+ default 2
+ help
+ The total representation size of the metadata is the sum of
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LEVEL_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LINE_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_FLAG_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_MODULE_BITS``
+
+config PIGWEED_LOG_TOKENIZED_MODULE_BITS
+ int "Number of bits required for logging the PW_LOG_MODULE_NAME"
+ default 16
+ help
+ The total representation size of the metadata is the sum of
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LEVEL_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_LINE_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_FLAG_BITS``
+ - ``CONFIG_PIGWEED_LOG_TOKENIZED_MODULE_BITS``
diff --git a/pw_log_zephyr/docs.rst b/pw_log_zephyr/docs.rst
index beb6a691f..62638e011 100644
--- a/pw_log_zephyr/docs.rst
+++ b/pw_log_zephyr/docs.rst
@@ -23,10 +23,22 @@ Using Pigweed tokenized logging
-------------------------------
Using the pigweed logging can be done by enabling
``CONFIG_PIGWEED_LOG_TOKENIZED=y``. At that point ``pw_log_tokenized`` is set
-as the backedn for ``pw_log`` and all Zephyr logs are routed to Pigweed's
+as the backend for ``pw_log`` and all Zephyr logs are routed to Pigweed's
logging facade. This means that any logging statements made in Zephyr itself
are also tokenized.
+When enabled, a few extra configurations are available to control the tokenized
+metadata bits such as log level bits, line number bits, custom flag bits, and
+module string bits.
+The log format string may also be modified by defining your own ``PW_LOG_TOKENIZED_FORMAT_STRING``.
+This can be done in your cmake by including your own header that defines it.
+
+.. code-block::
+
+ add_library(log_tokenized_config INTERFACE)
+ target_compile_options(log_tokenized_config INTERFACE -include header_file_that_sets_that_macro.h)
+ pw_set_module_config(pw_log_tokenized_CONFIG log_tokenized_config)
+
Setting the log level
---------------------
In order to remain compatible with existing Pigweed code, the logging backend
diff --git a/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc b/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
index bdc64ed49..8e4fd3c7f 100644
--- a/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
+++ b/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
@@ -12,20 +12,47 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <zephyr/logging/log_backend.h>
-#include <zephyr/logging/log_msg.h>
+#include <pw_log_tokenized/config.h>
+#include <pw_log_tokenized/handler.h>
+#include <pw_log_tokenized/metadata.h>
+#include <pw_span/span.h>
+#include <pw_sync/interrupt_spin_lock.h>
+#include <pw_tokenizer/base64.h>
+#include <zephyr/logging/log_core.h>
-#include "pw_log_tokenized/handler.h"
+namespace pw::log_zephyr {
+namespace {
-namespace pw::log_tokenized {
+// The Zephyr console may output raw text along with Base64 tokenized messages,
+// which could interfere with detokenization. Output a character to mark the end
+// of a Base64 message.
+constexpr char kEndDelimiter = '#';
+
+sync::InterruptSpinLock log_encode_lock;
+
+} // namespace
extern "C" void pw_log_tokenized_HandleLog(uint32_t metadata,
const uint8_t log_buffer[],
size_t size_bytes) {
- ARG_UNUSED(metadata);
- ARG_UNUSED(log_buffer);
- ARG_UNUSED(size_bytes);
- // TODO(asemjonovs): implement this function
+ pw::log_tokenized::Metadata meta(metadata);
+
+ // Encode the tokenized message as Base64.
+ const InlineBasicString base64_string =
+ log_tokenized::PrefixedBase64Encode(log_buffer, size_bytes);
+
+ if (base64_string.empty()) {
+ return;
+ }
+
+ // TODO(asemjonovs): https://github.com/zephyrproject-rtos/zephyr/issues/59454
+ // Zephyr frontend should protect messages from getting corrupted
+ // from multiple threads.
+ log_encode_lock.lock();
+ // _is_raw is set to 0 here because the print string is required to be a
+ // string literal if _is_raw is set to 1.
+ Z_LOG_PRINTK(/*_is_raw=*/0, "%s%c", base64_string.c_str(), kEndDelimiter);
+ log_encode_lock.unlock();
}
-} // namespace pw::log_tokenized
+} // namespace pw::log_zephyr
diff --git a/pw_log_zephyr/py/pw_log_zephyr/pw_zephyr_detokenizer.py b/pw_log_zephyr/py/pw_log_zephyr/pw_zephyr_detokenizer.py
new file mode 100644
index 000000000..b51f83af3
--- /dev/null
+++ b/pw_log_zephyr/py/pw_log_zephyr/pw_zephyr_detokenizer.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+r"""Used to parse a data stream containing base64 tokenized messages and
+ raw text from Zephyr console. A tokenized message will be decoded
+ and sent to the log handler. The raw text is just flushed to the log
+ handler.
+"""
+
+import enum
+import os
+
+from pathlib import Path
+from typing import Iterable, Optional, Callable, Union, Any
+
+from pw_tokenizer.detokenize import AutoUpdatingDetokenizer
+from pw_tokenizer import detokenize, encode
+
+
+class TokenStatus(enum.Enum):
+ """Indicates that an error occurred."""
+
+ OK = 'OK'
+ TOKEN_ERROR = 'invalid flag or escape characters'
+
+
+class _State(enum.Enum):
+ RAW_TEXT = 0
+ TOKEN_STARTED = 1
+ TOKEN_ENDED = 2
+
+
+PREFIX = ord('$')
+EOT = ord('#')
+
+_PathOrStr = Union[Path, str]
+
+
+class Token:
+ """Represents an Zephyr Base64 Token."""
+
+ def __init__(
+ self,
+ raw_encoded: bytes,
+ raw_decoded: bytes,
+ status: TokenStatus = TokenStatus.OK,
+ ):
+ """Parses fields from an Zephyr Token.
+
+ Arguments:
+ raw_encoded: The complete base64 message
+ raw_decoded: The complete base64 token.
+ status: Whether parsing the token succeeded.
+ """
+ self.raw_encoded = raw_encoded
+ self.raw_decoded = raw_decoded
+ self.status = status
+ self.data: bytes = b''
+
+ if status == TokenStatus.OK:
+ self.data = raw_decoded
+
+ def ok(self) -> bool:
+ """True if this represents a valid token.
+
+ If false, then parsing failed. The status is set to indicate what type
+ of error occurred, and the data field contains all bytes parsed from the
+ token.
+ """
+ return self.status is TokenStatus.OK
+
+ def __repr__(self) -> str:
+ if self.ok():
+ body = f'data={self.data!r}'
+ else:
+ body = (
+ f'raw_encoded={self.raw_encoded!r}, '
+ f'status={str(self.status)}'
+ )
+
+ return f'{type(self).__name__}({body})'
+
+
+class ZephyrTokenDecoder:
+ """Decodes one or more Zephyr tokens from a stream of data."""
+
+ def __init__(self) -> None:
+ self._decoded_data = bytearray()
+ self._raw_data = bytearray()
+ self._state = _State.RAW_TEXT
+
+ def process(self, data: bytes) -> Iterable[Token]:
+ """Decodes and yields Zephyr tokens, including corrupt tokens.
+
+ The ok() method on Token indicates whether it is valid or represents a
+ token parsing error.
+
+ Yields:
+ Tokens, which may be valid (token.ok()) or corrupt (!token.ok())
+ """
+ for byte in data:
+ token = self.process_byte(byte)
+ if token:
+ yield token
+
+ def process_valid_tokens(self, data: bytes) -> Iterable[Token]:
+ """Decodes and yields valid Zephyr tokens, logging any errors."""
+ for token in self.process(data):
+ if token.ok():
+ yield token
+ else:
+ print(
+ 'Failed to decode token: %s; discarded %d bytes',
+ token.status.value,
+ len(token.raw_encoded),
+ )
+ print('Discarded data: %s', token.raw_encoded)
+
+ def _finish_token(self, status: TokenStatus) -> Token:
+ token = Token(bytes(self._raw_data), bytes(self._decoded_data), status)
+ self._raw_data.clear()
+ self._decoded_data.clear()
+ return token
+
+ def process_byte(self, byte: int) -> Optional[Token]:
+ """Processes a single byte and returns a token if one was completed."""
+ token: Optional[Token] = None
+
+ self._raw_data.append(byte)
+
+ if self._state is _State.RAW_TEXT:
+ if byte == PREFIX:
+ self._state = _State.TOKEN_STARTED
+ self._decoded_data.append(byte)
+ elif self._state is _State.TOKEN_STARTED:
+ if byte == EOT:
+ self._state = _State.TOKEN_ENDED
+ token = self._finish_token(TokenStatus.OK)
+ else:
+ self._decoded_data.append(byte)
+ elif self._state is _State.TOKEN_ENDED:
+ if byte == PREFIX:
+ self._state = _State.TOKEN_STARTED
+ self._decoded_data.append(byte)
+ else:
+ self._state = _State.RAW_TEXT
+ else:
+ raise AssertionError(f'Invalid decoder state: {self._state}')
+
+ return token
+
+
+class ZephyrDetokenizer:
+ """Processes both base64 message and non-token data in a stream."""
+
+ def __init__(
+ self, log_handler: Callable[[bytes], Any], *paths_or_files: _PathOrStr
+ ) -> None:
+ """Yields valid Zephyr tokens and passes non-token data to callback."""
+ self._log_handler = log_handler
+
+ self._raw_data = bytearray()
+ self._zephyr_decoder = ZephyrTokenDecoder()
+
+ print(f'Loading Token database: {paths_or_files}')
+ self.detokenizer = AutoUpdatingDetokenizer(*paths_or_files)
+ self.detokenizer.show_errors = True
+ self.base64_re = detokenize._base64_message_regex(b'$')
+
+ def flush_non_frame_data(self) -> None:
+ """Flushes any data in the buffer as non-token data.
+
+ If a valid token was flushed partway, the data for the first part
+ of the frame will be included both in the raw data and in the frame.
+ """
+ self._flush_non_frame()
+
+ def _flush_non_frame(self, to_index: Optional[int] = None):
+ if self._raw_data:
+ self._log_handler(bytes(self._raw_data[:to_index]))
+ del self._raw_data[:to_index]
+
+ def decode(self, data: bytes):
+ for token in self.process(data):
+ if token.ok():
+ print(f'token: {token.data}')
+ result = detokenize.detokenize_base64(
+ self.detokenizer, token.data
+ )
+ print(f'result: {result.decode()}')
+ self._log_handler(result)
+
+ def process(self, data: bytes) -> Iterable[Token]:
+ """Processes a stream of mixed Tokens and raw text data.
+
+ Yields OK tokens and calls log_handler with raw text data.
+ """
+ for byte in data:
+ yield from self._process_byte(byte)
+
+ if self._zephyr_decoder._state is _State.RAW_TEXT:
+ self._flush_non_frame()
+
+ def _process_byte(self, byte: int) -> Iterable[Token]:
+ self._raw_data.append(byte)
+ token = self._zephyr_decoder.process_byte(byte)
+
+ if token is None:
+ return
+
+ if token.ok():
+ # Drop the valid token from the data. Only drop matching bytes in
+ # case the token was flushed prematurely.
+ for suffix_byte in reversed(token.raw_encoded):
+ if not self._raw_data or self._raw_data[-1] != suffix_byte:
+ break
+ self._raw_data.pop()
+
+ self._flush_non_frame() # Flush the raw data before the token.
+
+ yield token
+ else:
+ # Don't flush a final flag byte yet because it might be the start of
+ # an token.
+ to_index = -1 if self._raw_data[-1] == PREFIX else None
+ self._flush_non_frame(to_index)
diff --git a/pw_trace_tokenized/py/pyproject.toml b/pw_log_zephyr/py/pyproject.toml
index 798b747ec..798b747ec 100644
--- a/pw_trace_tokenized/py/pyproject.toml
+++ b/pw_log_zephyr/py/pyproject.toml
diff --git a/pw_trace_tokenized/py/setup.cfg b/pw_log_zephyr/py/setup.cfg
index 98038d397..fbbb666aa 100644
--- a/pw_trace_tokenized/py/setup.cfg
+++ b/pw_log_zephyr/py/setup.cfg
@@ -12,18 +12,15 @@
# License for the specific language governing permissions and limitations under
# the License.
[metadata]
-name = pw_trace_tokenized
+name = pw_log_zephyr
version = 0.0.1
author = Pigweed Authors
author_email = pigweed-developers@googlegroups.com
-description = pw_trace backend to tokenize trace events
+description = Tools for working with Zephyr tokenized logs
[options]
packages = find:
zip_safe = False
-install_requires =
- pyserial>=3.5,<4.0
- types-pyserial>=3.5,<4.0
[options.package_data]
-pw_trace_tokenized = py.typed
+pw_log_zephyr = py.typed
diff --git a/pw_log_zephyr/public_overrides/zephyr_custom_log.h b/pw_log_zephyr/zephyr_public_overrides/zephyr_custom_log.h
index 8485d5e02..1b96e3b28 100644
--- a/pw_log_zephyr/public_overrides/zephyr_custom_log.h
+++ b/pw_log_zephyr/zephyr_public_overrides/zephyr_custom_log.h
@@ -14,6 +14,7 @@
#pragma once
+#include <zephyr/logging/log_core.h>
#include <zephyr/sys/__assert.h>
// If static_assert wasn't defined by zephyr/sys/__assert.h that means it's not
@@ -29,7 +30,19 @@
#undef LOG_WRN
#undef LOG_ERR
-#define LOG_DBG(format, ...) PW_LOG_DEBUG(format, ##__VA_ARGS__)
-#define LOG_INF(format, ...) PW_LOG_INFO(format, ##__VA_ARGS__)
-#define LOG_WRN(format, ...) PW_LOG_WARN(format, ##__VA_ARGS__)
-#define LOG_ERR(format, ...) PW_LOG_ERROR(format, ##__VA_ARGS__)
+#define Z_PW_LOG(_level, fn, format, ...) \
+ do { \
+ if (!Z_LOG_CONST_LEVEL_CHECK(_level)) { \
+ break; \
+ } \
+ fn(format, ##__VA_ARGS__); \
+ } while (false)
+
+#define LOG_DBG(format, ...) \
+ Z_PW_LOG(LOG_LEVEL_DBG, PW_LOG_DEBUG, format, ##__VA_ARGS__)
+#define LOG_INF(format, ...) \
+ Z_PW_LOG(LOG_LEVEL_INF, PW_LOG_INFO, format, ##__VA_ARGS__)
+#define LOG_WRN(format, ...) \
+ Z_PW_LOG(LOG_LEVEL_WRN, PW_LOG_WARN, format, ##__VA_ARGS__)
+#define LOG_ERR(format, ...) \
+ Z_PW_LOG(LOG_LEVEL_ERR, PW_LOG_ERROR, format, ##__VA_ARGS__)
diff --git a/pw_malloc/BUILD.bazel b/pw_malloc/BUILD.bazel
index b4c9169b8..8a65a4585 100644
--- a/pw_malloc/BUILD.bazel
+++ b/pw_malloc/BUILD.bazel
@@ -32,14 +32,17 @@ pw_cc_facade(
pw_cc_library(
name = "pw_malloc",
+ hdrs = [
+ "public/pw_malloc/malloc.h",
+ ],
+ includes = ["public"],
deps = [
- ":facade",
- "@pigweed_config//:pw_malloc_backend",
+ "@pigweed//targets:pw_malloc_backend",
],
)
pw_cc_library(
name = "backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = ["//pw_malloc_freelist"],
)
diff --git a/pw_malloc/docs.rst b/pw_malloc/docs.rst
index 2c0357d3d..53249f5e4 100644
--- a/pw_malloc/docs.rst
+++ b/pw_malloc/docs.rst
@@ -3,7 +3,6 @@
---------
pw_malloc
---------
-
This module defines an interface for replacing the standard libc dynamic memory
operations.
@@ -19,9 +18,9 @@ Setup
=====
This module requires the following setup:
- 1. Chose a ``pw_malloc`` backend, or write one yourself.
- 2. If using GN build, Specify the ``pw_malloc_BACKEND`` GN build arg to point
- the library that provides a ``pw_malloc`` backend.
+1. Chose a ``pw_malloc`` backend, or write one yourself.
+2. If using GN build, Specify the ``pw_malloc_BACKEND`` GN build arg to point
+ the library that provides a ``pw_malloc`` backend.
Module usage
============
diff --git a/pw_malloc_freelist/BUILD.bazel b/pw_malloc_freelist/BUILD.bazel
index c0e6a8c9c..4822b1c0e 100644
--- a/pw_malloc_freelist/BUILD.bazel
+++ b/pw_malloc_freelist/BUILD.bazel
@@ -63,7 +63,7 @@ pw_cc_test(
srcs = [
"freelist_malloc_test.cc",
],
- # TODO(b/257530518): Get this test to work with Bazel.
+ # TODO: b/257530518 - Get this test to work with Bazel.
tags = ["manual"],
deps = [
":headers",
diff --git a/pw_metric/Android.bp b/pw_metric/Android.bp
new file mode 100644
index 000000000..7349e15bb
--- /dev/null
+++ b/pw_metric/Android.bp
@@ -0,0 +1,56 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package{
+ default_applicable_licenses : ["external_pigweed_license"],
+}
+
+cc_library {
+ name: "pw_metric",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_span_headers",
+ "pw_preprocessor_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_span_headers",
+ "pw_preprocessor_headers",
+ ],
+ static_libs: [
+ "pw_base64",
+ "pw_containers",
+ "pw_tokenizer",
+ "pw_tokenizer_base64",
+ ],
+ export_static_lib_headers: [
+ "pw_base64",
+ "pw_containers",
+ "pw_tokenizer",
+ "pw_tokenizer_base64",
+ ],
+ srcs: [
+ "metric.cc",
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
diff --git a/pw_metric/BUILD.bazel b/pw_metric/BUILD.bazel
index 2eae45c18..0f44d9f41 100644
--- a/pw_metric/BUILD.bazel
+++ b/pw_metric/BUILD.bazel
@@ -17,8 +17,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -70,7 +70,7 @@ pw_cc_library(
name = "metric_service_nanopb",
srcs = ["metric_service_nanopb.cc"],
hdrs = ["public/pw_metric/metric_service_nanopb.h"],
- # TODO(b/258078909): Get this target to build.
+ # TODO: b/258078909 - Get this target to build.
tags = ["manual"],
deps = [
":metric",
@@ -110,7 +110,7 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "metric_proto_py_pb2",
tags = ["manual"],
@@ -147,7 +147,7 @@ pw_cc_test(
srcs = [
"metric_service_nanopb_test.cc",
],
- # TODO(b/258078909): Get this target to build.
+ # TODO: b/258078909 - Get this target to build.
tags = ["manual"],
deps = [
":metric_service_nanopb",
diff --git a/pw_metric/BUILD.gn b/pw_metric/BUILD.gn
index 26b2fea85..e5ca6aeb2 100644
--- a/pw_metric/BUILD.gn
+++ b/pw_metric/BUILD.gn
@@ -37,6 +37,9 @@ pw_source_set("pw_metric") {
dir_pw_tokenizer,
]
deps = [ dir_pw_span ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
# This gives access to the "PW_METRIC_GLOBAL()" macros, for globally-registered
diff --git a/pw_metric/CMakeLists.txt b/pw_metric/CMakeLists.txt
index 0a4233437..c599e6903 100644
--- a/pw_metric/CMakeLists.txt
+++ b/pw_metric/CMakeLists.txt
@@ -43,6 +43,41 @@ pw_add_library(pw_metric.global STATIC
global.cc
)
+pw_proto_library(pw_metric.metric_service_proto
+ SOURCES
+ pw_metric_proto/metric_service.proto
+ INPUTS
+ pw_metric_proto/metric_service.options
+)
+
+pw_add_library(pw_metric.metric_walker INTERFACE
+ HEADERS
+ pw_metric_private/metric_walker.h
+ PUBLIC_DEPS
+ pw_metric
+ pw_assert
+ pw_containers
+ pw_status
+ pw_tokenizer
+)
+
+pw_add_library(pw_metric.metric_service_pwpb STATIC
+ HEADERS
+ public/pw_metric/metric_service_pwpb.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_metric.metric_service_proto.pwpb_rpc
+ pw_metric.metric_service_proto.raw_rpc
+ pw_metric.metric_walker
+ pw_metric
+ pw_bytes
+ pw_containers
+ pw_rpc.raw.server_api
+ SOURCES
+ metric_service_pwpb.cc
+)
+
pw_add_test(pw_metric.metric_test
SOURCES
metric_test.cc
diff --git a/pw_metric/docs.rst b/pw_metric/docs.rst
index 93863658e..21360dd24 100644
--- a/pw_metric/docs.rst
+++ b/pw_metric/docs.rst
@@ -5,9 +5,8 @@ pw_metric
=========
.. attention::
-
- This module is **not yet production ready**; ask us if you are interested in
- using it out or have ideas about how to improve it.
+ This module is **not yet production ready**; ask us if you are interested in
+ using it out or have ideas about how to improve it.
--------
Overview
@@ -45,30 +44,30 @@ and metrics might look like. In this case, the object's
``MySubsystem::metrics()`` member is not globally registered; the user is on
their own for combining this subsystem's metrics with others.
-.. code::
+.. code-block::
- #include "pw_metric/metric.h"
+ #include "pw_metric/metric.h"
- class MySubsystem {
- public:
- void DoSomething() {
- attempts_.Increment();
- if (ActionSucceeds()) {
- successes_.Increment();
- }
- }
- Group& metrics() { return metrics_; }
+ class MySubsystem {
+ public:
+ void DoSomething() {
+ attempts_.Increment();
+ if (ActionSucceeds()) {
+ successes_.Increment();
+ }
+ }
+ Group& metrics() { return metrics_; }
- private:
- PW_METRIC_GROUP(metrics_, "my_subsystem");
- PW_METRIC(metrics_, attempts_, "attempts", 0u);
- PW_METRIC(metrics_, successes_, "successes", 0u);
- };
+ private:
+ PW_METRIC_GROUP(metrics_, "my_subsystem");
+ PW_METRIC(metrics_, attempts_, "attempts", 0u);
+ PW_METRIC(metrics_, successes_, "successes", 0u);
+ };
The metrics subsystem has no canonical output format at this time, but a JSON
dump might look something like this:
-.. code:: none
+.. code-block:: none
{
"my_subsystem" : {
@@ -89,39 +88,39 @@ this use case. For example:
**Before instrumenting:**
-.. code::
+.. code-block::
- // This code was passed down from generations of developers before; no one
- // knows what it does or how it works. But it needs to be fixed!
- void OldCodeThatDoesntWorkButWeDontKnowWhy() {
- if (some_variable) {
- DoSomething();
- } else {
- DoSomethingElse();
- }
- }
+ // This code was passed down from generations of developers before; no one
+ // knows what it does or how it works. But it needs to be fixed!
+ void OldCodeThatDoesntWorkButWeDontKnowWhy() {
+ if (some_variable) {
+ DoSomething();
+ } else {
+ DoSomethingElse();
+ }
+ }
**After instrumenting:**
-.. code::
-
- #include "pw_metric/global.h"
- #include "pw_metric/metric.h"
+.. code-block::
- PW_METRIC_GLOBAL(legacy_do_something, "legacy_do_something");
- PW_METRIC_GLOBAL(legacy_do_something_else, "legacy_do_something_else");
+ #include "pw_metric/global.h"
+ #include "pw_metric/metric.h"
- // This code was passed down from generations of developers before; no one
- // knows what it does or how it works. But it needs to be fixed!
- void OldCodeThatDoesntWorkButWeDontKnowWhy() {
- if (some_variable) {
- legacy_do_something.Increment();
- DoSomething();
- } else {
- legacy_do_something_else.Increment();
- DoSomethingElse();
- }
- }
+ PW_METRIC_GLOBAL(legacy_do_something, "legacy_do_something");
+ PW_METRIC_GLOBAL(legacy_do_something_else, "legacy_do_something_else");
+
+ // This code was passed down from generations of developers before; no one
+ // knows what it does or how it works. But it needs to be fixed!
+ void OldCodeThatDoesntWorkButWeDontKnowWhy() {
+ if (some_variable) {
+ legacy_do_something.Increment();
+ DoSomething();
+ } else {
+ legacy_do_something_else.Increment();
+ DoSomethingElse();
+ }
+ }
In this case, the developer merely had to add the metrics header, define some
metrics, and then start incrementing them. These metrics will be available
@@ -180,20 +179,20 @@ The metric object is 12 bytes on 32-bit platforms.
.. cpp:class:: pw::metric::Metric
- .. cpp:function:: Increment(uint32_t amount = 0)
+ .. cpp:function:: Increment(uint32_t amount = 0)
- Increment the metric by the given amount. Results in undefined behaviour if
- the metric is not of type int.
+ Increment the metric by the given amount. Results in undefined behaviour if
+ the metric is not of type int.
- .. cpp:function:: Set(uint32_t value)
+ .. cpp:function:: Set(uint32_t value)
- Set the metric to the given value. Results in undefined behaviour if the
- metric is not of type int.
+ Set the metric to the given value. Results in undefined behaviour if the
+ metric is not of type int.
- .. cpp:function:: Set(float value)
+ .. cpp:function:: Set(float value)
- Set the metric to the given value. Results in undefined behaviour if the
- metric is not of type float.
+ Set the metric to the given value. Results in undefined behaviour if the
+ metric is not of type float.
Group
-----
@@ -208,39 +207,39 @@ The group object is 16 bytes on 32-bit platforms.
.. cpp:class:: pw::metric::Group
- .. cpp:function:: Dump(int indent_level = 0)
+ .. cpp:function:: Dump(int indent_level = 0)
- Recursively dump a metrics group to ``pw_log``. Produces output like:
+ Recursively dump a metrics group to ``pw_log``. Produces output like:
- .. code:: none
+ .. code-block:: none
- "$6doqFw==": {
- "$05OCZw==": {
- "$VpPfzg==": 1,
- "$LGPMBQ==": 1.000000,
- "$+iJvUg==": 5,
- }
- "$9hPNxw==": 65,
- "$oK7HmA==": 13,
- "$FCM4qQ==": 0,
- }
+ "$6doqFw==": {
+ "$05OCZw==": {
+ "$VpPfzg==": 1,
+ "$LGPMBQ==": 1.000000,
+ "$+iJvUg==": 5,
+ }
+ "$9hPNxw==": 65,
+ "$oK7HmA==": 13,
+ "$FCM4qQ==": 0,
+ }
- Note the metric names are tokenized with base64. Decoding requires using
- the Pigweed detokenizer. With a detokenizing-enabled logger, you could get
- something like:
-
- .. code:: none
-
- "i2c_1": {
- "gyro": {
- "num_sampleses": 1,
- "init_time_us": 1.000000,
- "initialized": 5,
- }
- "bus_errors": 65,
- "transactions": 13,
- "bytes_sent": 0,
- }
+ Note the metric names are tokenized with base64. Decoding requires using
+ the Pigweed detokenizer. With a detokenizing-enabled logger, you could get
+ something like:
+
+ .. code-block:: none
+
+ "i2c_1": {
+ "gyro": {
+ "num_sampleses": 1,
+ "init_time_us": 1.000000,
+ "initialized": 5,
+ }
+ "bus_errors": 65,
+ "transactions": 13,
+ "bytes_sent": 0,
+ }
Macros
------
@@ -253,150 +252,147 @@ tokenizing the metric and group names.
.. cpp:function:: PW_METRIC_STATIC(identifier, name, value)
.. cpp:function:: PW_METRIC_STATIC(group, identifier, name, value)
- Declare a metric, optionally adding it to a group.
+ Declare a metric, optionally adding it to a group.
- - **identifier** - An identifier name for the created variable or member.
- For example: ``i2c_transactions`` might be used as a local or global
- metric; inside a class, could be named according to members
- (``i2c_transactions_`` for Google's C++ style).
- - **name** - The string name for the metric. This will be tokenized. There
- are no restrictions on the contents of the name; however, consider
- restricting these to be valid C++ identifiers to ease integration with
- other systems.
- - **value** - The initial value for the metric. Must be either a floating
- point value (e.g. ``3.2f``) or unsigned int (e.g. ``21u``).
- - **group** - A ``pw::metric::Group`` instance. If provided, the metric is
- added to the given group.
+ - **identifier** - An identifier name for the created variable or member.
+ For example: ``i2c_transactions`` might be used as a local or global
+ metric; inside a class, could be named according to members
+ (``i2c_transactions_`` for Google's C++ style).
+ - **name** - The string name for the metric. This will be tokenized. There
+ are no restrictions on the contents of the name; however, consider
+ restricting these to be valid C++ identifiers to ease integration with
+ other systems.
+ - **value** - The initial value for the metric. Must be either a floating
+ point value (e.g. ``3.2f``) or unsigned int (e.g. ``21u``).
+ - **group** - A ``pw::metric::Group`` instance. If provided, the metric is
+ added to the given group.
- The macro declares a variable or member named "name" with type
- ``pw::metric::Metric``, and works in three contexts: global, local, and
- member.
+ The macro declares a variable or member named "name" with type
+ ``pw::metric::Metric``, and works in three contexts: global, local, and
+ member.
- If the `_STATIC` variant is used, the macro declares a variable with static
- storage. These can be used in function scopes, but not in classes.
+ If the `_STATIC` variant is used, the macro declares a variable with static
+ storage. These can be used in function scopes, but not in classes.
- 1. At global scope:
+ 1. At global scope:
- .. code::
+ .. code-block::
- PW_METRIC(foo, "foo", 15.5f);
+ PW_METRIC(foo, "foo", 15.5f);
- void MyFunc() {
- foo.Increment();
- }
-
- 2. At local function or member function scope:
+ void MyFunc() {
+ foo.Increment();
+ }
- .. code::
+ 2. At local function or member function scope:
- void MyFunc() {
- PW_METRIC(foo, "foo", 15.5f);
- foo.Increment();
- // foo goes out of scope here; be careful!
- }
+ .. code-block::
- 3. At member level inside a class or struct:
+ void MyFunc() {
+ PW_METRIC(foo, "foo", 15.5f);
+ foo.Increment();
+ // foo goes out of scope here; be careful!
+ }
- .. code::
+ 3. At member level inside a class or struct:
- struct MyStructy {
- void DoSomething() {
- somethings.Increment();
- }
- // Every instance of MyStructy will have a separate somethings counter.
- PW_METRIC(somethings, "somethings", 0u);
- }
+ .. code-block::
- You can also put a metric into a group with the macro. Metrics can belong to
- strictly one group, otherwise an assertion will fail. Example:
+ struct MyStructy {
+ void DoSomething() {
+ somethings.Increment();
+ }
+ // Every instance of MyStructy will have a separate somethings counter.
+ PW_METRIC(somethings, "somethings", 0u);
+ }
- .. code::
+ You can also put a metric into a group with the macro. Metrics can belong to
+ strictly one group, otherwise an assertion will fail. Example:
- PW_METRIC_GROUP(my_group, "my_group");
- PW_METRIC(my_group, foo, "foo", 0.2f);
- PW_METRIC(my_group, bar, "bar", 44000u);
- PW_METRIC(my_group, zap, "zap", 3.14f);
+ .. code-block::
- .. tip::
+ PW_METRIC_GROUP(my_group, "my_group");
+ PW_METRIC(my_group, foo, "foo", 0.2f);
+ PW_METRIC(my_group, bar, "bar", 44000u);
+ PW_METRIC(my_group, zap, "zap", 3.14f);
- If you want a globally registered metric, see ``pw_metric/global.h``; in
- that contexts, metrics are globally registered without the need to
- centrally register in a single place.
+ .. tip::
+ If you want a globally registered metric, see ``pw_metric/global.h``; in
+ that contexts, metrics are globally registered without the need to
+ centrally register in a single place.
.. cpp:function:: PW_METRIC_GROUP(identifier, name)
.. cpp:function:: PW_METRIC_GROUP(parent_group, identifier, name)
.. cpp:function:: PW_METRIC_GROUP_STATIC(identifier, name)
.. cpp:function:: PW_METRIC_GROUP_STATIC(parent_group, identifier, name)
- Declares a ``pw::metric::Group`` with name name; the name is tokenized.
- Works similar to ``PW_METRIC`` and can be used in the same contexts (global,
- local, and member). Optionally, the group can be added to a parent group.
+ Declares a ``pw::metric::Group`` with name name; the name is tokenized.
+ Works similar to ``PW_METRIC`` and can be used in the same contexts (global,
+ local, and member). Optionally, the group can be added to a parent group.
- If the `_STATIC` variant is used, the macro declares a variable with static
- storage. These can be used in function scopes, but not in classes.
+ If the `_STATIC` variant is used, the macro declares a variable with static
+ storage. These can be used in function scopes, but not in classes.
- Example:
+ Example:
- .. code::
+ .. code-block::
- PW_METRIC_GROUP(my_group, "my_group");
- PW_METRIC(my_group, foo, "foo", 0.2f);
- PW_METRIC(my_group, bar, "bar", 44000u);
- PW_METRIC(my_group, zap, "zap", 3.14f);
+ PW_METRIC_GROUP(my_group, "my_group");
+ PW_METRIC(my_group, foo, "foo", 0.2f);
+ PW_METRIC(my_group, bar, "bar", 44000u);
+ PW_METRIC(my_group, zap, "zap", 3.14f);
.. cpp:function:: PW_METRIC_GLOBAL(identifier, name, value)
- Declare a ``pw::metric::Metric`` with name name, and register it in the
- global metrics list ``pw::metric::global_metrics``.
+ Declare a ``pw::metric::Metric`` with name name, and register it in the
+ global metrics list ``pw::metric::global_metrics``.
- Example:
+ Example:
- .. code::
+ .. code-block::
- #include "pw_metric/metric.h"
- #include "pw_metric/global.h"
+ #include "pw_metric/metric.h"
+ #include "pw_metric/global.h"
- // No need to coordinate collection of foo and bar; they're autoregistered.
- PW_METRIC_GLOBAL(foo, "foo", 0.2f);
- PW_METRIC_GLOBAL(bar, "bar", 44000u);
+ // No need to coordinate collection of foo and bar; they're autoregistered.
+ PW_METRIC_GLOBAL(foo, "foo", 0.2f);
+ PW_METRIC_GLOBAL(bar, "bar", 44000u);
- Note that metrics defined with ``PW_METRIC_GLOBAL`` should never be added to
- groups defined with ``PW_METRIC_GROUP_GLOBAL``. Each metric can only belong
- to one group, and metrics defined with ``PW_METRIC_GLOBAL`` are
- pre-registered with the global metrics list.
+ Note that metrics defined with ``PW_METRIC_GLOBAL`` should never be added to
+ groups defined with ``PW_METRIC_GROUP_GLOBAL``. Each metric can only belong
+ to one group, and metrics defined with ``PW_METRIC_GLOBAL`` are
+ pre-registered with the global metrics list.
- .. attention::
-
- Do not create ``PW_METRIC_GLOBAL`` instances anywhere other than global
- scope. Putting these on an instance (member context) would lead to dangling
- pointers and misery. Metrics are never deleted or unregistered!
+ .. attention::
+ Do not create ``PW_METRIC_GLOBAL`` instances anywhere other than global
+ scope. Putting these on an instance (member context) would lead to dangling
+ pointers and misery. Metrics are never deleted or unregistered!
.. cpp:function:: PW_METRIC_GROUP_GLOBAL(identifier, name, value)
- Declare a ``pw::metric::Group`` with name name, and register it in the
- global metric groups list ``pw::metric::global_groups``.
-
- Note that metrics created with ``PW_METRIC_GLOBAL`` should never be added to
- groups! Instead, just create a freestanding metric and register it into the
- global group (like in the example below).
+ Declare a ``pw::metric::Group`` with name name, and register it in the
+ global metric groups list ``pw::metric::global_groups``.
- Example:
+ Note that metrics created with ``PW_METRIC_GLOBAL`` should never be added to
+ groups! Instead, just create a freestanding metric and register it into the
+ global group (like in the example below).
- .. code::
+ Example:
- #include "pw_metric/metric.h"
- #include "pw_metric/global.h"
+ .. code-block::
- // No need to coordinate collection of this group; it's globally registered.
- PW_METRIC_GROUP_GLOBAL(leagcy_system, "legacy_system");
- PW_METRIC(leagcy_system, foo, "foo",0.2f);
- PW_METRIC(leagcy_system, bar, "bar",44000u);
+ #include "pw_metric/metric.h"
+ #include "pw_metric/global.h"
- .. attention::
+ // No need to coordinate collection of this group; it's globally registered.
+ PW_METRIC_GROUP_GLOBAL(leagcy_system, "legacy_system");
+ PW_METRIC(leagcy_system, foo, "foo",0.2f);
+ PW_METRIC(leagcy_system, bar, "bar",44000u);
- Do not create ``PW_METRIC_GROUP_GLOBAL`` instances anywhere other than
- global scope. Putting these on an instance (member context) would lead to
- dangling pointers and misery. Metrics are never deleted or unregistered!
+ .. attention::
+ Do not create ``PW_METRIC_GROUP_GLOBAL`` instances anywhere other than
+ global scope. Putting these on an instance (member context) would lead to
+ dangling pointers and misery. Metrics are never deleted or unregistered!
----------------------
Usage & Best Practices
@@ -413,62 +409,62 @@ to a pattern where rich/large objects are statically constructed at global
scope, then interacted with via tasks or threads. For example, consider a
hypothetical global ``Uart`` object:
-.. code::
+.. code-block::
- class Uart {
- public:
- Uart(span<std::byte> rx_buffer, span<std::byte> tx_buffer)
- : rx_buffer_(rx_buffer), tx_buffer_(tx_buffer) {}
+ class Uart {
+ public:
+ Uart(span<std::byte> rx_buffer, span<std::byte> tx_buffer)
+ : rx_buffer_(rx_buffer), tx_buffer_(tx_buffer) {}
- // Send/receive here...
+ // Send/receive here...
- private:
- pw::span<std::byte> rx_buffer;
- pw::span<std::byte> tx_buffer;
- };
+ private:
+ pw::span<std::byte> rx_buffer;
+ pw::span<std::byte> tx_buffer;
+ };
- std::array<std::byte, 512> uart_rx_buffer;
- std::array<std::byte, 512> uart_tx_buffer;
- Uart uart1(uart_rx_buffer, uart_tx_buffer);
+ std::array<std::byte, 512> uart_rx_buffer;
+ std::array<std::byte, 512> uart_tx_buffer;
+ Uart uart1(uart_rx_buffer, uart_tx_buffer);
Through the course of building a product, the team may want to add metrics to
the UART to for example gain insight into which operations are triggering lots
of data transfer. When adding metrics to the above imaginary UART object, one
might consider the following approach:
-.. code::
-
- class Uart {
- public:
- Uart(span<std::byte> rx_buffer,
- span<std::byte> tx_buffer,
- Group& parent_metrics)
- : rx_buffer_(rx_buffer),
- tx_buffer_(tx_buffer) {
- // PROBLEM! parent_metrics may not be constructed if it's a reference
- // to a static global.
- parent_metrics.Add(tx_bytes_);
- parent_metrics.Add(rx_bytes_);
- }
+.. code-block::
+
+ class Uart {
+ public:
+ Uart(span<std::byte> rx_buffer,
+ span<std::byte> tx_buffer,
+ Group& parent_metrics)
+ : rx_buffer_(rx_buffer),
+ tx_buffer_(tx_buffer) {
+ // PROBLEM! parent_metrics may not be constructed if it's a reference
+ // to a static global.
+ parent_metrics.Add(tx_bytes_);
+ parent_metrics.Add(rx_bytes_);
+ }
- // Send/receive here which increment tx/rx_bytes.
+ // Send/receive here which increment tx/rx_bytes.
- private:
- pw::span<std::byte> rx_buffer;
- pw::span<std::byte> tx_buffer;
+ private:
+ pw::span<std::byte> rx_buffer;
+ pw::span<std::byte> tx_buffer;
- PW_METRIC(tx_bytes_, "tx_bytes", 0);
- PW_METRIC(rx_bytes_, "rx_bytes", 0);
- };
+ PW_METRIC(tx_bytes_, "tx_bytes", 0);
+ PW_METRIC(rx_bytes_, "rx_bytes", 0);
+ };
- PW_METRIC_GROUP(global_metrics, "/");
- PW_METRIC_GROUP(global_metrics, uart1_metrics, "uart1");
+ PW_METRIC_GROUP(global_metrics, "/");
+ PW_METRIC_GROUP(global_metrics, uart1_metrics, "uart1");
- std::array<std::byte, 512> uart_rx_buffer;
- std::array<std::byte, 512> uart_tx_buffer;
- Uart uart1(uart_rx_buffer,
- uart_tx_buffer,
- uart1_metrics);
+ std::array<std::byte, 512> uart_rx_buffer;
+ std::array<std::byte, 512> uart_tx_buffer;
+ Uart uart1(uart_rx_buffer,
+ uart_tx_buffer,
+ uart1_metrics);
However, this **is incorrect**, since the ``parent_metrics`` (pointing to
``uart1_metrics`` in this case) may not be constructed at the point of
@@ -484,50 +480,49 @@ phases: The constructor where references are stored, and a ``Init()`` function
which is called after all static constructors have run. This approach works
correctly, even when the objects are allocated globally:
-.. code::
+.. code-block::
- class Uart {
- public:
- // Note that metrics is not passed in here at all.
- Uart(span<std::byte> rx_buffer,
- span<std::byte> tx_buffer)
- : rx_buffer_(rx_buffer),
- tx_buffer_(tx_buffer) {}
+ class Uart {
+ public:
+ // Note that metrics is not passed in here at all.
+ Uart(span<std::byte> rx_buffer,
+ span<std::byte> tx_buffer)
+ : rx_buffer_(rx_buffer),
+ tx_buffer_(tx_buffer) {}
- // Precondition: parent_metrics is already constructed.
- void Init(Group& parent_metrics) {
- parent_metrics.Add(tx_bytes_);
- parent_metrics.Add(rx_bytes_);
- }
+ // Precondition: parent_metrics is already constructed.
+ void Init(Group& parent_metrics) {
+ parent_metrics.Add(tx_bytes_);
+ parent_metrics.Add(rx_bytes_);
+ }
- // Send/receive here which increment tx/rx_bytes.
+ // Send/receive here which increment tx/rx_bytes.
- private:
- pw::span<std::byte> rx_buffer;
- pw::span<std::byte> tx_buffer;
+ private:
+ pw::span<std::byte> rx_buffer;
+ pw::span<std::byte> tx_buffer;
- PW_METRIC(tx_bytes_, "tx_bytes", 0);
- PW_METRIC(rx_bytes_, "rx_bytes", 0);
- };
+ PW_METRIC(tx_bytes_, "tx_bytes", 0);
+ PW_METRIC(rx_bytes_, "rx_bytes", 0);
+ };
- PW_METRIC_GROUP(root_metrics, "/");
- PW_METRIC_GROUP(root_metrics, uart1_metrics, "uart1");
+ PW_METRIC_GROUP(root_metrics, "/");
+ PW_METRIC_GROUP(root_metrics, uart1_metrics, "uart1");
- std::array<std::byte, 512> uart_rx_buffer;
- std::array<std::byte, 512> uart_tx_buffer;
- Uart uart1(uart_rx_buffer,
- uart_tx_buffer);
+ std::array<std::byte, 512> uart_rx_buffer;
+ std::array<std::byte, 512> uart_tx_buffer;
+ Uart uart1(uart_rx_buffer,
+ uart_tx_buffer);
- void main() {
- // uart1_metrics is guaranteed to be initialized by this point, so it is
- safe to pass it to Init().
- uart1.Init(uart1_metrics);
- }
+ void main() {
+ // uart1_metrics is guaranteed to be initialized by this point, so it is
+ safe to pass it to Init().
+ uart1.Init(uart1_metrics);
+ }
.. attention::
-
- Be extra careful about **static global metric registration**. Consider using
- the ``Init()`` pattern.
+ Be extra careful about **static global metric registration**. Consider using
+ the ``Init()`` pattern.
Metric member order matters in objects
--------------------------------------
@@ -535,42 +530,42 @@ The order of declaring in-class groups and metrics matters if the metrics are
within a group declared inside the class. For example, the following class will
work fine:
-.. code::
+.. code-block::
- #include "pw_metric/metric.h"
+ #include "pw_metric/metric.h"
- class PowerSubsystem {
- public:
- Group& metrics() { return metrics_; }
- const Group& metrics() const { return metrics_; }
+ class PowerSubsystem {
+ public:
+ Group& metrics() { return metrics_; }
+ const Group& metrics() const { return metrics_; }
- private:
- PW_METRIC_GROUP(metrics_, "power"); // Note metrics_ declared first.
- PW_METRIC(metrics_, foo, "foo", 0.2f);
- PW_METRIC(metrics_, bar, "bar", 44000u);
- };
+ private:
+ PW_METRIC_GROUP(metrics_, "power"); // Note metrics_ declared first.
+ PW_METRIC(metrics_, foo, "foo", 0.2f);
+ PW_METRIC(metrics_, bar, "bar", 44000u);
+ };
but the following one will not since the group is constructed after the metrics
(and will result in a compile error):
-.. code::
+.. code-block::
- #include "pw_metric/metric.h"
+ #include "pw_metric/metric.h"
- class PowerSubsystem {
- public:
- Group& metrics() { return metrics_; }
- const Group& metrics() const { return metrics_; }
+ class PowerSubsystem {
+ public:
+ Group& metrics() { return metrics_; }
+ const Group& metrics() const { return metrics_; }
- private:
- PW_METRIC(metrics_, foo, "foo", 0.2f);
- PW_METRIC(metrics_, bar, "bar", 44000u);
- PW_METRIC_GROUP(metrics_, "power"); // Error: metrics_ must be first.
- };
+ private:
+ PW_METRIC(metrics_, foo, "foo", 0.2f);
+ PW_METRIC(metrics_, bar, "bar", 44000u);
+ PW_METRIC_GROUP(metrics_, "power"); // Error: metrics_ must be first.
+ };
.. attention::
- Put **groups before metrics** when declaring metrics members inside classes.
+ Put **groups before metrics** when declaring metrics members inside classes.
Thread safety
-------------
@@ -586,9 +581,9 @@ synchronization, and can be used from ISRs.
.. attention::
- **You must synchronize access to metrics**. ``pw_metrics`` does not
- internally synchronize access during construction. Metric Set/Increment are
- safe.
+ **You must synchronize access to metrics**. ``pw_metrics`` does not
+ internally synchronize access during construction. Metric Set/Increment are
+ safe.
Lifecycle
---------
@@ -607,26 +602,25 @@ there are no helper functions for this, so be careful.
Below is an example that **is incorrect**. Don't do what follows!
-.. code::
+.. code-block::
- #include "pw_metric/metric.h"
+ #include "pw_metric/metric.h"
- void main() {
- PW_METRIC_GROUP(root, "/");
- {
- // BAD! The metrics have a different lifetime than the group.
- PW_METRIC(root, temperature, "temperature_f", 72.3f);
- PW_METRIC(root, humidity, "humidity_relative_percent", 33.2f);
- }
- // OOPS! root now has a linked list that points to the destructed
- // "humidity" object.
- }
+ void main() {
+ PW_METRIC_GROUP(root, "/");
+ {
+ // BAD! The metrics have a different lifetime than the group.
+ PW_METRIC(root, temperature, "temperature_f", 72.3f);
+ PW_METRIC(root, humidity, "humidity_relative_percent", 33.2f);
+ }
+ // OOPS! root now has a linked list that points to the destructed
+ // "humidity" object.
+ }
.. attention::
-
- **Don't destruct metrics**. Metrics are designed to be registered /
- structured upfront, then manipulated during a device's active phase. They do
- not support destruction.
+ **Don't destruct metrics**. Metrics are designed to be registered /
+ structured upfront, then manipulated during a device's active phase. They do
+ not support destruction.
-----------------
Exporting metrics
@@ -646,16 +640,16 @@ Batching the returned metrics avoids requiring a large buffer or large RPC MTU.
The returned metric objects have flattened paths to the root. For example, the
returned metrics (post detokenization and jsonified) might look something like:
-.. code:: none
+.. code-block:: none
- {
- "/i2c1/failed_txns": 17,
- "/i2c1/total_txns": 2013,
- "/i2c1/gyro/resets": 24,
- "/i2c1/gyro/hangs": 1,
- "/spi1/thermocouple/reads": 242,
- "/spi1/thermocouple/temp_celsius": 34.52,
- }
+ {
+ "/i2c1/failed_txns": 17,
+ "/i2c1/total_txns": 2013,
+ "/i2c1/gyro/resets": 24,
+ "/i2c1/gyro/hangs": 1,
+ "/spi1/thermocouple/reads": 242,
+ "/spi1/thermocouple/temp_celsius": 34.52,
+ }
Note that there is no nesting of the groups; the nesting is implied from the
path.
@@ -672,7 +666,7 @@ To expose a ``MetricService`` in your application, do the following:
For example:
-.. code::
+.. code-block::
#include "pw_rpc/server.h"
#include "pw_metric/metric.h"
@@ -705,25 +699,23 @@ For example:
}
.. attention::
-
- Take care when exporting metrics. Ensure **appropriate access control** is in
- place. In some cases it may make sense to entirely disable metrics export for
- production builds. Although reading metrics via RPC won't influence the
- device, in some cases the metrics could expose sensitive information if
- product owners are not careful.
+ Take care when exporting metrics. Ensure **appropriate access control** is in
+ place. In some cases it may make sense to entirely disable metrics export for
+ production builds. Although reading metrics via RPC won't influence the
+ device, in some cases the metrics could expose sensitive information if
+ product owners are not careful.
.. attention::
+ **MetricService::Get is a synchronous RPC method**
- **MetricService::Get is a synchronous RPC method**
-
- Calls to is ``MetricService::Get`` are blocking and will send all metrics
- immediately, even though it is a server-streaming RPC. This will work fine if
- the device doesn't have too many metrics, or doesn't have concurrent RPCs
- like logging, but could be a problem in some cases.
+ Calls to is ``MetricService::Get`` are blocking and will send all metrics
+ immediately, even though it is a server-streaming RPC. This will work fine if
+ the device doesn't have too many metrics, or doesn't have concurrent RPCs
+ like logging, but could be a problem in some cases.
- We plan to offer an async version where the application is responsible for
- pumping the metrics into the streaming response. This gives flow control to
- the application.
+ We plan to offer an async version where the application is responsible for
+ pumping the metrics into the streaming response. This gives flow control to
+ the application.
-----------
Size report
@@ -734,10 +726,9 @@ metrics. This does not include the RPC service.
.. include:: metric_size_report
.. attention::
-
- At time of writing, **the above sizes show an unexpectedly large flash
- impact**. We are investigating why GCC is inserting large global static
- constructors per group, when all the logic should be reused across objects.
+ At time of writing, **the above sizes show an unexpectedly large flash
+ impact**. We are investigating why GCC is inserting large global static
+ constructors per group, when all the logic should be reused across objects.
-------------
Metric Parser
diff --git a/pw_metric/metric.cc b/pw_metric/metric.cc
index af23821e2..18721126c 100644
--- a/pw_metric/metric.cc
+++ b/pw_metric/metric.cc
@@ -34,7 +34,7 @@ span<const std::byte> AsSpan(const T& t) {
// TODO(keir): Consider putting this into upstream pw_tokenizer.
struct Base64EncodedToken {
Base64EncodedToken(Token token) {
- int encoded_size = tokenizer::PrefixedBase64Encode(AsSpan(token), data);
+ size_t encoded_size = tokenizer::PrefixedBase64Encode(AsSpan(token), data);
data[encoded_size] = 0;
}
@@ -109,8 +109,6 @@ void Metric::Dump(IntrusiveList<Metric>& metrics, int level) {
}
}
-Group::Group(Token name) : name_(name) {}
-
Group::Group(Token name, IntrusiveList<Group>& groups) : name_(name) {
groups.push_front(*this);
}
diff --git a/pw_metric/metric_service_nanopb.cc b/pw_metric/metric_service_nanopb.cc
index 5422be161..3bc5a7d8d 100644
--- a/pw_metric/metric_service_nanopb.cc
+++ b/pw_metric/metric_service_nanopb.cc
@@ -77,7 +77,7 @@ class NanopbMetricWriter : public virtual internal::MetricWriter {
void Flush() {
if (response_.metrics_count) {
response_writer_.Write(response_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
response_ = pw_metric_proto_MetricResponse_init_zero;
}
}
diff --git a/pw_metric/public/pw_metric/metric.h b/pw_metric/public/pw_metric/metric.h
index 5a6a3e030..5dfd1ab64 100644
--- a/pw_metric/public/pw_metric/metric.h
+++ b/pw_metric/public/pw_metric/metric.h
@@ -65,10 +65,10 @@ class Metric : public IntrusiveList<Metric>::Item {
void operator=(const Metric&) = delete;
protected:
- Metric(Token name, float value)
+ constexpr Metric(Token name, float value)
: name_and_type_((name & kTokenMask) | kTypeFloat), float_(value) {}
- Metric(Token name, uint32_t value)
+ constexpr Metric(Token name, uint32_t value)
: name_and_type_((name & kTokenMask) | kTypeInt), uint_(value) {}
Metric(Token name, float value, IntrusiveList<Metric>& metrics);
@@ -112,7 +112,7 @@ class TypedMetric;
template <>
class TypedMetric<float> : public Metric {
public:
- TypedMetric(Token name, float value) : Metric(name, value) {}
+ constexpr TypedMetric(Token name, float value) : Metric(name, value) {}
TypedMetric(Token name, float value, IntrusiveList<Metric>& metrics)
: Metric(name, value, metrics) {}
@@ -129,7 +129,7 @@ class TypedMetric<float> : public Metric {
template <>
class TypedMetric<uint32_t> : public Metric {
public:
- TypedMetric(Token name, uint32_t value) : Metric(name, value) {}
+ constexpr TypedMetric(Token name, uint32_t value) : Metric(name, value) {}
TypedMetric(Token name, uint32_t value, IntrusiveList<Metric>& metrics)
: Metric(name, value, metrics) {}
@@ -148,7 +148,7 @@ class TypedMetric<uint32_t> : public Metric {
// Size: 16 bytes/128 bits - next, name, metrics, children.
class Group : public IntrusiveList<Group>::Item {
public:
- Group(Token name);
+ constexpr Group(Token name) : name_(name) {}
Group(Token name, IntrusiveList<Group>& groups);
Token name() const { return name_; }
diff --git a/pw_metric/py/BUILD.bazel b/pw_metric/py/BUILD.bazel
index e0ea76184..10d08f32c 100644
--- a/pw_metric/py/BUILD.bazel
+++ b/pw_metric/py/BUILD.bazel
@@ -27,15 +27,20 @@ py_library(
imports = ["."],
deps = [
"//pw_rpc/py:pw_rpc",
+ "//pw_tokenizer/py:pw_tokenizer",
+ # TODO: b/241456982 - Add this dep back in
+ # "//pw_metric:metric_proto_py_pb2",
],
)
+# TODO: b/241456982 - Not expected to build yet.
py_test(
name = "metric_parser_test",
size = "small",
srcs = [
"metric_parser_test.py",
],
+ tags = ["manual"],
deps = [
":pw_metric",
],
diff --git a/pw_minimal_cpp_stdlib/BUILD.bazel b/pw_minimal_cpp_stdlib/BUILD.bazel
index 8dd4536a8..7e8968e30 100644
--- a/pw_minimal_cpp_stdlib/BUILD.bazel
+++ b/pw_minimal_cpp_stdlib/BUILD.bazel
@@ -25,23 +25,26 @@ licenses(["notice"])
pw_cc_library(
name = "pw_minimal_cpp_stdlib",
srcs = [
- "public/internal/algorithm.h",
- "public/internal/array.h",
- "public/internal/cinttypes.h",
- "public/internal/climits.h",
- "public/internal/cmath.h",
- "public/internal/cstdarg.h",
- "public/internal/cstddef.h",
- "public/internal/cstdint.h",
- "public/internal/cstdio.h",
- "public/internal/cstring.h",
- "public/internal/initializer_list.h",
- "public/internal/iterator.h",
- "public/internal/limits.h",
- "public/internal/new.h",
- "public/internal/string_view.h",
- "public/internal/type_traits.h",
- "public/internal/utility.h",
+ "public/pw_minimal_cpp_stdlib/internal/algorithm.h",
+ "public/pw_minimal_cpp_stdlib/internal/array.h",
+ "public/pw_minimal_cpp_stdlib/internal/cinttypes.h",
+ "public/pw_minimal_cpp_stdlib/internal/climits.h",
+ "public/pw_minimal_cpp_stdlib/internal/cmath.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstdarg.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstddef.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstdint.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstdio.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstring.h",
+ "public/pw_minimal_cpp_stdlib/internal/functional.h",
+ "public/pw_minimal_cpp_stdlib/internal/initializer_list.h",
+ "public/pw_minimal_cpp_stdlib/internal/iterator.h",
+ "public/pw_minimal_cpp_stdlib/internal/limits.h",
+ "public/pw_minimal_cpp_stdlib/internal/memory.h",
+ "public/pw_minimal_cpp_stdlib/internal/new.h",
+ "public/pw_minimal_cpp_stdlib/internal/string.h",
+ "public/pw_minimal_cpp_stdlib/internal/string_view.h",
+ "public/pw_minimal_cpp_stdlib/internal/type_traits.h",
+ "public/pw_minimal_cpp_stdlib/internal/utility.h",
],
hdrs = [
"public/algorithm",
@@ -54,10 +57,13 @@ pw_cc_library(
"public/cstdint",
"public/cstdio",
"public/cstring",
+ "public/functional",
"public/initializer_list",
"public/iterator",
"public/limits",
+ "public/memory",
"public/new",
+ "public/string",
"public/string_view",
"public/type_traits",
"public/utility",
@@ -73,7 +79,7 @@ pw_cc_library(
name = "minimal_cpp_stdlib_isolated_test",
srcs = ["isolated_test.cc"],
copts = ["-nostdinc++"],
- tags = ["manual"], # TODO(b/257529911): Fix build failures.
+ tags = ["manual"], # TODO: b/257529911 - Fix build failures.
deps = [
":pw_minimal_cpp_stdlib",
"//pw_polyfill",
@@ -86,7 +92,7 @@ pw_cc_test(
srcs = [
"test.cc",
],
- tags = ["manual"], # TODO(b/257529911): Fix build failures.
+ tags = ["manual"], # TODO: b/257529911 - Fix build failures.
deps = [
":minimal_cpp_stdlib_isolated_test",
"//pw_unit_test",
diff --git a/pw_minimal_cpp_stdlib/BUILD.gn b/pw_minimal_cpp_stdlib/BUILD.gn
index 779029cd2..7fcf11b01 100644
--- a/pw_minimal_cpp_stdlib/BUILD.gn
+++ b/pw_minimal_cpp_stdlib/BUILD.gn
@@ -27,16 +27,11 @@ config("no_cpp_includes") {
cflags = [ "-nostdinc++" ]
}
-config("use_minimal_cpp_stdlib") {
- configs = [
+pw_source_set("pw_minimal_cpp_stdlib") {
+ public_configs = [
":public_include_path",
":no_cpp_includes",
]
-}
-
-pw_source_set("pw_minimal_cpp_stdlib") {
- public_configs = [ ":public_include_path" ]
- configs = [ ":no_cpp_includes" ]
public = [
"public/algorithm",
"public/array",
@@ -48,34 +43,41 @@ pw_source_set("pw_minimal_cpp_stdlib") {
"public/cstdint",
"public/cstdio",
"public/cstring",
+ "public/functional",
"public/initializer_list",
"public/iterator",
"public/limits",
+ "public/memory",
"public/new",
+ "public/string",
"public/string_view",
"public/type_traits",
"public/utility",
]
sources = [
- "public/internal/algorithm.h",
- "public/internal/array.h",
- "public/internal/cinttypes.h",
- "public/internal/climits.h",
- "public/internal/cmath.h",
- "public/internal/cstdarg.h",
- "public/internal/cstddef.h",
- "public/internal/cstdint.h",
- "public/internal/cstdio.h",
- "public/internal/cstring.h",
- "public/internal/initializer_list.h",
- "public/internal/iterator.h",
- "public/internal/limits.h",
- "public/internal/new.h",
- "public/internal/string_view.h",
- "public/internal/type_traits.h",
- "public/internal/utility.h",
+ "public/pw_minimal_cpp_stdlib/internal/algorithm.h",
+ "public/pw_minimal_cpp_stdlib/internal/array.h",
+ "public/pw_minimal_cpp_stdlib/internal/cinttypes.h",
+ "public/pw_minimal_cpp_stdlib/internal/climits.h",
+ "public/pw_minimal_cpp_stdlib/internal/cmath.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstdarg.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstddef.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstdint.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstdio.h",
+ "public/pw_minimal_cpp_stdlib/internal/cstring.h",
+ "public/pw_minimal_cpp_stdlib/internal/functional.h",
+ "public/pw_minimal_cpp_stdlib/internal/initializer_list.h",
+ "public/pw_minimal_cpp_stdlib/internal/iterator.h",
+ "public/pw_minimal_cpp_stdlib/internal/limits.h",
+ "public/pw_minimal_cpp_stdlib/internal/memory.h",
+ "public/pw_minimal_cpp_stdlib/internal/new.h",
+ "public/pw_minimal_cpp_stdlib/internal/string.h",
+ "public/pw_minimal_cpp_stdlib/internal/string_view.h",
+ "public/pw_minimal_cpp_stdlib/internal/type_traits.h",
+ "public/pw_minimal_cpp_stdlib/internal/utility.h",
]
- public_deps = [ dir_pw_polyfill ]
+ public_deps = [ "$dir_pw_polyfill:standard_library" ]
+ remove_public_deps = [ "$dir_pw_minimal_cpp_stdlib" ]
}
pw_test_group("tests") {
diff --git a/pw_minimal_cpp_stdlib/CMakeLists.txt b/pw_minimal_cpp_stdlib/CMakeLists.txt
index 5ab9ffeee..af027fe61 100644
--- a/pw_minimal_cpp_stdlib/CMakeLists.txt
+++ b/pw_minimal_cpp_stdlib/CMakeLists.txt
@@ -14,3 +14,7 @@
add_library(pw_minimal_cpp_stdlib INTERFACE)
target_include_directories(pw_minimal_cpp_stdlib INTERFACE public)
+target_link_libraries(pw_minimal_cpp_stdlib
+ INTERFACE
+ pw_polyfill._standard_library_public
+)
diff --git a/pw_minimal_cpp_stdlib/Kconfig b/pw_minimal_cpp_stdlib/Kconfig
new file mode 100644
index 000000000..8b9faf915
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/Kconfig
@@ -0,0 +1,23 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+config PIGWEED_MINIMAL_CPP_STDLIB
+ bool "Link pw_minimal_cpp_stdlib"
+ depends on STD_CPP17 || STD_CPP2A || STD_CPP20 || STD_CPP2B
+ help
+ Uses the pw_minimal_cpp_stdlib C++ standard library implementation.
+ The build must set `-nostdinc++` to exclude the toolchain's standard
+ library. This is NOT a full standard library implementation and may
+ only be used to build pw_tokenizer.
+ see :ref:'module-pw_minimal_cpp_stdlib' for module details.
diff --git a/pw_minimal_cpp_stdlib/docs.rst b/pw_minimal_cpp_stdlib/docs.rst
index d48629b93..7742cd71e 100644
--- a/pw_minimal_cpp_stdlib/docs.rst
+++ b/pw_minimal_cpp_stdlib/docs.rst
@@ -3,20 +3,37 @@
=====================
pw_minimal_cpp_stdlib
=====================
-The ``pw_minimal_cpp_stdlib`` module provides an extremely limited
-implementation of the C++ Standard Library. This module falls far, far short of
-providing a complete C++ Standard Library and should only be used for testing
-and development when compiling with C++17 or newer without a C++ Standard
-Library. Production code should use a real C++ Standard Library implementation,
-such as `libc++ <https://libcxx.llvm.org/>`_ or
-`libstdc++ <https://gcc.gnu.org/onlinedocs/libstdc++/>`_.
+.. admonition:: 🛑 Stop 🛑
+
+ **Do not use this module** unless you have consulted with the Pigweed team.
+
+The ``pw_minimal_cpp_stdlib`` module provides an extremely limited, incomplete
+implementation of the C++ Standard Library. This module is only intended for
+testing and development when compiling with C++17 or newer, but without access
+to the C++ Standard Library (``-nostdinc++``).
+
+Production code should use a real C++ Standard Library implementation, such as
+`libc++ <https://libcxx.llvm.org/>`_ or `libstdc++
+<https://gcc.gnu.org/onlinedocs/libstdc++/>`_.
.. warning::
``pw_minimal_cpp_stdlib`` was created for a very specific purpose. It is NOT a
general purpose C++ Standard Library implementation and should not be used as
- one. Many features are missing, some features non-functioning stubs, and some
- features may not match the C++ standard.
+ one.
+
+ - Many library features are **missing**.
+ - Many features are **non-functioning stubs**.
+ - Some features **do not match the C++ standard**.
+ - Test coverage is **extremely limited**.
+
+-----------------
+Build integration
+-----------------
+The top-level ``build_with_minimal_cpp_stdlib`` GN group builds a few supported
+modules with ``pw_minimal_cpp_stdlib`` swapped in for the C++ library at the
+toolchain level. Notably, ``pw_minimal_cpp_stdlib`` does not support
+``pw_unit_test``, so this group does NOT run any tests.
-----------
Code layout
@@ -32,12 +49,7 @@ defined in ``public/``. These files are symlinks to their implementations in
.. code-block:: bash
- for f in $(ls internal/); do ln -s internal/$f ${f%.h}; done
-
-The top-level ``build_with_minimal_cpp_stdlib`` GN group builds a few supported
-modules with ``pw_minimal_cpp_stdlib`` swapped in for the C++ library at the
-toolchain level. Notably, ``pw_minimal_cpp_stdlib`` does not support
-``pw_unit_test``, so this group does not run any tests.
+ for f in $(ls pw_minimal_cpp_stdlib/internal/); do ln -s pw_minimal_cpp_stdlib/internal/$f ${f%.h}; done
------------
Requirements
diff --git a/pw_minimal_cpp_stdlib/isolated_test.cc b/pw_minimal_cpp_stdlib/isolated_test.cc
index 8fb13b113..6620dc960 100644
--- a/pw_minimal_cpp_stdlib/isolated_test.cc
+++ b/pw_minimal_cpp_stdlib/isolated_test.cc
@@ -22,10 +22,13 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
+#include <functional>
#include <initializer_list>
#include <iterator>
#include <limits>
+#include <memory>
#include <new>
+#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
@@ -78,6 +81,27 @@ SimpleTest* SimpleTest::all_tests = nullptr;
} \
} while (0)
+#define EXPECT_NE(lhs, rhs) \
+ do { \
+ if ((lhs) == (rhs)) { \
+ RecordTestFailure(); \
+ } \
+ } while (0)
+
+#define EXPECT_LT(lhs, rhs) \
+ do { \
+ if ((lhs) < (rhs)) { \
+ RecordTestFailure(); \
+ } \
+ } while (0)
+
+#define EXPECT_GT(lhs, rhs) \
+ do { \
+ if ((lhs) > (rhs)) { \
+ RecordTestFailure(); \
+ } \
+ } while (0)
+
#define EXPECT_TRUE(expr) EXPECT_EQ(true, expr)
#define EXPECT_FALSE(expr) EXPECT_EQ(false, expr)
#define EXPECT_STREQ(lhs, rhs) EXPECT_EQ(std::strcmp((lhs), (rhs)), 0)
@@ -297,6 +321,9 @@ TEST(TypeTraits, Basic) {
static_assert(std::is_same_v<float, float>);
static_assert(!std::is_same_v<char, unsigned char>);
+ static_assert(std::is_same_v<const int, std::add_const_t<int>>);
+ static_assert(std::is_same_v<const int, std::add_const_t<const int>>);
+ static_assert(!std::is_same_v<int, std::add_const_t<int>>);
}
TEST(TypeTraits, LogicalTraits) {
@@ -318,6 +345,16 @@ TEST(TypeTraits, LogicalTraits) {
static_assert(!std::negation_v<std::true_type>);
}
+TEST(TypeTraits, AlignmentOf) {
+ struct Foo {
+ char x;
+ double y;
+ };
+
+ static_assert(std::alignment_of_v<int> == alignof(int));
+ static_assert(std::alignment_of_v<Foo> == alignof(Foo));
+}
+
struct MoveTester {
MoveTester(int value) : magic_value(value), moved(false) {}
@@ -389,10 +426,19 @@ TEST(Iterator, Tags) {
#endif // PW_CXX_STANDARD_IS_SUPPORTED(20)
}
-TEST(TypeTrait, Basic) {
- static_assert(std::is_same_v<const int, std::add_const_t<int>>);
- static_assert(std::is_same_v<const int, std::add_const_t<const int>>);
- static_assert(!std::is_same_v<int, std::add_const_t<int>>);
+TEST(Memory, AddressOf) {
+ struct Foo {
+ Foo** operator&() { return nullptr; } // NOLINT
+ } nullptr_address;
+
+ EXPECT_EQ(&nullptr_address, nullptr);
+ EXPECT_NE(std::addressof(nullptr_address), nullptr);
+}
+
+TEST(String, CharTraits) {
+ static_assert(std::char_traits<char>::compare("1234a", "1234z", 4) == 0);
+ static_assert(std::char_traits<char>::compare("1234a", "1234z", 5) < 0);
+ static_assert(std::char_traits<char>::compare("1234z", "1234a", 5) > 0);
}
} // namespace
diff --git a/pw_minimal_cpp_stdlib/public/algorithm b/pw_minimal_cpp_stdlib/public/algorithm
index 81f8692fe..3f8358712 120000
--- a/pw_minimal_cpp_stdlib/public/algorithm
+++ b/pw_minimal_cpp_stdlib/public/algorithm
@@ -1 +1 @@
-internal/algorithm.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/algorithm.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/array b/pw_minimal_cpp_stdlib/public/array
index e9f7bca46..92fbb6693 120000
--- a/pw_minimal_cpp_stdlib/public/array
+++ b/pw_minimal_cpp_stdlib/public/array
@@ -1 +1 @@
-internal/array.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/array.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cinttypes b/pw_minimal_cpp_stdlib/public/cinttypes
index 8b251e577..cb0246276 120000
--- a/pw_minimal_cpp_stdlib/public/cinttypes
+++ b/pw_minimal_cpp_stdlib/public/cinttypes
@@ -1 +1 @@
-internal/cinttypes.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cinttypes.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/climits b/pw_minimal_cpp_stdlib/public/climits
index e098495d9..4b8f33a15 120000
--- a/pw_minimal_cpp_stdlib/public/climits
+++ b/pw_minimal_cpp_stdlib/public/climits
@@ -1 +1 @@
-internal/climits.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/climits.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cmath b/pw_minimal_cpp_stdlib/public/cmath
index caf50544e..b1204c53e 120000
--- a/pw_minimal_cpp_stdlib/public/cmath
+++ b/pw_minimal_cpp_stdlib/public/cmath
@@ -1 +1 @@
-internal/cmath.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cmath.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cstdarg b/pw_minimal_cpp_stdlib/public/cstdarg
index 5d649b59a..1c1caed54 120000
--- a/pw_minimal_cpp_stdlib/public/cstdarg
+++ b/pw_minimal_cpp_stdlib/public/cstdarg
@@ -1 +1 @@
-internal/cstdarg.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cstdarg.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cstddef b/pw_minimal_cpp_stdlib/public/cstddef
index c35111420..89a6b029f 120000
--- a/pw_minimal_cpp_stdlib/public/cstddef
+++ b/pw_minimal_cpp_stdlib/public/cstddef
@@ -1 +1 @@
-internal/cstddef.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cstddef.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cstdint b/pw_minimal_cpp_stdlib/public/cstdint
index 749150fa7..6b961cda6 120000
--- a/pw_minimal_cpp_stdlib/public/cstdint
+++ b/pw_minimal_cpp_stdlib/public/cstdint
@@ -1 +1 @@
-internal/cstdint.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cstdint.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cstdio b/pw_minimal_cpp_stdlib/public/cstdio
index 12ae70578..ffc679fa5 120000
--- a/pw_minimal_cpp_stdlib/public/cstdio
+++ b/pw_minimal_cpp_stdlib/public/cstdio
@@ -1 +1 @@
-internal/cstdio.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cstdio.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/cstring b/pw_minimal_cpp_stdlib/public/cstring
index 429e0263c..90047efff 120000
--- a/pw_minimal_cpp_stdlib/public/cstring
+++ b/pw_minimal_cpp_stdlib/public/cstring
@@ -1 +1 @@
-internal/cstring.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/cstring.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/functional b/pw_minimal_cpp_stdlib/public/functional
new file mode 120000
index 000000000..a9a97d3e2
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/public/functional
@@ -0,0 +1 @@
+pw_minimal_cpp_stdlib/internal/functional.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/initializer_list b/pw_minimal_cpp_stdlib/public/initializer_list
index cdccec8ce..60ec51ec8 120000
--- a/pw_minimal_cpp_stdlib/public/initializer_list
+++ b/pw_minimal_cpp_stdlib/public/initializer_list
@@ -1 +1 @@
-internal/initializer_list.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/initializer_list.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/iterator b/pw_minimal_cpp_stdlib/public/iterator
index 4703aa88c..beb633d31 120000
--- a/pw_minimal_cpp_stdlib/public/iterator
+++ b/pw_minimal_cpp_stdlib/public/iterator
@@ -1 +1 @@
-internal/iterator.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/iterator.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/limits b/pw_minimal_cpp_stdlib/public/limits
index 8309de0da..cee7e6a96 120000
--- a/pw_minimal_cpp_stdlib/public/limits
+++ b/pw_minimal_cpp_stdlib/public/limits
@@ -1 +1 @@
-internal/limits.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/limits.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/memory b/pw_minimal_cpp_stdlib/public/memory
new file mode 120000
index 000000000..89e025306
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/public/memory
@@ -0,0 +1 @@
+pw_minimal_cpp_stdlib/internal/memory.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/new b/pw_minimal_cpp_stdlib/public/new
index ed0129c94..8f3c1a0dc 120000
--- a/pw_minimal_cpp_stdlib/public/new
+++ b/pw_minimal_cpp_stdlib/public/new
@@ -1 +1 @@
-internal/new.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/new.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/internal/algorithm.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/algorithm.h
index aa2993f3e..66cf2bf87 100644
--- a/pw_minimal_cpp_stdlib/public/internal/algorithm.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/algorithm.h
@@ -13,8 +13,6 @@
// the License.
#pragma once
-#include <type_traits>
-
#include "pw_polyfill/standard_library/namespace.h"
_PW_POLYFILL_BEGIN_NAMESPACE_STD
@@ -51,16 +49,6 @@ constexpr InputIterator find(InputIterator first,
return last;
}
-template <typename T>
-constexpr T&& forward(remove_reference_t<T>& value) {
- return static_cast<T&&>(value);
-}
-
-template <typename T>
-constexpr T&& forward(remove_reference_t<T>&& value) {
- return static_cast<T&&>(value);
-}
-
template <class LhsIterator, class RhsIterator>
constexpr bool equal(LhsIterator first_l,
LhsIterator last_l,
diff --git a/pw_minimal_cpp_stdlib/public/internal/array.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/array.h
index 8a26e69bb..8a26e69bb 100644
--- a/pw_minimal_cpp_stdlib/public/internal/array.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/array.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cinttypes.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cinttypes.h
index 5141862e0..5141862e0 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cinttypes.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cinttypes.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/climits.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/climits.h
index 6acdaeac6..6acdaeac6 100644
--- a/pw_minimal_cpp_stdlib/public/internal/climits.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/climits.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cmath.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cmath.h
index 2cf754c80..2cf754c80 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cmath.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cmath.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cstdarg.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdarg.h
index 129a0d33c..129a0d33c 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cstdarg.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdarg.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cstddef.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstddef.h
index c6fd87a5b..c6fd87a5b 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cstddef.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstddef.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cstdint.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdint.h
index abe9e04fc..abe9e04fc 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cstdint.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdint.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cstdio.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdio.h
index 725c1d561..725c1d561 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cstdio.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstdio.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/cstring.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstring.h
index ed24ecb73..ed24ecb73 100644
--- a/pw_minimal_cpp_stdlib/public/internal/cstring.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/cstring.h
diff --git a/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/functional.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/functional.h
new file mode 100644
index 000000000..1f55a6736
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/functional.h
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_polyfill/standard_library/namespace.h"
+
+_PW_POLYFILL_BEGIN_NAMESPACE_STD
+
+template <typename T>
+class reference_wrapper {
+ public:
+ using type = T;
+
+ // This constructor is incomplete / incorrect!
+ constexpr reference_wrapper(T& reference) : pointer_(&reference) {}
+
+ constexpr reference_wrapper(const reference_wrapper&) noexcept = default;
+ constexpr reference_wrapper& operator=(const reference_wrapper&) noexcept =
+ default;
+
+ constexpr T& get() const noexcept { return *pointer_; }
+ constexpr operator T&() const noexcept { return *pointer_; }
+
+ private:
+ T* pointer_;
+};
+
+_PW_POLYFILL_END_NAMESPACE_STD
diff --git a/pw_minimal_cpp_stdlib/public/internal/initializer_list.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/initializer_list.h
index 44da38d20..44da38d20 100644
--- a/pw_minimal_cpp_stdlib/public/internal/initializer_list.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/initializer_list.h
diff --git a/pw_minimal_cpp_stdlib/public/internal/iterator.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/iterator.h
index 4828cc5c3..23704ccd0 100644
--- a/pw_minimal_cpp_stdlib/public/internal/iterator.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/iterator.h
@@ -42,7 +42,7 @@ constexpr auto data(const C& container) -> decltype(container.data()) {
return container.data();
}
-template <typename T, decltype(sizeof(int)) kSize>
+template <typename T, decltype(sizeof(0)) kSize>
constexpr T* data(T (&array)[kSize]) noexcept {
return array;
}
@@ -62,8 +62,8 @@ constexpr auto size(const C& container) -> decltype(container.size()) {
return container.size();
}
-template <typename T, decltype(sizeof(int)) kSize>
-constexpr decltype(sizeof(int)) size(const T (&)[kSize]) noexcept {
+template <typename T, decltype(sizeof(0)) kSize>
+constexpr decltype(sizeof(0)) size(const T (&)[kSize]) noexcept {
return kSize;
}
diff --git a/pw_minimal_cpp_stdlib/public/internal/limits.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/limits.h
index 85b4d59f7..5ce61ae4d 100644
--- a/pw_minimal_cpp_stdlib/public/internal/limits.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/limits.h
@@ -22,29 +22,42 @@ _PW_POLYFILL_BEGIN_NAMESPACE_STD
template <typename T>
struct numeric_limits {
static constexpr bool is_specialized = false;
+ static constexpr int digits = 0;
};
// Only a few of the numeric_limits methods are implemented.
-#define _PW_LIMITS_SPECIALIZATION( \
- type, val_signed, val_int, min_value, max_value) \
- template <> \
- struct numeric_limits<type> { \
- static constexpr bool is_specialized = true; \
- \
- static constexpr bool is_signed = (val_signed); \
- static constexpr bool is_integer = (val_int); \
- \
- static constexpr type min() noexcept { return (min_value); } \
- static constexpr type max() noexcept { return (max_value); } \
+#define _PW_LIMITS_SPECIALIZATION( \
+ type, val_signed, val_int, min_value, max_value, digits_value) \
+ template <> \
+ struct numeric_limits<type> { \
+ static constexpr bool is_specialized = true; \
+ \
+ static constexpr bool is_signed = (val_signed); \
+ static constexpr bool is_integer = (val_int); \
+ \
+ static constexpr int digits = (digits_value); \
+ \
+ static constexpr type min() noexcept { return (min_value); } \
+ static constexpr type max() noexcept { return (max_value); } \
}
-#define _PW_INTEGRAL_LIMIT(type, sname, uname) \
- _PW_LIMITS_SPECIALIZATION( \
- signed type, true, true, sname##_MIN, sname##_MAX); \
- _PW_LIMITS_SPECIALIZATION(unsigned type, false, true, 0u, uname##_MAX)
+#define _PW_INTEGRAL_LIMIT(type, sname, uname) \
+ _PW_LIMITS_SPECIALIZATION(signed type, \
+ true, \
+ true, \
+ sname##_MIN, \
+ sname##_MAX, \
+ CHAR_BIT * sizeof(type)); \
+ _PW_LIMITS_SPECIALIZATION(unsigned type, \
+ false, \
+ true, \
+ 0u, \
+ uname##_MAX, \
+ CHAR_BIT * sizeof(type) - 1)
-_PW_LIMITS_SPECIALIZATION(bool, false, true, false, true);
-_PW_LIMITS_SPECIALIZATION(char, char(-1) < char(0), true, CHAR_MIN, CHAR_MAX);
+_PW_LIMITS_SPECIALIZATION(bool, false, true, false, true, 1);
+_PW_LIMITS_SPECIALIZATION(
+ char, char(-1) < char(0), true, CHAR_MIN, CHAR_MAX, 1);
_PW_INTEGRAL_LIMIT(char, SCHAR, UCHAR);
_PW_INTEGRAL_LIMIT(short, SHRT, USHRT);
diff --git a/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/memory.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/memory.h
new file mode 100644
index 000000000..500db85e9
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/memory.h
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_polyfill/standard_library/namespace.h"
+
+_PW_POLYFILL_BEGIN_NAMESPACE_STD
+
+template <typename T>
+T* addressof(T& arg) noexcept {
+ return reinterpret_cast<T*>(
+ const_cast<char*>(reinterpret_cast<const volatile char*>(&arg)));
+}
+
+template <typename T>
+T* addressof(T&& arg) = delete;
+
+template <typename T>
+struct pointer_traits;
+
+template <typename T>
+struct pointer_traits<T*> {
+ using pointer = T*;
+ using element_type = T;
+ using difference_type = decltype((const char*)1 - (const char*)1);
+};
+
+_PW_POLYFILL_END_NAMESPACE_STD
diff --git a/pw_minimal_cpp_stdlib/public/internal/new.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/new.h
index 38fc8b5f0..38fc8b5f0 100644
--- a/pw_minimal_cpp_stdlib/public/internal/new.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/new.h
diff --git a/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string.h
new file mode 100644
index 000000000..a89125117
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string.h
@@ -0,0 +1,73 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_polyfill/standard_library/namespace.h"
+
+_PW_POLYFILL_BEGIN_NAMESPACE_STD
+
+// NOT IMPLEMENTED: Some functions and overloads are missing.
+template <typename T>
+class char_traits {
+ public:
+ static constexpr void assign(T& dest, const T& source) noexcept {
+ dest = source;
+ }
+
+ static constexpr T* assign(T* dest, decltype(sizeof(0)) count, T value) {
+ for (decltype(sizeof(0)) i = 0; i < count; ++i) {
+ dest[i] = value;
+ }
+ return dest;
+ }
+
+ static constexpr bool eq(T lhs, T rhs) noexcept { return lhs == rhs; }
+
+ static constexpr bool lt(T lhs, T rhs) noexcept { return lhs < rhs; }
+
+ static constexpr T* move(T* dest,
+ const T* source,
+ decltype(sizeof(0)) count) {
+ if (dest < source) {
+ copy(dest, source, count);
+ } else if (source < dest) {
+ for (decltype(sizeof(0)) i = count; i != 0; --i) {
+ assign(dest[i - 1], source[i - 1]);
+ }
+ }
+ return dest;
+ }
+
+ static constexpr T* copy(T* dest,
+ const T* source,
+ decltype(sizeof(0)) count) {
+ for (decltype(sizeof(0)) i = 0; i < count; ++i) {
+ char_traits<T>::assign(dest[i], source[i]);
+ }
+ return dest;
+ }
+
+ static constexpr int compare(const T* lhs,
+ const T* rhs,
+ decltype(sizeof(0)) count) {
+ for (decltype(sizeof(0)) i = 0; i < count; ++i) {
+ if (!eq(lhs[i], rhs[i])) {
+ return lt(lhs[i], rhs[i]) ? -1 : 1;
+ }
+ }
+ return 0;
+ }
+};
+
+_PW_POLYFILL_END_NAMESPACE_STD
diff --git a/pw_minimal_cpp_stdlib/public/internal/string_view.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string_view.h
index 6fbffa08c..5eafb8d38 100644
--- a/pw_minimal_cpp_stdlib/public/internal/string_view.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/string_view.h
@@ -36,7 +36,7 @@ class basic_string_view {
using iterator = const_iterator;
using const_reverse_iterator = ::std::reverse_iterator<const_iterator>;
using reverse_iterator = const_reverse_iterator;
- using size_type = size_t;
+ using size_type = decltype(sizeof(0));
using difference_type = ptrdiff_t;
static constexpr size_type npos = size_type(-1);
@@ -80,7 +80,7 @@ class basic_string_view {
constexpr size_type size() const noexcept { return size_; }
constexpr size_type length() const noexcept { return size(); }
- constexpr size_type max_size() const noexcept { return ~size_t{0}; }
+ constexpr size_type max_size() const noexcept { return ~size_type{0}; }
[[nodiscard]] constexpr bool empty() const noexcept { return size() == 0u; }
diff --git a/pw_minimal_cpp_stdlib/public/internal/type_traits.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/type_traits.h
index d228f853d..5d518be2e 100644
--- a/pw_minimal_cpp_stdlib/public/internal/type_traits.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/type_traits.h
@@ -55,29 +55,40 @@ using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
template <typename T>
-struct is_array : false_type {};
+struct is_aggregate : bool_constant<__is_aggregate(T)> {};
template <typename T>
-struct is_array<T[]> : true_type {};
+static constexpr bool is_aggregate_v = is_aggregate<T>::value;
-template <typename T, decltype(sizeof(int)) kSize>
+template <typename T>
+struct is_array : false_type {};
+template <typename T>
+struct is_array<T[]> : true_type {};
+template <typename T, decltype(sizeof(0)) kSize>
struct is_array<T[kSize]> : true_type {};
-
template <typename T>
inline constexpr bool is_array_v = is_array<T>::value;
template <typename T>
struct is_const : false_type {};
-
template <typename T>
struct is_const<const T> : true_type {};
+template <typename T>
+inline constexpr bool is_const_v = is_const<T>::value;
-// NOT IMPLEMENTED: is_enum requires compiler builtins.
template <typename T>
-struct is_enum : false_type {};
+struct is_lvalue_reference : false_type {};
+template <typename T>
+struct is_lvalue_reference<T&> : true_type {};
+template <typename T>
+inline constexpr bool is_lvalue_reference_v = is_lvalue_reference<T>::value;
template <typename T>
-inline constexpr bool is_enum_v = is_enum<T>::value;
+struct is_rvalue_reference : false_type {};
+template <typename T>
+struct is_rvalue_reference<T&&> : true_type {};
+template <typename T>
+inline constexpr bool is_rvalue_reference_v = is_rvalue_reference<T>::value;
template <typename T>
struct remove_cv; // Forward declaration
@@ -395,7 +406,7 @@ struct type_identity {
template <typename T>
using type_identity_t = typename type_identity<T>::type;
-#define __cpp_lib_void_t void_t 201411L
+#define __cpp_lib_void_t 201411L
template <typename...>
using void_t = void;
@@ -514,27 +525,129 @@ struct is_convertible
template <typename T, typename U>
inline constexpr bool is_convertible_v = is_convertible<T, U>::value;
+template <typename T>
+struct alignment_of : integral_constant<decltype(sizeof(0)), alignof(T)> {};
+template <typename T>
+inline constexpr decltype(sizeof(0)) alignment_of_v = alignment_of<T>::value;
+
+#define PW_STDLIB_UNIMPLEMENTED(name) \
+ [[deprecated(#name " is NOT IMPLEMENTED in pw_minimal_cpp_stdlib!")]]
+
// NOT IMPLEMENTED: Stubs are provided for these traits classes, but they do not
// return useful values. Many of these would require compiler builtins.
+#define PW_BOOLEAN_TRAIT_NOT_SUPPORTED(name) \
+ template <typename T> \
+ struct name : false_type {}; \
+ template <typename T> \
+ PW_STDLIB_UNIMPLEMENTED(name) \
+ inline constexpr bool name##_v = name<T>::value
+
+#define PW_BOOLEAN_TRAIT_NOT_SUPPORTED_2(name) \
+ template <typename T, typename U> \
+ struct name : false_type {}; \
+ template <typename T, typename U> \
+ PW_STDLIB_UNIMPLEMENTED(name) \
+ inline constexpr bool name##_v = name<T, U>::value
+
+#define PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(name) \
+ template <typename T, typename... Args> \
+ struct name : false_type {}; \
+ template <typename T, typename... Args> \
+ PW_STDLIB_UNIMPLEMENTED(name) \
+ inline constexpr bool name##_v = name<T, Args...>::value
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_class);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_enum);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_function);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_member_function_pointer);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_member_object_pointer);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_union);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_compound);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_fundamental);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_member_pointer);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_object);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_reference);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_scalar);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_abstract);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_empty);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_final);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_pod);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_polymorphic);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_standard_layout);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_trivial);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_trivially_copyable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_volatile);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_constructible);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_default_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_default_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_default_constructible);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_copy_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_copy_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_copy_constructible);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_move_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_move_constructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_move_constructible);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_assignable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_assignable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_assignable);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_copy_assignable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_copy_assignable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_copy_assignable);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_move_assignable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_nothrow_move_assignable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS(is_trivially_move_assignable);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_destructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_nothrow_destructible);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_trivially_destructible);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(has_virtual_destructor);
+
template <typename T>
-struct is_function : false_type {};
+struct extent : integral_constant<decltype(sizeof(0)), 1> {};
template <typename T>
-struct is_trivially_copyable : true_type {};
+PW_STDLIB_UNIMPLEMENTED(extent)
+inline constexpr decltype(sizeof(0)) extent_v = extent<T>::value;
+
template <typename T>
-struct is_polymorphic : false_type {};
-template <typename T, typename U>
-struct is_base_of : false_type {};
+struct rank : integral_constant<decltype(sizeof(0)), 1> {};
+template <typename T>
+PW_STDLIB_UNIMPLEMENTED(rank)
+inline constexpr decltype(sizeof(0)) rank_v = extent<T>::value;
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED_2(is_base_of);
+
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_invocable_r);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_invocable);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_nothrow_invocable_r);
+PW_BOOLEAN_TRAIT_NOT_SUPPORTED(is_nothrow_invocable);
+
template <typename T>
-struct extent : integral_constant<decltype(sizeof(int)), 1> {};
+struct invoke_result {};
template <typename T>
-inline constexpr bool extent_v = extent<T>::value;
+using invoke_result_t = typename invoke_result<T>::type;
+
template <typename T>
struct underlying_type {
using type = T;
};
template <typename T>
using underlying_type_t = typename underlying_type<T>::type;
-template <typename T>
-inline constexpr bool is_trivially_copyable_v = is_trivially_copyable<T>::value;
+
+#undef PW_BOOLEAN_TRAIT_NOT_SUPPORTED
+#undef PW_BOOLEAN_TRAIT_NOT_SUPPORTED_2
+#undef PW_BOOLEAN_TRAIT_NOT_SUPPORTED_VARARGS
+#undef PW_STDLIB_UNIMPLEMENTED
_PW_POLYFILL_END_NAMESPACE_STD
diff --git a/pw_minimal_cpp_stdlib/public/internal/utility.h b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/utility.h
index 31a704b41..a5a65343e 100644
--- a/pw_minimal_cpp_stdlib/public/internal/utility.h
+++ b/pw_minimal_cpp_stdlib/public/pw_minimal_cpp_stdlib/internal/utility.h
@@ -21,7 +21,17 @@ _PW_POLYFILL_BEGIN_NAMESPACE_STD
template <typename T>
constexpr remove_reference_t<T>&& move(T&& object) {
- return (remove_reference_t<T> &&) object;
+ return (remove_reference_t<T>&&)object;
+}
+
+template <typename T>
+constexpr T&& forward(remove_reference_t<T>& value) {
+ return static_cast<T&&>(value);
+}
+
+template <typename T>
+constexpr T&& forward(remove_reference_t<T>&& value) {
+ return static_cast<T&&>(value);
}
// Forward declare these classes, which are specialized in other headers.
@@ -34,8 +44,8 @@ struct tuple_size;
template <typename T, T... kSequence>
class integer_sequence;
-template <size_t... kSequence>
-using index_sequence = integer_sequence<decltype(sizeof(int)), kSequence...>;
+template <decltype(sizeof(0))... kSequence>
+using index_sequence = integer_sequence<decltype(sizeof(0)), kSequence...>;
template <typename T, T kEnd>
#if __has_builtin(__make_integer_seq)
@@ -44,8 +54,8 @@ using make_integer_sequence = __make_integer_seq<integer_sequence, T, kEnd>;
using make_integer_sequence = integer_sequence<T, __integer_pack(kEnd)...>;
#endif // make_integer_sequence
-template <size_t kEnd>
-using make_index_sequence = make_integer_sequence<size_t, kEnd>;
+template <decltype(sizeof(0)) kEnd>
+using make_index_sequence = make_integer_sequence<decltype(sizeof(0)), kEnd>;
struct in_place_t {
explicit constexpr in_place_t() = default;
diff --git a/pw_minimal_cpp_stdlib/public/string b/pw_minimal_cpp_stdlib/public/string
new file mode 120000
index 000000000..02144e675
--- /dev/null
+++ b/pw_minimal_cpp_stdlib/public/string
@@ -0,0 +1 @@
+pw_minimal_cpp_stdlib/internal/string.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/string_view b/pw_minimal_cpp_stdlib/public/string_view
index 8e1be6701..ea0385498 120000
--- a/pw_minimal_cpp_stdlib/public/string_view
+++ b/pw_minimal_cpp_stdlib/public/string_view
@@ -1 +1 @@
-internal/string_view.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/string_view.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/type_traits b/pw_minimal_cpp_stdlib/public/type_traits
index 71b1217ca..fae63ce86 120000
--- a/pw_minimal_cpp_stdlib/public/type_traits
+++ b/pw_minimal_cpp_stdlib/public/type_traits
@@ -1 +1 @@
-internal/type_traits.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/type_traits.h \ No newline at end of file
diff --git a/pw_minimal_cpp_stdlib/public/utility b/pw_minimal_cpp_stdlib/public/utility
index 58c32f231..1d13fc8c8 120000
--- a/pw_minimal_cpp_stdlib/public/utility
+++ b/pw_minimal_cpp_stdlib/public/utility
@@ -1 +1 @@
-internal/utility.h \ No newline at end of file
+pw_minimal_cpp_stdlib/internal/utility.h \ No newline at end of file
diff --git a/pw_module/py/BUILD.gn b/pw_module/py/BUILD.gn
index 876184043..eb0c9ab07 100644
--- a/pw_module/py/BUILD.gn
+++ b/pw_module/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_module/__init__.py",
diff --git a/pw_module/py/setup.py b/pw_module/py/setup.py
deleted file mode 100644
index 882d2cdbd..000000000
--- a/pw_module/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_module"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_multibuf/BUILD.bazel b/pw_multibuf/BUILD.bazel
new file mode 100644
index 000000000..c078e6098
--- /dev/null
+++ b/pw_multibuf/BUILD.bazel
@@ -0,0 +1,76 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "chunk",
+ srcs = ["chunk.cc"],
+ hdrs = ["public/pw_multibuf/chunk.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_preprocessor",
+ "//pw_span",
+ "//pw_sync:mutex",
+ ],
+)
+
+pw_cc_library(
+ name = "test_utils",
+ hdrs = ["public/pw_multibuf/internal/test_utils.h"],
+ includes = ["public"],
+ visibility = [":__subpackages__"],
+ deps = [
+ ":chunk",
+ "//pw_allocator:allocator_metric_proxy",
+ "//pw_allocator:split_free_list_allocator",
+ ],
+)
+
+pw_cc_test(
+ name = "chunk_test",
+ srcs = ["chunk_test.cc"],
+ deps = [
+ ":chunk",
+ ":test_utils",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_library(
+ name = "pw_multibuf",
+ srcs = ["multibuf.cc"],
+ hdrs = ["public/pw_multibuf/multibuf.h"],
+ deps = [":chunk"],
+)
+
+pw_cc_test(
+ name = "multibuf_test",
+ srcs = ["multibuf_test.cc"],
+ deps = [
+ ":pw_multibuf",
+ ":test_utils",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_multibuf/BUILD.gn b/pw_multibuf/BUILD.gn
new file mode 100644
index 000000000..41a9f20d9
--- /dev/null
+++ b/pw_multibuf/BUILD.gn
@@ -0,0 +1,82 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("chunk") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_multibuf/chunk.h" ]
+ sources = [ "chunk.cc" ]
+ public_deps = [
+ "$dir_pw_sync:mutex",
+ dir_pw_assert,
+ dir_pw_bytes,
+ dir_pw_preprocessor,
+ dir_pw_span,
+ ]
+ deps = [ "$dir_pw_assert:check" ]
+}
+
+pw_source_set("test_utils") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_multibuf/internal/test_utils.h" ]
+ public_deps = [
+ ":chunk",
+ "$dir_pw_allocator:allocator_metric_proxy",
+ "$dir_pw_allocator:split_free_list_allocator",
+ ]
+}
+
+pw_test("chunk_test") {
+ deps = [
+ ":chunk",
+ ":test_utils",
+ ]
+ sources = [ "chunk_test.cc" ]
+}
+
+pw_source_set("pw_multibuf") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_multibuf/multibuf.h" ]
+ sources = [ "multibuf.cc" ]
+ public_deps = [ ":chunk" ]
+}
+
+pw_test("multibuf_test") {
+ deps = [
+ ":pw_multibuf",
+ ":test_utils",
+ ]
+ sources = [ "multibuf_test.cc" ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":chunk_test",
+ ":multibuf_test",
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_multibuf/CMakeLists.txt b/pw_multibuf/CMakeLists.txt
new file mode 100644
index 000000000..217b70a60
--- /dev/null
+++ b/pw_multibuf/CMakeLists.txt
@@ -0,0 +1,75 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_multibuf.chunk STATIC
+ HEADERS
+ public/pw_multibuf/chunk.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
+ pw_preprocessor
+ pw_span
+ pw_sync.mutex
+ PRIVATE_DEPS
+ pw_assert.check
+ SOURCES
+ chunk.cc
+)
+
+pw_add_library(pw_multibuf.test_utils STATIC
+ HEADERS
+ public/pw_multibuf/internal/test_utils.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_multibuf.chunk
+ pw_allocator.allocator_metric_proxy
+ pw_allocator.split_free_list_allocator
+
+pw_add_test(pw_multibuf.chunk_test STATIC
+ SOURCES
+ chunk_test.cc
+ PRIVATE_DEPS
+ pw_multibuf.chunk
+ pw_multibuf.test_utils
+ GROUPS
+ modules
+ pw_multibuf
+)
+
+pw_add_library(pw_multibuf.pw_multibuf STATIC
+ HEADERS
+ public/pw_multibuf/multibuf.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_multibuf.chunk
+ SOURCES
+ multibuf.cc
+)
+
+pw_add_test(pw_multibuf.multibuf_test STATIC
+ SOURCES
+ multibuf_test.cc
+ PRIVATE_DEPS
+ pw_multibuf.multibuf
+ pw_multibuf.test_utils
+ GROUPS
+ modules
+ pw_multibuf
+)
diff --git a/pw_multibuf/chunk.cc b/pw_multibuf/chunk.cc
new file mode 100644
index 000000000..628a9d30c
--- /dev/null
+++ b/pw_multibuf/chunk.cc
@@ -0,0 +1,247 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_multibuf/chunk.h"
+
+#include <mutex>
+
+#include "pw_assert/check.h"
+
+namespace pw::multibuf {
+namespace {
+std::byte* CheckedAdd(std::byte* ptr, size_t offset) {
+ uintptr_t ptr_int = reinterpret_cast<uintptr_t>(ptr);
+ if (std::numeric_limits<uintptr_t>::max() - ptr_int < offset) {
+ return nullptr;
+ }
+ return reinterpret_cast<std::byte*>(ptr_int + offset);
+}
+
+std::byte* CheckedSub(std::byte* ptr, size_t offset) {
+ uintptr_t ptr_int = reinterpret_cast<uintptr_t>(ptr);
+ if (ptr_int < offset) {
+ return nullptr;
+ }
+ return reinterpret_cast<std::byte*>(ptr_int - offset);
+}
+
+std::byte* BeginPtr(ByteSpan span) { return span.data(); }
+
+std::byte* EndPtr(ByteSpan span) { return span.data() + span.size(); }
+
+} // namespace
+
+bool Chunk::CanMerge(const Chunk& next_chunk) const {
+ return region_tracker_ == next_chunk.region_tracker_ &&
+ EndPtr(span_) == BeginPtr(next_chunk.span_);
+}
+
+bool Chunk::Merge(OwnedChunk& next_chunk_owned) {
+ if (!CanMerge(*next_chunk_owned)) {
+ return false;
+ }
+ Chunk* next_chunk = next_chunk_owned.inner_;
+ next_chunk_owned.inner_ = nullptr;
+
+ // Note: Both chunks have the same ``region_tracker_``.
+ //
+ // We lock the one from `next_chunk` to satisfy the automatic
+ // checker that ``RemoveFromRegionList`` is safe to call.
+ std::lock_guard lock(next_chunk->region_tracker_->lock_);
+ PW_DCHECK(next_in_region_ == next_chunk);
+ span_ = ByteSpan(data(), size() + next_chunk->size());
+ next_chunk->RemoveFromRegionList();
+ region_tracker_->DeallocateChunkClass(next_chunk);
+ return true;
+}
+
+void Chunk::InsertAfterInRegionList(Chunk* new_chunk)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(region_tracker_ -> lock_) {
+ new_chunk->next_in_region_ = next_in_region_;
+ new_chunk->prev_in_region_ = this;
+ if (next_in_region_ != nullptr) {
+ next_in_region_->prev_in_region_ = new_chunk;
+ }
+ next_in_region_ = new_chunk;
+}
+
+void Chunk::InsertBeforeInRegionList(Chunk* new_chunk)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(region_tracker_ -> lock_) {
+ new_chunk->next_in_region_ = this;
+ new_chunk->prev_in_region_ = prev_in_region_;
+ if (prev_in_region_ != nullptr) {
+ prev_in_region_->next_in_region_ = new_chunk;
+ }
+ prev_in_region_ = new_chunk;
+}
+
+void Chunk::RemoveFromRegionList()
+ PW_EXCLUSIVE_LOCKS_REQUIRED(region_tracker_ -> lock_) {
+ if (prev_in_region_ != nullptr) {
+ prev_in_region_->next_in_region_ = next_in_region_;
+ }
+ if (next_in_region_ != nullptr) {
+ next_in_region_->prev_in_region_ = prev_in_region_;
+ }
+ prev_in_region_ = nullptr;
+ next_in_region_ = nullptr;
+}
+
+std::optional<OwnedChunk> Chunk::CreateFirstForRegion(
+ ChunkRegionTracker& region_tracker) {
+ void* memory = region_tracker.AllocateChunkClass();
+ if (memory == nullptr) {
+ return std::nullopt;
+ }
+ // Note: `Region()` is `const`, so no lock is required.
+ Chunk* chunk = new (memory) Chunk(&region_tracker, region_tracker.Region());
+ return OwnedChunk(chunk);
+}
+
+void Chunk::Free() {
+ span_ = ByteSpan();
+ bool region_empty;
+ // Record `region_track` so that it is available for use even after
+ // `this` is free'd by the call to `DeallocateChunkClass`.
+ ChunkRegionTracker* region_tracker = region_tracker_;
+ {
+ std::lock_guard lock(region_tracker_->lock_);
+ region_empty = prev_in_region_ == nullptr && next_in_region_ == nullptr;
+ RemoveFromRegionList();
+ // NOTE: do *not* attempt to access any fields of `this` after this point.
+ //
+ // The lock must be held while deallocating this, otherwise another
+ // ``Chunk::Release`` in the same region could race and call
+ // ``ChunkRegionTracker::Destroy``, making this call no longer valid.
+ region_tracker->DeallocateChunkClass(this);
+ }
+ if (region_empty) {
+ region_tracker->Destroy();
+ }
+}
+
+void OwnedChunk::Release() {
+ if (inner_ == nullptr) {
+ return;
+ }
+ inner_->Free();
+ inner_ = nullptr;
+}
+
+bool Chunk::ClaimPrefix(size_t bytes_to_claim) {
+ if (bytes_to_claim == 0) {
+ return true;
+ }
+ // In order to roll back `bytes_to_claim`, the current chunk must start at
+ // least `bytes_to_claim` after the beginning of the current region.
+ std::byte* new_start = CheckedSub(data(), bytes_to_claim);
+ // Note: `Region()` is `const`, so no lock is required.
+ if (new_start == nullptr || new_start < BeginPtr(region_tracker_->Region())) {
+ return false;
+ }
+
+ // `lock` is acquired in order to traverse the linked list and mutate `span_`.
+ std::lock_guard lock(region_tracker_->lock_);
+
+ // If there are any chunks before this one, they must not end after
+ // `new_start`.
+ Chunk* prev = prev_in_region_;
+ if (prev != nullptr && EndPtr(prev->span()) > new_start) {
+ return false;
+ }
+
+ size_t old_size = span_.size();
+ span_ = ByteSpan(new_start, old_size + bytes_to_claim);
+ return true;
+}
+
+bool Chunk::ClaimSuffix(size_t bytes_to_claim) {
+ if (bytes_to_claim == 0) {
+ return true;
+ }
+ // In order to expand forward `bytes_to_claim`, the current chunk must start
+ // at least `subytes` before the end of the current region.
+ std::byte* new_end = CheckedAdd(EndPtr(span()), bytes_to_claim);
+ // Note: `Region()` is `const`, so no lock is required.
+ if (new_end == nullptr || new_end > EndPtr(region_tracker_->Region())) {
+ return false;
+ }
+
+ // `lock` is acquired in order to traverse the linked list and mutate `span_`.
+ std::lock_guard lock(region_tracker_->lock_);
+
+ // If there are any chunks after this one, they must not start before
+ // `new_end`.
+ Chunk* next = next_in_region_;
+ if (next != nullptr && BeginPtr(next->span_) < new_end) {
+ return false;
+ }
+
+ size_t old_size = span_.size();
+ span_ = ByteSpan(data(), old_size + bytes_to_claim);
+ return true;
+}
+
+void Chunk::DiscardFront(size_t bytes_to_discard) {
+ Slice(bytes_to_discard, size());
+}
+
+void Chunk::Slice(size_t begin, size_t end) {
+ PW_DCHECK(begin <= size());
+ PW_DCHECK(end <= size());
+ PW_DCHECK(end >= begin);
+ ByteSpan new_span(data() + begin, end - begin);
+ std::lock_guard lock(region_tracker_->lock_);
+ span_ = new_span;
+}
+
+void Chunk::Truncate(size_t len) { Slice(0, len); }
+
+std::optional<OwnedChunk> Chunk::TakeFront(size_t bytes_to_take) {
+ void* new_chunk_memory = region_tracker_->AllocateChunkClass();
+ if (new_chunk_memory == nullptr) {
+ return std::nullopt;
+ }
+
+ PW_DCHECK(bytes_to_take <= size());
+ ByteSpan first_span = ByteSpan(data(), bytes_to_take);
+ ByteSpan second_span =
+ ByteSpan(data() + bytes_to_take, size() - bytes_to_take);
+
+ std::lock_guard lock(region_tracker_->lock_);
+ span_ = second_span;
+ Chunk* new_chunk = new (new_chunk_memory) Chunk(region_tracker_, first_span);
+ InsertBeforeInRegionList(new_chunk);
+ return OwnedChunk(new_chunk);
+}
+
+std::optional<OwnedChunk> Chunk::TakeTail(size_t bytes_to_take) {
+ void* new_chunk_memory = region_tracker_->AllocateChunkClass();
+ if (new_chunk_memory == nullptr) {
+ return std::nullopt;
+ }
+
+ PW_DCHECK(bytes_to_take <= size());
+ ByteSpan first_span = ByteSpan(data(), size() - bytes_to_take);
+ ByteSpan second_span =
+ ByteSpan(EndPtr(span()) - bytes_to_take, bytes_to_take);
+
+ std::lock_guard lock(region_tracker_->lock_);
+ span_ = first_span;
+ Chunk* new_chunk = new (new_chunk_memory) Chunk(region_tracker_, second_span);
+ InsertAfterInRegionList(new_chunk);
+ return OwnedChunk(new_chunk);
+}
+
+} // namespace pw::multibuf
diff --git a/pw_multibuf/chunk_test.cc b/pw_multibuf/chunk_test.cc
new file mode 100644
index 000000000..23ff0e3f6
--- /dev/null
+++ b/pw_multibuf/chunk_test.cc
@@ -0,0 +1,430 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_multibuf/chunk.h"
+
+#include <memory>
+
+#if __cplusplus >= 202002L
+#include <ranges>
+#endif // __cplusplus >= 202002L
+
+#include "gtest/gtest.h"
+#include "pw_multibuf/internal/test_utils.h"
+
+namespace pw::multibuf {
+namespace {
+
+using ::pw::multibuf::internal::HeaderChunkRegionTracker;
+using ::pw::multibuf::internal::TrackingAllocatorWithMemory;
+
+/// Returns literal with ``_size`` suffix as a ``size_t``.
+///
+/// This is useful for writing size-related test assertions without
+/// explicit (verbose) casts.
+constexpr size_t operator"" _size(unsigned long long n) { return n; }
+
+const size_t kArbitraryAllocatorSize = 1024;
+const size_t kArbitraryChunkSize = 32;
+
+#if __cplusplus >= 202002L
+static_assert(std::ranges::contiguous_range<Chunk>);
+#endif // __cplusplus >= 202002L
+
+void TakesSpan([[maybe_unused]] ByteSpan span) {}
+
+TEST(Chunk, IsImplicitlyConvertibleToSpan) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk.has_value());
+ // ``Chunk`` should convert to ``ByteSpan``.
+ TakesSpan(**chunk);
+}
+
+TEST(OwnedChunk, ReleaseDestroysChunkRegion) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ auto tracker =
+ HeaderChunkRegionTracker::AllocateRegion(&alloc, kArbitraryChunkSize);
+ ASSERT_NE(tracker, nullptr);
+ EXPECT_EQ(alloc.count(), 1_size);
+
+ std::optional<OwnedChunk> chunk_opt = Chunk::CreateFirstForRegion(*tracker);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ EXPECT_EQ(alloc.count(), 2_size);
+ EXPECT_EQ(chunk.size(), kArbitraryChunkSize);
+
+ chunk.Release();
+ EXPECT_EQ(chunk.size(), 0_size);
+ EXPECT_EQ(alloc.count(), 0_size);
+ EXPECT_EQ(alloc.used(), 0_size);
+}
+
+TEST(OwnedChunk, DestructorDestroysChunkRegion) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ auto tracker =
+ HeaderChunkRegionTracker::AllocateRegion(&alloc, kArbitraryChunkSize);
+ ASSERT_NE(tracker, nullptr);
+ EXPECT_EQ(alloc.count(), 1_size);
+
+ {
+ std::optional<OwnedChunk> chunk = Chunk::CreateFirstForRegion(*tracker);
+ ASSERT_TRUE(chunk.has_value());
+ EXPECT_EQ(alloc.count(), 2_size);
+ EXPECT_EQ(chunk->size(), kArbitraryChunkSize);
+ }
+
+ EXPECT_EQ(alloc.count(), 0_size);
+ EXPECT_EQ(alloc.used(), 0_size);
+}
+
+TEST(Chunk, DiscardFrontDiscardsFrontOfSpan) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk.span();
+ const size_t kDiscarded = 4;
+ chunk->DiscardFront(kDiscarded);
+ EXPECT_EQ(chunk.size(), old_span.size() - kDiscarded);
+ EXPECT_EQ(chunk.data(), old_span.data() + kDiscarded);
+}
+
+TEST(Chunk, TakeFrontTakesFrontOfSpan) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk.span();
+ const size_t kTaken = 4;
+ std::optional<OwnedChunk> front_opt = chunk->TakeFront(kTaken);
+ ASSERT_TRUE(front_opt.has_value());
+ auto& front = *front_opt;
+ EXPECT_EQ(front->size(), kTaken);
+ EXPECT_EQ(front->data(), old_span.data());
+ EXPECT_EQ(chunk.size(), old_span.size() - kTaken);
+ EXPECT_EQ(chunk.data(), old_span.data() + kTaken);
+}
+
+TEST(Chunk, TruncateDiscardsEndOfSpan) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk.span();
+ const size_t kShorter = 5;
+ chunk->Truncate(old_span.size() - kShorter);
+ EXPECT_EQ(chunk.size(), old_span.size() - kShorter);
+ EXPECT_EQ(chunk.data(), old_span.data());
+}
+
+TEST(Chunk, TakeTailTakesEndOfSpan) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk.span();
+ const size_t kTaken = 5;
+ std::optional<OwnedChunk> tail_opt = chunk->TakeTail(kTaken);
+ ASSERT_TRUE(tail_opt.has_value());
+ auto& tail = *tail_opt;
+ EXPECT_EQ(tail.size(), kTaken);
+ EXPECT_EQ(tail.data(), old_span.data() + old_span.size() - kTaken);
+ EXPECT_EQ(chunk.size(), old_span.size() - kTaken);
+ EXPECT_EQ(chunk.data(), old_span.data());
+}
+
+TEST(Chunk, SliceRemovesSidesOfSpan) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk.span();
+ const size_t kBegin = 4;
+ const size_t kEnd = 9;
+ chunk->Slice(kBegin, kEnd);
+ EXPECT_EQ(chunk.data(), old_span.data() + kBegin);
+ EXPECT_EQ(chunk.size(), kEnd - kBegin);
+}
+
+TEST(Chunk, RegionPersistsUntilAllChunksReleased) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ // One allocation for the region tracker, one for the chunk.
+ EXPECT_EQ(alloc.count(), 2_size);
+ const size_t kSplitPoint = 13;
+ auto split_opt = chunk->TakeFront(kSplitPoint);
+ ASSERT_TRUE(split_opt.has_value());
+ auto& split = *split_opt;
+ // One allocation for the region tracker, one for each of two chunks.
+ EXPECT_EQ(alloc.count(), 3_size);
+ chunk.Release();
+ EXPECT_EQ(alloc.count(), 2_size);
+ split.Release();
+ EXPECT_EQ(alloc.count(), 0_size);
+}
+
+TEST(Chunk, ClaimPrefixReclaimsDiscardedFront) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk.span();
+ const size_t kDiscarded = 4;
+ chunk->DiscardFront(kDiscarded);
+ EXPECT_TRUE(chunk->ClaimPrefix(kDiscarded));
+ EXPECT_EQ(chunk.size(), old_span.size());
+ EXPECT_EQ(chunk.data(), old_span.data());
+}
+
+TEST(Chunk, ClaimPrefixFailsOnFullRegionChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ EXPECT_FALSE(chunk->ClaimPrefix(1));
+}
+
+TEST(Chunk, ClaimPrefixFailsOnNeighboringChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ const size_t kSplitPoint = 22;
+ auto front = chunk->TakeFront(kSplitPoint);
+ ASSERT_TRUE(front.has_value());
+ EXPECT_FALSE(chunk->ClaimPrefix(1));
+}
+
+TEST(Chunk,
+ ClaimPrefixFailsAtStartOfRegionEvenAfterReleasingChunkAtEndOfRegion) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ const size_t kTaken = 13;
+ auto split = chunk->TakeTail(kTaken);
+ ASSERT_TRUE(split.has_value());
+ split->Release();
+ EXPECT_FALSE(chunk->ClaimPrefix(1));
+}
+
+TEST(Chunk, ClaimPrefixReclaimsPrecedingChunksDiscardedSuffix) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ const size_t kSplitPoint = 13;
+ auto split_opt = chunk->TakeFront(kSplitPoint);
+ ASSERT_TRUE(split_opt.has_value());
+ auto& split = *split_opt;
+ const size_t kDiscard = 3;
+ split->Truncate(split.size() - kDiscard);
+ EXPECT_TRUE(chunk->ClaimPrefix(kDiscard));
+ EXPECT_FALSE(chunk->ClaimPrefix(1));
+}
+
+TEST(Chunk, ClaimSuffixReclaimsTruncatedEnd) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ ConstByteSpan old_span = chunk->span();
+ const size_t kDiscarded = 4;
+ chunk->Truncate(old_span.size() - kDiscarded);
+ EXPECT_TRUE(chunk->ClaimSuffix(kDiscarded));
+ EXPECT_EQ(chunk->size(), old_span.size());
+ EXPECT_EQ(chunk->data(), old_span.data());
+}
+
+TEST(Chunk, ClaimSuffixFailsOnFullRegionChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ EXPECT_FALSE(chunk->ClaimSuffix(1));
+}
+
+TEST(Chunk, ClaimSuffixFailsWithNeighboringChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ const size_t kSplitPoint = 22;
+ auto split_opt = chunk->TakeFront(kSplitPoint);
+ ASSERT_TRUE(split_opt.has_value());
+ auto& split = *split_opt;
+ EXPECT_FALSE(split->ClaimSuffix(1));
+}
+
+TEST(Chunk, ClaimSuffixFailsAtEndOfRegionEvenAfterReleasingFirstChunkInRegion) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ const size_t kTaken = 22;
+ auto split_opt = chunk->TakeTail(kTaken);
+ ASSERT_TRUE(split_opt.has_value());
+ auto& split = *split_opt;
+ EXPECT_FALSE(split->ClaimSuffix(1));
+}
+
+TEST(Chunk, ClaimSuffixReclaimsFollowingChunksDiscardedPrefix) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_opt.has_value());
+ auto& chunk = *chunk_opt;
+ const size_t kSplitPoint = 22;
+ auto split_opt = chunk->TakeFront(kSplitPoint);
+ ASSERT_TRUE(split_opt.has_value());
+ auto& split = *split_opt;
+ const size_t kDiscarded = 3;
+ chunk->DiscardFront(kDiscarded);
+ EXPECT_TRUE(split->ClaimSuffix(kDiscarded));
+ EXPECT_FALSE(split->ClaimSuffix(1));
+}
+
+TEST(Chunk, MergeReturnsFalseForChunksFromDifferentRegions) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_1_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_1_opt.has_value());
+ OwnedChunk& chunk_1 = *chunk_1_opt;
+ std::optional<OwnedChunk> chunk_2_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_2_opt.has_value());
+ OwnedChunk& chunk_2 = *chunk_2_opt;
+ EXPECT_FALSE(chunk_1->CanMerge(*chunk_2));
+ EXPECT_FALSE(chunk_1->Merge(chunk_2));
+ // Ensure that neither chunk was modified
+ EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize);
+ EXPECT_EQ(chunk_2.size(), kArbitraryChunkSize);
+}
+
+TEST(Chunk, MergeReturnsFalseForNonAdjacentChunksFromSameRegion) {
+ const size_t kTakenFromOne = 8;
+ const size_t kTakenFromTwo = 4;
+
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_1_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_1_opt.has_value());
+ OwnedChunk& chunk_1 = *chunk_1_opt;
+
+ std::optional<OwnedChunk> chunk_2_opt = chunk_1->TakeTail(kTakenFromOne);
+ ASSERT_TRUE(chunk_2_opt.has_value());
+ OwnedChunk& chunk_2 = *chunk_2_opt;
+
+ std::optional<OwnedChunk> chunk_3_opt = chunk_2->TakeTail(kTakenFromTwo);
+ ASSERT_TRUE(chunk_3_opt.has_value());
+ OwnedChunk& chunk_3 = *chunk_3_opt;
+
+ EXPECT_FALSE(chunk_1->CanMerge(*chunk_3));
+ EXPECT_FALSE(chunk_1->Merge(chunk_3));
+ EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize - kTakenFromOne);
+ EXPECT_EQ(chunk_2.size(), kTakenFromOne - kTakenFromTwo);
+ EXPECT_EQ(chunk_3.size(), kTakenFromTwo);
+}
+
+TEST(Chunk, MergeJoinsMultipleAdjacentChunksFromSameRegion) {
+ const size_t kTakenFromOne = 8;
+ const size_t kTakenFromTwo = 4;
+
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_1_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_1_opt.has_value());
+ OwnedChunk& chunk_1 = *chunk_1_opt;
+
+ std::optional<OwnedChunk> chunk_2_opt = chunk_1->TakeTail(kTakenFromOne);
+ ASSERT_TRUE(chunk_2_opt.has_value());
+ OwnedChunk& chunk_2 = *chunk_2_opt;
+
+ std::optional<OwnedChunk> chunk_3_opt = chunk_2->TakeTail(kTakenFromTwo);
+ ASSERT_TRUE(chunk_3_opt.has_value());
+ OwnedChunk& chunk_3 = *chunk_3_opt;
+
+ EXPECT_TRUE(chunk_1->CanMerge(*chunk_2));
+ EXPECT_TRUE(chunk_1->Merge(chunk_2));
+ EXPECT_TRUE(chunk_1->CanMerge(*chunk_3));
+ EXPECT_TRUE(chunk_1->Merge(chunk_3));
+
+ EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize);
+ EXPECT_EQ(chunk_2.size(), 0_size);
+ EXPECT_EQ(chunk_3.size(), 0_size);
+}
+
+TEST(Chunk, MergeJoinsAdjacentChunksFromSameRegion) {
+ const size_t kTaken = 4;
+
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ std::optional<OwnedChunk> chunk_1_opt =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc,
+ kArbitraryChunkSize);
+ ASSERT_TRUE(chunk_1_opt.has_value());
+ OwnedChunk& chunk_1 = *chunk_1_opt;
+ std::optional<OwnedChunk> chunk_2_opt = chunk_1->TakeTail(kTaken);
+ ASSERT_TRUE(chunk_2_opt.has_value());
+ OwnedChunk& chunk_2 = *chunk_2_opt;
+ EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize - kTaken);
+ EXPECT_EQ(chunk_2.size(), kTaken);
+
+ EXPECT_TRUE(chunk_1->CanMerge(*chunk_2));
+ EXPECT_TRUE(chunk_1->Merge(chunk_2));
+ EXPECT_EQ(chunk_1.size(), kArbitraryChunkSize);
+ EXPECT_EQ(chunk_2.size(), 0_size);
+}
+
+} // namespace
+} // namespace pw::multibuf
diff --git a/pw_multibuf/docs.rst b/pw_multibuf/docs.rst
new file mode 100644
index 000000000..e19f8e7ce
--- /dev/null
+++ b/pw_multibuf/docs.rst
@@ -0,0 +1,87 @@
+.. _module-pw_multibuf:
+
+===========
+pw_multibuf
+===========
+.. pigweed-module::
+ :name: pw_multibuf
+ :tagline: A buffer API optimized for zero-copy messaging
+ :status: unstable
+ :languages: C++17
+
+Sending or receiving messages via RPC, transfer, or sockets often requires a
+series of intermediate buffers, each requiring their own copy of the data.
+``pw_multibuf`` allows data to be written *once*, eliminating the memory, CPU
+and latency overhead of copying.
+
+-----------------
+How does it work?
+-----------------
+``pw_multibuf`` uses several techniques to minimize copying of data:
+
+- **Header and Footer Reservation**: Lower-level components can reserve space
+ within a buffer for headers and/or footers. This allows headers and footers
+ to be added to user-provided data without moving users' data.
+- **Native Scatter/Gather and Fragmentation Support**: Buffers can refer to
+ multiple separate chunks of memory. Messages can be built up from
+ discontiguous allocations, and users' data can be fragmented across multiple
+ packets.
+- **Divisible Memory Regions**: Incoming buffers can be divided without a copy,
+ allowing incoming data to be freely demultiplexed.
+
+-------------------------------
+What kinds of data is this for?
+-------------------------------
+``pw_multibuf`` is best used in code that wants to read, write, or pass along
+data which are one of the following:
+
+- **Large**: ``pw_multibuf`` is designed to allow breaking up data into
+ multiple chunks. It also supports asynchronous allocation for when there may
+ not be sufficient space for incoming data.
+- **Communications-Oriented**: Data which is being received or sent across
+ sockets, various packets, or shared-memory protocols can benefit from the
+ fragmentation, multiplexing, and header/footer-reservation properties of
+ ``pw_multibuf``.
+- **Copy-Averse**: ``pw_multibuf`` is structured to allow users to pass around
+ and mutate buffers without copying or moving data in-memory. This can be
+ especially useful when working in systems that are latency-sensitive,
+ need to pass large amounts of data, or when memory usage is constrained.
+
+-------------
+API Reference
+-------------
+Most users of ``pw_multibuf`` will start by allocating a ``MultiBuf`` using
+a ``MultiBufAllocator`` class.
+
+``MultiBuf`` s consist of a number of ``Chunk`` s of contiguous memory.
+These ``Chunk`` s can be grown, shrunk, modified, or extracted from the
+``MultiBuf``. ``MultiBuf`` exposes an ``std::byte`` iterator interface as well
+as a ``Chunk`` iterator available through the ``Chunks()`` method.
+
+An RAII-style ``OwnedChunk`` is also provided, and manages the lifetime of
+``Chunk`` s which are not currently stored inside of a ``MultiBuf``.
+
+.. doxygenclass:: pw::multibuf::Chunk
+ :members:
+
+.. doxygenclass:: pw::multibuf::OwnedChunk
+ :members:
+
+.. doxygenclass:: pw::multibuf::MultiBuf
+ :members:
+
+.. doxygenclass:: pw::multibuf::MultiBufAllocator
+ :members:
+
+---------------------------
+Allocator Implementors' API
+---------------------------
+Some users will need to directly implement the ``MultiBufAllocator`` interface
+in order to provide allocation out of a particular region, provide particular
+allocation policy, fix Chunks to some size (such as MTU size - header for
+socket implementations), or specify other custom behavior.
+
+These users will also need to understand and implement the following APIs:
+
+.. doxygenclass:: pw::multibuf::ChunkRegionTracker
+ :members:
diff --git a/pw_multibuf/multibuf.cc b/pw_multibuf/multibuf.cc
new file mode 100644
index 000000000..b1eb80568
--- /dev/null
+++ b/pw_multibuf/multibuf.cc
@@ -0,0 +1,129 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_multibuf/multibuf.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::multibuf {
+
+void MultiBuf::Release() noexcept {
+ while (first_ != nullptr) {
+ Chunk* removed = first_;
+ first_ = first_->next_in_buf_;
+ removed->Free();
+ }
+}
+
+size_t MultiBuf::size() const {
+ size_t len = 0;
+ for (const auto& chunk : Chunks()) {
+ len += chunk.size();
+ }
+ return len;
+}
+
+void MultiBuf::PushFrontChunk(OwnedChunk chunk) {
+ PW_DCHECK(chunk->next_in_buf_ == nullptr);
+ Chunk* new_chunk = std::move(chunk).Take();
+ Chunk* old_first = first_;
+ new_chunk->next_in_buf_ = old_first;
+ first_ = new_chunk;
+}
+
+MultiBuf::ChunkIterator MultiBuf::InsertChunk(ChunkIterator position,
+ OwnedChunk chunk) {
+ // Note: this also catches the cases where ``first_ == nullptr``
+ PW_DCHECK(chunk->next_in_buf_ == nullptr);
+ if (position == ChunkBegin()) {
+ PushFrontChunk(std::move(chunk));
+ return ChunkIterator(first_);
+ }
+ Chunk* previous = Previous(position.chunk_);
+ Chunk* old_next = previous->next_in_buf_;
+ Chunk* new_chunk = std::move(chunk).Take();
+ new_chunk->next_in_buf_ = old_next;
+ previous->next_in_buf_ = new_chunk;
+ return ChunkIterator(new_chunk);
+}
+
+std::tuple<MultiBuf::ChunkIterator, OwnedChunk> MultiBuf::TakeChunk(
+ ChunkIterator position) {
+ Chunk* chunk = position.chunk_;
+ if (position == ChunkBegin()) {
+ Chunk* old_first = first_;
+ first_ = old_first->next_in_buf_;
+ old_first->next_in_buf_ = nullptr;
+ return std::make_tuple(ChunkIterator(first_), OwnedChunk(old_first));
+ }
+ Chunk* previous = Previous(chunk);
+ previous->next_in_buf_ = chunk->next_in_buf_;
+ chunk->next_in_buf_ = nullptr;
+ return std::make_tuple(ChunkIterator(previous->next_in_buf_),
+ OwnedChunk(chunk));
+}
+
+Chunk* MultiBuf::Previous(Chunk* chunk) const {
+ Chunk* previous = first_;
+ while (previous != nullptr && previous->next_in_buf_ != chunk) {
+ previous = previous->next_in_buf_;
+ }
+ return previous;
+}
+
+MultiBuf::iterator& MultiBuf::iterator::operator++() {
+ if (byte_index_ + 1 == chunk_->size()) {
+ chunk_ = chunk_->next_in_buf_;
+ byte_index_ = 0;
+ AdvanceToData();
+ } else {
+ ++byte_index_;
+ }
+ return *this;
+}
+
+void MultiBuf::iterator::AdvanceToData() {
+ while (chunk_ != nullptr && chunk_->size() == 0) {
+ chunk_ = chunk_->next_in_buf_;
+ }
+}
+
+MultiBuf::const_iterator& MultiBuf::const_iterator::operator++() {
+ if (byte_index_ + 1 == chunk_->size()) {
+ chunk_ = chunk_->next_in_buf_;
+ byte_index_ = 0;
+ AdvanceToData();
+ } else {
+ ++byte_index_;
+ }
+ return *this;
+}
+
+void MultiBuf::const_iterator::AdvanceToData() {
+ while (chunk_ != nullptr && chunk_->size() == 0) {
+ chunk_ = chunk_->next_in_buf_;
+ }
+}
+
+size_t MultiBuf::ChunkIterable::size() const {
+ Chunk* current = first_;
+ size_t i = 0;
+ while (current != nullptr) {
+ ++i;
+ current = current->next_in_buf_;
+ }
+ return i;
+}
+
+} // namespace pw::multibuf
diff --git a/pw_multibuf/multibuf_test.cc b/pw_multibuf/multibuf_test.cc
new file mode 100644
index 000000000..d1bfe969d
--- /dev/null
+++ b/pw_multibuf/multibuf_test.cc
@@ -0,0 +1,214 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_multibuf/multibuf.h"
+
+#include "gtest/gtest.h"
+#include "pw_bytes/suffix.h"
+#include "pw_multibuf/internal/test_utils.h"
+
+namespace pw::multibuf {
+namespace {
+
+using ::pw::multibuf::internal::HeaderChunkRegionTracker;
+using ::pw::multibuf::internal::TrackingAllocatorWithMemory;
+
+const size_t kArbitraryAllocatorSize = 1024;
+const size_t kArbitraryChunkSize = 32;
+
+#if __cplusplus >= 202002L
+static_assert(std::forward_iterator<MultiBuf::iterator>);
+static_assert(std::forward_iterator<MultiBuf::const_iterator>);
+static_assert(std::forward_iterator<MultiBuf::ChunkIterator>);
+static_assert(std::forward_iterator<MultiBuf::ConstChunkIterator>);
+#endif // __cplusplus >= 202002L
+
+OwnedChunk MakeChunk(pw::allocator::Allocator& alloc, size_t size) {
+ std::optional<OwnedChunk> chunk =
+ HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc, size);
+ assert(chunk.has_value());
+ return std::move(*chunk);
+}
+
+TEST(MultiBuf, IsDefaultConstructible) { [[maybe_unused]] MultiBuf buf; }
+
+TEST(MultiBuf, WithOneChunkReleases) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+ EXPECT_EQ(alloc.count(), 2U);
+ buf.Release();
+ EXPECT_EQ(alloc.count(), 0U);
+}
+
+TEST(MultiBuf, WithOneChunkReleasesOnDestruction) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ {
+ MultiBuf buf;
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+ EXPECT_EQ(alloc.count(), 2U);
+ }
+ EXPECT_EQ(alloc.count(), 0U);
+}
+
+TEST(MultiBuf, WithMultipleChunksReleasesAllOnDestruction) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ {
+ MultiBuf buf;
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+ EXPECT_EQ(alloc.count(), 4U);
+ }
+ EXPECT_EQ(alloc.count(), 0U);
+}
+
+TEST(MultiBuf, SizeReturnsNumberOfBytes) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+ EXPECT_EQ(buf.size(), 0U);
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+ EXPECT_EQ(buf.size(), kArbitraryChunkSize);
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+ EXPECT_EQ(buf.size(), kArbitraryChunkSize * 2);
+}
+
+TEST(MultiBuf, PushFrontChunkAddsBytesToFront) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+
+ const std::array<std::byte, 3> kBytesOne = {0_b, 1_b, 2_b};
+ auto chunk_one = MakeChunk(alloc, kBytesOne.size());
+ std::copy(kBytesOne.begin(), kBytesOne.end(), chunk_one->begin());
+ buf.PushFrontChunk(std::move(chunk_one));
+
+ size_t i = 0;
+ auto buf_iter = buf.begin();
+ for (; i < kBytesOne.size(); i++, buf_iter++) {
+ ASSERT_NE(buf_iter, buf.end());
+ EXPECT_EQ(*buf_iter, kBytesOne[i]);
+ }
+
+ const std::array<std::byte, 4> kBytesTwo = {9_b, 10_b, 11_b, 12_b};
+ auto chunk_two = MakeChunk(alloc, kBytesTwo.size());
+ std::copy(kBytesTwo.begin(), kBytesTwo.end(), chunk_two->begin());
+ buf.PushFrontChunk(std::move(chunk_two));
+
+ std::array<std::byte, 7> expected = {
+ 9_b,
+ 10_b,
+ 11_b,
+ 12_b,
+ 0_b,
+ 1_b,
+ 2_b,
+ };
+ i = 0;
+ buf_iter = buf.begin();
+ for (; i < expected.size(); i++, buf_iter++) {
+ ASSERT_NE(buf_iter, buf.end());
+ EXPECT_EQ(*buf_iter, expected[i]);
+ }
+}
+
+TEST(MultiBuf, InsertChunkOnEmptyBufAddsFirstChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+
+ const std::array<std::byte, 3> kBytes = {0_b, 1_b, 2_b};
+ auto chunk = MakeChunk(alloc, kBytes.size());
+ std::copy(kBytes.begin(), kBytes.end(), chunk->begin());
+ auto inserted_iter = buf.InsertChunk(buf.Chunks().begin(), std::move(chunk));
+ EXPECT_EQ(inserted_iter, buf.Chunks().begin());
+
+ size_t i = 0;
+ auto buf_iter = buf.begin();
+ for (; i < kBytes.size(); i++, buf_iter++) {
+ ASSERT_NE(buf_iter, buf.end());
+ EXPECT_EQ(*buf_iter, kBytes[i]);
+ }
+
+ EXPECT_EQ(++inserted_iter, buf.Chunks().end());
+}
+
+TEST(MultiBuf, InsertChunkAtEndOfBufAddsLastChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+
+ // Add a chunk to the beginning
+ buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+
+ const std::array<std::byte, 3> kBytes = {0_b, 1_b, 2_b};
+ auto chunk = MakeChunk(alloc, kBytes.size());
+ std::copy(kBytes.begin(), kBytes.end(), chunk->begin());
+ auto inserted_iter = buf.InsertChunk(buf.Chunks().end(), std::move(chunk));
+ EXPECT_EQ(inserted_iter, ++buf.Chunks().begin());
+ EXPECT_EQ(++inserted_iter, buf.Chunks().end());
+
+ size_t i = 0;
+ auto buf_iter = buf.Chunks().begin();
+ buf_iter++;
+ auto chunk_iter = buf_iter->begin();
+ for (; i < kBytes.size(); i++, chunk_iter++) {
+ ASSERT_NE(chunk_iter, buf_iter->end());
+ EXPECT_EQ(*chunk_iter, kBytes[i]);
+ }
+}
+
+TEST(MultiBuf, TakeChunkAtBeginRemovesAndReturnsFirstChunk) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+ auto insert_iter = buf.Chunks().begin();
+ insert_iter = buf.InsertChunk(insert_iter, MakeChunk(alloc, 2));
+ insert_iter = buf.InsertChunk(++insert_iter, MakeChunk(alloc, 4));
+
+ auto [chunk_iter, chunk] = buf.TakeChunk(buf.Chunks().begin());
+ EXPECT_EQ(chunk.size(), 2U);
+ EXPECT_EQ(chunk_iter->size(), 4U);
+ chunk_iter++;
+ EXPECT_EQ(chunk_iter, buf.Chunks().end());
+}
+
+TEST(MultiBuf, TakeChunkOnLastInsertedIterReturnsLastInserted) {
+ TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+ MultiBuf buf;
+ auto iter = buf.Chunks().begin();
+ iter = buf.InsertChunk(iter, MakeChunk(alloc, 42));
+ iter = buf.InsertChunk(++iter, MakeChunk(alloc, 11));
+ iter = buf.InsertChunk(++iter, MakeChunk(alloc, 65));
+ OwnedChunk chunk;
+ std::tie(iter, chunk) = buf.TakeChunk(iter);
+ EXPECT_EQ(iter, buf.Chunks().end());
+ EXPECT_EQ(chunk.size(), 65U);
+}
+
+TEST(MultiBuf, RangeBasedForLoopsCompile) {
+ MultiBuf buf;
+ for ([[maybe_unused]] std::byte& byte : buf) {
+ }
+ for ([[maybe_unused]] const std::byte& byte : buf) {
+ }
+ for ([[maybe_unused]] Chunk& chunk : buf.Chunks()) {
+ }
+ for ([[maybe_unused]] const Chunk& chunk : buf.Chunks()) {
+ }
+
+ const MultiBuf const_buf;
+ for ([[maybe_unused]] const std::byte& byte : const_buf) {
+ }
+ for ([[maybe_unused]] const Chunk& chunk : const_buf.Chunks()) {
+ }
+}
+
+} // namespace
+} // namespace pw::multibuf
diff --git a/pw_multibuf/public/pw_multibuf/chunk.h b/pw_multibuf/public/pw_multibuf/chunk.h
new file mode 100644
index 000000000..9747b944a
--- /dev/null
+++ b/pw_multibuf/public/pw_multibuf/chunk.h
@@ -0,0 +1,372 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <optional>
+
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_sync/mutex.h"
+
+namespace pw::multibuf {
+
+class ChunkRegionTracker;
+class OwnedChunk;
+
+/// A handle to a contiguous slice of data.
+///
+/// A ``Chunk`` is similar to a ``ByteSpan``, but is aware of the underlying
+/// memory allocation, and is able to split, shrink, and grow into neighboring
+/// empty space.
+///
+/// This class is optimized to allow multiple owners to write into neighboring
+/// regions of the same allocation. One important usecase for this is
+/// communication protocols which want to reserve space at the front or rear of
+/// a buffer for headers or footers.
+///
+/// In order to support zero-copy DMA of communications buffers, allocators can
+/// create properly-aligned ``Chunk`` regions in appropriate memory. The driver
+/// can then ``DiscardFront`` in order to reserve bytes for headers,
+/// ``Truncate`` in order to reserve bytes for footers, and then pass the
+/// ``Chunk`` to the user to fill in. The header and footer space can then
+/// be reclaimed using the ``ClaimPrefix`` and ``ClaimSuffix`` methods.
+class Chunk {
+ public:
+ Chunk() = delete;
+ // Not copyable or movable.
+ Chunk(Chunk&) = delete;
+ Chunk& operator=(Chunk&) = delete;
+ Chunk(Chunk&&) = delete;
+ Chunk& operator=(Chunk&&) = delete;
+
+ /// Creates the first ``Chunk`` referencing a whole region of memory.
+ ///
+ /// This must only be called once per ``ChunkRegionTracker``, when the region
+ /// is first created. Multiple calls will result in undefined behavior.
+ ///
+ /// Returns ``std::nullopt`` if ``AllocateChunkStorage`` returns ``nullptr``.
+ static std::optional<OwnedChunk> CreateFirstForRegion(
+ ChunkRegionTracker& region_tracker);
+
+ std::byte* data() { return span_.data(); }
+ const std::byte* data() const { return span_.data(); }
+ size_t size() const { return span_.size(); }
+ ByteSpan span() { return span_; }
+ ConstByteSpan span() const { return span_; }
+
+ std::byte& operator[](size_t index) { return span_[index]; }
+ const std::byte& operator[](size_t index) const { return span_[index]; }
+
+ // Container declarations
+ using element_type = std::byte;
+ using value_type = std::byte;
+ using size_type = size_t;
+ using difference_type = ptrdiff_t;
+ using pointer = std::byte*;
+ using const_pointer = const std::byte*;
+ using reference = std::byte&;
+ using const_reference = const std::byte&;
+ using iterator = std::byte*;
+ using const_iterator = const std::byte*;
+ using reverse_iterator = std::byte*;
+ using const_reverse_iterator = const std::byte*;
+
+ std::byte* begin() { return span_.data(); }
+ const std::byte* cbegin() const { return span_.data(); }
+ std::byte* end() { return span_.data() + span_.size(); }
+ const std::byte* cend() const { return span_.data() + span_.size(); }
+
+ /// Returns if ``next_chunk`` is mergeable into the end of this ``Chunk``.
+ ///
+ /// This will only succeed when the two ``Chunk`` s are adjacent in
+ /// memory and originated from the same allocation.
+ [[nodiscard]] bool CanMerge(const Chunk& next_chunk) const;
+
+ /// Attempts to merge ``next_chunk`` into the end of this ``Chunk``.
+ ///
+ /// If the chunks are successfully merged, this ``Chunk`` will be extended
+ /// forwards to encompass the space of ``next_chunk``, and ``next_chunk``
+ /// will be emptied and ``Release``d.
+ ///
+ /// This will only succeed when the two ``Chunk`` s are adjacent in
+ /// memory and originated from the same allocation.
+ ///
+ /// If the chunks are not mergeable, neither ``Chunk`` will be modified.
+ bool Merge(OwnedChunk& next_chunk);
+
+ /// Attempts to add ``bytes_to_claim`` to the front of this buffer by
+ /// advancing its range backwards in memory. Returns ``true`` if the operation
+ /// succeeded.
+ ///
+ /// This will only succeed if this ``Chunk`` points to a section of a region
+ /// that has unreferenced bytes preceeding it. For example, a ``Chunk`` which
+ /// has been shrunk using ``DiscardFront`` can be re-expanded using
+ /// ``ClaimPrefix``.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ [[nodiscard]] bool ClaimPrefix(size_t bytes_to_claim);
+
+ /// Attempts to add ``bytes_to_claim`` to the front of this buffer by
+ /// advancing its range forwards in memory. Returns ``true`` if the operation
+ /// succeeded.
+ ///
+ /// This will only succeed if this ``Chunk`` points to a section of a region
+ /// that has unreferenced bytes following it. For example, a ``Chunk`` which
+ /// has been shrunk using ``Truncate`` can be re-expanded using
+ ///
+ /// This method will acquire a mutex and is not IRQ safe. /// ``ClaimSuffix``.
+ [[nodiscard]] bool ClaimSuffix(size_t bytes_to_claim);
+
+ /// Shrinks this handle to refer to the data beginning at offset
+ /// ``bytes_to_discard``.
+ ///
+ /// Does not modify the underlying data.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ void DiscardFront(size_t bytes_to_discard);
+
+ /// Shrinks this handle to refer to data in the range ``begin..<end``.
+ ///
+ /// Does not modify the underlying data.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ void Slice(size_t begin, size_t end);
+
+ /// Shrinks this handle to refer to only the first ``len`` bytes.
+ ///
+ /// Does not modify the underlying data.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ void Truncate(size_t len);
+
+ /// Attempts to shrink this handle to refer to the data beginning at
+ /// offset ``bytes_to_take``, returning the first ``bytes_to_take`` bytes as
+ /// a new ``OwnedChunk``.
+ ///
+ /// If the inner call to ``AllocateChunkClass`` fails, this function
+ /// will return ``std::nullopt` and this handle's span will not change.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ std::optional<OwnedChunk> TakeFront(size_t bytes_to_take);
+
+ /// Attempts to shrink this handle to refer only the first
+ /// ``len - bytes_to_take`` bytes, returning the last ``bytes_to_take``
+ /// bytes as a new ``OwnedChunk``.
+ ///
+ /// If the inner call to ``AllocateChunkClass`` fails, this function
+ /// will return ``std::nullopt`` and this handle's span will not change.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ std::optional<OwnedChunk> TakeTail(size_t bytes_to_take);
+
+ private:
+ Chunk(ChunkRegionTracker* region_tracker, ByteSpan span)
+ : region_tracker_(region_tracker),
+ next_in_region_(nullptr),
+ prev_in_region_(nullptr),
+ span_(span) {}
+
+ // NOTE: these functions are logically
+ // `PW_EXCLUSIVE_LOCKS_REQUIRED(region_tracker_->lock_)`, however this
+ // annotation cannot be applied due to the forward declaration of
+ // `region_tracker_`.
+ void InsertAfterInRegionList(Chunk* new_chunk);
+ void InsertBeforeInRegionList(Chunk* new_chunk);
+ void RemoveFromRegionList();
+
+ /// Frees this ``Chunk``. Future accesses to this class after calls to
+ /// ``Free`` are undefined behavior.
+ ///
+ /// Decrements the reference count on the underlying chunk of data.
+ ///
+ /// Does not modify the underlying data, but may cause it to be deallocated
+ /// if this was the only remaining ``Chunk`` referring to its region.
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ void Free();
+
+ /// The region_tracker_ for this chunk.
+ ChunkRegionTracker* const region_tracker_;
+
+ /// Pointer to the next chunk in the same ``region_tracker_``.
+ ///
+ /// Guarded by ``region_tracker_->lock_`` (but not annotated as such due to
+ /// the fact that annotations aren't smart enough to understand that all
+ /// chunks within a region share a tracker + lock).
+ ///
+ /// ``nullptr`` if this is the last ``Chunk`` in its ``region_tracker_``.
+ Chunk* next_in_region_;
+
+ /// Pointer to the previous chunk in the same ``region_tracker_``.
+ ///
+ /// Guarded by ``region_tracker_->lock_`` (but not annotated as such due to
+ /// the fact that annotations aren't smart enough to understand that all
+ /// chunks within a region share a tracker + lock).
+ ///
+ /// ``nullptr`` if this is the first ``Chunk`` in its ``region_tracker_``.
+ Chunk* prev_in_region_;
+
+ /// Pointer to the next chunk in a ``MultiBuf``.
+ ///
+ /// This is ``nullptr`` if this chunk is not in a ``MultiBuf``, or is the
+ /// last element of a ``MultiBuf``.
+ //
+ // reserved for use by the MultiBuf class.
+ Chunk* next_in_buf_;
+
+ /// Pointer to the sub-region to which this chunk has exclusive access.
+ ///
+ /// This ``span_`` is conceptually owned by this ``Chunk`` object, and
+ /// may be read or written to by a ``Chunk`` user (the normal rules of
+ /// thread compatibility apply-- `const` methods may be called
+ /// concurrently, non-`const` methods may not).
+ ///
+ /// Neighboring ``Chunk`` objects in this region looking to expand via
+ /// ``ClaimPrefix`` or ``ClaimSuffix`` may read this span's
+ /// boundaries. These other ``Chunk``s must acquire
+ /// ``region_tracker_->lock_`` before reading, and this ``Chunk`` must
+ /// acquire ``region_tracker_->lock_`` before changing ``span_``.
+ ByteSpan span_;
+
+ friend class OwnedChunk; // for ``Free``.
+ friend class MultiBuf; // for ``Free`` and ``next_in_buf_``.
+};
+
+/// An object that manages a single allocated region which is referenced by one
+/// or more ``Chunk`` objects.
+///
+/// This class is typically implemented by ``MultiBufAllocator``
+/// implementations in order to customize the behavior of region deallocation.
+///
+/// ``ChunkRegionTracker`` s have three responsibilities:
+///
+/// - Tracking the region of memory into which ``Chunk`` s can expand.
+/// This is reported via the ``Region`` method. ``Chunk`` s in this region
+/// can refer to memory within this region sparsely, but they can grow or
+/// shrink so long as they stay within the bounds of the ``Region``.
+///
+/// - Deallocating the region and the ``ChunkRegionTracker`` itself.
+/// This is implemented via the ``Destroy`` method, which is called once all
+/// of the ``Chunk`` s in a region have been released.
+///
+/// - Allocating and deallocating space for ``Chunk`` classes. This is merely
+/// allocating space for the ``Chunk`` object itself, not the memory to which
+/// it refers. This can be implemented straightforwardly by delegating to an
+/// existing generic allocator such as ``malloc`` or a
+/// ``pw::allocator::Allocator`` implementation.
+class ChunkRegionTracker {
+ protected:
+ ChunkRegionTracker() = default;
+ virtual ~ChunkRegionTracker() = default;
+
+ /// Destroys the ``ChunkRegionTracker``.
+ ///
+ /// Typical implementations will call ``std::destroy_at(this)`` and then free
+ /// the memory associated with the region and the tracker.
+ virtual void Destroy() = 0;
+
+ /// Returns the entire span of the region being managed.
+ ///
+ /// ``Chunk`` s referencing this tracker will not expand beyond this region,
+ /// nor into one another's portions of the region.
+ ///
+ /// This region must not change for the lifetime of this
+ /// ``ChunkRegionTracker``.
+ virtual ByteSpan Region() const = 0;
+
+ /// Returns a pointer to ``sizeof(Chunk)`` bytes.
+ /// Returns ``nullptr`` on failure.
+ virtual void* AllocateChunkClass() = 0;
+
+ /// Deallocates a pointer returned by ``AllocateChunkClass``.
+ virtual void DeallocateChunkClass(void*) = 0;
+
+ private:
+ /// A lock used to manage an internal linked-list of ``Chunk``s.
+ ///
+ /// This allows chunks to:
+ /// - know whether they can expand to fill neighboring regions of memory.
+ /// - know when the last chunk has been destructed, triggering `Destroy`.
+ pw::sync::Mutex lock_;
+ friend Chunk;
+};
+
+/// An RAII handle to a contiguous slice of data.
+///
+/// Note: ``OwnedChunk`` may acquire a ``pw::sync::Mutex`` during destruction,
+/// and so must not be destroyed within ISR contexts.
+class OwnedChunk {
+ public:
+ OwnedChunk() : inner_(nullptr) {}
+ OwnedChunk(OwnedChunk&& other) noexcept : inner_(other.inner_) {
+ other.inner_ = nullptr;
+ }
+ OwnedChunk& operator=(OwnedChunk&& other) noexcept {
+ inner_ = other.inner_;
+ other.inner_ = nullptr;
+ return *this;
+ }
+ /// This method will acquire a mutex and is not IRQ safe.
+ ~OwnedChunk() { Release(); }
+
+ std::byte* data() { return span().data(); }
+ const std::byte* data() const { return span().data(); }
+ size_t size() const { return span().size(); }
+ ByteSpan span() { return inner_ == nullptr ? ByteSpan() : inner_->span(); }
+ ConstByteSpan span() const {
+ return inner_ == nullptr ? ConstByteSpan() : inner_->span();
+ }
+
+ std::byte& operator[](size_t index) { return span()[index]; }
+ std::byte operator[](size_t index) const { return span()[index]; }
+
+ Chunk& operator*() { return *inner_; }
+ const Chunk& operator*() const { return *inner_; }
+ Chunk* operator->() { return inner_; }
+ const Chunk* operator->() const { return inner_; }
+
+ /// Decrements the reference count on the underlying chunk of data and
+ /// empties this handle so that `span()` now returns an empty (zero-sized)
+ /// span.
+ ///
+ /// Does not modify the underlying data, but may cause it to be deallocated
+ /// if this was the only remaining ``Chunk`` referring to its region.
+ ///
+ /// This method is equivalent to ``{ Chunk _unused = std::move(chunk_ref); }``
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ void Release();
+
+ /// Returns the contained ``Chunk*`` and empties this ``OwnedChunk`` without
+ /// releasing the underlying ``Chunk``.
+ Chunk* Take() && {
+ Chunk* result = inner_;
+ inner_ = nullptr;
+ return result;
+ }
+
+ private:
+ /// Constructs a new ``OwnedChunk`` from an existing ``Chunk*``.
+ OwnedChunk(Chunk* inner) : inner_(inner) {}
+
+ /// A pointer to the owned ``Chunk``.
+ Chunk* inner_;
+
+ /// Allow ``Chunk`` and ``MultiBuf`` to create ``OwnedChunk``s using the
+ /// private constructor above.
+ friend class Chunk;
+ friend class MultiBuf;
+};
+
+} // namespace pw::multibuf
diff --git a/pw_multibuf/public/pw_multibuf/internal/test_utils.h b/pw_multibuf/public/pw_multibuf/internal/test_utils.h
new file mode 100644
index 000000000..07e325016
--- /dev/null
+++ b/pw_multibuf/public/pw_multibuf/internal/test_utils.h
@@ -0,0 +1,162 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <memory>
+
+#include "pw_allocator/allocator_metric_proxy.h"
+#include "pw_allocator/split_free_list_allocator.h"
+#include "pw_multibuf/chunk.h"
+
+namespace pw::multibuf::internal {
+
+/// A basic ``Allocator`` implementation that reports the number and size of
+/// allocations.
+class TrackingAllocator : public pw::allocator::Allocator {
+ public:
+ /// Constructs a new ``TrackingAllocator`` which allocates from the provided
+ /// region of memory.
+ TrackingAllocator(ByteSpan span) : alloc_stats_(kFakeToken) {
+ Status status = alloc_.Init(span, kFakeThreshold);
+ EXPECT_EQ(status, OkStatus());
+ alloc_stats_.Initialize(alloc_);
+ }
+
+ /// Returns the number of current allocations.
+ size_t count() const { return alloc_stats_.count(); }
+
+ /// Returns the combined size in bytes of all current allocations.
+ size_t used() const { return alloc_stats_.used(); }
+
+ protected:
+ void* DoAllocate(allocator::Layout layout) override {
+ return alloc_stats_.Allocate(layout);
+ }
+ bool DoResize(void* ptr,
+ allocator::Layout old_layout,
+ size_t new_size) override {
+ return alloc_stats_.Resize(ptr, old_layout, new_size);
+ }
+ void DoDeallocate(void* ptr, allocator::Layout layout) override {
+ alloc_stats_.Deallocate(ptr, layout);
+ }
+
+ private:
+ const size_t kFakeThreshold = 0;
+ const int32_t kFakeToken = 0;
+
+ pw::allocator::SplitFreeListAllocator<> alloc_;
+ pw::allocator::AllocatorMetricProxy alloc_stats_;
+};
+
+/// A ``TrackingAllocator`` which holds an internal buffer of size `num_buffer`
+/// for its allocations.
+template <auto num_bytes>
+class TrackingAllocatorWithMemory : public pw::allocator::Allocator {
+ public:
+ TrackingAllocatorWithMemory() : mem_(), alloc_(mem_) {}
+ size_t count() const { return alloc_.count(); }
+ size_t used() const { return alloc_.used(); }
+ void* DoAllocate(allocator::Layout layout) override {
+ return alloc_.Allocate(layout);
+ }
+ bool DoResize(void* ptr,
+ allocator::Layout old_layout,
+ size_t new_size) override {
+ return alloc_.Resize(ptr, old_layout, new_size);
+ }
+ void DoDeallocate(void* ptr, allocator::Layout layout) override {
+ alloc_.Deallocate(ptr, layout);
+ }
+
+ private:
+ std::array<std::byte, num_bytes> mem_;
+ TrackingAllocator alloc_;
+};
+
+/// A ``ChunkRegionTracker`` which stores its ``Chunk`` and region metadata
+/// in a ``pw::allocator::Allocator`` allocation alongside the data.
+class HeaderChunkRegionTracker final : public ChunkRegionTracker {
+ public:
+ /// Allocates a new ``Chunk`` region of ``size`` bytes in ``alloc``.
+ ///
+ /// The underlyiing allocation will also store the
+ /// ``HeaderChunkRegionTracker`` itself.
+ ///
+ /// Returns the newly-created ``OwnedChunk`` if successful.
+ static std::optional<OwnedChunk> AllocateRegionAsChunk(
+ pw::allocator::Allocator* alloc, size_t size) {
+ HeaderChunkRegionTracker* tracker = AllocateRegion(alloc, size);
+ if (tracker == nullptr) {
+ return std::nullopt;
+ }
+ std::optional<OwnedChunk> chunk = Chunk::CreateFirstForRegion(*tracker);
+ if (!chunk.has_value()) {
+ tracker->Destroy();
+ return std::nullopt;
+ }
+ return chunk;
+ }
+
+ /// Allocates a new ``Chunk`` region of ``size`` bytes in ``alloc``.
+ ///
+ /// The underlyiing allocation will also store the
+ /// ``HeaderChunkRegionTracker`` itself.
+ ///
+ /// Returns a pointer to the newly-created ``HeaderChunkRegionTracker``
+ /// or ``nullptr`` if the allocation failed.
+ static HeaderChunkRegionTracker* AllocateRegion(
+ pw::allocator::Allocator* alloc, size_t size) {
+ auto layout =
+ allocator::Layout::Of<HeaderChunkRegionTracker>().Extend(size);
+ void* ptr = alloc->Allocate(layout);
+ if (ptr == nullptr) {
+ return nullptr;
+ }
+ std::byte* data =
+ reinterpret_cast<std::byte*>(ptr) + sizeof(HeaderChunkRegionTracker);
+ return new (ptr) HeaderChunkRegionTracker(ByteSpan(data, size), alloc);
+ }
+
+ ByteSpan Region() const final { return region_; }
+ ~HeaderChunkRegionTracker() final {}
+
+ protected:
+ void Destroy() final {
+ std::byte* ptr = reinterpret_cast<std::byte*>(this);
+ auto layout = allocator::Layout::Of<HeaderChunkRegionTracker>().Extend(
+ region_.size());
+ auto alloc = alloc_;
+ std::destroy_at(this);
+ alloc->Deallocate(ptr, layout);
+ }
+ void* AllocateChunkClass() final {
+ return alloc_->Allocate(pw::allocator::Layout::Of<Chunk>());
+ }
+ void DeallocateChunkClass(void* ptr) final {
+ alloc_->Deallocate(ptr, pw::allocator::Layout::Of<Chunk>());
+ }
+
+ private:
+ ByteSpan region_;
+ pw::allocator::Allocator* alloc_;
+
+ // NOTE: `region` must directly follow this `FakeChunkRegionTracker`
+ // in memory allocated by allocated by `alloc`.
+ HeaderChunkRegionTracker(ByteSpan region, pw::allocator::Allocator* alloc)
+ : region_(region), alloc_(alloc) {}
+};
+
+} // namespace pw::multibuf::internal
diff --git a/pw_multibuf/public/pw_multibuf/multibuf.h b/pw_multibuf/public/pw_multibuf/multibuf.h
new file mode 100644
index 000000000..a48ce6fde
--- /dev/null
+++ b/pw_multibuf/public/pw_multibuf/multibuf.h
@@ -0,0 +1,373 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <tuple>
+
+#include "pw_multibuf/chunk.h"
+
+namespace pw::multibuf {
+
+/// A buffer optimized for zero-copy data transfer.
+///
+/// A ``MultiBuf`` consists of multiple ``Chunk`` s of data.
+class MultiBuf {
+ public:
+ class iterator;
+ class const_iterator;
+ class ChunkIterator;
+ class ConstChunkIterator;
+ class ChunkIterable;
+ class ConstChunkIterable;
+
+ constexpr MultiBuf() : first_(nullptr) {}
+ constexpr MultiBuf(MultiBuf&& other) noexcept : first_(other.first_) {
+ other.first_ = nullptr;
+ }
+ MultiBuf& operator=(MultiBuf&& other) noexcept {
+ Release();
+ first_ = other.first_;
+ other.first_ = nullptr;
+ return *this;
+ }
+
+ /// Decrements the reference count on the underlying chunks of data and
+ /// empties this ``MultiBuf`` so that ``size() == 0``.
+ ///
+ /// Does not modify the underlying data, but may cause it to be deallocated
+ ///
+ /// This method is equivalent to ``{ MultiBuf _unused = std::move(multibuf);
+ /// }``
+ ///
+ /// This method will acquire a mutex and is not IRQ safe.
+ void Release() noexcept;
+
+ /// This destructor will acquire a mutex and is not IRQ safe.
+ ~MultiBuf() { Release(); }
+
+ /// Returns the number of bytes in this container.
+ ///
+ /// This method's complexity is ``O(Chunks().size())``.
+ [[nodiscard]] size_t size() const;
+
+ /// Pushes ``Chunk`` onto the front of the ``MultiBuf``.
+ ///
+ /// This operation does not move any data and is ``O(1)``.
+ void PushFrontChunk(OwnedChunk chunk);
+
+ /// Inserts ``chunk`` into the specified position in the ``MultiBuf``.
+ ///
+ /// This operation does not move any data and is ``O(Chunks().size())``.
+ ///
+ /// Returns an iterator pointing to the newly-inserted ``Chunk``.
+ //
+ // Implementation note: ``Chunks().size()`` should be remain relatively
+ // small, but this could be made ``O(1)`` in the future by adding a ``prev``
+ // pointer to the ``ChunkIterator``.
+ ChunkIterator InsertChunk(ChunkIterator position, OwnedChunk chunk);
+
+ /// Removes a ``Chunk`` from the specified position.
+ ///
+ /// This operation does not move any data and is ``O(Chunks().size())``.
+ ///
+ /// Returns an iterator pointing to the ``Chunk`` after the removed
+ /// ``Chunk``, or ``Chunks().end()`` if this was the last ``Chunk`` in the
+ /// ``MultiBuf``.
+ //
+ // Implementation note: ``Chunks().size()`` should be remain relatively
+ // small, but this could be made ``O(1)`` in the future by adding a ``prev``
+ // pointer to the ``ChunkIterator``.
+ std::tuple<ChunkIterator, OwnedChunk> TakeChunk(ChunkIterator position);
+
+ /// Returns an iterator pointing to the first byte of this ``MultiBuf`.
+ iterator begin() { return iterator(first_, 0); }
+ /// Returns a const iterator pointing to the first byte of this ``MultiBuf`.
+ const_iterator begin() const { return const_iterator(first_, 0); }
+ /// Returns a const iterator pointing to the first byte of this ``MultiBuf`.
+ const_iterator cbegin() const { return const_iterator(first_, 0); }
+
+ /// Returns an iterator pointing to the end of this ``MultiBuf``.
+ iterator end() { return iterator::end(); }
+ /// Returns a const iterator pointing to the end of this ``MultiBuf``.
+ const_iterator end() const { return const_iterator::end(); }
+ /// Returns a const iterator pointing to the end of this ``MultiBuf``.
+ const_iterator cend() const { return const_iterator::end(); }
+
+ /// Returns an iterable container which yields the ``Chunk``s in this
+ /// ``MultiBuf``.
+ constexpr ChunkIterable Chunks() { return ChunkIterable(first_); }
+
+ /// Returns an iterable container which yields the ``const Chunk``s in
+ /// this ``MultiBuf``.
+ constexpr const ChunkIterable Chunks() const { return ChunkIterable(first_); }
+
+ /// Returns an iterator pointing to the first ``Chunk`` in this ``MultiBuf``.
+ constexpr ChunkIterator ChunkBegin() { return ChunkIterator(first_); }
+ /// Returns an iterator pointing to the end of the ``Chunk``s in this
+ /// ``MultiBuf``.
+ constexpr ChunkIterator ChunkEnd() { return ChunkIterator::end(); }
+ /// Returns a const iterator pointing to the first ``Chunk`` in this
+ /// ``MultiBuf``.
+ constexpr ConstChunkIterator ConstChunkBegin() {
+ return ConstChunkIterator(first_);
+ }
+ /// Returns a const iterator pointing to the end of the ``Chunk``s in this
+ /// ``MultiBuf``.
+ constexpr ConstChunkIterator ConstChunkEnd() {
+ return ConstChunkIterator::end();
+ }
+
+ ///////////////////////////////////////////////////////////////////
+ //--------------------- Iterator details ------------------------//
+ ///////////////////////////////////////////////////////////////////
+
+ using element_type = std::byte;
+ using value_type = std::byte;
+ using pointer = std::byte*;
+ using const_pointer = const std::byte*;
+ using reference = std::byte&;
+ using const_reference = const std::byte&;
+ using difference_type = std::ptrdiff_t;
+ using size_type = std::size_t;
+
+ /// An ``std::forward_iterator`` over the bytes of a ``MultiBuf``.
+ class iterator {
+ public:
+ using value_type = std::byte;
+ using difference_type = std::ptrdiff_t;
+ using reference = std::byte&;
+ using pointer = std::byte*;
+ using iterator_category = std::forward_iterator_tag;
+
+ constexpr iterator() = default;
+ iterator(Chunk* chunk, size_t byte_index)
+ : chunk_(chunk), byte_index_(byte_index) {
+ AdvanceToData();
+ }
+ static iterator end() { return iterator(nullptr, 0); }
+
+ reference operator*() const { return (*chunk_)[byte_index_]; }
+ pointer operator->() const { return &(*chunk_)[byte_index_]; }
+
+ iterator& operator++();
+ iterator operator++(int) {
+ iterator tmp = *this;
+ ++(*this);
+ return tmp;
+ }
+ constexpr bool operator==(const iterator& other) const {
+ return chunk_ == other.chunk_ && byte_index_ == other.byte_index_;
+ }
+ constexpr bool operator!=(const iterator& other) const {
+ return chunk_ != other.chunk_ || byte_index_ != other.byte_index_;
+ }
+
+ /// Returns the current ``Chunk`` pointed to by this `iterator`.
+ constexpr Chunk* chunk() const { return chunk_; }
+
+ /// Returns the index of the byte pointed to by this `iterator` within the
+ /// current ``Chunk``.
+ constexpr size_t byte_index() const { return byte_index_; }
+
+ private:
+ void AdvanceToData();
+
+ Chunk* chunk_ = nullptr;
+ size_t byte_index_ = 0;
+ };
+
+ /// A const ``std::forward_iterator`` over the bytes of a ``MultiBuf``.
+ class const_iterator {
+ public:
+ using value_type = std::byte;
+ using difference_type = std::ptrdiff_t;
+ using reference = const std::byte&;
+ using pointer = const std::byte*;
+ using iterator_category = std::forward_iterator_tag;
+
+ constexpr const_iterator() = default;
+ const_iterator(const Chunk* chunk, size_t byte_index)
+ : chunk_(chunk), byte_index_(byte_index) {
+ AdvanceToData();
+ }
+ static const_iterator end() { return const_iterator(nullptr, 0); }
+
+ reference operator*() const { return (*chunk_)[byte_index_]; }
+ pointer operator->() const { return &(*chunk_)[byte_index_]; }
+
+ const_iterator& operator++();
+ const_iterator operator++(int) {
+ const_iterator tmp = *this;
+ ++(*this);
+ return tmp;
+ }
+
+ constexpr bool operator==(const const_iterator& other) const {
+ return chunk_ == other.chunk_ && byte_index_ == other.byte_index_;
+ }
+
+ constexpr bool operator!=(const const_iterator& other) const {
+ return chunk_ != other.chunk_ || byte_index_ != other.byte_index_;
+ }
+
+ /// Returns the current ``Chunk`` pointed to by this `iterator`.
+ constexpr const Chunk* chunk() const { return chunk_; }
+
+ /// Returns the index of the byte pointed to by this `iterator` within the
+ /// current ``Chunk``.
+ constexpr size_t byte_index() const { return byte_index_; }
+
+ private:
+ void AdvanceToData();
+
+ const Chunk* chunk_ = nullptr;
+ size_t byte_index_ = 0;
+ };
+
+ /// An iterable containing the ``Chunk`` s of a ``MultiBuf``.
+ class ChunkIterable {
+ public:
+ using element_type = Chunk;
+ using value_type = Chunk;
+ using pointer = Chunk*;
+ using reference = Chunk&;
+ using const_pointer = const Chunk*;
+ using difference_type = std::ptrdiff_t;
+ using const_reference = const Chunk&;
+ using size_type = std::size_t;
+
+ constexpr ChunkIterator begin() { return ChunkIterator(first_); }
+ constexpr ConstChunkIterator begin() const { return cbegin(); }
+ constexpr ConstChunkIterator cbegin() const {
+ return ConstChunkIterator(first_);
+ }
+ constexpr ChunkIterator end() { return ChunkIterator::end(); }
+ constexpr ConstChunkIterator end() const { return cend(); }
+ constexpr ConstChunkIterator cend() const {
+ return ConstChunkIterator::end();
+ }
+
+ /// Returns the number of ``Chunk``s in this iterable.
+ size_t size() const;
+
+ private:
+ Chunk* first_ = nullptr;
+ constexpr ChunkIterable(Chunk* chunk) : first_(chunk) {}
+ friend class MultiBuf;
+ };
+
+ /// A ``std::forward_iterator`` over the ``Chunk``s of a ``MultiBuf``.
+ class ChunkIterator {
+ public:
+ using value_type = Chunk;
+ using difference_type = std::ptrdiff_t;
+ using reference = Chunk&;
+ using pointer = Chunk*;
+ using iterator_category = std::forward_iterator_tag;
+
+ constexpr ChunkIterator() = default;
+
+ constexpr reference operator*() const { return *chunk_; }
+ constexpr pointer operator->() const { return chunk_; }
+
+ constexpr ChunkIterator& operator++() {
+ chunk_ = chunk_->next_in_buf_;
+ return *this;
+ }
+
+ constexpr ChunkIterator operator++(int) {
+ ChunkIterator tmp = *this;
+ ++(*this);
+ return tmp;
+ }
+
+ constexpr bool operator==(const ChunkIterator& other) const {
+ return chunk_ == other.chunk_;
+ }
+
+ constexpr bool operator!=(const ChunkIterator& other) const {
+ return chunk_ != other.chunk_;
+ }
+
+ constexpr Chunk* chunk() const { return chunk_; }
+
+ private:
+ constexpr ChunkIterator(Chunk* chunk) : chunk_(chunk) {}
+ static constexpr ChunkIterator end() { return ChunkIterator(nullptr); }
+ Chunk* chunk_ = nullptr;
+ friend class MultiBuf;
+ friend class ChunkIterable;
+ };
+
+ /// A const ``std::forward_iterator`` over the ``Chunk``s of a ``MultiBuf``.
+ class ConstChunkIterator {
+ public:
+ using value_type = const Chunk;
+ using difference_type = std::ptrdiff_t;
+ using reference = const Chunk&;
+ using pointer = const Chunk*;
+ using iterator_category = std::forward_iterator_tag;
+
+ constexpr ConstChunkIterator() = default;
+
+ constexpr reference operator*() const { return *chunk_; }
+ constexpr pointer operator->() const { return chunk_; }
+
+ constexpr ConstChunkIterator& operator++() {
+ chunk_ = chunk_->next_in_buf_;
+ return *this;
+ }
+
+ constexpr ConstChunkIterator operator++(int) {
+ ConstChunkIterator tmp = *this;
+ ++(*this);
+ return tmp;
+ }
+
+ constexpr bool operator==(const ConstChunkIterator& other) const {
+ return chunk_ == other.chunk_;
+ }
+
+ constexpr bool operator!=(const ConstChunkIterator& other) const {
+ return chunk_ != other.chunk_;
+ }
+
+ constexpr const Chunk* chunk() const { return chunk_; }
+
+ private:
+ constexpr ConstChunkIterator(const Chunk* chunk) : chunk_(chunk) {}
+ static constexpr ConstChunkIterator end() {
+ return ConstChunkIterator(nullptr);
+ }
+ const Chunk* chunk_ = nullptr;
+ friend class MultiBuf;
+ friend class ChunkIterable;
+ };
+
+ private:
+ /// Returns the ``Chunk`` preceding ``chunk`` in this ``MultiBuf``.
+ ///
+ /// Requires that this ``MultiBuf`` is not empty, and that ``chunk``
+ /// is either in ``MultiBuf`` or is ``nullptr``, in which case the last
+ /// ``Chunk`` in ``MultiBuf`` will be returned.
+ ///
+ /// This operation is ``O(Chunks().size())``.
+ Chunk* Previous(Chunk* chunk) const;
+
+ Chunk* first_;
+};
+
+class MultiBufAllocator {};
+
+} // namespace pw::multibuf
diff --git a/pw_multisink/BUILD.gn b/pw_multisink/BUILD.gn
index 842dfac0f..960ecdacb 100644
--- a/pw_multisink/BUILD.gn
+++ b/pw_multisink/BUILD.gn
@@ -87,6 +87,9 @@ pw_source_set("test_thread") {
# that depends on this pw_source_set and a pw_source_set that provides the
# implementaiton of test_thread. See :stl_multisink_test as an example.
pw_source_set("multisink_threaded_test") {
+ # TODO: b/303282642 - Remove this testonly
+ testonly = pw_unit_test_TESTONLY
+
sources = [ "multisink_threaded_test.cc" ]
deps = [
":pw_multisink",
diff --git a/pw_multisink/Kconfig b/pw_multisink/Kconfig
new file mode 100644
index 000000000..c5d61405a
--- /dev/null
+++ b/pw_multisink/Kconfig
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+menu "pw_multisink"
+
+config PIGWEED_MULTISINK
+ bool "Multisink module used for forwarding multiple attached sinks"
+ help
+ See :ref:`module-pw_multisink` for module details.
+
+if PIGWEED_MULTISINK
+
+config PIGWEED_MULTISINK_LOCK_INTERRUPT_SAFE
+ bool "Use an interrupt safe lock"
+ default y
+ help
+ When reading and writing from the underlying ring-buffer. The multisink
+ module will use an interrupt safe lock instead of a mutex.
+
+config PIGWEED_MULTISINK_UTIL
+ bool "Link pw_multisink.util library"
+ help
+ Include the Pigweed provided utility functions for multisink operations.
+
+endif # PIGWEED_MULTISINK
+
+endmenu
diff --git a/pw_multisink/docs.rst b/pw_multisink/docs.rst
index c8cb8f31b..da95b4558 100644
--- a/pw_multisink/docs.rst
+++ b/pw_multisink/docs.rst
@@ -186,3 +186,12 @@ The `PeekEntry` and `PopEntry` return two different drop counts, one for the
number of entries a drain was skipped forward for providing a small buffer or
draining too slow, and the other for entries that failed to be added to the
MultiSink.
+
+Zephyr
+======
+To enable `pw_multisink` with Zephyr use the following Kconfigs:
+- `CONFIG_PIGWEED_MULTISINK` to link `pw_multisink` into your Zephyr build
+- `CONFIG_PIGWEED_MULTISINK_UTIL` to link `pw_multisink.util`
+
+To enable the Pigweed config value `PW_MULTISINK_CONFIG_LOCK_INTERRUPT_SAFE`, use
+`CONFIG_PIGWEED_MULTISINK_LOCK_INTERRUPT_SAFE`.
diff --git a/pw_multisink/public/pw_multisink/multisink.h b/pw_multisink/public/pw_multisink/multisink.h
index c65d03dd3..3224cffbb 100644
--- a/pw_multisink/public/pw_multisink/multisink.h
+++ b/pw_multisink/public/pw_multisink/multisink.h
@@ -135,13 +135,13 @@ class MultiSink {
Result<ConstByteSpan> PopEntry(ByteSpan buffer,
uint32_t& drain_drop_count_out,
uint32_t& ingress_drop_count_out)
- PW_LOCKS_EXCLUDED(multisink_->lock_);
+ PW_LOCKS_EXCLUDED(multisink_ -> lock_);
// Overload that combines drop counts.
// TODO(cachinchilla): remove when downstream projects migrated to new API.
[[deprecated("Use PopEntry with different drop count outputs")]] Result<
ConstByteSpan>
PopEntry(ByteSpan buffer, uint32_t& drop_count_out)
- PW_LOCKS_EXCLUDED(multisink_->lock_) {
+ PW_LOCKS_EXCLUDED(multisink_ -> lock_) {
uint32_t ingress_drop_count = 0;
Result<ConstByteSpan> result =
PopEntry(buffer, drop_count_out, ingress_drop_count);
@@ -173,7 +173,7 @@ class MultiSink {
// OK - the entry or entries were removed from the multisink succesfully.
// FAILED_PRECONDITION - The drain must be attached to a sink.
Status PopEntry(const PeekedEntry& entry)
- PW_LOCKS_EXCLUDED(multisink_->lock_);
+ PW_LOCKS_EXCLUDED(multisink_ -> lock_);
// Returns a copy of the next available entry if it exists and acquires the
// latest drop count if the drain was advanced, and the latest ingress drop
@@ -195,7 +195,7 @@ class MultiSink {
Result<PeekedEntry> PeekEntry(ByteSpan buffer,
uint32_t& drain_drop_count_out,
uint32_t& ingress_drop_count_out)
- PW_LOCKS_EXCLUDED(multisink_->lock_);
+ PW_LOCKS_EXCLUDED(multisink_ -> lock_);
// Drains are not copyable or movable.
Drain(const Drain&) = delete;
@@ -313,7 +313,7 @@ class MultiSink {
MultiSink(ByteSpan buffer)
: ring_buffer_(true), sequence_id_(0), total_ingress_drops_(0) {
ring_buffer_.SetBuffer(buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
AttachDrain(oldest_entry_drain_);
}
diff --git a/pw_multisink/public/pw_multisink/util.h b/pw_multisink/public/pw_multisink/util.h
index d9a335989..9b6a30a5e 100644
--- a/pw_multisink/public/pw_multisink/util.h
+++ b/pw_multisink/public/pw_multisink/util.h
@@ -27,7 +27,7 @@ namespace pw::multisink {
// pw.snapshot.Snapshot.
Status UnsafeDumpMultiSinkLogs(
MultiSink& sink,
- pw::log::LogEntries::StreamEncoder& encoder,
+ pw::log::pwpb::LogEntries::StreamEncoder& encoder,
size_t max_num_entries = std::numeric_limits<size_t>::max());
} // namespace pw::multisink
diff --git a/pw_multisink/util.cc b/pw_multisink/util.cc
index b3ad6616e..be0e95096 100644
--- a/pw_multisink/util.cc
+++ b/pw_multisink/util.cc
@@ -21,13 +21,15 @@
namespace pw::multisink {
-Status UnsafeDumpMultiSinkLogs(MultiSink& sink,
- pw::log::LogEntries::StreamEncoder& encoder,
- size_t max_num_entries) {
+Status UnsafeDumpMultiSinkLogs(
+ MultiSink& sink,
+ pw::log::pwpb::LogEntries::StreamEncoder& encoder,
+ size_t max_num_entries) {
auto callback = [&encoder](ConstByteSpan entry) {
encoder
.WriteBytes(
- static_cast<uint32_t>(pw::log::LogEntries::Fields::kEntries), entry)
+ static_cast<uint32_t>(pw::log::pwpb::LogEntries::Fields::kEntries),
+ entry)
.IgnoreError();
};
return sink.UnsafeForEachEntry(callback, max_num_entries);
diff --git a/pw_package/docs.rst b/pw_package/docs.rst
index 63a4e0eeb..8d2e57985 100644
--- a/pw_package/docs.rst
+++ b/pw_package/docs.rst
@@ -16,14 +16,6 @@ this module instead are listed below.
* The dependency needs to be "installed" into the system in some manner beyond
just extraction and thus isn't a good match for distribution with CIPD.
-Pigweed itself includes a number of packages that simply clone git repositories.
-In general, these should not be used by projects using Pigweed. Pigweed uses
-these packages to avoid using submodules so downstream projects don't have
-multiple copies of a given repository in their source tree. Projects using
-Pigweed should use submodules instead of packages because submodules are
-supported by much more mature tooling: git. To install these packages anyway,
-use ``--force`` on the command line or ``force=True`` in Python code.
-
-----
Usage
-----
@@ -48,6 +40,18 @@ has several subcommands.
By default ``pw package`` operates on the directory referenced by
``PW_PACKAGE_ROOT``.
+.. _module-pw_package-middleware-only-packages:
+
+Middleware-Only Packages
+~~~~~~~~~~~~~~~~~~~~~~~~
+Pigweed itself includes a number of packages that simply clone git repositories.
+In general, these should not be used by projects using Pigweed. Pigweed uses
+these packages to avoid using submodules so downstream projects don't have
+multiple copies of a given repository in their source tree. Projects using
+Pigweed should use submodules instead of packages because submodules are
+supported by much more mature tooling: git. To install these packages anyway,
+use ``--force`` on the command line or ``force=True`` in Python code.
+
-----------
Configuring
-----------
@@ -119,3 +123,23 @@ file is based off of ``pw_package/pigweed_packages.py``.
def main(argv=None) -> int:
return package_manager.run(**vars(package_manager.parse_args(argv)))
+
+Options
+~~~~~~~
+Options for code formatting can be specified in the ``pigweed.json`` file
+(see also :ref:`SEED-0101 <seed-0101>`). This is currently limited to one
+option.
+
+* ``allow_middleware_only_packages``: Allow middleware-only packages to be
+ installed. See :ref:`module-pw_package-middleware-only-packages` for more.
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_package": {
+ "allow_middleware_only_packages": true
+ }
+ }
+ }
+
diff --git a/pw_package/py/BUILD.gn b/pw_package/py/BUILD.gn
index c180a50ac..6d61a0074 100644
--- a/pw_package/py/BUILD.gn
+++ b/pw_package/py/BUILD.gn
@@ -20,27 +20,33 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_package/__init__.py",
"pw_package/git_repo.py",
"pw_package/package_manager.py",
"pw_package/packages/__init__.py",
+ "pw_package/packages/abseil_cpp.py",
"pw_package/packages/arduino_core.py",
"pw_package/packages/boringssl.py",
+ "pw_package/packages/chre.py",
"pw_package/packages/chromium_verifier.py",
"pw_package/packages/crlset.py",
"pw_package/packages/emboss.py",
"pw_package/packages/freertos.py",
+ "pw_package/packages/fuzztest.py",
"pw_package/packages/googletest.py",
"pw_package/packages/mbedtls.py",
+ "pw_package/packages/mcuxpresso.py",
"pw_package/packages/micro_ecc.py",
"pw_package/packages/nanopb.py",
"pw_package/packages/pico_sdk.py",
+ "pw_package/packages/picotool.py",
"pw_package/packages/protobuf.py",
+ "pw_package/packages/re2.py",
"pw_package/packages/smartfusion_mss.py",
"pw_package/packages/stm32cube.py",
+ "pw_package/packages/zephyr.py",
"pw_package/pigweed_packages.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
diff --git a/pw_package/py/pw_package/git_repo.py b/pw_package/py/pw_package/git_repo.py
index f18b45f51..b4397e91f 100644
--- a/pw_package/py/pw_package/git_repo.py
+++ b/pw_package/py/pw_package/git_repo.py
@@ -13,6 +13,7 @@
# the License.
"""Install and check status of Git repository-based packages."""
+import logging
import os
import pathlib
import shutil
@@ -22,12 +23,15 @@ import urllib.parse
import pw_package.package_manager
+_LOG: logging.Logger = logging.getLogger(__name__)
+
PathOrStr = Union[pathlib.Path, str]
def git_stdout(
*args: PathOrStr, show_stderr=False, repo: PathOrStr = '.'
) -> str:
+ _LOG.debug('executing %r in %r', args, repo)
return (
subprocess.run(
['git', '-C', repo, *args],
@@ -41,6 +45,7 @@ def git_stdout(
def git(*args: PathOrStr, repo: PathOrStr = '.') -> subprocess.CompletedProcess:
+ _LOG.debug('executing %r in %r', args, repo)
return subprocess.run(['git', '-C', repo, *args], check=True)
@@ -61,8 +66,10 @@ class GitRepo(pw_package.package_manager.Package):
self._allow_use_in_downstream = False
def status(self, path: pathlib.Path) -> bool:
+ _LOG.debug('%s: status', self.name)
# TODO(tonymd): Check the correct SHA is checked out here.
if not os.path.isdir(path / '.git'):
+ _LOG.debug('%s: no .git folder', self.name)
return False
remote = git_stdout('remote', 'get-url', 'origin', repo=path)
@@ -75,31 +82,56 @@ class GitRepo(pw_package.package_manager.Package):
if not host.endswith('.googlesource.com'):
host += '.googlesource.com'
remote = 'https://{}{}'.format(host, url.path)
+ if remote != self._url:
+ _LOG.debug(
+ "%s: remote doesn't match expected %s actual %s",
+ self.name,
+ self._url,
+ remote,
+ )
+ return False
commit = git_stdout('rev-parse', 'HEAD', repo=path)
if self._commit and self._commit != commit:
+ _LOG.debug(
+ "%s: commits don't match expected %s actual %s",
+ self.name,
+ self._commit,
+ commit,
+ )
return False
if self._tag:
tag = git_stdout('describe', '--tags', repo=path)
if self._tag != tag:
+ _LOG.debug(
+ "%s: tags don't match expected %s actual %s",
+ self.name,
+ self._tag,
+ tag,
+ )
return False
# If it is a sparse checkout, sparse list shall match.
if self._sparse_list:
if not self.check_sparse_list(path):
+ _LOG.debug("%s: sparse lists don't match", self.name)
return False
status = git_stdout('status', '--porcelain=v1', repo=path)
- return remote == self._url and not status
+ _LOG.debug('%s: status %r', self.name, status)
+ return not status
def install(self, path: pathlib.Path) -> None:
+ _LOG.debug('%s: install', self.name)
# If already installed and at correct version exit now.
if self.status(path):
+ _LOG.debug('%s: already installed, exiting', self.name)
return
# Otherwise delete current version and clone again.
if os.path.isdir(path):
+ _LOG.debug('%s: removing', self.name)
shutil.rmtree(path)
if self._sparse_list:
@@ -112,6 +144,7 @@ class GitRepo(pw_package.package_manager.Package):
# revision. If we later run commands that need history it will be
# retrieved on-demand. For small repositories the effect is negligible
# but for large repositories this should be a significant improvement.
+ _LOG.debug('%s: checkout_full', self.name)
if self._commit:
git('clone', '--filter=blob:none', self._url, path)
git('reset', '--hard', self._commit, repo=path)
@@ -119,6 +152,7 @@ class GitRepo(pw_package.package_manager.Package):
git('clone', '-b', self._tag, '--filter=blob:none', self._url, path)
def checkout_sparse(self, path: pathlib.Path) -> None:
+ _LOG.debug('%s: checkout_sparse', self.name)
# sparse checkout
git('init', path)
git('remote', 'add', 'origin', self._url, repo=path)
diff --git a/pw_package/py/pw_package/package_manager.py b/pw_package/py/pw_package/package_manager.py
index 6221798ec..5b2232bed 100644
--- a/pw_package/py/pw_package/package_manager.py
+++ b/pw_package/py/pw_package/package_manager.py
@@ -21,6 +21,8 @@ import pathlib
import shutil
from typing import Dict, List, Optional, Sequence, Tuple
+from pw_env_setup import config_file
+
_LOG: logging.Logger = logging.getLogger(__name__)
@@ -94,10 +96,10 @@ class Packages:
available: Tuple[str, ...]
-class UpstreamOnlyPackageError(Exception):
+class MiddlewareOnlyPackageError(Exception):
def __init__(self, pkg_name):
super().__init__(
- f'Package {pkg_name} is an upstream-only package--it should be '
+ f'Package {pkg_name} is a middleware-only package--it should be '
'imported as a submodule and not a package'
)
@@ -109,6 +111,12 @@ class PackageManager:
self._pkg_root = root
os.makedirs(root, exist_ok=True)
+ config = config_file.load().get('pw', {}).get('pw_package', {})
+ self._allow_middleware_only_packages = config.get(
+ 'allow_middleware_only_packages',
+ False,
+ )
+
def install(self, package: str, force: bool = False) -> None:
"""Install the named package.
@@ -121,11 +129,11 @@ class PackageManager:
pkg = _PACKAGES[package]
if not pkg.allow_use_in_downstream:
- if os.environ.get('PW_ROOT') != os.environ.get('PW_PROJECT_ROOT'):
+ if not self._allow_middleware_only_packages:
if force:
- _LOG.warning(str(UpstreamOnlyPackageError(pkg.name)))
+ _LOG.warning(str(MiddlewareOnlyPackageError(pkg.name)))
else:
- raise UpstreamOnlyPackageError(pkg.name)
+ raise MiddlewareOnlyPackageError(pkg.name)
if force:
self.remove(package)
diff --git a/pw_package/py/pw_package/packages/abseil_cpp.py b/pw_package/py/pw_package/packages/abseil_cpp.py
new file mode 100644
index 000000000..df880fded
--- /dev/null
+++ b/pw_package/py/pw_package/packages/abseil_cpp.py
@@ -0,0 +1,46 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of Abseil C++."""
+
+import pathlib
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class AbseilCPP(pw_package.git_repo.GitRepo):
+ """Install and check status of Abseil C++."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ name='abseil-cpp',
+ url=(
+ 'https://pigweed.googlesource.com/'
+ 'third_party/github/abseil/abseil-cpp'
+ ),
+ commit='65109ecdf01a829bdb5e428174b3abb181e75826',
+ **kwargs,
+ )
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (
+ f'{self.name} installed in: {path}',
+ "Enable by running 'gn args out' and adding this line:",
+ f' dir_pw_third_party_abseil_cpp = "{path}"',
+ )
+
+
+pw_package.package_manager.register(AbseilCPP)
diff --git a/pw_package/py/pw_package/packages/arduino_core.py b/pw_package/py/pw_package/packages/arduino_core.py
index ecbbfe4d3..f3fcdf7fb 100644
--- a/pw_package/py/pw_package/packages/arduino_core.py
+++ b/pw_package/py/pw_package/packages/arduino_core.py
@@ -41,53 +41,61 @@ class ArduinoCore(pw_package.package_manager.Package):
"""Check for arduino core availability in pigweed_internal cipd."""
package_path = path.parent.resolve()
core_name = self.name
- core_cache_path = package_path / ".cache" / core_name
+ core_cache_path = package_path / '.cache' / core_name
core_cache_path.mkdir(parents=True, exist_ok=True)
- cipd_package_subpath = "pigweed_internal/third_party/"
+ cipd_package_subpath = 'pigweed_internal/third_party/'
cipd_package_subpath += core_name
- cipd_package_subpath += "/${platform}"
+ cipd_package_subpath += '/${platform}'
# Check if teensy cipd package is readable
-
with tempfile.NamedTemporaryFile(
- prefix='cipd', delete=True
+ prefix='cipd', delete=True, dir=core_cache_path
) as temp_json:
+ temp_json_path = Path(temp_json.name)
cipd_acl_check_command = [
- "cipd",
- "acl-check",
+ 'cipd',
+ 'acl-check',
cipd_package_subpath,
- "-reader",
- "-json-output",
- temp_json.name,
+ '-reader',
+ '-json-output',
+ str(temp_json_path),
]
subprocess.run(cipd_acl_check_command, capture_output=True)
- # Return if no packages are readable.
- if not json.load(temp_json)['result']:
+
+ # Return if cipd_package_subpath does not exist or is not readable
+ # by the current user.
+ if not temp_json_path.is_file():
+ # Return and proceed with normal installation.
+ return
+ result_text = temp_json_path.read_text()
+ result_dict = json.loads(result_text)
+ if 'result' not in result_dict:
+ # Return and proceed with normal installation.
return
def _run_command(command):
- _LOG.debug("Running: `%s`", " ".join(command))
+ _LOG.debug('Running: `%s`', ' '.join(command))
result = subprocess.run(command, capture_output=True)
_LOG.debug(
- "Output:\n%s", result.stdout.decode() + result.stderr.decode()
+ 'Output:\n%s', result.stdout.decode() + result.stderr.decode()
)
- _run_command(["cipd", "init", "-force", core_cache_path.as_posix()])
+ _run_command(['cipd', 'init', '-force', core_cache_path.as_posix()])
_run_command(
[
- "cipd",
- "install",
+ 'cipd',
+ 'install',
cipd_package_subpath,
- "-root",
+ '-root',
core_cache_path.as_posix(),
- "-force",
+ '-force',
]
)
_LOG.debug(
- "Available Cache Files:\n%s",
- "\n".join([p.as_posix() for p in core_cache_path.glob("*")]),
+ 'Available Cache Files:\n%s',
+ '\n'.join([p.as_posix() for p in core_cache_path.glob('*')]),
)
def install(self, path: Path) -> None:
@@ -118,7 +126,7 @@ class ArduinoCore(pw_package.package_manager.Package):
for hardware_dir in [
path for path in (path / 'hardware').iterdir() if path.is_dir()
]:
- if path.name in ["arduino", "tools"]:
+ if path.name in ['arduino', 'tools']:
continue
for subdir in [
path for path in hardware_dir.iterdir() if path.is_dir()
@@ -132,7 +140,7 @@ class ArduinoCore(pw_package.package_manager.Package):
f' pw_arduino_build_PACKAGE_NAME = "{arduino_package_name}"',
' pw_arduino_build_BOARD = "BOARD_NAME"',
]
- message += ["\n".join(message_gn_args)]
+ message += ['\n'.join(message_gn_args)]
message += [
'Where BOARD_NAME is any supported board.',
# Have arduino_builder command appear on it's own line.
diff --git a/pw_package/py/pw_package/packages/boringssl.py b/pw_package/py/pw_package/packages/boringssl.py
index 1de38f8cf..7730814b4 100644
--- a/pw_package/py/pw_package/packages/boringssl.py
+++ b/pw_package/py/pw_package/packages/boringssl.py
@@ -38,7 +38,7 @@ class BoringSSL(pw_package.package_manager.Package):
'/third_party/boringssl/boringssl',
]
),
- commit='9f55d972854d0b34dae39c7cd3679d6ada3dfd5b',
+ commit='6d3db84c47643271cb553593ee67362be3820874',
)
self._allow_use_in_downstream = False
diff --git a/pw_package/py/pw_package/packages/chre.py b/pw_package/py/pw_package/packages/chre.py
new file mode 100644
index 000000000..111e0c06d
--- /dev/null
+++ b/pw_package/py/pw_package/packages/chre.py
@@ -0,0 +1,43 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of CHRE."""
+
+import pathlib
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class Chre(pw_package.git_repo.GitRepo):
+ """Install and check status of CHRE."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ name='chre',
+ url='https://android.googlesource.com/platform/system/chre',
+ commit='d768690052557f0d486eea2f9fb2b26a91f59372',
+ **kwargs,
+ )
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (
+ f'{self.name} installed in: {path}',
+ "Enable by running 'gn args out' and adding this line:",
+ f' dir_pw_third_party_chre = "{path}"',
+ )
+
+
+pw_package.package_manager.register(Chre)
diff --git a/pw_package/py/pw_package/packages/emboss.py b/pw_package/py/pw_package/packages/emboss.py
index 090858f6d..f46565e2e 100644
--- a/pw_package/py/pw_package/packages/emboss.py
+++ b/pw_package/py/pw_package/packages/emboss.py
@@ -27,13 +27,11 @@ class Emboss(pw_package.git_repo.GitRepo):
super().__init__(
*args,
name="emboss",
- url="".join(
- [
- "https://fuchsia.googlesource.com",
- "/third_party/github.com/google/emboss",
- ]
+ url=(
+ "https://pigweed.googlesource.com"
+ "/third_party/github/google/emboss"
),
- commit="b8e2750975aa241fb81f5723ecc4c50f71e6bd18",
+ commit="ac5e59b3be7ccac05921fff6ec274ff56b095ae6",
**kwargs,
)
diff --git a/pw_package/py/pw_package/packages/fuzztest.py b/pw_package/py/pw_package/packages/fuzztest.py
new file mode 100644
index 000000000..5fcf92012
--- /dev/null
+++ b/pw_package/py/pw_package/packages/fuzztest.py
@@ -0,0 +1,46 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of FuzzTest."""
+
+import pathlib
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class FuzzTest(pw_package.git_repo.GitRepo):
+ """Install and check status of FuzzTest."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ name='fuzztest',
+ url=(
+ 'https://pigweed.googlesource.com/'
+ 'third_party/github/google/fuzztest'
+ ),
+ commit='34e42dc7130ad73cb27c117ac063fe42ad92209d',
+ **kwargs,
+ )
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (
+ f'{self.name} installed in: {path}',
+ "Enable by running 'gn args out' and adding this line:",
+ f' dir_pw_third_party_fuzztest = "{path}"',
+ )
+
+
+pw_package.package_manager.register(FuzzTest)
diff --git a/pw_package/py/pw_package/packages/googletest.py b/pw_package/py/pw_package/packages/googletest.py
index 86da67cf2..685342484 100644
--- a/pw_package/py/pw_package/packages/googletest.py
+++ b/pw_package/py/pw_package/packages/googletest.py
@@ -27,16 +27,21 @@ class Googletest(pw_package.git_repo.GitRepo):
super().__init__(
*args,
name='googletest',
- url='https://github.com/google/googletest',
- commit='073293463e1733c5e931313da1c3f1de044e1db3',
+ url=(
+ 'https://pigweed.googlesource.com/'
+ 'third_party/github/google/googletest'
+ ),
+ commit="dea0484e4d3b6a2c50055c24c5617cd662a50c5f",
**kwargs,
)
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
f'{self.name} installed in: {path}',
- "Enable by running 'gn args out' and adding this line:",
+ "Enable by running 'gn args out' and adding these lines:",
f' dir_pw_third_party_googletest = "{path}"',
+ ' pw_unit_test_MAIN = "//third_party/googletest:gmock_main"',
+ ' pw_unit_test_GOOGLETEST_BACKEND = "//third_party/googletest"',
)
diff --git a/pw_package/py/pw_package/packages/mbedtls.py b/pw_package/py/pw_package/packages/mbedtls.py
index 96c49b3cf..cc905d2ba 100644
--- a/pw_package/py/pw_package/packages/mbedtls.py
+++ b/pw_package/py/pw_package/packages/mbedtls.py
@@ -33,7 +33,8 @@ class MbedTLS(pw_package.git_repo.GitRepo):
"/third_party/github/ARMmbed/mbedtls",
]
),
- commit='e483a77c85e1f9c1dd2eb1c5a8f552d2617fe400',
+ # mbedtls-3.2.1 released 2022-07-12
+ commit='869298bffeea13b205343361b7a7daf2b210e33d',
**kwargs,
)
diff --git a/pw_package/py/pw_package/packages/mcuxpresso.py b/pw_package/py/pw_package/packages/mcuxpresso.py
new file mode 100644
index 000000000..75683bb0f
--- /dev/null
+++ b/pw_package/py/pw_package/packages/mcuxpresso.py
@@ -0,0 +1,48 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of the NXP mcuxpresso SDK."""
+
+from pathlib import Path
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class McuxpressoSdk(pw_package.package_manager.Package):
+ """Install and check status of the NXP mcuxpresso SDK."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, name='mcuxpresso', **kwargs)
+ self._evkmimxrt595 = pw_package.git_repo.GitRepo(
+ name='evkmimxrt595',
+ url=(
+ 'https://pigweed-internal.googlesource.com/'
+ 'third_party/vendor/nxp/evkmimxrt595'
+ ),
+ commit='10176a38fa830208332de132b112addb189d6d24',
+ )
+
+ def install(self, path: Path) -> None:
+ self._evkmimxrt595.install(path)
+
+ def info(self, path: Path) -> Sequence[str]:
+ return (
+ f'{self.name} installed in: {path}',
+ "Enable by running 'gn args out' and adding this line:",
+ f' dir_pw_third_party_mcuxpresso = "{path}"',
+ )
+
+
+pw_package.package_manager.register(McuxpressoSdk)
diff --git a/pw_package/py/pw_package/packages/micro_ecc.py b/pw_package/py/pw_package/packages/micro_ecc.py
index a72ef433e..9d8e827cd 100644
--- a/pw_package/py/pw_package/packages/micro_ecc.py
+++ b/pw_package/py/pw_package/packages/micro_ecc.py
@@ -29,11 +29,12 @@ class MicroECC(pw_package.git_repo.GitRepo):
name='micro-ecc',
url="".join(
[
- "https://github.com",
- "/kmackay/micro-ecc.git",
+ "https://pigweed.googlesource.com",
+ "/third_party/github/kmackay/micro-ecc.git",
]
),
- commit='24c60e243580c7868f4334a1ba3123481fe1aa48',
+ # pigweed branch with Google-internal fixes.
+ commit='df3398e0c550f323afc00afd8faa3e08869f8874',
**kwargs,
)
diff --git a/pw_package/py/pw_package/packages/pico_sdk.py b/pw_package/py/pw_package/packages/pico_sdk.py
index 1014e0750..ef4713def 100644
--- a/pw_package/py/pw_package/packages/pico_sdk.py
+++ b/pw_package/py/pw_package/packages/pico_sdk.py
@@ -40,8 +40,11 @@ class PiPicoSdk(pw_package.package_manager.Package):
super().__init__(*args, name='pico_sdk', **kwargs)
self._pico_sdk = pw_package.git_repo.GitRepo(
name='pico_sdk',
- url='https://github.com/raspberrypi/pico-sdk',
- commit='2e6142b15b8a75c1227dd3edbe839193b2bf9041',
+ url=(
+ 'https://pigweed.googlesource.com/'
+ 'third_party/github/raspberrypi/pico-sdk'
+ ),
+ commit='6a7db34ff63345a7badec79ebea3aaef1712f374',
)
def install(self, path: Path) -> None:
diff --git a/pw_package/py/pw_package/packages/picotool.py b/pw_package/py/pw_package/packages/picotool.py
new file mode 100644
index 000000000..94acc55bb
--- /dev/null
+++ b/pw_package/py/pw_package/packages/picotool.py
@@ -0,0 +1,94 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of picotool."""
+
+from contextlib import contextmanager
+import logging
+import os
+import pathlib
+from pathlib import Path
+import shutil
+import subprocess
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+_LOG = logging.getLogger(__package__)
+
+
+@contextmanager
+def change_working_dir(directory: Path):
+ original_dir = Path.cwd()
+ try:
+ os.chdir(directory)
+ yield directory
+ finally:
+ os.chdir(original_dir)
+
+
+class Picotool(pw_package.package_manager.Package):
+ """Install and check status of picotool."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, name='picotool', **kwargs)
+
+ self._pico_tool_repo = pw_package.git_repo.GitRepo(
+ name='picotool',
+ url=(
+ 'https://pigweed.googlesource.com/third_party/'
+ 'github/raspberrypi/picotool.git'
+ ),
+ commit='f6fe6b7c321a2def8950d2a440335dfba19e2eab',
+ )
+
+ def install(self, path: Path) -> None:
+ self._pico_tool_repo.install(path)
+
+ env = os.environ.copy()
+ env['PICO_SDK_PATH'] = str(path.parent.absolute() / 'pico_sdk')
+ bootstrap_env_path = Path(env.get('_PW_ACTUAL_ENVIRONMENT_ROOT', ''))
+
+ commands = (
+ ('cmake', '-S', './', '-B', 'out/', '-G', 'Ninja'),
+ ('ninja', '-C', 'out'),
+ )
+
+ with change_working_dir(path) as _picotool_repo:
+ for command in commands:
+ _LOG.info('==> %s', ' '.join(command))
+ subprocess.run(
+ command,
+ env=env,
+ capture_output=True,
+ check=True,
+ )
+
+ picotool_bin = path / 'out' / 'picotool'
+ _LOG.info('Done! picotool binary located at:')
+ _LOG.info(picotool_bin)
+
+ if bootstrap_env_path.is_dir() and picotool_bin.is_file():
+ bin_path = (
+ bootstrap_env_path / 'cipd' / 'packages' / 'pigweed' / 'bin'
+ )
+ destination_path = bin_path / picotool_bin.name
+ _LOG.info('Copy %s -> %s', picotool_bin, destination_path)
+ shutil.copy(picotool_bin, destination_path)
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (f'{self.name} installed in: {path}',)
+
+
+pw_package.package_manager.register(Picotool)
diff --git a/pw_package/py/pw_package/packages/protobuf.py b/pw_package/py/pw_package/packages/protobuf.py
index 25ba48677..7f3ed2924 100644
--- a/pw_package/py/pw_package/packages/protobuf.py
+++ b/pw_package/py/pw_package/packages/protobuf.py
@@ -27,7 +27,12 @@ class Protobuf(pw_package.git_repo.GitRepo):
super().__init__(
*args,
name='protobuf',
- url="https://github.com/protocolbuffers/protobuf.git",
+ url="".join(
+ [
+ "https://pigweed.googlesource.com",
+ "/third_party/github/protocolbuffers/protobuf.git",
+ ]
+ ),
commit='15c40c6cdac2f816a56640d24a5c4c3ec0f84b00',
**kwargs,
)
diff --git a/pw_package/py/pw_package/packages/re2.py b/pw_package/py/pw_package/packages/re2.py
new file mode 100644
index 000000000..2c1122ec1
--- /dev/null
+++ b/pw_package/py/pw_package/packages/re2.py
@@ -0,0 +1,46 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of RE2."""
+
+import pathlib
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class Re2(pw_package.git_repo.GitRepo):
+ """Install and check status of RE2."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ name='re2',
+ url=(
+ 'https://pigweed.googlesource.com/'
+ 'third_party/github/google/re2'
+ ),
+ commit='11073deb73b3d01018308863c0bcdfd0d51d3e70',
+ **kwargs,
+ )
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (
+ f'{self.name} installed in: {path}',
+ "Enable by running 'gn args out' and adding this line:",
+ f' dir_pw_third_party_re2 = "{path}"',
+ )
+
+
+pw_package.package_manager.register(Re2)
diff --git a/pw_package/py/pw_package/packages/stm32cube.py b/pw_package/py/pw_package/packages/stm32cube.py
index a2e704e04..460029477 100644
--- a/pw_package/py/pw_package/packages/stm32cube.py
+++ b/pw_package/py/pw_package/packages/stm32cube.py
@@ -110,7 +110,10 @@ class Stm32Cube(pw_package.package_manager.Package):
def __init__(self, family, tags, *args, **kwargs):
super().__init__(*args, name=f'stm32cube_{family}', **kwargs)
- st_github_url = 'https://github.com/STMicroelectronics'
+ st_github_url = (
+ 'https://pigweed.googlesource.com/'
+ 'third_party/github/STMicroelectronics'
+ )
self._hal_driver = pw_package.git_repo.GitRepo(
name='hal_driver',
diff --git a/pw_package/py/pw_package/packages/zephyr.py b/pw_package/py/pw_package/packages/zephyr.py
new file mode 100644
index 000000000..89646d711
--- /dev/null
+++ b/pw_package/py/pw_package/packages/zephyr.py
@@ -0,0 +1,142 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Install and check status of Zephyr."""
+import importlib.resources
+import json
+import pathlib
+import os
+import subprocess
+import sys
+import tempfile
+
+from typing import Sequence
+
+import pw_env_setup.virtualenv_setup
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class Zephyr(pw_package.git_repo.GitRepo):
+ """Install and check status of Zephyr."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ name='zephyr',
+ url=(
+ 'https://pigweed.googlesource.com/third_party/'
+ 'github/zephyrproject-rtos/zephyr'
+ ),
+ commit='a6eef0ba3755f2530c5ce93524e5ac4f5be30194', # v3.5 release
+ **kwargs,
+ )
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (
+ f'{self.name} installed in: {path}',
+ 'Enable by running "gn args out" and adding this line:',
+ f' dir_pw_third_party_zephyr = "{path}"',
+ )
+
+ @staticmethod
+ def __populate_download_cache_from_cipd(path: pathlib.Path) -> None:
+ """Check for Zephyr SDK in cipd"""
+ package_path = path.parent.resolve()
+ core_cache_path = package_path / 'zephyr_sdk'
+ core_cache_path.mkdir(parents=True, exist_ok=True)
+
+ cipd_package_subpath = 'infra/3pp/tools/zephyr_sdk/${platform}'
+
+ # Check if the zephyr_sdk cipd package is readable
+ with tempfile.NamedTemporaryFile(
+ prefix='cipd', delete=True
+ ) as temp_json:
+ temp_json_path = pathlib.Path(temp_json.name)
+ cipd_acl_check_command = [
+ 'cipd',
+ 'acl-check',
+ cipd_package_subpath,
+ '-reader',
+ '-json-output',
+ str(temp_json_path),
+ ]
+ subprocess.run(cipd_acl_check_command, capture_output=True)
+
+ # Return if no packages are readable.
+ if not temp_json_path.is_file():
+ raise RuntimeError(
+ 'Failed to verify zephyr_sdk cipd package is readable.'
+ )
+ result_text = temp_json_path.read_text()
+ result_dict = json.loads(result_text)
+ if 'result' not in result_dict:
+ raise RuntimeError(
+ 'Failed to verify zephyr_sdk cipd package is readable.'
+ )
+
+ # Initialize cipd
+ subprocess.check_call(
+ [
+ 'cipd',
+ 'init',
+ '-force',
+ str(core_cache_path),
+ ]
+ )
+ # Install the Zephyr SDK
+ subprocess.check_call(
+ [
+ 'cipd',
+ 'install',
+ cipd_package_subpath,
+ '-root',
+ str(core_cache_path),
+ '-force',
+ ]
+ )
+ # Setup Zephyr SDK
+ setup_file = 'setup.cmd' if os.name == 'nt' else 'setup.sh'
+ subprocess.check_call(
+ [
+ str(core_cache_path / setup_file),
+ '-t',
+ 'all',
+ '-c',
+ '-h',
+ ]
+ )
+
+ def install(self, path: pathlib.Path) -> None:
+ super().install(path)
+
+ self.__populate_download_cache_from_cipd(path)
+ with importlib.resources.path(
+ pw_env_setup.virtualenv_setup, 'constraint.list'
+ ) as constraint:
+ subprocess.check_call(
+ [
+ sys.executable,
+ '-m',
+ 'pip',
+ 'install',
+ '-r',
+ f'{path}/scripts/requirements.txt',
+ '-c',
+ str(constraint),
+ ]
+ )
+
+
+pw_package.package_manager.register(Zephyr)
diff --git a/pw_package/py/pw_package/pigweed_packages.py b/pw_package/py/pw_package/pigweed_packages.py
index dec3bcbc9..50b48ebc6 100644
--- a/pw_package/py/pw_package/pigweed_packages.py
+++ b/pw_package/py/pw_package/pigweed_packages.py
@@ -18,20 +18,27 @@ import sys
from pw_package import package_manager
# pylint: disable=unused-import
+from pw_package.packages import abseil_cpp
from pw_package.packages import arduino_core
from pw_package.packages import boringssl
from pw_package.packages import chromium_verifier
from pw_package.packages import crlset
from pw_package.packages import emboss
from pw_package.packages import freertos
+from pw_package.packages import fuzztest
from pw_package.packages import googletest
from pw_package.packages import mbedtls
+from pw_package.packages import mcuxpresso
from pw_package.packages import micro_ecc
from pw_package.packages import nanopb
from pw_package.packages import pico_sdk
+from pw_package.packages import picotool
from pw_package.packages import protobuf
+from pw_package.packages import re2
from pw_package.packages import smartfusion_mss
from pw_package.packages import stm32cube
+from pw_package.packages import zephyr
+from pw_package.packages import chre
# pylint: enable=unused-import
diff --git a/pw_package/py/setup.py b/pw_package/py/setup.py
deleted file mode 100644
index aac918303..000000000
--- a/pw_package/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_package"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_perf_test/BUILD.bazel b/pw_perf_test/BUILD.bazel
index 84d3cea5d..75998ad69 100644
--- a/pw_perf_test/BUILD.bazel
+++ b/pw_perf_test/BUILD.bazel
@@ -29,43 +29,50 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "duration_unit",
- hdrs = [
- "public/pw_perf_test/internal/duration_unit.h",
+ name = "pw_perf_test",
+ srcs = [
+ "framework.cc",
+ "perf_test.cc",
+ "test_info.cc",
],
- includes = ["public"],
- visibility = ["//visibility:private"],
-)
-
-pw_cc_facade(
- name = "timer_interface_facade",
hdrs = [
- "public/pw_perf_test/internal/timer.h",
+ "public/pw_perf_test/internal/framework.h",
+ "public/pw_perf_test/internal/test_info.h",
+ "public/pw_perf_test/perf_test.h",
],
includes = ["public"],
- visibility = ["//visibility:private"],
deps = [
- ":duration_unit",
+ ":event_handler",
+ ":state",
+ ":timer",
+ "//pw_preprocessor",
],
)
pw_cc_library(
- name = "timer",
+ name = "state",
+ srcs = [
+ "state.cc",
+ ],
+ hdrs = [
+ "public/pw_perf_test/state.h",
+ ],
+ includes = ["public"],
deps = [
- ":timer_interface_facade",
- "@pigweed_config//:pw_perf_test_timer_backend",
+ ":event_handler",
+ ":timer",
+ "//pw_assert",
+ "//pw_log",
],
)
-pw_cc_library(
- name = "timer_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
- deps = select({
- "//conditions:default": [":chrono_timer"],
- }),
+pw_cc_test(
+ name = "state_test",
+ srcs = ["state_test.cc"],
+ deps = [":pw_perf_test"],
)
-# EventHandler Configuraitions
+# Event handlers
pw_cc_library(
name = "event_handler",
@@ -75,19 +82,6 @@ pw_cc_library(
)
pw_cc_library(
- name = "pw_perf_test",
- srcs = ["perf_test.cc"],
- hdrs = ["public/pw_perf_test/perf_test.h"],
- includes = ["public"],
- deps = [
- ":event_handler",
- ":timer",
- "//pw_assert",
- "//pw_log",
- ],
-)
-
-pw_cc_library(
name = "google_test_style_event_strings",
hdrs = ["public/pw_perf_test/googletest_style_event_handler.h"],
)
@@ -115,55 +109,63 @@ pw_cc_library(
],
)
-pw_cc_perf_test(
- name = "generic_test",
- srcs = ["performance_test_generic.cc"],
-)
-
-# Test Declarations
+# Timer facade
-pw_cc_test(
- name = "perf_test_test",
- srcs = ["perf_test_test.cc"],
- deps = [":pw_perf_test"],
+pw_cc_library(
+ name = "duration_unit",
+ hdrs = [
+ "public/pw_perf_test/internal/duration_unit.h",
+ ],
+ includes = ["public"],
+ visibility = ["//visibility:private"],
)
-pw_cc_test(
- name = "timer_test",
- srcs = ["timer_test.cc"],
+pw_cc_facade(
+ name = "timer_interface_facade",
+ hdrs = [
+ "public/pw_perf_test/internal/timer.h",
+ ],
+ includes = ["public"],
+ visibility = ["//visibility:private"],
deps = [
- ":timer",
- "//pw_chrono:system_clock",
- "//pw_thread:sleep",
+ ":duration_unit",
],
)
-pw_cc_test(
- name = "chrono_timer_test",
- srcs = ["chrono_test.cc"],
+pw_cc_library(
+ name = "timer",
+ hdrs = [
+ "public/pw_perf_test/internal/timer.h",
+ ],
+ includes = ["public"],
deps = [
- ":chrono_timer",
- "//pw_chrono:system_clock",
- "//pw_thread:sleep",
+ ":duration_unit",
+ "@pigweed//targets:pw_perf_test_timer_backend",
],
)
-pw_cc_test(
- name = "state_test",
- srcs = ["state_test.cc"],
- deps = [":pw_perf_test"],
+pw_cc_library(
+ name = "timer_multiplexer",
+ visibility = ["@pigweed//targets:__pkg__"],
+ deps = select({
+ "//conditions:default": [":chrono_timer"],
+ }),
)
-# Bazel does not yet support building docs.
-filegroup(
- name = "docs",
- srcs = ["docs.rst"],
+pw_cc_test(
+ name = "timer_test",
+ srcs = ["timer_test.cc"],
+ deps = [
+ ":timer",
+ "//pw_chrono:system_clock",
+ "//pw_thread:sleep",
+ ],
)
-# Chrono Implementation
+# Chrono timer facade implementation
pw_cc_library(
- name = "chrono_timer_headers",
+ name = "chrono_timer",
hdrs = [
"chrono_public_overrides/pw_perf_test_timer_backend/timer.h",
"public/pw_perf_test/internal/chrono_timer_interface.h",
@@ -172,25 +174,28 @@ pw_cc_library(
"chrono_public_overrides",
"public",
],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
":duration_unit",
+ ":timer_interface_facade",
"//pw_chrono:system_clock",
],
)
-pw_cc_library(
- name = "chrono_timer",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+pw_cc_test(
+ name = "chrono_timer_test",
+ srcs = ["chrono_test.cc"],
deps = [
- ":chrono_timer_headers",
- ":timer_interface_facade",
+ ":chrono_timer",
+ "//pw_chrono:system_clock",
+ "//pw_thread:sleep",
],
)
-# ARM Cortex Implementation
+# ARM Cortex timer facade implementation
pw_cc_library(
- name = "arm_cortex_timer_headers",
+ name = "arm_cortex_timer",
hdrs = [
"arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h",
"public/pw_perf_test/internal/cyccnt_timer_interface.h",
@@ -199,14 +204,22 @@ pw_cc_library(
"arm_cortex_cyccnt_public_overrides",
"public",
],
- deps = [":duration_unit"],
-)
-
-pw_cc_library(
- name = "arm_cortex_timer",
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":arm_cortex_timer_headers",
+ ":duration_unit",
":timer_interface_facade",
],
)
+
+# Module-level targets
+
+pw_cc_perf_test(
+ name = "example_perf_test",
+ srcs = ["examples/example_perf_test.cc"],
+)
+
+# Bazel does not yet support building docs.
+filegroup(
+ name = "docs",
+ srcs = ["docs.rst"],
+)
diff --git a/pw_perf_test/BUILD.gn b/pw_perf_test/BUILD.gn
index 78839dd2b..8772591c4 100644
--- a/pw_perf_test/BUILD.gn
+++ b/pw_perf_test/BUILD.gn
@@ -26,63 +26,51 @@ config("public_include_path") {
visibility = [ ":*" ]
}
-config("arm_config") {
- include_dirs = [ "arm_cortex_cyccnt_public_overrides" ]
- visibility = [ ":*" ]
-}
-
-config("chrono_config") {
- include_dirs = [ "chrono_public_overrides" ]
- visibility = [ ":*" ]
-}
-
-pw_test_group("tests") {
- tests = [
- ":perf_test_test",
- ":timer_facade_test",
- ":chrono_timer_test",
- ":state_test",
+pw_source_set("pw_perf_test") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_perf_test/internal/framework.h",
+ "public/pw_perf_test/internal/test_info.h",
+ "public/pw_perf_test/perf_test.h",
+ ]
+ public_deps = [
+ ":event_handler",
+ ":state",
+ ":timer_interface",
+ dir_pw_preprocessor,
+ ]
+ sources = [
+ "framework.cc",
+ "perf_test.cc",
+ "test_info.cc",
]
}
-group("perf_test_tests_test") {
- deps = [ ":generic_perf_test" ]
-}
-
-# Timing interface variables
-
-pw_source_set("duration_unit") {
- public = [ "public/pw_perf_test/internal/duration_unit.h" ]
+pw_source_set("state") {
public_configs = [ ":public_include_path" ]
- visibility = [ ":*" ]
+ public = [ "public/pw_perf_test/state.h" ]
+ public_deps = [
+ ":event_handler",
+ ":timer_interface",
+ dir_pw_assert,
+ ]
+ deps = [ dir_pw_log ]
+ sources = [ "state.cc" ]
}
-pw_facade("timer_interface") {
- backend = pw_perf_test_TIMER_INTERFACE_BACKEND
- public = [ "public/pw_perf_test/internal/timer.h" ]
- public_deps = [ ":duration_unit" ]
- visibility = [ ":*" ]
+pw_test("state_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ sources = [ "state_test.cc" ]
+ deps = [ ":state" ]
}
-# Event Handler Configurations
+# Event handlers
pw_source_set("event_handler") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_perf_test/event_handler.h" ]
}
-pw_source_set("pw_perf_test") {
- public_configs = [ ":public_include_path" ]
- public = [ "public/pw_perf_test/perf_test.h" ]
- public_deps = [
- ":event_handler",
- ":timer_interface",
- dir_pw_assert,
- ]
- deps = [ dir_pw_log ]
- sources = [ "perf_test.cc" ]
-}
-
pw_source_set("googletest_style_event_handler") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_perf_test/googletest_style_event_handler.h" ]
@@ -105,48 +93,34 @@ pw_source_set("log_perf_handler_main") {
sources = [ "log_perf_handler_main.cc" ]
}
-pw_perf_test("generic_perf_test") {
- enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
- sources = [ "performance_test_generic.cc" ]
-}
+# Timer facade
-# Declaring module tests
+pw_source_set("duration_unit") {
+ public = [ "public/pw_perf_test/internal/duration_unit.h" ]
+ public_configs = [ ":public_include_path" ]
+ visibility = [ ":*" ]
+}
-pw_test("perf_test_test") {
- enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
- sources = [ "perf_test_test.cc" ]
- deps = [ ":pw_perf_test" ]
+pw_facade("timer_interface") {
+ backend = pw_perf_test_TIMER_INTERFACE_BACKEND
+ public = [ "public/pw_perf_test/internal/timer.h" ]
+ public_deps = [ ":duration_unit" ]
+ visibility = [ ":*" ]
}
pw_test("timer_facade_test") {
enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
sources = [ "timer_test.cc" ]
- public_deps = [ ":timer_interface" ]
+ deps = [ ":timer_interface" ]
}
-pw_test("chrono_timer_test") {
- enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
- sources = [ "chrono_test.cc" ]
- public_deps = [
- ":chrono_timer",
- "$dir_pw_chrono:system_timer",
- "$dir_pw_thread:sleep",
- ]
-}
-
-pw_test("state_test") {
- enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
- sources = [ "state_test.cc" ]
- public_deps = [ ":pw_perf_test" ]
-}
-
-# Documentation declaration
+# Chrono timer facade implementation
-pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+config("chrono_config") {
+ include_dirs = [ "chrono_public_overrides" ]
+ visibility = [ ":*" ]
}
-# Chrono Implementation
pw_source_set("pw_perf_test_chrono") {
public_configs = [ ":chrono_config" ]
public = [ "chrono_public_overrides/pw_perf_test_timer_backend/timer.h" ]
@@ -166,7 +140,22 @@ pw_source_set("chrono_timer") {
visibility = [ ":*" ]
}
-# ARM Cortex Implementation
+pw_test("chrono_timer_test") {
+ enable_if = pw_chrono_SYSTEM_TIMER_BACKEND != ""
+ sources = [ "chrono_test.cc" ]
+ deps = [
+ ":chrono_timer",
+ "$dir_pw_chrono:system_timer",
+ "$dir_pw_thread:sleep",
+ ]
+}
+
+# ARM Cortex timer facade implementation
+
+config("arm_config") {
+ include_dirs = [ "arm_cortex_cyccnt_public_overrides" ]
+ visibility = [ ":*" ]
+}
pw_source_set("arm_cortex_timer") {
public_configs = [
@@ -185,3 +174,26 @@ pw_source_set("pw_perf_test_arm_cortex") {
]
public_deps = [ ":arm_cortex_timer" ]
}
+
+# Module-level targets
+
+pw_perf_test("example_perf_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ sources = [ "examples/example_perf_test.cc" ]
+}
+
+group("examples") {
+ deps = [ ":example_perf_test" ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":chrono_timer_test",
+ ":state_test",
+ ":timer_facade_test",
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_perf_test/CMakeLists.txt b/pw_perf_test/CMakeLists.txt
index a6d7749f4..3f5c5c782 100644
--- a/pw_perf_test/CMakeLists.txt
+++ b/pw_perf_test/CMakeLists.txt
@@ -16,44 +16,58 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_perf_test/backend.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
-pw_add_library(pw_perf_test.duration_unit INTERFACE
- HEADERS
- public/pw_perf_test/internal/duration_unit.h
+pw_add_library(pw_perf_test STATIC
PUBLIC_INCLUDES
public
-)
-
-pw_add_facade(pw_perf_test.timer INTERFACE
- BACKEND
- pw_perf_test.TIMER_INTERFACE_BACKEND
HEADERS
- public/pw_perf_test/internal/timer.h
- PUBLIC_INCLUDES
- public
+ public/pw_perf_test/internal/framework.h
+ public/pw_perf_test/internal/test_info.h
+ public/pw_perf_test/perf_test.h
PUBLIC_DEPS
- pw_perf_test.duration_unit
-)
-
-pw_add_library(pw_perf_test.event_handler INTERFACE
- HEADERS
- public/pw_perf_test/event_handler.h
- PUBLIC_INCLUDES
- public
+ pw_perf_test.event_handler
+ pw_perf_test.state
+ pw_perf_test.timer
+ SOURCES
+ framework.cc
+ perf_test.cc
+ test_info.cc
)
-pw_add_library(pw_perf_test STATIC
+pw_add_library(pw_perf_test.state STATIC
PUBLIC_INCLUDES
public
HEADERS
- public/pw_perf_test/perf_test.h
+ public/pw_perf_test/state.h
PUBLIC_DEPS
pw_perf_test.timer
pw_perf_test.event_handler
+ pw_assert
PRIVATE_DEPS
pw_log
- pw_assert
SOURCES
- perf_test.cc
+ state.cc
+)
+
+if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
+ pw_add_test(pw_perf_test.state_test
+ SOURCES
+ state_test.cc
+ PRIVATE_DEPS
+ pw_assert.assert
+ pw_perf_test
+ GROUPS
+ modules
+ pw_perf_test
+ )
+endif()
+
+# Event handlers
+
+pw_add_library(pw_perf_test.event_handler INTERFACE
+ HEADERS
+ public/pw_perf_test/event_handler.h
+ PUBLIC_INCLUDES
+ public
)
pw_add_library(pw_perf_test.googletest_style_event_handler INTERFACE
@@ -85,15 +99,23 @@ pw_add_library(pw_perf_test.log_perf_handler_main STATIC
log_perf_handler_main.cc
)
-pw_add_library(pw_perf_test.chrono_timer INTERFACE
+# Timer facade
+
+pw_add_library(pw_perf_test.duration_unit INTERFACE
HEADERS
- chrono_public_overrides/pw_perf_test_timer_backend/timer.h
- public/pw_perf_test/internal/chrono_timer_interface.h
+ public/pw_perf_test/internal/duration_unit.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_facade(pw_perf_test.timer INTERFACE
+ BACKEND
+ pw_perf_test.TIMER_INTERFACE_BACKEND
+ HEADERS
+ public/pw_perf_test/internal/timer.h
PUBLIC_INCLUDES
- chrono_public_overrides
public
PUBLIC_DEPS
- pw_chrono.system_clock
pw_perf_test.duration_unit
)
@@ -111,6 +133,20 @@ if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
)
endif()
+# Chrono timer facade implementation
+
+pw_add_library(pw_perf_test.chrono_timer INTERFACE
+ HEADERS
+ chrono_public_overrides/pw_perf_test_timer_backend/timer.h
+ public/pw_perf_test/internal/chrono_timer_interface.h
+ PUBLIC_INCLUDES
+ chrono_public_overrides
+ public
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_perf_test.duration_unit
+)
+
if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}"
STREQUAL "pw_chrono.SYSTEM_CLOCK_BACKEND.NO_BACKEND_SET")
pw_add_test(pw_perf_test.chrono_timer_test
@@ -125,27 +161,3 @@ if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}"
pw_perf_test
)
endif()
-
-if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
- pw_add_test(pw_perf_test.state_test
- SOURCES
- state_test.cc
- PRIVATE_DEPS
- pw_perf_test
- GROUPS
- modules
- pw_perf_test
- )
-endif()
-
-if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
- pw_add_test(pw_perf_test.perf_test_test
- SOURCES
- perf_test_test.cc
- PRIVATE_DEPS
- pw_perf_test
- GROUPS
- modules
- pw_perf_test
- )
-endif()
diff --git a/pw_perf_test/docs.rst b/pw_perf_test/docs.rst
index 63d348b6e..5d3fb6ec3 100644
--- a/pw_perf_test/docs.rst
+++ b/pw_perf_test/docs.rst
@@ -219,17 +219,19 @@ use the ``pw_perf_test`` template to register your code.
.. code-block::
- import("$dir_pw_perf_test/perf_test.gni")
+ import("$dir_pw_perf_test/perf_test.gni")
- pw_perf_test("foo_perf_test") {
- sources = [ "foo_perf_test.cc" ]
- }
+ pw_perf_test("foo_perf_test") {
+ sources = [ "foo_perf_test.cc" ]
+ }
.. note::
If you use ``pw_watch``, the template is configured to build automatically
with ``pw_watch``. However you will still need to add your test group to the
pw_perf_tests group in the top level BUILD.gn.
+.. _module-pw_perf_test-pw_perf_test:
+
pw_perf_test template
---------------------
``pw_perf_test`` defines a single perf test suite. It creates two sub-targets.
@@ -247,14 +249,14 @@ pw_perf_test template
**Example**
-.. code::
+.. code-block::
- import("$dir_pw_perf_test/perf_test.gni")
+ import("$dir_pw_perf_test/perf_test.gni")
- pw_perf_test("large_test") {
- sources = [ "large_test.cc" ]
- enable_if = device_has_1m_flash
- }
+ pw_perf_test("large_test") {
+ sources = [ "large_test.cc" ]
+ enable_if = device_has_1m_flash
+ }
Grouping
--------
@@ -263,24 +265,24 @@ For grouping tests, no special template is required. Simply create a basic GN
**Example**
-.. code::
+.. code-block::
- import("$dir_pw_perf_test/perf_test.gni")
+ import("$dir_pw_perf_test/perf_test.gni")
- pw_perf_test("foo_test") {
- sources = [ "foo.cc" ]
- }
+ pw_perf_test("foo_test") {
+ sources = [ "foo.cc" ]
+ }
- pw_perf_test("bar_test") {
- sources = [ "bar.cc" ]
- }
+ pw_perf_test("bar_test") {
+ sources = [ "bar.cc" ]
+ }
- group("my_perf_tests_collection") {
- deps = [
- ":foo_test",
- ":bar_test",
- ]
- }
+ group("my_perf_tests_collection") {
+ deps = [
+ ":foo_test",
+ ":bar_test",
+ ]
+ }
Running
-------
@@ -306,17 +308,17 @@ template from ``//pw_build:pigweed.bzl``.
**Example**
-.. code::
+.. code-block::
- load(
- "//pw_build:pigweed.bzl",
- "pw_cc_test",
- )
+ load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_test",
+ )
- pw_cc_perf_test(
- name = "foo_test",
- srcs = ["foo_perf_test.cc"],
- )
+ pw_cc_perf_test(
+ name = "foo_test",
+ srcs = ["foo_perf_test.cc"],
+ )
Running
-------
diff --git a/pw_perf_test/performance_test_generic.cc b/pw_perf_test/examples/example_perf_test.cc
index 5488a014d..b1318b062 100644
--- a/pw_perf_test/performance_test_generic.cc
+++ b/pw_perf_test/examples/example_perf_test.cc
@@ -20,9 +20,8 @@ namespace pw::perf_test {
namespace {
void SimpleTestingFunction(pw::perf_test::State& state) {
- [[maybe_unused]] int p = 0;
while (state.KeepRunning()) {
- ++p;
+ // Intentionally empty.
}
}
@@ -44,7 +43,6 @@ PW_PERF_TEST(LambdaFunction, [](pw::perf_test::State& state_) {
});
PW_PERF_TEST_SIMPLE(SimpleTest, TestSimple, 2, 4);
-PW_PERF_TEST_SIMPLE(
- SimpleLambda, [](int a, int b) { return a + b; }, 1, 3);
+PW_PERF_TEST_SIMPLE(SimpleLambda, [](int a, int b) { return a + b; }, 1, 3);
} // namespace
} // namespace pw::perf_test
diff --git a/pw_perf_test/framework.cc b/pw_perf_test/framework.cc
new file mode 100644
index 000000000..ccfc9cc3f
--- /dev/null
+++ b/pw_perf_test/framework.cc
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_perf_test/internal/framework.h"
+
+#include "pw_perf_test/internal/test_info.h"
+#include "pw_perf_test/internal/timer.h"
+
+namespace pw::perf_test::internal {
+
+Framework Framework::framework_;
+
+int Framework::RunAllTests() {
+ if (!internal::TimerPrepare()) {
+ return false;
+ }
+
+ event_handler_->RunAllTestsStart(run_info_);
+
+ for (const TestInfo* test = tests_; test != nullptr; test = test->next()) {
+ State test_state = internal::CreateState(
+ kDefaultIterations, *event_handler_, test->test_name());
+ test->Run(test_state);
+ }
+ internal::TimerCleanup();
+ event_handler_->RunAllTestsEnd();
+ return true;
+}
+
+void Framework::RegisterTest(TestInfo& new_test) {
+ ++run_info_.total_tests;
+ if (tests_ == nullptr) {
+ tests_ = &new_test;
+ return;
+ }
+ TestInfo* info = tests_;
+ for (; info->next() != nullptr; info = info->next()) {
+ }
+ info->SetNext(&new_test);
+}
+
+} // namespace pw::perf_test::internal
diff --git a/pw_perf_test/perf_test.cc b/pw_perf_test/perf_test.cc
index 0e2f8d1ee..3cbe0e2f1 100644
--- a/pw_perf_test/perf_test.cc
+++ b/pw_perf_test/perf_test.cc
@@ -16,89 +16,11 @@
#include "pw_perf_test/perf_test.h"
-#include <cstdint>
-
-#include "pw_log/log.h"
#include "pw_perf_test/event_handler.h"
+#include "pw_perf_test/internal/framework.h"
#include "pw_perf_test/internal/timer.h"
namespace pw::perf_test {
-namespace internal {
-
-Framework Framework::framework_;
-
-int Framework::RunAllTests() {
- if (!internal::TimerPrepare()) {
- return false;
- }
-
- event_handler_->RunAllTestsStart(run_info_);
-
- for (const TestInfo* test = tests_; test != nullptr; test = test->next()) {
- State test_state =
- CreateState(kDefaultIterations, *event_handler_, test->test_name());
- test->Run(test_state);
- }
- internal::TimerCleanup();
- event_handler_->RunAllTestsEnd();
- return true;
-}
-
-void Framework::RegisterTest(TestInfo& new_test) {
- ++run_info_.total_tests;
- if (tests_ == nullptr) {
- tests_ = &new_test;
- return;
- }
- TestInfo* info = tests_;
- for (; info->next() != nullptr; info = info->next()) {
- }
- info->SetNext(&new_test);
-}
-
-State CreateState(int durations,
- EventHandler& event_handler,
- const char* test_name) {
- return State(durations, event_handler, test_name);
-}
-} // namespace internal
-
-bool State::KeepRunning() {
- internal::Timestamp iteration_end = internal::GetCurrentTimestamp();
- if (current_iteration_ == -1) {
- ++current_iteration_;
- event_handler_->TestCaseStart(test_info);
- iteration_start_ = internal::GetCurrentTimestamp();
- return true;
- }
- int64_t duration = internal::GetDuration(iteration_start_, iteration_end);
- if (duration > max_) {
- max_ = duration;
- }
- if (duration < min_) {
- min_ = duration;
- }
- total_duration_ += duration;
- ++current_iteration_;
- PW_LOG_DEBUG("Iteration number: %d - Duration: %ld",
- current_iteration_,
- static_cast<long>(duration));
- event_handler_->TestCaseIteration({current_iteration_, duration});
- if (current_iteration_ == test_iterations_) {
- PW_LOG_DEBUG("Total Duration: %ld Total Iterations: %d",
- static_cast<long>(total_duration_),
- test_iterations_);
- mean_ = total_duration_ / test_iterations_;
- PW_LOG_DEBUG("Mean: %ld: ", static_cast<long>(mean_));
- PW_LOG_DEBUG("Minimum: %ld", static_cast<long>(min_));
- PW_LOG_DEBUG("Maxmimum: %ld", static_cast<long>(max_));
- event_handler_->TestCaseEnd(test_info,
- Results{mean_, max_, min_, test_iterations_});
- return false;
- }
- iteration_start_ = internal::GetCurrentTimestamp();
- return true;
-}
void RunAllTests(EventHandler& handler) {
internal::Framework::Get().RegisterEventHandler(handler);
diff --git a/pw_perf_test/perf_test.gni b/pw_perf_test/perf_test.gni
index 9dd4e7201..c8158dea8 100644
--- a/pw_perf_test/perf_test.gni
+++ b/pw_perf_test/perf_test.gni
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_build/test_info.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
import("$dir_pw_unit_test/test.gni")
@@ -37,12 +38,17 @@ declare_args() {
# <target_name> is a standalone executable which contains only the test sources
# specified in the pw_perf_test_template.
#
+# Targets defined using this template will produce test metadata with a
+# `test_type` of "perf_test" and an additional `test_directory` value describing
+# the location of the test binary within the build output.
+#
# Args:
-# - enable_if: (optional) Conditionally enables or disables this test. The
-# test target does nothing when the test is disabled. The
-# disabled test can still be built and run with the
-# <target_name>.DISABLED. Defaults to true (enable_if).
-# - All of the regular "executable" target args are accepted.
+# - The following args have the same meaning as for `pw_test`:
+# enable_if
+# tags
+# extra_metadata
+#
+# - All of the regular `executable` target args are accepted.
#
template("pw_perf_test") {
_test_target_name = target_name
@@ -70,11 +76,26 @@ template("pw_perf_test") {
deps += [ dir_pw_perf_test ]
}
+ _test_metadata = "${target_name}.metadata"
+ _extra_metadata = {
+ forward_variables_from(invoker, [ "extra_metadata" ])
+ test_directory = rebase_path(_test_output_dir, root_build_dir)
+ }
+ pw_test_info(_test_metadata) {
+ test_type = "perf_test"
+ test_name = _test_target_name
+ forward_variables_from(invoker, [ "tags" ])
+ extra_metadata = _extra_metadata
+ }
+
pw_internal_disableable_target(_test_target_name) {
target_type = pw_perf_test_EXECUTABLE_TARGET_TYPE
enable_if = _test_is_enabled
- deps = [ ":$_test_target_name.lib" ]
+ deps = [
+ ":$_test_metadata",
+ ":$_test_target_name.lib",
+ ]
if (_test_main != "") {
deps += [ _test_main ]
}
diff --git a/pw_perf_test/perf_test_test.cc b/pw_perf_test/perf_test_test.cc
deleted file mode 100644
index 68bb10e24..000000000
--- a/pw_perf_test/perf_test_test.cc
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2022 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-#include "pw_perf_test/perf_test.h"
-
-#include "gtest/gtest.h"
-
-void TestingFunction(pw::perf_test::State& state) {
- [[maybe_unused]] int p = 0;
- while (state.KeepRunning()) {
- ++p;
- }
-}
-
-// This function is intentionally left blank
-void SimpleFunction() {}
-
-void SimpleFunctionWithArgs(int, bool) {}
-
-namespace pw::perf_test {
-namespace {
-
-PW_PERF_TEST(TestingComponentRegistration, TestingFunction);
-
-PW_PERF_TEST_SIMPLE(TestingSimpleRegistration, SimpleFunction);
-
-PW_PERF_TEST_SIMPLE(TestingSimpleRegistrationArgs,
- SimpleFunctionWithArgs,
- 123,
- false);
-
-} // namespace
-} // namespace pw::perf_test
diff --git a/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h b/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h
index df547dbbb..bda0f488c 100644
--- a/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h
+++ b/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h
@@ -49,7 +49,7 @@ inline constexpr DurationUnit kDurationUnit = DurationUnit::kClockCycle;
}
// Disables the DWT clock
-inline void TimerCleanup() { kDwtCtrl &= ~0x00000001; }
+inline void TimerCleanup() { kDwtCtrl &= ~0x00000001u; }
inline Timestamp GetCurrentTimestamp() { return kDwtCcynt; }
diff --git a/pw_perf_test/public/pw_perf_test/internal/framework.h b/pw_perf_test/public/pw_perf_test/internal/framework.h
new file mode 100644
index 000000000..bb3ebab22
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/internal/framework.h
@@ -0,0 +1,57 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_perf_test/event_handler.h"
+
+namespace pw::perf_test::internal {
+
+// Forward declaration.
+class TestInfo;
+
+/// Singleton that manages and runs performance tests.
+///
+/// This class mimics pw::unit_test::Framework.
+class Framework {
+ public:
+ constexpr Framework()
+ : event_handler_(nullptr),
+ tests_(nullptr),
+ run_info_{.total_tests = 0, .default_iterations = kDefaultIterations} {}
+
+ static Framework& Get() { return framework_; }
+
+ void RegisterEventHandler(EventHandler& event_handler) {
+ event_handler_ = &event_handler;
+ }
+
+ void RegisterTest(TestInfo&);
+
+ int RunAllTests();
+
+ private:
+ static constexpr int kDefaultIterations = 10;
+
+ EventHandler* event_handler_;
+
+ // Pointer to the list of tests
+ TestInfo* tests_;
+
+ TestRunInfo run_info_;
+
+ // Singleton
+ static Framework framework_;
+};
+
+} // namespace pw::perf_test::internal
diff --git a/pw_perf_test/public/pw_perf_test/internal/test_info.h b/pw_perf_test/public/pw_perf_test/internal/test_info.h
new file mode 100644
index 000000000..fd776e9d4
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/internal/test_info.h
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_perf_test/state.h"
+
+namespace pw::perf_test::internal {
+
+/// Represents a single test case.
+///
+/// Each instance includes a pointer to a function which constructs and runs the
+/// test class. These are statically allocated instead of the test classes, as
+/// test classes can be very large.
+///
+/// This class mimics pw::unit_test::TestInfo.
+class TestInfo {
+ public:
+ TestInfo(const char* test_name, void (*function_body)(State&));
+
+ // Returns the next registered test
+ TestInfo* next() const { return next_; }
+
+ void SetNext(TestInfo* next) { next_ = next; }
+
+ void Run(State& state) const { run_(state); }
+
+ const char* test_name() const { return test_name_; }
+
+ private:
+ // Function pointer to the code that will be measured
+ void (*run_)(State&);
+
+ // Intrusively linked list, this acts as a pointer to the next test
+ TestInfo* next_ = nullptr;
+
+ const char* test_name_;
+};
+
+} // namespace pw::perf_test::internal
diff --git a/pw_perf_test/public/pw_perf_test/perf_test.h b/pw_perf_test/public/pw_perf_test/perf_test.h
index e092087f9..90efb0cb0 100644
--- a/pw_perf_test/public/pw_perf_test/perf_test.h
+++ b/pw_perf_test/public/pw_perf_test/perf_test.h
@@ -13,13 +13,9 @@
// the License.
#pragma once
-#include <cstdint>
-#include <limits>
-
-#include "pw_assert/assert.h"
#include "pw_perf_test/event_handler.h"
-#include "pw_perf_test/internal/duration_unit.h"
-#include "pw_perf_test/internal/timer.h"
+#include "pw_perf_test/internal/test_info.h"
+#include "pw_perf_test/state.h"
#include "pw_preprocessor/arguments.h"
#define PW_PERF_TEST(name, function, ...) \
@@ -42,137 +38,6 @@
namespace pw::perf_test {
-class State;
-
-namespace internal {
-
-class TestInfo;
-
-// Allows access to the private State object constructor
-State CreateState(int durations,
- EventHandler& event_handler,
- const char* test_name);
-
-class Framework {
- public:
- constexpr Framework()
- : event_handler_(nullptr),
- tests_(nullptr),
- run_info_{.total_tests = 0, .default_iterations = kDefaultIterations} {}
-
- static Framework& Get() { return framework_; }
-
- void RegisterEventHandler(EventHandler& event_handler) {
- event_handler_ = &event_handler;
- }
-
- void RegisterTest(TestInfo&);
-
- int RunAllTests();
-
- private:
- static constexpr int kDefaultIterations = 10;
-
- EventHandler* event_handler_;
-
- // Pointer to the list of tests
- TestInfo* tests_;
-
- TestRunInfo run_info_;
-
- static Framework framework_;
-};
-
-class TestInfo {
- public:
- TestInfo(const char* test_name, void (*function_body)(State&))
- : run_(function_body), test_name_(test_name) {
- // Once a TestInfo object is created by the macro, this adds itself to the
- // list of registered tests
- Framework::Get().RegisterTest(*this);
- }
-
- // Returns the next registered test
- TestInfo* next() const { return next_; }
-
- void SetNext(TestInfo* next) { next_ = next; }
-
- void Run(State& state) const { run_(state); }
-
- const char* test_name() const { return test_name_; }
-
- private:
- // Function pointer to the code that will be measured
- void (*run_)(State&);
-
- // Intrusively linked list, this acts as a pointer to the next test
- TestInfo* next_ = nullptr;
-
- const char* test_name_;
-};
-
-} // namespace internal
-
-class State {
- public:
- // KeepRunning() should be called in a while loop. Responsible for managing
- // iterations and timestamps.
- bool KeepRunning();
-
- private:
- // Allows the framework to create state objects and unit tests for the state
- // class
- friend State internal::CreateState(int durations,
- EventHandler& event_handler,
- const char* test_name);
-
- // Privated constructor to prevent unauthorized instances of the state class.
- constexpr State(int iterations,
- EventHandler& event_handler,
- const char* test_name)
- : mean_(-1),
- test_iterations_(iterations),
- total_duration_(0),
- min_(std::numeric_limits<int64_t>::max()),
- max_(std::numeric_limits<int64_t>::min()),
- iteration_start_(),
- current_iteration_(-1),
- event_handler_(&event_handler),
- test_info{.name = test_name} {
- PW_ASSERT(test_iterations_ > 0);
- }
- // Set public after deciding how exactly to set user-defined iterations
- void SetIterations(int iterations) {
- PW_ASSERT(current_iteration_ == -1);
- test_iterations_ = iterations;
- PW_ASSERT(test_iterations_ > 0);
- }
-
- int64_t mean_;
-
- // Stores the total number of iterations wanted
- int test_iterations_;
-
- // Stores the total duration of the tests.
- int64_t total_duration_;
-
- // Smallest value of the iterations
- int64_t min_;
-
- // Largest value of the iterations
- int64_t max_;
-
- // Time at the start of the iteration
- internal::Timestamp iteration_start_;
-
- // The current iteration
- int current_iteration_;
-
- EventHandler* event_handler_;
-
- TestCase test_info;
-};
-
-void RunAllTests(pw::perf_test::EventHandler& handler);
+void RunAllTests(EventHandler& handler);
} // namespace pw::perf_test
diff --git a/pw_perf_test/public/pw_perf_test/state.h b/pw_perf_test/public/pw_perf_test/state.h
new file mode 100644
index 000000000..cd0e04f0c
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/state.h
@@ -0,0 +1,87 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstdint>
+#include <limits>
+
+#include "pw_assert/assert.h"
+#include "pw_perf_test/event_handler.h"
+#include "pw_perf_test/internal/timer.h"
+
+namespace pw::perf_test {
+
+// Forward declaration.
+class State;
+
+namespace internal {
+
+// Allows access to the private State object constructor
+State CreateState(int durations,
+ EventHandler& event_handler,
+ const char* test_name);
+
+} // namespace internal
+
+/// Records the performance of a test case over many iterations.
+class State {
+ public:
+ // KeepRunning() should be called in a while loop. Responsible for managing
+ // iterations and timestamps.
+ bool KeepRunning();
+
+ private:
+ // Allows the framework to create state objects and unit tests for the state
+ // class
+ friend State internal::CreateState(int durations,
+ EventHandler& event_handler,
+ const char* test_name);
+
+ // Privated constructor to prevent unauthorized instances of the state class.
+ constexpr State(int iterations,
+ EventHandler& event_handler,
+ const char* test_name)
+ : test_iterations_(iterations),
+ iteration_start_(),
+ event_handler_(&event_handler),
+ test_info{.name = test_name} {
+ PW_ASSERT(test_iterations_ > 0);
+ }
+
+ int64_t mean_ = -1;
+
+ // Stores the total number of iterations wanted
+ int test_iterations_;
+
+ // Stores the total duration of the tests.
+ int64_t total_duration_ = 0;
+
+ // Smallest value of the iterations
+ int64_t min_ = std::numeric_limits<int64_t>::max();
+
+ // Largest value of the iterations
+ int64_t max_ = std::numeric_limits<int64_t>::min();
+
+ // Time at the start of the iteration
+ internal::Timestamp iteration_start_;
+
+ // The current iteration.
+ int current_iteration_ = -1;
+
+ EventHandler* event_handler_;
+
+ TestCase test_info;
+};
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/state.cc b/pw_perf_test/state.cc
new file mode 100644
index 000000000..7fde71adc
--- /dev/null
+++ b/pw_perf_test/state.cc
@@ -0,0 +1,66 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_perf_test/state.h"
+
+#include "pw_log/log.h"
+
+namespace pw::perf_test {
+namespace internal {
+
+State CreateState(int durations,
+ EventHandler& event_handler,
+ const char* test_name) {
+ return State(durations, event_handler, test_name);
+}
+} // namespace internal
+
+bool State::KeepRunning() {
+ internal::Timestamp iteration_end = internal::GetCurrentTimestamp();
+ if (current_iteration_ == -1) {
+ ++current_iteration_;
+ event_handler_->TestCaseStart(test_info);
+ iteration_start_ = internal::GetCurrentTimestamp();
+ return true;
+ }
+ int64_t duration = internal::GetDuration(iteration_start_, iteration_end);
+ if (duration > max_) {
+ max_ = duration;
+ }
+ if (duration < min_) {
+ min_ = duration;
+ }
+ total_duration_ += duration;
+ ++current_iteration_;
+ PW_LOG_DEBUG("Iteration number: %d - Duration: %ld",
+ current_iteration_,
+ static_cast<long>(duration));
+ event_handler_->TestCaseIteration({current_iteration_, duration});
+ if (current_iteration_ == test_iterations_) {
+ PW_LOG_DEBUG("Total Duration: %ld Total Iterations: %d",
+ static_cast<long>(total_duration_),
+ test_iterations_);
+ mean_ = total_duration_ / test_iterations_;
+ PW_LOG_DEBUG("Mean: %ld: ", static_cast<long>(mean_));
+ PW_LOG_DEBUG("Minimum: %ld", static_cast<long>(min_));
+ PW_LOG_DEBUG("Maxmimum: %ld", static_cast<long>(max_));
+ event_handler_->TestCaseEnd(test_info,
+ Results{mean_, max_, min_, test_iterations_});
+ return false;
+ }
+ iteration_start_ = internal::GetCurrentTimestamp();
+ return true;
+}
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/state_test.cc b/pw_perf_test/state_test.cc
index 647e47a3b..bd3e87330 100644
--- a/pw_perf_test/state_test.cc
+++ b/pw_perf_test/state_test.cc
@@ -12,9 +12,10 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include "pw_perf_test/state.h"
+
#include "gtest/gtest.h"
#include "pw_perf_test/event_handler.h"
-#include "pw_perf_test/perf_test.h"
namespace pw::perf_test {
namespace {
diff --git a/pw_perf_test/test_info.cc b/pw_perf_test/test_info.cc
new file mode 100644
index 000000000..91a390ef0
--- /dev/null
+++ b/pw_perf_test/test_info.cc
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_perf_test/internal/test_info.h"
+
+#include "pw_perf_test/internal/framework.h"
+
+namespace pw::perf_test::internal {
+
+TestInfo::TestInfo(const char* test_name, void (*function_body)(State&))
+ : run_(function_body), test_name_(test_name) {
+ // Once a TestInfo object is created by the macro, this adds itself to the
+ // list of registered tests
+ Framework::Get().RegisterTest(*this);
+}
+
+} // namespace pw::perf_test::internal
diff --git a/pw_perf_test/timer_test.cc b/pw_perf_test/timer_test.cc
index ba65ffc01..1bb721e12 100644
--- a/pw_perf_test/timer_test.cc
+++ b/pw_perf_test/timer_test.cc
@@ -20,12 +20,16 @@ namespace pw::perf_test::internal {
namespace {
TEST(TimerTest, DurationIsPositive) {
+ ASSERT_TRUE(TimerPrepare());
+
Timestamp start = GetCurrentTimestamp();
for (volatile int i = 0; i < 1000; i = i + 1) {
}
Timestamp end = GetCurrentTimestamp();
int64_t duration = GetDuration(start, end);
EXPECT_GT(duration, 0);
+
+ TimerCleanup();
}
} // namespace
diff --git a/pw_persistent_ram/BUILD.bazel b/pw_persistent_ram/BUILD.bazel
index e714450af..76d986feb 100644
--- a/pw_persistent_ram/BUILD.bazel
+++ b/pw_persistent_ram/BUILD.bazel
@@ -38,6 +38,18 @@ pw_cc_library(
],
)
+pw_cc_library(
+ name = "flat_file_system_entry",
+ hdrs = [
+ "public/pw_persistent_ram/flat_file_system_entry.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_file:flat_file_system",
+ "//pw_persistent_ram",
+ ],
+)
+
pw_cc_test(
name = "persistent_test",
srcs = [
@@ -65,3 +77,13 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "flat_file_system_entry_test",
+ srcs = [
+ "flat_file_system_entry_test.cc",
+ ],
+ deps = [
+ ":flat_file_system_entry",
+ ],
+)
diff --git a/pw_persistent_ram/BUILD.gn b/pw_persistent_ram/BUILD.gn
index 9930035a2..4ad32ba4e 100644
--- a/pw_persistent_ram/BUILD.gn
+++ b/pw_persistent_ram/BUILD.gn
@@ -41,10 +41,23 @@ pw_source_set("pw_persistent_ram") {
]
}
+pw_source_set("flat_file_system_entry") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_persistent_ram/flat_file_system_entry.h" ]
+ sources = []
+ public_deps = [
+ ":pw_persistent_ram",
+ "$dir_pw_file:flat_file_system",
+ dir_pw_status,
+ ]
+ deps = []
+}
+
pw_test_group("tests") {
tests = [
":persistent_test",
":persistent_buffer_test",
+ ":flat_file_system_entry_test",
]
}
@@ -64,6 +77,11 @@ pw_test("persistent_buffer_test") {
sources = [ "persistent_buffer_test.cc" ]
}
+pw_test("flat_file_system_entry_test") {
+ deps = [ ":flat_file_system_entry" ]
+ sources = [ "flat_file_system_entry_test.cc" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
report_deps = [ ":persistent_size" ]
diff --git a/pw_persistent_ram/CMakeLists.txt b/pw_persistent_ram/CMakeLists.txt
index 59d69944e..cc1e00856 100644
--- a/pw_persistent_ram/CMakeLists.txt
+++ b/pw_persistent_ram/CMakeLists.txt
@@ -31,6 +31,16 @@ pw_add_library(pw_persistent_ram STATIC
persistent_buffer.cc
)
+pw_add_library(pw_persistent_ram.flat_file_system_entry INTERFACE
+ HEADERS
+ public/pw_persistent_ram/flat_file_system_entry.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_file.flat_file_system
+ pw_persistent_ram
+)
+
pw_add_test(pw_persistent_ram.persistent_test
SOURCES
persistent_test.cc
@@ -52,3 +62,13 @@ pw_add_test(pw_persistent_ram.persistent_buffer_test
modules
pw_persistent_ram
)
+
+pw_add_test(pw_persistent_ram.flat_file_system_entry_test
+ SOURCES
+ flat_file_system_entry_test.cc
+ PRIVATE_DEPS
+ pw_persistent_ram.flat_file_system_entry
+ GROUPS
+ modules
+ pw_persistent_ram
+)
diff --git a/pw_persistent_ram/flat_file_system_entry_test.cc b/pw_persistent_ram/flat_file_system_entry_test.cc
new file mode 100644
index 000000000..5af6affa8
--- /dev/null
+++ b/pw_persistent_ram/flat_file_system_entry_test.cc
@@ -0,0 +1,127 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_persistent_ram/flat_file_system_entry.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::persistent_ram {
+namespace {
+
+class FlatFileSystemPersistentBufferEntryTest : public ::testing::Test {
+ protected:
+ static constexpr uint32_t kBufferSize = 16;
+ static constexpr size_t kMaxFileNameLength = 32;
+
+ FlatFileSystemPersistentBufferEntryTest() {}
+
+ // Emulate invalidation of persistent section(s).
+ void ZeroPersistentMemory() { memset(buffer_, 0, sizeof(buffer_)); }
+
+ PersistentBuffer<kBufferSize>& GetPersistentBuffer() {
+ return *(new (buffer_) PersistentBuffer<kBufferSize>());
+ }
+
+ // Allocate a chunk of aligned storage that can be independently controlled.
+ alignas(PersistentBuffer<kBufferSize>)
+ std::byte buffer_[sizeof(PersistentBuffer<kBufferSize>)];
+};
+
+TEST_F(FlatFileSystemPersistentBufferEntryTest, BasicProperties) {
+ constexpr std::string_view kExpectedFileName("file_1.bin");
+ constexpr file::FlatFileSystemService::Entry::Id kExpectedFileId = 7;
+ constexpr file::FlatFileSystemService::Entry::FilePermissions
+ kExpectedPermissions =
+ file::FlatFileSystemService::Entry::FilePermissions::READ;
+
+ ZeroPersistentMemory();
+ auto& persistent = GetPersistentBuffer();
+
+ // write some data to create the file
+ constexpr uint32_t kExpectedNumber = 0x6C2C6582;
+ auto writer = persistent.GetWriter();
+ ASSERT_EQ(OkStatus(), writer.Write(as_bytes(span(&kExpectedNumber, 1))));
+
+ FlatFileSystemPersistentBufferEntry persistent_file(
+ kExpectedFileName, kExpectedFileId, kExpectedPermissions, persistent);
+
+ std::array<char, kMaxFileNameLength> tmp_buffer = {};
+ static_assert(kExpectedFileName.size() <= tmp_buffer.size());
+ StatusWithSize sws = persistent_file.Name(tmp_buffer);
+ ASSERT_EQ(OkStatus(), sws.status());
+
+ EXPECT_EQ(
+ 0, std::memcmp(tmp_buffer.data(), kExpectedFileName.data(), sws.size()));
+ EXPECT_EQ(sizeof(kExpectedNumber), persistent_file.SizeBytes());
+ EXPECT_EQ(kExpectedPermissions, persistent_file.Permissions());
+ EXPECT_EQ(kExpectedFileId, persistent_file.FileId());
+}
+
+TEST_F(FlatFileSystemPersistentBufferEntryTest, Delete) {
+ constexpr std::string_view kExpectedFileName("file_2.bin");
+ constexpr file::FlatFileSystemService::Entry::Id kExpectedFileId = 8;
+ constexpr file::FlatFileSystemService::Entry::FilePermissions
+ kExpectedPermissions =
+ file::FlatFileSystemService::Entry::FilePermissions::WRITE;
+
+ ZeroPersistentMemory();
+ auto& persistent = GetPersistentBuffer();
+
+ // write some data to create the file
+ constexpr uint32_t kExpectedNumber = 0x6C2C6582;
+ auto writer = persistent.GetWriter();
+ ASSERT_EQ(OkStatus(), writer.Write(as_bytes(span(&kExpectedNumber, 1))));
+
+ FlatFileSystemPersistentBufferEntry persistent_file(
+ kExpectedFileName, kExpectedFileId, kExpectedPermissions, persistent);
+
+ std::array<char, kMaxFileNameLength> tmp_buffer = {};
+ static_assert(kExpectedFileName.size() <= tmp_buffer.size());
+ StatusWithSize sws = persistent_file.Name(tmp_buffer);
+ ASSERT_EQ(OkStatus(), sws.status());
+
+ EXPECT_EQ(
+ 0, std::memcmp(tmp_buffer.data(), kExpectedFileName.data(), sws.size()));
+ EXPECT_EQ(sizeof(kExpectedNumber), persistent_file.SizeBytes());
+
+ ASSERT_EQ(OkStatus(), persistent_file.Delete());
+
+ sws = persistent_file.Name(tmp_buffer);
+ ASSERT_EQ(Status::NotFound(), sws.status());
+ EXPECT_EQ(0u, persistent_file.SizeBytes());
+}
+
+TEST_F(FlatFileSystemPersistentBufferEntryTest, NoData) {
+ constexpr std::string_view kExpectedFileName("file_2.bin");
+ constexpr file::FlatFileSystemService::Entry::Id kExpectedFileId = 9;
+ constexpr file::FlatFileSystemService::Entry::FilePermissions
+ kExpectedPermissions =
+ file::FlatFileSystemService::Entry::FilePermissions::READ_AND_WRITE;
+
+ ZeroPersistentMemory();
+ auto& persistent = GetPersistentBuffer();
+
+ FlatFileSystemPersistentBufferEntry persistent_file(
+ kExpectedFileName, kExpectedFileId, kExpectedPermissions, persistent);
+
+ std::array<char, kMaxFileNameLength> tmp_buffer = {};
+ static_assert(kExpectedFileName.size() <= tmp_buffer.size());
+
+ StatusWithSize sws = persistent_file.Name(tmp_buffer);
+ ASSERT_EQ(Status::NotFound(), sws.status());
+ EXPECT_EQ(0u, persistent_file.SizeBytes());
+}
+
+} // namespace
+} // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/public/pw_persistent_ram/flat_file_system_entry.h b/pw_persistent_ram/public/pw_persistent_ram/flat_file_system_entry.h
new file mode 100644
index 000000000..4d0e04019
--- /dev/null
+++ b/pw_persistent_ram/public/pw_persistent_ram/flat_file_system_entry.h
@@ -0,0 +1,73 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "pw_file/flat_file_system.h"
+#include "pw_persistent_ram/persistent_buffer.h"
+
+namespace pw::persistent_ram {
+
+template <size_t kMaxSizeBytes>
+class FlatFileSystemPersistentBufferEntry final
+ : public file::FlatFileSystemService::Entry {
+ public:
+ FlatFileSystemPersistentBufferEntry(
+ std::string_view file_name,
+ file::FlatFileSystemService::Entry::Id file_id,
+ file::FlatFileSystemService::Entry::FilePermissions permissions,
+ PersistentBuffer<kMaxSizeBytes>& persistent_buffer)
+ : file_name_(file_name),
+ file_id_(file_id),
+ permissions_(permissions),
+ persistent_buffer_(persistent_buffer) {}
+
+ StatusWithSize Name(span<char> dest) final {
+ if (file_name_.empty() || !persistent_buffer_.has_value()) {
+ return StatusWithSize(Status::NotFound(), 0);
+ }
+
+ size_t bytes_to_copy = std::min(dest.size_bytes(), file_name_.size());
+ std::memcpy(dest.data(), file_name_.data(), bytes_to_copy);
+ if (bytes_to_copy != file_name_.size()) {
+ return StatusWithSize(Status::ResourceExhausted(), bytes_to_copy);
+ }
+
+ return StatusWithSize(OkStatus(), bytes_to_copy);
+ }
+
+ size_t SizeBytes() final { return persistent_buffer_.size(); }
+
+ Status Delete() final {
+ persistent_buffer_.clear();
+ return pw::OkStatus();
+ }
+
+ file::FlatFileSystemService::Entry::FilePermissions Permissions()
+ const final {
+ return permissions_;
+ }
+
+ file::FlatFileSystemService::Entry::Id FileId() const final {
+ return file_id_;
+ }
+
+ private:
+ const std::string_view file_name_;
+ const file::FlatFileSystemService::Entry::Id file_id_;
+ const file::FlatFileSystemService::Entry::FilePermissions permissions_;
+ PersistentBuffer<kMaxSizeBytes>& persistent_buffer_;
+};
+
+} // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/public/pw_persistent_ram/persistent.h b/pw_persistent_ram/public/pw_persistent_ram/persistent.h
index eb8e5a76e..553f6923f 100644
--- a/pw_persistent_ram/public/pw_persistent_ram/persistent.h
+++ b/pw_persistent_ram/public/pw_persistent_ram/persistent.h
@@ -48,7 +48,7 @@ PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wmaybe-uninitialized");
// WARNING: Unlike a DoubleBufferedPersistent, a Persistent will be lost if a
// write/set operation is interrupted or otherwise not completed.
//
-// TODO(b/235277454): Consider a different integrity check implementation which
+// TODO: b/235277454 - Consider a different integrity check implementation which
// does not use a 512B lookup table.
template <typename T>
class Persistent {
diff --git a/pw_polyfill/BUILD.bazel b/pw_polyfill/BUILD.bazel
index 473173a37..eca11f07a 100644
--- a/pw_polyfill/BUILD.bazel
+++ b/pw_polyfill/BUILD.bazel
@@ -31,56 +31,19 @@ pw_cc_library(
includes = ["public"],
)
-# Provides <cstddef>'s std::byte for C++14.
-pw_cc_library(
- name = "cstddef",
- hdrs = [
- "cstddef_public_overrides/cstddef",
- "standard_library_public/pw_polyfill/standard_library/cstddef.h",
- ],
- includes = [
- "public_overrides",
- "standard_library_public",
- ],
- # Polyfills aren't supported in the Bazel build, so disallow use.
- visibility = ["//visibility:private"],
- deps = [":standard_library"],
-)
-
-# Provides <iterator>'s std::data and std::size for C++14.
-pw_cc_library(
- name = "iterator",
- hdrs = [
- "iterator_public_overrides/iterator",
- "standard_library_public/pw_polyfill/standard_library/iterator.h",
- ],
- includes = [
- "public_overrides",
- "standard_library_public",
- ],
- # Polyfills aren't supported in the Bazel build, so disallow use.
- visibility = ["//visibility:private"],
- deps = [":standard_library"],
-)
-
pw_cc_library(
name = "standard_library",
hdrs = [
"standard_library_public/pw_polyfill/standard_library/namespace.h",
],
includes = ["standard_library_public"],
- visibility = [
- "//pw_minimal_cpp_stdlib:__pkg__",
- "//pw_span:__pkg__",
- ],
+ visibility = ["//pw_minimal_cpp_stdlib:__pkg__"],
)
pw_cc_test(
name = "test",
srcs = ["test.cc"],
deps = [
- ":cstddef",
- ":iterator",
":pw_polyfill",
":standard_library",
"//pw_unit_test",
diff --git a/pw_polyfill/BUILD.gn b/pw_polyfill/BUILD.gn
index 0302de39a..a99e98de3 100644
--- a/pw_polyfill/BUILD.gn
+++ b/pw_polyfill/BUILD.gn
@@ -26,52 +26,15 @@ config("public_include_path") {
pw_source_set("pw_polyfill") {
public_configs = [ ":public_include_path" ]
remove_public_deps = [ "*" ]
- public_deps = [ ":standard_library" ]
public = [
"public/pw_polyfill/language_feature_macros.h",
"public/pw_polyfill/standard.h",
]
}
-config("cstddef_overrides_config") {
- include_dirs = [ "cstddef_public_overrides" ]
- cflags = [ "-Wno-gnu-include-next" ]
- visibility = [ ":*" ]
-}
-
-config("iterator_overrides_config") {
- include_dirs = [ "iterator_public_overrides" ]
- cflags = [ "-Wno-gnu-include-next" ]
- visibility = [ ":*" ]
-}
-
config("standard_library_public") {
include_dirs = [ "standard_library_public" ]
-}
-
-# Provides <cstddef>'s std::byte for C++14.
-pw_source_set("cstddef") {
- public_configs = [
- ":standard_library_public",
- ":cstddef_overrides_config",
- ]
- public_deps = [ ":standard_library" ]
- remove_public_deps = [ "*" ]
- public = [ "cstddef_public_overrides/cstddef" ]
- sources = [ "standard_library_public/pw_polyfill/standard_library/cstddef.h" ]
-}
-
-# Provides <iterator>'s std::data and std::size for C++14.
-pw_source_set("iterator") {
- public_configs = [
- ":standard_library_public",
- ":iterator_overrides_config",
- ]
- public_deps = [ ":standard_library" ]
- remove_public_deps = [ "*" ]
- public = [ "iterator_public_overrides/iterator" ]
- sources =
- [ "standard_library_public/pw_polyfill/standard_library/iterator.h" ]
+ visibility = [ ":*" ]
}
pw_source_set("standard_library") {
@@ -79,7 +42,10 @@ pw_source_set("standard_library") {
remove_public_deps = [ "*" ]
public =
[ "standard_library_public/pw_polyfill/standard_library/namespace.h" ]
- visibility = [ ":*" ]
+ visibility = [
+ ":*",
+ "$dir_pw_minimal_cpp_stdlib:*",
+ ]
}
pw_test_group("tests") {
@@ -88,16 +54,11 @@ pw_test_group("tests") {
}
pw_test("test") {
- deps = [ ":pw_polyfill" ]
-
- # Do not depend on :cstddef and :iterator since they override library headers.
- # Instead, add their include path and list them as sources.
- configs = [ ":standard_library_public" ]
- sources = [
- "standard_library_public/pw_polyfill/standard_library/cstddef.h",
- "standard_library_public/pw_polyfill/standard_library/iterator.h",
- "test.cc",
+ deps = [
+ ":pw_polyfill",
+ ":standard_library",
]
+ sources = [ "test.cc" ]
}
pw_doc_group("docs") {
diff --git a/pw_polyfill/CMakeLists.txt b/pw_polyfill/CMakeLists.txt
index 036857fee..781ca94e1 100644
--- a/pw_polyfill/CMakeLists.txt
+++ b/pw_polyfill/CMakeLists.txt
@@ -21,11 +21,8 @@ pw_add_library(pw_polyfill INTERFACE
PUBLIC_INCLUDES
public
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_POLYFILL)
- zephyr_link_libraries(pw_polyfill)
-endif()
-pw_add_library(pw_polyfill.standard_library INTERFACE
+pw_add_library(pw_polyfill._standard_library INTERFACE
HEADERS
standard_library_public/pw_polyfill/standard_library/namespace.h
PUBLIC_INCLUDES
diff --git a/pw_polyfill/Kconfig b/pw_polyfill/Kconfig
index c575dd8fb..6f26e1770 100644
--- a/pw_polyfill/Kconfig
+++ b/pw_polyfill/Kconfig
@@ -12,9 +12,11 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_polyfill"
+
config PIGWEED_POLYFILL
- bool "Enable the Pigweed polyfill library (pw_polyfill)"
+ bool "Link pw_polyfill library"
+ help
+ See :ref:`module-pw_polyfill` for module details.
-config PIGWEED_POLYFILL_OVERRIDES
- bool "Enable the Pigweed polyfill overrides library (pw_polyfill.overrides)"
- depends on PIGWEED_POLYFILL
+endmenu
diff --git a/pw_polyfill/README.md b/pw_polyfill/README.md
index fa360fd0f..1b1f862c4 100644
--- a/pw_polyfill/README.md
+++ b/pw_polyfill/README.md
@@ -1 +1 @@
-# pw\_polyfill: Backports C++17 features to C++14
+# pw\_polyfill: Supports writing code against different C++ standards
diff --git a/pw_polyfill/docs.rst b/pw_polyfill/docs.rst
index 419ac22aa..08eff995c 100644
--- a/pw_polyfill/docs.rst
+++ b/pw_polyfill/docs.rst
@@ -4,7 +4,7 @@
pw_polyfill
===========
The ``pw_polyfill`` module supports compiling code against different C++
-standards. It also supports backporting a few C++17 features to C++14.
+standards.
----------------------------------------------------
Adapt code to compile with different versions of C++
@@ -61,12 +61,7 @@ Pigweed backports a few C++ features to older C++ standards. These features
are provided in the ``pw`` namespace. If the features are provided by the
toolchain, the ``pw`` versions are aliases of the ``std`` versions.
-``pw_polyfill`` also backports a few C++17 library features to C++14 by wrapping
-the standard C++ and C headers. The wrapper headers include the original header
-using `#include_next
-<https://gcc.gnu.org/onlinedocs/cpp/Wrapper-Headers.html>`_, then add missing
-features. The backported features are only defined if they aren't provided by
-the standard header and can only be used when compiling with C++14 in GN.
+These features are documented here, but are not implemented in ``pw_polyfill``.
Backported features
===================
@@ -91,18 +86,12 @@ Backported features
- :ref:`module-pw_bytes`
- ``pw_bytes/bit.h``
- ``pw::endian``
- * - ``<cstdlib>``
- - ``std::byte``
- - ``__cpp_lib_byte``
- - pw_polyfill
- - ``<cstdlib>``
- - ``std::byte``
- * - ``<iterator>``
- - ``std::data``, ``std::size``
- - ``__cpp_lib_nonmember_container_access``
- - pw_polyfill
- - ``<iterator>``
- - ``std::data``, ``std::size``
+ * - ``<expected>``
+ - ``std::expected``
+ - ``__cpp_lib_expected``
+ - :ref:`module-pw_result`
+ - ``pw_result/expected.h``
+ - ``pw::expected``
* - ``<span>``
- ``std::span``
- ``__cpp_lib_span``
@@ -110,12 +99,8 @@ Backported features
- ``pw_span/span.h``
- ``pw::span``
--------------
-Compatibility
--------------
-C++14
-
+------
Zephyr
-======
+------
To enable ``pw_polyfill`` for Zephyr add ``CONFIG_PIGWEED_POLYFILL=y`` to the
project's configuration.
diff --git a/pw_polyfill/public/pw_polyfill/language_feature_macros.h b/pw_polyfill/public/pw_polyfill/language_feature_macros.h
index b4d9d2ace..34df96f05 100644
--- a/pw_polyfill/public/pw_polyfill/language_feature_macros.h
+++ b/pw_polyfill/public/pw_polyfill/language_feature_macros.h
@@ -36,11 +36,11 @@
#endif // __cpp_consteval >= 201811L
// Mark functions as constinit if supported by the compiler.
-#if defined(__cpp_constinit)
+#if defined(__cpp_constinit) && __cpp_constinit >= 201907L
#define PW_CONSTINIT constinit
#elif defined(__clang__)
#define PW_CONSTINIT [[clang::require_constant_initialization]]
-#elif defined(__GNUC__) && __GNUC__ >= 10
+#elif defined(__GNUC__) && __GNUC__ >= 13
#define PW_CONSTINIT __constinit
#else
#define PW_CONSTINIT
diff --git a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h b/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h
deleted file mode 100644
index fc6bfa26c..000000000
--- a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#pragma once
-
-#include <cstddef>
-
-#include "pw_polyfill/standard_library/namespace.h"
-
-// Defines the std::byte type if it is not present.
-#ifndef __cpp_lib_byte
-#define __cpp_lib_byte 201603L
-
-_PW_POLYFILL_BEGIN_NAMESPACE_STD
-
-enum class byte : unsigned char {};
-
-template <typename I>
-constexpr I to_integer(byte b) noexcept {
- return I(b);
-}
-
-constexpr byte operator|(byte l, byte r) noexcept {
- return byte(static_cast<unsigned int>(l) | static_cast<unsigned int>(r));
-}
-
-constexpr byte operator&(byte l, byte r) noexcept {
- return byte(static_cast<unsigned int>(l) & static_cast<unsigned int>(r));
-}
-
-constexpr byte operator^(byte l, byte r) noexcept {
- return byte(static_cast<unsigned int>(l) ^ static_cast<unsigned int>(r));
-}
-
-constexpr byte operator~(byte b) noexcept {
- return byte(~static_cast<unsigned int>(b));
-}
-
-template <typename I>
-constexpr byte operator<<(byte b, I shift) noexcept {
- return byte(static_cast<unsigned int>(b) << shift);
-}
-
-template <typename I>
-constexpr byte operator>>(byte b, I shift) noexcept {
- return byte(static_cast<unsigned int>(b) >> shift);
-}
-
-constexpr byte& operator|=(byte& l, byte r) noexcept { return l = l | r; }
-constexpr byte& operator&=(byte& l, byte r) noexcept { return l = l & r; }
-constexpr byte& operator^=(byte& l, byte r) noexcept { return l = l ^ r; }
-
-template <typename I>
-constexpr byte& operator<<=(byte& b, I shift) noexcept {
- return b = b << shift;
-}
-
-template <typename I>
-constexpr byte& operator>>=(byte& b, I shift) noexcept {
- return b = b >> shift;
-}
-
-_PW_POLYFILL_END_NAMESPACE_STD
-
-#endif // __cpp_lib_byte
diff --git a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/iterator.h b/pw_polyfill/standard_library_public/pw_polyfill/standard_library/iterator.h
deleted file mode 100644
index 2a49b92b1..000000000
--- a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/iterator.h
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#pragma once
-
-#include <iterator>
-
-#include "pw_polyfill/standard_library/namespace.h"
-
-// Define std::data and std::size.
-#ifndef __cpp_lib_nonmember_container_access
-#define __cpp_lib_nonmember_container_access 201411L
-
-#include <cstddef>
-
-_PW_POLYFILL_BEGIN_NAMESPACE_STD
-
-template <typename C>
-constexpr auto data(C& container) -> decltype(container.data()) {
- return container.data();
-}
-
-template <typename C>
-constexpr auto data(const C& container) -> decltype(container.data()) {
- return container.data();
-}
-
-template <typename T, size_t kSize>
-constexpr T* data(T (&array)[kSize]) noexcept {
- return array;
-}
-
-template <typename C>
-constexpr auto size(const C& container) -> decltype(container.size()) {
- return container.size();
-}
-
-template <typename T, size_t kSize>
-constexpr size_t size(const T (&)[kSize]) noexcept {
- return kSize;
-}
-
-_PW_POLYFILL_END_NAMESPACE_STD
-
-#endif // __cpp_lib_nonmember_container_access
diff --git a/pw_polyfill/test.cc b/pw_polyfill/test.cc
index d17475df4..2f0397e6b 100644
--- a/pw_polyfill/test.cc
+++ b/pw_polyfill/test.cc
@@ -12,13 +12,9 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <array>
-
#include "gtest/gtest.h"
#include "pw_polyfill/language_feature_macros.h"
#include "pw_polyfill/standard.h"
-#include "pw_polyfill/standard_library/cstddef.h"
-#include "pw_polyfill/standard_library/iterator.h"
namespace pw {
namespace polyfill {
@@ -44,43 +40,11 @@ static_assert(PW_CXX_STANDARD_IS_SUPPORTED(20), "C++20 must be supported");
static_assert(!PW_CXX_STANDARD_IS_SUPPORTED(20), "C++20 must not be supported");
#endif // __cplusplus >= 202002L
-TEST(Cstddef, Byte_Operators) {
- std::byte value = std::byte(0);
- EXPECT_EQ((value | std::byte(0x12)), std::byte(0x12));
- EXPECT_EQ((value & std::byte(0x12)), std::byte(0));
- EXPECT_EQ((value ^ std::byte(0x12)), std::byte(0x12));
- EXPECT_EQ(~std::byte(0), std::byte(-1));
- EXPECT_EQ((std::byte(1) << 3), std::byte(0x8));
- EXPECT_EQ((std::byte(0x8) >> 3), std::byte(1));
-}
-
-TEST(Cstddef, Byte_AssignmentOperators) {
- std::byte value = std::byte(0);
- EXPECT_EQ((value |= std::byte(0x12)), std::byte(0x12));
- EXPECT_EQ((value &= std::byte(0x0F)), std::byte(0x02));
- EXPECT_EQ((value ^= std::byte(0xFF)), std::byte(0xFD));
- EXPECT_EQ((value <<= 4), std::byte(0xD0));
- EXPECT_EQ((value >>= 5), std::byte(0x6));
-}
-
// Check that consteval is at least equivalent to constexpr.
PW_CONSTEVAL int ConstevalFunction() { return 123; }
static_assert(ConstevalFunction() == 123,
"Function should work in static_assert");
-int c_array[5423] = {};
-std::array<int, 32> array;
-
-TEST(Iterator, Size) {
- EXPECT_EQ(std::size(c_array), sizeof(c_array) / sizeof(*c_array));
- EXPECT_EQ(std::size(array), array.size());
-}
-
-TEST(Iterator, Data) {
- EXPECT_EQ(std::data(c_array), c_array);
- EXPECT_EQ(std::data(array), array.data());
-}
-
PW_CONSTINIT bool mutable_value = true;
TEST(Constinit, ValueIsMutable) {
diff --git a/pw_preprocessor/CMakeLists.txt b/pw_preprocessor/CMakeLists.txt
index acf4a7c22..1449f8755 100644
--- a/pw_preprocessor/CMakeLists.txt
+++ b/pw_preprocessor/CMakeLists.txt
@@ -32,7 +32,3 @@ pw_add_library(pw_preprocessor.arch INTERFACE
PUBLIC_INCLUDES
public
)
-
-if(Zephyr_FOUND AND CONFIG_PIGWEED_PREPROCESSOR)
- zephyr_link_libraries(pw_preprocessor)
-endif()
diff --git a/pw_preprocessor/Kconfig b/pw_preprocessor/Kconfig
index ec5bc9213..82562c4f3 100644
--- a/pw_preprocessor/Kconfig
+++ b/pw_preprocessor/Kconfig
@@ -12,5 +12,11 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_preprocessor"
+
config PIGWEED_PREPROCESSOR
- bool "Enable Pigweed preprocessor library (pw_preprocessor)"
+ bool "Link pw_preprocessor library"
+ help
+ See :ref:`module-pw_preprocessor` for module details.
+
+endmenu
diff --git a/pw_preprocessor/arguments_test.cc b/pw_preprocessor/arguments_test.cc
index 0a7b69ed3..346597217 100644
--- a/pw_preprocessor/arguments_test.cc
+++ b/pw_preprocessor/arguments_test.cc
@@ -128,9 +128,9 @@ TEST(CommaVarargs, EmptyFinalArgument) {
#define BAD_DEMO(fmt, ...) _BAD_DEMO_ADD_123(fmt PW_COMMA_ARGS(__VA_ARGS__))
#define _BAD_DEMO_ADD_123(fmt, ...) \
- _BAD_DEMO_CAPTURE_ARGS("%d: " fmt, 123 PW_COMMA_ARGS(__VA_ARGS__))
+ _CAPTURE_ARGS_AS_TUPLE("%d: " fmt, 123 PW_COMMA_ARGS(__VA_ARGS__))
-#define _BAD_DEMO_CAPTURE_ARGS(...) std::make_tuple(__VA_ARGS__)
+#define _CAPTURE_ARGS_AS_TUPLE(...) std::make_tuple(__VA_ARGS__)
TEST(CommaVarargs, MisbehavesWithMacroToMacroUse_NoArgs_ArgsAreOkay) {
auto [a1, a2] = BAD_DEMO("Hello world");
@@ -255,6 +255,72 @@ TEST(DropLastArgIfEmpty, EmptyLastArg) {
static_assert(FunctionArgCount(PW_DROP_LAST_ARG_IF_EMPTY(1, 2, 3, )) == 3);
}
+// This test demonstrates that PW_DROP_LAST_ARG_IF_EMPTY behaves unexpectedly
+// when it is used when invoking another macro. DO NOT use
+// PW_DROP_LAST_ARG_IF_EMPTY when invoking another macro!
+#define BAD_DROP_LAST_DEMO(fmt, ...) \
+ _BAD_DROP_LAST_DEMO_ADD_123(PW_DROP_LAST_ARG_IF_EMPTY(fmt, __VA_ARGS__))
+
+#define _BAD_DROP_LAST_DEMO_ADD_123(fmt, ...) \
+ _CAPTURE_ARGS_AS_TUPLE("%d: " fmt, \
+ PW_DROP_LAST_ARG_IF_EMPTY(123, __VA_ARGS__))
+
+TEST(DropLastArgIfEmpty, EmptyLastArgArgsLoseOrder) {
+ // If there are any additional arguments, the order is incorrect! The 123
+ // argument should go before the 3, 2, 1 arguments, but it is inserted after.
+ // This would be a compilation error if these arguments were passed to printf.
+ // What's worse is that this can silently fail if the arguments happen to be
+ // compatible types.
+ auto [a1, a2, a3, a4, a5] =
+ BAD_DROP_LAST_DEMO("Countdown in %d %d %d", 3, 2, 1, );
+ EXPECT_STREQ(a1, "%d: Countdown in %d %d %d");
+ EXPECT_EQ(a2, 3);
+ EXPECT_EQ(a3, 2);
+ EXPECT_EQ(a4, 1);
+ EXPECT_EQ(a5, 123);
+}
+
+TEST(DropLastArgIfEmpty, NonEmptyLastArgArgsLoseOrder) {
+ // If there are any additional arguments, the order is incorrect! The 123
+ // argument should go before the 3, 2, 1 arguments, but it is inserted after.
+ // This would be a compilation error if these arguments were passed to printf.
+ // What's worse is that this can silently fail if the arguments happen to be
+ // compatible types.
+ auto [a1, a2, a3, a4, a5] =
+ BAD_DROP_LAST_DEMO("Countdown in %d %d %d", 3, 2, 1);
+ EXPECT_STREQ(a1, "%d: Countdown in %d %d %d");
+ EXPECT_EQ(a2, 3);
+ EXPECT_EQ(a3, 2);
+ EXPECT_EQ(a4, 1);
+ EXPECT_EQ(a5, 123);
+}
+
+// When PW_DROP_LAST_ARG_IF_EMPTY is used once, and there are no other
+// modifications to __VA_ARGS__, then the order is kept.
+#define DROP_LAST_DEMO(fmt, arg_a, arg_b, ...) \
+ _CAPTURE_ARGS_AS_TUPLE( \
+ "%d: " fmt, PW_DROP_LAST_ARG_IF_EMPTY(123, arg_a, arg_b, __VA_ARGS__))
+
+TEST(DropLastArgIfEmpty, EmptyLastArgAllArgsInOrder) {
+ const auto [a1, a2, a3, a4, a5] =
+ DROP_LAST_DEMO("Countdown in %d %d %d", 3, 2, 1, );
+ EXPECT_STREQ(a1, "%d: Countdown in %d %d %d");
+ EXPECT_EQ(a2, 123);
+ EXPECT_EQ(a3, 3);
+ EXPECT_EQ(a4, 2);
+ EXPECT_EQ(a5, 1);
+}
+
+TEST(DropLastArgIfEmpty, NonEmptyLastArgAllArgsInOrder) {
+ const auto [a1, a2, a3, a4, a5] =
+ DROP_LAST_DEMO("Countdown in %d %d %d", 3, 2, 1);
+ EXPECT_STREQ(a1, "%d: Countdown in %d %d %d");
+ EXPECT_EQ(a2, 123);
+ EXPECT_EQ(a3, 3);
+ EXPECT_EQ(a4, 2);
+ EXPECT_EQ(a5, 1);
+}
+
#define SOME_VARIADIC_MACRO(...) PW_MACRO_ARG_COUNT(__VA_ARGS__)
#define ANOTHER_VARIADIC_MACRO(arg, ...) SOME_VARIADIC_MACRO(__VA_ARGS__)
diff --git a/pw_preprocessor/public/pw_preprocessor/arch.h b/pw_preprocessor/public/pw_preprocessor/arch.h
index 676a386c2..e4f264f93 100644
--- a/pw_preprocessor/public/pw_preprocessor/arch.h
+++ b/pw_preprocessor/public/pw_preprocessor/arch.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-// TODO(b/234887943): arch.h should be refactored out of pw_preprocessor as the
+// TODO: b/234887943 - arch.h should be refactored out of pw_preprocessor as the
// scope is outside of the module. The intended scope of arch.h is only to
// provide architecture targeting and not any added utilities and capabilities.
// Perhaps it should be placed under pw_compiler along with b/234877280, e.g.
diff --git a/pw_preprocessor/public/pw_preprocessor/compiler.h b/pw_preprocessor/public/pw_preprocessor/compiler.h
index 44c2fa9f1..17f970150 100644
--- a/pw_preprocessor/public/pw_preprocessor/compiler.h
+++ b/pw_preprocessor/public/pw_preprocessor/compiler.h
@@ -18,7 +18,7 @@
#include <assert.h>
-// TODO(b/234877280): compiler.h should be refactored out of pw_preprocessor as
+// TODO: b/234877280 - compiler.h should be refactored out of pw_preprocessor as
// the scope is outside of the module. Perhaps it should be split up and placed
// under pw_compiler, e.g. pw_compiler/attributes.h & pw_compiler/builtins.h.
diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst
index 5be527bc9..f7f8a5b6b 100644
--- a/pw_presubmit/docs.rst
+++ b/pw_presubmit/docs.rst
@@ -28,6 +28,8 @@ system. If it's not Bazel formatting passes without checking.)
The ``pw_presubmit`` package includes presubmit checks that can be used with any
project. These checks include:
+.. todo-check: disable
+
* Check code format of several languages including C, C++, and Python
* Initialize a Python environment
* Run all Python tests
@@ -36,7 +38,15 @@ project. These checks include:
* Ensure source files are included in the GN and Bazel builds
* Build and run all tests with GN
* Build and run all tests with Bazel
-* Ensure all header files contain ``#pragma once``
+* Ensure all header files contain ``#pragma once`` (or, that they have matching
+ ``#ifndef``/``#define`` lines)
+* Ensure lists are kept in alphabetical order
+* Forbid non-inclusive language
+* Check format of TODO lines
+* Apply various rules to ``.gitmodules`` or ``OWNERS`` files
+* Ensure all source files are in the build
+
+.. todo-check: enable
-------------
Compatibility
@@ -60,11 +70,23 @@ The ``pw_presubmit.cli`` module sets up the command-line interface for a
presubmit script. This defines a standard set of arguments for invoking
presubmit checks. Its use is optional, but recommended.
-pw_presubmit.cli
-----------------
+Common ``pw presubmit`` command line arguments
+----------------------------------------------
+.. argparse::
+ :module: pw_presubmit.cli
+ :func: _get_default_parser
+ :prog: pw presubmit
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
+
+
+``pw_presubmit.cli`` Python API
+-------------------------------
.. automodule:: pw_presubmit.cli
:members: add_arguments, run
+
Presubmit output directory
--------------------------
The ``pw_presubmit`` command line interface includes an ``--output-directory``
@@ -73,6 +95,8 @@ path is ``out/presubmit``. A subdirectory is created for each presubmit step.
This directory persists between presubmit runs and can be cleaned by deleting it
or running ``pw presubmit --clean``.
+.. _module-pw_presubmit-presubmit-checks:
+
Presubmit checks
================
A presubmit check is defined as a function or other callable. The function must
@@ -86,16 +110,16 @@ Either of these functions could be used as presubmit checks:
.. code-block:: python
- @pw_presubmit.filter_paths(endswith='.py')
- def file_contains_ni(ctx: PresubmitContext):
- for path in ctx.paths:
- with open(path) as file:
- contents = file.read()
- if 'ni' not in contents and 'nee' not in contents:
- raise PresumitFailure('Files must say "ni"!', path=path)
+ @pw_presubmit.filter_paths(endswith='.py')
+ def file_contains_ni(ctx: PresubmitContext):
+ for path in ctx.paths:
+ with open(path) as file:
+ contents = file.read()
+ if 'ni' not in contents and 'nee' not in contents:
+ raise PresumitFailure('Files must say "ni"!', path=path)
- def run_the_build(_):
- subprocess.run(['make', 'release'], check=True)
+ def run_the_build(_):
+ subprocess.run(['make', 'release'], check=True)
Presubmit checks functions are grouped into "programs" -- a named series of
checks. Projects may find it helpful to have programs for different purposes,
@@ -103,65 +127,33 @@ such as a quick program for local use and a full program for automated use. The
:ref:`example script <example-script>` uses ``pw_presubmit.Programs`` to define
``quick`` and ``full`` programs.
-``PresubmitContext`` has the following members:
-
-* ``root``: Source checkout root directory
-* ``repos``: Repositories (top-level and submodules) processed by
- ``pw presubmit``
-* ``output_dir``: Output directory for this specific presubmit step
-* ``failure_summary_log``: File path where steps should write a brief summary
- of any failures
-* ``paths``: Modified files for the presubmit step to check (often used in
- formatting steps but ignored in compile steps)
-* ``all_paths``: All files in the repository tree.
-* ``package_root``: Root directory for ``pw package`` installations
-* ``override_gn_args``: Additional GN args processed by ``build.gn_gen()``
-* ``luci``: Information about the LUCI build or None if not running in LUCI
-* ``num_jobs``: Number of jobs to run in parallel
-* ``continue_after_build_error``: For steps that compile, don't exit on the
- first compilation error
-
-The ``luci`` member is of type ``LuciContext`` and has the following members:
-
-* ``buildbucket_id``: The globally-unique buildbucket id of the build
-* ``build_number``: The builder-specific incrementing build number, if
- configured for this builder
-* ``project``: The LUCI project under which this build is running (often
- ``pigweed`` or ``pigweed-internal``)
-* ``bucket``: The LUCI bucket under which this build is running (often ends
- with ``ci`` or ``try``)
-* ``builder``: The builder being run
-* ``swarming_server``: The swarming server on which this build is running
-* ``swarming_task_id``: The swarming task id of this build
-* ``cas_instance``: The CAS instance accessible from this build
-* ``pipeline``: Information about the build pipeline, if applicable.
-* ``triggers``: Information about triggering commits, if applicable.
-
-The ``pipeline`` member, if present, is of type ``LuciPipeline`` and has the
-following members:
-
-* ``round``: The zero-indexed round number.
-* ``builds_from_previous_iteration``: A list of the buildbucket ids from the
- previous round, if any, encoded as strs.
-
-The ``triggers`` member is a sequence of ``LuciTrigger`` objects, which have the
-following members:
-
-* ``number``: The number of the change in Gerrit.
-* ``patchset``: The number of the patchset of the change.
-* ``remote``: The full URL of the remote.
-* ``branch``: The name of the branch on which this change is being/was
- submitted.
-* ``ref``: The ``refs/changes/..`` path that can be used to reference the
- patch for unsubmitted changes and the hash for submitted changes.
-* ``gerrit_name``: The name of the googlesource.com Gerrit host.
-* ``submitted``: Whether the change has been submitted or is still pending.
+By default, presubmit steps are only run on files changed since ``@{upstream}``.
+If all such files are filtered out by ``filter_paths``, then that step will be
+skipped. This can be overridden with the ``--base`` and ``--full`` arguments to
+``pw presubmit``. In automated testing ``--full`` is recommended, except for
+lint/format checks where ``--base HEAD~1`` is recommended.
+
+.. autoclass:: pw_presubmit.presubmit_context.PresubmitContext
+ :members:
+ :noindex:
Additional members can be added by subclassing ``PresubmitContext`` and
``Presubmit``. Then override ``Presubmit._create_presubmit_context()`` to
return the subclass of ``PresubmitContext``. Finally, add
``presubmit_class=PresubmitSubClass`` when calling ``cli.run()``.
+.. autoclass:: pw_presubmit.presubmit_context.LuciContext
+ :members:
+ :noindex:
+
+.. autoclass:: pw_presubmit.presubmit_context.LuciPipeline
+ :members:
+ :noindex:
+
+.. autoclass:: pw_presubmit.presubmit_context.LuciTrigger
+ :members:
+ :noindex:
+
Substeps
--------
Presubmit steps can define substeps that can run independently in other tooling.
@@ -190,6 +182,13 @@ others. All of these checks can be included by adding
``pw_presubmit.format_code.presubmit_checks()`` to a presubmit program. These
all use language-specific formatters like clang-format or black.
+Example changes demonstrating how to add formatters:
+
+* `CSS <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178810>`_
+* `JSON <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/171991>`_
+* `reStructuredText <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168541>`_
+* `TypeScript <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/164825>`_
+
These will suggest fixes using ``pw format --fix``.
Options for code formatting can be specified in the ``pigweed.json`` file
@@ -201,37 +200,41 @@ or fix code formatting.
by Pigweed itself) and ``yapf`` (the default).
* ``black_path``: If ``python_formatter`` is ``black``, use this as the
executable instead of ``black``.
-
-.. TODO(b/264578594) Add exclude to pigweed.json file.
-.. * ``exclude``: List of path regular expressions to ignore.
+* ``black_config_file``: Set the config file for the black formatter.
+* ``exclude``: List of path regular expressions to ignore. Will be evaluated
+ against paths relative to the checkout root using ``re.search``.
Example section from a ``pigweed.json`` file:
-.. code-block::
-
- {
- "pw": {
- "pw_presubmit": {
- "format": {
- "python_formatter": "black",
- "black_path": "black"
- }
- }
- }
- }
+.. code-block:: json
+
+ {
+ "pw": {
+ "pw_presubmit": {
+ "format": {
+ "python_formatter": "black",
+ "black_config_file": "$pw_env{PW_PROJECT_ROOT}/config/.black.toml"
+ "black_path": "black",
+ "exclude": [
+ "\\bthird_party/foo/src"
+ ]
+ }
+ }
+ }
+ }
Sorted Blocks
^^^^^^^^^^^^^
Blocks of code can be required to be kept in sorted order using comments like
the following:
-.. code-block::
+.. code-block:: python
- # keep-sorted: start
- bar
- baz
- foo
- # keep-sorted: end
+ # keep-sorted: start
+ bar
+ baz
+ foo
+ # keep-sorted: end
This can be included by adding ``pw_presubmit.keep_sorted.presubmit_check`` to a
presubmit program. Adding ``ignore-case`` to the start line will use
@@ -245,13 +248,13 @@ Prefixes can be ignored by adding ``ignore-prefix=`` followed by a
comma-separated list of prefixes. The list below will be kept in this order.
Neither commas nor whitespace are supported in prefixes.
-.. code-block::
+.. code-block:: python
- # keep-sorted: start ignore-prefix=',"
- 'bar',
- "baz",
- 'foo',
- # keep-sorted: end
+ # keep-sorted: start ignore-prefix=',"
+ 'bar',
+ "baz",
+ 'foo',
+ # keep-sorted: end
Inline comments are assumed to be associated with the following line. For
example, the following is already sorted. This can be disabled with
@@ -259,14 +262,14 @@ example, the following is already sorted. This can be disabled with
.. todo-check: disable
-.. code-block::
+.. code-block:: python
- # keep-sorted: start
- # TODO(b/1234) Fix this.
- bar,
- # TODO(b/5678) Also fix this.
- foo,
- # keep-sorted: end
+ # keep-sorted: start
+ # TODO: b/1234 - Fix this.
+ bar,
+ # TODO: b/5678 - Also fix this.
+ foo,
+ # keep-sorted: end
.. todo-check: enable
@@ -280,12 +283,12 @@ nested, so there's no ability to add a keep-sorted block for the sub-items.
.. code-block::
- # keep-sorted: start
- * abc
- * xyz
- * uvw
- * def
- # keep-sorted: end
+ # keep-sorted: start
+ * abc
+ * xyz
+ * uvw
+ * def
+ # keep-sorted: end
The presubmit check will suggest fixes using ``pw keep-sorted --fix``.
@@ -298,7 +301,8 @@ by adding ``pw_presubmit.gitmodules.create()`` to a presubmit program. This
function takes an optional argument of type ``pw_presubmit.gitmodules.Config``.
``Config`` objects have several properties.
-* ``allow_non_googlesource_hosts: bool = False`` — If false, all submodules URLs
+* ``allow_submodules: bool = True`` — If false, don't allow any submodules.
+* ``allow_non_googlesource_hosts: bool = False`` — If false, all submodule URLs
must be on a Google-managed Gerrit server.
* ``allowed_googlesource_hosts: Sequence[str] = ()`` — If set, any
Google-managed Gerrit URLs for submodules most be in this list. Entries
@@ -309,7 +313,7 @@ function takes an optional argument of type ``pw_presubmit.gitmodules.Config``.
URLs are prohibited.
* ``allow_git_corp_google_com: bool = True`` — If false, ``git.corp.google.com``
submodule URLs are prohibited.
-* ``require_branch: bool = False`` — If True, all submodules must reference a
+* ``require_branch: bool = False`` — If true, all submodules must reference a
branch.
* ``validator: Callable[[PresubmitContext, Path, str, Dict[str, str]], None] = None``
— A function that can be used for arbitrary submodule validation. It's called
@@ -322,14 +326,40 @@ There's a ``pragma_once`` check that confirms the first non-comment line of
C/C++ headers is ``#pragma once``. This is enabled by adding
``pw_presubmit.cpp_checks.pragma_once`` to a presubmit program.
+#ifndef/#define
+^^^^^^^^^^^^^^^
+There's an ``ifndef_guard`` check that confirms the first two non-comment lines
+of C/C++ headers are ``#ifndef HEADER_H`` and ``#define HEADER_H``. This is
+enabled by adding ``pw_presubmit.cpp_checks.include_guard_check()`` to a
+presubmit program. ``include_guard_check()`` has options for specifying what the
+header guard should be based on the path.
+
+This check is not used in Pigweed itself but is available to projects using
+Pigweed.
+
.. todo-check: disable
TODO(b/###) Formatting
^^^^^^^^^^^^^^^^^^^^^^^^^
There's a check that confirms ``TODO`` lines match a given format. Upstream
-Pigweed expects these to look like ``TODO(b/###): Explanation``, but makes it
+Pigweed expects these to look like ``TODO: b/### - Explanation``, but makes it
easy for projects to define their own pattern instead.
+Some older forms are still allowed but discouraged. In order of preference we
+allow the following formats by default.
+
+.. todo-check: disable
+
+.. code-block::
+
+ # TODO: b/1234 - Explanation.
+ # TODO: username@ - Explanation.
+ # TODO: username@example.com - Explanation.
+ # TODO: b/1234 - Explanation.
+ # TODO(username) Explanation.
+
+.. todo-check: enable
+
To use this check add ``todo_check.create(todo_check.BUGS_OR_USERNAMES)`` to a
presubmit program.
@@ -370,6 +400,11 @@ a callable as an argument that indicates, for a given file, where a controlling
Formatting of ``OWNERS`` files is handled similary to formatting of other
source files and is discussed in `Code Formatting`.
+JSON
+^^^^
+The JSON check requires all ``*.json`` files to be valid JSON files. It can be
+included by adding ``json_check.presubmit_check()`` to a presubmit program.
+
Source in Build
^^^^^^^^^^^^^^^
Pigweed provides checks that source files are configured as part of the build
@@ -412,163 +447,174 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
.. code-block:: python
- """Example presubmit check script."""
-
- import argparse
- import logging
- import os
- from pathlib import Path
- import re
- import sys
- from typing import List, Optional, Pattern
-
- try:
- import pw_cli.log
- except ImportError:
- print('ERROR: Activate the environment before running presubmits!',
- file=sys.stderr)
- sys.exit(2)
-
- import pw_presubmit
- from pw_presubmit import (
- build,
- cli,
- cpp_checks,
- environment,
- format_code,
- git_repo,
- inclusive_language,
- filter_paths,
- python_checks,
- PresubmitContext,
- )
- from pw_presubmit.install_hook import install_hook
-
- # Set up variables for key project paths.
- PROJECT_ROOT = Path(os.environ['MY_PROJECT_ROOT'])
- PIGWEED_ROOT = PROJECT_ROOT / 'pigweed'
-
- # Rerun the build if files with these extensions change.
- _BUILD_EXTENSIONS = frozenset(
- ['.rst', '.gn', '.gni', *format_code.C_FORMAT.extensions])
-
-
- #
- # Presubmit checks
- #
- def release_build(ctx: PresubmitContext):
- build.gn_gen(ctx, build_type='release')
- build.ninja(ctx)
-
-
- def host_tests(ctx: PresubmitContext):
- build.gn_gen(ctx, run_host_tests='true')
- build.ninja(ctx)
-
-
- # Avoid running some checks on certain paths.
- PATH_EXCLUSIONS = (
- re.compile(r'^external/'),
- re.compile(r'^vendor/'),
- )
-
-
- # Use the upstream pragma_once check, but apply a different set of path
- # filters with @filter_paths.
- @filter_paths(endswith='.h', exclude=PATH_EXCLUSIONS)
- def pragma_once(ctx: PresubmitContext):
- cpp_checks.pragma_once(ctx)
-
-
- #
- # Presubmit check programs
- #
- OTHER = (
- # Checks not ran by default but that should be available. These might
- # include tests that are expensive to run or that don't yet pass.
- build.gn_quick_check,
- )
-
- QUICK = (
- # List some presubmit checks to run
- pragma_once,
- host_tests,
- # Use the upstream formatting checks, with custom path filters applied.
- format_code.presubmit_checks(exclude=PATH_EXCLUSIONS),
- # Include the upstream inclusive language check.
- inclusive_language.presubmit_check,
- # Include just the lint-related Python checks.
- python_checks.gn_pylint.with_filter(exclude=PATH_EXCLUSIONS),
- )
-
- FULL = (
- QUICK, # Add all checks from the 'quick' program
- release_build,
- # Use the upstream Python checks, with custom path filters applied.
- # Checks listed multiple times are only run once.
- python_checks.gn_python_check.with_filter(exclude=PATH_EXCLUSIONS),
- )
-
- PROGRAMS = pw_presubmit.Programs(other=OTHER, quick=QUICK, full=FULL)
-
-
- #
- # Allowlist of remote refs for presubmit. If the remote ref being pushed to
- # matches any of these values (with regex matching), then the presubmits
- # checks will be run before pushing.
- #
- PRE_PUSH_REMOTE_REF_ALLOWLIST = (
- 'refs/for/main',
- )
-
-
- def run(install: bool, remote_ref: Optional[str], **presubmit_args) -> int:
- """Process the --install argument then invoke pw_presubmit."""
-
- # Install the presubmit Git pre-push hook, if requested.
- if install:
- # '$remote_ref' will be replaced by the actual value of the remote ref
- # at runtime.
- install_git_hook('pre-push', [
- 'python', '-m', 'tools.presubmit_check', '--base', 'HEAD~',
- '--remote-ref', '$remote_ref'
- ])
- return 0
-
- # Run the checks if either no remote_ref was passed, or if the remote ref
- # matches anything in the allowlist.
- if remote_ref is None or any(
- re.search(pattern, remote_ref)
- for pattern in PRE_PUSH_REMOTE_REF_ALLOWLIST):
- return cli.run(root=PROJECT_ROOT, **presubmit_args)
-
-
- def main() -> int:
- """Run the presubmit checks for this repository."""
- parser = argparse.ArgumentParser(description=__doc__)
- cli.add_arguments(parser, PROGRAMS, 'quick')
-
- # Define an option for installing a Git pre-push hook for this script.
- parser.add_argument(
- '--install',
- action='store_true',
- help='Install the presubmit as a Git pre-push hook and exit.')
-
- # Define an optional flag to pass the remote ref into this script, if it
- # is run as a pre-push hook. The destination variable in the parsed args
- # will be `remote_ref`, as dashes are replaced with underscores to make
- # valid variable names.
- parser.add_argument(
- '--remote-ref',
- default=None,
- nargs='?', # Make optional.
- help='Remote ref of the push command, for use by the pre-push hook.')
-
- return run(**vars(parser.parse_args()))
-
- if __name__ == '__main__':
- pw_cli.log.install(logging.INFO)
- sys.exit(main())
+ """Example presubmit check script."""
+
+ import argparse
+ import logging
+ import os
+ from pathlib import Path
+ import re
+ import sys
+ from typing import Optional
+
+ try:
+ import pw_cli.log
+ except ImportError:
+ print("ERROR: Activate the environment before running presubmits!", file=sys.stderr)
+ sys.exit(2)
+
+ import pw_presubmit
+ from pw_presubmit import (
+ build,
+ cli,
+ cpp_checks,
+ format_code,
+ inclusive_language,
+ python_checks,
+ )
+ from pw_presubmit.presubmit import filter_paths
+ from pw_presubmit.presubmit_context import PresubmitContext
+ from pw_presubmit.install_hook import install_git_hook
+
+ # Set up variables for key project paths.
+ PROJECT_ROOT = Path(os.environ["MY_PROJECT_ROOT"])
+ PIGWEED_ROOT = PROJECT_ROOT / "pigweed"
+
+ # Rerun the build if files with these extensions change.
+ _BUILD_EXTENSIONS = frozenset(
+ [".rst", ".gn", ".gni", *format_code.C_FORMAT.extensions]
+ )
+
+
+ #
+ # Presubmit checks
+ #
+ def release_build(ctx: PresubmitContext):
+ build.gn_gen(ctx, build_type="release")
+ build.ninja(ctx)
+ build.gn_check(ctx) # Run after building to check generated files.
+
+
+ def host_tests(ctx: PresubmitContext):
+ build.gn_gen(ctx, run_host_tests="true")
+ build.ninja(ctx)
+ build.gn_check(ctx)
+
+
+ # Avoid running some checks on certain paths.
+ PATH_EXCLUSIONS = (
+ re.compile(r"^external/"),
+ re.compile(r"^vendor/"),
+ )
+
+
+ # Use the upstream pragma_once check, but apply a different set of path
+ # filters with @filter_paths.
+ @filter_paths(endswith=".h", exclude=PATH_EXCLUSIONS)
+ def pragma_once(ctx: PresubmitContext):
+ cpp_checks.pragma_once(ctx)
+
+
+ #
+ # Presubmit check programs
+ #
+ OTHER = (
+ # Checks not ran by default but that should be available. These might
+ # include tests that are expensive to run or that don't yet pass.
+ build.gn_gen_check,
+ )
+
+ QUICK = (
+ # List some presubmit checks to run
+ pragma_once,
+ host_tests,
+ # Use the upstream formatting checks, with custom path filters applied.
+ format_code.presubmit_checks(exclude=PATH_EXCLUSIONS),
+ # Include the upstream inclusive language check.
+ inclusive_language.presubmit_check,
+ # Include just the lint-related Python checks.
+ python_checks.gn_python_lint.with_filter(exclude=PATH_EXCLUSIONS),
+ )
+
+ FULL = (
+ QUICK, # Add all checks from the 'quick' program
+ release_build,
+ # Use the upstream Python checks, with custom path filters applied.
+ # Checks listed multiple times are only run once.
+ python_checks.gn_python_check.with_filter(exclude=PATH_EXCLUSIONS),
+ )
+
+ PROGRAMS = pw_presubmit.Programs(other=OTHER, quick=QUICK, full=FULL)
+
+
+ #
+ # Allowlist of remote refs for presubmit. If the remote ref being pushed to
+ # matches any of these values (with regex matching), then the presubmits
+ # checks will be run before pushing.
+ #
+ PRE_PUSH_REMOTE_REF_ALLOWLIST = ("refs/for/main",)
+
+
+ def run(install: bool, remote_ref: Optional[str], **presubmit_args) -> int:
+ """Process the --install argument then invoke pw_presubmit."""
+
+ # Install the presubmit Git pre-push hook, if requested.
+ if install:
+ # '$remote_ref' will be replaced by the actual value of the remote ref
+ # at runtime.
+ install_git_hook(
+ "pre-push",
+ [
+ "python",
+ "-m",
+ "tools.presubmit_check",
+ "--base",
+ "HEAD~",
+ "--remote-ref",
+ "$remote_ref",
+ ],
+ )
+ return 0
+
+ # Run the checks if either no remote_ref was passed, or if the remote ref
+ # matches anything in the allowlist.
+ if remote_ref is None or any(
+ re.search(pattern, remote_ref)
+ for pattern in PRE_PUSH_REMOTE_REF_ALLOWLIST
+ ):
+ return cli.run(root=PROJECT_ROOT, **presubmit_args)
+ return 0
+
+
+ def main() -> int:
+ """Run the presubmit checks for this repository."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ cli.add_arguments(parser, PROGRAMS, "quick")
+
+ # Define an option for installing a Git pre-push hook for this script.
+ parser.add_argument(
+ "--install",
+ action="store_true",
+ help="Install the presubmit as a Git pre-push hook and exit.",
+ )
+
+ # Define an optional flag to pass the remote ref into this script, if it
+ # is run as a pre-push hook. The destination variable in the parsed args
+ # will be `remote_ref`, as dashes are replaced with underscores to make
+ # valid variable names.
+ parser.add_argument(
+ "--remote-ref",
+ default=None,
+ nargs="?", # Make optional.
+ help="Remote ref of the push command, for use by the pre-push hook.",
+ )
+
+ return run(**vars(parser.parse_args()))
+
+
+ if __name__ == "__main__":
+ pw_cli.log.install(logging.INFO)
+ sys.exit(main())
---------------------
Code formatting tools
@@ -585,35 +631,35 @@ a formatter or remove/disable a PigWeed supplied one.
.. code-block:: python
- #!/usr/bin/env python
- """Formats files in repository. """
+ #!/usr/bin/env python
+ """Formats files in repository. """
- import logging
- import sys
+ import logging
+ import sys
- import pw_cli.log
- from pw_presubmit import format_code
- from your_project import presubmit_checks
- from your_project import your_check
+ import pw_cli.log
+ from pw_presubmit import format_code
+ from your_project import presubmit_checks
+ from your_project import your_check
- YOUR_CODE_FORMAT = CodeFormat('YourFormat',
- filter=FileFilter(suffix=('.your', )),
- check=your_check.check,
- fix=your_check.fix)
+ YOUR_CODE_FORMAT = CodeFormat('YourFormat',
+ filter=FileFilter(suffix=('.your', )),
+ check=your_check.check,
+ fix=your_check.fix)
- CODE_FORMATS = (*format_code.CODE_FORMATS, YOUR_CODE_FORMAT)
+ CODE_FORMATS = (*format_code.CODE_FORMATS, YOUR_CODE_FORMAT)
- def _run(exclude, **kwargs) -> int:
- """Check and fix formatting for source files in the repo."""
- return format_code.format_paths_in_repo(exclude=exclude,
- code_formats=CODE_FORMATS,
- **kwargs)
+ def _run(exclude, **kwargs) -> int:
+ """Check and fix formatting for source files in the repo."""
+ return format_code.format_paths_in_repo(exclude=exclude,
+ code_formats=CODE_FORMATS,
+ **kwargs)
- def main():
- return _run(**vars(format_code.arguments(git_paths=True).parse_args()))
+ def main():
+ return _run(**vars(format_code.arguments(git_paths=True).parse_args()))
- if __name__ == '__main__':
- pw_cli.log.install(logging.INFO)
- sys.exit(main())
+ if __name__ == '__main__':
+ pw_cli.log.install(logging.INFO)
+ sys.exit(main())
diff --git a/pw_presubmit/py/BUILD.gn b/pw_presubmit/py/BUILD.gn
index 0c29c5152..c00935ddf 100644
--- a/pw_presubmit/py/BUILD.gn
+++ b/pw_presubmit/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_presubmit/__init__.py",
@@ -33,6 +32,8 @@ pw_python_package("py") {
"pw_presubmit/gitmodules.py",
"pw_presubmit/inclusive_language.py",
"pw_presubmit/install_hook.py",
+ "pw_presubmit/javascript_checks.py",
+ "pw_presubmit/json_check.py",
"pw_presubmit/keep_sorted.py",
"pw_presubmit/module_owners.py",
"pw_presubmit/ninja_parser.py",
@@ -40,7 +41,9 @@ pw_python_package("py") {
"pw_presubmit/owners_checks.py",
"pw_presubmit/pigweed_presubmit.py",
"pw_presubmit/presubmit.py",
+ "pw_presubmit/presubmit_context.py",
"pw_presubmit/python_checks.py",
+ "pw_presubmit/rst_format.py",
"pw_presubmit/shell_checks.py",
"pw_presubmit/source_in_build.py",
"pw_presubmit/todo_check.py",
@@ -48,6 +51,8 @@ pw_python_package("py") {
]
tests = [
"bazel_parser_test.py",
+ "context_test.py",
+ "cpp_checks_test.py",
"git_repo_test.py",
"gitmodules_test.py",
"keep_sorted_test.py",
diff --git a/pw_presubmit/py/bazel_parser_test.py b/pw_presubmit/py/bazel_parser_test.py
index 4c7d45e10..6248b866b 100644
--- a/pw_presubmit/py/bazel_parser_test.py
+++ b/pw_presubmit/py/bazel_parser_test.py
@@ -73,9 +73,419 @@ pw_kvs/entry.cc:49:23: error: expected ';' after return statement
^
;
2 errors generated.
+INFO: Elapsed time: 5.662s, Critical Path: 1.01s
+INFO: 12 processes: 12 internal.
+FAILED: Build did NOT complete successfully
+FAILED: Build did NOT complete successfully
"""
-_STOP = 'INFO:\n'
+_REAL_TEST_INPUT_2 = """
+[4,421 / 4,540] 302 / 343 tests; Compiling pw_transfer/client_test.cc; 7s linux-sandbox ... (15 actions running)
+[4,425 / 4,540] 302 / 343 tests; Compiling pw_transfer/client_test.cc; 9s linux-sandbox ... (16 actions, 15 running)
+[4,426 / 4,540] 302 / 343 tests; Compiling pw_transfer/client_test.cc; 10s linux-sandbox ... (16 actions running)
+[4,430 / 4,540] 302 / 343 tests; Compiling pw_rpc/raw/service_nc_test.cc; 7s linux-sandbox ... (14 actions, 13 running)
+[4,439 / 4,542] 304 / 343 tests; Compiling pw_rpc/nanopb/client_server_context_test.cc; 7s linux-sandbox ... (15 actions running)
+[4,443 / 4,542] 304 / 343 tests; Compiling pw_rpc/pwpb/synchronous_call_test.cc; 7s linux-sandbox ... (15 actions running)
+[4,449 / 4,544] 306 / 343 tests; Compiling pw_transfer/transfer.cc; 6s linux-sandbox ... (16 actions, 15 running)
+[4,458 / 4,546] 308 / 343 tests; Compiling pw_rpc/test_helpers_test.cc; 4s linux-sandbox ... (16 actions running)
+[4,460 / 4,547] 309 / 343 tests; Compiling pw_rpc/test_helpers_test.cc; 5s linux-sandbox ... (16 actions running)
+[4,466 / 4,548] 310 / 343 tests; Compiling pw_rpc/test_helpers_test.cc; 6s linux-sandbox ... (16 actions, 15 running)
+[4,469 / 4,549] 311 / 343 tests; Compiling pw_rpc/test_helpers_test.cc; 7s linux-sandbox ... (16 actions running)
+[4,478 / 4,550] 312 / 343 tests; Compiling pw_transfer/client_test.cc; 8s linux-sandbox ... (14 actions, 13 running)
+[4,481 / 4,550] 312 / 343 tests; Compiling pw_transfer/client_test.cc; 9s linux-sandbox ... (16 actions, 15 running)
+[4,486 / 4,551] 313 / 343 tests; Compiling pw_transfer/client_test.cc; 10s linux-sandbox ... (16 actions, 15 running)
+[4,492 / 4,552] 314 / 343 tests; Compiling pw_transfer/integration_test/server.cc; 7s linux-sandbox ... (16 actions, 15 running)
+[4,501 / 4,556] 318 / 343 tests; Compiling pw_transfer/atomic_file_transfer_handler_test.cc; 5s linux-sandbox ... (15 actions running)
+[4,506 / 4,556] 318 / 343 tests; Compiling pw_rpc/pwpb/client_server_context_threaded_test.cc; 5s linux-sandbox ... (16 actions, 15 running)
+[4,513 / 4,557] 319 / 343 tests; Compiling pw_transfer/client.cc; 6s linux-sandbox ... (15 actions, 14 running)
+[4,519 / 4,557] 319 / 343 tests; Compiling pw_rpc/pwpb/client_server_context_threaded_test.cc; 5s linux-sandbox ... (16 actions, 15 running)
+[4,524 / 4,558] 320 / 343 tests; Compiling pw_transfer/integration_test/client.cc; 6s linux-sandbox ... (16 actions running)
+[4,533 / 4,560] 322 / 343 tests; Compiling pw_rpc/pwpb/fake_channel_output_test.cc; 4s linux-sandbox ... (16 actions running)
+[4,537 / 4,560] 322 / 343 tests; Compiling pw_rpc/pwpb/fake_channel_output_test.cc; 5s linux-sandbox ... (14 actions running)
+[4,544 / 4,569] 325 / 343 tests; Compiling pw_rpc/pwpb/fake_channel_output_test.cc; 6s linux-sandbox ... (15 actions running)
+[4,550 / 4,569] 325 / 343 tests; Compiling pw_rpc/pwpb/method_lookup_test.cc; 5s linux-sandbox ... (14 actions, 13 running)
+[4,560 / 4,571] 327 / 343 tests; Compiling pw_rpc/pwpb/server_reader_writer_test.cc; 4s linux-sandbox ... (9 actions, 8 running)
+[4,566 / 4,573] 329 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 4s linux-sandbox ... (7 actions, 6 running)
+[4,567 / 4,573] 329 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 5s linux-sandbox ... (6 actions running)
+[4,567 / 4,573] 329 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 14s linux-sandbox ... (6 actions running)
+[4,567 / 4,573] 330 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 15s linux-sandbox ... (6 actions running)
+[4,568 / 4,573] 330 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 16s linux-sandbox ... (5 actions running)
+[4,568 / 4,573] 330 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 26s linux-sandbox ... (5 actions running)
+[4,568 / 4,573] 330 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 52s linux-sandbox ... (5 actions running)
+[4,569 / 4,573] 331 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 53s linux-sandbox ... (4 actions running)
+[4,570 / 4,573] 332 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 54s linux-sandbox ... (3 actions running)
+[4,570 / 4,573] 332 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 65s linux-sandbox ... (3 actions running)
+[4,570 / 4,573] 332 / 343 tests; Testing //pw_transfer/integration_test:cross_language_medium_read_test; 83s linux-sandbox ... (3 actions running)
+FAIL: //pw_transfer/integration_test:cross_language_medium_read_test (see /b/s/w/ir/cache/bazel/_bazel_swarming/823a25200ba977576a072121498f22ec/execroot/pigweed/bazel-out/k8-fastbuild/testlogs/pw_transfer/integration_test/cross_language_medium_read_test/test.log)
+INFO: From Testing //pw_transfer/integration_test:cross_language_medium_read_test:
+stdout (/b/s/w/ir/cache/bazel/_bazel_swarming/823a25200ba977576a072121498f22ec/execroot/pigweed/bazel-out/_tmp/actions/stdout-2115) 1131999 exceeds maximum size of --experimental_ui_max_stdouterr_bytes=1048576 bytes; skipping
+[4,572 / 4,573] 334 / 343 tests, 1 failed; Testing //pw_transfer/integration_test:expected_errors_test; 84s linux-sandbox
+[4,572 / 4,573] 334 / 343 tests, 1 failed; Testing //pw_transfer/integration_test:expected_errors_test; 94s linux-sandbox
+[4,572 / 4,573] 334 / 343 tests, 1 failed; Testing //pw_transfer/integration_test:expected_errors_test; 124s linux-sandbox
+[4,572 / 4,573] 334 / 343 tests, 1 failed; Testing //pw_transfer/integration_test:expected_errors_test; 168s linux-sandbox
+INFO: Elapsed time: 507.459s, Critical Path: 181.66s
+INFO: 1206 processes: 1235 linux-sandbox, 4 worker.
+INFO: Build completed, 1 test FAILED, 1206 total actions
+//pw_allocator:block_test (cached) PASSED in 0.1s
+//pw_allocator:freelist_heap_test (cached) PASSED in 0.1s
+//pw_allocator:freelist_test (cached) PASSED in 0.0s
+//pw_analog:analog_input_test (cached) PASSED in 0.1s
+//pw_analog:microvolt_input_test (cached) PASSED in 0.0s
+//pw_arduino_build/py:builder_test (cached) PASSED in 0.7s
+//pw_arduino_build/py:file_operations_test (cached) PASSED in 1.0s
+//pw_assert:assert_backend_compile_test (cached) PASSED in 0.2s
+//pw_assert:assert_facade_test (cached) PASSED in 0.1s
+//pw_async:fake_dispatcher_test (cached) PASSED in 0.1s
+//pw_async_basic:dispatcher_test (cached) PASSED in 0.1s
+//pw_async_basic:fake_dispatcher_fixture_test (cached) PASSED in 0.1s
+//pw_async_basic:heap_dispatcher_test (cached) PASSED in 0.1s
+//pw_base64:base64_test (cached) PASSED in 0.2s
+//pw_blob_store:blob_store_chunk_write_test (cached) PASSED in 0.8s
+//pw_blob_store:blob_store_deferred_write_test (cached) PASSED in 0.5s
+//pw_blob_store:blob_store_test (cached) PASSED in 1.0s
+//pw_blob_store:flat_file_system_entry_test (cached) PASSED in 0.1s
+//pw_bluetooth:address_test (cached) PASSED in 0.3s
+//pw_bluetooth:api_test (cached) PASSED in 0.4s
+//pw_bluetooth:result_test (cached) PASSED in 0.2s
+//pw_bluetooth:uuid_test (cached) PASSED in 0.1s
+//pw_bluetooth_hci:packet_test (cached) PASSED in 0.1s
+//pw_bluetooth_hci:uart_transport_fuzzer (cached) PASSED in 0.2s
+//pw_bluetooth_hci:uart_transport_test (cached) PASSED in 0.1s
+//pw_bluetooth_profiles:device_info_service_test (cached) PASSED in 0.0s
+//pw_build:file_prefix_map_test (cached) PASSED in 0.1s
+//pw_build_info:build_id_test (cached) PASSED in 0.1s
+//pw_bytes:array_test (cached) PASSED in 0.1s
+//pw_bytes:bit_test (cached) PASSED in 0.1s
+//pw_bytes:byte_builder_test (cached) PASSED in 0.0s
+//pw_bytes:endian_test (cached) PASSED in 0.1s
+//pw_bytes:units_test (cached) PASSED in 0.1s
+//pw_checksum:crc16_ccitt_test (cached) PASSED in 0.1s
+//pw_checksum:crc32_test (cached) PASSED in 0.1s
+//pw_chrono:simulated_system_clock_test (cached) PASSED in 0.1s
+//pw_chrono:system_clock_facade_test (cached) PASSED in 0.1s
+//pw_chrono:system_timer_facade_test (cached) PASSED in 0.7s
+//pw_cli/py:envparse_test (cached) PASSED in 0.8s
+//pw_cli/py:plugins_test (cached) PASSED in 0.7s
+//pw_containers:algorithm_test (cached) PASSED in 0.1s
+//pw_containers:filtered_view_test (cached) PASSED in 0.1s
+//pw_containers:flat_map_test (cached) PASSED in 0.1s
+//pw_containers:inline_deque_test (cached) PASSED in 0.3s
+//pw_containers:inline_queue_test (cached) PASSED in 0.1s
+//pw_containers:intrusive_list_test (cached) PASSED in 0.1s
+//pw_containers:raw_storage_test (cached) PASSED in 0.1s
+//pw_containers:to_array_test (cached) PASSED in 0.1s
+//pw_containers:vector_test (cached) PASSED in 0.1s
+//pw_containers:wrapped_iterator_test (cached) PASSED in 0.2s
+//pw_cpu_exception_cortex_m/py:exception_analyzer_test (cached) PASSED in 1.0s
+//pw_crypto:ecdsa_test (cached) PASSED in 0.1s
+//pw_crypto:sha256_mock_test (cached) PASSED in 0.1s
+//pw_crypto:sha256_test (cached) PASSED in 0.0s
+//pw_digital_io:digital_io_test (cached) PASSED in 0.2s
+//pw_file:flat_file_system_test (cached) PASSED in 0.2s
+//pw_function:function_test (cached) PASSED in 0.1s
+//pw_function:pointer_test (cached) PASSED in 0.3s
+//pw_function:scope_guard_test (cached) PASSED in 0.1s
+//pw_fuzzer/examples/fuzztest:metrics_fuzztest (cached) PASSED in 0.1s
+//pw_fuzzer/examples/fuzztest:metrics_unittest (cached) PASSED in 0.2s
+//pw_fuzzer/examples/libfuzzer:toy_fuzzer (cached) PASSED in 0.2s
+//pw_hdlc:decoder_test (cached) PASSED in 0.3s
+//pw_hdlc:encoded_size_test (cached) PASSED in 0.2s
+//pw_hdlc:encoder_test (cached) PASSED in 0.1s
+//pw_hdlc:rpc_channel_test (cached) PASSED in 0.1s
+//pw_hdlc:wire_packet_parser_test (cached) PASSED in 0.2s
+//pw_hdlc/java/test/dev/pigweed/pw_hdlc:DecoderTest (cached) PASSED in 0.5s
+//pw_hdlc/java/test/dev/pigweed/pw_hdlc:EncoderTest (cached) PASSED in 0.6s
+//pw_hdlc/java/test/dev/pigweed/pw_hdlc:FrameTest (cached) PASSED in 0.9s
+//pw_hex_dump:hex_dump_test (cached) PASSED in 0.1s
+//pw_i2c:address_test (cached) PASSED in 0.1s
+//pw_i2c:device_test (cached) PASSED in 0.1s
+//pw_i2c:i2c_service_test (cached) PASSED in 0.0s
+//pw_i2c:initiator_mock_test (cached) PASSED in 0.1s
+//pw_i2c:register_device_test (cached) PASSED in 0.1s
+//pw_i2c_linux:initiator_test (cached) PASSED in 0.1s
+//pw_intrusive_ptr:intrusive_ptr_test (cached) PASSED in 0.0s
+//pw_intrusive_ptr:recyclable_test (cached) PASSED in 0.1s
+//pw_kvs:alignment_test (cached) PASSED in 0.1s
+//pw_kvs:checksum_test (cached) PASSED in 0.2s
+//pw_kvs:converts_to_span_test (cached) PASSED in 0.1s
+//pw_kvs:entry_cache_test (cached) PASSED in 0.1s
+//pw_kvs:entry_test (cached) PASSED in 0.2s
+//pw_kvs:fake_flash_test_key_value_store_test (cached) PASSED in 0.1s
+//pw_kvs:flash_partition_16_alignment_test (cached) PASSED in 2.4s
+//pw_kvs:flash_partition_1_alignment_test (cached) PASSED in 32.1s
+//pw_kvs:flash_partition_256_alignment_test (cached) PASSED in 0.5s
+//pw_kvs:flash_partition_256_write_size_test (cached) PASSED in 0.4s
+//pw_kvs:flash_partition_64_alignment_test (cached) PASSED in 1.0s
+//pw_kvs:flash_partition_stream_test (cached) PASSED in 0.3s
+//pw_kvs:key_test (cached) PASSED in 0.1s
+//pw_kvs:key_value_store_16_alignment_flash_test (cached) PASSED in 0.1s
+//pw_kvs:key_value_store_1_alignment_flash_test (cached) PASSED in 0.3s
+//pw_kvs:key_value_store_256_alignment_flash_test (cached) PASSED in 0.1s
+//pw_kvs:key_value_store_64_alignment_flash_test (cached) PASSED in 0.2s
+//pw_kvs:key_value_store_binary_format_test (cached) PASSED in 0.1s
+//pw_kvs:key_value_store_map_test (cached) PASSED in 6.4s
+//pw_kvs:key_value_store_put_test (cached) PASSED in 0.2s
+//pw_kvs:key_value_store_test (cached) PASSED in 0.3s
+//pw_kvs:key_value_store_wear_test (cached) PASSED in 0.5s
+//pw_kvs:sectors_test (cached) PASSED in 0.1s
+//pw_libc:memset_test (cached) PASSED in 0.1s
+//pw_log:glog_adapter_test (cached) PASSED in 0.1s
+//pw_log:proto_utils_test (cached) PASSED in 0.0s
+//pw_log:test (cached) PASSED in 0.2s
+//pw_log_null:test (cached) PASSED in 0.0s
+//pw_log_rpc:log_filter_service_test (cached) PASSED in 0.1s
+//pw_log_rpc:log_filter_test (cached) PASSED in 0.0s
+//pw_log_rpc:log_service_test (cached) PASSED in 0.0s
+//pw_log_rpc:rpc_log_drain_test (cached) PASSED in 0.2s
+//pw_log_tokenized:log_tokenized_test (cached) PASSED in 0.1s
+//pw_log_tokenized:metadata_test (cached) PASSED in 0.2s
+//pw_log_tokenized/py:format_string_test (cached) PASSED in 0.9s
+//pw_log_tokenized/py:metadata_test (cached) PASSED in 0.6s
+//pw_metric:global_test (cached) PASSED in 0.6s
+//pw_metric:metric_service_pwpb_test (cached) PASSED in 0.1s
+//pw_metric:metric_test (cached) PASSED in 0.2s
+//pw_multisink:multisink_test (cached) PASSED in 0.2s
+//pw_multisink:stl_multisink_threaded_test (cached) PASSED in 0.1s
+//pw_perf_test:chrono_timer_test (cached) PASSED in 0.1s
+//pw_perf_test:perf_test_test (cached) PASSED in 0.1s
+//pw_perf_test:state_test (cached) PASSED in 0.2s
+//pw_perf_test:timer_test (cached) PASSED in 0.1s
+//pw_persistent_ram:flat_file_system_entry_test (cached) PASSED in 0.1s
+//pw_persistent_ram:persistent_buffer_test (cached) PASSED in 0.3s
+//pw_persistent_ram:persistent_test (cached) PASSED in 0.1s
+//pw_polyfill:test (cached) PASSED in 0.1s
+//pw_preprocessor:arguments_test (cached) PASSED in 0.2s
+//pw_preprocessor:boolean_test (cached) PASSED in 0.2s
+//pw_preprocessor:compiler_test (cached) PASSED in 0.2s
+//pw_preprocessor:concat_test (cached) PASSED in 0.1s
+//pw_preprocessor:util_test (cached) PASSED in 0.1s
+//pw_protobuf:codegen_decoder_test (cached) PASSED in 0.1s
+//pw_protobuf:codegen_encoder_test (cached) PASSED in 0.2s
+//pw_protobuf:decoder_fuzz_test (cached) PASSED in 0.2s
+//pw_protobuf:decoder_test (cached) PASSED in 0.1s
+//pw_protobuf:encoder_fuzz_test (cached) PASSED in 0.3s
+//pw_protobuf:encoder_test (cached) PASSED in 0.1s
+//pw_protobuf:find_test (cached) PASSED in 0.1s
+//pw_protobuf:map_utils_test (cached) PASSED in 0.1s
+//pw_protobuf:message_test (cached) PASSED in 0.1s
+//pw_protobuf:serialized_size_test (cached) PASSED in 0.2s
+//pw_protobuf:stream_decoder_test (cached) PASSED in 0.2s
+//pw_protobuf_compiler:nested_packages_test (cached) PASSED in 0.1s
+//pw_protobuf_compiler/py:compiled_protos_test (cached) PASSED in 0.4s
+//pw_random:xor_shift_test (cached) PASSED in 0.2s
+//pw_result:expected_test (cached) PASSED in 0.1s
+//pw_result:result_test (cached) PASSED in 0.1s
+//pw_result:statusor_test (cached) PASSED in 0.2s
+//pw_ring_buffer:prefixed_entry_ring_buffer_test (cached) PASSED in 0.2s
+//pw_router:static_router_test (cached) PASSED in 0.2s
+//pw_rpc:call_test (cached) PASSED in 0.1s
+//pw_rpc:callback_test (cached) PASSED in 0.3s
+//pw_rpc:channel_test (cached) PASSED in 0.2s
+//pw_rpc:client_server_test (cached) PASSED in 0.1s
+//pw_rpc:fake_channel_output_test (cached) PASSED in 0.1s
+//pw_rpc:method_test (cached) PASSED in 0.1s
+//pw_rpc:packet_meta_test (cached) PASSED in 0.1s
+//pw_rpc:packet_test (cached) PASSED in 0.0s
+//pw_rpc:server_test (cached) PASSED in 0.1s
+//pw_rpc:service_test (cached) PASSED in 0.1s
+//pw_rpc:test_helpers_test (cached) PASSED in 0.2s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:ClientTest (cached) PASSED in 1.5s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:EndpointTest (cached) PASSED in 1.5s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:FutureCallTest (cached) PASSED in 1.5s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:IdsTest (cached) PASSED in 0.7s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:PacketsTest (cached) PASSED in 0.6s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:ServiceTest (cached) PASSED in 0.6s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:StreamObserverCallTest (cached) PASSED in 1.5s
+//pw_rpc/java/test/dev/pigweed/pw_rpc:StreamObserverMethodClientTest (cached) PASSED in 1.6s
+//pw_rpc/nanopb:callback_test (cached) PASSED in 0.3s
+//pw_rpc/nanopb:client_call_test (cached) PASSED in 0.1s
+//pw_rpc/nanopb:client_reader_writer_test (cached) PASSED in 0.1s
+//pw_rpc/nanopb:client_server_context_test (cached) PASSED in 0.1s
+//pw_rpc/nanopb:client_server_context_threaded_test (cached) PASSED in 0.1s
+//pw_rpc/nanopb:codegen_test (cached) PASSED in 0.1s
+//pw_rpc/nanopb:fake_channel_output_test (cached) PASSED in 0.2s
+//pw_rpc/nanopb:method_info_test (cached) PASSED in 0.2s
+//pw_rpc/nanopb:method_lookup_test (cached) PASSED in 0.2s
+//pw_rpc/nanopb:method_test (cached) PASSED in 0.1s
+//pw_rpc/nanopb:method_union_test (cached) PASSED in 0.2s
+//pw_rpc/nanopb:serde_test (cached) PASSED in 0.2s
+//pw_rpc/nanopb:server_callback_test (cached) PASSED in 0.2s
+//pw_rpc/nanopb:server_reader_writer_test (cached) PASSED in 0.0s
+//pw_rpc/nanopb:stub_generation_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:client_call_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:client_reader_writer_test (cached) PASSED in 0.2s
+//pw_rpc/pwpb:client_server_context_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:client_server_context_threaded_test (cached) PASSED in 0.0s
+//pw_rpc/pwpb:codegen_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:echo_service_test (cached) PASSED in 0.0s
+//pw_rpc/pwpb:fake_channel_output_test (cached) PASSED in 0.2s
+//pw_rpc/pwpb:method_info_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:method_lookup_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:method_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:method_union_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:serde_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:server_callback_test (cached) PASSED in 0.2s
+//pw_rpc/pwpb:server_reader_writer_test (cached) PASSED in 0.1s
+//pw_rpc/pwpb:stub_generation_test (cached) PASSED in 0.2s
+//pw_rpc/raw:client_reader_writer_test (cached) PASSED in 0.2s
+//pw_rpc/raw:client_test (cached) PASSED in 0.1s
+//pw_rpc/raw:codegen_test (cached) PASSED in 0.2s
+//pw_rpc/raw:method_info_test (cached) PASSED in 0.1s
+//pw_rpc/raw:method_test (cached) PASSED in 0.1s
+//pw_rpc/raw:method_union_test (cached) PASSED in 0.1s
+//pw_rpc/raw:server_reader_writer_test (cached) PASSED in 0.2s
+//pw_rpc/raw:service_nc_test (cached) PASSED in 0.1s
+//pw_rpc/raw:stub_generation_test (cached) PASSED in 0.1s
+//pw_rpc_transport:egress_ingress_test (cached) PASSED in 0.1s
+//pw_rpc_transport:hdlc_framing_test (cached) PASSED in 0.1s
+//pw_rpc_transport:local_rpc_egress_test (cached) PASSED in 0.1s
+//pw_rpc_transport:packet_buffer_queue_test (cached) PASSED in 0.1s
+//pw_rpc_transport:rpc_integration_test (cached) PASSED in 0.2s
+//pw_rpc_transport:simple_framing_test (cached) PASSED in 0.1s
+//pw_rpc_transport:socket_rpc_transport_test (cached) PASSED in 0.2s
+//pw_rpc_transport:stream_rpc_dispatcher_test (cached) PASSED in 0.1s
+//pw_snapshot:cpp_compile_test (cached) PASSED in 0.1s
+//pw_snapshot:uuid_test (cached) PASSED in 0.2s
+//pw_span:compatibility_test (cached) PASSED in 0.1s
+//pw_span:pw_span_test (cached) PASSED in 0.1s
+//pw_spi:initiator_mock_test (cached) PASSED in 0.2s
+//pw_spi:spi_test (cached) PASSED in 0.0s
+//pw_status:status_test (cached) PASSED in 0.2s
+//pw_status:status_with_size_test (cached) PASSED in 0.2s
+//pw_status:try_test (cached) PASSED in 0.2s
+//pw_status/rust:pw_status_doc_test (cached) PASSED in 1.0s
+//pw_status/rust:pw_status_test (cached) PASSED in 0.0s
+//pw_stream:interval_reader_test (cached) PASSED in 0.2s
+//pw_stream:memory_stream_test (cached) PASSED in 0.1s
+//pw_stream:mpsc_stream_test (cached) PASSED in 0.1s
+//pw_stream:null_stream_test (cached) PASSED in 0.2s
+//pw_stream:seek_test (cached) PASSED in 0.1s
+//pw_stream:socket_stream_test (cached) PASSED in 0.1s
+//pw_stream:std_file_stream_test (cached) PASSED in 0.2s
+//pw_stream:stream_test (cached) PASSED in 0.2s
+//pw_stream/rust:pw_stream_doc_test (cached) PASSED in 0.8s
+//pw_stream/rust:pw_stream_test (cached) PASSED in 0.0s
+//pw_stream_uart_linux:stream_test (cached) PASSED in 0.1s
+//pw_string:format_test (cached) PASSED in 0.1s
+//pw_string:string_builder_test (cached) PASSED in 0.1s
+//pw_string:string_test (cached) PASSED in 0.1s
+//pw_string:to_string_test (cached) PASSED in 0.1s
+//pw_string:type_to_string_test (cached) PASSED in 0.1s
+//pw_string:util_test (cached) PASSED in 0.1s
+//pw_symbolizer/py:symbolizer_test (cached) PASSED in 0.6s
+//pw_sync:binary_semaphore_facade_test (cached) PASSED in 0.3s
+//pw_sync:borrow_test (cached) PASSED in 0.2s
+//pw_sync:counting_semaphore_facade_test (cached) PASSED in 0.2s
+//pw_sync:inline_borrowable_test (cached) PASSED in 0.3s
+//pw_sync:interrupt_spin_lock_facade_test (cached) PASSED in 0.2s
+//pw_sync:lock_traits_test (cached) PASSED in 0.2s
+//pw_sync:mutex_facade_test (cached) PASSED in 0.1s
+//pw_sync:recursive_mutex_facade_test (cached) PASSED in 0.4s
+//pw_sync:thread_notification_facade_test (cached) PASSED in 0.1s
+//pw_sync:timed_mutex_facade_test (cached) PASSED in 0.1s
+//pw_sync:timed_thread_notification_facade_test (cached) PASSED in 0.2s
+//pw_thread:id_facade_test (cached) PASSED in 0.2s
+//pw_thread:sleep_facade_test (cached) PASSED in 0.3s
+//pw_thread:test_thread_context_facade_test (cached) PASSED in 0.2s
+//pw_thread:thread_info_test (cached) PASSED in 0.1s
+//pw_thread:thread_snapshot_service_test (cached) PASSED in 0.1s
+//pw_thread:yield_facade_test (cached) PASSED in 0.3s
+//pw_thread_stl:thread_backend_test (cached) PASSED in 0.2s
+//pw_tokenizer:argument_types_test (cached) PASSED in 0.1s
+//pw_tokenizer:base64_test (cached) PASSED in 0.1s
+//pw_tokenizer:decode_test (cached) PASSED in 0.2s
+//pw_tokenizer:detokenize_fuzzer (cached) PASSED in 0.3s
+//pw_tokenizer:detokenize_test (cached) PASSED in 0.2s
+//pw_tokenizer:encode_args_test (cached) PASSED in 0.1s
+//pw_tokenizer:hash_test (cached) PASSED in 0.1s
+//pw_tokenizer:simple_tokenize_test (cached) PASSED in 0.1s
+//pw_tokenizer:token_database_test (cached) PASSED in 0.1s
+//pw_tokenizer:tokenize_test (cached) PASSED in 0.1s
+//pw_tokenizer/rust:pw_tokenizer_core_doc_test (cached) PASSED in 1.0s
+//pw_tokenizer/rust:pw_tokenizer_core_test (cached) PASSED in 0.2s
+//pw_tokenizer/rust:pw_tokenizer_doc_test (cached) PASSED in 0.6s
+//pw_tokenizer/rust:pw_tokenizer_macro_test (cached) PASSED in 0.1s
+//pw_tokenizer/rust:pw_tokenizer_printf_doc_test (cached) PASSED in 1.0s
+//pw_tokenizer/rust:pw_tokenizer_printf_test (cached) PASSED in 0.4s
+//pw_tokenizer/rust:pw_tokenizer_test (cached) PASSED in 0.1s
+//pw_toolchain:no_destructor_test (cached) PASSED in 0.1s
+//pw_trace:trace_backend_compile_test (cached) PASSED in 0.1s
+//pw_trace:trace_facade_test (cached) PASSED in 0.1s
+//pw_trace:trace_zero_facade_test (cached) PASSED in 0.2s
+//pw_transfer:atomic_file_transfer_handler_test (cached) PASSED in 0.1s
+//pw_transfer:chunk_test (cached) PASSED in 0.1s
+//pw_transfer:client_test (cached) PASSED in 0.6s
+//pw_transfer:handler_test (cached) PASSED in 0.2s
+//pw_transfer:transfer_test (cached) PASSED in 0.2s
+//pw_transfer:transfer_thread_test (cached) PASSED in 0.0s
+//pw_transfer/java/test/dev/pigweed/pw_transfer:TransferClientTest (cached) PASSED in 5.4s
+//pw_unit_test:framework_test (cached) PASSED in 0.1s
+//pw_unit_test:static_library_support_test (cached) PASSED in 0.1s
+//pw_varint:stream_test (cached) PASSED in 0.2s
+//pw_varint:varint_test (cached) PASSED in 0.2s
+//pw_varint/rust:pw_varint_doc_test (cached) PASSED in 1.1s
+//pw_varint/rust:pw_varint_test (cached) PASSED in 0.1s
+//pw_work_queue:stl_work_queue_test (cached) PASSED in 0.2s
+//pw_cpu_exception_cortex_m:cpu_exception_entry_test SKIPPED
+//pw_cpu_exception_cortex_m:util_test SKIPPED
+//pw_digital_io_mcuxpresso:mimxrt595_test SKIPPED
+//pw_i2c_mcuxpresso:initiator_test SKIPPED
+//pw_spi_mcuxpresso:flexspi_test SKIPPED
+//pw_spi_mcuxpresso:spi_test SKIPPED
+//pw_stream_shmem_mcuxpresso:stream_test SKIPPED
+//pw_stream_uart_mcuxpresso:stream_test SKIPPED
+//pw_console/py:command_runner_test PASSED in 5.3s
+//pw_console/py:console_app_test PASSED in 5.0s
+//pw_console/py:console_prefs_test PASSED in 4.5s
+//pw_console/py:help_window_test PASSED in 2.8s
+//pw_console/py:log_filter_test PASSED in 4.2s
+//pw_console/py:log_store_test PASSED in 4.1s
+//pw_console/py:log_view_test PASSED in 4.4s
+//pw_console/py:repl_pane_test PASSED in 5.2s
+//pw_console/py:table_test PASSED in 3.0s
+//pw_console/py:text_formatting_test PASSED in 3.5s
+//pw_console/py:window_manager_test PASSED in 5.9s
+//pw_hdlc/py:decode_test PASSED in 0.6s
+//pw_hdlc/py:encode_test PASSED in 0.5s
+//pw_hdlc/py:rpc_test PASSED in 1.5s
+//pw_rpc/nanopb:synchronous_call_test PASSED in 0.1s
+//pw_rpc/pwpb:synchronous_call_test PASSED in 0.1s
+//pw_rpc/py:callback_client_test PASSED in 3.2s
+//pw_rpc/py:client_test PASSED in 2.1s
+//pw_rpc/py:descriptors_test PASSED in 1.1s
+//pw_rpc/py:ids_test PASSED in 1.1s
+//pw_rpc/py:packets_test PASSED in 1.2s
+//pw_rpc/raw:synchronous_call_test PASSED in 0.2s
+//pw_tokenizer/py:decode_test PASSED in 0.9s
+//pw_tokenizer/py:detokenize_test PASSED in 0.8s
+//pw_tokenizer/py:elf_reader_test PASSED in 1.1s
+//pw_tokenizer/py:encode_test PASSED in 0.9s
+//pw_tokenizer/py:tokens_test PASSED in 0.9s
+//pw_transfer/integration_test:cross_language_medium_write_test PASSED in 82.6s
+//pw_transfer/integration_test:cross_language_small_test PASSED in 51.0s
+//pw_transfer/integration_test:expected_errors_test PASSED in 167.6s
+//pw_transfer/integration_test:legacy_binaries_test PASSED in 53.2s
+//pw_transfer/integration_test:multi_transfer_test PASSED in 13.1s
+//pw_transfer/integration_test:proxy_test PASSED in 2.4s
+//pw_transfer/py:transfer_test PASSED in 1.4s
+//pw_transfer/integration_test:cross_language_medium_read_test FAILED in 82.1s
+/b/s/w/ir/cache/bazel/_bazel_swarming/823a25200ba977576a072121498f22ec/execroot/pigweed/bazel-out/k8-fastbuild/testlogs/pw_transfer/integration_test/cross_language_medium_read_test/test.log
+
+Executed 35 out of 343 tests: 334 tests pass, 1 fails locally and 8 were skipped.
+There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
+"""
+
+_REAL_TEST_SUMMARY_2 = """
+FAIL: //pw_transfer/integration_test:cross_language_medium_read_test (see /b/s/w/ir/cache/bazel/_bazel_swarming/823a25200ba977576a072121498f22ec/execroot/pigweed/bazel-out/k8-fastbuild/testlogs/pw_transfer/integration_test/cross_language_medium_read_test/test.log)
+INFO: From Testing //pw_transfer/integration_test:cross_language_medium_read_test:
+stdout (/b/s/w/ir/cache/bazel/_bazel_swarming/823a25200ba977576a072121498f22ec/execroot/pigweed/bazel-out/_tmp/actions/stdout-2115) 1131999 exceeds maximum size of --experimental_ui_max_stdouterr_bytes=1048576 bytes; skipping
+INFO: Elapsed time: 507.459s, Critical Path: 181.66s
+INFO: 1206 processes: 1235 linux-sandbox, 4 worker.
+INFO: Build completed, 1 test FAILED, 1206 total actions
+//pw_transfer/integration_test:cross_language_medium_read_test FAILED in 82.1s
+/b/s/w/ir/cache/bazel/_bazel_swarming/823a25200ba977576a072121498f22ec/execroot/pigweed/bazel-out/k8-fastbuild/testlogs/pw_transfer/integration_test/cross_language_medium_read_test/test.log
+Executed 35 out of 343 tests: 334 tests pass, 1 fails locally and 8 were skipped.
+"""
# pylint: disable=attribute-defined-outside-init
@@ -94,24 +504,23 @@ class TestBazelParser(unittest.TestCase):
def test_simple(self) -> None:
error = 'ERROR: abc\nerror 1\nerror2\n'
- self._run('[0/10] foo\n[1/10] bar\n' + error + _STOP)
+ self._run('[0/10] foo\n[1/10] bar\n' + error)
self.assertEqual(error.strip(), self.output.strip())
def test_path(self) -> None:
error_in = 'ERROR: abc\n PATH=... \\\nerror 1\nerror2\n'
error_out = 'ERROR: abc\nerror 1\nerror2\n'
- self._run('[0/10] foo\n[1/10] bar\n' + error_in + _STOP)
+ self._run('[0/10] foo\n[1/10] bar\n' + error_in)
self.assertEqual(error_out.strip(), self.output.strip())
- def test_unterminated(self) -> None:
- error = 'ERROR: abc\nerror 1\nerror 2\n'
- self._run('[0/10] foo\n[1/10] bar\n' + error)
- self.assertEqual(error.strip(), self.output.strip())
-
def test_failure(self) -> None:
self._run(_REAL_TEST_INPUT)
self.assertEqual(_REAL_TEST_SUMMARY.strip(), self.output.strip())
+ def test_failure_2(self) -> None:
+ self._run(_REAL_TEST_INPUT_2)
+ self.assertEqual(_REAL_TEST_SUMMARY_2.strip(), self.output.strip())
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_presubmit/py/context_test.py b/pw_presubmit/py/context_test.py
new file mode 100644
index 000000000..c1b7f41b1
--- /dev/null
+++ b/pw_presubmit/py/context_test.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for presubmit context classes."""
+
+import unittest
+
+from pw_presubmit import presubmit_context
+
+
+class ContextTest(unittest.TestCase):
+ """Tests for presubmit context classes."""
+
+ def test_presubmitcontext(self): # pylint: disable=no-self-use
+ _ = presubmit_context.PresubmitContext.create_for_testing()
+
+ def test_lucicontext(self):
+ ctx = presubmit_context.LuciContext.create_for_testing(
+ BUILDBUCKET_ID='88123',
+ BUILDBUCKET_NAME='project:bucket.dev.ci:builder-linux',
+ BUILD_NUMBER='12',
+ SWARMING_SERVER='https://chrome-swarming.appspot.com',
+ SWARMING_TASK_ID='123def',
+ )
+
+ self.assertEqual(ctx.buildbucket_id, 88123)
+ self.assertEqual(ctx.build_number, 12)
+ self.assertEqual(ctx.project, 'project')
+ self.assertEqual(ctx.bucket, 'bucket.dev.ci')
+ self.assertEqual(ctx.builder, 'builder-linux')
+ self.assertEqual(
+ ctx.swarming_server,
+ 'https://chrome-swarming.appspot.com',
+ )
+ self.assertEqual(ctx.swarming_task_id, '123def')
+ self.assertEqual(
+ ctx.cas_instance,
+ 'projects/chrome-swarming/instances/default_instance',
+ )
+
+ self.assertFalse(ctx.is_try)
+ self.assertTrue(ctx.is_ci)
+ self.assertTrue(ctx.is_dev)
+
+ def test_lucitrigger(self):
+ trigger = presubmit_context.LuciTrigger.create_for_testing(
+ number=1234,
+ patchset=5,
+ remote='https://pigweed-internal.googlesource.com/pigweed',
+ project='pigweed',
+ branch='main',
+ ref='refs/changes/34/1234/5',
+ gerrit_name='pigweed-internal',
+ submitted=False,
+ )[0]
+
+ self.assertEqual(
+ trigger.gerrit_host,
+ 'https://pigweed-internal-review.googlesource.com',
+ )
+ self.assertEqual(
+ trigger.gerrit_url,
+ 'https://pigweed-internal-review.googlesource.com/c/1234',
+ )
+ self.assertEqual(
+ trigger.gitiles_url,
+ 'https://pigweed-internal.googlesource.com/pigweed/+/'
+ 'refs/changes/34/1234/5',
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/cpp_checks_test.py b/pw_presubmit/py/cpp_checks_test.py
new file mode 100644
index 000000000..77e448227
--- /dev/null
+++ b/pw_presubmit/py/cpp_checks_test.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Tests for cpp_checks."""
+
+import os
+from pathlib import Path
+import unittest
+from unittest.mock import MagicMock, mock_open, patch
+
+from pw_presubmit import cpp_checks
+
+# pylint: disable=attribute-defined-outside-init
+
+
+def _temproot():
+ root = Path(os.environ['_PW_ACTUAL_ENVIRONMENT_ROOT']) / 'temp'
+ root.mkdir(exist_ok=True)
+ return root
+
+
+class TestPragmaOnce(unittest.TestCase):
+ """Test pragma_once check."""
+
+ def _run(self, contents: str) -> None:
+ self.ctx = MagicMock()
+ self.ctx.fail = MagicMock()
+ self.ctx.format_options.filter_paths = lambda x: x
+ path = MagicMock(spec=Path('include/foo.h'))
+
+ def mocked_open_read(*args, **kwargs):
+ return mock_open(read_data=contents)(*args, **kwargs)
+
+ with patch.object(path, 'open', mocked_open_read):
+ self.ctx.paths = [path]
+ cpp_checks.pragma_once(self.ctx)
+
+ def test_empty(self) -> None:
+ self._run('')
+ self.ctx.fail.assert_called()
+
+ def test_simple(self) -> None:
+ self._run('#pragma once')
+ self.ctx.fail.assert_not_called()
+
+ def test_not_first(self) -> None:
+ self._run('1\n2\n3\n#pragma once')
+ self.ctx.fail.assert_not_called()
+
+ def test_different_pragma(self) -> None:
+ self._run('#pragma thrice')
+ self.ctx.fail.assert_called()
+
+
+def guard(path: Path) -> str:
+ return str(path.name).replace('.', '_').upper()
+
+
+class TestIncludeGuard(unittest.TestCase):
+ """Test pragma_once check."""
+
+ def _run(self, contents: str, **kwargs) -> None:
+ self.ctx = MagicMock()
+ self.ctx.format_options.filter_paths = lambda x: x
+ self.ctx.fail = MagicMock()
+ path = MagicMock(spec=Path('abc/def/foo.h'))
+ path.name = 'foo.h'
+
+ def mocked_open_read(*args, **kwargs):
+ return mock_open(read_data=contents)(*args, **kwargs)
+
+ with patch.object(path, 'open', mocked_open_read):
+ self.ctx.paths = [path]
+ check = cpp_checks.include_guard_check(**kwargs)
+ check(self.ctx)
+
+ def test_empty(self) -> None:
+ self._run('')
+ self.ctx.fail.assert_called()
+
+ def test_simple(self) -> None:
+ self._run('#ifndef FOO_H\n#define FOO_H', guard=guard)
+ self.ctx.fail.assert_not_called()
+
+ def test_simple_any(self) -> None:
+ self._run('#ifndef BAR_H\n#define BAR_H')
+ self.ctx.fail.assert_not_called()
+
+ def test_simple_comments(self) -> None:
+ self._run('// abc\n#ifndef BAR_H\n// def\n#define BAR_H\n// ghi')
+ self.ctx.fail.assert_not_called()
+
+ def test_simple_whitespace(self) -> None:
+ self._run(
+ ' // abc\n #ifndef BAR_H\n// def\n'
+ ' #define BAR_H\n// ghi\n #endif'
+ )
+ self.ctx.fail.assert_not_called()
+
+ def test_simple_not_expected(self) -> None:
+ self._run('#ifnef BAR_H\n#define BAR_H', guard=guard)
+ self.ctx.fail.assert_called()
+
+ def test_no_define(self) -> None:
+ self._run('#ifndef FOO_H')
+ self.ctx.fail.assert_called()
+
+ def test_non_matching_define(self) -> None:
+ self._run('#ifndef FOO_H\n#define BAR_H')
+ self.ctx.fail.assert_called()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/gitmodules_test.py b/pw_presubmit/py/gitmodules_test.py
index df7854546..ee873f315 100644
--- a/pw_presubmit/py/gitmodules_test.py
+++ b/pw_presubmit/py/gitmodules_test.py
@@ -60,6 +60,17 @@ class TestGitmodules(unittest.TestCase):
gitmodules.process_gitmodules(self.ctx, config, path)
+ def test_ok_no_submodules(self) -> None:
+ self._run(gitmodules.Config(), '')
+ self.ctx.fail.assert_not_called()
+
+ def test_no_submodules_allowed(self) -> None:
+ self._run(
+ gitmodules.Config(allow_submodules=False),
+ dotgitmodules(url='../foo'),
+ )
+ self.ctx.fail.assert_called()
+
def test_ok_default(self) -> None:
self._run(gitmodules.Config(), dotgitmodules(url='../foo'))
self.ctx.fail.assert_not_called()
diff --git a/pw_presubmit/py/ninja_parser_test.py b/pw_presubmit/py/ninja_parser_test.py
index 4710f5ea3..a810ab59f 100644
--- a/pw_presubmit/py/ninja_parser_test.py
+++ b/pw_presubmit/py/ninja_parser_test.py
@@ -22,10 +22,10 @@ from pw_presubmit import ninja_parser
_STOP = 'ninja: build stopped:\n'
-_REAL_TEST_INPUT = """
+_REAL_BUILD_INPUT = """
[1168/1797] cp ../../pw_software_update/py/dev_sign_test.py python/gen/pw_software_update/py/py.generated_python_package/dev_sign_test.py
[1169/1797] ACTION //pw_presubmit/py:py.lint.mypy(//pw_build/python_toolchain:python)
-FAILED: python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp
+\x1b[31mFAILED: python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp
python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-path ../../pw_presubmit/py --default-toolchain=//pw_toolchain/default:default --current-toolchain=//pw_build/python_toolchain:python --env=MYPY_FORCE_COLOR=1 --touch python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp --capture-output --module mypy --python-virtualenv-config python/gen/pw_env_setup/pigweed_build_venv/venv_metadata.json --python-dep-list-files python/gen/pw_presubmit/py/py.lint.mypy_metadata_path_list.txt -- --pretty --show-error-codes ../../pw_presubmit/py/pw_presubmit/__init__.py ../../pw_presubmit/py/pw_presubmit/build.py ../../pw_presubmit/py/pw_presubmit/cli.py ../../pw_presubmit/py/pw_presubmit/cpp_checks.py ../../pw_presubmit/py/pw_presubmit/format_code.py ../../pw_presubmit/py/pw_presubmit/git_repo.py ../../pw_presubmit/py/pw_presubmit/inclusive_language.py ../../pw_presubmit/py/pw_presubmit/install_hook.py ../../pw_presubmit/py/pw_presubmit/keep_sorted.py ../../pw_presubmit/py/pw_presubmit/ninja_parser.py ../../pw_presubmit/py/pw_presubmit/npm_presubmit.py ../../pw_presubmit/py/pw_presubmit/pigweed_presubmit.py ../../pw_presubmit/py/pw_presubmit/presubmit.py ../../pw_presubmit/py/pw_presubmit/python_checks.py ../../pw_presubmit/py/pw_presubmit/shell_checks.py ../../pw_presubmit/py/pw_presubmit/todo_check.py ../../pw_presubmit/py/pw_presubmit/tools.py ../../pw_presubmit/py/git_repo_test.py ../../pw_presubmit/py/keep_sorted_test.py ../../pw_presubmit/py/ninja_parser_test.py ../../pw_presubmit/py/presubmit_test.py ../../pw_presubmit/py/tools_test.py ../../pw_presubmit/py/setup.py
Requirement already satisfied: pyserial in c:\\b\\s\\w\\ir\\x\\w\\co\\environment\\pigweed-venv\\lib\\site-packages (from pigweed==0.0.13+20230126212203) (3.5)
../../pw_presubmit/py/presubmit_test.py:63: error: Module has no attribute
@@ -54,9 +54,9 @@ ninja: build stopped: subcommand failed.
[FINISHED]
"""
-_REAL_TEST_SUMMARY = """
+_REAL_BUILD_SUMMARY = """
[1169/1797] ACTION //pw_presubmit/py:py.lint.mypy(//pw_build/python_toolchain:python)
-FAILED: python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp
+\x1b[31mFAILED: python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp
python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-path ../../pw_presubmit/py --default-toolchain=//pw_toolchain/default:default --current-toolchain=//pw_build/python_toolchain:python --env=MYPY_FORCE_COLOR=1 --touch python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp --capture-output --module mypy --python-virtualenv-config python/gen/pw_env_setup/pigweed_build_venv/venv_metadata.json --python-dep-list-files python/gen/pw_presubmit/py/py.lint.mypy_metadata_path_list.txt -- --pretty --show-error-codes ../../pw_presubmit/py/pw_presubmit/__init__.py ../../pw_presubmit/py/pw_presubmit/build.py ../../pw_presubmit/py/pw_presubmit/cli.py ../../pw_presubmit/py/pw_presubmit/cpp_checks.py ../../pw_presubmit/py/pw_presubmit/format_code.py ../../pw_presubmit/py/pw_presubmit/git_repo.py ../../pw_presubmit/py/pw_presubmit/inclusive_language.py ../../pw_presubmit/py/pw_presubmit/install_hook.py ../../pw_presubmit/py/pw_presubmit/keep_sorted.py ../../pw_presubmit/py/pw_presubmit/ninja_parser.py ../../pw_presubmit/py/pw_presubmit/npm_presubmit.py ../../pw_presubmit/py/pw_presubmit/pigweed_presubmit.py ../../pw_presubmit/py/pw_presubmit/presubmit.py ../../pw_presubmit/py/pw_presubmit/python_checks.py ../../pw_presubmit/py/pw_presubmit/shell_checks.py ../../pw_presubmit/py/pw_presubmit/todo_check.py ../../pw_presubmit/py/pw_presubmit/tools.py ../../pw_presubmit/py/git_repo_test.py ../../pw_presubmit/py/keep_sorted_test.py ../../pw_presubmit/py/ninja_parser_test.py ../../pw_presubmit/py/presubmit_test.py ../../pw_presubmit/py/tools_test.py ../../pw_presubmit/py/setup.py
../../pw_presubmit/py/presubmit_test.py:63: error: Module has no attribute
"Filter" [attr-defined]
@@ -65,6 +65,88 @@ python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-pa
Found 1 error in 1 file (checked 23 source files)
"""
+_REAL_TEST_INPUT = r"""
+[20278/31188] Finished [ld pw_strict_host_clang_size_optimized/obj/pw_rpc/bin/test_rpc_server] (0.0s)
+[20278/31188] Started [c++ pw_strict_host_clang_size_optimized/obj/pw_rpc/pwpb/fake_channel_output_test.lib.fake_channel_output_test.cc.o]
+[20279/31188] Finished [c++ pw_strict_host_clang_size_optimized/obj/pw_log_rpc/log_service.log_service.cc.o] (4.9s)
+[20279/31188] Started [c++ pw_strict_host_clang_size_optimized/obj/pw_rpc/pwpb/method_info_test.lib.method_info_test.cc.o]
+[20280/31188] Finished [ACTION //pw_rpc:cpp_client_server_integration_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)] (10.2s)
+[ACTION //pw_rpc:cpp_client_server_integration_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)]
+[31mFAILED: [0mpw_strict_host_clang_debug/gen/pw_rpc/cpp_client_server_integration_test.pw_pystamp
+python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-path ../../pw_rpc --default-toolchain=//pw_toolchain/default:default --current-toolchain=//targets/host/pigweed_internal:pw_strict_host_clang_debug --touch pw_strict_host_clang_debug/gen/pw_rpc/cpp_client_server_integration_test.pw_pystamp --capture-output --python-virtualenv-config python/gen/pw_env_setup/pigweed_build_venv/venv_metadata.json --python-dep-list-files pw_strict_host_clang_debug/gen/pw_rpc/cpp_client_server_integration_test_metadata_path_list.txt -- ../../pw_rpc/py/pw_rpc/testing.py --server \<TARGET_FILE\(:test_rpc_server\)\> --client \<TARGET_FILE\(:client_integration_test\)\> -- 30577
+[35m[1mINF[0m Starting pw_rpc server on port 30577
+[35m[1mINF[0m Connecting to pw_rpc client at localhost:30577
+[35m[1mINF[0m [==========] Running all tests.
+[35m[1mINF[0m [ RUN ] RawRpcIntegrationTest.Unary
+[35m[1mINF[0m Test output line
+[35m[1mINF[0m [ OK ] RawRpcIntegrationTest.Unary
+[35m[1mINF[0m [ RUN ] RawRpcIntegrationTest.BidirectionalStreaming
+[35m[1mINF[0m [ OK ] RawRpcIntegrationTest.BidirectionalStreaming
+[35m[1mINF[0m [ RUN ] RawRpcIntegrationTest.OnNextOverwritesItsOwnCall
+[33m[1mWRN[0m RPC client received stream message for an unknown call
+[31m
+
+ ▄████▄ ██▀███ ▄▄▄ ██████ ██░ ██
+ ▒██▀ ▀█ ▓██ ▒ ██▒ ▒████▄ ▒██ ▒ ▓██░ ██▒
+ ▒▓█ 💥 ▄ ▓██ ░▄█ ▒ ▒██ ▀█▄ ░ ▓██▄ ▒██▀▀██░
+ ▒▓▓▄ ▄██▒ ▒██▀▀█▄ ░██▄▄▄▄██ ▒ ██▒ ░▓█ ░██
+ ▒ ▓███▀ ░ ░██▓ ▒██▒ ▓█ ▓██▒ ▒██████▒▒ ░▓█▒░██▓
+ ░ ░▒ ▒ ░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒
+ ░ ▒ ░▒ ░ ▒░ ▒ ▒▒ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░
+ ░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
+ ░ ░ ░ ░ ░ ░ ░ ░ ░
+ ░
+
+[0m
+ Welp, that didn't go as planned. It seems we crashed. Terribly sorry!
+
+[33m CRASH MESSAGE[0m
+
+ Check failed: sem_.try_acquire_for(10s).
+
+[33m CRASH FILE & LINE[0m
+
+ pw_rpc/client_integration_test.cc:52
+
+[33m CRASH FUNCTION[0m
+
+ const char *rpc_test::(anonymous namespace)::StringReceiver::Wait()
+
+[20281/31188] Finished [c++ pw_strict_host_clang_size_optimized/obj/pw_log_rpc/rpc_log_drain.rpc_log_drain.cc.o] (5.1s)
+[20282/31188] Finished [c++ pw_strict_host_clang_size_optimized/obj/pw_log_rpc/log_filter_service_test.lib.log_filter_service_test.cc.o] (5.9s)
+[20283/31188] Finished [c++ pw_strict_host_clang_size_optimized/obj/pw_rpc/fuzz/engine_test.lib.engine_test.cc.o] (2.4s)
+"""
+
+_REAL_TEST_SUMMARY = r"""
+[ACTION //pw_rpc:cpp_client_server_integration_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)]
+[31mFAILED: [0mpw_strict_host_clang_debug/gen/pw_rpc/cpp_client_server_integration_test.pw_pystamp
+python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-path ../../pw_rpc --default-toolchain=//pw_toolchain/default:default --current-toolchain=//targets/host/pigweed_internal:pw_strict_host_clang_debug --touch pw_strict_host_clang_debug/gen/pw_rpc/cpp_client_server_integration_test.pw_pystamp --capture-output --python-virtualenv-config python/gen/pw_env_setup/pigweed_build_venv/venv_metadata.json --python-dep-list-files pw_strict_host_clang_debug/gen/pw_rpc/cpp_client_server_integration_test_metadata_path_list.txt -- ../../pw_rpc/py/pw_rpc/testing.py --server \<TARGET_FILE\(:test_rpc_server\)\> --client \<TARGET_FILE\(:client_integration_test\)\> -- 30577
+[35m[1mINF[0m Starting pw_rpc server on port 30577
+[35m[1mINF[0m Connecting to pw_rpc client at localhost:30577
+[35m[1mINF[0m [==========] Running all tests.
+[35m[1mINF[0m [ RUN ] RawRpcIntegrationTest.OnNextOverwritesItsOwnCall
+[33m[1mWRN[0m RPC client received stream message for an unknown call
+[31m
+ ▄████▄ ██▀███ ▄▄▄ ██████ ██░ ██
+ ▒██▀ ▀█ ▓██ ▒ ██▒ ▒████▄ ▒██ ▒ ▓██░ ██▒
+ ▒▓█ 💥 ▄ ▓██ ░▄█ ▒ ▒██ ▀█▄ ░ ▓██▄ ▒██▀▀██░
+ ▒▓▓▄ ▄██▒ ▒██▀▀█▄ ░██▄▄▄▄██ ▒ ██▒ ░▓█ ░██
+ ▒ ▓███▀ ░ ░██▓ ▒██▒ ▓█ ▓██▒ ▒██████▒▒ ░▓█▒░██▓
+ ░ ░▒ ▒ ░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒
+ ░ ▒ ░▒ ░ ▒░ ▒ ▒▒ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░
+ ░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
+ ░ ░ ░ ░ ░ ░ ░ ░ ░
+ ░
+[0m
+ Welp, that didn't go as planned. It seems we crashed. Terribly sorry!
+[33m CRASH MESSAGE[0m
+ Check failed: sem_.try_acquire_for(10s).
+[33m CRASH FILE & LINE[0m
+ pw_rpc/client_integration_test.cc:52
+[33m CRASH FUNCTION[0m
+ const char *rpc_test::(anonymous namespace)::StringReceiver::Wait()
+"""
+
class TestNinjaParser(unittest.TestCase):
"""Test ninja_parser."""
@@ -93,7 +175,11 @@ class TestNinjaParser(unittest.TestCase):
result = self._run('[0/10] foo\n[1/10] bar\n' + error)
self.assertEqual('', result.strip())
- def test_real(self) -> None:
+ def test_real_build(self) -> None:
+ result = self._run(_REAL_BUILD_INPUT)
+ self.assertEqual(_REAL_BUILD_SUMMARY.strip(), result.strip())
+
+ def test_real_test(self) -> None:
result = self._run(_REAL_TEST_INPUT)
self.assertEqual(_REAL_TEST_SUMMARY.strip(), result.strip())
diff --git a/pw_presubmit/py/presubmit_test.py b/pw_presubmit/py/presubmit_test.py
index 2ecbcde00..befbfa9f6 100755
--- a/pw_presubmit/py/presubmit_test.py
+++ b/pw_presubmit/py/presubmit_test.py
@@ -155,16 +155,5 @@ class ProgramsTest(unittest.TestCase):
# pylint: enable=protected-access
-class ContextTest(unittest.TestCase):
- def test_presubmitcontext(self): # pylint: disable=no-self-use
- _ = presubmit.PresubmitContext.create_for_testing()
-
- def test_lucicontext(self): # pylint: disable=no-self-use
- _ = presubmit.LuciContext.create_for_testing()
-
- def test_lucitrigger(self): # pylint: disable=no-self-use
- _ = presubmit.LuciTrigger.create_for_testing()
-
-
if __name__ == '__main__':
unittest.main()
diff --git a/pw_presubmit/py/pw_presubmit/bazel_parser.py b/pw_presubmit/py/pw_presubmit/bazel_parser.py
index 39f42cdcd..aaf3319c8 100644
--- a/pw_presubmit/py/pw_presubmit/bazel_parser.py
+++ b/pw_presubmit/py/pw_presubmit/bazel_parser.py
@@ -16,12 +16,13 @@
from pathlib import Path
import re
import sys
+from typing import List
def parse_bazel_stdout(bazel_stdout: Path) -> str:
"""Extracts a concise error from a bazel log."""
- seen_error = False
- error_lines = []
+ seen_error: bool = False
+ error_lines: List[str] = []
with bazel_stdout.open() as ins:
for line in ins:
@@ -30,7 +31,7 @@ def parse_bazel_stdout(bazel_stdout: Path) -> str:
# be significant, especially for compiler error messages.
line = line.rstrip()
- if re.match(r'^ERROR:', line):
+ if re.match(r'^(ERROR|FAIL):', line):
seen_error = True
if seen_error:
@@ -48,9 +49,16 @@ def parse_bazel_stdout(bazel_stdout: Path) -> str:
if line.strip().startswith('# Configuration'):
continue
- # An "<ALLCAPS>:" line usually means compiler output is done
- # and useful compiler errors are complete.
- if re.match(r'^(?!ERROR)[A-Z]+:', line):
+ # Ignore passing and skipped tests.
+ if 'PASSED' in line or 'SKIPPED' in line:
+ continue
+
+ # Ignore intermixed lines from other build steps.
+ if re.match(r'\[\s*[\d,]+\s*/\s*[\d,]+\s*\]', line):
+ continue
+
+ if re.match(r'Executed \d+ out of \d+ tests', line):
+ error_lines.append(line)
break
error_lines.append(line)
diff --git a/pw_presubmit/py/pw_presubmit/build.py b/pw_presubmit/py/pw_presubmit/build.py
index 6ef59e880..17ca25f40 100644
--- a/pw_presubmit/py/pw_presubmit/build.py
+++ b/pw_presubmit/py/pw_presubmit/build.py
@@ -13,16 +13,20 @@
# the License.
"""Functions for building code during presubmit checks."""
+import base64
import contextlib
+from dataclasses import dataclass
import itertools
import json
import logging
import os
+import posixpath
from pathlib import Path
import re
import subprocess
from shutil import which
import sys
+import tarfile
from typing import (
Any,
Callable,
@@ -31,6 +35,7 @@ from typing import (
ContextManager,
Dict,
Iterable,
+ Iterator,
List,
Mapping,
Optional,
@@ -40,29 +45,34 @@ from typing import (
Union,
)
-from pw_presubmit import (
- bazel_parser,
+from pw_presubmit.presubmit import (
call,
Check,
FileFilter,
filter_paths,
- format_code,
install_package,
- Iterator,
- log_run,
- ninja_parser,
- plural,
- PresubmitContext,
- PresubmitFailure,
PresubmitResult,
SubStep,
- tools,
+)
+from pw_presubmit.presubmit_context import (
+ PresubmitContext,
+ PresubmitFailure,
+)
+from pw_presubmit import (
+ bazel_parser,
+ format_code,
+ ninja_parser,
+)
+from pw_presubmit.tools import (
+ plural,
+ log_run,
+ format_command,
)
_LOG = logging.getLogger(__name__)
-def bazel(ctx: PresubmitContext, cmd: str, *args: str) -> None:
+def bazel(ctx: PresubmitContext, cmd: str, *args: str, **kwargs) -> None:
"""Invokes Bazel with some common flags set.
Intended for use with bazel build and test. May not work with others.
@@ -77,6 +87,7 @@ def bazel(ctx: PresubmitContext, cmd: str, *args: str) -> None:
keep_going.append('--keep_going')
bazel_stdout = ctx.output_dir / 'bazel.stdout'
+ ctx.output_dir.mkdir(exist_ok=True, parents=True)
try:
with bazel_stdout.open('w') as outs:
call(
@@ -90,8 +101,9 @@ def bazel(ctx: PresubmitContext, cmd: str, *args: str) -> None:
*keep_going,
*args,
cwd=ctx.root,
- env=env_with_clang_vars(),
tee=outs,
+ call_annotation={'build_system': 'bazel'},
+ **kwargs,
)
except PresubmitFailure as exc:
@@ -123,8 +135,8 @@ def _gn_value(value) -> str:
return str(value)
-def gn_args(**kwargs) -> str:
- """Builds a string to use for the --args argument to gn gen.
+def gn_args_list(**kwargs) -> List[str]:
+ """Return a list of formatted strings to use as gn args.
Currently supports bool, int, and str values. In the case of str values,
quotation marks will be added automatically, unless the string already
@@ -136,28 +148,70 @@ def gn_args(**kwargs) -> str:
transformed_args.append(f'{arg}={_gn_value(val)}')
# Use ccache if available for faster repeat presubmit runs.
- if which('ccache'):
+ if which('ccache') and 'pw_command_launcher' not in kwargs:
transformed_args.append('pw_command_launcher="ccache"')
- return '--args=' + ' '.join(transformed_args)
+ return transformed_args
+
+
+def gn_args(**kwargs) -> str:
+ """Builds a string to use for the --args argument to gn gen.
+
+ Currently supports bool, int, and str values. In the case of str values,
+ quotation marks will be added automatically, unless the string already
+ contains one or more double quotation marks, or starts with a { or [
+ character, in which case it will be passed through as-is.
+ """
+ return '--args=' + ' '.join(gn_args_list(**kwargs))
+
+
+def write_gn_args_file(destination_file: Path, **kwargs) -> str:
+ """Write gn args to a file.
+
+ Currently supports bool, int, and str values. In the case of str values,
+ quotation marks will be added automatically, unless the string already
+ contains one or more double quotation marks, or starts with a { or [
+ character, in which case it will be passed through as-is.
+
+ Returns:
+ The contents of the written file.
+ """
+ contents = '\n'.join(gn_args_list(**kwargs))
+ # Add a trailing linebreak
+ contents += '\n'
+ destination_file.parent.mkdir(exist_ok=True, parents=True)
+
+ if (
+ destination_file.is_file()
+ and destination_file.read_text(encoding='utf-8') == contents
+ ):
+ # File is identical, don't re-write.
+ return contents
+
+ destination_file.write_text(contents, encoding='utf-8')
+ return contents
def gn_gen(
ctx: PresubmitContext,
*args: str,
- gn_check: bool = True,
+ gn_check: bool = True, # pylint: disable=redefined-outer-name
gn_fail_on_unused: bool = True,
export_compile_commands: Union[bool, str] = True,
preserve_args_gn: bool = False,
**gn_arguments,
) -> None:
- """Runs gn gen in the specified directory with optional GN args."""
+ """Runs gn gen in the specified directory with optional GN args.
+
+ Runs with --check=system if gn_check=True. Note that this does not cover
+ generated files. Run gn_check() after building to check generated files.
+ """
all_gn_args = dict(gn_arguments)
all_gn_args.update(ctx.override_gn_args)
_LOG.debug('%r', all_gn_args)
args_option = gn_args(**all_gn_args)
- if not preserve_args_gn:
+ if not ctx.dry_run and not preserve_args_gn:
# Delete args.gn to ensure this is a clean build.
args_gn = ctx.output_dir / 'args.gn'
if args_gn.is_file():
@@ -174,26 +228,38 @@ def gn_gen(
'gen',
ctx.output_dir,
'--color=always',
+ *(['--check=system'] if gn_check else []),
*(['--fail-on-unused-args'] if gn_fail_on_unused else []),
*([export_commands_arg] if export_commands_arg else []),
*args,
*([args_option] if all_gn_args else []),
cwd=ctx.root,
+ call_annotation={
+ 'gn_gen_args': all_gn_args,
+ 'gn_gen_args_option': args_option,
+ },
)
- if gn_check:
- call(
- 'gn',
- 'check',
- ctx.output_dir,
- '--check-generated',
- '--check-system',
- cwd=ctx.root,
- )
+
+def gn_check(ctx: PresubmitContext) -> PresubmitResult:
+ """Runs gn check, including on generated and system files."""
+ call(
+ 'gn',
+ 'check',
+ ctx.output_dir,
+ '--check-generated',
+ '--check-system',
+ cwd=ctx.root,
+ )
+ return PresubmitResult.PASS
def ninja(
- ctx: PresubmitContext, *args, save_compdb=True, save_graph=True, **kwargs
+ ctx: PresubmitContext,
+ *args,
+ save_compdb: bool = True,
+ save_graph: bool = True,
+ **kwargs,
) -> None:
"""Runs ninja in the specified directory."""
@@ -206,22 +272,25 @@ def ninja(
keep_going.extend(('-k', '0'))
if save_compdb:
- proc = subprocess.run(
+ proc = log_run(
['ninja', '-C', ctx.output_dir, '-t', 'compdb', *args],
capture_output=True,
**kwargs,
)
- (ctx.output_dir / 'ninja.compdb').write_bytes(proc.stdout)
+ if not ctx.dry_run:
+ (ctx.output_dir / 'ninja.compdb').write_bytes(proc.stdout)
if save_graph:
- proc = subprocess.run(
+ proc = log_run(
['ninja', '-C', ctx.output_dir, '-t', 'graph', *args],
capture_output=True,
**kwargs,
)
- (ctx.output_dir / 'ninja.graph').write_bytes(proc.stdout)
+ if not ctx.dry_run:
+ (ctx.output_dir / 'ninja.graph').write_bytes(proc.stdout)
ninja_stdout = ctx.output_dir / 'ninja.stdout'
+ ctx.output_dir.mkdir(exist_ok=True, parents=True)
try:
with ninja_stdout.open('w') as outs:
if sys.platform == 'win32':
@@ -239,6 +308,7 @@ def ninja(
*args,
tee=outs,
propagate_sigterm=True,
+ call_annotation={'build_system': 'ninja'},
**kwargs,
)
@@ -253,7 +323,7 @@ def ninja(
def get_gn_args(directory: Path) -> List[Dict[str, Dict[str, str]]]:
"""Dumps GN variables to JSON."""
- proc = subprocess.run(
+ proc = log_run(
['gn', 'args', directory, '--list', '--json'], stdout=subprocess.PIPE
)
return json.loads(proc.stdout)
@@ -296,7 +366,7 @@ def _get_paths_from_command(source_dir: Path, *args, **kwargs) -> Set[Path]:
)
_LOG.error(
'[COMMAND] %s\n%s\n%s',
- *tools.format_command(args, kwargs),
+ *format_command(args, kwargs),
process.stderr.decode(),
)
raise PresubmitFailure
@@ -398,7 +468,7 @@ def check_bazel_build_for_files(
if bazel_dirs:
for path in (p for p in files if p.suffix in bazel_extensions_to_check):
if path not in bazel_builds:
- # TODO(b/234883555) Replace this workaround for fuzzers.
+ # TODO: b/234883555 - Replace this workaround for fuzzers.
if 'fuzz' not in str(path):
missing.append(path)
@@ -523,6 +593,37 @@ def test_server(executable: str, output_dir: Path):
proc.terminate() # pylint: disable=used-before-assignment
+@contextlib.contextmanager
+def modified_env(**envvars):
+ """Context manager that sets environment variables.
+
+ Use by assigning values to variable names in the argument list, e.g.:
+ `modified_env(MY_FLAG="some value")`
+
+ Args:
+ envvars: Keyword arguments
+ """
+ saved_env = os.environ.copy()
+ os.environ.update(envvars)
+ try:
+ yield
+ finally:
+ os.environ = saved_env
+
+
+def fuzztest_prng_seed(ctx: PresubmitContext) -> str:
+ """Convert the RNG seed to the format expected by FuzzTest.
+
+ FuzzTest can be configured to use the seed by setting the
+ `FUZZTEST_PRNG_SEED` environment variable to this value.
+
+ Args:
+ ctx: The context that includes a pseudorandom number generator seed.
+ """
+ rng_bytes = ctx.rng_seed.to_bytes(32, sys.byteorder)
+ return base64.urlsafe_b64encode(rng_bytes).decode('ascii').rstrip('=')
+
+
@filter_paths(
file_filter=FileFilter(endswith=('.bzl', '.bazel'), name=('WORKSPACE',))
)
@@ -550,41 +651,102 @@ def gn_gen_check(ctx: PresubmitContext):
gn_gen(ctx, gn_check=True)
+Item = Union[int, str]
+Value = Union[Item, Sequence[Item]]
+ValueCallable = Callable[[PresubmitContext], Value]
+InputItem = Union[Item, ValueCallable]
+InputValue = Union[InputItem, Sequence[InputItem]]
+
+
+def _value(ctx: PresubmitContext, val: InputValue) -> Value:
+ """Process any lambdas inside val
+
+ val is a single value or a list of values, any of which might be a lambda
+ that needs to be resolved. Call each of these lambdas with ctx and replace
+ the lambda with the result. Return the updated top-level structure.
+ """
+
+ # TODO(mohrr) Use typing.TypeGuard instead of "type: ignore"
+
+ if isinstance(val, (str, int)):
+ return val
+ if callable(val):
+ return val(ctx)
+
+ result: List[Item] = []
+ for item in val:
+ if callable(item):
+ call_result = item(ctx)
+ if isinstance(item, (int, str)):
+ result.append(call_result)
+ else: # Sequence.
+ result.extend(call_result) # type: ignore
+ elif isinstance(item, (int, str)):
+ result.append(item)
+ else: # Sequence.
+ result.extend(item)
+ return result
+
+
_CtxMgrLambda = Callable[[PresubmitContext], ContextManager]
_CtxMgrOrLambda = Union[ContextManager, _CtxMgrLambda]
+_INCREMENTAL_COVERAGE_TOOL = 'cloud_client'
+_ABSOLUTE_COVERAGE_TOOL = 'raw_coverage_cloud_uploader'
+
+
+@dataclass(frozen=True)
+class CoverageOptions:
+ """Coverage collection configuration.
+
+ For Google use only. See go/kalypsi-abs and go/kalypsi-inc for documentation
+ of the metadata fields.
+ """
+
+ target_bucket_root: str
+ target_bucket_project: str
+ project: str
+ trace_type: str
+ ref: str
+ source: str
+ owner: str
+ bug_component: str
+ trim_prefix: str = ''
+ add_prefix: str = ''
-class GnGenNinja(Check):
- """Thin wrapper of Check for steps that just call gn/ninja."""
+
+class _NinjaBase(Check):
+ """Thin wrapper of Check for steps that call ninja."""
def __init__(
self,
*args,
packages: Sequence[str] = (),
- gn_args: Optional[ # pylint: disable=redefined-outer-name
- Dict[str, Any]
- ] = None,
ninja_contexts: Sequence[_CtxMgrOrLambda] = (),
ninja_targets: Union[str, Sequence[str], Sequence[Sequence[str]]] = (),
+ coverage_options: Optional[CoverageOptions] = None,
**kwargs,
):
- """Initializes a GnGenNinja object.
+ """Initializes a _NinjaBase object.
Args:
*args: Passed on to superclass.
packages: List of 'pw package' packages to install.
- gn_args: Dict of GN args.
ninja_contexts: List of context managers to apply around ninja
calls.
ninja_targets: Single ninja target, list of Ninja targets, or list
of list of ninja targets. If a list of a list, ninja will be
called multiple times with the same build directory.
+ coverage_options: Coverage collection options (or None, if not
+ collecting coverage data).
**kwargs: Passed on to superclass.
"""
- super().__init__(self._substeps(), *args, **kwargs)
- self.packages: Sequence[str] = packages
- self.gn_args: Dict[str, Any] = gn_args or {}
- self.ninja_contexts: Tuple[_CtxMgrOrLambda, ...] = tuple(ninja_contexts)
+ super().__init__(*args, **kwargs)
+ self._packages: Sequence[str] = packages
+ self._ninja_contexts: Tuple[_CtxMgrOrLambda, ...] = tuple(
+ ninja_contexts
+ )
+ self._coverage_options = coverage_options
if isinstance(ninja_targets, str):
ninja_targets = (ninja_targets,)
@@ -594,14 +756,18 @@ class GnGenNinja(Check):
if ninja_targets and all_strings != any_strings:
raise ValueError(repr(ninja_targets))
- self.ninja_target_lists: Tuple[Tuple[str, ...], ...]
+ self._ninja_target_lists: Tuple[Tuple[str, ...], ...]
if all_strings:
targets: List[str] = []
for target in ninja_targets:
targets.append(target) # type: ignore
- self.ninja_target_lists = (tuple(targets),)
+ self._ninja_target_lists = (tuple(targets),)
else:
- self.ninja_target_lists = tuple(tuple(x) for x in ninja_targets)
+ self._ninja_target_lists = tuple(tuple(x) for x in ninja_targets)
+
+ @property
+ def ninja_targets(self) -> List[str]:
+ return list(itertools.chain(*self._ninja_target_lists))
def _install_package( # pylint: disable=no-self-use
self,
@@ -611,63 +777,95 @@ class GnGenNinja(Check):
install_package(ctx, package)
return PresubmitResult.PASS
- def _gn_gen(self, ctx: PresubmitContext) -> PresubmitResult:
- Item = Union[int, str]
- Value = Union[Item, Sequence[Item]]
- ValueCallable = Callable[[PresubmitContext], Value]
- InputItem = Union[Item, ValueCallable]
- InputValue = Union[InputItem, Sequence[InputItem]]
-
- # TODO(mohrr) Use typing.TypeGuard instead of "type: ignore"
-
- def value(val: InputValue) -> Value:
- if isinstance(val, (str, int)):
- return val
- if callable(val):
- return val(ctx)
-
- result: List[Item] = []
- for item in val:
- if callable(item):
- call_result = item(ctx)
- if isinstance(item, (int, str)):
- result.append(call_result)
- else: # Sequence.
- result.extend(call_result) # type: ignore
- elif isinstance(item, (int, str)):
- result.append(item)
- else: # Sequence.
- result.extend(item)
- return result
-
- args = {k: value(v) for k, v in self.gn_args.items()}
- gn_gen(ctx, **args) # type: ignore
- return PresubmitResult.PASS
-
- def _ninja(
- self, ctx: PresubmitContext, targets: Sequence[str]
- ) -> PresubmitResult:
+ @contextlib.contextmanager
+ def _context(self, ctx: PresubmitContext):
+ """Apply any context managers necessary for building."""
with contextlib.ExitStack() as stack:
- for mgr in self.ninja_contexts:
+ for mgr in self._ninja_contexts:
if isinstance(mgr, contextlib.AbstractContextManager):
stack.enter_context(mgr)
else:
stack.enter_context(mgr(ctx)) # type: ignore
+ yield
+
+ def _ninja(
+ self, ctx: PresubmitContext, targets: Sequence[str]
+ ) -> PresubmitResult:
+ with self._context(ctx):
ninja(ctx, *targets)
return PresubmitResult.PASS
- def _substeps(self) -> Iterator[SubStep]:
- for package in self.packages:
+ def _coverage(
+ self, ctx: PresubmitContext, options: CoverageOptions
+ ) -> PresubmitResult:
+ """Archive and (on LUCI) upload coverage reports."""
+ reports = ctx.output_dir / 'coverage_reports'
+ os.makedirs(reports, exist_ok=True)
+ coverage_jsons: List[Path] = []
+ for path in ctx.output_dir.rglob('coverage_report'):
+ _LOG.debug('exploring %s', path)
+ name = str(path.relative_to(ctx.output_dir))
+ name = name.replace('_', '').replace('/', '_')
+ with tarfile.open(reports / f'{name}.tar.gz', 'w:gz') as tar:
+ tar.add(path, arcname=name, recursive=True)
+ json_path = path / 'json' / 'report.json'
+ if json_path.is_file():
+ _LOG.debug('found json %s', json_path)
+ coverage_jsons.append(json_path)
+
+ if not coverage_jsons:
+ ctx.fail('No coverage json file found')
+ return PresubmitResult.FAIL
+
+ if len(coverage_jsons) > 1:
+ _LOG.warning(
+ 'More than one coverage json file, selecting first: %r',
+ coverage_jsons,
+ )
+
+ coverage_json = coverage_jsons[0]
+
+ if ctx.luci:
+ if ctx.luci.is_dev:
+ _LOG.warning('Not uploading coverage since running in dev')
+ return PresubmitResult.PASS
+
+ with self._context(ctx):
+ # GCS bucket paths are POSIX-like.
+ coverage_gcs_path = posixpath.join(
+ options.target_bucket_root,
+ 'incremental' if ctx.luci.is_try else 'absolute',
+ options.target_bucket_project,
+ str(ctx.luci.buildbucket_id),
+ )
+ _copy_to_gcs(
+ ctx,
+ coverage_json,
+ posixpath.join(coverage_gcs_path, 'report.json'),
+ )
+ metadata_json = _write_coverage_metadata(ctx, options)
+ _copy_to_gcs(
+ ctx,
+ metadata_json,
+ posixpath.join(coverage_gcs_path, 'metadata.json'),
+ )
+
+ return PresubmitResult.PASS
+
+ _LOG.warning('Not uploading coverage since running locally')
+ return PresubmitResult.PASS
+
+ def _package_substeps(self) -> Iterator[SubStep]:
+ for package in self._packages:
yield SubStep(
f'install {package} package',
self._install_package,
(package,),
)
- yield SubStep('gn gen', self._gn_gen)
-
+ def _ninja_substeps(self) -> Iterator[SubStep]:
targets_parts = set()
- for targets in self.ninja_target_lists:
+ for targets in self._ninja_target_lists:
targets_part = " ".join(targets)
maxlen = 70
if len(targets_part) > maxlen:
@@ -675,3 +873,127 @@ class GnGenNinja(Check):
assert targets_part not in targets_parts
targets_parts.add(targets_part)
yield SubStep(f'ninja {targets_part}', self._ninja, (targets,))
+
+ def _coverage_substeps(self) -> Iterator[SubStep]:
+ if self._coverage_options is not None:
+ yield SubStep('coverage', self._coverage, (self._coverage_options,))
+
+
+def _copy_to_gcs(ctx: PresubmitContext, filepath: Path, gcs_dst: str):
+ cmd = [
+ "gsutil",
+ "cp",
+ filepath,
+ gcs_dst,
+ ]
+
+ upload_stdout = ctx.output_dir / (filepath.name + '.stdout')
+ with upload_stdout.open('w') as outs:
+ call(*cmd, tee=outs)
+
+
+def _write_coverage_metadata(
+ ctx: PresubmitContext, options: CoverageOptions
+) -> Path:
+ """Write out Kalypsi coverage metadata and return the path to it."""
+ assert ctx.luci is not None
+ assert len(ctx.luci.triggers) == 1
+ change = ctx.luci.triggers[0]
+
+ metadata = {
+ 'host': change.gerrit_host,
+ 'project': options.project,
+ 'trace_type': options.trace_type,
+ 'trim_prefix': options.trim_prefix,
+ 'patchset_num': change.patchset,
+ 'change_id': change.number,
+ 'owner': options.owner,
+ 'bug_component': options.bug_component,
+ }
+
+ if ctx.luci.is_try:
+ # Running in CQ: uploading incremental coverage
+ metadata.update(
+ {
+ # Note: no `add_prefix`. According to the documentation, that's
+ # only supported for absolute coverage.
+ #
+ # TODO(tpudlik): Follow up with Kalypsi team, since this is
+ # surprising (given that trim_prefix is supported for both types
+ # of coverage). This might be an error in the docs.
+ 'patchset_num': change.patchset,
+ 'change_id': change.number,
+ }
+ )
+ else:
+ # Running in CI: uploading absolute coverage
+ metadata.update(
+ {
+ 'add_prefix': options.add_prefix,
+ 'commit_id': change.ref,
+ 'ref': options.ref,
+ 'source': options.source,
+ }
+ )
+
+ metadata_json = ctx.output_dir / "metadata.json"
+ with metadata_json.open('w') as metadata_file:
+ json.dump(metadata, metadata_file)
+ return metadata_json
+
+
+class GnGenNinja(_NinjaBase):
+ """Thin wrapper of Check for steps that just call gn/ninja.
+
+ Runs gn gen, ninja, then gn check.
+ """
+
+ def __init__(
+ self,
+ *args,
+ gn_args: Optional[ # pylint: disable=redefined-outer-name
+ Dict[str, Any]
+ ] = None,
+ **kwargs,
+ ):
+ """Initializes a GnGenNinja object.
+
+ Args:
+ *args: Passed on to superclass.
+ gn_args: Dict of GN args.
+ **kwargs: Passed on to superclass.
+ """
+ super().__init__(self._substeps(), *args, **kwargs)
+ self._gn_args: Dict[str, Any] = gn_args or {}
+
+ @property
+ def gn_args(self) -> Dict[str, Any]:
+ return self._gn_args
+
+ def _gn_gen(self, ctx: PresubmitContext) -> PresubmitResult:
+ args: Dict[str, Any] = {}
+ if self._coverage_options is not None:
+ args['pw_toolchain_COVERAGE_ENABLED'] = True
+ args['pw_build_PYTHON_TEST_COVERAGE'] = True
+ args['pw_C_OPTIMIZATION_LEVELS'] = ('debug',)
+
+ if ctx.incremental:
+ args['pw_toolchain_PROFILE_SOURCE_FILES'] = [
+ f'//{x.relative_to(ctx.root)}' for x in ctx.paths
+ ]
+
+ args.update({k: _value(ctx, v) for k, v in self._gn_args.items()})
+ gn_gen(ctx, gn_check=False, **args) # type: ignore
+ return PresubmitResult.PASS
+
+ def _substeps(self) -> Iterator[SubStep]:
+ yield from self._package_substeps()
+
+ yield SubStep('gn gen', self._gn_gen)
+
+ yield from self._ninja_substeps()
+
+ # Run gn check after building so it can check generated files.
+ yield SubStep('gn check', gn_check)
+
+ yield from self._coverage_substeps()
diff --git a/pw_presubmit/py/pw_presubmit/cli.py b/pw_presubmit/py/pw_presubmit/cli.py
index d29dd1606..477b640ef 100644
--- a/pw_presubmit/py/pw_presubmit/cli.py
+++ b/pw_presubmit/py/pw_presubmit/cli.py
@@ -14,6 +14,7 @@
"""Argument parsing code for presubmit checks."""
import argparse
+import fnmatch
import logging
import os
from pathlib import Path
@@ -56,7 +57,8 @@ def add_path_arguments(parser) -> None:
default=git_repo.TRACKING_BRANCH_ALIAS,
help=(
'Git revision against which to diff for changed files. '
- 'Default is the tracking branch of the current branch.'
+ 'Default is the tracking branch of the current branch: '
+ f'{git_repo.TRACKING_BRANCH_ALIAS}'
),
)
@@ -142,21 +144,28 @@ def _add_programs_arguments(
help='List all the available steps.',
)
- def presubmit_step(arg: str) -> presubmit.Check:
- if arg not in all_steps:
+ def presubmit_step(arg: str) -> List[presubmit.Check]:
+ """Return a list of matching presubmit steps."""
+ filtered_step_names = fnmatch.filter(all_steps.keys(), arg)
+
+ if not filtered_step_names:
all_step_names = ', '.join(sorted(all_steps.keys()))
raise argparse.ArgumentTypeError(
- f'{arg} is not the name of a presubmit step\n\n'
+ f'"{arg}" does not match the name of a presubmit step.\n\n'
f'Valid Steps:\n{all_step_names}'
)
- return all_steps[arg]
+
+ return list(all_steps[name] for name in filtered_step_names)
parser.add_argument(
'--step',
- action='append',
- choices=all_steps.values(),
+ action='extend',
default=[],
- help='Run specific steps instead of running a full program.',
+ help=(
+ 'Run specific steps instead of running a full program. Include an '
+ 'asterix to match more than one step name. For example: --step '
+ "'*_format'"
+ ),
type=presubmit_step,
)
@@ -194,12 +203,22 @@ def add_arguments(
"""Adds common presubmit check options to an argument parser."""
add_path_arguments(parser)
+
+ parser.add_argument(
+ '--dry-run',
+ action='store_true',
+ help=(
+ 'Execute the presubits with in dry-run mode. System commands that'
+ 'pw_presubmit would run are instead printed to the terminal.'
+ ),
+ )
parser.add_argument(
'-k',
'--keep-going',
action='store_true',
help='Continue running presubmit steps after a failure.',
)
+
parser.add_argument(
'--continue-after-build-error',
action='store_true',
@@ -208,11 +227,20 @@ def add_arguments(
'failure.'
),
)
+
+ parser.add_argument(
+ '--rng-seed',
+ type=int,
+ default=1,
+ help='Seed for random number generators.',
+ )
+
parser.add_argument(
'--output-directory',
type=Path,
help=f'Output directory (default: {"<repo root>" / DEFAULT_PATH})',
)
+
parser.add_argument(
'--package-root',
type=Path,
@@ -242,6 +270,13 @@ def add_arguments(
)
+def _get_default_parser() -> argparse.ArgumentParser:
+ """Return all common pw presubmit args for sphinx documentation."""
+ parser = argparse.ArgumentParser(description="Runs local presubmit checks.")
+ add_arguments(parser)
+ return parser
+
+
def run( # pylint: disable=too-many-arguments
default_program: Optional[presubmit.Program],
program: Sequence[presubmit.Program],
@@ -254,6 +289,7 @@ def run( # pylint: disable=too-many-arguments
repositories: Collection[Path] = (),
only_list_steps=False,
list_steps: Optional[Callable[[], None]] = None,
+ dry_run: bool = False,
**other_args,
) -> int:
"""Processes arguments from add_arguments and runs the presubmit.
@@ -335,8 +371,19 @@ def run( # pylint: disable=too-many-arguments
output_directory=output_directory,
package_root=package_root,
substep=substep,
+ dry_run=dry_run,
**other_args,
):
return 0
+ # Check if this failed presumbit was run as a Git hook by looking for GIT_*
+ # environment variables. Mention using --no-verify to skip if so.
+ for env_var in os.environ:
+ if env_var.startswith('GIT'):
+ _LOG.info(
+ 'To skip these checks and continue with this push, '
+ 'add --no-verify to the git command'
+ )
+ break
+
return 1
diff --git a/pw_presubmit/py/pw_presubmit/cpp_checks.py b/pw_presubmit/py/pw_presubmit/cpp_checks.py
index 849242455..a86cb092c 100644
--- a/pw_presubmit/py/pw_presubmit/cpp_checks.py
+++ b/pw_presubmit/py/pw_presubmit/cpp_checks.py
@@ -14,30 +14,144 @@
"""C++-related checks."""
import logging
+from pathlib import Path
+import re
+from typing import Callable, Optional, Iterable, Iterator
+from pw_presubmit.presubmit import (
+ Check,
+ filter_paths,
+)
+from pw_presubmit.presubmit_context import PresubmitContext
from pw_presubmit import (
build,
- Check,
format_code,
- PresubmitContext,
- filter_paths,
+ presubmit_context,
)
_LOG: logging.Logger = logging.getLogger(__name__)
+def _fail(ctx, error, path):
+ ctx.fail(error, path=path)
+ with open(ctx.failure_summary_log, 'a') as outs:
+ print(f'{path}\n{error}\n', file=outs)
+
+
@filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',))
def pragma_once(ctx: PresubmitContext) -> None:
"""Presubmit check that ensures all header files contain '#pragma once'."""
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+
for path in ctx.paths:
_LOG.debug('Checking %s', path)
- with open(path) as file:
+ with path.open() as file:
for line in file:
if line.startswith('#pragma once'):
break
else:
- ctx.fail('#pragma once is missing!', path=path)
+ _fail(ctx, '#pragma once is missing!', path=path)
+
+
+def include_guard_check(
+ guard: Optional[Callable[[Path], str]] = None,
+ allow_pragma_once: bool = True,
+) -> Check:
+ """Create an include guard check.
+
+ Args:
+ guard: Callable that generates an expected include guard name for the
+ given Path. If None, any include guard is acceptable, as long as
+ it's consistent between the '#ifndef' and '#define' lines.
+ allow_pragma_once: Whether to allow headers to use '#pragma once'
+ instead of '#ifndef'/'#define'.
+ """
+
+ def stripped_non_comment_lines(iterable: Iterable[str]) -> Iterator[str]:
+ """Yield non-comment non-empty lines from a C++ file."""
+ multi_line_comment = False
+ for line in iterable:
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith('//'):
+ continue
+ if line.startswith('/*'):
+ multi_line_comment = True
+ if multi_line_comment:
+ if line.endswith('*/'):
+ multi_line_comment = False
+ continue
+ yield line
+
+ def check_path(ctx: PresubmitContext, path: Path) -> None:
+ """Check if path has a valid include guard."""
+
+ _LOG.debug('checking %s', path)
+ expected: Optional[str] = None
+ if guard:
+ expected = guard(path)
+ _LOG.debug('expecting guard %r', expected)
+
+ with path.open() as ins:
+ iterable = stripped_non_comment_lines(ins)
+ first_line = next(iterable, '')
+ _LOG.debug('first line %r', first_line)
+
+ if allow_pragma_once and first_line.startswith('#pragma once'):
+ _LOG.debug('found %r', first_line)
+ return
+
+ if expected:
+ ifndef_expected = f'#ifndef {expected}'
+ if not re.match(rf'^#ifndef {expected}$', first_line):
+ _fail(
+ ctx,
+ 'Include guard is missing! Expected: '
+ f'{ifndef_expected!r}, Found: {first_line!r}',
+ path=path,
+ )
+ return
+
+ else:
+ match = re.match(
+ r'^#\s*ifndef\s+([a-zA-Z_][a-zA-Z_0-9]*)$',
+ first_line,
+ )
+ if not match:
+ _fail(
+ ctx,
+ 'Include guard is missing! Expected "#ifndef" line, '
+ f'Found: {first_line!r}',
+ path=path,
+ )
+ return
+ expected = match.group(1)
+
+ second_line = next(iterable, '')
+ _LOG.debug('second line %r', second_line)
+
+ if not re.match(rf'^#\s*define\s+{expected}$', second_line):
+ define_expected = f'#define {expected}'
+ _fail(
+ ctx,
+ 'Include guard is missing! Expected: '
+ f'{define_expected!r}, Found: {second_line!r}',
+ path=path,
+ )
+ return
+
+ _LOG.debug('passed')
+
+ @filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',))
+ def include_guard(ctx: PresubmitContext):
+ """Check that all header files contain an include guard."""
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+ for path in ctx.paths:
+ check_path(ctx, path)
+
+ return include_guard
@Check
@@ -76,6 +190,6 @@ def runtime_sanitizers(ctx: PresubmitContext) -> None:
def all_sanitizers():
- # TODO(b/234876100): msan will not work until the C++ standard library
+ # TODO: b/234876100 - msan will not work until the C++ standard library
# included in the sysroot has a variant built with msan.
return [asan, tsan, ubsan, runtime_sanitizers]
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index 9b073ea05..df81ad3f9 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -22,10 +22,12 @@ code. These tools must be available on the path when this script is invoked!
import argparse
import collections
import difflib
+import json
import logging
import os
from pathlib import Path
import re
+import shutil
import subprocess
import sys
import tempfile
@@ -44,26 +46,33 @@ from typing import (
Union,
)
-try:
- import pw_presubmit
-except ImportError:
- # Append the pw_presubmit package path to the module search path to allow
- # running this module without installing the pw_presubmit package.
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- import pw_presubmit
-
import pw_cli.color
import pw_cli.env
-from pw_presubmit.presubmit import FileFilter
-from pw_presubmit import (
- cli,
+import pw_env_setup.config_file
+from pw_presubmit.presubmit import (
+ FileFilter,
+ filter_paths,
+)
+from pw_presubmit.presubmit_context import (
FormatContext,
FormatOptions,
+ PresubmitContext,
+ PresubmitFailure,
+)
+from pw_presubmit import (
+ cli,
git_repo,
owners_checks,
- PresubmitContext,
+ presubmit_context,
)
-from pw_presubmit.tools import exclude_paths, file_summary, log_run, plural
+from pw_presubmit.tools import (
+ exclude_paths,
+ file_summary,
+ log_run,
+ plural,
+ colorize_diff,
+)
+from pw_presubmit.rst_format import reformat_rst
_LOG: logging.Logger = logging.getLogger(__name__)
_COLOR = pw_cli.color.colors()
@@ -72,27 +81,15 @@ _DEFAULT_PATH = Path('out', 'format')
_Context = Union[PresubmitContext, FormatContext]
-def _colorize_diff_line(line: str) -> str:
- if line.startswith('--- ') or line.startswith('+++ '):
- return _COLOR.bold_white(line)
- if line.startswith('-'):
- return _COLOR.red(line)
- if line.startswith('+'):
- return _COLOR.green(line)
- if line.startswith('@@ '):
- return _COLOR.cyan(line)
- return line
-
-
-def colorize_diff(lines: Iterable[str]) -> str:
- """Takes a diff str or list of str lines and returns a colorized version."""
- if isinstance(lines, str):
- lines = lines.splitlines(True)
-
- return ''.join(_colorize_diff_line(line) for line in lines)
+def _ensure_newline(orig: bytes) -> bytes:
+ if orig.endswith(b'\n'):
+ return orig
+ return orig + b'\nNo newline at end of file\n'
def _diff(path, original: bytes, formatted: bytes) -> str:
+ original = _ensure_newline(original)
+ formatted = _ensure_newline(formatted)
return ''.join(
difflib.unified_diff(
original.decode(errors='replace').splitlines(True),
@@ -103,24 +100,31 @@ def _diff(path, original: bytes, formatted: bytes) -> str:
)
-Formatter = Callable[[str, bytes], bytes]
+FormatterT = Callable[[str, bytes], bytes]
-def _diff_formatted(path, formatter: Formatter) -> Optional[str]:
+def _diff_formatted(
+ path, formatter: FormatterT, dry_run: bool = False
+) -> Optional[str]:
"""Returns a diff comparing a file to its formatted version."""
with open(path, 'rb') as fd:
original = fd.read()
formatted = formatter(path, original)
+ if dry_run:
+ return None
+
return None if formatted == original else _diff(path, original, formatted)
-def _check_files(files, formatter: Formatter) -> Dict[Path, str]:
+def _check_files(
+ files, formatter: FormatterT, dry_run: bool = False
+) -> Dict[Path, str]:
errors = {}
for path in files:
- difference = _diff_formatted(path, formatter)
+ difference = _diff_formatted(path, formatter, dry_run)
if difference:
errors[path] = difference
@@ -138,7 +142,11 @@ def _clang_format(*args: Union[Path, str], **kwargs) -> bytes:
def clang_format_check(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
- return _check_files(ctx.paths, lambda path, _: _clang_format(path))
+ return _check_files(
+ ctx.paths,
+ lambda path, _: _clang_format(path),
+ ctx.dry_run,
+ )
def clang_format_fix(ctx: _Context) -> Dict[Path, str]:
@@ -147,6 +155,30 @@ def clang_format_fix(ctx: _Context) -> Dict[Path, str]:
return {}
+def _typescript_format(*args: Union[Path, str], **kwargs) -> bytes:
+ return log_run(
+ ['npx', 'prettier@3.0.1', *args],
+ stdout=subprocess.PIPE,
+ check=True,
+ **kwargs,
+ ).stdout
+
+
+def typescript_format_check(ctx: _Context) -> Dict[Path, str]:
+ """Checks formatting; returns {path: diff} for files with bad formatting."""
+ return _check_files(
+ ctx.paths,
+ lambda path, _: _typescript_format(path),
+ ctx.dry_run,
+ )
+
+
+def typescript_format_fix(ctx: _Context) -> Dict[Path, str]:
+ """Fixes formatting for the provided files in place."""
+ _typescript_format('--write', *ctx.paths)
+ return {}
+
+
def check_gn_format(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
return _check_files(
@@ -157,6 +189,7 @@ def check_gn_format(ctx: _Context) -> Dict[Path, str]:
stdout=subprocess.PIPE,
check=True,
).stdout,
+ ctx.dry_run,
)
@@ -185,7 +218,7 @@ def check_bazel_format(ctx: _Context) -> Dict[Path, str]:
errors[Path(path)] = stderr
return build.read_bytes()
- result = _check_files(ctx.paths, _format_temp)
+ result = _check_files(ctx.paths, _format_temp, ctx.dry_run)
result.update(errors)
return result
@@ -215,6 +248,7 @@ def check_go_format(ctx: _Context) -> Dict[Path, str]:
lambda path, _: log_run(
['gofmt', path], stdout=subprocess.PIPE, check=True
).stdout,
+ ctx.dry_run,
)
@@ -224,7 +258,7 @@ def fix_go_format(ctx: _Context) -> Dict[Path, str]:
return {}
-# TODO(b/259595799) Remove yapf support.
+# TODO: b/259595799 - Remove yapf support.
def _yapf(*args, **kwargs) -> subprocess.CompletedProcess:
return log_run(
['python', '-m', 'yapf', '--parallel', *args],
@@ -268,6 +302,20 @@ def fix_py_format_yapf(ctx: _Context) -> Dict[Path, str]:
def _enumerate_black_configs() -> Iterable[Path]:
+ config = pw_env_setup.config_file.load()
+ black_config_file = (
+ config.get('pw', {})
+ .get('pw_presubmit', {})
+ .get('format', {})
+ .get('black_config_file', {})
+ )
+ if black_config_file:
+ explicit_path = Path(black_config_file)
+ if not explicit_path.is_file():
+ raise ValueError(f'Black config file not found: {explicit_path}')
+ yield explicit_path
+ return # If an explicit path is provided, don't try implicit paths.
+
if directory := os.environ.get('PW_PROJECT_ROOT'):
yield Path(directory, '.black.toml')
yield Path(directory, 'pyproject.toml')
@@ -335,6 +383,7 @@ def check_py_format_black(ctx: _Context) -> Dict[Path, str]:
result = _check_files(
[x for x in ctx.paths if str(x).endswith(paths)],
_format_temp,
+ ctx.dry_run,
)
result.update(errors)
return result
@@ -399,6 +448,47 @@ def _check_trailing_space(paths: Iterable[Path], fix: bool) -> Dict[Path, str]:
return errors
+def _format_json(contents: bytes) -> bytes:
+ return json.dumps(json.loads(contents), indent=2).encode() + b'\n'
+
+
+def _json_error(exc: json.JSONDecodeError, path: Path) -> str:
+ return f'{path}: {exc.msg} {exc.lineno}:{exc.colno}\n'
+
+
+def check_json_format(ctx: _Context) -> Dict[Path, str]:
+ errors = {}
+
+ for path in ctx.paths:
+ orig = path.read_bytes()
+ try:
+ formatted = _format_json(orig)
+ except json.JSONDecodeError as exc:
+ errors[path] = _json_error(exc, path)
+ continue
+
+ if orig != formatted:
+ errors[path] = _diff(path, orig, formatted)
+
+ return errors
+
+
+def fix_json_format(ctx: _Context) -> Dict[Path, str]:
+ errors = {}
+ for path in ctx.paths:
+ orig = path.read_bytes()
+ try:
+ formatted = _format_json(orig)
+ except json.JSONDecodeError as exc:
+ errors[path] = _json_error(exc, path)
+ continue
+
+ if orig != formatted:
+ path.write_bytes(formatted)
+
+ return errors
+
+
def check_trailing_space(ctx: _Context) -> Dict[Path, str]:
return _check_trailing_space(ctx.paths, fix=False)
@@ -408,6 +498,24 @@ def fix_trailing_space(ctx: _Context) -> Dict[Path, str]:
return {}
+def rst_format_check(ctx: _Context) -> Dict[Path, str]:
+ errors = {}
+ for path in ctx.paths:
+ result = reformat_rst(path, diff=True, in_place=False)
+ if result:
+ errors[path] = ''.join(result)
+ return errors
+
+
+def rst_format_fix(ctx: _Context) -> Dict[Path, str]:
+ errors = {}
+ for path in ctx.paths:
+ result = reformat_rst(path, diff=True, in_place=True)
+ if result:
+ errors[path] = ''.join(result)
+ return errors
+
+
def print_format_check(
errors: Dict[Path, str],
show_fix_commands: bool,
@@ -457,7 +565,7 @@ class CodeFormat(NamedTuple):
@property
def extensions(self):
- # TODO(b/23842636): Switch calls of this to using 'filter' and remove.
+ # TODO: b/23842636 - Switch calls of this to using 'filter' and remove.
return self.filter.endswith
@@ -467,7 +575,7 @@ CPP_SOURCE_EXTS = frozenset(
)
CPP_EXTS = CPP_HEADER_EXTS.union(CPP_SOURCE_EXTS)
CPP_FILE_FILTER = FileFilter(
- endswith=CPP_EXTS, exclude=(r'\.pb\.h$', r'\.pb\.c$')
+ endswith=CPP_EXTS, exclude=[r'\.pb\.h$', r'\.pb\.c$']
)
C_FORMAT = CodeFormat(
@@ -476,102 +584,133 @@ C_FORMAT = CodeFormat(
PROTO_FORMAT: CodeFormat = CodeFormat(
'Protocol buffer',
- FileFilter(endswith=('.proto',)),
+ FileFilter(endswith=['.proto']),
clang_format_check,
clang_format_fix,
)
JAVA_FORMAT: CodeFormat = CodeFormat(
'Java',
- FileFilter(endswith=('.java',)),
+ FileFilter(endswith=['.java']),
clang_format_check,
clang_format_fix,
)
JAVASCRIPT_FORMAT: CodeFormat = CodeFormat(
'JavaScript',
- FileFilter(endswith=('.js',)),
- clang_format_check,
- clang_format_fix,
+ FileFilter(endswith=['.js']),
+ typescript_format_check,
+ typescript_format_fix,
+)
+
+TYPESCRIPT_FORMAT: CodeFormat = CodeFormat(
+ 'TypeScript',
+ FileFilter(endswith=['.ts']),
+ typescript_format_check,
+ typescript_format_fix,
+)
+
+# TODO: b/308948504 - Add real code formatting support for CSS
+CSS_FORMAT: CodeFormat = CodeFormat(
+ 'css',
+ FileFilter(endswith=['.css']),
+ check_trailing_space,
+ fix_trailing_space,
)
GO_FORMAT: CodeFormat = CodeFormat(
- 'Go', FileFilter(endswith=('.go',)), check_go_format, fix_go_format
+ 'Go', FileFilter(endswith=['.go']), check_go_format, fix_go_format
)
PYTHON_FORMAT: CodeFormat = CodeFormat(
'Python',
- FileFilter(endswith=('.py',)),
+ FileFilter(endswith=['.py']),
check_py_format,
fix_py_format,
)
GN_FORMAT: CodeFormat = CodeFormat(
- 'GN', FileFilter(endswith=('.gn', '.gni')), check_gn_format, fix_gn_format
+ 'GN', FileFilter(endswith=['.gn', '.gni']), check_gn_format, fix_gn_format
)
BAZEL_FORMAT: CodeFormat = CodeFormat(
'Bazel',
- FileFilter(endswith=('BUILD', '.bazel', '.bzl'), name=('WORKSPACE')),
+ FileFilter(endswith=['.bazel', '.bzl'], name=['^BUILD$', '^WORKSPACE$']),
check_bazel_format,
fix_bazel_format,
)
COPYBARA_FORMAT: CodeFormat = CodeFormat(
'Copybara',
- FileFilter(endswith=('.bara.sky',)),
+ FileFilter(endswith=['.bara.sky']),
check_bazel_format,
fix_bazel_format,
)
-# TODO(b/234881054): Add real code formatting support for CMake
+# TODO: b/234881054 - Add real code formatting support for CMake
CMAKE_FORMAT: CodeFormat = CodeFormat(
'CMake',
- FileFilter(endswith=('CMakeLists.txt', '.cmake')),
+ FileFilter(endswith=['.cmake'], name=['^CMakeLists.txt$']),
check_trailing_space,
fix_trailing_space,
)
RST_FORMAT: CodeFormat = CodeFormat(
'reStructuredText',
- FileFilter(endswith=('.rst',)),
- check_trailing_space,
- fix_trailing_space,
+ FileFilter(endswith=['.rst']),
+ rst_format_check,
+ rst_format_fix,
)
MARKDOWN_FORMAT: CodeFormat = CodeFormat(
'Markdown',
- FileFilter(endswith=('.md',)),
+ FileFilter(endswith=['.md']),
check_trailing_space,
fix_trailing_space,
)
OWNERS_CODE_FORMAT = CodeFormat(
'OWNERS',
- filter=FileFilter(name=('OWNERS',)),
+ filter=FileFilter(name=['^OWNERS$']),
check=check_owners_format,
fix=fix_owners_format,
)
-CODE_FORMATS: Tuple[CodeFormat, ...] = (
- # keep-sorted: start
- BAZEL_FORMAT,
- CMAKE_FORMAT,
- COPYBARA_FORMAT,
- C_FORMAT,
- GN_FORMAT,
- GO_FORMAT,
- JAVASCRIPT_FORMAT,
- JAVA_FORMAT,
- MARKDOWN_FORMAT,
- OWNERS_CODE_FORMAT,
- PROTO_FORMAT,
- PYTHON_FORMAT,
- RST_FORMAT,
- # keep-sorted: end
+JSON_FORMAT: CodeFormat = CodeFormat(
+ 'JSON',
+ FileFilter(endswith=['.json']),
+ check=check_json_format,
+ fix=fix_json_format,
)
-# TODO(b/264578594) Remove these lines when these globals aren't referenced.
+CODE_FORMATS: Tuple[CodeFormat, ...] = tuple(
+ filter(
+ None,
+ (
+ # keep-sorted: start
+ BAZEL_FORMAT,
+ CMAKE_FORMAT,
+ COPYBARA_FORMAT,
+ CSS_FORMAT,
+ C_FORMAT,
+ GN_FORMAT,
+ GO_FORMAT,
+ JAVASCRIPT_FORMAT if shutil.which('npx') else None,
+ JAVA_FORMAT,
+ JSON_FORMAT,
+ MARKDOWN_FORMAT,
+ OWNERS_CODE_FORMAT,
+ PROTO_FORMAT,
+ PYTHON_FORMAT,
+ RST_FORMAT,
+ TYPESCRIPT_FORMAT if shutil.which('npx') else None,
+ # keep-sorted: end
+ ),
+ )
+)
+
+
+# TODO: b/264578594 - Remove these lines when these globals aren't referenced.
CODE_FORMATS_WITH_BLACK: Tuple[CodeFormat, ...] = CODE_FORMATS
CODE_FORMATS_WITH_YAPF: Tuple[CodeFormat, ...] = CODE_FORMATS
@@ -591,8 +730,9 @@ def presubmit_check(
file_filter = FileFilter(**vars(code_format.filter))
file_filter.exclude += tuple(re.compile(e) for e in exclude)
- @pw_presubmit.filter_paths(file_filter=file_filter)
- def check_code_format(ctx: pw_presubmit.PresubmitContext):
+ @filter_paths(file_filter=file_filter)
+ def check_code_format(ctx: PresubmitContext):
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
errors = code_format.check(ctx)
print_format_check(
errors,
@@ -610,7 +750,7 @@ def presubmit_check(
file=outs,
)
- raise pw_presubmit.PresubmitFailure
+ raise PresubmitFailure
language = code_format.language.lower().replace('+', 'p').replace(' ', '_')
check_code_format.name = f'{language}_format'
@@ -646,10 +786,16 @@ class CodeFormatter:
package_root: Optional[Path] = None,
):
self.root = root
- self.paths = list(files)
self._formats: Dict[CodeFormat, List] = collections.defaultdict(list)
self.root_output_dir = output_dir
self.package_root = package_root or output_dir / 'packages'
+ self._format_options = FormatOptions.load()
+ raw_paths = files
+ self.paths: Tuple[Path, ...] = self._format_options.filter_paths(files)
+
+ filtered_paths = set(raw_paths) - set(self.paths)
+ for path in sorted(filtered_paths):
+ _LOG.debug('filtered out %s', path)
for path in self.paths:
for code_format in code_formats:
@@ -671,7 +817,7 @@ class CodeFormatter:
output_dir=outdir,
paths=tuple(self._formats[code_format]),
package_root=self.package_root,
- format_options=FormatOptions.load(),
+ format_options=self._format_options,
)
def check(self) -> Dict[Path, str]:
@@ -882,19 +1028,6 @@ def main() -> int:
return format_paths_in_repo(**vars(arguments(git_paths=True).parse_args()))
-def _pigweed_upstream_main() -> int:
- """Check and fix formatting for source files in upstream Pigweed.
-
- Excludes third party sources.
- """
- args = arguments(git_paths=True).parse_args()
-
- # Exclude paths with third party code from formatting.
- args.exclude.append(re.compile('^third_party/fuchsia/repo/'))
-
- return format_paths_in_repo(**vars(args))
-
-
if __name__ == '__main__':
try:
# If pw_cli is available, use it to initialize logs.
diff --git a/pw_presubmit/py/pw_presubmit/git_repo.py b/pw_presubmit/py/pw_presubmit/git_repo.py
index a3847f8ca..0fb82568c 100644
--- a/pw_presubmit/py/pw_presubmit/git_repo.py
+++ b/pw_presubmit/py/pw_presubmit/git_repo.py
@@ -26,6 +26,7 @@ PatternOrStr = Union[Pattern, str]
TRACKING_BRANCH_ALIAS = '@{upstream}'
_TRACKING_BRANCH_ALIASES = TRACKING_BRANCH_ALIAS, '@{u}'
+_NON_TRACKING_FALLBACK = 'HEAD~10'
def git_stdout(
@@ -37,6 +38,7 @@ def git_stdout(
stdout=subprocess.PIPE,
stderr=None if show_stderr else subprocess.DEVNULL,
check=True,
+ ignore_dry_run=True,
)
.stdout.decode()
.strip()
@@ -73,7 +75,10 @@ def _diff_names(
yield full_path
-def tracking_branch(repo_path: Optional[Path] = None) -> Optional[str]:
+def tracking_branch(
+ repo_path: Optional[Path] = None,
+ fallback: Optional[str] = None,
+) -> Optional[str]:
"""Returns the tracking branch of the current branch.
Since most callers of this function can safely handle a return value of
@@ -105,7 +110,7 @@ def tracking_branch(repo_path: Optional[Path] = None) -> Optional[str]:
)
except subprocess.CalledProcessError:
- return None
+ return fallback
def list_files(
@@ -127,7 +132,7 @@ def list_files(
repo_path = Path.cwd()
if commit in _TRACKING_BRANCH_ALIASES:
- commit = tracking_branch(repo_path)
+ commit = tracking_branch(repo_path, fallback=_NON_TRACKING_FALLBACK)
if commit:
try:
@@ -164,6 +169,7 @@ def has_uncommitted_changes(repo: Optional[Path] = None) -> bool:
['git', '-C', repo, 'update-index', '-q', '--refresh'],
capture_output=True,
check=True,
+ ignore_dry_run=True,
)
except subprocess.CalledProcessError as err:
if err.stderr or i == retries - 1:
@@ -172,7 +178,8 @@ def has_uncommitted_changes(repo: Optional[Path] = None) -> bool:
# diff-index exits with 1 if there are uncommitted changes.
return (
log_run(
- ['git', '-C', repo, 'diff-index', '--quiet', 'HEAD', '--']
+ ['git', '-C', repo, 'diff-index', '--quiet', 'HEAD', '--'],
+ ignore_dry_run=True,
).returncode
== 1
)
diff --git a/pw_presubmit/py/pw_presubmit/gitmodules.py b/pw_presubmit/py/pw_presubmit/gitmodules.py
index 67e9854e6..5796a496d 100644
--- a/pw_presubmit/py/pw_presubmit/gitmodules.py
+++ b/pw_presubmit/py/pw_presubmit/gitmodules.py
@@ -19,18 +19,22 @@ from pathlib import Path
from typing import Callable, Dict, Optional, Sequence
import urllib.parse
-from pw_presubmit import (
- git_repo,
+from pw_presubmit.presubmit import filter_paths
+from pw_presubmit.presubmit_context import (
PresubmitContext,
PresubmitFailure,
- filter_paths,
)
+from pw_presubmit import git_repo, plural, presubmit_context
+
_LOG: logging.Logger = logging.getLogger(__name__)
@dataclasses.dataclass
class Config:
+ # Allow submodules to exist in any form.
+ allow_submodules: bool = True
+
# Allow direct references to non-Google hosts.
allow_non_googlesource_hosts: bool = False
@@ -89,6 +93,12 @@ def process_gitmodules(ctx: PresubmitContext, config: Config, path: Path):
_LOG.debug('Evaluating path %s', path)
submodules: Dict[str, Dict[str, str]] = _parse_gitmodules(path)
+ if submodules and not config.allow_submodules:
+ ctx.fail(
+ f'submodules are not permitted but '
+ f'{plural(submodules, "submodule", exist=True)} {tuple(submodules)}'
+ )
+
assert isinstance(config.allowed_googlesource_hosts, (list, tuple))
for allowed in config.allowed_googlesource_hosts:
if '.' in allowed or '-review' in allowed:
@@ -186,6 +196,8 @@ def create(config: Config = Config()):
@filter_paths(endswith='.gitmodules')
def gitmodules(ctx: PresubmitContext):
"""Check various rules for .gitmodules files."""
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+
for path in ctx.paths:
process_gitmodules(ctx, config, path)
diff --git a/pw_presubmit/py/pw_presubmit/inclusive_language.py b/pw_presubmit/py/pw_presubmit/inclusive_language.py
index 8ef0a9c00..34fde80f2 100644
--- a/pw_presubmit/py/pw_presubmit/inclusive_language.py
+++ b/pw_presubmit/py/pw_presubmit/inclusive_language.py
@@ -18,7 +18,7 @@ from pathlib import Path
import re
from typing import Dict, List, Union
-from . import presubmit
+from . import presubmit, presubmit_context
# List borrowed from Android:
# https://source.android.com/setup/contribute/respectful-code
@@ -107,13 +107,20 @@ class LineMatch:
@presubmit.check(name='inclusive_language')
def presubmit_check(
- ctx: presubmit.PresubmitContext,
+ ctx: presubmit_context.PresubmitContext,
words_regex=NON_INCLUSIVE_WORDS_REGEX,
):
"""Presubmit check that ensures files do not contain banned words."""
+ # No subprocesses are run for inclusive_language so don't perform this check
+ # if dry_run is on.
+ if ctx.dry_run:
+ return
+
found_words: Dict[Path, List[Union[PathMatch, LineMatch]]] = {}
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+
for path in ctx.paths:
match = words_regex.search(str(path.relative_to(ctx.root)))
if match:
@@ -174,7 +181,7 @@ ignored with "inclusive-language: disable" and reenabled with
)
# Re-enable just in case: inclusive-language: enable.
- raise presubmit.PresubmitFailure
+ raise presubmit_context.PresubmitFailure
def inclusive_language_checker(*words):
@@ -183,7 +190,7 @@ def inclusive_language_checker(*words):
regex = _process_inclusive_language(*words)
def inclusive_language( # pylint: disable=redefined-outer-name
- ctx: presubmit.PresubmitContext,
+ ctx: presubmit_context.PresubmitContext,
):
globals()['inclusive_language'](ctx, regex)
diff --git a/pw_presubmit/py/pw_presubmit/javascript_checks.py b/pw_presubmit/py/pw_presubmit/javascript_checks.py
new file mode 100644
index 000000000..3dc8b8f68
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/javascript_checks.py
@@ -0,0 +1,44 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""JavaScript and TypeScript validity check."""
+
+from pw_presubmit import presubmit_context
+from pw_presubmit.presubmit import (
+ Check,
+ filter_paths,
+)
+from pw_presubmit.presubmit_context import (
+ PresubmitContext,
+ PresubmitFailure,
+)
+from pw_presubmit.tools import log_run
+
+
+@filter_paths(endswith=('.js', '.ts'))
+@Check
+def eslint(ctx: PresubmitContext):
+ """Presubmit check that ensures JavaScript files are valid."""
+
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+
+ # Check if npm deps are installed.
+ npm_list = log_run(['npm', 'list'])
+ if npm_list.returncode != 0:
+ npm_install = log_run(['npm', 'install'])
+ if npm_install.returncode != 0:
+ raise PresubmitFailure('npm install failed.')
+
+ result = log_run(['npx', 'eslint@8.47.0', *ctx.paths])
+ if result.returncode != 0:
+ raise PresubmitFailure('eslint identifed issues.')
diff --git a/pw_presubmit/py/pw_presubmit/json_check.py b/pw_presubmit/py/pw_presubmit/json_check.py
new file mode 100644
index 000000000..09ab1f715
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/json_check.py
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""JSON validity check."""
+
+import json
+
+from . import presubmit, presubmit_context
+
+
+@presubmit.filter_paths(endswith=('.json',))
+@presubmit.check(name='json_check')
+def presubmit_check(ctx: presubmit_context.PresubmitContext):
+ """Presubmit check that ensures JSON files are valid."""
+
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+
+ for path in ctx.paths:
+ with path.open('r') as ins:
+ try:
+ json.load(ins)
+ except json.decoder.JSONDecodeError as exc:
+ intro_line = f'failed to parse {path.relative_to(ctx.root)}'
+ with ctx.failure_summary_log.open('w') as outs:
+ print(intro_line, file=outs)
+ print(exc, file=outs)
+ ctx.fail(intro_line)
+ ctx.fail(str(exc))
diff --git a/pw_presubmit/py/pw_presubmit/keep_sorted.py b/pw_presubmit/py/pw_presubmit/keep_sorted.py
index a1068f30c..fc2fab090 100644
--- a/pw_presubmit/py/pw_presubmit/keep_sorted.py
+++ b/pw_presubmit/py/pw_presubmit/keep_sorted.py
@@ -34,11 +34,10 @@ from typing import (
)
import pw_cli
-from . import cli, format_code, git_repo, presubmit, tools
+from . import cli, git_repo, presubmit, presubmit_context, tools
DEFAULT_PATH = Path('out', 'presubmit', 'keep_sorted')
-_COLOR = pw_cli.color.colors()
_LOG: logging.Logger = logging.getLogger(__name__)
# Ignore a whole section. Please do not change the order of these lines.
@@ -182,7 +181,7 @@ class _FileSorter:
if not block.allow_dupes:
lines = list({x.full: x for x in lines}.values())
- StrLinePair = Tuple[str, _Line]
+ StrLinePair = Tuple[str, _Line] # pylint: disable=invalid-name
sort_key_funcs: List[Callable[[StrLinePair], StrLinePair]] = []
if block.ignored_prefixes:
@@ -377,7 +376,7 @@ def _process_files(
if not errors:
return errors
- ctx.fail(f'Found {len(errors)} files with keep-sorted errors:')
+ ctx.fail(f'Found {tools.plural(errors, "file")} with keep-sorted errors:')
with ctx.failure_summary_log.open('w') as outs:
for path, diffs in errors.items():
@@ -390,7 +389,7 @@ def _process_files(
)
outs.write(diff)
- print(format_code.colorize_diff(diff))
+ print(tools.colorize_diff(diff))
return errors
@@ -399,6 +398,7 @@ def _process_files(
def presubmit_check(ctx: presubmit.PresubmitContext) -> None:
"""Presubmit check that ensures specified lists remain sorted."""
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
errors = _process_files(ctx)
if errors:
diff --git a/pw_presubmit/py/pw_presubmit/module_owners.py b/pw_presubmit/py/pw_presubmit/module_owners.py
index f16d6e3d4..0df437e75 100644
--- a/pw_presubmit/py/pw_presubmit/module_owners.py
+++ b/pw_presubmit/py/pw_presubmit/module_owners.py
@@ -17,11 +17,11 @@ import logging
from pathlib import Path
from typing import Callable, Optional, Tuple
-from pw_presubmit import (
+from pw_presubmit.presubmit_context import (
PresubmitContext,
PresubmitFailure,
- presubmit,
)
+from pw_presubmit import presubmit
_LOG: logging.Logger = logging.getLogger(__name__)
diff --git a/pw_presubmit/py/pw_presubmit/ninja_parser.py b/pw_presubmit/py/pw_presubmit/ninja_parser.py
index 131137df6..487af1754 100644
--- a/pw_presubmit/py/pw_presubmit/ninja_parser.py
+++ b/pw_presubmit/py/pw_presubmit/ninja_parser.py
@@ -16,40 +16,81 @@
# https://fuchsia.googlesource.com/infra/recipes/+/336933647862a1a9718b4ca18f0a67e89c2419f8/recipe_modules/ninja/resources/ninja_wrapper.py
"""Extracts a concise error from a ninja log."""
+import argparse
+import logging
from pathlib import Path
import re
+from typing import List, IO
+import sys
-_RULE_RE = re.compile(r'^\s*\[\d+/\d+\] (\S+)')
-_FAILED_RE = re.compile(r'^\s*FAILED: (.*)$')
-_FAILED_END_RE = re.compile(r'^\s*ninja: build stopped:.*')
+_LOG: logging.Logger = logging.getLogger(__name__)
+# Assume any of these lines could be prefixed with ANSI color codes.
+_COLOR_CODES_PREFIX = r'^(?:\x1b)?(?:\[\d+m\s*)?'
-def parse_ninja_stdout(ninja_stdout: Path) -> str:
- """Extract an error summary from ninja output."""
+_GOOGLETEST_FAILED, _GOOGLETEST_RUN, _GOOGLETEST_OK, _GOOGLETEST_DISABLED = (
+ '[ FAILED ]',
+ '[ RUN ]',
+ '[ OK ]',
+ '[ DISABLED ]',
+)
- failure_begins = False
- failure_lines = []
- last_line = ''
- with ninja_stdout.open() as ins:
- for line in ins:
- # Trailing whitespace isn't significant, as it doesn't affect the
- # way the line shows up in the logs. However, leading whitespace may
- # be significant, especially for compiler error messages.
- line = line.rstrip()
- if failure_begins:
- if not _RULE_RE.match(line) and not _FAILED_END_RE.match(line):
- failure_lines.append(line)
- else:
- # Output of failed step ends, save its info.
- failure_begins = False
+def _remove_passing_tests(failure_lines: List[str]) -> List[str]:
+ test_lines: List[str] = []
+ result = []
+ for line in failure_lines:
+ if test_lines:
+ if _GOOGLETEST_OK in line:
+ test_lines = []
+ elif _GOOGLETEST_FAILED in line:
+ result.extend(test_lines)
+ test_lines = []
+ result.append(line)
else:
- failed_nodes_match = _FAILED_RE.match(line)
- failure_begins = False
- if failed_nodes_match:
- failure_begins = True
- failure_lines.extend([last_line, line])
- last_line = line
+ test_lines.append(line)
+ elif _GOOGLETEST_RUN in line:
+ test_lines.append(line)
+ elif _GOOGLETEST_DISABLED in line:
+ pass
+ else:
+ result.append(line)
+ result.extend(test_lines)
+ return result
+
+
+def _parse_ninja(ins: IO) -> str:
+ failure_lines: List[str] = []
+ last_line: str = ''
+
+ for line in ins:
+ _LOG.debug('processing %r', line)
+ # Trailing whitespace isn't significant, as it doesn't affect the
+ # way the line shows up in the logs. However, leading whitespace may
+ # be significant, especially for compiler error messages.
+ line = line.rstrip()
+
+ if failure_lines:
+ _LOG.debug('inside failure block')
+
+ if re.match(_COLOR_CODES_PREFIX + r'\[\d+/\d+\] (\S+)', line):
+ _LOG.debug('next rule started, ending failure block')
+ break
+
+ if re.match(_COLOR_CODES_PREFIX + r'ninja: build stopped:.*', line):
+ _LOG.debug('ninja build stopped, ending failure block')
+ break
+ failure_lines.append(line)
+
+ else:
+ if re.match(_COLOR_CODES_PREFIX + r'FAILED: (.*)$', line):
+ _LOG.debug('starting failure block')
+ failure_lines.extend([last_line, line])
+ elif 'FAILED' in line:
+ _LOG.debug('not a match')
+ last_line = line
+
+ failure_lines = _remove_passing_tests(failure_lines)
# Remove "Requirement already satisfied:" lines since many of those might
# be printed during Python installation, and they usually have no relevance
@@ -60,5 +101,25 @@ def parse_ninja_stdout(ninja_stdout: Path) -> str:
if not x.lstrip().startswith('Requirement already satisfied:')
]
- result = '\n'.join(failure_lines)
+ result: str = '\n'.join(failure_lines)
return re.sub(r'\n+', '\n', result)
+
+
+def parse_ninja_stdout(ninja_stdout: Path) -> str:
+ """Extract an error summary from ninja output."""
+ with ninja_stdout.open() as ins:
+ return _parse_ninja(ins)
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser()
+ parser.add_argument('input', type=argparse.FileType('r'))
+ parser.add_argument('output', type=argparse.FileType('w'))
+ args = parser.parse_args(argv)
+
+ for line in _parse_ninja(args.input):
+ args.output.write(line)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_presubmit/py/pw_presubmit/npm_presubmit.py b/pw_presubmit/py/pw_presubmit/npm_presubmit.py
index 565d6c91c..17ccc59a4 100644
--- a/pw_presubmit/py/pw_presubmit/npm_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/npm_presubmit.py
@@ -13,7 +13,8 @@
# the License.
"""Presubmit to npm install and run tests"""
-from pw_presubmit import call, PresubmitContext
+from pw_presubmit.presubmit import call
+from pw_presubmit.presubmit_context import PresubmitContext
def npm_test(ctx: PresubmitContext) -> None:
diff --git a/pw_presubmit/py/pw_presubmit/owners_checks.py b/pw_presubmit/py/pw_presubmit/owners_checks.py
index d751ff9a7..2d0d177e0 100644
--- a/pw_presubmit/py/pw_presubmit/owners_checks.py
+++ b/pw_presubmit/py/pw_presubmit/owners_checks.py
@@ -35,8 +35,9 @@ from typing import (
Set,
Union,
)
+
from pw_presubmit import git_repo
-from pw_presubmit.presubmit import PresubmitFailure
+from pw_presubmit.presubmit_context import PresubmitFailure
_LOG = logging.getLogger(__name__)
@@ -74,10 +75,6 @@ _LINE_TYPERS: OrderedDict[
class OwnersError(Exception):
"""Generic level OWNERS file error."""
- def __init__(self, message: str, *args: object) -> None:
- super().__init__(*args)
- self.message = message
-
class FormatterError(OwnersError):
"""Errors where formatter doesn't know how to act."""
@@ -126,8 +123,9 @@ class OwnersFile:
def __init__(self, path: pathlib.Path) -> None:
if not path.exists():
- error_msg = f"Tried to import {path} but it does not exists"
- raise OwnersDependencyError(error_msg)
+ raise OwnersDependencyError(
+ f"Tried to import {path} but it does not exist"
+ )
self.path = path
self.original_lines = self.load_owners_file(self.path)
@@ -335,9 +333,10 @@ class OwnersFile:
# all file: rules:
for file_rule in self.sections.get(LineType.FILE_RULE, []):
file_str = file_rule.content[len("file:") :]
- if ":" in file_str:
+ path = self.__complete_path(file_str)
+ if ":" in file_str and not path.is_file():
_LOG.warning(
- "TODO(b/254322931): This check does not yet support "
+ "TODO: b/254322931 - This check does not yet support "
"<project> or <branch> in a file: rule"
)
_LOG.warning(
@@ -346,7 +345,8 @@ class OwnersFile:
self.path,
)
- dependencies.append(self.__complete_path(file_str))
+ else:
+ dependencies.append(path)
# all the per-file rule includes
for per_file in self.sections.get(LineType.PER_FILE, []):
@@ -393,7 +393,8 @@ def _format_owners_file(owners_obj: OwnersFile) -> None:
def _list_unwrapper(
- func, list_or_path: Union[Iterable[pathlib.Path], pathlib.Path]
+ func: Callable[[OwnersFile], None],
+ list_or_path: Union[Iterable[pathlib.Path], pathlib.Path],
) -> Dict[pathlib.Path, str]:
"""Decorator that accepts Paths or list of Paths and iterates as needed."""
errors: Dict[pathlib.Path, str] = {}
@@ -415,10 +416,8 @@ def _list_unwrapper(
try:
func(current_owners)
except OwnersError as err:
- errors[current_owners.path] = err.message
- _LOG.error(
- "%s: %s", str(current_owners.path.absolute()), err.message
- )
+ errors[current_owners.path] = str(err)
+ _LOG.error("%s: %s", current_owners.path.absolute(), err)
return errors
@@ -452,7 +451,7 @@ def main() -> int:
owners_obj.look_for_owners_errors()
owners_obj.check_style()
except OwnersError as err:
- _LOG.error("%s %s", err, err.message)
+ _LOG.error("%s", err)
return 1
return 0
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index 1369fbc5e..65f1d7f0e 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -26,14 +26,6 @@ import subprocess
import sys
from typing import Callable, Iterable, List, Sequence, TextIO
-try:
- import pw_presubmit
-except ImportError:
- # Append the pw_presubmit package path to the module search path to allow
- # running this module without installing the pw_presubmit package.
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- import pw_presubmit
-
import pw_package.pigweed_packages
from pw_presubmit import (
@@ -43,23 +35,31 @@ from pw_presubmit import (
format_code,
git_repo,
gitmodules,
- call,
- filter_paths,
inclusive_language,
+ javascript_checks,
+ json_check,
keep_sorted,
module_owners,
npm_presubmit,
owners_checks,
- plural,
- presubmit,
- PresubmitContext,
- PresubmitFailure,
- Programs,
python_checks,
shell_checks,
source_in_build,
todo_check,
)
+from pw_presubmit.presubmit import (
+ FileFilter,
+ Programs,
+ call,
+ filter_paths,
+)
+from pw_presubmit.presubmit_context import (
+ FormatOptions,
+ PresubmitContext,
+ PresubmitFailure,
+)
+from pw_presubmit.tools import log_run, plural
+
from pw_presubmit.install_hook import install_git_hook
_LOG = logging.getLogger(__name__)
@@ -67,7 +67,7 @@ _LOG = logging.getLogger(__name__)
pw_package.pigweed_packages.initialize()
# Trigger builds if files with these extensions change.
-_BUILD_FILE_FILTER = presubmit.FileFilter(
+_BUILD_FILE_FILTER = FileFilter(
suffix=(
*format_code.C_FORMAT.extensions,
'.cfg',
@@ -90,17 +90,24 @@ def _at_all_optimization_levels(target):
#
# Build presubmit checks
#
+gn_all = build.GnGenNinja(
+ name='gn_all',
+ path_filter=_BUILD_FILE_FILTER,
+ gn_args=dict(pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS),
+ ninja_targets=('all',),
+)
+
+
def gn_clang_build(ctx: PresubmitContext):
"""Checks all compile targets that rely on LLVM tooling."""
build_targets = [
*_at_all_optimization_levels('host_clang'),
- 'cpp14_compatibility',
'cpp20_compatibility',
'asan',
'tsan',
'ubsan',
'runtime_sanitizers',
- # TODO(b/234876100): msan will not work until the C++ standard library
+ # TODO: b/234876100 - msan will not work until the C++ standard library
# included in the sysroot has a variant built with msan.
]
@@ -110,18 +117,19 @@ def gn_clang_build(ctx: PresubmitContext):
# QEMU doesn't run on Windows.
if sys.platform != 'win32':
- # TODO(b/244604080): For the pw::InlineString tests, qemu_clang_debug
+ # TODO: b/244604080 - For the pw::InlineString tests, qemu_clang_debug
# and qemu_clang_speed_optimized produce a binary too large for the
# QEMU target's 256KB flash. Restore debug and speed optimized
# builds when this is fixed.
build_targets.append('qemu_clang_size_optimized')
- # TODO(b/240982565): SocketStream currently requires Linux.
+ # TODO: b/240982565 - SocketStream currently requires Linux.
if sys.platform.startswith('linux'):
build_targets.append('integration_tests')
build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
build.ninja(ctx, *build_targets)
+ build.gn_check(ctx)
_HOST_COMPILER = 'gcc' if sys.platform == 'win32' else 'clang'
@@ -141,6 +149,7 @@ def gn_full_qemu_check(ctx: PresubmitContext):
*_at_all_optimization_levels('qemu_gcc'),
*_at_all_optimization_levels('qemu_clang'),
)
+ build.gn_check(ctx)
def _gn_combined_build_check_targets() -> Sequence[str]:
@@ -151,13 +160,16 @@ def _gn_combined_build_check_targets() -> Sequence[str]:
'python.tests',
'python.lint',
'docs',
- 'fuzzers',
'pigweed_pypi_distribution',
]
- # TODO(b/234645359): Re-enable on Windows when compatibility tests build.
+ # C headers seem to be missing when building with pw_minimal_cpp_stdlib, so
+ # skip it on Windows.
+ if sys.platform != 'win32':
+ build_targets.append('build_with_pw_minimal_cpp_stdlib')
+
+ # TODO: b/234645359 - Re-enable on Windows when compatibility tests build.
if sys.platform != 'win32':
- build_targets.append('cpp14_compatibility')
build_targets.append('cpp20_compatibility')
# clang-tidy doesn't run on Windows.
@@ -166,18 +178,21 @@ def _gn_combined_build_check_targets() -> Sequence[str]:
# QEMU doesn't run on Windows.
if sys.platform != 'win32':
- build_targets.extend(_at_all_optimization_levels('qemu_gcc'))
-
- # TODO(b/244604080): For the pw::InlineString tests, qemu_clang_debug
- # and qemu_clang_speed_optimized produce a binary too large for the
+ # TODO: b/244604080 - For the pw::InlineString tests, qemu_*_debug
+ # and qemu_*_speed_optimized produce a binary too large for the
# QEMU target's 256KB flash. Restore debug and speed optimized
# builds when this is fixed.
+ build_targets.append('qemu_gcc_size_optimized')
build_targets.append('qemu_clang_size_optimized')
- # TODO(b/240982565): SocketStream currently requires Linux.
+ # TODO: b/240982565 - SocketStream currently requires Linux.
if sys.platform.startswith('linux'):
build_targets.append('integration_tests')
+ # TODO: b/269354373 - clang is not supported on windows yet
+ if sys.platform != 'win32':
+ build_targets.append('host_clang_debug_dynamic_allocation')
+
return build_targets
@@ -185,15 +200,37 @@ gn_combined_build_check = build.GnGenNinja(
name='gn_combined_build_check',
doc='Run most host and device (QEMU) tests.',
path_filter=_BUILD_FILE_FILTER,
- gn_args=dict(pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS),
+ gn_args=dict(
+ pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
+ pw_BUILD_BROKEN_GROUPS=True, # Enable to fully test the GN build
+ ),
ninja_targets=_gn_combined_build_check_targets(),
)
+coverage = build.GnGenNinja(
+ name='coverage',
+ doc='Run coverage for the host build.',
+ path_filter=_BUILD_FILE_FILTER,
+ ninja_targets=('coverage',),
+ coverage_options=build.CoverageOptions(
+ target_bucket_root='gs://ng3-metrics/ng3-pigweed-coverage',
+ target_bucket_project='pigweed',
+ project='codesearch',
+ trace_type='LLVM',
+ trim_prefix='/b/s/w/ir/x/w/co',
+ ref='refs/heads/main',
+ source='infra:main',
+ owner='pigweed-infra@google.com',
+ bug_component='503634',
+ ),
+)
+
@_BUILD_FILE_FILTER.apply_to_check()
def gn_arm_build(ctx: PresubmitContext):
build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
build.ninja(ctx, *_at_all_optimization_levels('stm32f429i'))
+ build.gn_check(ctx)
stm32f429i = build.GnGenNinja(
@@ -212,7 +249,6 @@ stm32f429i = build.GnGenNinja(
ninja_targets=_at_all_optimization_levels('stm32f429i'),
)
-
gn_emboss_build = build.GnGenNinja(
name='gn_emboss_build',
packages=('emboss',),
@@ -241,48 +277,57 @@ gn_nanopb_build = build.GnGenNinja(
),
)
-gn_crypto_mbedtls_build = build.GnGenNinja(
- name='gn_crypto_mbedtls_build',
+gn_chre_build = build.GnGenNinja(
+ name='gn_chre_build',
path_filter=_BUILD_FILE_FILTER,
- packages=('mbedtls',),
- gn_args={
- 'dir_pw_third_party_mbedtls': lambda ctx: '"{}"'.format(
- ctx.package_root / 'mbedtls'
+ packages=('chre',),
+ gn_args=dict(
+ dir_pw_third_party_chre=lambda ctx: '"{}"'.format(
+ ctx.package_root / 'chre'
),
- 'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
- ctx.root / 'pw_crypto:sha256_mbedtls'
+ pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
+ ),
+ ninja_targets=(*_at_all_optimization_levels('host_clang'),),
+)
+
+gn_emboss_nanopb_build = build.GnGenNinja(
+ name='gn_emboss_nanopb_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('emboss', 'nanopb'),
+ gn_args=dict(
+ dir_pw_third_party_emboss=lambda ctx: '"{}"'.format(
+ ctx.package_root / 'emboss'
),
- 'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
- ctx.root / 'pw_crypto:ecdsa_mbedtls'
+ dir_pw_third_party_nanopb=lambda ctx: '"{}"'.format(
+ ctx.package_root / 'nanopb'
),
- 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
- },
+ pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
+ ),
ninja_targets=(
- *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
- # TODO(b/240982565): SocketStream currently requires Linux.
- *(('integration_tests',) if sys.platform.startswith('linux') else ()),
+ *_at_all_optimization_levels('stm32f429i'),
+ *_at_all_optimization_levels('host_clang'),
),
)
-gn_crypto_boringssl_build = build.GnGenNinja(
- name='gn_crypto_boringssl_build',
+gn_crypto_mbedtls_build = build.GnGenNinja(
+ name='gn_crypto_mbedtls_build',
path_filter=_BUILD_FILE_FILTER,
- packages=('boringssl',),
+ packages=('mbedtls',),
gn_args={
- 'dir_pw_third_party_boringssl': lambda ctx: '"{}"'.format(
- ctx.package_root / 'boringssl'
+ 'dir_pw_third_party_mbedtls': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'mbedtls'
),
'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
- ctx.root / 'pw_crypto:sha256_boringssl'
+ ctx.root / 'pw_crypto:sha256_mbedtls_v3'
),
'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
- ctx.root / 'pw_crypto:ecdsa_boringssl'
+ ctx.root / 'pw_crypto:ecdsa_mbedtls_v3'
),
'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
},
ninja_targets=(
*_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
- # TODO(b/240982565): SocketStream currently requires Linux.
+ # TODO: b/240982565 - SocketStream currently requires Linux.
*(('integration_tests',) if sys.platform.startswith('linux') else ()),
),
)
@@ -302,7 +347,7 @@ gn_crypto_micro_ecc_build = build.GnGenNinja(
},
ninja_targets=(
*_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
- # TODO(b/240982565): SocketStream currently requires Linux.
+ # TODO: b/240982565 - SocketStream currently requires Linux.
*(('integration_tests',) if sys.platform.startswith('linux') else ()),
),
)
@@ -316,7 +361,7 @@ gn_teensy_build = build.GnGenNinja(
str(ctx.package_root)
),
'pw_arduino_build_CORE_NAME': 'teensy',
- 'pw_arduino_build_PACKAGE_NAME': 'teensy/avr',
+ 'pw_arduino_build_PACKAGE_NAME': 'avr/1.58.1',
'pw_arduino_build_BOARD': 'teensy40',
'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
},
@@ -336,6 +381,41 @@ gn_pico_build = build.GnGenNinja(
ninja_targets=('pi_pico',),
)
+gn_mimxrt595_build = build.GnGenNinja(
+ name='gn_mimxrt595_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('mcuxpresso',),
+ gn_args={
+ 'dir_pw_third_party_mcuxpresso': lambda ctx: '"{}"'.format(
+ str(ctx.package_root / 'mcuxpresso')
+ ),
+ 'pw_target_mimxrt595_evk_MANIFEST': '$dir_pw_third_party_mcuxpresso'
+ + '/EVK-MIMXRT595_manifest_v3_8.xml',
+ 'pw_third_party_mcuxpresso_SDK': '//targets/mimxrt595_evk:sample_sdk',
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=('mimxrt595'),
+)
+
+gn_mimxrt595_freertos_build = build.GnGenNinja(
+ name='gn_mimxrt595_freertos_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('freertos', 'mcuxpresso'),
+ gn_args={
+ 'dir_pw_third_party_freertos': lambda ctx: '"{}"'.format(
+ str(ctx.package_root / 'freertos')
+ ),
+ 'dir_pw_third_party_mcuxpresso': lambda ctx: '"{}"'.format(
+ str(ctx.package_root / 'mcuxpresso')
+ ),
+ 'pw_target_mimxrt595_evk_freertos_MANIFEST': '{}/{}'.format(
+ "$dir_pw_third_party_mcuxpresso", "EVK-MIMXRT595_manifest_v3_8.xml"
+ ),
+ 'pw_third_party_mcuxpresso_SDK': '//targets/mimxrt595_evk_freertos:sdk',
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=('mimxrt595_freertos'),
+)
gn_software_update_build = build.GnGenNinja(
name='gn_software_update_build',
@@ -358,7 +438,7 @@ gn_software_update_build = build.GnGenNinja(
ctx.package_root / 'mbedtls'
),
'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
- ctx.root / 'pw_crypto:sha256_mbedtls'
+ ctx.root / 'pw_crypto:sha256_mbedtls_v3'
),
'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
},
@@ -386,7 +466,152 @@ gn_pw_system_demo_build = build.GnGenNinja(
ninja_targets=('pw_system_demo',),
)
-gn_docs_build = build.GnGenNinja(name='gn_docs_build', ninja_targets=('docs',))
+gn_googletest_build = build.GnGenNinja(
+ name='gn_googletest_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('googletest',),
+ gn_args={
+ 'dir_pw_third_party_googletest': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'googletest'
+ ),
+ 'pw_unit_test_MAIN': lambda ctx: '"{}"'.format(
+ ctx.root / 'third_party/googletest:gmock_main'
+ ),
+ 'pw_unit_test_GOOGLETEST_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'third_party/googletest'
+ ),
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
+)
+
+gn_fuzz_build = build.GnGenNinja(
+ name='gn_fuzz_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('abseil-cpp', 'fuzztest', 'googletest', 're2'),
+ gn_args={
+ 'dir_pw_third_party_abseil_cpp': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'abseil-cpp'
+ ),
+ 'dir_pw_third_party_fuzztest': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'fuzztest'
+ ),
+ 'dir_pw_third_party_googletest': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'googletest'
+ ),
+ 'dir_pw_third_party_re2': lambda ctx: '"{}"'.format(
+ ctx.package_root / 're2'
+ ),
+ 'pw_unit_test_MAIN': lambda ctx: '"{}"'.format(
+ ctx.root / 'third_party/googletest:gmock_main'
+ ),
+ 'pw_unit_test_GOOGLETEST_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'third_party/googletest'
+ ),
+ },
+ ninja_targets=('fuzzers',),
+ ninja_contexts=(
+ lambda ctx: build.modified_env(
+ FUZZTEST_PRNG_SEED=build.fuzztest_prng_seed(ctx),
+ ),
+ ),
+)
+
+oss_fuzz_build = build.GnGenNinja(
+ name='oss_fuzz_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('abseil-cpp', 'fuzztest', 'googletest', 're2'),
+ gn_args={
+ 'dir_pw_third_party_abseil_cpp': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'abseil-cpp'
+ ),
+ 'dir_pw_third_party_fuzztest': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'fuzztest'
+ ),
+ 'dir_pw_third_party_googletest': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'googletest'
+ ),
+ 'dir_pw_third_party_re2': lambda ctx: '"{}"'.format(
+ ctx.package_root / 're2'
+ ),
+ 'pw_toolchain_OSS_FUZZ_ENABLED': True,
+ },
+ ninja_targets=('oss_fuzz',),
+)
+
+
+def _env_with_zephyr_vars(ctx: PresubmitContext) -> dict:
+ """Returns the environment variables with ... set for Zephyr."""
+ env = os.environ.copy()
+ # Set some variables here.
+ env['ZEPHYR_BASE'] = str(ctx.package_root / 'zephyr')
+ env['ZEPHYR_MODULES'] = str(ctx.root)
+ return env
+
+
+def zephyr_build(ctx: PresubmitContext) -> None:
+ """Run Zephyr compatible tests"""
+ # Install the Zephyr package
+ build.install_package(ctx, 'zephyr')
+ # Configure the environment
+ env = _env_with_zephyr_vars(ctx)
+ # Get the python twister runner
+ twister = ctx.package_root / 'zephyr' / 'scripts' / 'twister'
+ # Run twister
+ call(
+ sys.executable,
+ twister,
+ '--ninja',
+ '--integration',
+ '--clobber-output',
+ '--inline-logs',
+ '--verbose',
+ '--testsuite-root',
+ ctx.root / 'pw_unit_test_zephyr',
+ env=env,
+ )
+ # Produces reports at (ctx.root / 'twister_out' / 'twister*.xml')
+
+
+def docs_build(ctx: PresubmitContext) -> None:
+ """Build Pigweed docs"""
+
+ # Build main docs through GN/Ninja.
+ build.install_package(ctx, 'nanopb')
+ build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
+ build.ninja(ctx, 'docs')
+ build.gn_check(ctx)
+
+ # Build Rust docs through Bazel.
+ build.bazel(
+ ctx,
+ 'build',
+ '--',
+ '//pw_rust:docs',
+ )
+
+ # Copy rust docs from Bazel's out directory into where the GN build
+ # put the main docs.
+ rust_docs_bazel_dir = ctx.output_dir.joinpath(
+ '.bazel-bin', 'pw_rust', 'docs.rustdoc'
+ )
+ rust_docs_output_dir = ctx.output_dir.joinpath(
+ 'docs', 'gen', 'docs', 'html', 'rustdoc'
+ )
+
+ # Remove the docs tree to avoid including stale files from previous runs.
+ shutil.rmtree(rust_docs_output_dir, ignore_errors=True)
+
+ # Bazel generates files and directories without write permissions. In
+ # order to allow this rule to be run multiple times we use shutil.copyfile
+ # for the actual copies to not copy permissions of files.
+ shutil.copytree(
+ rust_docs_bazel_dir,
+ rust_docs_output_dir,
+ copy_function=shutil.copyfile,
+ dirs_exist_ok=True,
+ )
+
gn_host_tools = build.GnGenNinja(
name='gn_host_tools',
@@ -418,6 +643,7 @@ def _run_cmake(ctx: PresubmitContext, toolchain='host_clang') -> None:
def cmake_clang(ctx: PresubmitContext):
_run_cmake(ctx, toolchain='host_clang')
build.ninja(ctx, 'pw_apps', 'pw_run_tests.modules')
+ build.gn_check(ctx)
@filter_paths(
@@ -426,6 +652,7 @@ def cmake_clang(ctx: PresubmitContext):
def cmake_gcc(ctx: PresubmitContext):
_run_cmake(ctx, toolchain='host_gcc')
build.ninja(ctx, 'pw_apps', 'pw_run_tests.modules')
+ build.gn_check(ctx)
@filter_paths(
@@ -497,20 +724,25 @@ def bazel_build(ctx: PresubmitContext) -> None:
# This is just a minimal presubmit intended to ensure we don't break what
# support we have.
#
- # TODO(b/271465588): Eventually just build the entire repo for this
+ # TODO: b/271465588 - Eventually just build the entire repo for this
# platform.
build.bazel(
ctx,
'build',
- # Designated initializers produce a warning-treated-as-error when
- # compiled with -std=c++17.
- #
- # TODO(b/271299438): Remove this.
- '--copt=-Wno-pedantic',
'--platforms=//pw_build/platforms:testonly_freertos',
'//pw_sync/...',
'//pw_thread/...',
'//pw_thread_freertos/...',
+ '//pw_interrupt/...',
+ '//pw_cpu_exception/...',
+ )
+
+ # Build the pw_system example for the Discovery board using STM32Cube.
+ build.bazel(
+ ctx,
+ 'build',
+ '--config=stm32f429i',
+ '//pw_system:system_example',
)
@@ -556,7 +788,7 @@ def _clang_system_include_paths(lang: str) -> List[str]:
f'{os.devnull}',
'-fsyntax-only',
]
- process = subprocess.run(
+ process = log_run(
command, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
@@ -597,6 +829,13 @@ _EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
r'\bDoxyfile$',
r'\bPW_PLUGINS$',
r'\bconstraint.list$',
+ r'\bconstraint_hashes_darwin.list$',
+ r'\bconstraint_hashes_linux.list$',
+ r'\bconstraint_hashes_windows.list$',
+ r'\bpython_base_requirements.txt$',
+ r'\bupstream_requirements_darwin_lock.txt$',
+ r'\bupstream_requirements_linux_lock.txt$',
+ r'\bupstream_requirements_windows_lock.txt$',
r'^(?:.+/)?\..+$',
# keep-sorted: end
# Metadata
@@ -611,6 +850,7 @@ _EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
r'\brequirements.txt$',
r'\byarn.lock$',
r'^docker/tag$',
+ r'^patches.json$',
# keep-sorted: end
# Data files
# keep-sorted: start
@@ -623,6 +863,7 @@ _EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
r'\.json$',
r'\.png$',
r'\.svg$',
+ r'\.vsix$',
r'\.xml$',
# keep-sorted: end
# Documentation
@@ -636,6 +877,10 @@ _EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
r'\.pb\.h$',
r'\_pb2.pyi?$',
# keep-sorted: end
+ # Generated third-party files
+ # keep-sorted: start
+ r'\bthird_party/.*\.bazelrc$',
+ # keep-sorted: end
# Diff/Patch files
# keep-sorted: start
r'\.diff$',
@@ -674,6 +919,8 @@ _COPYRIGHT = re.compile(
_SKIP_LINE_PREFIXES = (
'#!',
+ '#autoload',
+ '#compdef',
'@echo off',
':<<',
'/*',
@@ -710,9 +957,12 @@ def copyright_notice(ctx: PresubmitContext):
if path.stat().st_size == 0:
continue # Skip empty files
- with path.open() as file:
- if not _COPYRIGHT.match(''.join(_read_notice_lines(file))):
- errors.append(path)
+ try:
+ with path.open() as file:
+ if not _COPYRIGHT.match(''.join(_read_notice_lines(file))):
+ errors.append(path)
+ except UnicodeDecodeError as exc:
+ raise PresubmitFailure(f'failed to read {path}') from exc
if errors:
_LOG.warning(
@@ -793,7 +1043,7 @@ def commit_message_format(_: PresubmitContext):
if (
'Reland' in lines[0]
and 'This is a reland of ' in git_repo.commit_message()
- and "Original change's description: " in git_repo.commit_message()
+ and "Original change's description:" in git_repo.commit_message()
):
_LOG.warning('Ignoring apparent Gerrit-generated reland')
return
@@ -819,7 +1069,7 @@ def commit_message_format(_: PresubmitContext):
# Check that the first line matches the expected pattern.
match = re.match(
- r'^(?:[\w*/]+(?:{[\w* ,]+})?[\w*/]*|SEED-\d+): (?P<desc>.+)$', lines[0]
+ r'^(?:[.\w*/]+(?:{[\w* ,]+})?[\w*/]*|SEED-\d+): (?P<desc>.+)$', lines[0]
)
if not match:
_LOG.warning('The first line does not match the expected format')
@@ -886,6 +1136,7 @@ def static_analysis(ctx: PresubmitContext):
"""Runs all available static analysis tools."""
build.gn_gen(ctx)
build.ninja(ctx, 'python.lint', 'static_analysis')
+ build.gn_check(ctx)
_EXCLUDE_FROM_TODO_CHECK = (
@@ -895,6 +1146,7 @@ _EXCLUDE_FROM_TODO_CHECK = (
r'.gitignore$',
r'.pylintrc$',
r'\bdocs/build_system.rst',
+ r'\bdocs/code_reviews.rst',
r'\bpw_assert_basic/basic_handler.cc',
r'\bpw_assert_basic/public/pw_assert_basic/handler.h',
r'\bpw_blob_store/public/pw_blob_store/flat_file_system_entry.h',
@@ -934,7 +1186,7 @@ def owners_lint_checks(ctx: PresubmitContext):
owners_checks.presubmit_check(ctx.paths)
-SOURCE_FILES_FILTER = presubmit.FileFilter(
+SOURCE_FILES_FILTER = FileFilter(
endswith=_BUILD_FILE_FILTER.endswith,
suffix=('.bazel', '.bzl', '.gn', '.gni', *_BUILD_FILE_FILTER.suffix),
exclude=(
@@ -945,42 +1197,55 @@ SOURCE_FILES_FILTER = presubmit.FileFilter(
),
)
-
#
# Presubmit check programs
#
OTHER_CHECKS = (
# keep-sorted: start
- # TODO(b/235277910): Enable all Bazel tests when they're fixed.
+ # TODO: b/235277910 - Enable all Bazel tests when they're fixed.
bazel_test,
build.gn_gen_check,
cmake_clang,
cmake_gcc,
- gitmodules.create(),
+ coverage,
+ # TODO: b/234876100 - Remove once msan is added to all_sanitizers().
+ cpp_checks.msan,
+ docs_build,
+ gitmodules.create(gitmodules.Config(allow_submodules=False)),
+ gn_all,
gn_clang_build,
gn_combined_build_check,
module_owners.presubmit_check(),
npm_presubmit.npm_test,
pw_transfer_integration_test,
+ python_checks.update_upstream_python_constraints,
+ python_checks.vendor_python_wheels,
# TODO(hepler): Many files are missing from the CMake build. Add this check
# to lintformat when the missing files are fixed.
source_in_build.cmake(SOURCE_FILES_FILTER, _run_cmake),
static_analysis,
stm32f429i,
todo_check.create(todo_check.BUGS_OR_USERNAMES),
+ zephyr_build,
# keep-sorted: end
)
+ARDUINO_PICO = (
+ gn_teensy_build,
+ gn_pico_build,
+ gn_pw_system_demo_build,
+)
+
+INTERNAL = (gn_mimxrt595_build, gn_mimxrt595_freertos_build)
+
# The misc program differs from other_checks in that checks in the misc
# program block CQ on Linux.
MISC = (
# keep-sorted: start
- gn_emboss_build,
- gn_nanopb_build,
- gn_pico_build,
- gn_pw_system_demo_build,
- gn_teensy_build,
+ gn_chre_build,
+ gn_emboss_nanopb_build,
+ gn_googletest_build,
# keep-sorted: end
)
@@ -988,15 +1253,16 @@ SANITIZERS = (cpp_checks.all_sanitizers(),)
SECURITY = (
# keep-sorted: start
- gn_crypto_boringssl_build,
gn_crypto_mbedtls_build,
gn_crypto_micro_ecc_build,
+ gn_fuzz_build,
gn_software_update_build,
+ oss_fuzz_build,
# keep-sorted: end
)
# Avoid running all checks on specific paths.
-PATH_EXCLUSIONS = (re.compile(r'\bthird_party/fuchsia/repo/'),)
+PATH_EXCLUSIONS = FormatOptions.load().exclude
_LINTFORMAT = (
commit_message_format,
@@ -1014,6 +1280,8 @@ _LINTFORMAT = (
source_in_build.gn(SOURCE_FILES_FILTER),
source_is_in_cmake_build_warn_only,
shell_checks.shellcheck if shutil.which('shellcheck') else (),
+ javascript_checks.eslint if shutil.which('npx') else (),
+ json_check.presubmit_check,
keep_sorted.presubmit_check,
todo_check_with_exceptions,
)
@@ -1025,14 +1293,14 @@ LINTFORMAT = (
# (https://stackoverflow.com/q/71024130/1224002). These are cached, but
# after a roll it can be quite slow.
source_in_build.bazel(SOURCE_FILES_FILTER),
- pw_presubmit.python_checks.check_python_versions,
- pw_presubmit.python_checks.gn_python_lint,
+ python_checks.check_python_versions,
+ python_checks.gn_python_lint,
)
QUICK = (
_LINTFORMAT,
gn_quick_build_check,
- # TODO(b/34884583): Re-enable CMake and Bazel for Mac after we have fixed
+ # TODO: b/34884583 - Re-enable CMake and Bazel for Mac after we have fixed
# the clang issues. The problem is that all clang++ invocations need the
# two extra flags: "-nostdc++" and "${clang_prefix}/../lib/libc++.a".
cmake_clang if sys.platform != 'darwin' else (),
@@ -1042,10 +1310,11 @@ FULL = (
_LINTFORMAT,
gn_combined_build_check,
gn_host_tools,
- bazel_test if sys.platform == 'linux' else (),
- bazel_build if sys.platform == 'linux' else (),
+ bazel_test,
+ bazel_build,
python_checks.gn_python_check,
python_checks.gn_python_test_coverage,
+ python_checks.check_upstream_python_constraints,
build_env_setup,
# Skip gn_teensy_build if running on Windows. The Teensycore installer is
# an exe that requires an admin role.
@@ -1054,7 +1323,9 @@ FULL = (
PROGRAMS = Programs(
# keep-sorted: start
+ arduino_pico=ARDUINO_PICO,
full=FULL,
+ internal=INTERNAL,
lintformat=LINTFORMAT,
misc=MISC,
other_checks=OTHER_CHECKS,
diff --git a/pw_presubmit/py/pw_presubmit/presubmit.py b/pw_presubmit/py/pw_presubmit/presubmit.py
index ef10a3abe..3d2303a9e 100644
--- a/pw_presubmit/py/pw_presubmit/presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/presubmit.py
@@ -51,11 +51,10 @@ import logging
import os
from pathlib import Path
import re
-import shutil
import signal
import subprocess
import sys
-import tempfile as tf
+import tempfile
import time
import types
from typing import (
@@ -73,14 +72,22 @@ from typing import (
Tuple,
Union,
)
-import urllib
import pw_cli.color
import pw_cli.env
-import pw_env_setup.config_file
from pw_package import package_manager
+
from pw_presubmit import git_repo, tools
from pw_presubmit.tools import plural
+from pw_presubmit.presubmit_context import (
+ FormatContext,
+ FormatOptions,
+ LuciContext,
+ PRESUBMIT_CONTEXT,
+ PresubmitContext,
+ PresubmitFailure,
+ log_check_traces,
+)
_LOG: logging.Logger = logging.getLogger(__name__)
@@ -121,23 +128,6 @@ def _box(style, left, middle, right, box=tools.make_box('><>')) -> str:
)
-class PresubmitFailure(Exception):
- """Optional exception to use for presubmit failures."""
-
- def __init__(
- self,
- description: str = '',
- path: Optional[Path] = None,
- line: Optional[int] = None,
- ):
- line_part: str = ''
- if line is not None:
- line_part = f'{line}:'
- super().__init__(
- f'{path}:{line_part} {description}' if path else description
- )
-
-
class PresubmitResult(enum.Enum):
PASS = 'PASSED' # Check completed successfully.
FAIL = 'FAILED' # Check failed.
@@ -214,66 +204,6 @@ class Programs(collections.abc.Mapping):
return len(self._programs)
-@dataclasses.dataclass(frozen=True)
-class FormatOptions:
- python_formatter: Optional[str] = 'yapf'
- black_path: Optional[str] = 'black'
-
- # TODO(b/264578594) Add exclude to pigweed.json file.
- # exclude: Sequence[re.Pattern] = dataclasses.field(default_factory=list)
-
- @staticmethod
- def load() -> 'FormatOptions':
- config = pw_env_setup.config_file.load()
- fmt = config.get('pw', {}).get('pw_presubmit', {}).get('format', {})
- return FormatOptions(
- python_formatter=fmt.get('python_formatter', 'yapf'),
- black_path=fmt.get('black_path', 'black'),
- # exclude=tuple(re.compile(x) for x in fmt.get('exclude', ())),
- )
-
-
-@dataclasses.dataclass
-class LuciPipeline:
- round: int
- builds_from_previous_iteration: Sequence[str]
-
- @staticmethod
- def create(
- bbid: int,
- fake_pipeline_props: Optional[Dict[str, Any]] = None,
- ) -> Optional['LuciPipeline']:
- pipeline_props: Dict[str, Any]
- if fake_pipeline_props is not None:
- pipeline_props = fake_pipeline_props
- else:
- pipeline_props = (
- get_buildbucket_info(bbid)
- .get('input', {})
- .get('properties', {})
- .get('$pigweed/pipeline', {})
- )
- if not pipeline_props.get('inside_a_pipeline', False):
- return None
-
- return LuciPipeline(
- round=int(pipeline_props['round']),
- builds_from_previous_iteration=list(
- pipeline_props['builds_from_previous_iteration']
- ),
- )
-
-
-def get_buildbucket_info(bbid) -> Dict[str, Any]:
- if not bbid or not shutil.which('bb'):
- return {}
-
- output = subprocess.check_output(
- ['bb', 'get', '-json', '-p', f'{bbid}'], text=True
- )
- return json.loads(output)
-
-
def download_cas_artifact(
ctx: PresubmitContext, digest: str, output_dir: str
) -> None:
@@ -327,8 +257,8 @@ def archive_cas_artifact(
for path in upload_paths:
assert os.path.abspath(path)
- with tf.NamedTemporaryFile(mode='w+t') as tmp_digest_file:
- with tf.NamedTemporaryFile(mode='w+t') as tmp_paths_file:
+ with tempfile.NamedTemporaryFile(mode='w+t') as tmp_digest_file:
+ with tempfile.NamedTemporaryFile(mode='w+t') as tmp_paths_file:
json_paths = json.dumps(
[
[str(root), str(os.path.relpath(path, root))]
@@ -357,257 +287,6 @@ def archive_cas_artifact(
return uploaded_digest
-@dataclasses.dataclass
-class LuciTrigger:
- """Details the pending change or submitted commit triggering the build."""
-
- number: int
- remote: str
- branch: str
- ref: str
- gerrit_name: str
- submitted: bool
-
- @property
- def gerrit_url(self):
- if not self.number:
- return self.gitiles_url
- return 'https://{}-review.googlesource.com/c/{}'.format(
- self.gerrit_name, self.number
- )
-
- @property
- def gitiles_url(self):
- return '{}/+/{}'.format(self.remote, self.ref)
-
- @staticmethod
- def create_from_environment(
- env: Optional[Dict[str, str]] = None,
- ) -> Sequence['LuciTrigger']:
- if not env:
- env = os.environ.copy()
- raw_path = env.get('TRIGGERING_CHANGES_JSON')
- if not raw_path:
- return ()
- path = Path(raw_path)
- if not path.is_file():
- return ()
-
- result = []
- with open(path, 'r') as ins:
- for trigger in json.load(ins):
- keys = {
- 'number',
- 'remote',
- 'branch',
- 'ref',
- 'gerrit_name',
- 'submitted',
- }
- if keys <= trigger.keys():
- result.append(LuciTrigger(**{x: trigger[x] for x in keys}))
-
- return tuple(result)
-
- @staticmethod
- def create_for_testing():
- change = {
- 'number': 123456,
- 'remote': 'https://pigweed.googlesource.com/pigweed/pigweed',
- 'branch': 'main',
- 'ref': 'refs/changes/56/123456/1',
- 'gerrit_name': 'pigweed',
- 'submitted': True,
- }
- with tf.TemporaryDirectory() as tempdir:
- changes_json = Path(tempdir) / 'changes.json'
- with changes_json.open('w') as outs:
- json.dump([change], outs)
- env = {'TRIGGERING_CHANGES_JSON': changes_json}
- return LuciTrigger.create_from_environment(env)
-
-
-@dataclasses.dataclass
-class LuciContext:
- """LUCI-specific information about the environment."""
-
- buildbucket_id: int
- build_number: int
- project: str
- bucket: str
- builder: str
- swarming_server: str
- swarming_task_id: str
- cas_instance: str
- pipeline: Optional[LuciPipeline]
- triggers: Sequence[LuciTrigger] = dataclasses.field(default_factory=tuple)
-
- @staticmethod
- def create_from_environment(
- env: Optional[Dict[str, str]] = None,
- fake_pipeline_props: Optional[Dict[str, Any]] = None,
- ) -> Optional['LuciContext']:
- """Create a LuciContext from the environment."""
-
- if not env:
- env = os.environ.copy()
-
- luci_vars = [
- 'BUILDBUCKET_ID',
- 'BUILDBUCKET_NAME',
- 'BUILD_NUMBER',
- 'SWARMING_TASK_ID',
- 'SWARMING_SERVER',
- ]
- if any(x for x in luci_vars if x not in env):
- return None
-
- project, bucket, builder = env['BUILDBUCKET_NAME'].split(':')
-
- bbid: int = 0
- pipeline: Optional[LuciPipeline] = None
- try:
- bbid = int(env['BUILDBUCKET_ID'])
- pipeline = LuciPipeline.create(bbid, fake_pipeline_props)
-
- except ValueError:
- pass
-
- # Logic to identify cas instance from swarming server is derived from
- # https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipe_modules/cas/api.py
- swarm_server = env['SWARMING_SERVER']
- cas_project = urllib.parse.urlparse(swarm_server).netloc.split('.')[0]
- cas_instance = f'projects/{cas_project}/instances/default_instance'
-
- result = LuciContext(
- buildbucket_id=bbid,
- build_number=int(env['BUILD_NUMBER']),
- project=project,
- bucket=bucket,
- builder=builder,
- swarming_server=env['SWARMING_SERVER'],
- swarming_task_id=env['SWARMING_TASK_ID'],
- cas_instance=cas_instance,
- pipeline=pipeline,
- triggers=LuciTrigger.create_from_environment(env),
- )
- _LOG.debug('%r', result)
- return result
-
- @staticmethod
- def create_for_testing():
- env = {
- 'BUILDBUCKET_ID': '881234567890',
- 'BUILDBUCKET_NAME': 'pigweed:bucket.try:builder-name',
- 'BUILD_NUMBER': '123',
- 'SWARMING_SERVER': 'https://chromium-swarm.appspot.com',
- 'SWARMING_TASK_ID': 'cd2dac62d2',
- }
- return LuciContext.create_from_environment(env, {})
-
-
-@dataclasses.dataclass
-class FormatContext:
- """Context passed into formatting helpers.
-
- This class is a subset of PresubmitContext containing only what's needed by
- formatters.
-
- For full documentation on the members see the PresubmitContext section of
- pw_presubmit/docs.rst.
-
- Args:
- root: Source checkout root directory
- output_dir: Output directory for this specific language
- paths: Modified files for the presubmit step to check (often used in
- formatting steps but ignored in compile steps)
- package_root: Root directory for pw package installations
- format_options: Formatting options, derived from pigweed.json
- """
-
- root: Optional[Path]
- output_dir: Path
- paths: Tuple[Path, ...]
- package_root: Path
- format_options: FormatOptions
-
-
-@dataclasses.dataclass
-class PresubmitContext: # pylint: disable=too-many-instance-attributes
- """Context passed into presubmit checks.
-
- For full documentation on the members see pw_presubmit/docs.rst.
-
- Args:
- root: Source checkout root directory
- repos: Repositories (top-level and submodules) processed by
- pw presubmit
- output_dir: Output directory for this specific presubmit step
- failure_summary_log: Path where steps should write a brief summary of
- any failures encountered for use by other tooling.
- paths: Modified files for the presubmit step to check (often used in
- formatting steps but ignored in compile steps)
- all_paths: All files in the tree.
- package_root: Root directory for pw package installations
- override_gn_args: Additional GN args processed by build.gn_gen()
- luci: Information about the LUCI build or None if not running in LUCI
- format_options: Formatting options, derived from pigweed.json
- num_jobs: Number of jobs to run in parallel
- continue_after_build_error: For steps that compile, don't exit on the
- first compilation error
- """
-
- root: Path
- repos: Tuple[Path, ...]
- output_dir: Path
- failure_summary_log: Path
- paths: Tuple[Path, ...]
- all_paths: Tuple[Path, ...]
- package_root: Path
- luci: Optional[LuciContext]
- override_gn_args: Dict[str, str]
- format_options: FormatOptions
- num_jobs: Optional[int] = None
- continue_after_build_error: bool = False
- _failed: bool = False
-
- @property
- def failed(self) -> bool:
- return self._failed
-
- def fail(
- self,
- description: str,
- path: Optional[Path] = None,
- line: Optional[int] = None,
- ):
- """Add a failure to this presubmit step.
-
- If this is called at least once the step fails, but not immediately—the
- check is free to continue and possibly call this method again.
- """
- _LOG.warning('%s', PresubmitFailure(description, path, line))
- self._failed = True
-
- @staticmethod
- def create_for_testing():
- parsed_env = pw_cli.env.pigweed_environment()
- root = parsed_env.PW_PROJECT_ROOT
- presubmit_root = root / 'out' / 'presubmit'
- return PresubmitContext(
- root=root,
- repos=(root,),
- output_dir=presubmit_root / 'test',
- failure_summary_log=presubmit_root / 'failure-summary.log',
- paths=(root / 'foo.cc', root / 'foo.py'),
- all_paths=(root / 'BUILD.gn', root / 'foo.cc', root / 'foo.py'),
- package_root=root / 'environment' / 'packages',
- luci=None,
- override_gn_args={},
- format_options=FormatOptions(),
- )
-
-
class FileFilter:
"""Allows checking if a path matches a series of filters.
@@ -707,7 +386,7 @@ class FilteredCheck:
class Presubmit:
"""Runs a series of presubmit checks on a list of files."""
- def __init__(
+ def __init__( # pylint: disable=too-many-arguments
self,
root: Path,
repos: Sequence[Path],
@@ -717,6 +396,8 @@ class Presubmit:
package_root: Path,
override_gn_args: Dict[str, str],
continue_after_build_error: bool,
+ rng_seed: int,
+ full: bool,
):
self._root = root.resolve()
self._repos = tuple(repos)
@@ -729,15 +410,17 @@ class Presubmit:
self._package_root = package_root.resolve()
self._override_gn_args = override_gn_args
self._continue_after_build_error = continue_after_build_error
+ self._rng_seed = rng_seed
+ self._full = full
def run(
self,
program: Program,
keep_going: bool = False,
substep: Optional[str] = None,
+ dry_run: bool = False,
) -> bool:
"""Executes a series of presubmit checks on the paths."""
-
checks = self.apply_filters(program)
if substep:
assert (
@@ -767,7 +450,9 @@ class Presubmit:
_LOG.debug('Checks:\n%s', '\n'.join(c.name for c in checks))
start_time: float = time.time()
- passed, failed, skipped = self._execute_checks(checks, keep_going)
+ passed, failed, skipped = self._execute_checks(
+ checks, keep_going, dry_run
+ )
self._log_summary(time.time() - start_time, passed, failed, skipped)
return not failed and not skipped
@@ -853,7 +538,7 @@ class Presubmit:
return PresubmitContext(**kwargs)
@contextlib.contextmanager
- def _context(self, filtered_check: FilteredCheck):
+ def _context(self, filtered_check: FilteredCheck, dry_run: bool = False):
# There are many characters banned from filenames on Windows. To
# simplify things, just strip everything that's not a letter, digit,
# or underscore.
@@ -882,21 +567,27 @@ class Presubmit:
package_root=self._package_root,
override_gn_args=self._override_gn_args,
continue_after_build_error=self._continue_after_build_error,
+ rng_seed=self._rng_seed,
+ full=self._full,
luci=LuciContext.create_from_environment(),
format_options=FormatOptions.load(),
+ dry_run=dry_run,
)
finally:
_LOG.removeHandler(handler)
def _execute_checks(
- self, program: List[FilteredCheck], keep_going: bool
+ self,
+ program: List[FilteredCheck],
+ keep_going: bool,
+ dry_run: bool = False,
) -> Tuple[int, int, int]:
"""Runs presubmit checks; returns (passed, failed, skipped) lists."""
passed = failed = 0
for i, filtered_check in enumerate(program, 1):
- with self._context(filtered_check) as ctx:
+ with self._context(filtered_check, dry_run) as ctx:
result = filtered_check.run(ctx, i, len(program))
if result is PresubmitResult.PASS:
@@ -944,6 +635,48 @@ def _process_pathspecs(
return pathspecs_by_repo
+def fetch_file_lists(
+ root: Path,
+ repo: Path,
+ pathspecs: List[str],
+ exclude: Sequence[Pattern] = (),
+ base: Optional[str] = None,
+) -> Tuple[List[Path], List[Path]]:
+ """Returns lists of all files and modified files for the given repo.
+
+ Args:
+ root: root path of the project
+ repo: path to the roots of Git repository to check
+ base: optional base Git commit to list files against
+ pathspecs: optional list of Git pathspecs to run the checks against
+ exclude: regular expressions for Posix-style paths to exclude
+ """
+
+ all_files: List[Path] = []
+ modified_files: List[Path] = []
+
+ all_files_repo = tuple(
+ tools.exclude_paths(
+ exclude, git_repo.list_files(None, pathspecs, repo), root
+ )
+ )
+ all_files += all_files_repo
+
+ if base is None:
+ modified_files += all_files_repo
+ else:
+ modified_files += tools.exclude_paths(
+ exclude, git_repo.list_files(base, pathspecs, repo), root
+ )
+
+ _LOG.info(
+ 'Checking %s',
+ git_repo.describe_files(repo, repo, base, pathspecs, exclude, root),
+ )
+
+ return all_files, modified_files
+
+
def run( # pylint: disable=too-many-arguments,too-many-locals
program: Sequence[Check],
root: Path,
@@ -957,9 +690,11 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
override_gn_args: Sequence[Tuple[str, str]] = (),
keep_going: bool = False,
continue_after_build_error: bool = False,
+ rng_seed: int = 1,
presubmit_class: type = Presubmit,
list_steps_file: Optional[Path] = None,
substep: Optional[str] = None,
+ dry_run: bool = False,
) -> bool:
"""Lists files in the current Git repo and runs a Presubmit with them.
@@ -987,6 +722,8 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
override_gn_args: additional GN args to set on steps
keep_going: continue running presubmit steps after a step fails
continue_after_build_error: continue building if a build step fails
+ rng_seed: seed for a random number generator, for the few steps that
+ need one
presubmit_class: class to use to run Presubmits, should inherit from
Presubmit class above
list_steps_file: File created by --only-list-steps, used to keep from
@@ -1030,26 +767,11 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
else:
for repo, pathspecs in pathspecs_by_repo.items():
- all_files_repo = tuple(
- tools.exclude_paths(
- exclude, git_repo.list_files(None, pathspecs, repo), root
- )
- )
- all_files += all_files_repo
-
- if base is None:
- modified_files += all_files_repo
- else:
- modified_files += tools.exclude_paths(
- exclude, git_repo.list_files(base, pathspecs, repo), root
- )
-
- _LOG.info(
- 'Checking %s',
- git_repo.describe_files(
- repo, repo, base, pathspecs, exclude, root
- ),
+ new_all_files_items, new_modified_file_items = fetch_file_lists(
+ root, repo, pathspecs, exclude, base
)
+ all_files.extend(new_all_files_items)
+ modified_files.extend(new_modified_file_items)
if output_directory is None:
output_directory = root / '.presubmit'
@@ -1066,6 +788,8 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
package_root=package_root,
override_gn_args=dict(override_gn_args or {}),
continue_after_build_error=continue_after_build_error,
+ rng_seed=rng_seed,
+ full=bool(base is None),
)
if only_list_steps:
@@ -1091,7 +815,7 @@ def run( # pylint: disable=too-many-arguments,too-many-locals
if not isinstance(program, Program):
program = Program('', program)
- return presubmit.run(program, keep_going, substep=substep)
+ return presubmit.run(program, keep_going, substep=substep, dry_run=dry_run)
def _make_str_tuple(value: Union[Iterable[str], str]) -> Tuple[str, ...]:
@@ -1195,6 +919,8 @@ class Check:
self.filter = path_filter
self.always_run: bool = always_run
+ self._is_presubmit_check_object = True
+
def substeps(self) -> Sequence[SubStep]:
"""Return the SubSteps of the current step.
@@ -1294,6 +1020,9 @@ class Check:
time_str = _format_time(time.time() - start_time_s)
_LOG.debug('%s %s', self.name, result.value)
+ if ctx.dry_run:
+ log_check_traces(ctx)
+
_print_ui(
_box(_CHECK_LOWER, result.colorized(_LEFT), self.name, time_str)
)
@@ -1416,7 +1145,7 @@ def filter_paths(
'endswith/exclude args, not both'
)
else:
- # TODO(b/238426363): Remove these arguments and use FileFilter only.
+ # TODO: b/238426363 - Remove these arguments and use FileFilter only.
real_file_filter = FileFilter(
endswith=_make_str_tuple(endswith), exclude=exclude
)
@@ -1427,8 +1156,19 @@ def filter_paths(
return filter_paths_for_function
-def call(*args, **kwargs) -> None:
+def call(
+ *args, call_annotation: Optional[Dict[Any, Any]] = None, **kwargs
+) -> None:
"""Optional subprocess wrapper that causes a PresubmitFailure on errors."""
+ ctx = PRESUBMIT_CONTEXT.get()
+ if ctx:
+ call_annotation = call_annotation if call_annotation else {}
+ ctx.append_check_command(
+ *args, call_annotation=call_annotation, **kwargs
+ )
+ if ctx.dry_run:
+ return
+
attributes, command = tools.format_command(args, kwargs)
_LOG.debug('[RUN] %s\n%s', attributes, command)
@@ -1495,6 +1235,16 @@ def install_package(
root = ctx.package_root
mgr = package_manager.PackageManager(root)
+ ctx.append_check_command(
+ 'pw',
+ 'package',
+ 'install',
+ name,
+ call_annotation={'pw_package_install': name},
+ )
+ if ctx.dry_run:
+ return
+
if not mgr.list():
raise PresubmitFailure(
'no packages configured, please import your pw_package '
diff --git a/pw_presubmit/py/pw_presubmit/presubmit_context.py b/pw_presubmit/py/pw_presubmit/presubmit_context.py
new file mode 100644
index 000000000..131be90c0
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/presubmit_context.py
@@ -0,0 +1,624 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""pw_presubmit ContextVar."""
+
+from contextvars import ContextVar
+import dataclasses
+import enum
+import inspect
+import logging
+import json
+import os
+from pathlib import Path
+import re
+import shlex
+import shutil
+import subprocess
+import tempfile
+from typing import (
+ Any,
+ Dict,
+ List,
+ Iterable,
+ NamedTuple,
+ Optional,
+ Sequence,
+ Tuple,
+ TYPE_CHECKING,
+)
+import urllib
+
+import pw_cli.color
+import pw_cli.env
+import pw_env_setup.config_file
+
+if TYPE_CHECKING:
+ from pw_presubmit.presubmit import Check
+
+_COLOR = pw_cli.color.colors()
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+PRESUBMIT_CHECK_TRACE: ContextVar[
+ Dict[str, List['PresubmitCheckTrace']]
+] = ContextVar('pw_presubmit_check_trace', default={})
+
+
+@dataclasses.dataclass(frozen=True)
+class FormatOptions:
+ python_formatter: Optional[str] = 'yapf'
+ black_path: Optional[str] = 'black'
+ exclude: Sequence[re.Pattern] = dataclasses.field(default_factory=list)
+
+ @staticmethod
+ def load(env: Optional[Dict[str, str]] = None) -> 'FormatOptions':
+ config = pw_env_setup.config_file.load(env=env)
+ fmt = config.get('pw', {}).get('pw_presubmit', {}).get('format', {})
+ return FormatOptions(
+ python_formatter=fmt.get('python_formatter', 'yapf'),
+ black_path=fmt.get('black_path', 'black'),
+ exclude=tuple(re.compile(x) for x in fmt.get('exclude', ())),
+ )
+
+ def filter_paths(self, paths: Iterable[Path]) -> Tuple[Path, ...]:
+ root = Path(pw_cli.env.pigweed_environment().PW_PROJECT_ROOT)
+ relpaths = [x.relative_to(root) for x in paths]
+
+ for filt in self.exclude:
+ relpaths = [x for x in relpaths if not filt.search(str(x))]
+ return tuple(root / x for x in relpaths)
+
+
+def get_buildbucket_info(bbid) -> Dict[str, Any]:
+ if not bbid or not shutil.which('bb'):
+ return {}
+
+ output = subprocess.check_output(
+ ['bb', 'get', '-json', '-p', f'{bbid}'], text=True
+ )
+ return json.loads(output)
+
+
+@dataclasses.dataclass
+class LuciPipeline:
+ """Details of previous builds in this pipeline, if applicable.
+
+ Attributes:
+ round: The zero-indexed round number.
+ builds_from_previous_iteration: A list of the buildbucket ids from the
+ previous round, if any.
+ """
+
+ round: int
+ builds_from_previous_iteration: Sequence[int]
+
+ @staticmethod
+ def create(
+ bbid: int,
+ fake_pipeline_props: Optional[Dict[str, Any]] = None,
+ ) -> Optional['LuciPipeline']:
+ pipeline_props: Dict[str, Any]
+ if fake_pipeline_props is not None:
+ pipeline_props = fake_pipeline_props
+ else:
+ pipeline_props = (
+ get_buildbucket_info(bbid)
+ .get('input', {})
+ .get('properties', {})
+ .get('$pigweed/pipeline', {})
+ )
+ if not pipeline_props.get('inside_a_pipeline', False):
+ return None
+
+ return LuciPipeline(
+ round=int(pipeline_props['round']),
+ builds_from_previous_iteration=list(
+ int(x) for x in pipeline_props['builds_from_previous_iteration']
+ ),
+ )
+
+
+@dataclasses.dataclass
+class LuciTrigger:
+ """Details the pending change or submitted commit triggering the build.
+
+ Attributes:
+ number: The number of the change in Gerrit.
+ patchset: The number of the patchset of the change.
+ remote: The full URL of the remote.
+ project: The name of the project in Gerrit.
+ branch: The name of the branch on which this change is being/was
+ submitted.
+ ref: The "refs/changes/.." path that can be used to reference the
+ patch for unsubmitted changes and the hash for submitted changes.
+ gerrit_name: The name of the googlesource.com Gerrit host.
+ submitted: Whether the change has been submitted or is still pending.
+ gerrit_host: The scheme and hostname of the googlesource.com Gerrit
+ host.
+ gerrit_url: The full URL to this change on the Gerrit host.
+ gitiles_url: The full URL to this commit in Gitiles.
+ """
+
+ number: int
+ patchset: int
+ remote: str
+ project: str
+ branch: str
+ ref: str
+ gerrit_name: str
+ submitted: bool
+
+ @property
+ def gerrit_host(self):
+ return f'https://{self.gerrit_name}-review.googlesource.com'
+
+ @property
+ def gerrit_url(self):
+ if not self.number:
+ return self.gitiles_url
+ return f'{self.gerrit_host}/c/{self.number}'
+
+ @property
+ def gitiles_url(self):
+ return f'{self.remote}/+/{self.ref}'
+
+ @staticmethod
+ def create_from_environment(
+ env: Optional[Dict[str, str]] = None,
+ ) -> Sequence['LuciTrigger']:
+ if not env:
+ env = os.environ.copy()
+ raw_path = env.get('TRIGGERING_CHANGES_JSON')
+ if not raw_path:
+ return ()
+ path = Path(raw_path)
+ if not path.is_file():
+ return ()
+
+ result = []
+ with open(path, 'r') as ins:
+ for trigger in json.load(ins):
+ keys = {
+ 'number',
+ 'patchset',
+ 'remote',
+ 'project',
+ 'branch',
+ 'ref',
+ 'gerrit_name',
+ 'submitted',
+ }
+ if keys <= trigger.keys():
+ result.append(LuciTrigger(**{x: trigger[x] for x in keys}))
+
+ return tuple(result)
+
+ @staticmethod
+ def create_for_testing(**kwargs):
+ change = {
+ 'number': 123456,
+ 'patchset': 1,
+ 'remote': 'https://pigweed.googlesource.com/pigweed/pigweed',
+ 'project': 'pigweed/pigweed',
+ 'branch': 'main',
+ 'ref': 'refs/changes/56/123456/1',
+ 'gerrit_name': 'pigweed',
+ 'submitted': True,
+ }
+ change.update(kwargs)
+
+ with tempfile.TemporaryDirectory() as tempdir:
+ changes_json = Path(tempdir) / 'changes.json'
+ with changes_json.open('w') as outs:
+ json.dump([change], outs)
+ env = {'TRIGGERING_CHANGES_JSON': changes_json}
+ return LuciTrigger.create_from_environment(env)
+
+
+@dataclasses.dataclass
+class LuciContext:
+ """LUCI-specific information about the environment.
+
+ Attributes:
+ buildbucket_id: The globally-unique buildbucket id of the build.
+ build_number: The builder-specific incrementing build number, if
+ configured for this builder.
+ project: The LUCI project under which this build is running (often
+ "pigweed" or "pigweed-internal").
+ bucket: The LUCI bucket under which this build is running (often ends
+ with "ci" or "try").
+ builder: The builder being run.
+ swarming_server: The swarming server on which this build is running.
+ swarming_task_id: The swarming task id of this build.
+ cas_instance: The CAS instance accessible from this build.
+ pipeline: Information about the build pipeline, if applicable.
+ triggers: Information about triggering commits, if applicable.
+ is_try: True if the bucket is a try bucket.
+ is_ci: True if the bucket is a ci bucket.
+ is_dev: True if the bucket is a dev bucket.
+ """
+
+ buildbucket_id: int
+ build_number: int
+ project: str
+ bucket: str
+ builder: str
+ swarming_server: str
+ swarming_task_id: str
+ cas_instance: str
+ pipeline: Optional[LuciPipeline]
+ triggers: Sequence[LuciTrigger] = dataclasses.field(default_factory=tuple)
+
+ @property
+ def is_try(self):
+ return re.search(r'\btry$', self.bucket)
+
+ @property
+ def is_ci(self):
+ return re.search(r'\bci$', self.bucket)
+
+ @property
+ def is_dev(self):
+ return re.search(r'\bdev\b', self.bucket)
+
+ @staticmethod
+ def create_from_environment(
+ env: Optional[Dict[str, str]] = None,
+ fake_pipeline_props: Optional[Dict[str, Any]] = None,
+ ) -> Optional['LuciContext']:
+ """Create a LuciContext from the environment."""
+
+ if not env:
+ env = os.environ.copy()
+
+ luci_vars = [
+ 'BUILDBUCKET_ID',
+ 'BUILDBUCKET_NAME',
+ 'BUILD_NUMBER',
+ 'SWARMING_TASK_ID',
+ 'SWARMING_SERVER',
+ ]
+ if any(x for x in luci_vars if x not in env):
+ return None
+
+ project, bucket, builder = env['BUILDBUCKET_NAME'].split(':')
+
+ bbid: int = 0
+ pipeline: Optional[LuciPipeline] = None
+ try:
+ bbid = int(env['BUILDBUCKET_ID'])
+ pipeline = LuciPipeline.create(bbid, fake_pipeline_props)
+
+ except ValueError:
+ pass
+
+ # Logic to identify cas instance from swarming server is derived from
+ # https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipe_modules/cas/api.py
+ swarm_server = env['SWARMING_SERVER']
+ cas_project = urllib.parse.urlparse(swarm_server).netloc.split('.')[0]
+ cas_instance = f'projects/{cas_project}/instances/default_instance'
+
+ result = LuciContext(
+ buildbucket_id=bbid,
+ build_number=int(env['BUILD_NUMBER']),
+ project=project,
+ bucket=bucket,
+ builder=builder,
+ swarming_server=env['SWARMING_SERVER'],
+ swarming_task_id=env['SWARMING_TASK_ID'],
+ cas_instance=cas_instance,
+ pipeline=pipeline,
+ triggers=LuciTrigger.create_from_environment(env),
+ )
+ _LOG.debug('%r', result)
+ return result
+
+ @staticmethod
+ def create_for_testing(**kwargs):
+ env = {
+ 'BUILDBUCKET_ID': '881234567890',
+ 'BUILDBUCKET_NAME': 'pigweed:bucket.try:builder-name',
+ 'BUILD_NUMBER': '123',
+ 'SWARMING_SERVER': 'https://chromium-swarm.appspot.com',
+ 'SWARMING_TASK_ID': 'cd2dac62d2',
+ }
+ env.update(kwargs)
+
+ return LuciContext.create_from_environment(env, {})
+
+
+@dataclasses.dataclass
+class FormatContext:
+ """Context passed into formatting helpers.
+
+ This class is a subset of PresubmitContext containing only what's needed by
+ formatters.
+
+ For full documentation on the members see the PresubmitContext section of
+ pw_presubmit/docs.rst.
+
+ Attributes:
+ root: Source checkout root directory
+ output_dir: Output directory for this specific language.
+ paths: Modified files for the presubmit step to check (often used in
+ formatting steps but ignored in compile steps).
+ package_root: Root directory for pw package installations.
+ format_options: Formatting options, derived from pigweed.json.
+ dry_run: Whether to just report issues or also fix them.
+ """
+
+ root: Optional[Path]
+ output_dir: Path
+ paths: Tuple[Path, ...]
+ package_root: Path
+ format_options: FormatOptions
+ dry_run: bool = False
+
+ def append_check_command(self, *command_args, **command_kwargs) -> None:
+ """Empty append_check_command."""
+
+
+class PresubmitFailure(Exception):
+ """Optional exception to use for presubmit failures."""
+
+ def __init__(
+ self,
+ description: str = '',
+ path: Optional[Path] = None,
+ line: Optional[int] = None,
+ ):
+ line_part: str = ''
+ if line is not None:
+ line_part = f'{line}:'
+ super().__init__(
+ f'{path}:{line_part} {description}' if path else description
+ )
+
+
+@dataclasses.dataclass
+class PresubmitContext: # pylint: disable=too-many-instance-attributes
+ """Context passed into presubmit checks.
+
+ For full documentation on the members see pw_presubmit/docs.rst.
+
+ Attributes:
+ root: Source checkout root directory.
+ repos: Repositories (top-level and submodules) processed by
+ `pw presubmit`.
+ output_dir: Output directory for this specific presubmit step.
+ failure_summary_log: Path where steps should write a brief summary of
+ any failures encountered for use by other tooling.
+ paths: Modified files for the presubmit step to check (often used in
+ formatting steps but ignored in compile steps).
+ all_paths: All files in the tree.
+ package_root: Root directory for pw package installations.
+ override_gn_args: Additional GN args processed by `build.gn_gen()`.
+ luci: Information about the LUCI build or None if not running in LUCI.
+ format_options: Formatting options, derived from pigweed.json.
+ num_jobs: Number of jobs to run in parallel.
+ continue_after_build_error: For steps that compile, don't exit on the
+ first compilation error.
+ rng_seed: Seed for a random number generator, for the few steps that
+ need one.
+ _failed: Whether the presubmit step in question has failed. Set to True
+ by calling ctx.fail().
+ full: Whether this is a full or incremental presubmit run.
+ """
+
+ root: Path
+ repos: Tuple[Path, ...]
+ output_dir: Path
+ failure_summary_log: Path
+ paths: Tuple[Path, ...]
+ all_paths: Tuple[Path, ...]
+ package_root: Path
+ luci: Optional[LuciContext]
+ override_gn_args: Dict[str, str]
+ format_options: FormatOptions
+ num_jobs: Optional[int] = None
+ continue_after_build_error: bool = False
+ rng_seed: int = 1
+ full: bool = False
+ _failed: bool = False
+ dry_run: bool = False
+
+ @property
+ def failed(self) -> bool:
+ return self._failed
+
+ @property
+ def incremental(self) -> bool:
+ return not self.full
+
+ def fail(
+ self,
+ description: str,
+ path: Optional[Path] = None,
+ line: Optional[int] = None,
+ ):
+ """Add a failure to this presubmit step.
+
+ If this is called at least once the step fails, but not immediately—the
+ check is free to continue and possibly call this method again.
+ """
+ _LOG.warning('%s', PresubmitFailure(description, path, line))
+ self._failed = True
+
+ @staticmethod
+ def create_for_testing(**kwargs):
+ parsed_env = pw_cli.env.pigweed_environment()
+ root = parsed_env.PW_PROJECT_ROOT
+ presubmit_root = root / 'out' / 'presubmit'
+ presubmit_kwargs = {
+ 'root': root,
+ 'repos': (root,),
+ 'output_dir': presubmit_root / 'test',
+ 'failure_summary_log': presubmit_root / 'failure-summary.log',
+ 'paths': (root / 'foo.cc', root / 'foo.py'),
+ 'all_paths': (root / 'BUILD.gn', root / 'foo.cc', root / 'foo.py'),
+ 'package_root': root / 'environment' / 'packages',
+ 'luci': None,
+ 'override_gn_args': {},
+ 'format_options': FormatOptions(),
+ }
+ presubmit_kwargs.update(kwargs)
+ return PresubmitContext(**presubmit_kwargs)
+
+ def append_check_command(
+ self,
+ *command_args,
+ call_annotation: Optional[Dict[Any, Any]] = None,
+ **command_kwargs,
+ ) -> None:
+ """Save a subprocess command annotation to this presubmit context.
+
+ This is used to capture commands that will be run for display in ``pw
+ presubmit --dry-run.``
+
+ Args:
+
+ command_args: All args that would normally be passed to
+ subprocess.run
+
+ call_annotation: Optional key value pairs of data to save for this
+ command. Examples:
+
+ ::
+
+ call_annotation={'pw_package_install': 'teensy'}
+ call_annotation={'build_system': 'bazel'}
+ call_annotation={'build_system': 'ninja'}
+
+ command_kwargs: keyword args that would normally be passed to
+ subprocess.run.
+ """
+ call_annotation = call_annotation if call_annotation else {}
+ calling_func: Optional[str] = None
+ calling_check = None
+
+ # Loop through the current call stack looking for `self`, and stopping
+ # when self is a Check() instance and if the __call__ or _try_call
+ # functions are in the stack.
+
+ # This used to be an isinstance(obj, Check) call, but it was changed to
+ # this so Check wouldn't need to be imported here. Doing so would create
+ # a dependency loop.
+ def is_check_object(obj):
+ return getattr(obj, '_is_presubmit_check_object', False)
+
+ for frame_info in inspect.getouterframes(inspect.currentframe()):
+ self_obj = frame_info.frame.f_locals.get('self', None)
+ if (
+ self_obj
+ and is_check_object(self_obj)
+ and frame_info.function in ['_try_call', '__call__']
+ ):
+ calling_func = frame_info.function
+ calling_check = self_obj
+
+ save_check_trace(
+ self.output_dir,
+ PresubmitCheckTrace(
+ self,
+ calling_check,
+ calling_func,
+ command_args,
+ command_kwargs,
+ call_annotation,
+ ),
+ )
+
+ def __post_init__(self) -> None:
+ PRESUBMIT_CONTEXT.set(self)
+
+ def __hash__(self):
+ return hash(
+ tuple(
+ tuple(attribute.items())
+ if isinstance(attribute, dict)
+ else attribute
+ for attribute in dataclasses.astuple(self)
+ )
+ )
+
+
+PRESUBMIT_CONTEXT: ContextVar[Optional[PresubmitContext]] = ContextVar(
+ 'pw_presubmit_context', default=None
+)
+
+
+def get_presubmit_context():
+ return PRESUBMIT_CONTEXT.get()
+
+
+class PresubmitCheckTraceType(enum.Enum):
+ BAZEL = 'BAZEL'
+ CMAKE = 'CMAKE'
+ GN_NINJA = 'GN_NINJA'
+ PW_PACKAGE = 'PW_PACKAGE'
+
+
+class PresubmitCheckTrace(NamedTuple):
+ ctx: 'PresubmitContext'
+ check: Optional['Check']
+ func: Optional[str]
+ args: Iterable[Any]
+ kwargs: Dict[Any, Any]
+ call_annotation: Dict[Any, Any]
+
+ def __repr__(self) -> str:
+ return f'''CheckTrace(
+ ctx={self.ctx.output_dir}
+ id(ctx)={id(self.ctx)}
+ check={self.check}
+ args={self.args}
+ kwargs={self.kwargs.keys()}
+ call_annotation={self.call_annotation}
+)'''
+
+
+def save_check_trace(output_dir: Path, trace: PresubmitCheckTrace) -> None:
+ trace_key = str(output_dir.resolve())
+ trace_list = PRESUBMIT_CHECK_TRACE.get().get(trace_key, [])
+ trace_list.append(trace)
+ PRESUBMIT_CHECK_TRACE.get()[trace_key] = trace_list
+
+
+def get_check_traces(ctx: 'PresubmitContext') -> List[PresubmitCheckTrace]:
+ trace_key = str(ctx.output_dir.resolve())
+ return PRESUBMIT_CHECK_TRACE.get().get(trace_key, [])
+
+
+def log_check_traces(ctx: 'PresubmitContext') -> None:
+ traces = PRESUBMIT_CHECK_TRACE.get()
+
+ for _output_dir, check_traces in traces.items():
+ for check_trace in check_traces:
+ if check_trace.ctx != ctx:
+ continue
+
+ quoted_command_args = ' '.join(
+ shlex.quote(str(arg)) for arg in check_trace.args
+ )
+ _LOG.info(
+ '%s %s',
+ _COLOR.blue('Run ==>'),
+ quoted_command_args,
+ )
+
+
+def apply_exclusions(
+ ctx: PresubmitContext,
+ paths: Optional[Sequence[Path]] = None,
+) -> Tuple[Path, ...]:
+ return ctx.format_options.filter_paths(paths or ctx.paths)
diff --git a/pw_presubmit/py/pw_presubmit/python_checks.py b/pw_presubmit/py/pw_presubmit/python_checks.py
index f694b354c..307a7256a 100644
--- a/pw_presubmit/py/pw_presubmit/python_checks.py
+++ b/pw_presubmit/py/pw_presubmit/python_checks.py
@@ -16,36 +16,40 @@
These checks assume that they are running in a preconfigured Python environment.
"""
+import difflib
import json
import logging
-import os
from pathlib import Path
-import subprocess
+import platform
+import shutil
import sys
+from tempfile import TemporaryDirectory
from typing import Optional
-try:
- import pw_presubmit
-except ImportError:
- # Append the pw_presubmit package path to the module search path to allow
- # running this module without installing the pw_presubmit package.
- sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
- import pw_presubmit
-
from pw_env_setup import python_packages
-from pw_presubmit import (
- build,
+
+from pw_presubmit.presubmit import (
call,
Check,
filter_paths,
+)
+from pw_presubmit.presubmit_context import (
PresubmitContext,
PresubmitFailure,
)
+from pw_presubmit import build
+from pw_presubmit.tools import log_run, colorize_diff_line
_LOG = logging.getLogger(__name__)
_PYTHON_EXTENSIONS = ('.py', '.gn', '.gni')
+_PYTHON_PACKAGE_EXTENSIONS = (
+ 'setup.cfg',
+ 'constraint.list',
+ 'requirements.txt',
+)
+
_PYTHON_IS_3_9_OR_HIGHER = sys.version_info >= (
3,
9,
@@ -78,7 +82,7 @@ def _transform_lcov_file_paths(lcov_file: Path, repo_root: Path) -> str:
file_string = line[3:].rstrip()
source_file_path = Path(file_string)
- # TODO(b/248257406) Remove once we drop support for Python 3.8.
+ # TODO: b/248257406 - Remove once we drop support for Python 3.8.
def is_relative_to(path: Path, other: Path) -> bool:
try:
path.relative_to(other)
@@ -149,7 +153,7 @@ def gn_python_test_coverage(ctx: PresubmitContext):
coverage_omit_patterns,
]
report_args += changed_python_files
- subprocess.run(report_args, check=False, cwd=ctx.output_dir)
+ log_run(report_args, check=False, cwd=ctx.output_dir)
# Generate a json report
call('coverage', 'lcov', coverage_omit_patterns, cwd=ctx.output_dir)
@@ -165,8 +169,204 @@ def gn_python_test_coverage(ctx: PresubmitContext):
_LOG.info('Coverage html report saved to: %s', html_report.resolve())
+@filter_paths(endswith=_PYTHON_PACKAGE_EXTENSIONS)
+def vendor_python_wheels(ctx: PresubmitContext) -> None:
+ """Download Python packages locally for the current platform."""
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'pip_vendor_wheels')
+
+ download_log = (
+ ctx.output_dir
+ / 'python/gen/pw_env_setup/pigweed_build_venv.vendor_wheels'
+ / 'pip_download_log.txt'
+ )
+ _LOG.info('Python package download log: %s', download_log)
+
+ wheel_output = (
+ ctx.output_dir
+ / 'python/gen/pw_env_setup'
+ / 'pigweed_build_venv.vendor_wheels/wheels/'
+ )
+ wheel_destination = ctx.output_dir / 'python_wheels'
+ shutil.rmtree(wheel_destination, ignore_errors=True)
+ shutil.copytree(wheel_output, wheel_destination, dirs_exist_ok=True)
+
+ _LOG.info('Python packages downloaded to: %s', wheel_destination)
+
+
+def _generate_constraint_with_hashes(
+ ctx: PresubmitContext, input_file: Path, output_file: Path
+) -> None:
+ assert input_file.is_file()
+
+ call(
+ "pip-compile",
+ input_file,
+ "--generate-hashes",
+ "--reuse-hashes",
+ "--resolver=backtracking",
+ "--strip-extras",
+ # Force pinning pip and setuptools
+ "--allow-unsafe",
+ "-o",
+ output_file,
+ )
+
+ # Remove absolute paths from comments
+ output_text = output_file.read_text()
+ output_text = output_text.replace(str(ctx.output_dir), '')
+ output_text = output_text.replace(str(ctx.root), '')
+ output_text = output_text.replace(str(output_file.parent), '')
+
+ final_output_text = ''
+ for line in output_text.splitlines(keepends=True):
+ # Remove --find-links lines
+ if line.startswith('--find-links'):
+ continue
+ # Remove blank lines
+ if line == '\n':
+ continue
+ final_output_text += line
+
+ output_file.write_text(final_output_text)
+
+
+def _update_upstream_python_constraints(
+ ctx: PresubmitContext,
+ update_files: bool = False,
+) -> None:
+ """Regenerate platform specific Python constraint files with hashes."""
+ with TemporaryDirectory() as tmpdirname:
+ out_dir = Path(tmpdirname)
+ build.gn_gen(
+ ctx,
+ pw_build_PIP_REQUIREMENTS=[],
+ # Use the constraint file without hashes as the input. This is where
+ # new packages are added by developers.
+ pw_build_PIP_CONSTRAINTS=[
+ '//pw_env_setup/py/pw_env_setup/virtualenv_setup/'
+ 'constraint.list',
+ ],
+ # This should always be set to false when regenrating constraints.
+ pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES=False,
+ )
+ build.ninja(ctx, 'pip_constraint_update')
+
+ # Either: darwin, linux or windows
+ platform_name = platform.system().lower()
+
+ constraint_hashes_filename = f'constraint_hashes_{platform_name}.list'
+ constraint_hashes_original = (
+ ctx.root
+ / 'pw_env_setup/py/pw_env_setup/virtualenv_setup'
+ / constraint_hashes_filename
+ )
+ constraint_hashes_tmp_out = out_dir / constraint_hashes_filename
+ _generate_constraint_with_hashes(
+ ctx,
+ input_file=(
+ ctx.output_dir
+ / 'python/gen/pw_env_setup/pigweed_build_venv'
+ / 'compiled_requirements.txt'
+ ),
+ output_file=constraint_hashes_tmp_out,
+ )
+
+ build.gn_gen(
+ ctx,
+ # This should always be set to false when regenrating constraints.
+ pw_build_PYTHON_PIP_INSTALL_REQUIRE_HASHES=False,
+ )
+ build.ninja(ctx, 'pip_constraint_update')
+
+ upstream_requirements_lock_filename = (
+ f'upstream_requirements_{platform_name}_lock.txt'
+ )
+ upstream_requirements_lock_original = (
+ ctx.root
+ / 'pw_env_setup/py/pw_env_setup/virtualenv_setup'
+ / upstream_requirements_lock_filename
+ )
+ upstream_requirements_lock_tmp_out = (
+ out_dir / upstream_requirements_lock_filename
+ )
+ _generate_constraint_with_hashes(
+ ctx,
+ input_file=(
+ ctx.output_dir
+ / 'python/gen/pw_env_setup/pigweed_build_venv'
+ / 'compiled_requirements.txt'
+ ),
+ output_file=upstream_requirements_lock_tmp_out,
+ )
+
+ if update_files:
+ constraint_hashes_original.write_text(
+ constraint_hashes_tmp_out.read_text()
+ )
+ _LOG.info('Updated: %s', constraint_hashes_original)
+ upstream_requirements_lock_original.write_text(
+ upstream_requirements_lock_tmp_out.read_text()
+ )
+ _LOG.info('Updated: %s', upstream_requirements_lock_original)
+ return
+
+ # Make a diff of required changes
+ constraint_hashes_diff = list(
+ difflib.unified_diff(
+ constraint_hashes_original.read_text(
+ 'utf-8', errors='replace'
+ ).splitlines(),
+ constraint_hashes_tmp_out.read_text(
+ 'utf-8', errors='replace'
+ ).splitlines(),
+ fromfile=str(constraint_hashes_original) + ' (original)',
+ tofile=str(constraint_hashes_original) + ' (updated)',
+ lineterm='',
+ n=1,
+ )
+ )
+ upstream_requirements_lock_diff = list(
+ difflib.unified_diff(
+ upstream_requirements_lock_original.read_text(
+ 'utf-8', errors='replace'
+ ).splitlines(),
+ upstream_requirements_lock_tmp_out.read_text(
+ 'utf-8', errors='replace'
+ ).splitlines(),
+ fromfile=str(upstream_requirements_lock_original)
+ + ' (original)',
+ tofile=str(upstream_requirements_lock_original) + ' (updated)',
+ lineterm='',
+ n=1,
+ )
+ )
+ if constraint_hashes_diff:
+ for line in constraint_hashes_diff:
+ print(colorize_diff_line(line))
+ if upstream_requirements_lock_diff:
+ for line in upstream_requirements_lock_diff:
+ print(colorize_diff_line(line))
+ if constraint_hashes_diff or upstream_requirements_lock_diff:
+ raise PresubmitFailure(
+ 'Please run:\n'
+ '\n'
+ ' pw presubmit --step update_upstream_python_constraints'
+ )
+
+
+@filter_paths(endswith=_PYTHON_PACKAGE_EXTENSIONS)
+def check_upstream_python_constraints(ctx: PresubmitContext) -> None:
+ _update_upstream_python_constraints(ctx, update_files=False)
+
+
+@filter_paths(endswith=_PYTHON_PACKAGE_EXTENSIONS)
+def update_upstream_python_constraints(ctx: PresubmitContext) -> None:
+ _update_upstream_python_constraints(ctx, update_files=True)
+
+
@filter_paths(endswith=_PYTHON_EXTENSIONS + ('.pylintrc',))
-def gn_python_lint(ctx: pw_presubmit.PresubmitContext) -> None:
+def gn_python_lint(ctx: PresubmitContext) -> None:
build.gn_gen(ctx)
build.ninja(ctx, 'python.lint')
diff --git a/pw_presubmit/py/pw_presubmit/rst_format.py b/pw_presubmit/py/pw_presubmit/rst_format.py
new file mode 100644
index 000000000..ea5d309b5
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/rst_format.py
@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Restructured Text Formatting."""
+
+import argparse
+from dataclasses import dataclass, field
+import difflib
+from functools import cached_property
+from pathlib import Path
+import textwrap
+from typing import List, Optional
+
+from pw_presubmit.tools import colorize_diff
+
+DEFAULT_TAB_WIDTH = 8
+CODE_BLOCK_INDENTATION = 3
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+
+ parser.add_argument(
+ '--tab-width',
+ default=DEFAULT_TAB_WIDTH,
+ help='Number of spaces to use when converting tab characters.',
+ )
+ parser.add_argument(
+ '--diff',
+ action='store_true',
+ help='Print a diff of formatting changes.',
+ )
+ parser.add_argument(
+ '-i',
+ '--in-place',
+ action='store_true',
+ help='Replace existing file with the reformatted copy.',
+ )
+ parser.add_argument(
+ 'rst_files',
+ nargs='+',
+ default=[],
+ type=Path,
+ help='Paths to rst files.',
+ )
+
+ return parser.parse_args()
+
+
+def _indent_amount(line: str) -> int:
+ return len(line) - len(line.lstrip())
+
+
+def _reindent(input_text: str, amount: int) -> str:
+ text = ''
+ for line in textwrap.dedent(input_text).splitlines():
+ if len(line.strip()) == 0:
+ text += '\n'
+ continue
+ text += ' ' * amount
+ text += line
+ text += '\n'
+ return text
+
+
+def _strip_trailing_whitespace(line: str) -> str:
+ return line.rstrip() + '\n'
+
+
+@dataclass
+class CodeBlock:
+ """Store a single code block."""
+
+ directive_lineno: int
+ directive_line: str
+ first_line_indent: Optional[int] = None
+ end_lineno: Optional[int] = None
+ option_lines: List[str] = field(default_factory=list)
+ code_lines: List[str] = field(default_factory=list)
+
+ def __post_init__(self) -> None:
+ self._blank_line_after_options_found = False
+
+ def finished(self) -> bool:
+ return self.end_lineno is not None
+
+ def append_line(self, index: int, line: str) -> None:
+ """Process a line for this code block."""
+ # Check if outside the code block (indentation is less):
+ if (
+ self.first_line_indent is not None
+ and len(line.strip()) > 0
+ and _indent_amount(line) < self.first_line_indent
+ ):
+ # Code block ended
+ self.end_lineno = index
+ return
+
+ # If first line indent hasn't been found
+ if self.first_line_indent is None:
+ # Check if the first word is a directive option.
+ # E.g. :caption:
+ line_words = line.split()
+ if (
+ line_words
+ and line_words[0].startswith(':')
+ and line_words[0].endswith(':')
+ ):
+ self.option_lines.append(line.rstrip())
+ return
+ # Check for a blank line
+ if len(line.strip()) == 0:
+ if (
+ self.option_lines
+ and not self._blank_line_after_options_found
+ ):
+ self._blank_line_after_options_found = True
+ return
+ # Check for a line that is a continuation of a previous option.
+ if self.option_lines and not self._blank_line_after_options_found:
+ self.option_lines.append(line.rstrip())
+ return
+
+ self.first_line_indent = _indent_amount(line)
+
+ # Save this line as code.
+ self.code_lines.append(line.rstrip())
+
+ @cached_property
+ def directive_indent_amount(self) -> int:
+ return _indent_amount(self.directive_line)
+
+ def options_block_text(self) -> str:
+ return _reindent(
+ input_text='\n'.join(self.option_lines),
+ amount=self.directive_indent_amount + CODE_BLOCK_INDENTATION,
+ )
+
+ def code_block_text(self) -> str:
+ return _reindent(
+ input_text='\n'.join(self.code_lines),
+ amount=self.directive_indent_amount + CODE_BLOCK_INDENTATION,
+ )
+
+ def to_text(self) -> str:
+ text = ''
+ text += self.directive_line
+ if self.option_lines:
+ text += self.options_block_text()
+ text += '\n'
+ text += self.code_block_text()
+ text += '\n'
+ return text
+
+ def __repr__(self) -> str:
+ return self.to_text()
+
+
+def reindent_code_blocks(in_text: str) -> str:
+ """Reindent code blocks to 3 spaces."""
+ out_text = ''
+ current_block: Optional[CodeBlock] = None
+ for index, line in enumerate(in_text.splitlines(keepends=True)):
+ # If a code block is active, process this line.
+ if current_block:
+ current_block.append_line(index, line)
+ if current_block.finished():
+ out_text += current_block.to_text()
+ # This line wasn't part of the code block, process as normal.
+ out_text += _strip_trailing_whitespace(line)
+ # Erase this code_block variable
+ current_block = None
+ # Check for new code block start
+ elif line.lstrip().startswith('.. code') and line.rstrip().endswith(
+ '::'
+ ):
+ current_block = CodeBlock(
+ directive_lineno=index, directive_line=line
+ )
+ continue
+ else:
+ out_text += _strip_trailing_whitespace(line)
+ # If the document ends with a code block it may still need to be written.
+ if current_block is not None:
+ out_text += current_block.to_text()
+ return out_text
+
+
+def reformat_rst(
+ file_name: Path,
+ diff: bool = False,
+ in_place: bool = False,
+ tab_width: int = DEFAULT_TAB_WIDTH,
+) -> List[str]:
+ """Reformat an rst file.
+
+ Returns a list of diff lines."""
+ in_text = file_name.read_text()
+
+ # Replace tabs with spaces
+ out_text = in_text.replace('\t', ' ' * tab_width)
+
+ # Indent .. code-block:: directives.
+ out_text = reindent_code_blocks(in_text)
+
+ result_diff = list(
+ difflib.unified_diff(
+ in_text.splitlines(True),
+ out_text.splitlines(True),
+ f'{file_name} (original)',
+ f'{file_name} (reformatted)',
+ )
+ )
+ if diff and result_diff:
+ print(''.join(colorize_diff(result_diff)))
+
+ if in_place:
+ file_name.write_text(out_text)
+
+ return result_diff
+
+
+def rst_format_main(
+ rst_files: List[Path],
+ diff: bool = False,
+ in_place: bool = False,
+ tab_width: int = DEFAULT_TAB_WIDTH,
+) -> None:
+ for rst_file in rst_files:
+ reformat_rst(rst_file, diff, in_place, tab_width)
+
+
+if __name__ == '__main__':
+ rst_format_main(**vars(_parse_args()))
diff --git a/pw_presubmit/py/pw_presubmit/shell_checks.py b/pw_presubmit/py/pw_presubmit/shell_checks.py
index 510c09f06..96b481f0b 100644
--- a/pw_presubmit/py/pw_presubmit/shell_checks.py
+++ b/pw_presubmit/py/pw_presubmit/shell_checks.py
@@ -14,13 +14,17 @@
"""Shell related checks."""
import logging
-from pw_presubmit import (
+
+from pw_presubmit import presubmit_context
+from pw_presubmit.presubmit import (
Check,
- PresubmitContext,
filter_paths,
- tools,
+)
+from pw_presubmit.presubmit_context import (
+ PresubmitContext,
PresubmitFailure,
)
+from pw_presubmit.tools import log_run
_LOG = logging.getLogger(__name__)
@@ -32,11 +36,14 @@ _SHELL_EXTENSIONS = ('.sh', '.bash')
def shellcheck(ctx: PresubmitContext) -> None:
"""Run shell script static analiyzer on presubmit."""
- _LOG.warning(
- "The Pigweed project discourages use of shellscripts. "
- "https://pigweed.dev/docs/faq.html"
- )
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
+
+ if ctx.paths:
+ _LOG.warning(
+ "The Pigweed project discourages use of shellscripts. "
+ "https://pigweed.dev/docs/faq.html"
+ )
- result = tools.log_run(['shellcheck', *ctx.paths])
+ result = log_run(['shellcheck', *ctx.paths])
if result.returncode != 0:
raise PresubmitFailure('Shellcheck identifed issues.')
diff --git a/pw_presubmit/py/pw_presubmit/source_in_build.py b/pw_presubmit/py/pw_presubmit/source_in_build.py
index f19dd1174..e48a19124 100644
--- a/pw_presubmit/py/pw_presubmit/source_in_build.py
+++ b/pw_presubmit/py/pw_presubmit/source_in_build.py
@@ -16,10 +16,12 @@
import logging
from typing import Callable, Sequence
-from pw_presubmit import build, format_code, git_repo
+from pw_presubmit import build, format_code, git_repo, presubmit_context
from pw_presubmit.presubmit import (
Check,
FileFilter,
+)
+from pw_presubmit.presubmit_context import (
PresubmitContext,
PresubmitFailure,
)
@@ -54,6 +56,7 @@ def bazel(
"""Checks that source files are in the Bazel build."""
paths = source_filter.filter(ctx.all_paths)
+ paths = presubmit_context.apply_exclusions(ctx, paths)
missing = build.check_bazel_build_for_files(
files_and_extensions_to_check,
@@ -100,6 +103,7 @@ def gn( # pylint: disable=invalid-name
"""Checks that source files are in the GN build."""
paths = source_filter.filter(ctx.all_paths)
+ paths = presubmit_context.apply_exclusions(ctx, paths)
missing = build.check_gn_build_for_files(
files_and_extensions_to_check,
@@ -146,6 +150,7 @@ def cmake(
"""Checks that source files are in the CMake build."""
paths = source_filter.filter(ctx.all_paths)
+ paths = presubmit_context.apply_exclusions(ctx, paths)
run_cmake(ctx)
missing = build.check_compile_commands_for_files(
diff --git a/pw_presubmit/py/pw_presubmit/todo_check.py b/pw_presubmit/py/pw_presubmit/todo_check.py
index 783667f10..53a587edd 100644
--- a/pw_presubmit/py/pw_presubmit/todo_check.py
+++ b/pw_presubmit/py/pw_presubmit/todo_check.py
@@ -18,7 +18,9 @@ from pathlib import Path
import re
from typing import Iterable, Pattern, Sequence, Union
-from pw_presubmit import PresubmitContext, filter_paths
+from pw_presubmit import presubmit_context
+from pw_presubmit.presubmit import filter_paths
+from pw_presubmit.presubmit_context import PresubmitContext
_LOG: logging.Logger = logging.getLogger(__name__)
@@ -39,9 +41,37 @@ EXCLUDE: Sequence[str] = (
)
# todo-check: disable
-BUGS_ONLY = re.compile(r'\bTODO\(b/\d+(?:, ?b/\d+)*\).*\w')
+BUGS_ONLY = re.compile(
+ r'(?:\bTODO\(b/\d+(?:, ?b/\d+)*\).*\w)|'
+ r'(?:\bTODO: b/\d+(?:, ?b/\d+)* - )'
+)
BUGS_OR_USERNAMES = re.compile(
- r'\bTODO\((?:b/\d+|[a-z]+)(?:, ?(?:b/\d+|[a-z]+))*\).*\w'
+ r"""
+(?: # Legacy style.
+ \bTODO\(
+ (?:b/\d+|[a-z]+) # Username or bug.
+ (?:,[ ]?(?:b/\d+|[a-z]+))* # Additional usernames or bugs.
+ \)
+.*\w # Explanation.
+)|
+(?: # New style.
+ \bTODO:[ ]
+ (?:
+ b/\d+| # Bug.
+ # Username@ with optional domain.
+ [a-z]+@(?:[a-z][-a-z0-9]*(?:\.[a-z][-a-z0-9]*)+)?
+ )
+ (?:,[ ]? # Additional.
+ (?:
+ b/\d+| # Bug.
+ # Username@ with optional domain.
+ [a-z]+@(?:[a-z][-a-z0-9]*(?:\.[a-z][-a-z0-9]*)+)?
+ )
+ )*
+[ ]-[ ].*\w # Explanation.
+)
+ """,
+ re.VERBOSE,
)
_TODO = re.compile(r'\bTODO\b')
# todo-check: enable
@@ -102,6 +132,7 @@ def create(
@filter_paths(exclude=exclude)
def todo_check(ctx: PresubmitContext):
"""Check that TODO lines are valid.""" # todo-check: ignore
+ ctx.paths = presubmit_context.apply_exclusions(ctx)
for path in ctx.paths:
_process_file(ctx, todo_pattern, path)
diff --git a/pw_presubmit/py/pw_presubmit/tools.py b/pw_presubmit/py/pw_presubmit/tools.py
index bf184db73..9e1319e09 100644
--- a/pw_presubmit/py/pw_presubmit/tools.py
+++ b/pw_presubmit/py/pw_presubmit/tools.py
@@ -32,7 +32,31 @@ from typing import (
Tuple,
)
+import pw_cli.color
+from pw_presubmit.presubmit_context import PRESUBMIT_CONTEXT
+
_LOG: logging.Logger = logging.getLogger(__name__)
+_COLOR = pw_cli.color.colors()
+
+
+def colorize_diff_line(line: str) -> str:
+ if line.startswith('--- ') or line.startswith('+++ '):
+ return _COLOR.bold_white(line)
+ if line.startswith('-'):
+ return _COLOR.red(line)
+ if line.startswith('+'):
+ return _COLOR.green(line)
+ if line.startswith('@@ '):
+ return _COLOR.cyan(line)
+ return line
+
+
+def colorize_diff(lines: Iterable[str]) -> str:
+ """Takes a diff str or list of str lines and returns a colorized version."""
+ if isinstance(lines, str):
+ lines = lines.splitlines(True)
+
+ return ''.join(colorize_diff_line(line) for line in lines)
def plural(
@@ -42,8 +66,20 @@ def plural(
these: bool = False,
number: bool = True,
are: bool = False,
+ exist: bool = False,
) -> str:
- """Returns the singular or plural form of a word based on a count."""
+ """Returns the singular or plural form of a word based on a count.
+
+ Args:
+ items_or_count: Number of items or a collection of items
+ singular: Singular form of the name of the item
+ count_format: .format()-style specification for items_or_count
+ these: Prefix the string with "this" or "these", depending on number
+ number: Include the number in the return string (e.g., "3 things" vs.
+ "things")
+ are: Suffix the string with "is" or "are", depending on number
+ exist: Suffix the string with "exists" or "exist", depending on number
+ """
try:
count = len(items_or_count)
@@ -52,7 +88,14 @@ def plural(
prefix = ('this ' if count == 1 else 'these ') if these else ''
num = f'{count:{count_format}} ' if number else ''
- suffix = (' is' if count == 1 else ' are') if are else ''
+
+ suffix = ''
+ if are and exist:
+ raise ValueError(f'cannot combine are ({are}) and exist ({exist})')
+ if are:
+ suffix = ' is' if count == 1 else ' are'
+ if exist:
+ suffix = ' exists' if count == 1 else ' exist'
if singular.endswith('y'):
result = f'{singular[:-1]}{"y" if count == 1 else "ies"}'
@@ -181,11 +224,26 @@ def format_command(args: Sequence, kwargs: dict) -> Tuple[str, str]:
return attr, ' '.join(shlex.quote(str(arg)) for arg in args)
-def log_run(args, **kwargs) -> subprocess.CompletedProcess:
+def log_run(
+ args, ignore_dry_run: bool = False, **kwargs
+) -> subprocess.CompletedProcess:
"""Logs a command then runs it with subprocess.run.
- Takes the same arguments as subprocess.run.
+ Takes the same arguments as subprocess.run. The command is only executed if
+ dry-run is not enabled.
"""
+ ctx = PRESUBMIT_CONTEXT.get()
+ if ctx:
+ if not ignore_dry_run:
+ ctx.append_check_command(*args, **kwargs)
+ if ctx.dry_run and not ignore_dry_run:
+ # Return an empty CompletedProcess
+ empty_proc: subprocess.CompletedProcess = (
+ subprocess.CompletedProcess('', 0)
+ )
+ empty_proc.stdout = b''
+ empty_proc.stderr = b''
+ return empty_proc
_LOG.debug('[COMMAND] %s\n%s', *format_command(args, kwargs))
return subprocess.run(args, **kwargs)
diff --git a/pw_presubmit/py/setup.py b/pw_presubmit/py/setup.py
deleted file mode 100644
index 46177377a..000000000
--- a/pw_presubmit/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_presubmit"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_presubmit/py/todo_check_test.py b/pw_presubmit/py/todo_check_test.py
index c4b8d1b5a..803b58229 100644
--- a/pw_presubmit/py/todo_check_test.py
+++ b/pw_presubmit/py/todo_check_test.py
@@ -25,6 +25,7 @@ from pw_presubmit import todo_check
# todo-check: disable
+# pylint: disable-next=too-many-public-methods
class TestTodoCheck(unittest.TestCase):
"""Test TODO checker."""
@@ -48,104 +49,225 @@ class TestTodoCheck(unittest.TestCase):
def _run_bugs(self, contents: str) -> None:
self._run(todo_check.BUGS_ONLY, contents)
- def test_one_bug(self) -> None:
- contents = 'TODO(b/123): foo\n'
+ def test_one_bug_legacy(self) -> None:
+ contents = 'TODO: b/123 - foo\n'
self._run_bugs_users(contents)
self.ctx.fail.assert_not_called()
self._run_bugs(contents)
self.ctx.fail.assert_not_called()
- def test_two_bugs(self) -> None:
+ def test_one_bug_new(self) -> None:
+ contents = 'TODO: b/123 - foo\n'
+ self._run_bugs_users(contents)
+ self.ctx.fail.assert_not_called()
+ self._run_bugs(contents)
+ self.ctx.fail.assert_not_called()
+
+ def test_two_bugs_legacy(self) -> None:
contents = 'TODO(b/123, b/456): foo\n'
self._run_bugs_users(contents)
self.ctx.fail.assert_not_called()
self._run_bugs(contents)
self.ctx.fail.assert_not_called()
- def test_three_bugs(self) -> None:
+ def test_two_bugs_new(self) -> None:
+ contents = 'TODO: b/123, b/456 - foo\n'
+ self._run_bugs_users(contents)
+ self.ctx.fail.assert_not_called()
+ self._run_bugs(contents)
+ self.ctx.fail.assert_not_called()
+
+ def test_three_bugs_legacy(self) -> None:
contents = 'TODO(b/123,b/456,b/789): foo\n'
self._run_bugs_users(contents)
self.ctx.fail.assert_not_called()
self._run_bugs(contents)
self.ctx.fail.assert_not_called()
- def test_one_username(self) -> None:
+ def test_three_bugs_new(self) -> None:
+ contents = 'TODO: b/123,b/456,b/789 - foo\n'
+ self._run_bugs_users(contents)
+ self.ctx.fail.assert_not_called()
+ self._run_bugs(contents)
+ self.ctx.fail.assert_not_called()
+
+ def test_one_username_legacy(self) -> None:
self._run_bugs_users('TODO(usera): foo\n')
self.ctx.fail.assert_not_called()
- def test_two_usernames(self) -> None:
+ def test_one_username_new(self) -> None:
+ self._run_bugs_users('TODO: usera@ - foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_one_username_new_noat(self) -> None:
+ self._run_bugs_users('TODO: usera - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_one_username_new_short_domain(self) -> None:
+ self._run_bugs_users('TODO: usera@com - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_one_username_new_medium_domain(self) -> None:
+ self._run_bugs_users('TODO: usera@example.com - foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_one_username_new_long_domain(self) -> None:
+ self._run_bugs_users('TODO: usera@a.b.c.d.example.com - foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_two_usernames_legacy(self) -> None:
self._run_bugs_users('TODO(usera, userb): foo\n')
self.ctx.fail.assert_not_called()
- def test_three_usernames(self) -> None:
+ def test_two_usernames_new(self) -> None:
+ self._run_bugs_users('TODO: usera@, userb@ - foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_three_usernames_legacy(self) -> None:
self._run_bugs_users('TODO(usera,userb,userc): foo\n')
self.ctx.fail.assert_not_called()
- def test_username_not_allowed(self) -> None:
+ def test_three_usernames_new(self) -> None:
+ self._run_bugs_users('TODO: usera@,userb@example.com,userc@ - foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_username_not_allowed_legacy(self) -> None:
self._run_bugs('TODO(usera): foo\n')
self.ctx.fail.assert_called()
- def test_space_after_todo_bugsonly(self) -> None:
+ def test_username_not_allowed_new(self) -> None:
+ self._run_bugs('TODO: usera@ - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_todo_bugsonly_legacy(self) -> None:
self._run_bugs('TODO (b/123): foo\n')
self.ctx.fail.assert_called()
- def test_space_after_todo_bugsusers(self) -> None:
+ def test_space_after_todo_bugsonly_new(self) -> None:
+ self._run_bugs('TODO : b/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_todo_bugsusers_legacy(self) -> None:
self._run_bugs_users('TODO (b/123): foo\n')
self.ctx.fail.assert_called()
- def test_space_before_bug_bugsonly(self) -> None:
+ def test_space_after_todo_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO : b/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_before_bug_bugsonly_legacy(self) -> None:
self._run_bugs('TODO( b/123): foo\n')
self.ctx.fail.assert_called()
- def test_space_before_bug_bugsusers(self) -> None:
+ def test_no_space_before_bug_bugsonly_new(self) -> None:
+ self._run_bugs('TODO:b/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_before_bug_bugsusers_legacy(self) -> None:
self._run_bugs_users('TODO( b/123): foo\n')
self.ctx.fail.assert_called()
- def test_space_after_bug_bugsonly(self) -> None:
+ def test_no_space_before_bug_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO:b/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_bug_bugsonly_legacy(self) -> None:
self._run_bugs('TODO(b/123 ): foo\n')
self.ctx.fail.assert_called()
- def test_space_after_bug_bugsusers(self) -> None:
+ def test_no_space_after_bug_bugsonly_new(self) -> None:
+ self._run_bugs('TODO: b/123- foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_no_space_before_explanation_bugsonly_new(self) -> None:
+ self._run_bugs('TODO: b/123 -foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_bug_bugsusers_legacy(self) -> None:
self._run_bugs_users('TODO(b/123 ): foo\n')
self.ctx.fail.assert_called()
- def test_missing_explanation_bugsonly(self) -> None:
- self._run_bugs('TODO(b/123)\n')
+ def test_no_space_before_explanation_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO: b/123 -foo\n')
self.ctx.fail.assert_called()
- def test_missing_explanation_bugsusers(self) -> None:
- self._run_bugs_users('TODO(b/123)\n')
+ def test_missing_explanation_bugsonly_legacy(self) -> None:
+ self._run_bugs('TODO: b/123 -\n')
self.ctx.fail.assert_called()
- def test_not_a_bug_bugsonly(self) -> None:
+ def test_missing_explanation_bugsonly_new(self) -> None:
+ self._run_bugs('TODO: b/123\n')
+ self.ctx.fail.assert_called()
+
+ def test_missing_explanation_bugsusers_legacy(self) -> None:
+ self._run_bugs_users('TODO: b/123 -\n')
+ self.ctx.fail.assert_called()
+
+ def test_missing_explanation_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO: b/123\n')
+ self.ctx.fail.assert_called()
+
+ def test_not_a_bug_bugsonly_legacy(self) -> None:
self._run_bugs('TODO(cl/123): foo\n')
self.ctx.fail.assert_called()
- def test_not_a_bug_bugsusers(self) -> None:
+ def test_not_a_bug_bugsonly_new(self) -> None:
+ self._run_bugs('TODO: cl/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_not_a_bug_bugsusers_legacy(self) -> None:
self._run_bugs_users('TODO(cl/123): foo\n')
self.ctx.fail.assert_called()
- def test_but_not_bug_bugsonly(self) -> None:
+ def test_not_a_bug_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO: cl/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_but_not_bug_bugsonly_legacy(self) -> None:
self._run_bugs('TODO(b/123, cl/123): foo\n')
self.ctx.fail.assert_called()
- def test_bug_not_bug_bugsusers(self) -> None:
+ def test_but_not_bug_bugsonly_new(self) -> None:
+ self._run_bugs('TODO: b/123, cl/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bug_not_bug_bugsusers_legacy(self) -> None:
self._run_bugs_users('TODO(b/123, cl/123): foo\n')
self.ctx.fail.assert_called()
- def test_empty_bugsonly(self) -> None:
+ def test_bug_not_bug_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO: b/123, cl/123 - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_empty_bugsonly_legacy(self) -> None:
self._run_bugs('TODO(): foo\n')
self.ctx.fail.assert_called()
- def test_empty_bugsusers(self) -> None:
+ def test_empty_bugsonly_new(self) -> None:
+ self._run_bugs('TODO: - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_empty_bugsusers_legacy(self) -> None:
self._run_bugs_users('TODO(): foo\n')
self.ctx.fail.assert_called()
- def test_bare_bugsonly(self) -> None:
+ def test_empty_bugsusers_new(self) -> None:
+ self._run_bugs_users('TODO: - foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bare_bugsonly_legacy(self) -> None:
+ self._run_bugs('TODO: foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bare_bugsonly_new(self) -> None:
self._run_bugs('TODO: foo\n')
self.ctx.fail.assert_called()
- def test_bare_bugsusers(self) -> None:
+ def test_bare_bugsusers_legacy(self) -> None:
+ self._run_bugs_users('TODO: foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bare_bugsusers_new(self) -> None:
self._run_bugs_users('TODO: foo\n')
self.ctx.fail.assert_called()
diff --git a/pw_presubmit/py/tools_test.py b/pw_presubmit/py/tools_test.py
index af6d685ed..8a145caad 100755
--- a/pw_presubmit/py/tools_test.py
+++ b/pw_presubmit/py/tools_test.py
@@ -56,5 +56,57 @@ class FlattenTest(unittest.TestCase):
)
+class PluralTest(unittest.TestCase):
+ """Test the plural function, which adds an 's' to nouns."""
+
+ def test_single_list(self):
+ self.assertEqual('1 item', tools.plural([1], 'item'))
+
+ def test_single_count(self):
+ self.assertEqual('1 item', tools.plural(1, 'item'))
+
+ def test_multiple_list(self):
+ self.assertEqual('3 items', tools.plural([1, 2, 3], 'item'))
+
+ def test_multiple_count(self):
+ self.assertEqual('3 items', tools.plural(3, 'item'))
+
+ def test_single_these(self):
+ self.assertEqual('this 1 item', tools.plural(1, 'item', these=True))
+
+ def test_multiple_these(self):
+ self.assertEqual('these 3 items', tools.plural(3, 'item', these=True))
+
+ def test_single_are(self):
+ self.assertEqual('1 item is', tools.plural(1, 'item', are=True))
+
+ def test_multiple_are(self):
+ self.assertEqual('3 items are', tools.plural(3, 'item', are=True))
+
+ def test_single_exist(self):
+ self.assertEqual('1 item exists', tools.plural(1, 'item', exist=True))
+
+ def test_multiple_exist(self):
+ self.assertEqual('3 items exist', tools.plural(3, 'item', exist=True))
+
+ def test_single_y(self):
+ self.assertEqual('1 thingy', tools.plural(1, 'thingy'))
+
+ def test_multiple_y(self):
+ self.assertEqual('3 thingies', tools.plural(3, 'thingy'))
+
+ def test_single_s(self):
+ self.assertEqual('1 bus', tools.plural(1, 'bus'))
+
+ def test_multiple_s(self):
+ self.assertEqual('3 buses', tools.plural(3, 'bus'))
+
+ def test_format_hex(self):
+ self.assertEqual(
+ '14 items',
+ tools.plural(20, 'item', count_format='x'),
+ )
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_protobuf/Android.bp b/pw_protobuf/Android.bp
index 0e3f8a04c..1a2c79a8f 100644
--- a/pw_protobuf/Android.bp
+++ b/pw_protobuf/Android.bp
@@ -22,8 +22,8 @@ cc_library_static {
vendor_available: true,
export_include_dirs: ["public"],
header_libs: [
- "fuschia_sdk_lib_fit",
- "fuschia_sdk_lib_stdcompat",
+ "fuchsia_sdk_lib_fit",
+ "fuchsia_sdk_lib_stdcompat",
"pw_assert_headers",
"pw_assert_log_headers",
"pw_function_headers",
@@ -34,6 +34,16 @@ cc_library_static {
"pw_result_headers",
"pw_span_headers",
],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_function_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
host_supported: true,
srcs: [
"decoder.cc",
@@ -51,6 +61,108 @@ cc_library_static {
"pw_string",
"pw_varint",
],
+ export_static_lib_headers: [
+ "pw_bytes",
+ "pw_containers",
+ "pw_status",
+ "pw_stream",
+ "pw_varint",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_protobuf_pwpb_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ generated_headers: [
+ "google_protobuf_descriptor_pwpb_h",
+ "pw_protobuf_protos_common_pwpb_h",
+ ],
+ export_generated_headers: [
+ "google_protobuf_descriptor_pwpb_h",
+ "pw_protobuf_protos_common_pwpb_h",
+ ],
+}
+
+// Copies the proto files to a prefix directory to add the prefix to the
+// compiled proto. The prefix is taken from the directory name of the first
+// item listen in out.
+genrule_defaults {
+ name: "pw_protobuf_add_prefix_to_proto",
+ cmd: "out_files=($(out)); prefix=$$(dirname $${out_files[0]}); " +
+ "mkdir -p $${prefix}; cp -t $${prefix} $(in);"
+}
+
+filegroup {
+ name: "pw_protobuf_common_proto",
+ srcs: [
+ "pw_protobuf_protos/common.proto",
+ ],
+}
+
+genrule {
+ name: "pw_protobuf_common_proto_with_prefix",
+ defaults: ["pw_protobuf_add_prefix_to_proto"],
+ srcs: [
+ "pw_protobuf_protos/common.proto",
+ ],
+ out: [
+ "pw_protobuf/pw_protobuf_protos/common.proto",
+ ],
+}
+
+genrule {
+ name: "pw_protobuf_protos_common_pwpb_h",
+ srcs: [":pw_protobuf_common_proto_with_prefix",],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--proto-path=external/pigweed/pw_protobuf/ " +
+ "--proto-path=external/protobuf/src/ " +
+ "--out-dir=$$(dirname $(location pw_protobuf_protos/common.pwpb.h)) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) ",
+ out: [
+ "pw_protobuf_protos/common.pwpb.h",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Generate the google/protobuf/descriptor.pwpb.h which is commonly imported.
+genrule {
+ name: "google_protobuf_descriptor_pwpb_h",
+ // The libprotobuf-internal-descriptor-proto filegroup is unavailable so
+ // instead filter for just the descriptor.proto.
+ srcs: [":libprotobuf-internal-protos",],
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*descriptor.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--proto-path=external/protobuf/src/ " +
+ "--out-dir=$$(dirname $(location google/protobuf/descriptor.pwpb.h)) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$${compile_dir} " +
+ "--sources $${proto_files} " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) ",
+ out: [
+ "google/protobuf/descriptor.pwpb.h",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
}
genrule {
@@ -82,7 +194,7 @@ genrule {
"pw_protobuf_protos/common.proto",
"pw_protobuf_protos/field_options.proto",
"pw_protobuf_protos/status.proto",
- ":libprotobuf-internal-descriptor-proto",
+ ":libprotobuf-internal-protos",
],
cmd: "$(location aprotoc) " +
"-I$$(dirname $(location pw_protobuf_protos/common.proto)) " +
@@ -105,4 +217,4 @@ python_library_host {
":pw_protobuf_protos_py",
],
pkg_path: "pw_protobuf_protos",
-} \ No newline at end of file
+}
diff --git a/pw_protobuf/BUILD.bazel b/pw_protobuf/BUILD.bazel
index 33c68926e..030d960c5 100644
--- a/pw_protobuf/BUILD.bazel
+++ b/pw_protobuf/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"//pw_build:pigweed.bzl",
@@ -20,9 +21,7 @@ load(
"pw_cc_test",
)
load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
-load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
-load("@rules_proto_grpc//:defs.bzl", "proto_plugin")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -114,7 +113,6 @@ pw_cc_fuzz_test(
"fuzz.h",
],
deps = [
- "//pw_fuzzer",
"//pw_protobuf",
"//pw_span",
],
@@ -127,7 +125,6 @@ pw_cc_fuzz_test(
"fuzz.h",
],
deps = [
- "//pw_fuzzer",
"//pw_protobuf",
"//pw_span",
],
@@ -138,6 +135,7 @@ pw_cc_test(
srcs = ["find_test.cc"],
deps = [
":pw_protobuf",
+ "//pw_string",
"//pw_unit_test",
],
)
@@ -210,6 +208,11 @@ proto_library(
strip_import_prefix = "/pw_protobuf",
)
+java_lite_proto_library(
+ name = "common_java_proto_lite",
+ deps = [":common_proto"],
+)
+
proto_library(
name = "field_options_proto",
srcs = [
@@ -246,16 +249,22 @@ py_proto_library(
],
)
-proto_library(
- name = "codegen_test_deps_protos",
+pw_proto_filegroup(
+ name = "codegen_test_deps_protos_and_options",
srcs = [
"pw_protobuf_test_deps_protos/imported.proto",
],
- strip_import_prefix = "/pw_protobuf",
+ options_files = ["pw_protobuf_test_deps_protos/imported.options"],
)
proto_library(
- name = "codegen_test_proto",
+ name = "codegen_test_deps_protos",
+ srcs = [":codegen_test_deps_protos_and_options"],
+ strip_import_prefix = "/pw_protobuf",
+)
+
+pw_proto_filegroup(
+ name = "codegen_test_proto_and_options",
srcs = [
"pw_protobuf_test_protos/full_test.proto",
"pw_protobuf_test_protos/imported.proto",
@@ -266,6 +275,17 @@ proto_library(
"pw_protobuf_test_protos/repeated.proto",
"pw_protobuf_test_protos/size_report.proto",
],
+ options_files = [
+ "pw_protobuf_test_protos/full_test.options",
+ "pw_protobuf_test_protos/optional.options",
+ "pw_protobuf_test_protos/imported.options",
+ "pw_protobuf_test_protos/repeated.options",
+ ],
+)
+
+proto_library(
+ name = "codegen_test_proto",
+ srcs = [":codegen_test_proto_and_options"],
strip_import_prefix = "/pw_protobuf",
deps = [
":codegen_test_deps_protos",
@@ -330,14 +350,3 @@ filegroup(
"size_report/*.cc",
]),
)
-
-proto_plugin(
- name = "pw_cc_plugin",
- outputs = [
- "{protopath}.pwpb.h",
- ],
- protoc_plugin_name = "pwpb",
- tool = "@pigweed//pw_protobuf/py:plugin",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
diff --git a/pw_protobuf/BUILD.gn b/pw_protobuf/BUILD.gn
index 97f8c82fe..bbcfcb0fd 100644
--- a/pw_protobuf/BUILD.gn
+++ b/pw_protobuf/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -49,6 +49,7 @@ pw_source_set("pw_protobuf") {
"$dir_pw_bytes:bit",
"$dir_pw_containers:vector",
"$dir_pw_stream:interval_reader",
+ "$dir_pw_string:string",
"$dir_pw_varint:stream",
dir_pw_assert,
dir_pw_bytes,
@@ -81,7 +82,6 @@ pw_source_set("pw_protobuf") {
"message.cc",
"stream_decoder.cc",
]
- deps = [ "$dir_pw_string:string" ]
}
pw_source_set("bytes_utils") {
@@ -120,8 +120,6 @@ pw_test_group("tests") {
":codegen_message_test",
":decoder_test",
":encoder_test",
- ":encoder_fuzzer_test",
- ":decoder_fuzzer_test",
":find_test",
":map_utils_test",
":message_test",
@@ -129,10 +127,11 @@ pw_test_group("tests") {
":stream_decoder_test",
":varint_size_test",
]
+ group_deps = [ ":fuzzers" ]
}
-group("fuzzers") {
- deps = [
+pw_fuzzer_group("fuzzers") {
+ fuzzers = [
":decoder_fuzzer",
":encoder_fuzzer",
]
@@ -141,26 +140,44 @@ group("fuzzers") {
pw_test("decoder_test") {
deps = [ ":pw_protobuf" ]
sources = [ "decoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("encoder_test") {
deps = [ ":pw_protobuf" ]
sources = [ "encoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("find_test") {
- deps = [ ":pw_protobuf" ]
+ deps = [
+ ":pw_protobuf",
+ dir_pw_string,
+ ]
sources = [ "find_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("codegen_decoder_test") {
deps = [ ":codegen_test_protos.pwpb" ]
sources = [ "codegen_decoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("codegen_encoder_test") {
deps = [ ":codegen_test_protos.pwpb" ]
sources = [ "codegen_encoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("codegen_message_test") {
@@ -169,26 +186,41 @@ pw_test("codegen_message_test") {
dir_pw_string,
]
sources = [ "codegen_message_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("serialized_size_test") {
deps = [ ":pw_protobuf" ]
sources = [ "serialized_size_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("stream_decoder_test") {
deps = [ ":pw_protobuf" ]
sources = [ "stream_decoder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("map_utils_test") {
deps = [ ":pw_protobuf" ]
sources = [ "map_utils_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("message_test") {
deps = [ ":pw_protobuf" ]
sources = [ "message_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
group("perf_tests") {
@@ -199,6 +231,9 @@ pw_perf_test("encoder_perf_test") {
enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
deps = [ ":pw_protobuf" ]
sources = [ "encoder_perf_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
config("one_byte_varint") {
@@ -217,6 +252,9 @@ pw_facade_test("varint_size_test") {
}
deps = [ ":pw_protobuf" ]
sources = [ "varint_size_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_proto_library("codegen_protos") {
@@ -260,10 +298,11 @@ pw_proto_library("codegen_test_protos") {
}
# The tests below have a large amount of global and static data.
-# TODO(b/234883746): Replace this with a better size-based check.
+# TODO: b/234883746 - Replace this with a better size-based check.
_small_executable_target_types = [
"stm32f429i_executable",
"lm3s6965evb_executable",
+ "arduino_executable", # Fails for Teensy 3.2
]
_supports_large_tests =
_small_executable_target_types + [ pw_build_EXECUTABLE_TARGET_TYPE ] -
@@ -276,10 +315,12 @@ pw_fuzzer("encoder_fuzzer") {
]
deps = [
":pw_protobuf",
- dir_pw_fuzzer,
dir_pw_span,
]
enable_test_if = _supports_large_tests
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_fuzzer("decoder_fuzzer") {
@@ -289,10 +330,12 @@ pw_fuzzer("decoder_fuzzer") {
]
deps = [
":pw_protobuf",
- dir_pw_fuzzer,
dir_pw_span,
dir_pw_status,
dir_pw_stream,
]
enable_test_if = _supports_large_tests
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
diff --git a/pw_protobuf/CMakeLists.txt b/pw_protobuf/CMakeLists.txt
index 77d0ccf32..b7ecf059c 100644
--- a/pw_protobuf/CMakeLists.txt
+++ b/pw_protobuf/CMakeLists.txt
@@ -53,10 +53,9 @@ pw_add_library(pw_protobuf STATIC
pw_status
pw_stream
pw_stream.interval_reader
+ pw_string.string
pw_varint
pw_varint.stream
- PRIVATE_DEPS
- pw_string.string
SOURCES
decoder.cc
encoder.cc
diff --git a/pw_protobuf/codegen_decoder_test.cc b/pw_protobuf/codegen_decoder_test.cc
index 09621a7c0..31d856354 100644
--- a/pw_protobuf/codegen_decoder_test.cc
+++ b/pw_protobuf/codegen_decoder_test.cc
@@ -354,7 +354,7 @@ TEST(Codegen, StreamDecoder) {
EXPECT_EQ(proto.Next(), Status::OutOfRange());
}
- for (int i = 0; i < 5; ++i) {
+ for (unsigned i = 0; i < 5; ++i) {
EXPECT_EQ(pigweed.Next(), OkStatus());
EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kId);
@@ -519,7 +519,7 @@ TEST(CodegenRepeated, NonPackedScalar) {
stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
- for (int i = 0; i < 4; ++i) {
+ for (uint32_t i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
@@ -528,7 +528,7 @@ TEST(CodegenRepeated, NonPackedScalar) {
EXPECT_EQ(result.value(), i * 16u);
}
- for (int i = 0; i < 4; ++i) {
+ for (unsigned i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
@@ -561,7 +561,7 @@ TEST(CodegenRepeated, NonPackedScalarVector) {
pw::Vector<uint32_t, 8> uint32s{};
- for (int i = 0; i < 4; ++i) {
+ for (unsigned i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
@@ -570,13 +570,13 @@ TEST(CodegenRepeated, NonPackedScalarVector) {
EXPECT_EQ(uint32s.size(), i + 1u);
}
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
pw::Vector<uint32_t, 8> fixed32s{};
- for (int i = 0; i < 4; ++i) {
+ for (unsigned i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
@@ -585,7 +585,7 @@ TEST(CodegenRepeated, NonPackedScalarVector) {
EXPECT_EQ(fixed32s.size(), i + 1u);
}
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(fixed32s[i], i * 16u);
}
@@ -626,7 +626,7 @@ TEST(CodegenRepeated, NonPackedVarintScalarVectorFull) {
EXPECT_EQ(status, Status::ResourceExhausted());
EXPECT_EQ(uint32s.size(), 2u);
- for (int i = 0; i < 2; ++i) {
+ for (unsigned short i = 0; i < 2; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
}
@@ -665,7 +665,7 @@ TEST(CodegenRepeated, NonPackedFixedScalarVectorFull) {
EXPECT_EQ(status, Status::ResourceExhausted());
EXPECT_EQ(fixed32s.size(), 2u);
- for (int i = 0; i < 2; ++i) {
+ for (unsigned short i = 0; i < 2; ++i) {
EXPECT_EQ(fixed32s[i], i * 16u);
}
}
@@ -698,7 +698,7 @@ TEST(CodegenRepeated, PackedScalar) {
EXPECT_EQ(sws.status(), OkStatus());
EXPECT_EQ(sws.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
@@ -709,7 +709,7 @@ TEST(CodegenRepeated, PackedScalar) {
EXPECT_EQ(sws.status(), OkStatus());
EXPECT_EQ(sws.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(fixed32s[i], i * 16u);
}
@@ -738,7 +738,7 @@ TEST(CodegenRepeated, PackedVarintScalarExhausted) {
EXPECT_EQ(sws.status(), Status::ResourceExhausted());
EXPECT_EQ(sws.size(), 2u);
- for (int i = 0; i < 2; ++i) {
+ for (unsigned short i = 0; i < 2; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
}
@@ -794,7 +794,7 @@ TEST(CodegenRepeated, PackedScalarVector) {
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(uint32s.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
@@ -805,7 +805,7 @@ TEST(CodegenRepeated, PackedScalarVector) {
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(fixed32s.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(fixed32s[i], i * 16u);
}
@@ -834,7 +834,7 @@ TEST(CodegenRepeated, PackedVarintScalarVectorFull) {
EXPECT_EQ(status, Status::ResourceExhausted());
EXPECT_EQ(uint32s.size(), 2u);
- for (int i = 0; i < 2; ++i) {
+ for (unsigned short i = 0; i < 2; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
}
@@ -908,7 +908,7 @@ TEST(CodegenRepeated, PackedScalarVectorRepeated) {
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(uint32s.size(), 8u);
- for (int i = 0; i < 8; ++i) {
+ for (unsigned short i = 0; i < 8; ++i) {
EXPECT_EQ(uint32s[i], i * 16u);
}
@@ -925,7 +925,7 @@ TEST(CodegenRepeated, PackedScalarVectorRepeated) {
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(fixed32s.size(), 8u);
- for (int i = 0; i < 8; ++i) {
+ for (unsigned short i = 0; i < 8; ++i) {
EXPECT_EQ(fixed32s[i], i * 16u);
}
@@ -949,7 +949,7 @@ TEST(CodegenRepeated, NonScalar) {
constexpr std::array<std::string_view, 4> kExpectedString{
{{"the"}, {"quick"}, {"brown"}, {"fox"}}};
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kStrings);
std::array<char, 32> string{};
@@ -983,7 +983,7 @@ TEST(CodegenRepeated, PackedEnum) {
EXPECT_EQ(sws.status(), OkStatus());
ASSERT_EQ(sws.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_TRUE(IsValidEnum(enums[i]));
}
@@ -1013,7 +1013,7 @@ TEST(CodegenRepeated, PackedEnumVector) {
EXPECT_EQ(status, OkStatus());
ASSERT_EQ(enums.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_TRUE(IsValidEnum(enums[i]));
}
@@ -1025,5 +1025,251 @@ TEST(CodegenRepeated, PackedEnumVector) {
EXPECT_EQ(repeated_test.Next(), Status::OutOfRange());
}
+TEST(Codegen, FindBuffer) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.magic_number
+ 0x08, 0x49,
+ // pigweed.ziggy
+ 0x10, 0xdd, 0x01,
+ // pigweed.cycles
+ 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40,
+ // pigweed.ratio
+ 0x25, 0x8f, 0xc2, 0xb5, 0xbf,
+ // pigweed.error_message
+ 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ',
+ 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r',
+ // pigweed.bin
+ 0x40, 0x01,
+ // pigweed.pigweed
+ 0x3a, 0x02,
+ // pigweed.pigweed.status
+ 0x08, 0x02,
+ // pigweed.proto
+ 0x4a, 0x56,
+ // pigweed.proto.bin
+ 0x10, 0x00,
+ // pigweed.proto.pigweed_pigweed_bin
+ 0x18, 0x00,
+ // pigweed.proto.pigweed_protobuf_bin
+ 0x20, 0x01,
+ // pigweed.proto.meta
+ 0x2a, 0x0f,
+ // pigweed.proto.meta.file_name
+ 0x0a, 0x0b, '/', 'e', 't', 'c', '/', 'p', 'a', 's', 's', 'w', 'd',
+ // pigweed.proto.meta.status
+ 0x10, 0x02,
+ // pigweed.proto.nested_pigweed
+ 0x0a, 0x3d,
+ // pigweed.proto.nested_pigweed.error_message
+ 0x2a, 0x10, 'h', 'e', 'r', 'e', ' ', 'w', 'e', ' ',
+ 'g', 'o', ' ', 'a', 'g', 'a', 'i', 'n',
+ // pigweed.proto.nested_pigweed.magic_number
+ 0x08, 0xe8, 0x04,
+ // pigweed.proto.nested_pigweed.device_info
+ 0x32, 0x26,
+ // pigweed.proto.nested_pigweed.device_info.attributes[0]
+ 0x22, 0x10,
+ // pigweed.proto.nested_pigweed.device_info.attributes[0].key
+ 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n',
+ // pigweed.proto.nested_pigweed.device_info.attributes[0].value
+ 0x12, 0x05, '5', '.', '3', '.', '1',
+ // pigweed.proto.nested_pigweed.device_info.attributes[1]
+ 0x22, 0x10,
+ // pigweed.proto.nested_pigweed.device_info.attributes[1].key
+ 0x0a, 0x04, 'c', 'h', 'i', 'p',
+ // pigweed.proto.nested_pigweed.device_info.attributes[1].value
+ 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c',
+ // pigweed.proto.nested_pigweed.device_info.status
+ 0x18, 0x03,
+ // pigweed.id[0]
+ 0x52, 0x02,
+ // pigweed.id[0].id
+ 0x08, 0x31,
+ // pigweed.id[1]
+ 0x52, 0x02,
+ // pigweed.id[1].id
+ 0x08, 0x39,
+ // pigweed.id[2]
+ 0x52, 0x02,
+ // pigweed.id[2].id
+ 0x08, 0x4b,
+ // pigweed.id[3]
+ 0x52, 0x02,
+ // pigweed.id[3].id
+ 0x08, 0x67,
+ // pigweed.id[4]
+ 0x52, 0x03,
+ // pigweed.id[4].id
+ 0x08, 0x8d, 0x01
+
+ };
+ // clang-format on
+
+ EXPECT_EQ(Pigweed::FindMagicNumber(as_bytes(span(proto_data))).value(),
+ 0x49u);
+ EXPECT_EQ(Pigweed::FindZiggy(as_bytes(span(proto_data))).value(), -111);
+ EXPECT_EQ(Pigweed::FindCycles(as_bytes(span(proto_data))).value(),
+ 0x40302010fecaaddeu);
+ EXPECT_EQ(Pigweed::FindRatio(as_bytes(span(proto_data))).value(), -1.42f);
+
+ auto result = Pigweed::FindErrorMessage(as_bytes(span(proto_data)));
+ EXPECT_EQ(result.status(), OkStatus());
+ InlineString<32> str(*result);
+ EXPECT_STREQ(str.c_str(), "not a typewriter");
+
+ EXPECT_EQ(Pigweed::FindBin(as_bytes(span(proto_data))).value(),
+ Pigweed::Protobuf::Binary::ZERO);
+
+ Result<ConstByteSpan> pigweed =
+ Pigweed::FindPigweed(as_bytes(span(proto_data)));
+ EXPECT_EQ(result.status(), OkStatus());
+ EXPECT_EQ(pigweed->size(), 2u);
+
+ EXPECT_EQ(Pigweed::Pigweed::FindStatus(*pigweed).value(),
+ Bool::FILE_NOT_FOUND);
+
+ // Nonexisting fields.
+ EXPECT_EQ(Pigweed::FindData(as_bytes(span(proto_data))).status(),
+ Status::NotFound());
+ EXPECT_EQ(Pigweed::FindDescription(as_bytes(span(proto_data))).status(),
+ Status::NotFound());
+ EXPECT_EQ(Pigweed::FindSpecialProperty(as_bytes(span(proto_data))).status(),
+ Status::NotFound());
+ EXPECT_EQ(Pigweed::FindBungle(as_bytes(span(proto_data))).status(),
+ Status::NotFound());
+}
+
+TEST(Codegen, FindStream) {
+ stream::MemoryReader reader({});
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.magic_number
+ 0x08, 0x49,
+ // pigweed.ziggy
+ 0x10, 0xdd, 0x01,
+ // pigweed.cycles
+ 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40,
+ // pigweed.ratio
+ 0x25, 0x8f, 0xc2, 0xb5, 0xbf,
+ // pigweed.error_message
+ 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ',
+ 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r',
+ // pigweed.bin
+ 0x40, 0x01,
+ // pigweed.pigweed
+ 0x3a, 0x02,
+ // pigweed.pigweed.status
+ 0x08, 0x02,
+ // pigweed.proto
+ 0x4a, 0x56,
+ // pigweed.proto.bin
+ 0x10, 0x00,
+ // pigweed.proto.pigweed_pigweed_bin
+ 0x18, 0x00,
+ // pigweed.proto.pigweed_protobuf_bin
+ 0x20, 0x01,
+ // pigweed.proto.meta
+ 0x2a, 0x0f,
+ // pigweed.proto.meta.file_name
+ 0x0a, 0x0b, '/', 'e', 't', 'c', '/', 'p', 'a', 's', 's', 'w', 'd',
+ // pigweed.proto.meta.status
+ 0x10, 0x02,
+ // pigweed.proto.nested_pigweed
+ 0x0a, 0x3d,
+ // pigweed.proto.nested_pigweed.error_message
+ 0x2a, 0x10, 'h', 'e', 'r', 'e', ' ', 'w', 'e', ' ',
+ 'g', 'o', ' ', 'a', 'g', 'a', 'i', 'n',
+ // pigweed.proto.nested_pigweed.magic_number
+ 0x08, 0xe8, 0x04,
+ // pigweed.proto.nested_pigweed.device_info
+ 0x32, 0x26,
+ // pigweed.proto.nested_pigweed.device_info.attributes[0]
+ 0x22, 0x10,
+ // pigweed.proto.nested_pigweed.device_info.attributes[0].key
+ 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n',
+ // pigweed.proto.nested_pigweed.device_info.attributes[0].value
+ 0x12, 0x05, '5', '.', '3', '.', '1',
+ // pigweed.proto.nested_pigweed.device_info.attributes[1]
+ 0x22, 0x10,
+ // pigweed.proto.nested_pigweed.device_info.attributes[1].key
+ 0x0a, 0x04, 'c', 'h', 'i', 'p',
+ // pigweed.proto.nested_pigweed.device_info.attributes[1].value
+ 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c',
+ // pigweed.proto.nested_pigweed.device_info.status
+ 0x18, 0x03,
+ // pigweed.id[0]
+ 0x52, 0x02,
+ // pigweed.id[0].id
+ 0x08, 0x31,
+ // pigweed.id[1]
+ 0x52, 0x02,
+ // pigweed.id[1].id
+ 0x08, 0x39,
+ // pigweed.id[2]
+ 0x52, 0x02,
+ // pigweed.id[2].id
+ 0x08, 0x4b,
+ // pigweed.id[3]
+ 0x52, 0x02,
+ // pigweed.id[3].id
+ 0x08, 0x67,
+ // pigweed.id[4]
+ 0x52, 0x03,
+ // pigweed.id[4].id
+ 0x08, 0x8d, 0x01
+
+ };
+ // clang-format on
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindMagicNumber(reader).value(), 0x49u);
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindZiggy(reader).value(), -111);
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindCycles(reader).value(), 0x40302010fecaaddeu);
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindRatio(reader).value(), -1.42f);
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ char str[32] = {'\0'};
+ auto result = Pigweed::FindErrorMessage(reader, str);
+ EXPECT_EQ(result.status(), OkStatus());
+ EXPECT_EQ(result.size(), 16u);
+ EXPECT_STREQ(str, "not a typewriter");
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ InlineString<32> error_message;
+ result = Pigweed::FindErrorMessage(reader, error_message);
+ EXPECT_EQ(result.status(), OkStatus());
+ EXPECT_EQ(result.size(), 16u);
+ EXPECT_STREQ(error_message.c_str(), "not a typewriter");
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindBin(reader).value(), Pigweed::Protobuf::Binary::ZERO);
+
+ // Nonexisting fields.
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ std::byte buf[32];
+ EXPECT_EQ(Pigweed::FindData(reader, buf).status(), Status::NotFound());
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindDescription(reader, str).status(), Status::NotFound());
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindSpecialProperty(reader).status(), Status::NotFound());
+
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindBungle(reader).status(), Status::NotFound());
+
+ // Advance the stream past `magic_number`, then attempt to find it.
+ reader = stream::MemoryReader(as_bytes(span(proto_data)));
+ EXPECT_EQ(Pigweed::FindZiggy(reader).status(), OkStatus());
+ EXPECT_EQ(Pigweed::FindMagicNumber(reader).status(), Status::NotFound());
+}
+
} // namespace
} // namespace pw::protobuf
diff --git a/pw_protobuf/codegen_encoder_test.cc b/pw_protobuf/codegen_encoder_test.cc
index de0a581ef..ffb6bfb00 100644
--- a/pw_protobuf/codegen_encoder_test.cc
+++ b/pw_protobuf/codegen_encoder_test.cc
@@ -118,7 +118,7 @@ TEST(Codegen, Codegen) {
}
}
- for (int i = 0; i < 5; ++i) {
+ for (unsigned i = 0; i < 5; ++i) {
Proto::ID::StreamEncoder id = pigweed.GetIdEncoder();
ASSERT_EQ(OkStatus(), id.WriteId(5 * i * i + 3 * i + 49));
}
@@ -257,11 +257,11 @@ TEST(CodegenRepeated, NonPackedScalar) {
stream::MemoryWriter writer(encode_buffer);
RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
- for (int i = 0; i < 4; ++i) {
+ for (uint32_t i = 0; i < 4; ++i) {
ASSERT_EQ(OkStatus(), repeated_test.WriteUint32s(i * 16));
}
- for (int i = 0; i < 4; ++i) {
+ for (uint32_t i = 0; i < 4; ++i) {
ASSERT_EQ(OkStatus(), repeated_test.WriteFixed32s(i * 16));
}
@@ -444,7 +444,7 @@ TEST(CodegenRepeated, Message) {
std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
RepeatedTest::MemoryEncoder repeated_test(encode_buffer);
- for (int i = 0; i < 3; ++i) {
+ for (uint32_t i = 0; i < 3; ++i) {
auto structs = repeated_test.GetStructsEncoder();
ASSERT_EQ(OkStatus(), structs.WriteOne(i * 1));
ASSERT_EQ(OkStatus(), structs.WriteTwo(i * 2));
diff --git a/pw_protobuf/codegen_message_test.cc b/pw_protobuf/codegen_message_test.cc
index f049715ea..cf987c604 100644
--- a/pw_protobuf/codegen_message_test.cc
+++ b/pw_protobuf/codegen_message_test.cc
@@ -321,6 +321,35 @@ TEST(CodegenMessage, FixReservedIdentifiers) {
PW_MODIFY_DIAGNOSTICS_POP();
+TEST(CodegenMessage, SetEncoder) {
+ Pigweed::Message msg{};
+
+ EXPECT_FALSE(msg.id);
+ msg.id.SetEncoder(
+ [](Pigweed::StreamEncoder&) -> Status { return OkStatus(); });
+ EXPECT_TRUE(msg.id);
+}
+
+TEST(CodegenMessage, SetDecoder) {
+ Pigweed::Message msg{};
+
+ EXPECT_FALSE(msg.id);
+ msg.id.SetDecoder(
+ [](Pigweed::StreamDecoder&) -> Status { return OkStatus(); });
+ EXPECT_TRUE(msg.id);
+}
+
+TEST(CodegenMessage, SetEncoderAndDecoder) {
+ Pigweed::Message msg{};
+
+ EXPECT_FALSE(msg.id);
+ msg.id.SetEncoder(
+ [](Pigweed::StreamEncoder&) -> Status { return OkStatus(); });
+ msg.id.SetDecoder(
+ [](Pigweed::StreamDecoder&) -> Status { return OkStatus(); });
+ EXPECT_TRUE(msg.id);
+}
+
TEST(CodegenMessage, Read) {
// clang-format off
constexpr uint8_t proto_data[] = {
@@ -388,12 +417,12 @@ TEST(CodegenMessage, ReadNonPackedScalar) {
ASSERT_EQ(status, OkStatus());
ASSERT_EQ(message.uint32s.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(message.uint32s[i], i * 16u);
}
ASSERT_EQ(message.fixed32s.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(message.fixed32s[i], i * 16u);
}
}
@@ -424,12 +453,12 @@ TEST(CodegenMessage, ReadPackedScalar) {
ASSERT_EQ(status, OkStatus());
ASSERT_EQ(message.uint32s.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(message.uint32s[i], i * 16u);
}
ASSERT_EQ(message.fixed32s.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_EQ(message.fixed32s[i], i * 16u);
}
}
@@ -472,12 +501,12 @@ TEST(CodegenMessage, ReadPackedScalarRepeated) {
ASSERT_EQ(status, OkStatus());
ASSERT_EQ(message.uint32s.size(), 8u);
- for (int i = 0; i < 8; ++i) {
+ for (unsigned short i = 0; i < 8; ++i) {
EXPECT_EQ(message.uint32s[i], i * 16u);
}
ASSERT_EQ(message.fixed32s.size(), 8u);
- for (int i = 0; i < 8; ++i) {
+ for (unsigned short i = 0; i < 8; ++i) {
EXPECT_EQ(message.fixed32s[i], i * 16u);
}
}
@@ -655,7 +684,7 @@ TEST(CodegenMessage, ReadPackedEnum) {
ASSERT_EQ(status, OkStatus());
ASSERT_EQ(message.enums.size(), 4u);
- for (int i = 0; i < 4; ++i) {
+ for (unsigned short i = 0; i < 4; ++i) {
EXPECT_TRUE(IsValidEnum(message.enums[i]));
}
@@ -1020,7 +1049,7 @@ TEST(CodegenMessage, ReadNestedRepeated) {
// Repeated nested messages require a callback since there would otherwise be
// no way to set callbacks on the nested message.
RepeatedTest::Message message{};
- int i = 0;
+ unsigned i = 0;
message.structs.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) {
EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStructs);
@@ -1029,7 +1058,7 @@ TEST(CodegenMessage, ReadNestedRepeated) {
const auto status = structs_decoder.Read(structs_message);
EXPECT_EQ(status, OkStatus());
- EXPECT_LT(i, 2);
+ EXPECT_LT(i, 2u);
EXPECT_EQ(structs_message.one, i * 32 + 16u);
EXPECT_EQ(structs_message.two, i * 32 + 32u);
++i;
@@ -1385,7 +1414,7 @@ TEST(CodegenMessage, WriteDefaults) {
TEST(CodegenMessage, WritePackedScalar) {
RepeatedTest::Message message{};
- for (int i = 0; i < 4; ++i) {
+ for (unsigned i = 0; i < 4; ++i) {
message.uint32s.push_back(i * 16u);
message.fixed32s.push_back(i * 16u);
}
@@ -1423,7 +1452,7 @@ TEST(CodegenMessage, WritePackedScalar) {
TEST(CodegenMessage, WritePackedScalarFixedLength) {
RepeatedTest::Message message{};
- for (int i = 0; i < 4; ++i) {
+ for (unsigned i = 0; i < 4; ++i) {
message.uint64s[i] = (i + 1) * 1000u;
}
message.doubles[0] = 3.14159;
@@ -1622,7 +1651,7 @@ TEST(CodegenMessage, WriteNestedRepeated) {
// Repeated nested messages require a callback since there would otherwise be
// no way to set callbacks on the nested message.
message.structs.SetEncoder([](RepeatedTest::StreamEncoder& encoder) {
- for (int i = 0; i < 2; ++i) {
+ for (uint32_t i = 0; i < 2; ++i) {
Struct::Message struct_message{};
struct_message.one = i * 32 + 16u;
struct_message.two = i * 32 + 32u;
@@ -2014,7 +2043,7 @@ TEST(CodegenMessage, CallbackInSubclass) {
{"the appetite may sicken, and so die"}};
EXPECT_EQ(message.all_strings.size(), 3u);
- for (int i = 0; i < 3; ++i) {
+ for (unsigned short i = 0; i < 3; ++i) {
EXPECT_EQ(std::memcmp(message.all_strings[i].data(),
kExpectedStrings[i].data(),
kExpectedStrings[i].size()),
@@ -2023,5 +2052,30 @@ TEST(CodegenMessage, CallbackInSubclass) {
}
}
+TEST(CodegenMessage, MaxSize) {
+ // Verify constants generated from max_size options in full_test.options
+ static_assert(Pigweed::kErrorMessageMaxSize == 64);
+ static_assert(Pigweed::kDataMaxSize == 8);
+
+ Pigweed::Message size_message;
+ EXPECT_EQ(size_message.error_message.max_size(),
+ Pigweed::kErrorMessageMaxSize);
+ EXPECT_EQ(size_message.data.max_size(), Pigweed::kDataMaxSize);
+
+ // Verify constants generated from max_count options in repeated.options
+ static_assert(RepeatedTest::kUint32sMaxSize == 8);
+ static_assert(RepeatedTest::kFixed32sMaxSize == 8);
+ static_assert(RepeatedTest::kDoublesMaxSize == 2);
+ static_assert(RepeatedTest::kUint64sMaxSize == 4);
+ static_assert(RepeatedTest::kEnumsMaxSize == 4);
+
+ RepeatedTest::Message count_message;
+ EXPECT_EQ(count_message.uint32s.max_size(), RepeatedTest::kUint32sMaxSize);
+ EXPECT_EQ(count_message.fixed32s.max_size(), RepeatedTest::kFixed32sMaxSize);
+ EXPECT_EQ(count_message.doubles.max_size(), RepeatedTest::kDoublesMaxSize);
+ EXPECT_EQ(count_message.uint64s.max_size(), RepeatedTest::kUint64sMaxSize);
+ EXPECT_EQ(count_message.enums.max_size(), RepeatedTest::kEnumsMaxSize);
+}
+
} // namespace
} // namespace pw::protobuf
diff --git a/pw_protobuf/decoder.cc b/pw_protobuf/decoder.cc
index 2f58eb501..7e2ae5a06 100644
--- a/pw_protobuf/decoder.cc
+++ b/pw_protobuf/decoder.cc
@@ -16,6 +16,7 @@
#include <cstring>
+#include "pw_assert/check.h"
#include "pw_varint/varint.h"
namespace pw::protobuf {
@@ -53,7 +54,8 @@ uint32_t Decoder::FieldNumber() const {
if (!FieldKey::IsValidKey(key)) {
return 0;
}
- return FieldKey(key).field_number();
+ PW_DCHECK(key <= std::numeric_limits<uint32_t>::max());
+ return FieldKey(static_cast<uint32_t>(key)).field_number();
}
Status Decoder::ReadUint32(uint32_t* out) {
@@ -65,7 +67,7 @@ Status Decoder::ReadUint32(uint32_t* out) {
if (value > std::numeric_limits<uint32_t>::max()) {
return Status::OutOfRange();
}
- *out = value;
+ *out = static_cast<uint32_t>(value);
return OkStatus();
}
@@ -78,7 +80,7 @@ Status Decoder::ReadSint32(int32_t* out) {
if (value > std::numeric_limits<int32_t>::max()) {
return Status::OutOfRange();
}
- *out = value;
+ *out = static_cast<uint32_t>(value);
return OkStatus();
}
@@ -124,7 +126,8 @@ size_t Decoder::FieldSize() const {
uint64_t value = 0;
size_t expected_size = 0;
- switch (FieldKey(key).wire_type()) {
+ PW_DCHECK(key <= std::numeric_limits<uint32_t>::max());
+ switch (FieldKey(static_cast<uint32_t>(key)).wire_type()) {
case WireType::kVarint:
expected_size = varint::Decode(remainder, &value);
if (expected_size == 0) {
@@ -168,7 +171,8 @@ Status Decoder::ConsumeKey(WireType expected_type) {
return Status::DataLoss();
}
- if (FieldKey(key).wire_type() != expected_type) {
+ PW_DCHECK(key <= std::numeric_limits<uint32_t>::max());
+ if (FieldKey(static_cast<uint32_t>(key)).wire_type() != expected_type) {
return Status::FailedPrecondition();
}
diff --git a/pw_protobuf/docs.rst b/pw_protobuf/docs.rst
index 021d03fde..a6182ef8b 100644
--- a/pw_protobuf/docs.rst
+++ b/pw_protobuf/docs.rst
@@ -8,9 +8,9 @@ the Protocol Buffer wire format with a lightweight code and data footprint.
.. note::
- The protobuf module is a work in progress. Wire format encoding and decoding
- is supported, though the APIs are not final. C++ code generation exists for
- encoding and decoding, but not yet optimized for in-memory decoding.
+ The protobuf module is a work in progress. Wire format encoding and decoding
+ is supported, though the APIs are not final. C++ code generation exists for
+ encoding and decoding, but not yet optimized for in-memory decoding.
--------
Overview
@@ -23,9 +23,9 @@ suitable for their product on top of the implementation.
The API is designed in three layers, which can be freely intermixed with each
other in your code, depending on point of use requirements:
- 1. Message Structures,
- 2. Per-Field Writers and Readers,
- 3. Direct Writers and Readers.
+1. Message Structures,
+2. Per-Field Writers and Readers,
+3. Direct Writers and Readers.
This has a few benefits. The primary one is that it allows the core proto
serialization and deserialization libraries to be relatively small.
@@ -35,29 +35,29 @@ serialization and deserialization libraries to be relatively small.
To demonstrate these layers, we use the following protobuf message definition
in the examples:
-.. code::
+.. code-block:: protobuf
- message Customer {
- enum Status {
- NEW = 1;
- ACTIVE = 2;
- INACTIVE = 3;
- }
- int32 age = 1;
- string name = 2;
- Status status = 3;
- }
+ message Customer {
+ enum Status {
+ NEW = 1;
+ ACTIVE = 2;
+ INACTIVE = 3;
+ }
+ int32 age = 1;
+ string name = 2;
+ Status status = 3;
+ }
And the following accompanying options file:
-.. code::
+.. code-block:: text
- Customer.name max_size:32
+ Customer.name max_size:32
.. toctree::
- :maxdepth: 1
+ :maxdepth: 1
- size_report
+ size_report
Message Structures
==================
@@ -66,50 +66,50 @@ code generation, integrated with Pigweed's build system.
This results in the following generated structure:
-.. code:: c++
+.. code-block:: c++
- enum class Customer::Status : uint32_t {
- NEW = 1,
- ACTIVE = 2,
- INACTIVE = 3,
+ enum class Customer::Status : uint32_t {
+ NEW = 1,
+ ACTIVE = 2,
+ INACTIVE = 3,
- kNew = NEW,
- kActive = ACTIVE,
- kInactive = INACTIVE,
- };
+ kNew = NEW,
+ kActive = ACTIVE,
+ kInactive = INACTIVE,
+ };
- struct Customer::Message {
- int32_t age;
- pw::InlineString<32> name;
- Customer::Status status;
- };
+ struct Customer::Message {
+ int32_t age;
+ pw::InlineString<32> name;
+ Customer::Status status;
+ };
Which can be encoded with the code:
-.. code:: c++
+.. code-block:: c++
- #include "example_protos/customer.pwpb.h"
+ #include "example_protos/customer.pwpb.h"
- pw::Status EncodeCustomer(Customer::StreamEncoder& encoder) {
- return encoder.Write({
- age = 33,
- name = "Joe Bloggs",
- status = Customer::Status::INACTIVE
- });
- }
+ pw::Status EncodeCustomer(Customer::StreamEncoder& encoder) {
+ return encoder.Write({
+ age = 33,
+ name = "Joe Bloggs",
+ status = Customer::Status::INACTIVE
+ });
+ }
And decoded into a struct with the code:
-.. code:: c++
+.. code-block:: c++
- #include "example_protos/customer.pwpb.h"
+ #include "example_protos/customer.pwpb.h"
- pw::Status DecodeCustomer(Customer::StreamDecoder& decoder) {
- Customer::Message customer{};
- PW_TRY(decoder.Read(customer));
- // Read fields from customer
- return pw::OkStatus();
- }
+ pw::Status DecodeCustomer(Customer::StreamDecoder& decoder) {
+ Customer::Message customer{};
+ PW_TRY(decoder.Read(customer));
+ // Read fields from customer
+ return pw::OkStatus();
+ }
These structures can be moved, copied, and compared with each other for
equality.
@@ -130,28 +130,28 @@ To check if the equality operator of a generated message covers all fields,
.. code-block:: c++
- template <typename Message>
- constexpr bool IsTriviallyComparable<Message>();
+ template <typename Message>
+ constexpr bool IsTriviallyComparable<Message>();
For example, given the following protobuf definitions:
-.. code-block::
+.. code-block:: protobuf
- message Point {
- int32 x = 1;
- int32 y = 2;
- }
+ message Point {
+ int32 x = 1;
+ int32 y = 2;
+ }
- message Label {
- Point point = 1;
- string label = 2;
- }
+ message Label {
+ Point point = 1;
+ string label = 2;
+ }
And the accompanying options file:
-.. code-block::
+.. code-block:: text
- Label.label use_callback:true
+ Label.label use_callback:true
The ``Point`` message can be fully compared for equality, but ``Label`` cannot.
``Label`` still defines an ``operator==``, but it ignores the ``label`` string.
@@ -172,22 +172,22 @@ buffer to encode to. The code generation includes a ``kMaxEncodedSizeBytes``
constant that represents the maximum encoded size of the protobuf message,
excluding the contents of any field values which require a callback.
-.. code:: c++
+.. code-block:: c++
- #include "example_protos/customer.pwpb.h"
+ #include "example_protos/customer.pwpb.h"
- std::byte buffer[Customer::kMaxEncodedSizeBytes];
- Customer::MemoryEncoder encoder(buffer);
- const auto status = encoder.Write({
- age = 22,
- name = "Wolfgang Bjornson",
- status = Customer::Status::ACTIVE
- });
+ std::byte buffer[Customer::kMaxEncodedSizeBytes];
+ Customer::MemoryEncoder encoder(buffer);
+ const auto status = encoder.Write({
+ age = 22,
+ name = "Wolfgang Bjornson",
+ status = Customer::Status::ACTIVE
+ });
- // Always check the encoder status or return values from Write calls.
- if (!status.ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
- }
+ // Always check the encoder status or return values from Write calls.
+ if (!status.ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
In the above example, because the ``name`` field has a ``max_size`` specified
in the accompanying options file, ``kMaxEncodedSizeBytes`` includes the maximum
@@ -202,54 +202,54 @@ For example if a ``bytes`` field length is not specified in the options file,
but is known to your code (``kMaxImageDataSize`` in this example being a
constant in your own code), you can simply add it to the generated constant:
-.. code:: c++
+.. code-block:: c++
- #include "example_protos/store.pwpb.h"
+ #include "example_protos/store.pwpb.h"
- const std::byte image_data[kMaxImageDataSize] = { ... };
+ const std::byte image_data[kMaxImageDataSize] = { ... };
- Store::Message store{};
- // Calling SetEncoder means we must always extend the buffer size.
- store.image_data.SetEncoder([](Store::StreamEncoder& encoder) {
- return encoder.WriteImageData(image_data);
- });
+ Store::Message store{};
+ // Calling SetEncoder means we must always extend the buffer size.
+ store.image_data.SetEncoder([](Store::StreamEncoder& encoder) {
+ return encoder.WriteImageData(image_data);
+ });
- std::byte buffer[Store::kMaxEncodedSizeBytes + kMaxImageDataSize];
- Store::MemoryEncoder encoder(buffer);
- const auto status = encoder.Write(store);
+ std::byte buffer[Store::kMaxEncodedSizeBytes + kMaxImageDataSize];
+ Store::MemoryEncoder encoder(buffer);
+ const auto status = encoder.Write(store);
- // Always check the encoder status or return values from Write calls.
- if (!status.ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
- }
+ // Always check the encoder status or return values from Write calls.
+ if (!status.ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
Or when using a variable number of repeated submessages, where the maximum
number is known to your code but not to the proto, you can add the constants
from one message type to another:
-.. code:: c++
+.. code-block:: c++
- #include "example_protos/person.pwpb.h"
+ #include "example_protos/person.pwpb.h"
- Person::Message grandchild{};
- // Calling SetEncoder means we must always extend the buffer size.
- grandchild.grandparent.SetEncoder([](Person::StreamEncoder& encoder) {
- PW_TRY(encoder.GetGrandparentEncoder().Write(maternal_grandma));
- PW_TRY(encoder.GetGrandparentEncoder().Write(maternal_grandpa));
- PW_TRY(encoder.GetGrandparentEncoder().Write(paternal_grandma));
- PW_TRY(encoder.GetGrandparentEncoder().Write(paternal_grandpa));
- return pw::OkStatus();
- });
+ Person::Message grandchild{};
+ // Calling SetEncoder means we must always extend the buffer size.
+ grandchild.grandparent.SetEncoder([](Person::StreamEncoder& encoder) {
+ PW_TRY(encoder.GetGrandparentEncoder().Write(maternal_grandma));
+ PW_TRY(encoder.GetGrandparentEncoder().Write(maternal_grandpa));
+ PW_TRY(encoder.GetGrandparentEncoder().Write(paternal_grandma));
+ PW_TRY(encoder.GetGrandparentEncoder().Write(paternal_grandpa));
+ return pw::OkStatus();
+ });
- std::byte buffer[Person::kMaxEncodedSizeBytes +
- Grandparent::kMaxEncodedSizeBytes * 4];
- Person::MemoryEncoder encoder(buffer);
- const auto status = encoder.Write(grandchild);
+ std::byte buffer[Person::kMaxEncodedSizeBytes +
+ Grandparent::kMaxEncodedSizeBytes * 4];
+ Person::MemoryEncoder encoder(buffer);
+ const auto status = encoder.Write(grandchild);
- // Always check the encoder status or return values from Write calls.
- if (!status.ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
- }
+ // Always check the encoder status or return values from Write calls.
+ if (!status.ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
.. warning::
Encoding to a buffer that is insufficiently large will return
@@ -273,33 +273,33 @@ There are lightweight wrappers around the core implementation, calling the
underlying methods with the correct field numbers and value types, and result
in no additional binary code over correctly using the core implementation.
-.. code:: c++
+.. code-block:: c++
- class Customer::StreamEncoder : pw::protobuf::StreamEncoder {
- public:
- // Message Structure Writer.
- pw::Status Write(const Customer::Message&);
+ class Customer::StreamEncoder : pw::protobuf::StreamEncoder {
+ public:
+ // Message Structure Writer.
+ pw::Status Write(const Customer::Message&);
- // Per-Field Typed Writers.
- pw::Status WriteAge(int32_t);
+ // Per-Field Typed Writers.
+ pw::Status WriteAge(int32_t);
- pw::Status WriteName(std::string_view);
- pw::Status WriteName(const char*, size_t);
+ pw::Status WriteName(std::string_view);
+ pw::Status WriteName(const char*, size_t);
- pw::Status WriteStatus(Customer::Status);
- };
+ pw::Status WriteStatus(Customer::Status);
+ };
So the same encoding method could be written as:
-.. code:: c++
+.. code-block:: c++
- #include "example_protos/customer.pwpb.h"
+ #include "example_protos/customer.pwpb.h"
- Status EncodeCustomer(Customer::StreamEncoder& encoder) {
- PW_TRY(encoder.WriteAge(33));
- PW_TRY(encoder.WriteName("Joe Bloggs"sv));
- PW_TRY(encoder.WriteStatus(Customer::Status::INACTIVE));
- }
+ Status EncodeCustomer(Customer::StreamEncoder& encoder) {
+ PW_TRY(encoder.WriteAge(33));
+ PW_TRY(encoder.WriteName("Joe Bloggs"sv));
+ PW_TRY(encoder.WriteStatus(Customer::Status::INACTIVE));
+ }
Pigweed's protobuf encoders encode directly to the wire format of a proto rather
than staging information to a mutable datastructure. This means any writes of a
@@ -307,7 +307,7 @@ value are final, and can't be referenced or modified as a later step in the
encode process.
Casting between generated StreamEncoder types
-=============================================
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pw_protobuf guarantees that all generated ``StreamEncoder`` classes can be
converted among each other. It's also safe to convert any ``MemoryEncoder`` to
any other ``StreamEncoder``.
@@ -319,69 +319,69 @@ messages.
For example:
-.. code::
+.. code-block:: protobuf
- // The first half of the overlaid message.
- message BaseMessage {
- uint32 length = 1;
- reserved 2; // Reserved for Overlay
- }
+ // The first half of the overlaid message.
+ message BaseMessage {
+ uint32 length = 1;
+ reserved 2; // Reserved for Overlay
+ }
- // OK: The second half of the overlaid message.
- message Overlay {
- reserved 1; // Reserved for BaseMessage
- uint32 height = 2;
- }
+ // OK: The second half of the overlaid message.
+ message Overlay {
+ reserved 1; // Reserved for BaseMessage
+ uint32 height = 2;
+ }
- // OK: A message that overlays and bundles both types together.
- message Both {
- uint32 length = 1; // Defined independently by BaseMessage
- uint32 height = 2; // Defined independently by Overlay
- }
+ // OK: A message that overlays and bundles both types together.
+ message Both {
+ uint32 length = 1; // Defined independently by BaseMessage
+ uint32 height = 2; // Defined independently by Overlay
+ }
- // BAD: Diverges from BaseMessage's definition, and can cause decode
- // errors/corruption.
- message InvalidOverlay {
- fixed32 length = 1;
- }
+ // BAD: Diverges from BaseMessage's definition, and can cause decode
+ // errors/corruption.
+ message InvalidOverlay {
+ fixed32 length = 1;
+ }
The ``StreamEncoderCast<>()`` helper template reduces very messy casting into
a much easier to read syntax:
-.. code:: c++
-
- #include "pw_protobuf/encoder.h"
- #include "pw_protobuf_test_protos/full_test.pwpb.h"
-
- Result<ConstByteSpan> EncodeOverlaid(uint32_t height,
- uint32_t length,
- ConstByteSpan encode_buffer) {
- BaseMessage::MemoryEncoder base(encode_buffer);
+.. code-block:: c++
- // Without StreamEncoderCast<>(), this line would be:
- // Overlay::StreamEncoder& overlay =
- // *static_cast<Overlay::StreamEncoder*>(
- // static_cast<pw::protobuf::StreamEncoder*>(&base)
- Overlay::StreamEncoder& overlay =
- StreamEncoderCast<Overlay::StreamEncoder>(base);
- if (!overlay.WriteHeight(height).ok()) {
- return overlay.status();
- }
- if (!base.WriteLength(length).ok()) {
- return base.status();
- }
- return ConstByteSpan(base);
- }
+ #include "pw_protobuf/encoder.h"
+ #include "pw_protobuf_test_protos/full_test.pwpb.h"
+
+ Result<ConstByteSpan> EncodeOverlaid(uint32_t height,
+ uint32_t length,
+ ConstByteSpan encode_buffer) {
+ BaseMessage::MemoryEncoder base(encode_buffer);
+
+ // Without StreamEncoderCast<>(), this line would be:
+ // Overlay::StreamEncoder& overlay =
+ // *static_cast<Overlay::StreamEncoder*>(
+ // static_cast<pw::protobuf::StreamEncoder*>(&base)
+ Overlay::StreamEncoder& overlay =
+ StreamEncoderCast<Overlay::StreamEncoder>(base);
+ if (!overlay.WriteHeight(height).ok()) {
+ return overlay.status();
+ }
+ if (!base.WriteLength(length).ok()) {
+ return base.status();
+ }
+ return ConstByteSpan(base);
+ }
While this use case is somewhat uncommon, it's a core supported use case of
pw_protobuf.
.. warning::
- Using this to convert one stream encoder to another when the messages
- themselves do not safely overlay will result in corrupt protos. Be careful
- when doing this as there's no compile-time way to detect whether or not two
- messages are meant to overlay.
+ Using this to convert one stream encoder to another when the messages
+ themselves do not safely overlay will result in corrupt protos. Be careful
+ when doing this as there's no compile-time way to detect whether or not two
+ messages are meant to overlay.
Decoding
--------
@@ -389,62 +389,126 @@ For decoding, in addition to the ``Read()`` method that populates a message
structure, the following additional methods are also generated in the typed
``StreamDecoder`` class.
-.. code:: c++
+.. code-block:: c++
- class Customer::StreamDecoder : pw::protobuf::StreamDecoder {
- public:
- // Message Structure Reader.
- pw::Status Read(Customer::Message&);
+ class Customer::StreamDecoder : pw::protobuf::StreamDecoder {
+ public:
+ // Message Structure Reader.
+ pw::Status Read(Customer::Message&);
- // Returns the identity of the current field.
- ::pw::Result<Fields> Field();
+ // Returns the identity of the current field.
+ ::pw::Result<Fields> Field();
- // Per-Field Typed Readers.
- pw::Result<int32_t> ReadAge();
+ // Per-Field Typed Readers.
+ pw::Result<int32_t> ReadAge();
- pw::StatusWithSize ReadName(pw::span<char>);
- BytesReader GetNameReader(); // Read name as a stream of bytes.
+ pw::StatusWithSize ReadName(pw::span<char>);
+ BytesReader GetNameReader(); // Read name as a stream of bytes.
- pw::Result<Customer::Status> ReadStatus();
- };
+ pw::Result<Customer::Status> ReadStatus();
+ };
Complete and correct decoding requires looping through the fields, so is more
complex than encoding or using the message structure.
-.. code:: c++
-
- pw::Status DecodeCustomer(Customer::StreamDecoder& decoder) {
- uint32_t age;
- char name[32];
- Customer::Status status;
-
- while ((status = decoder.Next()).ok()) {
- switch (decoder.Field().value()) {
- case Customer::Fields::kAge: {
- PW_TRY_ASSIGN(age, decoder.ReadAge());
- break;
- }
- case Customer::Fields::kName: {
- PW_TRY(decoder.ReadName(name));
- break;
- }
- case Customer::Fields::kStatus: {
- PW_TRY_ASSIGN(status, decoder.ReadStatus());
- break;
- }
- }
- }
+.. code-block:: c++
- return status.IsOutOfRange() ? OkStatus() : status;
- }
+ pw::Status DecodeCustomer(Customer::StreamDecoder& decoder) {
+ uint32_t age;
+ char name[32];
+ Customer::Status status;
+
+ while ((status = decoder.Next()).ok()) {
+ switch (decoder.Field().value()) {
+ case Customer::Fields::kAge: {
+ PW_TRY_ASSIGN(age, decoder.ReadAge());
+ break;
+ }
+ case Customer::Fields::kName: {
+ PW_TRY(decoder.ReadName(name));
+ break;
+ }
+ case Customer::Fields::kStatus: {
+ PW_TRY_ASSIGN(status, decoder.ReadStatus());
+ break;
+ }
+ }
+ }
+
+ return status.IsOutOfRange() ? OkStatus() : status;
+ }
.. warning:: ``Fields::SNAKE_CASE`` is deprecated. Use ``Fields::kCamelCase``.
- Transitional support for ``Fields::SNAKE_CASE`` will soon only be available by
- explicitly setting the following GN variable in your project:
- ``pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES=true``
+ Transitional support for ``Fields::SNAKE_CASE`` will soon only be available by
+ explicitly setting the following GN variable in your project:
+ ``pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES=true``
+
+ This support will be removed after downstream projects have been migrated.
+
+
+Reading a single field
+----------------------
+Sometimes, only a single field from a serialized message needs to be read. In
+these cases, setting up a decoder and iterating through the message is a lot of
+boilerplate. ``pw_protobuf`` generates convenient ``Find*()`` functions for
+most fields in a message which handle this for you.
+
+.. code-block:: c++
+
+ pw::Status ReadCustomerData(pw::ConstByteSpan serialized_customer) {
+ pw::Result<uint32_t> age = Customer::FindAge(serialized_customer);
+ if (!age.ok()) {
+ return age.status();
+ }
+
+ // This will scan the buffer again from the start, which is less efficient
+ // than writing a custom decoder loop.
+ pw::Result<std::string_view> name = Customer::FindName(serialized_customer);
+ if (!age.ok()) {
+ return age.status();
+ }
+
+ DoStuff(age, name);
+ return pw::OkStatus();
+ }
+
+The ``Find`` APIs also work with streamed data, as shown below.
- This support will be removed after downstream projects have been migrated.
+.. code-block:: c++
+
+ pw::Status ReadCustomerData(pw::stream::Reader& customer_stream) {
+ pw::Result<uint32_t> age = Customer::FindAge(customer_stream);
+ if (!age.ok()) {
+ return age.status();
+ }
+
+ // This will begin scanning for `name` from the current position of the
+ // stream (following the `age` field). If `name` appeared before `age` in
+ // the serialized data, it will not be found.
+ //
+ // Note that unlike with the buffer APIs, stream Find methods copy `string`
+ // and `bytes` fields into a user-provided buffer.
+ char name[32];
+ pw::StatusWithSize sws = Customer::FindName(serialized_customer, name);
+ if (!sws.ok()) {
+ return sws.status();
+ }
+ if (sws.size() >= sizeof(name)) {
+ return pw::Status::OutOfRange();
+ }
+ name[sws.size()] = '\0';
+
+ DoStuff(age, name);
+ return pw::OkStatus();
+ }
+
+.. note::
+
+ Each call to ``Find*()`` linearly scans through the message. If you have to
+ read multiple fields, it is more efficient to instantiate your own decoder as
+ described above. Additionally, to avoid confusion, ``Find*()`` methods are
+ not generated for repeated fields.
Direct Writers and Readers
@@ -465,37 +529,37 @@ without needing to build the complete message in memory
To encode the same message we've used in the examples thus far, we would use
the following parts of the core API:
-.. code:: c++
+.. code-block:: c++
- class pw::protobuf::StreamEncoder {
- public:
- Status WriteInt32(uint32_t field_number, int32_t);
- Status WriteUint32(uint32_t field_number, uint32_t);
+ class pw::protobuf::StreamEncoder {
+ public:
+ Status WriteInt32(uint32_t field_number, int32_t);
+ Status WriteUint32(uint32_t field_number, uint32_t);
- Status WriteString(uint32_t field_number, std::string_view);
- Status WriteString(uint32_t field_number, const char*, size_t);
+ Status WriteString(uint32_t field_number, std::string_view);
+ Status WriteString(uint32_t field_number, const char*, size_t);
- // And many other methods, see pw_protobuf/encoder.h
- };
+ // And many other methods, see pw_protobuf/encoder.h
+ };
Encoding the same message requires that we specify the field numbers, which we
can hardcode, or supplement using the C++ code generated ``Fields`` enum, and
cast the enumerated type.
-.. code:: c++
+.. code-block:: c++
- #include "pw_protobuf/encoder.h"
- #include "example_protos/customer.pwpb.h"
+ #include "pw_protobuf/encoder.h"
+ #include "example_protos/customer.pwpb.h"
- Status EncodeCustomer(pw::protobuf::StreamEncoder& encoder) {
- PW_TRY(encoder.WriteInt32(static_cast<uint32_t>(Customer::Fields::kAge),
- 33));
- PW_TRY(encoder.WriteString(static_cast<uint32_t>(Customer::Fields::kName),
- "Joe Bloggs"sv));
- PW_TRY(encoder.WriteUint32(
- static_cast<uint32_t>(Customer::Fields::kStatus),
- static_cast<uint32_t>(Customer::Status::INACTIVE)));
- }
+ Status EncodeCustomer(pw::protobuf::StreamEncoder& encoder) {
+ PW_TRY(encoder.WriteInt32(static_cast<uint32_t>(Customer::Fields::kAge),
+ 33));
+ PW_TRY(encoder.WriteString(static_cast<uint32_t>(Customer::Fields::kName),
+ "Joe Bloggs"sv));
+ PW_TRY(encoder.WriteUint32(
+ static_cast<uint32_t>(Customer::Fields::kStatus),
+ static_cast<uint32_t>(Customer::Status::INACTIVE)));
+ }
Decoding
--------
@@ -504,52 +568,57 @@ of the encoders.
To decode the same message we would use the following parts of the core API:
-.. code:: c++
+.. code-block:: c++
- class pw::protobuf::StreamDecoder {
- public:
- // Returns the identity of the current field.
- ::pw::Result<uint32_t> FieldNumber();
+ class pw::protobuf::StreamDecoder {
+ public:
+ // Returns the identity of the current field.
+ ::pw::Result<uint32_t> FieldNumber();
- Result<int32_t> ReadInt32();
- Result<uint32_t> ReadUint32();
+ Result<int32_t> ReadInt32();
+ Result<uint32_t> ReadUint32();
- StatusWithSize ReadString(pw::span<char>);
+ StatusWithSize ReadString(pw::span<char>);
- // And many other methods, see pw_protobuf/stream_decoder.h
- };
+ // And many other methods, see pw_protobuf/stream_decoder.h
+ };
As with the typed per-field API, complete and correct decoding requires looping
through the fields and checking the field numbers, along with casting types.
-.. code:: c++
-
- pw::Status DecodeCustomer(pw::protobuf::StreamDecoder& decoder) {
- uint32_t age;
- char name[32];
- Customer::Status status;
-
- while ((status = decoder.Next()).ok()) {
- switch (decoder.FieldNumber().value()) {
- case static_cast<uint32_t>(Customer::Fields::kAge): {
- PW_TRY_ASSIGN(age, decoder.ReadInt32());
- break;
- }
- case static_cast<uint32_t>(Customer::Fields::kName): {
- PW_TRY(decoder.ReadString(name));
- break;
- }
- case static_cast<uint32_t>(Customer::Fields::kStatus): {
- uint32_t status_value;
- PW_TRY_ASSIGN(status_value, decoder.ReadUint32());
- status = static_cast<Customer::Status>(status_value);
- break;
- }
- }
- }
+.. code-block:: c++
- return status.IsOutOfRange() ? OkStatus() : status;
- }
+ pw::Status DecodeCustomer(pw::protobuf::StreamDecoder& decoder) {
+ uint32_t age;
+ char name[32];
+ Customer::Status status;
+
+ while ((status = decoder.Next()).ok()) {
+ switch (decoder.FieldNumber().value()) {
+ case static_cast<uint32_t>(Customer::Fields::kAge): {
+ PW_TRY_ASSIGN(age, decoder.ReadInt32());
+ break;
+ }
+ case static_cast<uint32_t>(Customer::Fields::kName): {
+ PW_TRY(decoder.ReadString(name));
+ break;
+ }
+ case static_cast<uint32_t>(Customer::Fields::kStatus): {
+ uint32_t status_value;
+ PW_TRY_ASSIGN(status_value, decoder.ReadUint32());
+ status = static_cast<Customer::Status>(status_value);
+ break;
+ }
+ }
+ }
+
+ return status.IsOutOfRange() ? OkStatus() : status;
+ }
+
+Find APIs
+---------
+
+.. doxygenfile:: pw_protobuf/public/pw_protobuf/find.h
Handling of packages
@@ -564,7 +633,7 @@ order to avoid clashes for projects that link against multiple C++ proto
libraries in the same library.
..
- TODO(b/258832150) Remove this section, if possible
+ TODO: b/258832150 - Remove this section, if possible
In some cases, pw_protobuf codegen may encounter external message references
during parsing, where it is unable to resolve the package name of the message.
@@ -586,29 +655,29 @@ system integration for pw_protobuf codegen.
Example ``BUILD.gn``:
-.. code::
+.. code-block::
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_build/target_types.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
- # This target controls where the *.pwpb.h headers end up on the include path.
- # In this example, it's at "pet_daycare_protos/client.pwpb.h".
- pw_proto_library("pet_daycare_protos") {
- sources = [
- "pet_daycare_protos/client.proto",
- ]
- }
+ # This target controls where the *.pwpb.h headers end up on the include path.
+ # In this example, it's at "pet_daycare_protos/client.pwpb.h".
+ pw_proto_library("pet_daycare_protos") {
+ sources = [
+ "pet_daycare_protos/client.proto",
+ ]
+ }
- pw_source_set("example_client") {
- sources = [ "example_client.cc" ]
- deps = [
- ":pet_daycare_protos.pwpb",
- dir_pw_bytes,
- dir_pw_stream,
- ]
- }
+ pw_source_set("example_client") {
+ sources = [ "example_client.cc" ]
+ deps = [
+ ":pet_daycare_protos.pwpb",
+ dir_pw_bytes,
+ dir_pw_stream,
+ ]
+ }
-------------
Configuration
@@ -656,13 +725,13 @@ especially useful for host builds using upstream protoc code generation, where
host software can use the reflection API to query for the options and validate
messages comply with the specified limitations.
-.. code::
+.. code-block:: text
- import "pw_protobuf_protos/field_options.proto";
+ import "pw_protobuf_protos/field_options.proto";
- message Demo {
- string size_limited_string = 1 [(pw.protobuf.pwpb).max_size = 16];
- };
+ message Demo {
+ string size_limited_string = 1 [(pw.protobuf.pwpb).max_size = 16];
+ };
Options Files
=============
@@ -675,30 +744,30 @@ comments, and blank lines are ignored.
Example:
-.. code::
+.. code-block::
- // Set an option for a specific field.
- fuzzy_friends.Client.visit_dates max_count:16
+ // Set an option for a specific field.
+ fuzzy_friends.Client.visit_dates max_count:16
- // Set options for multiple fields by wildcard matching.
- fuzzy_friends.Pet.* max_size:32
+ // Set options for multiple fields by wildcard matching.
+ fuzzy_friends.Pet.* max_size:32
- // Set multiple options in one go.
- fuzzy_friends.Dog.paws max_count:4 fixed_count:true
+ // Set multiple options in one go.
+ fuzzy_friends.Dog.paws max_count:4 fixed_count:true
Options files should be listed as ``inputs`` when defining ``pw_proto_library``,
e.g.
-.. code::
+.. code-block::
- pw_proto_library("pet_daycare_protos") {
- sources = [
- "pet_daycare_protos/client.proto",
- ]
- inputs = [
- "pet_daycare_protos/client.options",
- ]
- }
+ pw_proto_library("pet_daycare_protos") {
+ sources = [
+ "pet_daycare_protos/client.proto",
+ ]
+ inputs = [
+ "pet_daycare_protos/client.options",
+ ]
+ }
Valid options are:
@@ -751,177 +820,197 @@ that can hold the set of values encoded by it, following these rules.
* Scalar fields are represented by their appropriate C++ type.
- .. code::
+ .. code-block:: protobuf
- message Customer {
- int32 age = 1;
- uint32 birth_year = 2;
- sint64 rating = 3;
- bool is_active = 4;
- }
+ message Customer {
+ int32 age = 1;
+ uint32 birth_year = 2;
+ sint64 rating = 3;
+ bool is_active = 4;
+ }
- .. code:: c++
+ .. code-block:: c++
- struct Customer::Message {
- int32_t age;
- uint32_t birth_year;
- int64_t rating;
- bool is_active;
- };
+ struct Customer::Message {
+ int32_t age;
+ uint32_t birth_year;
+ int64_t rating;
+ bool is_active;
+ };
* Enumerations are represented by a code generated namespaced proto enum.
- .. code::
+ .. code-block:: protobuf
- message Award {
- enum Service {
- BRONZE = 1;
- SILVER = 2;
- GOLD = 3;
- }
- Service service = 1;
- }
+ message Award {
+ enum Service {
+ BRONZE = 1;
+ SILVER = 2;
+ GOLD = 3;
+ }
+ Service service = 1;
+ }
- .. code:: c++
+ .. code-block:: c++
- enum class Award::Service : uint32_t {
- BRONZE = 1,
- SILVER = 2,
- GOLD = 3,
+ enum class Award::Service : uint32_t {
+ BRONZE = 1,
+ SILVER = 2,
+ GOLD = 3,
- kBronze = BRONZE,
- kSilver = SILVER,
- kGold = GOLD,
- };
+ kBronze = BRONZE,
+ kSilver = SILVER,
+ kGold = GOLD,
+ };
- struct Award::Message {
- Award::Service service;
- };
+ struct Award::Message {
+ Award::Service service;
+ };
Aliases to the enum values are also included in the "constant" style to match
your preferred coding style. These aliases have any common prefix to the
enumeration values removed, such that:
- .. code::
+ .. code-block:: protobuf
- enum Activity {
- ACTIVITY_CYCLING = 1;
- ACTIVITY_RUNNING = 2;
- ACTIVITY_SWIMMING = 3;
- }
+ enum Activity {
+ ACTIVITY_CYCLING = 1;
+ ACTIVITY_RUNNING = 2;
+ ACTIVITY_SWIMMING = 3;
+ }
- .. code:: c++
+ .. code-block:: c++
- enum class Activity : uint32_t {
- ACTIVITY_CYCLING = 1,
- ACTIVITY_RUNNING = 2,
- ACTIVITY_SWIMMING = 3,
+ enum class Activity : uint32_t {
+ ACTIVITY_CYCLING = 1,
+ ACTIVITY_RUNNING = 2,
+ ACTIVITY_SWIMMING = 3,
- kCycling = ACTIVITY_CYCLING,
- kRunning = ACTIVITY_RUNNING,
- kSwimming = ACTIVITY_SWIMMING,
- };
+ kCycling = ACTIVITY_CYCLING,
+ kRunning = ACTIVITY_RUNNING,
+ kSwimming = ACTIVITY_SWIMMING,
+ };
* Nested messages are represented by their own ``struct Message`` provided that
a reference cycle does not exist.
- .. code::
+ .. code-block:: protobuf
- message Sale {
- Customer customer = 1;
- Product product = 2;
- }
+ message Sale {
+ Customer customer = 1;
+ Product product = 2;
+ }
- .. code:: c++
+ .. code-block:: c++
- struct Sale::Message {
- Customer::Message customer;
- Product::Message product;
- };
+ struct Sale::Message {
+ Customer::Message customer;
+ Product::Message product;
+ };
* Optional scalar fields are represented by the appropriate C++ type wrapped in
``std::optional``. Optional fields are not encoded when the value is not
present.
- .. code::
+ .. code-block:: protobuf
- message Loyalty {
- optional int32 points = 1;
- }
+ message Loyalty {
+ optional int32 points = 1;
+ }
- .. code:: c++
+ .. code-block:: c++
- struct Loyalty::Message {
- std::optional<int32_t> points;
- };
+ struct Loyalty::Message {
+ std::optional<int32_t> points;
+ };
* Repeated scalar fields are represented by ``pw::Vector`` when the
``max_count`` option is set for that field, or by ``std::array`` when both
``max_count`` and ``fixed_count:true`` are set.
- .. code::
+ The max count is exposed as an UpperCamelCase constant ``k{FieldName}MaxSize``.
+
+ .. code-block:: protobuf
message Register {
repeated int32 cash_in = 1;
repeated int32 cash_out = 2;
}
- .. code::
+ .. code-block:: text
- Register.cash_in max_count:32 fixed_count:true
- Register.cash_out max_count:64
+ Register.cash_in max_count:32 fixed_count:true
+ Register.cash_out max_count:64
- .. code:: c++
+ .. code-block:: c++
- struct Register::Message {
- std::array<int32_t, 32> cash_in;
- pw::Vector<int32_t, 64> cash_out;
- };
+ namespace Register {
+ static constexpr size_t kCashInMaxSize = 32;
+ static constexpr size_t kCashOutMaxSize = 64;
+ }
+
+ struct Register::Message {
+ std::array<int32_t, kCashInMaxSize> cash_in;
+ pw::Vector<int32_t, kCashOutMaxSize> cash_out;
+ };
* `bytes` fields are represented by ``pw::Vector`` when the ``max_size`` option
is set for that field, or by ``std::array`` when both ``max_size`` and
``fixed_size:true`` are set.
- .. code::
+ The max size is exposed as an UpperCamelCase constant ``k{FieldName}MaxSize``.
- message Product {
- bytes sku = 1;
- bytes serial_number = 2;
- }
+ .. code-block:: protobuf
+
+ message Product {
+ bytes sku = 1;
+ bytes serial_number = 2;
+ }
+
+ .. code-block:: text
- .. code::
+ Product.sku max_size:8 fixed_size:true
+ Product.serial_number max_size:64
- Product.sku max_size:8 fixed_size:true
- Product.serial_number max_size:64
+ .. code-block:: c++
- .. code:: c++
+ namespace Product {
+ static constexpr size_t kSkuMaxSize = 8;
+ static constexpr size_t kSerialNumberMaxSize = 64;
+ }
- struct Product::Message {
- std::array<std::byte, 8> sku;
- pw::Vector<std::byte, 64> serial_number;
- };
+ struct Product::Message {
+ std::array<std::byte, kSkuMaxSize> sku;
+ pw::Vector<std::byte, kSerialNumberMaxSize> serial_number;
+ };
* `string` fields are represented by a :cpp:type:`pw::InlineString` when the
``max_size`` option is set for that field. The string can hold up to
``max_size`` characters, and is always null terminated. The null terminator is
not counted in ``max_size``.
- .. code::
+ The max size is exposed as an UpperCamelCase constant ``k{FieldName}MaxSize``.
- message Employee {
- string name = 1;
- }
+ .. code-block:: protobuf
+
+ message Employee {
+ string name = 1;
+ }
+
+ .. code-block:: text
- .. code::
+ Employee.name max_size:128
- Employee.name max_size:128
+ .. code-block:: c++
- .. code:: c++
+ namespace Employee {
+ static constexpr size_t kNameMaxSize = 128;
+ }
- struct Employee::Message {
- pw::InlineString<128> name;
- };
+ struct Employee::Message {
+ pw::InlineString<kNameMaxSize> name;
+ };
* Nested messages with a dependency cycle, repeated scalar fields without a
``max_count`` option set, `bytes` and `strings` fields without a ``max_size``
@@ -931,29 +1020,32 @@ that can hold the set of values encoded by it, following these rules.
You set the callback to a custom function for encoding or decoding
before passing the structure to ``Write()`` or ``Read()`` appropriately.
- .. code::
+ .. code-block:: protobuf
- message Store {
- Store nearest_store = 1;
- repeated int32 employee_numbers = 2;
- string driections = 3;
- repeated string address = 4;
- repeated Employee employees = 5;
- }
+ message Store {
+ Store nearest_store = 1;
+ repeated int32 employee_numbers = 2;
+ string driections = 3;
+ repeated string address = 4;
+ repeated Employee employees = 5;
+ }
+
+ .. code-block::
- .. code::
+ // No options set.
- // No options set.
+ .. code-block:: c++
- .. code:: c++
+ struct Store::Message {
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> nearest_store;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> employee_numbers;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> directions;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> address;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> employees;
+ };
- struct Store::Message {
- pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> nearest_store;
- pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> employee_numbers;
- pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> directions;
- pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> address;
- pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> employees;
- };
+ A Callback object can be converted to a ``bool`` indicating whether a callback
+ is set.
Message structures can be copied, but doing so will clear any assigned
callbacks. To preserve functions applied to callbacks, ensure that the message
@@ -971,21 +1063,21 @@ failures. This can be seen below in ``Channel.operator``, which is mapped to
``Channel::Message::operator_`` to avoid conflicting with the ``operator``
keyword.
-.. code::
+.. code-block:: protobuf
- message Channel {
- int32 bitrate = 1;
- float signal_to_noise_ratio = 2;
- Company operator = 3;
- }
+ message Channel {
+ int32 bitrate = 1;
+ float signal_to_noise_ratio = 2;
+ Company operator = 3;
+ }
-.. code:: c++
+.. code-block:: c++
- struct Channel::Message {
- int32_t bitrate;
- float signal_to_noise_ratio;
- Company::Message operator_;
- };
+ struct Channel::Message {
+ int32_t bitrate;
+ float signal_to_noise_ratio;
+ Company::Message operator_;
+ };
Similarly, as shown in the example below, some POSIX-signal names conflict with
macros defined by the standard-library header ``<csignal>`` and therefore
@@ -996,136 +1088,135 @@ won't cause any problems unless the user defines custom macros for them. Any
naming conflicts caused by user-defined macros are the user's responsibility
(https://google.github.io/styleguide/cppguide.html#Preprocessor_Macros).
-.. code::
-
- enum PosixSignal {
- NONE = 0;
- SIGHUP = 1;
- SIGINT = 2;
- SIGQUIT = 3;
- SIGILL = 4;
- SIGTRAP = 5;
- SIGABRT = 6;
- SIGFPE = 8;
- SIGKILL = 9;
- SIGSEGV = 11;
- SIGPIPE = 13;
- SIGALRM = 14;
- SIGTERM = 15;
- }
-
-.. code:: c++
-
- enum class PosixSignal : uint32_t {
- NONE = 0,
- SIGHUP = 1,
- SIGINT_ = 2,
- SIGQUIT = 3,
- SIGILL_ = 4,
- SIGTRAP = 5,
- SIGABRT_ = 6,
- SIGFPE_ = 8,
- SIGKILL = 9,
- SIGSEGV_ = 11,
- SIGPIPE = 13,
- SIGALRM = 14,
- SIGTERM_ = 15,
-
- kNone = NONE,
- kSighup = SIGHUP,
- kSigint = SIGINT_,
- kSigquit = SIGQUIT,
- kSigill = SIGILL_,
- kSigtrap = SIGTRAP,
- kSigabrt = SIGABRT_,
- kSigfpe = SIGFPE_,
- kSigkill = SIGKILL,
- kSigsegv = SIGSEGV_,
- kSigpipe = SIGPIPE,
- kSigalrm = SIGALRM,
- kSigterm = SIGTERM_,
- };
+.. code-block:: protobuf
+
+ enum PosixSignal {
+ NONE = 0;
+ SIGHUP = 1;
+ SIGINT = 2;
+ SIGQUIT = 3;
+ SIGILL = 4;
+ SIGTRAP = 5;
+ SIGABRT = 6;
+ SIGFPE = 8;
+ SIGKILL = 9;
+ SIGSEGV = 11;
+ SIGPIPE = 13;
+ SIGALRM = 14;
+ SIGTERM = 15;
+ }
+
+.. code-block:: c++
+
+ enum class PosixSignal : uint32_t {
+ NONE = 0,
+ SIGHUP = 1,
+ SIGINT_ = 2,
+ SIGQUIT = 3,
+ SIGILL_ = 4,
+ SIGTRAP = 5,
+ SIGABRT_ = 6,
+ SIGFPE_ = 8,
+ SIGKILL = 9,
+ SIGSEGV_ = 11,
+ SIGPIPE = 13,
+ SIGALRM = 14,
+ SIGTERM_ = 15,
+
+ kNone = NONE,
+ kSighup = SIGHUP,
+ kSigint = SIGINT_,
+ kSigquit = SIGQUIT,
+ kSigill = SIGILL_,
+ kSigtrap = SIGTRAP,
+ kSigabrt = SIGABRT_,
+ kSigfpe = SIGFPE_,
+ kSigkill = SIGKILL,
+ kSigsegv = SIGSEGV_,
+ kSigpipe = SIGPIPE,
+ kSigalrm = SIGALRM,
+ kSigterm = SIGTERM_,
+ };
Much like reserved words and macros, the names ``Message`` and ``Fields`` are
suffixed with underscores in generated C++ code. This is to prevent name
conflicts with the codegen internals if they're used in a nested context as in
the example below.
-.. code::
-
- message Function {
- message Message {
- string content = 1;
- }
-
- enum Fields {
- NONE = 0;
- COMPLEX_NUMBERS = 1;
- INTEGERS_MOD_5 = 2;
- MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3;
- OTHER = 4;
- }
-
- Message description = 1;
- Fields domain = 2;
- Fields codomain = 3;
- }
+.. code-block:: protobuf
-.. code::
+ message Function {
+ message Message {
+ string content = 1;
+ }
- Function.Message.content max_size:128
+ enum Fields {
+ NONE = 0;
+ COMPLEX_NUMBERS = 1;
+ INTEGERS_MOD_5 = 2;
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3;
+ OTHER = 4;
+ }
-.. code:: c++
+ Message description = 1;
+ Fields domain = 2;
+ Fields codomain = 3;
+ }
- struct Function::Message_::Message {
- pw::InlineString<128> content;
- };
-
- enum class Function::Message_::Fields : uint32_t {
- CONTENT = 1,
- };
-
- enum class Function::Fields_ uint32_t {
- NONE = 0,
- COMPLEX_NUMBERS = 1,
- INTEGERS_MOD_5 = 2,
- MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3,
- OTHER = 4,
+.. code-block::
- kNone = NONE,
- kComplexNumbers = COMPLEX_NUMBERS,
- kIntegersMod5 = INTEGERS_MOD_5,
- kMeromorphicFunctionsOnComplexPlane =
- MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE,
- kOther = OTHER,
- };
+ Function.Message.content max_size:128
- struct Function::Message {
- Function::Message_::Message description;
- Function::Fields_ domain;
- Function::Fields_ codomain;
- };
+.. code-block:: c++
- enum class Function::Fields : uint32_t {
- DESCRIPTION = 1,
- DOMAIN = 2,
- CODOMAIN = 3,
- };
+ struct Function::Message_::Message {
+ pw::InlineString<128> content;
+ };
+
+ enum class Function::Message_::Fields : uint32_t {
+ CONTENT = 1,
+ };
+
+ enum class Function::Fields_ uint32_t {
+ NONE = 0,
+ COMPLEX_NUMBERS = 1,
+ INTEGERS_MOD_5 = 2,
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3,
+ OTHER = 4,
+
+ kNone = NONE,
+ kComplexNumbers = COMPLEX_NUMBERS,
+ kIntegersMod5 = INTEGERS_MOD_5,
+ kMeromorphicFunctionsOnComplexPlane =
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE,
+ kOther = OTHER,
+ };
+
+ struct Function::Message {
+ Function::Message_::Message description;
+ Function::Fields_ domain;
+ Function::Fields_ codomain;
+ };
+
+ enum class Function::Fields : uint32_t {
+ DESCRIPTION = 1,
+ DOMAIN = 2,
+ CODOMAIN = 3,
+ };
.. warning::
- Note that the C++ spec also reserves two categories of identifiers for the
- compiler to use in ways that may conflict with generated code:
-
- * Any identifier that contains two consecutive underscores anywhere in it.
+ Note that the C++ spec also reserves two categories of identifiers for the
+ compiler to use in ways that may conflict with generated code:
- * Any identifier that starts with an underscore followed by a capital letter.
+ * Any identifier that contains two consecutive underscores anywhere in it.
+ * Any identifier that starts with an underscore followed by a capital letter.
- Appending underscores to symbols in these categories wouldn't change the fact
- that they match patterns reserved for the compiler, so the codegen does not
- currently attempt to fix them. Such names will therefore result in
- non-portable code that may or may not work depending on the compiler. These
- naming patterns are of course strongly discouraged in any protobufs that will
- be used with ``pw_protobuf`` codegen.
+ Appending underscores to symbols in these categories wouldn't change the fact
+ that they match patterns reserved for the compiler, so the codegen does not
+ currently attempt to fix them. Such names will therefore result in
+ non-portable code that may or may not work depending on the compiler. These
+ naming patterns are of course strongly discouraged in any protobufs that will
+ be used with ``pw_protobuf`` codegen.
Overhead
========
@@ -1143,27 +1234,27 @@ Encoding
The simplest way to use ``MemoryEncoder`` to encode a proto is from its code
generated ``Message`` structure into an in-memory buffer.
-.. code:: c++
+.. code-block:: c++
- #include "my_protos/my_proto.pwpb.h"
- #include "pw_bytes/span.h"
- #include "pw_protobuf/encoder.h"
- #include "pw_status/status_with_size.h"
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_bytes/span.h"
+ #include "pw_protobuf/encoder.h"
+ #include "pw_status/status_with_size.h"
- // Writes a proto response to the provided buffer, returning the encode
- // status and number of bytes written.
- pw::StatusWithSize WriteProtoResponse(pw::ByteSpan response) {
- MyProto::Message message{}
- message.magic_number = 0x1a1a2b2b;
- message.favorite_food = "cookies";
- message.calories = 600;
+ // Writes a proto response to the provided buffer, returning the encode
+ // status and number of bytes written.
+ pw::StatusWithSize WriteProtoResponse(pw::ByteSpan response) {
+ MyProto::Message message{}
+ message.magic_number = 0x1a1a2b2b;
+ message.favorite_food = "cookies";
+ message.calories = 600;
- // All proto writes are directly written to the `response` buffer.
- MyProto::MemoryEncoder encoder(response);
- encoder.Write(message);
+ // All proto writes are directly written to the `response` buffer.
+ MyProto::MemoryEncoder encoder(response);
+ encoder.Write(message);
- return pw::StatusWithSize(encoder.status(), encoder.size());
- }
+ return pw::StatusWithSize(encoder.status(), encoder.size());
+ }
All fields of a message are written, including those initialized to their
default values.
@@ -1173,52 +1264,52 @@ encoded, fields can be written a field at a time through the code generated
or lower-level APIs. This can be more convenient if finer grained control or
other custom handling is required.
-.. code:: c++
-
- #include "my_protos/my_proto.pwpb.h"
- #include "pw_bytes/span.h"
- #include "pw_protobuf/encoder.h"
- #include "pw_status/status_with_size.h"
-
- // Writes a proto response to the provided buffer, returning the encode
- // status and number of bytes written.
- pw::StatusWithSize WriteProtoResponse(pw::ByteSpan response) {
- // All proto writes are directly written to the `response` buffer.
- MyProto::MemoryEncoder encoder(response);
- encoder.WriteMagicNumber(0x1a1a2b2b);
- encoder.WriteFavoriteFood("cookies");
- // Only conditionally write calories.
- if (on_diet) {
- encoder.WriteCalories(600);
- }
- return pw::StatusWithSize(encoder.status(), encoder.size());
- }
+.. code-block:: c++
+
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_bytes/span.h"
+ #include "pw_protobuf/encoder.h"
+ #include "pw_status/status_with_size.h"
+
+ // Writes a proto response to the provided buffer, returning the encode
+ // status and number of bytes written.
+ pw::StatusWithSize WriteProtoResponse(pw::ByteSpan response) {
+ // All proto writes are directly written to the `response` buffer.
+ MyProto::MemoryEncoder encoder(response);
+ encoder.WriteMagicNumber(0x1a1a2b2b);
+ encoder.WriteFavoriteFood("cookies");
+ // Only conditionally write calories.
+ if (on_diet) {
+ encoder.WriteCalories(600);
+ }
+ return pw::StatusWithSize(encoder.status(), encoder.size());
+ }
StreamEncoder
=============
``StreamEncoder`` is constructed with the destination stream, and a scratch
buffer used to handle nested submessages.
-.. code:: c++
+.. code-block:: c++
- #include "my_protos/my_proto.pwpb.h"
- #include "pw_bytes/span.h"
- #include "pw_protobuf/encoder.h"
- #include "pw_stream/sys_io_stream.h"
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_bytes/span.h"
+ #include "pw_protobuf/encoder.h"
+ #include "pw_stream/sys_io_stream.h"
- pw::stream::SysIoWriter sys_io_writer;
- MyProto::StreamEncoder encoder(sys_io_writer, pw::ByteSpan());
+ pw::stream::SysIoWriter sys_io_writer;
+ MyProto::StreamEncoder encoder(sys_io_writer, pw::ByteSpan());
- // Once this line returns, the field has been written to the Writer.
- encoder.WriteTimestamp(system::GetUnixEpoch());
+ // Once this line returns, the field has been written to the Writer.
+ encoder.WriteTimestamp(system::GetUnixEpoch());
- // There's no intermediate buffering when writing a string directly to a
- // StreamEncoder.
- encoder.WriteWelcomeMessage("Welcome to Pigweed!");
+ // There's no intermediate buffering when writing a string directly to a
+ // StreamEncoder.
+ encoder.WriteWelcomeMessage("Welcome to Pigweed!");
- if (!encoder.status().ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
- }
+ if (!encoder.status().ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
Callbacks
=========
@@ -1234,25 +1325,25 @@ Callback implementations may use any level of API. For example a callback for a
nested submessage (with a dependency cycle, or repeated) can be implemented by
calling ``Write()`` on a nested encoder.
-.. code:: c++
+.. code-block:: c++
- Store::Message store{};
- store.employees.SetEncoder([](Store::StreamEncoder& encoder) {
- Employee::Message employee{};
- // Populate `employee`.
- return encoder.GetEmployeesEncoder().Write(employee);
- ));
+ Store::Message store{};
+ store.employees.SetEncoder([](Store::StreamEncoder& encoder) {
+ Employee::Message employee{};
+ // Populate `employee`.
+ return encoder.GetEmployeesEncoder().Write(employee);
+ ));
Nested submessages
==================
Code generated ``GetFieldEncoder`` methods are provided that return a correctly
typed ``StreamEncoder`` or ``MemoryEncoder`` for the message.
-.. code::
+.. code-block:: protobuf
- message Owner {
- Animal pet = 1;
- }
+ message Owner {
+ Animal pet = 1;
+ }
Note that the accessor method is named for the field, while the returned encoder
is named for the message type.
@@ -1269,9 +1360,9 @@ writing the tag number for the nested encoder, if no data was written to
that nested decoder.)
.. warning::
- When a nested submessage is created, any use of the parent encoder that
- created the nested encoder will trigger a crash. To resume using the parent
- encoder, destroy the submessage encoder first.
+ When a nested submessage is created, any use of the parent encoder that
+ created the nested encoder will trigger a crash. To resume using the parent
+ encoder, destroy the submessage encoder first.
Buffering
---------
@@ -1298,40 +1389,40 @@ When calculating yourself, the ``MaxScratchBufferSize()`` helper function can
also be useful in estimating how much space to allocate to account for nested
submessage encoding overhead.
-.. code:: c++
+.. code-block:: c++
- #include "my_protos/pets.pwpb.h"
- #include "pw_bytes/span.h"
- #include "pw_protobuf/encoder.h"
- #include "pw_stream/sys_io_stream.h"
+ #include "my_protos/pets.pwpb.h"
+ #include "pw_bytes/span.h"
+ #include "pw_protobuf/encoder.h"
+ #include "pw_stream/sys_io_stream.h"
- pw::stream::SysIoWriter sys_io_writer;
- // The scratch buffer should be at least as big as the largest nested
- // submessage. It's a good idea to be a little generous.
- std::byte submessage_scratch_buffer[Owner::kScratchBufferSizeBytes];
+ pw::stream::SysIoWriter sys_io_writer;
+ // The scratch buffer should be at least as big as the largest nested
+ // submessage. It's a good idea to be a little generous.
+ std::byte submessage_scratch_buffer[Owner::kScratchBufferSizeBytes];
- // Provide the scratch buffer to the proto encoder. The buffer's lifetime must
- // match the lifetime of the encoder.
- Owner::StreamEncoder owner_encoder(sys_io_writer, submessage_scratch_buffer);
+ // Provide the scratch buffer to the proto encoder. The buffer's lifetime must
+ // match the lifetime of the encoder.
+ Owner::StreamEncoder owner_encoder(sys_io_writer, submessage_scratch_buffer);
- {
- // Note that the parent encoder, owner_encoder, cannot be used until the
- // nested encoder, pet_encoder, has been destroyed.
- Animal::StreamEncoder pet_encoder = owner_encoder.GetPetEncoder();
+ {
+ // Note that the parent encoder, owner_encoder, cannot be used until the
+ // nested encoder, pet_encoder, has been destroyed.
+ Animal::StreamEncoder pet_encoder = owner_encoder.GetPetEncoder();
- // There's intermediate buffering when writing to a nested encoder.
- pet_encoder.WriteName("Spot");
- pet_encoder.WriteType(Pet::Type::DOG);
+ // There's intermediate buffering when writing to a nested encoder.
+ pet_encoder.WriteName("Spot");
+ pet_encoder.WriteType(Pet::Type::DOG);
- // When this scope ends, the nested encoder is serialized to the Writer.
- // In addition, the parent encoder, owner_encoder, can be used again.
- }
+ // When this scope ends, the nested encoder is serialized to the Writer.
+ // In addition, the parent encoder, owner_encoder, can be used again.
+ }
- // If an encode error occurs when encoding the nested messages, it will be
- // reflected at the root encoder.
- if (!owner_encoder.status().ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", owner_encoder.status().str());
- }
+ // If an encode error occurs when encoding the nested messages, it will be
+ // reflected at the root encoder.
+ if (!owner_encoder.status().ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", owner_encoder.status().str());
+ }
MemoryEncoder objects use the final destination buffer rather than relying on a
scratch buffer. The ``kMaxEncodedSizeBytes`` constant takes into account the
@@ -1339,10 +1430,10 @@ overhead required for nesting submessages. If you calculate the buffer size
yourself, your destination buffer might need additional space.
.. warning::
- If the scratch buffer size is not sufficient, the encoding will fail with
- ``Status::ResourceExhausted()``. Always check the results of ``Write`` calls
- or the encoder status to ensure success, as otherwise the encoded data will
- be invalid.
+ If the scratch buffer size is not sufficient, the encoding will fail with
+ ``Status::ResourceExhausted()``. Always check the results of ``Write`` calls
+ or the encoder status to ensure success, as otherwise the encoded data will
+ be invalid.
Scalar Fields
=============
@@ -1371,10 +1462,10 @@ generation includes a ``Fields`` enum to provide the field number values.
The following two method calls are equivalent, where the first is using the
code generated API, and the second implemented by hand.
-.. code:: c++
+.. code-block:: c++
- my_proto_encoder.WriteAge(42);
- my_proto_encoder.WriteInt32(static_cast<uint32_t>(MyProto::Fields::kAge), 42);
+ my_proto_encoder.WriteAge(42);
+ my_proto_encoder.WriteInt32(static_cast<uint32_t>(MyProto::Fields::kAge), 42);
Repeated Fields
---------------
@@ -1383,13 +1474,13 @@ are provided.
.. cpp:function:: Status MyProto::StreamEncoder::WriteFoos(T)
- This writes a single unpacked value.
+ This writes a single unpacked value.
.. cpp:function:: Status MyProto::StreamEncoder::WriteFoos(pw::span<const T>)
.. cpp:function:: Status MyProto::StreamEncoder::WriteFoos(const pw::Vector<T>&)
- These write a packed field containing all of the values in the provided span
- or vector.
+ These write a packed field containing all of the values in the provided span
+ or vector.
These too can be freely intermixed with the lower-level API methods, both to
write a single value, or to write packed values from either a ``pw::span`` or
@@ -1421,14 +1512,14 @@ write a single value, or to write packed values from either a ``pw::span`` or
The following two method calls are equivalent, where the first is using the
code generated API, and the second implemented by hand.
-.. code:: c++
+.. code-block:: c++
- constexpr std::array<int32_t, 5> numbers = { 4, 8, 15, 16, 23, 42 };
+ constexpr std::array<int32_t, 5> numbers = { 4, 8, 15, 16, 23, 42 };
- my_proto_encoder.WriteNumbers(numbers);
- my_proto_encoder.WritePackedInt32(
- static_cast<uint32_t>(MyProto::Fields::kNumbers),
- numbers);
+ my_proto_encoder.WriteNumbers(numbers);
+ my_proto_encoder.WritePackedInt32(
+ static_cast<uint32_t>(MyProto::Fields::kNumbers),
+ numbers);
Enumerations
============
@@ -1444,12 +1535,12 @@ the field number and value to the ``uint32_t`` type.
The following two methods are equivalent, where the first is code generated,
and the second implemented by hand.
-.. code:: c++
+.. code-block:: c++
- my_proto_encoder.WriteAward(MyProto::Award::SILVER);
- my_proto_encoder.WriteUint32(
- static_cast<uint32_t>(MyProto::Fields::kAward),
- static_cast<uint32_t>(MyProto::Award::SILVER));
+ my_proto_encoder.WriteAward(MyProto::Award::SILVER);
+ my_proto_encoder.WriteUint32(
+ static_cast<uint32_t>(MyProto::Fields::kAward),
+ static_cast<uint32_t>(MyProto::Award::SILVER));
Repeated Fields
---------------
@@ -1458,13 +1549,13 @@ are provided.
.. cpp:function:: Status MyProto::StreamEncoder::WriteEnums(MyProto::Enums)
- This writes a single unpacked value.
+ This writes a single unpacked value.
.. cpp:function:: Status MyProto::StreamEncoder::WriteEnums(pw::span<const MyProto::Enums>)
.. cpp:function:: Status MyProto::StreamEncoder::WriteEnums(const pw::Vector<MyProto::Enums>&)
- These write a packed field containing all of the values in the provided span
- or vector.
+ These write a packed field containing all of the values in the provided span
+ or vector.
Their use is as scalar fields.
@@ -1485,9 +1576,9 @@ stream.
.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteStringFromStream(uint32_t field_number, stream::Reader& bytes_reader, size_t num_bytes, ByteSpan stream_pipe_buffer)
- The payload for the value is provided through the stream::Reader
- ``bytes_reader``. The method reads a chunk of the data from the reader using
- the ``stream_pipe_buffer`` and writes it to the encoder.
+ The payload for the value is provided through the stream::Reader
+ ``bytes_reader``. The method reads a chunk of the data from the reader using
+ the ``stream_pipe_buffer`` and writes it to the encoder.
Bytes
=====
@@ -1503,9 +1594,9 @@ And with the API method that can write bytes from another stream.
.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteBytesFromStream(uint32_t field_number, stream::Reader& bytes_reader, size_t num_bytes, ByteSpan stream_pipe_buffer)
- The payload for the value is provided through the stream::Reader
- ``bytes_reader``. The method reads a chunk of the data from the reader using
- the ``stream_pipe_buffer`` and writes it to the encoder.
+ The payload for the value is provided through the stream::Reader
+ ``bytes_reader``. The method reads a chunk of the data from the reader using
+ the ``stream_pipe_buffer`` and writes it to the encoder.
Error Handling
==============
@@ -1519,8 +1610,8 @@ Some additional helpers for encoding more complex but common protobuf
submessages (e.g. ``map<string, bytes>``) are provided in
``pw_protobuf/map_utils.h``.
-.. Note::
- The helper API are currently in-development and may not remain stable.
+.. note::
+ The helper API are currently in-development and may not remain stable.
--------
Decoding
@@ -1528,19 +1619,19 @@ Decoding
The simplest way to use ``StreamDecoder`` is to decode a proto from the stream
into its code generated ``Message`` structure.
-.. code:: c++
+.. code-block:: c++
- #include "my_protos/my_proto.pwpb.h"
- #include "pw_protobuf/stream_decoder.h"
- #include "pw_status/status.h"
- #include "pw_stream/stream.h"
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_protobuf/stream_decoder.h"
+ #include "pw_status/status.h"
+ #include "pw_stream/stream.h"
- pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
- MyProto::Message message{};
- MyProto::StreamDecoder decoder(reader);
- decoder.Read(message);
- return decoder.status();
- }
+ pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
+ MyProto::Message message{};
+ MyProto::StreamDecoder decoder(reader);
+ decoder.Read(message);
+ return decoder.status();
+ }
In the case of errors, the decoding will stop and return with the cursor on the
field that caused the error. It is valid in some cases to inspect the error and
@@ -1562,47 +1653,47 @@ as a typed ``Fields`` enumeration member, while the lower-level API provides a
.. cpp:function:: Result<MyProto::Fields> MyProto::StreamDecoder::Field()
.. cpp:function:: Result<uint32_t> pw::protobuf::StreamDecoder::FieldNumber()
-.. code:: c++
-
- #include "my_protos/my_proto.pwpb.h"
- #include "pw_protobuf/strema_decoder.h"
- #include "pw_status/status.h"
- #include "pw_status/try.h"
- #include "pw_stream/stream.h"
-
- pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
- MyProto::StreamDecoder decoder(reader);
- pw::Status status;
-
- uint32_t age;
- char name[16];
-
- // Iterate over the fields in the message. A return value of OK indicates
- // that a valid field has been found and can be read. When the decoder
- // reaches the end of the message, Next() will return OUT_OF_RANGE.
- // Other return values indicate an error trying to decode the message.
- while ((status = decoder.Next()).ok()) {
- // Field() returns a Result<Fields> as it may fail sometimes.
- // However, Field() is guaranteed to be valid after a call to Next()
- // that returns OK, so the value can be used directly here.
- switch (decoder.Field().value()) {
- case MyProto::Fields::kAge: {
- PW_TRY_ASSIGN(age, decoder.ReadAge());
- break;
- }
- case MyProto::Fields::kName:
- // The string field is copied into the provided buffer. If the buffer
- // is too small to fit the string, RESOURCE_EXHAUSTED is returned and
- // the decoder is not advanced, allowing the field to be re-read.
- PW_TRY(decoder.ReadName(name));
- break;
- }
- }
-
- // Do something with the fields...
+.. code-block:: c++
- return status.IsOutOfRange() ? OkStatus() : status;
- }
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_protobuf/strema_decoder.h"
+ #include "pw_status/status.h"
+ #include "pw_status/try.h"
+ #include "pw_stream/stream.h"
+
+ pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
+ MyProto::StreamDecoder decoder(reader);
+ pw::Status status;
+
+ uint32_t age;
+ char name[16];
+
+ // Iterate over the fields in the message. A return value of OK indicates
+ // that a valid field has been found and can be read. When the decoder
+ // reaches the end of the message, Next() will return OUT_OF_RANGE.
+ // Other return values indicate an error trying to decode the message.
+ while ((status = decoder.Next()).ok()) {
+ // Field() returns a Result<Fields> as it may fail sometimes.
+ // However, Field() is guaranteed to be valid after a call to Next()
+ // that returns OK, so the value can be used directly here.
+ switch (decoder.Field().value()) {
+ case MyProto::Fields::kAge: {
+ PW_TRY_ASSIGN(age, decoder.ReadAge());
+ break;
+ }
+ case MyProto::Fields::kName:
+ // The string field is copied into the provided buffer. If the buffer
+ // is too small to fit the string, RESOURCE_EXHAUSTED is returned and
+ // the decoder is not advanced, allowing the field to be re-read.
+ PW_TRY(decoder.ReadName(name));
+ break;
+ }
+ }
+
+ // Do something with the fields...
+
+ return status.IsOutOfRange() ? OkStatus() : status;
+ }
Callbacks
=========
@@ -1618,29 +1709,29 @@ Callback implementations may use any level of API. For example a callback for a
nested submessage (with a dependency cycle, or repeated) can be implemented by
calling ``Read()`` on a nested decoder.
-.. code:: c++
+.. code-block:: c++
- Store::Message store{};
- store.employees.SetDecoder([](Store::StreamDecoder& decoder) {
- PW_ASSERT(decoder.Field().value() == Store::Fields::kEmployees);
+ Store::Message store{};
+ store.employees.SetDecoder([](Store::StreamDecoder& decoder) {
+ PW_ASSERT(decoder.Field().value() == Store::Fields::kEmployees);
- Employee::Message employee{};
- // Set any callbacks on `employee`.
- PW_TRY(decoder.GetEmployeesDecoder().Read(employee));
- // Do things with `employee`.
- return OkStatus();
- ));
+ Employee::Message employee{};
+ // Set any callbacks on `employee`.
+ PW_TRY(decoder.GetEmployeesDecoder().Read(employee));
+ // Do things with `employee`.
+ return OkStatus();
+ ));
Nested submessages
==================
Code generated ``GetFieldDecoder`` methods are provided that return a correctly
typed ``StreamDecoder`` for the message.
-.. code::
+.. code-block:: protobuf
- message Owner {
- Animal pet = 1;
- }
+ message Owner {
+ Animal pet = 1;
+ }
As with encoding, note that the accessor method is named for the field, while
the returned decoder is named for the message type.
@@ -1653,28 +1744,28 @@ lower-level API methods. This can be moved to a typed decoder later.
.. cpp:function:: pw::protobuf::StreamDecoder pw::protobuf::StreamDecoder::GetNestedDecoder()
.. warning::
- When a nested submessage is being decoded, any use of the parent decoder that
- created the nested decoder will trigger a crash. To resume using the parent
- decoder, destroy the submessage decoder first.
+ When a nested submessage is being decoded, any use of the parent decoder that
+ created the nested decoder will trigger a crash. To resume using the parent
+ decoder, destroy the submessage decoder first.
-.. code:: c++
+.. code-block:: c++
- case Owner::Fields::kPet: {
- // Note that the parent decoder, owner_decoder, cannot be used until the
- // nested decoder, pet_decoder, has been destroyed.
- Animal::StreamDecoder pet_decoder = owner_decoder.GetPetDecoder();
+ case Owner::Fields::kPet: {
+ // Note that the parent decoder, owner_decoder, cannot be used until the
+ // nested decoder, pet_decoder, has been destroyed.
+ Animal::StreamDecoder pet_decoder = owner_decoder.GetPetDecoder();
- while ((status = pet_decoder.Next()).ok()) {
- switch (pet_decoder.Field().value()) {
- // Decode pet fields...
- }
- }
+ while ((status = pet_decoder.Next()).ok()) {
+ switch (pet_decoder.Field().value()) {
+ // Decode pet fields...
+ }
+ }
- // When this scope ends, the nested decoder is destroyed and the
- // parent decoder, owner_decoder, can be used again.
- break;
- }
+ // When this scope ends, the nested decoder is destroyed and the
+ // parent decoder, owner_decoder, can be used again.
+ break;
+ }
Scalar Fields
=============
@@ -1701,15 +1792,15 @@ per field type, requiring that the caller first check the field number.
The following two code snippets are equivalent, where the first uses the code
generated API, and the second implemented by hand.
-.. code:: c++
+.. code-block:: c++
- pw::Result<int32_t> age = my_proto_decoder.ReadAge();
+ pw::Result<int32_t> age = my_proto_decoder.ReadAge();
-.. code:: c++
+.. code-block:: c++
- PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
- static_cast<uint32_t>(MyProto::Fields::kAge));
- pw::Result<int32_t> my_proto_decoder.ReadInt32();
+ PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
+ static_cast<uint32_t>(MyProto::Fields::kAge));
+ pw::Result<int32_t> my_proto_decoder.ReadInt32();
Repeated Fields
---------------
@@ -1718,20 +1809,20 @@ are provided.
.. cpp:function:: Result<T> MyProto::StreamDecoder::ReadFoos()
- This reads a single unpacked value.
+ This reads a single unpacked value.
.. cpp:function:: StatusWithSize MyProto::StreamDecoder::ReadFoos(pw::span<T>)
- This reads a packed field containing all of the values into the provided span.
+ This reads a packed field containing all of the values into the provided span.
.. cpp:function:: Status MyProto::StreamDecoder::ReadFoos(pw::Vector<T>&)
- Protobuf encoders are permitted to choose either repeating single unpacked
- values, or a packed field, including splitting repeated fields up into
- multiple packed fields.
+ Protobuf encoders are permitted to choose either repeating single unpacked
+ values, or a packed field, including splitting repeated fields up into
+ multiple packed fields.
- This method supports either format, appending values to the provided
- ``pw::Vector``.
+ This method supports either format, appending values to the provided
+ ``pw::Vector``.
These too can be freely intermixed with the lower-level API methods, to read a
single value, a field of packed values into a ``pw::span``, or support both
@@ -1763,19 +1854,19 @@ formats appending to a ``pw::Vector`` source.
The following two code blocks are equivalent, where the first uses the code
generated API, and the second is implemented by hand.
-.. code:: c++
+.. code-block:: c++
- pw::Vector<int32_t, 8> numbers;
+ pw::Vector<int32_t, 8> numbers;
- my_proto_decoder.ReadNumbers(numbers);
+ my_proto_decoder.ReadNumbers(numbers);
-.. code:: c++
+.. code-block:: c++
- pw::Vector<int32_t, 8> numbers;
+ pw::Vector<int32_t, 8> numbers;
- PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
- static_cast<uint32_t>(MyProto::Fields::kNumbers));
- my_proto_decoder.ReadRepeatedInt32(numbers);
+ PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
+ static_cast<uint32_t>(MyProto::Fields::kNumbers));
+ my_proto_decoder.ReadRepeatedInt32(numbers);
Enumerations
============
@@ -1789,14 +1880,14 @@ that return the enumeration as the appropriate generated type.
.. cpp:function:: constexpr bool MyProto::IsValidEnum(MyProto::Enum value)
- Validates the value encoded in the wire format against the known set of
- enumerates.
+ Validates the value encoded in the wire format against the known set of
+ enumerates.
.. cpp:function:: constexpr const char* MyProto::EnumToString(MyProto::Enum value)
- Returns the string representation of the enum value. For example,
- ``FooToString(Foo::kBarBaz)`` returns ``"BAR_BAZ"``. Returns the empty string
- if the value is not a valid value.
+ Returns the string representation of the enum value. For example,
+ ``FooToString(Foo::kBarBaz)`` returns ``"BAR_BAZ"``. Returns the empty string
+ if the value is not a valid value.
To read enumerations with the lower-level API, you would need to cast the
retured value from the ``uint32_t``.
@@ -1806,19 +1897,19 @@ generated API, and the second implemented by hand.
.. code-block:: c++
- pw::Result<MyProto::Award> award = my_proto_decoder.ReadAward();
- if (!MyProto::IsValidAward(award)) {
- PW_LOG_DBG("Unknown award");
- }
+ pw::Result<MyProto::Award> award = my_proto_decoder.ReadAward();
+ if (!MyProto::IsValidAward(award)) {
+ PW_LOG_DBG("Unknown award");
+ }
.. code-block:: c++
- PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
- static_cast<uint32_t>(MyProto::Fields::kAward));
- pw::Result<uint32_t> award_value = my_proto_decoder.ReadUint32();
- if (award_value.ok()) {
- MyProto::Award award = static_cast<MyProto::Award>(award_value);
- }
+ PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
+ static_cast<uint32_t>(MyProto::Fields::kAward));
+ pw::Result<uint32_t> award_value = my_proto_decoder.ReadUint32();
+ if (award_value.ok()) {
+ MyProto::Award award = static_cast<MyProto::Award>(award_value);
+ }
Repeated Fields
---------------
@@ -1827,17 +1918,17 @@ are provided.
.. cpp:function:: Result<MyProto::Enums> MyProto::StreamDecoder::ReadEnums()
- This reads a single unpacked value.
+ This reads a single unpacked value.
.. cpp:function:: StatusWithSize MyProto::StreamDecoder::ReadEnums(pw::span<MyProto::Enums>)
- This reads a packed field containing all of the checked values into the
- provided span.
+ This reads a packed field containing all of the checked values into the
+ provided span.
.. cpp:function:: Status MyProto::StreamDecoder::ReadEnums(pw::Vector<MyProto::Enums>&)
- This method supports either repeated unpacked or packed formats, appending
- checked values to the provided ``pw::Vector``.
+ This method supports either repeated unpacked or packed formats, appending
+ checked values to the provided ``pw::Vector``.
Their use is as scalar fields.
@@ -1901,9 +1992,9 @@ Where the length of the protobuf message is known in advance, the decoder can
be prevented from reading from the stream beyond the known bounds by specifying
the known length to the decoder:
-.. code:: c++
+.. code-block:: c++
- pw::protobuf::StreamDecoder decoder(reader, message_length);
+ pw::protobuf::StreamDecoder decoder(reader, message_length);
When a decoder constructed in this way goes out of scope, it will consume any
remaining bytes in ``message_length`` allowing the next ``Read()`` on the stream
@@ -1926,39 +2017,39 @@ number.
When reading ``bytes`` and ``string`` fields, the decoder returns a view of that
field within the buffer; no data is copied out.
-.. code:: c++
-
- #include "pw_protobuf/decoder.h"
- #include "pw_status/try.h"
-
- pw::Status DecodeProtoFromBuffer(pw::span<const std::byte> buffer) {
- pw::protobuf::Decoder decoder(buffer);
- pw::Status status;
-
- uint32_t uint32_field;
- std::string_view string_field;
-
- // Iterate over the fields in the message. A return value of OK indicates
- // that a valid field has been found and can be read. When the decoder
- // reaches the end of the message, Next() will return OUT_OF_RANGE.
- // Other return values indicate an error trying to decode the message.
- while ((status = decoder.Next()).ok()) {
- switch (decoder.FieldNumber()) {
- case 1:
- PW_TRY(decoder.ReadUint32(&uint32_field));
- break;
- case 2:
- // The passed-in string_view will point to the contents of the string
- // field within the buffer.
- PW_TRY(decoder.ReadString(&string_field));
- break;
- }
- }
-
- // Do something with the fields...
+.. code-block:: c++
- return status.IsOutOfRange() ? OkStatus() : status;
- }
+ #include "pw_protobuf/decoder.h"
+ #include "pw_status/try.h"
+
+ pw::Status DecodeProtoFromBuffer(pw::span<const std::byte> buffer) {
+ pw::protobuf::Decoder decoder(buffer);
+ pw::Status status;
+
+ uint32_t uint32_field;
+ std::string_view string_field;
+
+ // Iterate over the fields in the message. A return value of OK indicates
+ // that a valid field has been found and can be read. When the decoder
+ // reaches the end of the message, Next() will return OUT_OF_RANGE.
+ // Other return values indicate an error trying to decode the message.
+ while ((status = decoder.Next()).ok()) {
+ switch (decoder.FieldNumber()) {
+ case 1:
+ PW_TRY(decoder.ReadUint32(&uint32_field));
+ break;
+ case 2:
+ // The passed-in string_view will point to the contents of the string
+ // field within the buffer.
+ PW_TRY(decoder.ReadString(&string_field));
+ break;
+ }
+ }
+
+ // Do something with the fields...
+
+ return status.IsOutOfRange() ? OkStatus() : status;
+ }
---------------
Message Decoder
@@ -1966,8 +2057,8 @@ Message Decoder
.. note::
- ``pw::protobuf::Message`` is unrelated to the codegen ``struct Message``
- used with ``StreamDecoder``.
+ ``pw::protobuf::Message`` is unrelated to the codegen ``struct Message``
+ used with ``StreamDecoder``.
The module implements a message parsing helper class ``Message``, in
``pw_protobuf/message.h``, to faciliate proto message parsing and field access.
@@ -1978,120 +2069,120 @@ uint32, bytes, string, map<>, repeated etc. The class works on top of
message access. The following gives examples for using the class to process
different fields in a proto message:
-.. code:: c++
-
- // Consider the proto messages defined as follows:
- //
- // message Nested {
- // string nested_str = 1;
- // bytes nested_bytes = 2;
- // }
- //
- // message {
- // uint32 integer = 1;
- // string str = 2;
- // bytes bytes = 3;
- // Nested nested = 4;
- // repeated string rep_str = 5;
- // repeated Nested rep_nested = 6;
- // map<string, bytes> str_to_bytes = 7;
- // map<string, Nested> str_to_nested = 8;
- // }
-
- // Given a seekable `reader` that reads the top-level proto message, and
- // a <proto_size> that gives the size of the proto message:
- Message message(reader, proto_size);
-
- // Parse a proto integer field
- Uint32 integer = messasge_parser.AsUint32(1);
- if (!integer.ok()) {
- // handle parsing error. i.e. return integer.status().
- }
- uint32_t integer_value = integer.value(); // obtained the value
-
- // Parse a string field
- String str = message.AsString(2);
- if (!str.ok()) {
- // handle parsing error. i.e. return str.status();
- }
-
- // check string equal
- Result<bool> str_check = str.Equal("foo");
-
- // Parse a bytes field
- Bytes bytes = message.AsBytes(3);
- if (!bytes.ok()) {
- // handle parsing error. i.e. return bytes.status();
- }
-
- // Get a reader to the bytes.
- stream::IntervalReader bytes_reader = bytes.GetBytesReader();
-
- // Parse nested message `Nested nested = 4;`
- Message nested = message.AsMessage(4).
- // Get the fields in the nested message.
- String nested_str = nested.AsString(1);
- Bytes nested_bytes = nested.AsBytes(2);
-
- // Parse repeated field `repeated string rep_str = 5;`
- RepeatedStrings rep_str = message.AsRepeatedString(5);
- // Iterate through the entries. If proto is malformed when
- // iterating, the next element (`str` in this case) will be invalid
- // and loop will end in the iteration after.
- for (String element : rep_str) {
- // Check status
- if (!str.ok()) {
- // In the case of error, loop will end in the next iteration if
- // continues. This is the chance for code to catch the error.
- }
- // Process str
- }
-
- // Parse repeated field `repeated Nested rep_nested = 6;`
- RepeatedStrings rep_str = message.AsRepeatedString(6);
- // Iterate through the entries. For iteration
- for (Message element : rep_rep_nestedstr) {
- // Check status
- if (!element.ok()) {
- // In the case of error, loop will end in the next iteration if
- // continues. This is the chance for code to catch the error.
- }
- // Process element
- }
-
- // Parse map field `map<string, bytes> str_to_bytes = 7;`
- StringToBytesMap str_to_bytes = message.AsStringToBytesMap(7);
- // Access the entry by a given key value
- Bytes bytes_for_key = str_to_bytes["key"];
- // Or iterate through map entries
- for (StringToBytesMapEntry entry : str_to_bytes) {
- // Check status
- if (!entry.ok()) {
- // In the case of error, loop will end in the next iteration if
- // continues. This is the chance for code to catch the error.
- }
- String key = entry.Key();
- Bytes value = entry.Value();
- // process entry
- }
-
- // Parse map field `map<string, Nested> str_to_nested = 8;`
- StringToMessageMap str_to_nested = message.AsStringToBytesMap(8);
- // Access the entry by a given key value
- Message nested_for_key = str_to_nested["key"];
- // Or iterate through map entries
- for (StringToMessageMapEntry entry : str_to_nested) {
- // Check status
- if (!entry.ok()) {
- // In the case of error, loop will end in the next iteration if
- // continues. This is the chance for code to catch the error.
- // However it is still recommended that the user breaks here.
- break;
- }
- String key = entry.Key();
- Message value = entry.Value();
- // process entry
- }
+.. code-block:: c++
+
+ // Consider the proto messages defined as follows:
+ //
+ // message Nested {
+ // string nested_str = 1;
+ // bytes nested_bytes = 2;
+ // }
+ //
+ // message {
+ // uint32 integer = 1;
+ // string str = 2;
+ // bytes bytes = 3;
+ // Nested nested = 4;
+ // repeated string rep_str = 5;
+ // repeated Nested rep_nested = 6;
+ // map<string, bytes> str_to_bytes = 7;
+ // map<string, Nested> str_to_nested = 8;
+ // }
+
+ // Given a seekable `reader` that reads the top-level proto message, and
+ // a <proto_size> that gives the size of the proto message:
+ Message message(reader, proto_size);
+
+ // Parse a proto integer field
+ Uint32 integer = messasge_parser.AsUint32(1);
+ if (!integer.ok()) {
+ // handle parsing error. i.e. return integer.status().
+ }
+ uint32_t integer_value = integer.value(); // obtained the value
+
+ // Parse a string field
+ String str = message.AsString(2);
+ if (!str.ok()) {
+ // handle parsing error. i.e. return str.status();
+ }
+
+ // check string equal
+ Result<bool> str_check = str.Equal("foo");
+
+ // Parse a bytes field
+ Bytes bytes = message.AsBytes(3);
+ if (!bytes.ok()) {
+ // handle parsing error. i.e. return bytes.status();
+ }
+
+ // Get a reader to the bytes.
+ stream::IntervalReader bytes_reader = bytes.GetBytesReader();
+
+ // Parse nested message `Nested nested = 4;`
+ Message nested = message.AsMessage(4).
+ // Get the fields in the nested message.
+ String nested_str = nested.AsString(1);
+ Bytes nested_bytes = nested.AsBytes(2);
+
+ // Parse repeated field `repeated string rep_str = 5;`
+ RepeatedStrings rep_str = message.AsRepeatedString(5);
+ // Iterate through the entries. If proto is malformed when
+ // iterating, the next element (`str` in this case) will be invalid
+ // and loop will end in the iteration after.
+ for (String element : rep_str) {
+ // Check status
+ if (!str.ok()) {
+ // In the case of error, loop will end in the next iteration if
+ // continues. This is the chance for code to catch the error.
+ }
+ // Process str
+ }
+
+ // Parse repeated field `repeated Nested rep_nested = 6;`
+ RepeatedStrings rep_str = message.AsRepeatedString(6);
+ // Iterate through the entries. For iteration
+ for (Message element : rep_rep_nestedstr) {
+ // Check status
+ if (!element.ok()) {
+ // In the case of error, loop will end in the next iteration if
+ // continues. This is the chance for code to catch the error.
+ }
+ // Process element
+ }
+
+ // Parse map field `map<string, bytes> str_to_bytes = 7;`
+ StringToBytesMap str_to_bytes = message.AsStringToBytesMap(7);
+ // Access the entry by a given key value
+ Bytes bytes_for_key = str_to_bytes["key"];
+ // Or iterate through map entries
+ for (StringToBytesMapEntry entry : str_to_bytes) {
+ // Check status
+ if (!entry.ok()) {
+ // In the case of error, loop will end in the next iteration if
+ // continues. This is the chance for code to catch the error.
+ }
+ String key = entry.Key();
+ Bytes value = entry.Value();
+ // process entry
+ }
+
+ // Parse map field `map<string, Nested> str_to_nested = 8;`
+ StringToMessageMap str_to_nested = message.AsStringToBytesMap(8);
+ // Access the entry by a given key value
+ Message nested_for_key = str_to_nested["key"];
+ // Or iterate through map entries
+ for (StringToMessageMapEntry entry : str_to_nested) {
+ // Check status
+ if (!entry.ok()) {
+ // In the case of error, loop will end in the next iteration if
+ // continues. This is the chance for code to catch the error.
+ // However it is still recommended that the user breaks here.
+ break;
+ }
+ String key = entry.Key();
+ Message value = entry.Value();
+ // process entry
+ }
The methods in ``Message`` for parsing a single field, i.e. everty `AsXXX()`
method except AsRepeatedXXX() and AsStringMapXXX(), internally performs a
@@ -2103,28 +2194,28 @@ is recommended to use the following for-range style to iterate and process
single fields directly.
-.. code:: c++
+.. code-block:: c++
- for (Message::Field field : message) {
- // Check status
- if (!field.ok()) {
- // In the case of error, loop will end in the next iteration if
- // continues. This is the chance for code to catch the error.
- }
- if (field.field_number() == 1) {
- Uint32 integer = field.As<Uint32>();
- ...
- } else if (field.field_number() == 2) {
- String str = field.As<String>();
- ...
- } else if (field.field_number() == 3) {
- Bytes bytes = field.As<Bytes>();
- ...
- } else if (field.field_number() == 4) {
- Message nested = field.As<Message>();
- ...
- }
- }
+ for (Message::Field field : message) {
+ // Check status
+ if (!field.ok()) {
+ // In the case of error, loop will end in the next iteration if
+ // continues. This is the chance for code to catch the error.
+ }
+ if (field.field_number() == 1) {
+ Uint32 integer = field.As<Uint32>();
+ ...
+ } else if (field.field_number() == 2) {
+ String str = field.As<String>();
+ ...
+ } else if (field.field_number() == 3) {
+ Bytes bytes = field.As<Bytes>();
+ ...
+ } else if (field.field_number() == 4) {
+ Message nested = field.As<Message>();
+ ...
+ }
+ }
.. Note::
@@ -2183,17 +2274,17 @@ status.proto
============
Contains the enum for pw::Status.
-.. Note::
- ``pw::protobuf::StatusCode`` values should not be used outside of a .proto
- file. Instead, the StatusCodes should be converted to the Status type in the
- language. In C++, this would be:
+.. note::
+ ``pw::protobuf::StatusCode`` values should not be used outside of a .proto
+ file. Instead, the StatusCodes should be converted to the Status type in the
+ language. In C++, this would be:
- .. code:: c++
+ .. code-block:: c++
- // Reading from a proto
- pw::Status status = static_cast<pw::Status::Code>(proto.status_field));
- // Writing to a proto
- proto.status_field = static_cast<pw::protobuf::StatusCode>(status.code()));
+ // Reading from a proto
+ pw::Status status = static_cast<pw::Status::Code>(proto.status_field));
+ // Writing to a proto
+ proto.status_field = static_cast<pw::protobuf::StatusCode>(status.code()));
----------------------------------------
Comparison with other protobuf libraries
diff --git a/pw_protobuf/encoder.cc b/pw_protobuf/encoder.cc
index 7c8491d55..65ffbf51b 100644
--- a/pw_protobuf/encoder.cc
+++ b/pw_protobuf/encoder.cc
@@ -125,7 +125,7 @@ Status StreamEncoder::WriteVarintField(uint32_t field_number, uint64_t value) {
field_number, WireType::kVarint, varint::EncodedSize(value)));
WriteVarint(FieldKey(field_number, WireType::kVarint))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return WriteVarint(value);
}
@@ -180,7 +180,7 @@ Status StreamEncoder::WriteFixed(uint32_t field_number, ConstByteSpan data) {
PW_TRY(UpdateStatusForWrite(field_number, type, data.size()));
WriteVarint(FieldKey(field_number, type))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
if (Status status = writer_.Write(data); !status.ok()) {
status_ = status;
}
@@ -200,9 +200,9 @@ Status StreamEncoder::WritePackedFixed(uint32_t field_number,
PW_TRY(UpdateStatusForWrite(
field_number, WireType::kDelimited, values.size_bytes()));
WriteVarint(FieldKey(field_number, WireType::kDelimited))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
WriteVarint(values.size_bytes())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
for (auto val_start = values.begin(); val_start != values.end();
val_start += elem_size) {
diff --git a/pw_protobuf/encoder_fuzzer.cc b/pw_protobuf/encoder_fuzzer.cc
index ebd0984be..02834fa48 100644
--- a/pw_protobuf/encoder_fuzzer.cc
+++ b/pw_protobuf/encoder_fuzzer.cc
@@ -26,7 +26,7 @@
namespace pw::protobuf::fuzz {
namespace {
-// TODO(b/235289495): Move this to pw_fuzzer/fuzzed_data_provider.h
+// TODO: b/235289495 - Move this to pw_fuzzer/fuzzed_data_provider.h
// Uses the given |provider| to pick and return a number between 0 and the
// maximum numbers of T that can be generated from the remaining input data.
diff --git a/pw_protobuf/encoder_perf_test.cc b/pw_protobuf/encoder_perf_test.cc
index 9b4671a17..672b429cf 100644
--- a/pw_protobuf/encoder_perf_test.cc
+++ b/pw_protobuf/encoder_perf_test.cc
@@ -22,7 +22,7 @@
namespace pw::protobuf {
namespace {
-void BasicIntegerPerformance(pw::perf_test::State& state, int64_t value) {
+void BasicIntegerPerformance(pw::perf_test::State& state, uint32_t value) {
std::byte encode_buffer[30];
while (state.KeepRunning()) {
diff --git a/pw_protobuf/encoder_test.cc b/pw_protobuf/encoder_test.cc
index 754e45b04..bef4d9d6f 100644
--- a/pw_protobuf/encoder_test.cc
+++ b/pw_protobuf/encoder_test.cc
@@ -103,7 +103,7 @@ TEST(StreamEncoder, EncodePrimitives) {
EXPECT_EQ(encoder.WriteSint32(kTestProtoZiggyField, -13), OkStatus());
EXPECT_EQ(encoder.WriteFixed64(kTestProtoCyclesField, 0xdeadbeef8badf00d),
OkStatus());
- EXPECT_EQ(encoder.WriteFloat(kTestProtoRatioField, 1.618034), OkStatus());
+ EXPECT_EQ(encoder.WriteFloat(kTestProtoRatioField, 1.618034f), OkStatus());
EXPECT_EQ(encoder.WriteString(kTestProtoErrorMessageField, "broken 💩"),
OkStatus());
@@ -136,7 +136,7 @@ TEST(StreamEncoder, EncodeInsufficientSpace) {
EXPECT_EQ(encoder.WriteFixed64(kTestProtoCyclesField, 0xdeadbeef8badf00d),
Status::ResourceExhausted());
// Any further write operations should fail.
- EXPECT_EQ(encoder.WriteFloat(kTestProtoRatioField, 1.618034),
+ EXPECT_EQ(encoder.WriteFloat(kTestProtoRatioField, 1.618034f),
Status::ResourceExhausted());
ASSERT_EQ(encoder.status(), Status::ResourceExhausted());
diff --git a/pw_protobuf/find.cc b/pw_protobuf/find.cc
index c5bd64f78..6d6ffb368 100644
--- a/pw_protobuf/find.cc
+++ b/pw_protobuf/find.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -14,28 +14,43 @@
#include "pw_protobuf/find.h"
-namespace pw::protobuf {
+#include "pw_protobuf/wire_format.h"
-Status FindDecodeHandler::ProcessField(CallbackDecoder& decoder,
- uint32_t field_number) {
- if (field_number != field_number_) {
- // Continue to the next field.
- return OkStatus();
+namespace pw::protobuf::internal {
+
+Status AdvanceToField(Decoder& decoder, uint32_t field_number) {
+ if (!ValidFieldNumber(field_number)) {
+ return Status::InvalidArgument();
}
- found_ = true;
- if (nested_handler_ == nullptr) {
- return Status::Cancelled();
+ Status status;
+
+ while ((status = decoder.Next()).ok()) {
+ if (decoder.FieldNumber() == field_number) {
+ return OkStatus();
+ }
}
- span<const std::byte> submessage;
- if (Status status = decoder.ReadBytes(&submessage); !status.ok()) {
- return status;
+ // As this is a backend for the Find() APIs, remap OUT_OF_RANGE to NOT_FOUND.
+ return status.IsOutOfRange() ? Status::NotFound() : status;
+}
+
+Status AdvanceToField(StreamDecoder& decoder, uint32_t field_number) {
+ if (!ValidFieldNumber(field_number)) {
+ return Status::InvalidArgument();
+ }
+
+ Status status;
+
+ while ((status = decoder.Next()).ok()) {
+ PW_TRY_ASSIGN(uint32_t field, decoder.FieldNumber());
+ if (field == field_number) {
+ return OkStatus();
+ }
}
- CallbackDecoder subdecoder;
- subdecoder.set_handler(nested_handler_);
- return subdecoder.Decode(submessage);
+ // As this is a backend for the Find() APIs, remap OUT_OF_RANGE to NOT_FOUND.
+ return status.IsOutOfRange() ? Status::NotFound() : status;
}
-} // namespace pw::protobuf
+} // namespace pw::protobuf::internal
diff --git a/pw_protobuf/find_test.cc b/pw_protobuf/find_test.cc
index bacac86ca..cb78708a1 100644
--- a/pw_protobuf/find_test.cc
+++ b/pw_protobuf/find_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -15,12 +15,14 @@
#include "pw_protobuf/find.h"
#include "gtest/gtest.h"
+#include "pw_stream/memory_stream.h"
+#include "pw_string/string.h"
namespace pw::protobuf {
namespace {
// clang-format off
-constexpr uint8_t encoded_proto[] = {
+constexpr uint8_t _encoded_proto[] = {
// type=int32, k=1, v=42
0x08, 0x2a,
// type=sint32, k=2, v=-13
@@ -39,53 +41,112 @@ constexpr uint8_t encoded_proto[] = {
// (nested) type=uint32, k=1, v=3
0x08, 0x03
};
+ConstByteSpan encoded_proto(as_bytes(span(_encoded_proto)));
+
+TEST(Find, PresentField) {
+ EXPECT_EQ(FindInt32(encoded_proto, 1).value(), 42);
+ EXPECT_EQ(FindSint32(encoded_proto, 2).value(), -13);
+ EXPECT_EQ(FindBool(encoded_proto, 3).value(), false);
+ EXPECT_EQ(FindDouble(encoded_proto, 4).value(), 3.14159);
+ EXPECT_EQ(FindFixed32(encoded_proto, 5).value(), 0xdeadbeef);
+
+ Result<std::string_view> result = FindString(encoded_proto, 6);
+ ASSERT_EQ(result.status(), OkStatus());
+ InlineString<32> str(*result);
+ EXPECT_STREQ(str.c_str(), "Hello world");
+}
-TEST(FindDecodeHandler, SingleLevel_FindsExistingField) {
- CallbackDecoder decoder;
- FindDecodeHandler finder(3);
+TEST(Find, MissingField) {
+ EXPECT_EQ(FindUint32(encoded_proto, 8).status(), Status::NotFound());
+ EXPECT_EQ(FindUint32(encoded_proto, 66).status(), Status::NotFound());
+ EXPECT_EQ(FindUint32(encoded_proto, 123456789).status(), Status::NotFound());
+}
- decoder.set_handler(&finder);
- ASSERT_EQ(Status::Cancelled(), decoder.Decode(as_bytes(span(encoded_proto))));
+TEST(Find, InvalidFieldNumber) {
+ EXPECT_EQ(FindUint32(encoded_proto, 0).status(), Status::InvalidArgument());
+ EXPECT_EQ(FindUint32(encoded_proto, uint32_t(-1)).status(), Status::InvalidArgument());
+}
- EXPECT_TRUE(finder.found());
- EXPECT_TRUE(decoder.cancelled());
+TEST(Find, WrongWireType) {
+ // Field 5 is a fixed32, but we request a uint32 (varint).
+ EXPECT_EQ(FindUint32(encoded_proto, 5).status(), Status::FailedPrecondition());
}
-TEST(FindDecodeHandler, SingleLevel_DoesntFindNonExistingField) {
- CallbackDecoder decoder;
- FindDecodeHandler finder(8);
+TEST(Find, MultiLevel) {
+ Result<ConstByteSpan> submessage = FindSubmessage(encoded_proto, 7);
+ ASSERT_EQ(submessage.status(), OkStatus());
+ EXPECT_EQ(submessage->size(), 2u);
+
+ // Read a field from the submessage.
+ EXPECT_EQ(FindUint32(*submessage, 1).value(), 3u);
+}
+
+TEST(FindStream, PresentField) {
+ stream::MemoryReader reader(encoded_proto);
+
+ EXPECT_EQ(FindInt32(reader, 1).value(), 42);
+ EXPECT_EQ(FindSint32(reader, 2).value(), -13);
+ EXPECT_EQ(FindBool(reader, 3).value(), false);
+ EXPECT_EQ(FindDouble(encoded_proto, 4).value(), 3.14159);
- decoder.set_handler(&finder);
- ASSERT_EQ(OkStatus(), decoder.Decode(as_bytes(span(encoded_proto))));
+ EXPECT_EQ(FindFixed32(reader, 5).value(), 0xdeadbeef);
- EXPECT_FALSE(finder.found());
- EXPECT_FALSE(decoder.cancelled());
+ char str[32];
+ StatusWithSize sws = FindString(reader, 6, str);
+ ASSERT_EQ(sws.status(), OkStatus());
+ ASSERT_EQ(sws.size(), 11u);
+ str[sws.size()] = '\0';
+ EXPECT_STREQ(str, "Hello world");
}
-TEST(FindDecodeHandler, MultiLevel_FindsExistingNestedField) {
- CallbackDecoder decoder;
- FindDecodeHandler nested_finder(1);
- FindDecodeHandler finder(7, &nested_finder);
+TEST(FindStream, MissingField) {
+ stream::MemoryReader reader(encoded_proto);
+ EXPECT_EQ(FindUint32(reader, 8).status(), Status::NotFound());
- decoder.set_handler(&finder);
- ASSERT_EQ(Status::Cancelled(), decoder.Decode(as_bytes(span(encoded_proto))));
+ reader = stream::MemoryReader(encoded_proto);
+ EXPECT_EQ(FindUint32(reader, 66).status(), Status::NotFound());
- EXPECT_TRUE(finder.found());
- EXPECT_TRUE(nested_finder.found());
- EXPECT_TRUE(decoder.cancelled());
+ reader = stream::MemoryReader(encoded_proto);
+ EXPECT_EQ(FindUint32(reader, 123456789).status(), Status::NotFound());
}
-TEST(FindDecodeHandler, MultiLevel_DoesntFindNonExistingNestedField) {
- CallbackDecoder decoder;
- FindDecodeHandler nested_finder(3);
- FindDecodeHandler finder(7, &nested_finder);
+TEST(FindStream, InvalidFieldNumber) {
+ stream::MemoryReader reader(encoded_proto);
+ EXPECT_EQ(FindUint32(reader, 0).status(), Status::InvalidArgument());
- decoder.set_handler(&finder);
- ASSERT_EQ(OkStatus(), decoder.Decode(as_bytes(span(encoded_proto))));
+ reader = stream::MemoryReader(encoded_proto);
+ EXPECT_EQ(FindUint32(reader, uint32_t(-1)).status(), Status::InvalidArgument());
+}
+
+TEST(FindStream, WrongWireType) {
+ stream::MemoryReader reader(encoded_proto);
+
+ // Field 5 is a fixed32, but we request a uint32 (varint).
+ EXPECT_EQ(FindUint32(reader, 5).status(), Status::FailedPrecondition());
+}
+
+enum class Fields : uint32_t {
+ kField1 = 1,
+ kField2 = 2,
+ kField3 = 3,
+ kField4 = 4,
+ kField5 = 5,
+ kField6 = 6,
+ kField7 = 7,
+};
- EXPECT_TRUE(finder.found());
- EXPECT_FALSE(nested_finder.found());
- EXPECT_FALSE(decoder.cancelled());
+TEST(FindEnum, PresentField) {
+ EXPECT_EQ(FindInt32(encoded_proto, Fields::kField1).value(), 42);
+ EXPECT_EQ(FindSint32(encoded_proto, Fields::kField2).value(), -13);
+ EXPECT_EQ(FindBool(encoded_proto, Fields::kField3).value(), false);
+ EXPECT_EQ(FindDouble(encoded_proto, Fields::kField4).value(), 3.14159);
+ EXPECT_EQ(FindFixed32(encoded_proto, Fields::kField5).value(), 0xdeadbeef);
+
+ stream::MemoryReader reader(encoded_proto);
+ InlineString<32> str;
+ StatusWithSize result = FindString(reader, Fields::kField6, str);
+ ASSERT_EQ(result.status(), OkStatus());
+ EXPECT_STREQ(str.c_str(), "Hello world");
}
} // namespace
diff --git a/pw_protobuf/public/pw_protobuf/encoder.h b/pw_protobuf/public/pw_protobuf/encoder.h
index 21b12cb1f..dccc6869a 100644
--- a/pw_protobuf/public/pw_protobuf/encoder.h
+++ b/pw_protobuf/public/pw_protobuf/encoder.h
@@ -221,7 +221,7 @@ class StreamEncoder {
// Writes a repeated uint64 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedUint64(uint64_t field_number, span<const uint64_t> values) {
+ Status WritePackedUint64(uint32_t field_number, span<const uint64_t> values) {
return WritePackedVarints(
field_number, values, internal::VarintType::kNormal);
}
@@ -240,7 +240,8 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WriteInt32(uint32_t field_number, int32_t value) {
- return WriteUint64(field_number, value);
+ // Signed numbers are sent as 2's complement so this cast is correct.
+ return WriteUint64(field_number, static_cast<uint64_t>(value));
}
// Writes a repeated int32 using packed encoding.
@@ -268,7 +269,8 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WriteInt64(uint32_t field_number, int64_t value) {
- return WriteUint64(field_number, value);
+ // Signed numbers are sent as 2's complement so this cast is correct.
+ return WriteUint64(field_number, static_cast<uint64_t>(value));
}
// Writes a repeated int64 using packed encoding.
@@ -729,16 +731,16 @@ class StreamEncoder {
}
WriteVarint(FieldKey(field_number, WireType::kDelimited))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
WriteVarint(payload_size)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
for (T value : values) {
if (encode_type == internal::VarintType::kZigZag) {
WriteZigzagVarint(static_cast<std::make_signed_t<T>>(value))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
} else {
WriteVarint(value)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
diff --git a/pw_protobuf/public/pw_protobuf/find.h b/pw_protobuf/public/pw_protobuf/find.h
index 64ad94dcc..ceef025ca 100644
--- a/pw_protobuf/public/pw_protobuf/find.h
+++ b/pw_protobuf/public/pw_protobuf/find.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -13,34 +13,736 @@
// the License.
#pragma once
+/// @file pw_protobuf/find.h
+///
+/// Sometimes, only a single field from a serialized message needs to be read.
+/// In these cases, setting up a decoder and iterating through the message is a
+/// lot of boilerplate. ``pw_protobuf`` provides convenient ``Find*()``
+/// functions which handle this for you.
+///
+/// @note Each call to ``Find*()`` linearly scans through the message. If you
+/// have to read multiple fields, it is more efficient to instantiate your own
+/// decoder as described above.
+///
+/// @code{.cpp}
+///
+/// pw::Status PrintCustomerAge(pw::ConstByteSpan serialized_customer) {
+/// pw::Result<uint32_t> age = pw::protobuf::FindUint32(
+/// serialized_customer, Customer::Fields::kAge);
+/// if (!age.ok()) {
+/// return age.status();
+/// }
+///
+/// PW_LOG_INFO("Customer's age is %u", *age);
+/// return pw::OkStatus();
+/// }
+///
+/// @endcode
+
+#include "pw_bytes/span.h"
#include "pw_protobuf/decoder.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_result/result.h"
+#include "pw_status/try.h"
+#include "pw_string/string.h"
namespace pw::protobuf {
+namespace internal {
+
+Status AdvanceToField(Decoder& decoder, uint32_t field_number);
+Status AdvanceToField(StreamDecoder& decoder, uint32_t field_number);
+
+template <typename T, auto ReadFn>
+Result<T> Find(ConstByteSpan message, uint32_t field_number) {
+ T output;
+ Decoder decoder(message);
+ PW_TRY(AdvanceToField(decoder, field_number));
+ PW_TRY((decoder.*ReadFn)(&output));
+ return output;
+}
+
+template <typename T, auto ReadFn>
+Result<T> Find(stream::Reader& reader, uint32_t field_number) {
+ StreamDecoder decoder(reader);
+ PW_TRY(AdvanceToField(decoder, field_number));
+ Result<T> result = (decoder.*ReadFn)();
+
+ // The StreamDecoder returns a NOT_FOUND if trying to read the wrong type for
+ // a field. Remap this to FAILED_PRECONDITION for consistency with the
+ // non-stream Find.
+ return result.status().IsNotFound() ? Result<T>(Status::FailedPrecondition())
+ : result;
+}
+
+} // namespace internal
+
+/// @brief Scans a serialized protobuf message for a `uint32` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint32_t> FindUint32(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<uint32_t, &Decoder::ReadUint32>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint32_t> FindUint32(ConstByteSpan message, T field) {
+ return FindUint32(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `uint32` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint32_t> FindUint32(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<uint32_t, &StreamDecoder::ReadUint32>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint32_t> FindUint32(stream::Reader& message_stream, T field) {
+ return FindUint32(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `int32` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int32_t> FindInt32(ConstByteSpan message, uint32_t field_number) {
+ return internal::Find<int32_t, &Decoder::ReadInt32>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int32_t> FindInt32(ConstByteSpan message, T field) {
+ return FindInt32(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `int32` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int32_t> FindInt32(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<int32_t, &StreamDecoder::ReadInt32>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int32_t> FindInt32(stream::Reader& message_stream, T field) {
+ return FindInt32(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sint32` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int32_t> FindSint32(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<int32_t, &Decoder::ReadSint32>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int32_t> FindSint32(ConstByteSpan message, T field) {
+ return FindSint32(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sint32` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int32_t> FindSint32(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<int32_t, &StreamDecoder::ReadSint32>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int32_t> FindSint32(stream::Reader& message_stream, T field) {
+ return FindSint32(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `uint64` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint64_t> FindUint64(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<uint64_t, &Decoder::ReadUint64>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint64_t> FindUint64(ConstByteSpan message, T field) {
+ return FindUint64(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `uint64` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint64_t> FindUint64(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<uint64_t, &StreamDecoder::ReadUint64>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint64_t> FindUint64(stream::Reader& message_stream, T field) {
+ return FindUint64(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `int64` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int64_t> FindInt64(ConstByteSpan message, uint32_t field_number) {
+ return internal::Find<int64_t, &Decoder::ReadInt64>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int64_t> FindInt64(ConstByteSpan message, T field) {
+ return FindInt64(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `int64` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int64_t> FindInt64(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<int64_t, &StreamDecoder::ReadInt64>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int64_t> FindInt64(stream::Reader& message_stream, T field) {
+ return FindInt64(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sint64` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int64_t> FindSint64(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<int64_t, &Decoder::ReadSint64>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int64_t> FindSint64(ConstByteSpan message, T field) {
+ return FindSint64(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sint64` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int64_t> FindSint64(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<int64_t, &StreamDecoder::ReadSint64>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int64_t> FindSint64(stream::Reader& message_stream, T field) {
+ return FindSint64(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `bool` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<bool> FindBool(ConstByteSpan message, uint32_t field_number) {
+ return internal::Find<bool, &Decoder::ReadBool>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<bool> FindBool(ConstByteSpan message, T field) {
+ return FindBool(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `bool` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<bool> FindBool(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<bool, &StreamDecoder::ReadBool>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<bool> FindBool(stream::Reader& message_stream, T field) {
+ return FindBool(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `fixed32` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint32_t> FindFixed32(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<uint32_t, &Decoder::ReadFixed32>(message, field_number);
+}
-// DecodeHandler that searches for a specific field in a proto message. If the
-// field is found, it cancels the decode operation. Supports searching for
-// nested fields by passing in another instance of a FindDecodeHandler for the
-// nested message.
-class FindDecodeHandler final : public DecodeHandler {
- public:
- constexpr FindDecodeHandler(uint32_t field_number)
- : FindDecodeHandler(field_number, nullptr) {}
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint32_t> FindFixed32(ConstByteSpan message, T field) {
+ return FindFixed32(message, static_cast<uint32_t>(field));
+}
- constexpr FindDecodeHandler(uint32_t field_number, FindDecodeHandler* nested)
- : field_number_(field_number), found_(false), nested_handler_(nested) {}
+/// @brief Scans a serialized protobuf message for a `fixed32` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint32_t> FindFixed32(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<uint32_t, &StreamDecoder::ReadFixed32>(message_stream,
+ field_number);
+}
- Status ProcessField(CallbackDecoder& decoder, uint32_t field_number) override;
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint32_t> FindFixed32(stream::Reader& message_stream, T field) {
+ return FindFixed32(message_stream, static_cast<uint32_t>(field));
+}
- bool found() const { return found_; }
+/// @brief Scans a serialized protobuf message for a `fixed64` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint64_t> FindFixed64(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<uint64_t, &Decoder::ReadFixed64>(message, field_number);
+}
- void set_nested_handler(FindDecodeHandler* nested) {
- nested_handler_ = nested;
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint64_t> FindFixed64(ConstByteSpan message, T field) {
+ return FindFixed64(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `fixed64` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<uint64_t> FindFixed64(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<uint64_t, &StreamDecoder::ReadFixed64>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<uint64_t> FindFixed64(stream::Reader& message_stream, T field) {
+ return FindFixed64(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sfixed32` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int32_t> FindSfixed32(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<int32_t, &Decoder::ReadSfixed32>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int32_t> FindSfixed32(ConstByteSpan message, T field) {
+ return FindSfixed32(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sfixed32` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int32_t> FindSfixed32(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<int32_t, &StreamDecoder::ReadSfixed32>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int32_t> FindSfixed32(stream::Reader& message_stream, T field) {
+ return FindSfixed32(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sfixed64` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int64_t> FindSfixed64(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<int64_t, &Decoder::ReadSfixed64>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int64_t> FindSfixed64(ConstByteSpan message, T field) {
+ return FindSfixed64(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for an `sfixed64` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<int64_t> FindSfixed64(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<int64_t, &StreamDecoder::ReadSfixed64>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<int64_t> FindSfixed64(stream::Reader& message_stream, T field) {
+ return FindSfixed64(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `float` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<float> FindFloat(ConstByteSpan message, uint32_t field_number) {
+ return internal::Find<float, &Decoder::ReadFloat>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<float> FindFloat(ConstByteSpan message, T field) {
+ return FindFloat(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `float` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<float> FindFloat(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<float, &StreamDecoder::ReadFloat>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<float> FindFloat(stream::Reader& message_stream, T field) {
+ return FindFloat(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `double` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<double> FindDouble(ConstByteSpan message, uint32_t field_number) {
+ return internal::Find<double, &Decoder::ReadDouble>(message, field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<double> FindDouble(ConstByteSpan message, T field) {
+ return FindDouble(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `double` field.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The field if found, or a status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<double> FindDouble(stream::Reader& message_stream,
+ uint32_t field_number) {
+ return internal::Find<double, &StreamDecoder::ReadDouble>(message_stream,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<double> FindDouble(stream::Reader& message_stream, T field) {
+ return FindDouble(message_stream, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `string` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns Subspan of the buffer containing the string field if found, or a
+/// status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+/// *NOTE*: The returned string is NOT null-terminated.
+inline Result<std::string_view> FindString(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<std::string_view, &Decoder::ReadString>(message,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<std::string_view> FindString(ConstByteSpan message, T field) {
+ return FindString(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `string`field, copying its
+/// data into the provided buffer.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+/// @param out The buffer to which to write the string.
+///
+/// @returns The size of the copied data if found, or a status indicating the
+/// error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+/// *NOTE*: The returned string is NOT null-terminated.
+inline StatusWithSize FindString(stream::Reader& message_stream,
+ uint32_t field_number,
+ span<char> out) {
+ StreamDecoder decoder(message_stream);
+ Status status = internal::AdvanceToField(decoder, field_number);
+ if (!status.ok()) {
+ return StatusWithSize(status, 0);
}
+ StatusWithSize sws = decoder.ReadString(out);
+
+ // The StreamDecoder returns a NOT_FOUND if trying to read the wrong type for
+ // a field. Remap this to FAILED_PRECONDITION for consistency with the
+ // non-stream Find.
+ return sws.status().IsNotFound() ? StatusWithSize::FailedPrecondition() : sws;
+}
+
+/// @brief Scans a serialized protobuf message for a `string`field, copying its
+/// data into the provided buffer.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+/// @param out String to which to write the found value.
+///
+/// @returns The size of the copied data if found, or a status indicating the
+/// error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline StatusWithSize FindString(stream::Reader& message_stream,
+ uint32_t field_number,
+ InlineString<>& out) {
+ StatusWithSize sws;
+
+ out.resize_and_overwrite([&](char* data, size_t size) {
+ sws = FindString(message_stream, field_number, span(data, size));
+ return sws.size();
+ });
+
+ return sws;
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline StatusWithSize FindString(stream::Reader& message_stream,
+ T field,
+ span<char> out) {
+ return FindString(message_stream, static_cast<uint32_t>(field), out);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline StatusWithSize FindString(stream::Reader& message_stream,
+ T field,
+ InlineString<>& out) {
+ return FindString(message_stream, static_cast<uint32_t>(field), out);
+}
+
+/// @brief Scans a serialized protobuf message for a `bytes` field.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns Subspan of the buffer containing the bytes field if found, or a
+/// status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<ConstByteSpan> FindBytes(ConstByteSpan message,
+ uint32_t field_number) {
+ return internal::Find<ConstByteSpan, &Decoder::ReadBytes>(message,
+ field_number);
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<ConstByteSpan> FindBytes(ConstByteSpan message, T field) {
+ return FindBytes(message, static_cast<uint32_t>(field));
+}
+
+/// @brief Scans a serialized protobuf message for a `bytes` field, copying its
+/// data into the provided buffer.
+///
+/// @param message_stream The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns The size of the copied data if found, or a status indicating the
+/// error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline StatusWithSize FindBytes(stream::Reader& message_stream,
+ uint32_t field_number,
+ ByteSpan out) {
+ StreamDecoder decoder(message_stream);
+ Status status = internal::AdvanceToField(decoder, field_number);
+ if (!status.ok()) {
+ return StatusWithSize(status, 0);
+ }
+ StatusWithSize sws = decoder.ReadBytes(out);
+
+ // The StreamDecoder returns a NOT_FOUND if trying to read the wrong type for
+ // a field. Remap this to FAILED_PRECONDITION for consistency with the
+ // non-stream Find.
+ return sws.status().IsNotFound() ? StatusWithSize::FailedPrecondition() : sws;
+}
+
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline StatusWithSize FindBytes(stream::Reader& message_stream,
+ T field,
+ ByteSpan out) {
+ return FindBytes(message_stream, static_cast<uint32_t>(field), out);
+}
+
+/// @brief Scans a serialized protobuf message for a submessage.
+///
+/// @param message The serialized message to search.
+/// @param field_number Protobuf field number of the field.
+///
+/// @returns Subspan of the buffer containing the submessage if found, or a
+/// status indicating the error otherwise:
+/// * `NOT_FOUND` - The field is not present.
+/// * `DATA_LOSS` - The serialized message not a valid protobuf.
+/// * `FAILED_PRECONDITION` - The field exists, but is not the correct type.
+inline Result<ConstByteSpan> FindSubmessage(ConstByteSpan message,
+ uint32_t field_number) {
+ // On the wire, a submessage is identical to bytes. This function exists only
+ // to clarify users' intent.
+ return FindBytes(message, field_number);
+}
- private:
- uint32_t field_number_;
- bool found_;
- FindDecodeHandler* nested_handler_;
-};
+template <typename T, typename = std::enable_if_t<std::is_enum_v<T>>>
+inline Result<ConstByteSpan> FindSubmessage(ConstByteSpan message, T field) {
+ return FindSubmessage(message, static_cast<uint32_t>(field));
+}
} // namespace pw::protobuf
diff --git a/pw_protobuf/public/pw_protobuf/internal/codegen.h b/pw_protobuf/public/pw_protobuf/internal/codegen.h
index 39e6ccc5c..d3e13c578 100644
--- a/pw_protobuf/public/pw_protobuf/internal/codegen.h
+++ b/pw_protobuf/public/pw_protobuf/internal/codegen.h
@@ -21,7 +21,8 @@
#include "pw_span/span.h"
#include "pw_status/status.h"
-// TODO(b/259746255): Remove this manual application of -Wconversion when all of
+// TODO: b/259746255 - Remove this manual application of -Wconversion when all
+// of
// Pigweed builds with it.
PW_MODIFY_DIAGNOSTICS_PUSH();
PW_MODIFY_DIAGNOSTIC(error, "-Wconversion");
@@ -165,7 +166,7 @@ class MessageField {
uint32_t field_number_;
uint32_t field_info_;
size_t field_offset_;
- // TODO(b/234875722): Could be replaced by a class MessageDescriptor*
+ // TODO: b/234875722 - Could be replaced by a class MessageDescriptor*
const span<const MessageField>* nested_message_fields_;
};
static_assert(sizeof(MessageField) <= sizeof(size_t) * 4,
@@ -205,6 +206,9 @@ union Callback {
return *this;
}
+ // Evaluate to true if the encoder or decoder callback is set.
+ explicit operator bool() const { return encode_ || decode_; }
+
private:
friend StreamDecoder;
friend StreamEncoder;
diff --git a/pw_protobuf/public/pw_protobuf/serialized_size.h b/pw_protobuf/public/pw_protobuf/serialized_size.h
index a2d368ca1..8a80a62dc 100644
--- a/pw_protobuf/public/pw_protobuf/serialized_size.h
+++ b/pw_protobuf/public/pw_protobuf/serialized_size.h
@@ -69,7 +69,8 @@ constexpr size_t TagSizeBytes(T field_number) {
// Calculates the size of a varint field (uint32/64, int32/64, sint32/64, enum).
template <typename T, typename U>
constexpr size_t SizeOfVarintField(T field_number, U value) {
- return TagSizeBytes(field_number) + varint::EncodedSize(value);
+ return TagSizeBytes(field_number) +
+ varint::EncodedSize(static_cast<uint64_t>(value));
}
// Calculates the size of a delimited field (string, bytes, nested message,
@@ -111,7 +112,9 @@ constexpr size_t SizeOfField(T field_number,
WireType type,
size_t data_size_bytes) {
if (type == WireType::kDelimited) {
- return SizeOfDelimitedField(field_number, data_size_bytes);
+ PW_DASSERT(data_size_bytes <= std::numeric_limits<uint32_t>::max());
+ return SizeOfDelimitedField(field_number,
+ static_cast<uint32_t>(data_size_bytes));
}
return TagSizeBytes(field_number) + data_size_bytes;
}
diff --git a/pw_protobuf/public/pw_protobuf/stream_decoder.h b/pw_protobuf/public/pw_protobuf/stream_decoder.h
index edbb8779b..0a7b4e506 100644
--- a/pw_protobuf/public/pw_protobuf/stream_decoder.h
+++ b/pw_protobuf/public/pw_protobuf/stream_decoder.h
@@ -702,7 +702,9 @@ class StreamDecoder {
}
container.resize(container.capacity());
const auto sws = ReadDelimitedField(as_writable_bytes(span(container)));
- container.resize(sws.size());
+ size_t size = sws.size();
+ PW_DASSERT(size <= std::numeric_limits<uint16_t>::max());
+ container.resize(static_cast<uint16_t>(size));
return sws.status();
}
diff --git a/pw_protobuf/py/BUILD.bazel b/pw_protobuf/py/BUILD.bazel
index c3668335e..8c30184bf 100644
--- a/pw_protobuf/py/BUILD.bazel
+++ b/pw_protobuf/py/BUILD.bazel
@@ -34,7 +34,12 @@ filegroup(
py_library(
name = "plugin_library",
srcs = [":pw_protobuf_common_sources"],
- imports = ["."],
+ imports = [
+ # For importing pw_protobuf
+ ".",
+ # For importing pw_protobuf_codegen_protos and pw_protobuf_protos
+ "..",
+ ],
deps = [
"//pw_protobuf:codegen_protos_pb2",
"//pw_protobuf:field_options_proto_pb2",
diff --git a/pw_protobuf/py/BUILD.gn b/pw_protobuf/py/BUILD.gn
index c9e40192d..526461a8e 100644
--- a/pw_protobuf/py/BUILD.gn
+++ b/pw_protobuf/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_protobuf/__init__.py",
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index dd862645b..261960787 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -21,7 +21,7 @@ from graphlib import CycleError, TopologicalSorter # type: ignore
from itertools import takewhile
import os
import sys
-from typing import Dict, Iterable, List, Optional, Tuple
+from typing import Dict, Iterable, List, Optional, Tuple, Type
from typing import cast
from google.protobuf import descriptor_pb2
@@ -346,6 +346,47 @@ class PackedReadVectorMethod(ReadMethod):
return [('::pw::Vector<{}>&'.format(self._result_type()), 'out')]
+class FindMethod(ReadMethod):
+ def name(self) -> str:
+ return 'Find{}'.format(self._field.name())
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [('::pw::ConstByteSpan', 'message')]
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ f'return {PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message, {self.field_cast()});'
+ ]
+ return lines
+
+ def _find_fn(self) -> str:
+ """The find function to call.
+
+ Defined in subclasses.
+
+ e.g. 'FindUint32', 'FindBytes', etc.
+ """
+ raise NotImplementedError()
+
+
+class FindStreamMethod(FindMethod):
+ def name(self) -> str:
+ return 'Find{}'.format(self._field.name())
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [('::pw::stream::Reader&', 'message_stream')]
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ f'return {PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message_stream, {self.field_cast()});'
+ ]
+ return lines
+
+
class MessageProperty(ProtoMember):
"""Base class for a C++ property for a field in a protobuf message."""
@@ -375,7 +416,7 @@ class MessageProperty(ProtoMember):
return False
@staticmethod
- def repeated_field_container(type_name: str, max_size: int) -> str:
+ def repeated_field_container(type_name: str, max_size: str) -> str:
"""Returns the container type used for repeated fields.
Defaults to ::pw::Vector<type, max_size>. String fields use
@@ -423,42 +464,36 @@ class MessageProperty(ProtoMember):
def sub_table(self) -> str: # pylint: disable=no-self-use
return '{}'
- def struct_member(self, from_root: bool = False) -> Tuple[str, str]:
- """Returns the structure member."""
+ def struct_member_type(self, from_root: bool = False) -> str:
+ """Returns the structure member type."""
if self.use_callback():
return (
- f'{PROTOBUF_NAMESPACE}::Callback'
- '<StreamEncoder, StreamDecoder>',
- self.name(),
+ f'{PROTOBUF_NAMESPACE}::Callback<StreamEncoder, StreamDecoder>'
)
# Optional fields are wrapped in std::optional
if self.is_optional():
- return (
- 'std::optional<{}>'.format(self.type_name(from_root)),
- self.name(),
- )
+ return 'std::optional<{}>'.format(self.type_name(from_root))
# Non-repeated fields have a member of just the type name.
max_size = self.max_size()
if max_size == 0:
- return (self.type_name(from_root), self.name())
+ return self.type_name(from_root)
# Fixed size fields use std::array.
if self.is_fixed_size():
- return (
- 'std::array<{}, {}>'.format(
- self.type_name(from_root), max_size
- ),
- self.name(),
+ return 'std::array<{}, {}>'.format(
+ self.type_name(from_root), self.max_size_constant_name()
)
# Otherwise prefer pw::Vector for repeated fields.
- return (
- self.repeated_field_container(self.type_name(from_root), max_size),
- self.name(),
+ return self.repeated_field_container(
+ self.type_name(from_root), self.max_size_constant_name()
)
+ def max_size_constant_name(self) -> str:
+ return f'k{self._field.name()}MaxSize'
+
def _varint_type_table_entry(self) -> str:
if self.wire_type() == 'kVarint':
return '{}::VarintType::{}'.format(
@@ -574,6 +609,16 @@ class SubMessageDecoderMethod(ReadMethod):
return False
+class SubMessageFindMethod(FindMethod):
+ """Method which reads a proto submessage."""
+
+ def _result_type(self) -> str:
+ return '::pw::ConstByteSpan'
+
+ def _find_fn(self) -> str:
+ return 'FindBytes'
+
+
class SubMessageProperty(MessageProperty):
"""Property which contains a sub-message."""
@@ -716,6 +761,26 @@ class PackedDoubleReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedDouble'
+class DoubleFindMethod(FindMethod):
+ """Method which reads a proto double value."""
+
+ def _result_type(self) -> str:
+ return 'double'
+
+ def _find_fn(self) -> str:
+ return 'FindDouble'
+
+
+class DoubleFindStreamMethod(FindStreamMethod):
+ """Method which reads a proto double value."""
+
+ def _result_type(self) -> str:
+ return 'double'
+
+ def _find_fn(self) -> str:
+ return 'FindDouble'
+
+
class DoubleProperty(MessageProperty):
"""Property which holds a proto double value."""
@@ -789,6 +854,26 @@ class PackedFloatReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedFloat'
+class FloatFindMethod(FindMethod):
+ """Method which reads a proto float value."""
+
+ def _result_type(self) -> str:
+ return 'float'
+
+ def _find_fn(self) -> str:
+ return 'FindFloat'
+
+
+class FloatFindStreamMethod(FindStreamMethod):
+ """Method which reads a proto float value."""
+
+ def _result_type(self) -> str:
+ return 'float'
+
+ def _find_fn(self) -> str:
+ return 'FindFloat'
+
+
class FloatProperty(MessageProperty):
"""Property which holds a proto float value."""
@@ -862,6 +947,26 @@ class PackedInt32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedInt32'
+class Int32FindMethod(FindMethod):
+ """Method which reads a proto int32 value."""
+
+ def _result_type(self) -> str:
+ return 'int32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindInt32'
+
+
+class Int32FindStreamMethod(FindStreamMethod):
+ """Method which reads a proto int32 value."""
+
+ def _result_type(self) -> str:
+ return 'int32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindInt32'
+
+
class Int32Property(MessageProperty):
"""Property which holds a proto int32 value."""
@@ -938,6 +1043,26 @@ class PackedSint32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSint32'
+class Sint32FindMethod(FindMethod):
+ """Method which reads a proto sint32 value."""
+
+ def _result_type(self) -> str:
+ return 'int32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSint32'
+
+
+class Sint32FindStreamMethod(FindStreamMethod):
+ """Method which reads a proto sint32 value."""
+
+ def _result_type(self) -> str:
+ return 'int32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSint32'
+
+
class Sint32Property(MessageProperty):
"""Property which holds a proto sint32 value."""
@@ -1014,6 +1139,26 @@ class PackedSfixed32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSfixed32'
+class Sfixed32FindMethod(FindMethod):
+ """Method which reads a proto sfixed32 value."""
+
+ def _result_type(self) -> str:
+ return 'int32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSfixed32'
+
+
+class Sfixed32FindStreamMethod(FindStreamMethod):
+ """Method which reads a proto sfixed32 value."""
+
+ def _result_type(self) -> str:
+ return 'int32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSfixed32'
+
+
class Sfixed32Property(MessageProperty):
"""Property which holds a proto sfixed32 value."""
@@ -1087,6 +1232,26 @@ class PackedInt64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedInt64'
+class Int64FindMethod(FindMethod):
+ """Method which reads a proto int64 value."""
+
+ def _result_type(self) -> str:
+ return 'int64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindInt64'
+
+
+class Int64FindStreamMethod(FindStreamMethod):
+ """Method which reads a proto int64 value."""
+
+ def _result_type(self) -> str:
+ return 'int64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindInt64'
+
+
class Int64Property(MessageProperty):
"""Property which holds a proto int64 value."""
@@ -1163,6 +1328,26 @@ class PackedSint64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSint64'
+class Sint64FindMethod(FindMethod):
+ """Method which reads a proto sint64 value."""
+
+ def _result_type(self) -> str:
+ return 'int64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSint64'
+
+
+class Sint64FindStreamMethod(FindStreamMethod):
+ """Method which reads a proto sint64 value."""
+
+ def _result_type(self) -> str:
+ return 'int64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSint64'
+
+
class Sint64Property(MessageProperty):
"""Property which holds a proto sint64 value."""
@@ -1239,6 +1424,26 @@ class PackedSfixed64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSfixed64'
+class Sfixed64FindMethod(FindMethod):
+ """Method which reads a proto sfixed64 value."""
+
+ def _result_type(self) -> str:
+ return 'int64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSfixed64'
+
+
+class Sfixed64FindStreamMethod(FindStreamMethod):
+ """Method which reads a proto sfixed64 value."""
+
+ def _result_type(self) -> str:
+ return 'int64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindSfixed64'
+
+
class Sfixed64Property(MessageProperty):
"""Property which holds a proto sfixed64 value."""
@@ -1312,6 +1517,26 @@ class PackedUint32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedUint32'
+class Uint32FindMethod(FindMethod):
+ """Method which finds a proto uint32 value."""
+
+ def _result_type(self) -> str:
+ return 'uint32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindUint32'
+
+
+class Uint32FindStreamMethod(FindStreamMethod):
+ """Method which finds a proto uint32 value."""
+
+ def _result_type(self) -> str:
+ return 'uint32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindUint32'
+
+
class Uint32Property(MessageProperty):
"""Property which holds a proto uint32 value."""
@@ -1388,6 +1613,26 @@ class PackedFixed32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedFixed32'
+class Fixed32FindMethod(FindMethod):
+ """Method which finds a proto fixed32 value."""
+
+ def _result_type(self) -> str:
+ return 'uint32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindFixed32'
+
+
+class Fixed32FindStreamMethod(FindStreamMethod):
+ """Method which finds a proto fixed32 value."""
+
+ def _result_type(self) -> str:
+ return 'uint32_t'
+
+ def _find_fn(self) -> str:
+ return 'FindFixed32'
+
+
class Fixed32Property(MessageProperty):
"""Property which holds a proto fixed32 value."""
@@ -1461,6 +1706,26 @@ class PackedUint64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedUint64'
+class Uint64FindMethod(FindMethod):
+ """Method which finds a proto uint64 value."""
+
+ def _result_type(self) -> str:
+ return 'uint64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindUint64'
+
+
+class Uint64FindStreamMethod(FindStreamMethod):
+ """Method which finds a proto uint64 value."""
+
+ def _result_type(self) -> str:
+ return 'uint64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindUint64'
+
+
class Uint64Property(MessageProperty):
"""Property which holds a proto uint64 value."""
@@ -1537,6 +1802,26 @@ class PackedFixed64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedFixed64'
+class Fixed64FindMethod(FindMethod):
+ """Method which finds a proto fixed64 value."""
+
+ def _result_type(self) -> str:
+ return 'uint64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindFixed64'
+
+
+class Fixed64FindStreamMethod(FindStreamMethod):
+ """Method which finds a proto fixed64 value."""
+
+ def _result_type(self) -> str:
+ return 'uint64_t'
+
+ def _find_fn(self) -> str:
+ return 'FindFixed64'
+
+
class Fixed64Property(MessageProperty):
"""Property which holds a proto fixed64 value."""
@@ -1600,6 +1885,26 @@ class PackedBoolReadMethod(PackedReadMethod):
return 'ReadPackedBool'
+class BoolFindMethod(FindMethod):
+ """Method which finds a proto bool value."""
+
+ def _result_type(self) -> str:
+ return 'bool'
+
+ def _find_fn(self) -> str:
+ return 'FindBool'
+
+
+class BoolFindStreamMethod(FindStreamMethod):
+ """Method which finds a proto bool value."""
+
+ def _result_type(self) -> str:
+ return 'bool'
+
+ def _find_fn(self) -> str:
+ return 'FindBool'
+
+
class BoolProperty(MessageProperty):
"""Property which holds a proto bool value."""
@@ -1639,6 +1944,40 @@ class BytesReadMethod(ReadMethod):
return 'ReadBytes'
+class BytesFindMethod(FindMethod):
+ """Method which reads a proto bytes value."""
+
+ def _result_type(self) -> str:
+ return '::pw::ConstByteSpan'
+
+ def _find_fn(self) -> str:
+ return 'FindBytes'
+
+
+class BytesFindStreamMethod(FindStreamMethod):
+ """Method which reads a proto bytes value."""
+
+ def return_type(self, from_root: bool = False) -> str:
+ return '::pw::StatusWithSize'
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [
+ ('::pw::stream::Reader&', 'message_stream'),
+ ('::pw::ByteSpan', 'out'),
+ ]
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ f'return {PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message_stream, {self.field_cast()}, out);'
+ ]
+ return lines
+
+ def _find_fn(self) -> str:
+ return 'FindBytes'
+
+
class BytesProperty(MessageProperty):
"""Property which holds a proto bytes value."""
@@ -1676,7 +2015,7 @@ class BytesProperty(MessageProperty):
def _size_length(self) -> Optional[str]:
if self.use_callback():
return None
- return f'{self.max_size()}'
+ return self.max_size_constant_name()
class StringLenWriteMethod(WriteMethod):
@@ -1712,6 +2051,64 @@ class StringReadMethod(ReadMethod):
return 'ReadString'
+class StringFindMethod(FindMethod):
+ """Method which reads a proto string value."""
+
+ def _result_type(self) -> str:
+ return 'std::string_view'
+
+ def _find_fn(self) -> str:
+ return 'FindString'
+
+
+class StringFindStreamMethod(FindStreamMethod):
+ """Method which reads a proto string value."""
+
+ def return_type(self, from_root: bool = False) -> str:
+ return '::pw::StatusWithSize'
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [
+ ('::pw::stream::Reader&', 'message_stream'),
+ ('::pw::span<char>', 'out'),
+ ]
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ f'return {PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message_stream, {self.field_cast()}, out);'
+ ]
+ return lines
+
+ def _find_fn(self) -> str:
+ return 'FindString'
+
+
+class StringFindStreamMethodInlineString(FindStreamMethod):
+ """Method which reads a proto string value to an InlineString."""
+
+ def return_type(self, from_root: bool = False) -> str:
+ return '::pw::StatusWithSize'
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [
+ ('::pw::stream::Reader&', 'message_stream'),
+ ('::pw::InlineString<>&', 'out'),
+ ]
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ f'return {PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message_stream, {self.field_cast()}, out);'
+ ]
+ return lines
+
+ def _find_fn(self) -> str:
+ return 'FindString'
+
+
class StringProperty(MessageProperty):
"""Property which holds a proto string value."""
@@ -1739,7 +2136,7 @@ class StringProperty(MessageProperty):
return True
@staticmethod
- def repeated_field_container(type_name: str, max_size: int) -> str:
+ def repeated_field_container(type_name: str, max_size: str) -> str:
return f'::pw::InlineBasicString<{type_name}, {max_size}>'
def _size_fn(self) -> str:
@@ -1751,7 +2148,7 @@ class StringProperty(MessageProperty):
def _size_length(self) -> Optional[str]:
if self.use_callback():
return None
- return f'{self.max_size()}'
+ return self.max_size_constant_name()
class EnumWriteMethod(WriteMethod):
@@ -1863,6 +2260,52 @@ class PackedEnumReadVectorMethod(PackedReadVectorMethod):
]
+class EnumFindMethod(FindMethod):
+ """Method which finds a proto enum value."""
+
+ def _result_type(self) -> str:
+ return self._relative_type_namespace()
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ '::pw::Result<uint32_t> result = '
+ f'{PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message, {self.field_cast()});',
+ 'if (!result.ok()) {',
+ ' return result.status();',
+ '}',
+ f'return static_cast<{self._result_type()}>(result.value());',
+ ]
+ return lines
+
+ def _find_fn(self) -> str:
+ return 'FindUint32'
+
+
+class EnumFindStreamMethod(FindStreamMethod):
+ """Method which finds a proto enum value."""
+
+ def _result_type(self) -> str:
+ return self._relative_type_namespace()
+
+ def body(self) -> List[str]:
+ lines: List[str] = []
+ lines += [
+ '::pw::Result<uint32_t> result = '
+ f'{PROTOBUF_NAMESPACE}::{self._find_fn()}'
+ f'(message_stream, {self.field_cast()});',
+ 'if (!result.ok()) {',
+ ' return result.status();',
+ '}',
+ f'return static_cast<{self._result_type()}>(result.value());',
+ ]
+ return lines
+
+ def _find_fn(self) -> str:
+ return 'FindUint32'
+
+
class EnumProperty(MessageProperty):
"""Property which holds a proto enum value."""
@@ -2040,27 +2483,121 @@ PROTO_FIELD_READ_METHODS: Dict[int, List] = {
],
}
-PROTO_FIELD_PROPERTIES: Dict[int, List] = {
- descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE: [DoubleProperty],
- descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT: [FloatProperty],
- descriptor_pb2.FieldDescriptorProto.TYPE_INT32: [Int32Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_SINT32: [Sint32Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32: [Sfixed32Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_INT64: [Int64Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_SINT64: [Sint64Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64: [Sfixed32Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_UINT32: [Uint32Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32: [Fixed32Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_UINT64: [Uint64Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64: [Fixed64Property],
- descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: [BoolProperty],
- descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: [BytesProperty],
- descriptor_pb2.FieldDescriptorProto.TYPE_STRING: [StringProperty],
- descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: [SubMessageProperty],
- descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [EnumProperty],
+PROTO_FIELD_FIND_METHODS: Dict[int, List] = {
+ descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE: [
+ DoubleFindMethod,
+ DoubleFindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT: [
+ FloatFindMethod,
+ FloatFindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT32: [
+ Int32FindMethod,
+ Int32FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT32: [
+ Sint32FindMethod,
+ Sint32FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32: [
+ Sfixed32FindMethod,
+ Sfixed32FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT64: [
+ Int64FindMethod,
+ Int64FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT64: [
+ Sint64FindMethod,
+ Sint64FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64: [
+ Sfixed64FindMethod,
+ Sfixed64FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT32: [
+ Uint32FindMethod,
+ Uint32FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32: [
+ Fixed32FindMethod,
+ Fixed32FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT64: [
+ Uint64FindMethod,
+ Uint64FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64: [
+ Fixed64FindMethod,
+ Fixed64FindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: [
+ BoolFindMethod,
+ BoolFindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: [
+ BytesFindMethod,
+ BytesFindStreamMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_STRING: [
+ StringFindMethod,
+ StringFindStreamMethod,
+ StringFindStreamMethodInlineString,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: [
+ SubMessageFindMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [
+ EnumFindMethod,
+ EnumFindStreamMethod,
+ ],
+}
+
+PROTO_FIELD_PROPERTIES: Dict[int, Type[MessageProperty]] = {
+ descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE: DoubleProperty,
+ descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT: FloatProperty,
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT32: Int32Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT32: Sint32Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32: Sfixed32Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT64: Int64Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT64: Sint64Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64: Sfixed32Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT32: Uint32Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32: Fixed32Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT64: Uint64Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64: Fixed64Property,
+ descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: BoolProperty,
+ descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: BytesProperty,
+ descriptor_pb2.FieldDescriptorProto.TYPE_STRING: StringProperty,
+ descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: SubMessageProperty,
+ descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: EnumProperty,
}
+def proto_message_field_props(
+ message: ProtoMessage,
+ root: ProtoNode,
+) -> Iterable[MessageProperty]:
+ """Yields a MessageProperty for each field in a ProtoMessage.
+
+ Only properties which should_appear() is True are returned.
+
+ Args:
+ message: The ProtoMessage whose fields are iterated.
+ root: The root ProtoNode of the tree.
+
+ Yields:
+ An appropriately-typed MessageProperty object for each field
+ in the message, to which the property refers.
+ """
+ for field in message.fields():
+ property_class = PROTO_FIELD_PROPERTIES[field.type()]
+ prop = property_class(field, message, root)
+ if prop.should_appear():
+ yield prop
+
+
def proto_field_methods(class_type: ClassType, field_type: int) -> List:
return (
PROTO_FIELD_WRITE_METHODS[field_type]
@@ -2316,31 +2853,41 @@ def generate_to_string_for_enum(
def forward_declare(
- node: ProtoMessage,
+ message: ProtoMessage,
root: ProtoNode,
output: OutputFile,
exclude_legacy_snake_case_field_name_enums: bool,
) -> None:
"""Generates code forward-declaring entities in a message's namespace."""
- namespace = node.cpp_namespace(root=root)
+ namespace = message.cpp_namespace(root=root)
output.write_line()
output.write_line(f'namespace {namespace} {{')
# Define an enum defining each of the message's fields and their numbers.
output.write_line('enum class Fields : uint32_t {')
with output.indent():
- for field in node.fields():
+ for field in message.fields():
output.write_line(f'{field.enum_name()} = {field.number()},')
# Migration support from SNAKE_CASE to kConstantCase.
if not exclude_legacy_snake_case_field_name_enums:
- for field in node.fields():
+ for field in message.fields():
output.write_line(
f'{field.legacy_enum_name()} = {field.number()},'
)
output.write_line('};')
+ # Define constants for fixed-size fields.
+ output.write_line()
+ for prop in proto_message_field_props(message, root):
+ max_size = prop.max_size()
+ if max_size:
+ output.write_line(
+ f'static constexpr size_t {prop.max_size_constant_name()} '
+ f'= {max_size};'
+ )
+
# Declare the message's message struct.
output.write_line()
output.write_line('struct Message;')
@@ -2355,14 +2902,14 @@ def forward_declare(
output.write_line('class StreamDecoder;')
# Declare the message's enums.
- for child in node.children():
+ for child in message.children():
if child.type() == ProtoNode.Type.ENUM:
output.write_line()
- generate_code_for_enum(cast(ProtoEnum, child), node, output)
+ generate_code_for_enum(cast(ProtoEnum, child), message, output)
output.write_line()
- generate_function_for_enum(cast(ProtoEnum, child), node, output)
+ generate_function_for_enum(cast(ProtoEnum, child), message, output)
output.write_line()
- generate_to_string_for_enum(cast(ProtoEnum, child), node, output)
+ generate_to_string_for_enum(cast(ProtoEnum, child), message, output)
output.write_line(f'}} // namespace {namespace}')
@@ -2378,17 +2925,13 @@ def generate_struct_for_message(
# Generate members for each of the message's fields.
with output.indent():
cmp: List[str] = []
- for field in message.fields():
- for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
- prop = property_class(field, message, root)
- if not prop.should_appear():
- continue
-
- (type_name, name) = prop.struct_member()
- output.write_line(f'{type_name} {name};')
+ for prop in proto_message_field_props(message, root):
+ type_name = prop.struct_member_type()
+ name = prop.name()
+ output.write_line(f'{type_name} {name};')
- if not prop.use_callback():
- cmp.append(f'{name} == other.{name}')
+ if not prop.use_callback():
+ cmp.append(f'{name} == other.{name}')
# Equality operator
output.write_line()
@@ -2417,12 +2960,7 @@ def generate_table_for_message(
namespace = message.cpp_namespace(root=root)
output.write_line(f'namespace {namespace} {{')
- properties = []
- for field in message.fields():
- for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
- prop = property_class(field, message, root)
- if prop.should_appear():
- properties.append(prop)
+ properties = list(proto_message_field_props(message, root))
output.write_line('PW_MODIFY_DIAGNOSTICS_PUSH();')
output.write_line('PW_MODIFY_DIAGNOSTIC(ignored, "-Winvalid-offsetof");')
@@ -2469,7 +3007,7 @@ def generate_table_for_message(
)
member_list = ', '.join(
- [f'message.{prop.struct_member()[1]}' for prop in properties]
+ [f'message.{prop.name()}' for prop in properties]
)
# Generate std::tuple for Message fields.
@@ -2505,15 +3043,10 @@ def generate_sizes_for_message(
property_sizes: List[str] = []
scratch_sizes: List[str] = []
- for field in message.fields():
- for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
- prop = property_class(field, message, root)
- if not prop.should_appear():
- continue
-
- property_sizes.append(prop.max_encoded_size())
- if prop.include_in_scratch_size():
- scratch_sizes.append(prop.max_encoded_size())
+ for prop in proto_message_field_props(message, root):
+ property_sizes.append(prop.max_encoded_size())
+ if prop.include_in_scratch_size():
+ scratch_sizes.append(prop.max_encoded_size())
output.write_line('inline constexpr size_t kMaxEncodedSizeBytes =')
with output.indent():
@@ -2540,19 +3073,53 @@ def generate_sizes_for_message(
output.write_line(f'}} // namespace {namespace}')
-def generate_is_trivially_comparable_specialization(
+def generate_find_functions_for_message(
message: ProtoMessage, root: ProtoNode, output: OutputFile
) -> None:
- is_trivially_comparable = True
+ """Creates C++ constants for the encoded sizes of a protobuf message."""
+ assert message.type() == ProtoNode.Type.MESSAGE
+
+ namespace = message.cpp_namespace(root=root)
+ output.write_line(f'namespace {namespace} {{')
+
for field in message.fields():
- for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
- prop = property_class(field, message, root)
- if not prop.should_appear():
- continue
+ if field.is_repeated():
+ # Find methods don't account for repeated field semantics, so
+ # ignore them to avoid confusion.
+ continue
+
+ try:
+ methods = PROTO_FIELD_FIND_METHODS[field.type()]
+ except KeyError:
+ continue
+
+ for cls in methods:
+ method = cls(field, message, root, '')
+ method_signature = (
+ f'inline {method.return_type()} '
+ f'{method.name()}({method.param_string()})'
+ )
+
+ output.write_line()
+ output.write_line(f'{method_signature} {{')
+
+ with output.indent():
+ for line in method.body():
+ output.write_line(line)
+
+ output.write_line('}')
+
+ output.write_line(f'}} // namespace {namespace}')
+
- if prop.use_callback():
- is_trivially_comparable = False
- break
+def generate_is_trivially_comparable_specialization(
+ message: ProtoMessage, root: ProtoNode, output: OutputFile
+) -> None:
+ is_trivially_comparable = True
+ for prop in proto_message_field_props(message, root):
+ if prop.use_callback():
+ is_trivially_comparable = False
+ break
qualified_message = f'{message.cpp_namespace()}::Message'
@@ -2626,6 +3193,7 @@ def generate_code_for_package(
output.write_line('#include "pw_containers/vector.h"')
output.write_line('#include "pw_preprocessor/compiler.h"')
output.write_line('#include "pw_protobuf/encoder.h"')
+ output.write_line('#include "pw_protobuf/find.h"')
output.write_line('#include "pw_protobuf/internal/codegen.h"')
output.write_line('#include "pw_protobuf/serialized_size.h"')
output.write_line('#include "pw_protobuf/stream_decoder.h"')
@@ -2675,6 +3243,8 @@ def generate_code_for_package(
output.write_line()
generate_sizes_for_message(message, package, output)
output.write_line()
+ generate_find_functions_for_message(message, package, output)
+ output.write_line()
generate_class_for_message(
message, package, output, ClassType.STREAMING_ENCODER
)
@@ -2720,7 +3290,7 @@ def generate_code_for_package(
output.write_line(f'using namespace ::{package.cpp_namespace()};')
output.write_line(f'}} // namespace {legacy_namespace}')
- # TODO(b/250945489) Remove this if possible
+ # TODO: b/250945489 - Remove this if possible
output.write_line()
output.write_line(
'// Codegen implementation detail; do not use this namespace!'
diff --git a/pw_protobuf/py/pw_protobuf/proto_tree.py b/pw_protobuf/py/pw_protobuf/proto_tree.py
index 15bfb732a..bc506eeba 100644
--- a/pw_protobuf/py/pw_protobuf/proto_tree.py
+++ b/pw_protobuf/py/pw_protobuf/proto_tree.py
@@ -46,7 +46,7 @@ T = TypeVar('T') # pylint: disable=invalid-name
# `::pw::pwpb_codegen_private::my::external::package:ProtoMsg::SubMsg` to refer
# to the pw_protobuf generated code, when package name info is not available.
#
-# TODO(b/258832150) Explore removing this if possible
+# TODO: b/258832150 - Explore removing this if possible
EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE = 'pw::pwpb_codegen_private'
@@ -153,14 +153,15 @@ class ProtoNode(abc.ABC):
# Can't figure out where the namespace cutoff is. Punt to using
# the external symbol workaround.
#
- # TODO(b/250945489) Investigate removing this limitation / hack
+ # TODO: b/250945489 - Investigate removing this limitation /
+ # hack
return itertools.chain(
[EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE],
self._attr_hierarchy(ProtoNode.cpp_name, root=None),
)
if root is None or root_pkg_or_ext is None: # extra check for mypy
- # TODO(b/250945489): maybe elide "::{codegen_subnamespace}"
+ # TODO: b/250945489 - maybe elide "::{codegen_subnamespace}"
# here, if this node doesn't have any package?
same_package = False
else:
diff --git a/pw_protobuf/py/setup.cfg b/pw_protobuf/py/setup.cfg
index 883edded4..1be00722c 100644
--- a/pw_protobuf/py/setup.cfg
+++ b/pw_protobuf/py/setup.cfg
@@ -22,8 +22,10 @@ description = Lightweight streaming protobuf implementation
packages = find:
zip_safe = False
install_requires =
- protobuf>=3.20.1
- googleapis-common-protos>=1.56.2
+ # Ensure protoc and language-specific protobuf libraries are kept in sync.
+ # This is also specified in //pw_protobuf_compiler/py/setup.cfg
+ protobuf~=4.24.4
+ googleapis-common-protos>=1.61.0
graphlib-backport;python_version<'3.9'
[options.entry_points]
diff --git a/pw_protobuf/py/setup.py b/pw_protobuf/py/setup.py
deleted file mode 100644
index 7d15878d6..000000000
--- a/pw_protobuf/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_protobuf"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_protobuf/stream_decoder.cc b/pw_protobuf/stream_decoder.cc
index c94b8e88e..9f5f58639 100644
--- a/pw_protobuf/stream_decoder.cc
+++ b/pw_protobuf/stream_decoder.cc
@@ -69,7 +69,7 @@ Status StreamDecoder::BytesReader::DoSeek(ptrdiff_t offset, Whence origin) {
}
if (static_cast<size_t>(absolute_position) < start_offset_ ||
- static_cast<size_t>(absolute_position) >= end_offset_) {
+ static_cast<size_t>(absolute_position) > end_offset_) {
return Status::OutOfRange();
}
@@ -83,6 +83,10 @@ StatusWithSize StreamDecoder::BytesReader::DoRead(ByteSpan destination) {
return StatusWithSize(status_, 0);
}
+ if (decoder_.position_ >= end_offset_ || decoder_.position_ < start_offset_) {
+ return StatusWithSize::OutOfRange();
+ }
+
// Bound the read buffer to the size of the bytes field.
size_t max_length = end_offset_ - decoder_.position_;
if (destination.size() > max_length) {
@@ -221,7 +225,8 @@ Status StreamDecoder::ReadFieldKey() {
return Status::DataLoss();
}
- current_field_ = FieldKey(varint);
+ PW_DCHECK(varint <= std::numeric_limits<uint32_t>::max());
+ current_field_ = FieldKey(static_cast<uint32_t>(varint));
if (current_field_.wire_type() == WireType::kDelimited) {
// Read the length varint of length-delimited fields immediately to simplify
@@ -521,13 +526,13 @@ Status StreamDecoder::Read(span<std::byte> message,
while (Next().ok()) {
// Find the field in the table,
- // TODO(b/234876102): Finding the field can be made more efficient.
+ // TODO: b/234876102 - Finding the field can be made more efficient.
const auto field =
std::find(table.begin(), table.end(), current_field_.field_number());
if (field == table.end()) {
// If the field is not found, skip to the next one.
- // TODO(b/234873295): Provide a way to allow the caller to inspect unknown
- // fields, and serialize them back out later.
+ // TODO: b/234873295 - Provide a way to allow the caller to inspect
+ // unknown fields, and serialize them back out later.
continue;
}
diff --git a/pw_protobuf_compiler/BUILD.bazel b/pw_protobuf_compiler/BUILD.bazel
index 25728e84c..00d74c394 100644
--- a/pw_protobuf_compiler/BUILD.bazel
+++ b/pw_protobuf_compiler/BUILD.bazel
@@ -11,8 +11,8 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-load("@rules_proto//proto:defs.bzl", "proto_library")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
load("//pw_build:pigweed.bzl", "pw_cc_test")
package(default_visibility = ["//visibility:public"])
diff --git a/pw_protobuf_compiler/deps.bzl b/pw_protobuf_compiler/deps.bzl
index 94a30ee2b..7c062789a 100644
--- a/pw_protobuf_compiler/deps.bzl
+++ b/pw_protobuf_compiler/deps.bzl
@@ -24,10 +24,3 @@ def pw_protobuf_dependencies():
strip_prefix = "protobuf-3.12.3",
url = "https://github.com/protocolbuffers/protobuf/archive/v3.12.3.tar.gz",
)
- if "rules_proto_grpc" not in native.existing_rules():
- http_archive(
- name = "rules_proto_grpc",
- sha256 = "5f0f2fc0199810c65a2de148a52ba0aff14d631d4e8202f41aff6a9d590a471b",
- strip_prefix = "rules_proto_grpc-1.0.2",
- urls = ["https://github.com/rules-proto-grpc/rules_proto_grpc/archive/1.0.2.tar.gz"],
- )
diff --git a/pw_protobuf_compiler/docs.rst b/pw_protobuf_compiler/docs.rst
index 7644625c9..827d87522 100644
--- a/pw_protobuf_compiler/docs.rst
+++ b/pw_protobuf_compiler/docs.rst
@@ -55,9 +55,9 @@ For example, given the following target:
.. code-block::
- pw_proto_library("test_protos") {
- sources = [ "my_test_protos/test.proto" ]
- }
+ pw_proto_library("test_protos") {
+ sources = [ "my_test_protos/test.proto" ]
+ }
``test_protos.pwpb`` compiles code for pw_protobuf, and ``test_protos.nanopb``
compiles using Nanopb (if it's installed).
@@ -72,15 +72,15 @@ is the same. For example, these two labels are equivalent:
.. code-block::
- //path/to/my_protos:my_protos.pwpb
- //path/to/my_protos:pwpb
+ //path/to/my_protos:my_protos.pwpb
+ //path/to/my_protos:pwpb
``pw_python_package`` subtargets are also available on the ``python`` subtarget:
.. code-block::
- //path/to/my_protos:my_protos.python.lint
- //path/to/my_protos:python.lint
+ //path/to/my_protos:my_protos.python.lint
+ //path/to/my_protos:python.lint
**Supported Codegen**
@@ -117,57 +117,57 @@ sub-targets generated by a ``pw_proto_library``.
may be necessary to explicitly disable an unused sub-target if it conflicts
with another target in the same package. (For example, ``nanopb`` codegen can
conflict with the default C++ codegen provided by ``protoc``.)
- TODO(b/235132083): Remove this argument once we've removed the file-name
+ TODO: b/235132083 - Remove this argument once we've removed the file-name
conflict between nanopb and protoc code generators.
**Example**
.. code-block::
- import("$dir_pw_protobuf_compiler/proto.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
- pw_proto_library("my_protos") {
- sources = [
- "my_protos/foo.proto",
- "my_protos/bar.proto",
- ]
- }
+ pw_proto_library("my_protos") {
+ sources = [
+ "my_protos/foo.proto",
+ "my_protos/bar.proto",
+ ]
+ }
- pw_proto_library("my_other_protos") {
- sources = [ "some/other/path/baz.proto" ] # imports foo.proto
+ pw_proto_library("my_other_protos") {
+ sources = [ "some/other/path/baz.proto" ] # imports foo.proto
- # This removes the "some/other/path" prefix from the proto files.
- strip_prefix = "some/other/path"
+ # This removes the "some/other/path" prefix from the proto files.
+ strip_prefix = "some/other/path"
- # This adds the "my_other_protos/" prefix to the proto files.
- prefix = "my_other_protos"
+ # This adds the "my_other_protos/" prefix to the proto files.
+ prefix = "my_other_protos"
- # Proto libraries depend on other proto libraries directly.
- deps = [ ":my_protos" ]
- }
+ # Proto libraries depend on other proto libraries directly.
+ deps = [ ":my_protos" ]
+ }
- source_set("my_cc_code") {
- sources = [
- "foo.cc",
- "bar.cc",
- "baz.cc",
- ]
+ source_set("my_cc_code") {
+ sources = [
+ "foo.cc",
+ "bar.cc",
+ "baz.cc",
+ ]
- # When depending on protos in a source_set, specify the generator suffix.
- deps = [ ":my_other_protos.pwpb" ]
- }
+ # When depending on protos in a source_set, specify the generator suffix.
+ deps = [ ":my_other_protos.pwpb" ]
+ }
From C++, ``baz.proto`` included as follows:
.. code-block:: cpp
- #include "my_other_protos/baz.pwpb.h"
+ #include "my_other_protos/baz.pwpb.h"
From Python, ``baz.proto`` is imported as follows:
.. code-block:: python
- from my_other_protos import baz_pb2
+ from my_other_protos import baz_pb2
Proto file structure
--------------------
@@ -180,25 +180,25 @@ in external libraries. For example, consider this proto library:
.. code-block::
- pw_proto_library("external_protos") {
- sources = [
- "//other/external/some_library/src/protos/alpha.proto",
- "//other/external/some_library/src/protos/beta.proto,
- "//other/external/some_library/src/protos/internal/gamma.proto",
- ]
- strip_prefix = "//other/external/some_library/src/protos"
- prefix = "some_library"
- }
+ pw_proto_library("external_protos") {
+ sources = [
+ "//other/external/some_library/src/protos/alpha.proto",
+ "//other/external/some_library/src/protos/beta.proto,
+ "//other/external/some_library/src/protos/internal/gamma.proto",
+ ]
+ strip_prefix = "//other/external/some_library/src/protos"
+ prefix = "some_library"
+ }
These protos will be compiled by protoc as if they were in this file structure:
.. code-block::
- some_library/
- ├── alpha.proto
- ├── beta.proto
- └── internal
- └── gamma.proto
+ some_library/
+ ├── alpha.proto
+ ├── beta.proto
+ └── internal
+ └── gamma.proto
.. _module-pw_protobuf_compiler-add-to-python-package:
@@ -215,30 +215,30 @@ package declared by ``my_package``.
.. code-block::
- pw_proto_library("my_protos") {
- sources = [ "hello.proto ]
- prefix = "foo"
- python_package = ":my_package"
- }
+ pw_proto_library("my_protos") {
+ sources = [ "hello.proto ]
+ prefix = "foo"
+ python_package = ":my_package"
+ }
- pw_python_pacakge("my_package") {
- generate_setup = {
- metadata = {
- name = "foo"
- version = "1.0"
- }
- }
+ pw_python_pacakge("my_package") {
+ generate_setup = {
+ metadata = {
+ name = "foo"
+ version = "1.0"
+ }
+ }
- sources = [ "foo/cool_module.py" ]
- proto_library = ":my_protos"
- }
+ sources = [ "foo/cool_module.py" ]
+ proto_library = ":my_protos"
+ }
The ``hello_pb2.py`` proto module can be used alongside other files in the
``foo`` package.
.. code-block:: python
- from foo import cool_module, hello_pb2
+ from foo import cool_module, hello_pb2
Working with externally defined protos
--------------------------------------
@@ -256,11 +256,11 @@ For example, the ``pw_proto_library`` target for Nanopb sets
.. code-block::
- pw_proto_library("proto") {
- strip_prefix = "$dir_pw_third_party_nanopb/generator/proto"
- sources = [ "$dir_pw_third_party_nanopb/generator/proto/nanopb.proto" ]
- python_module_as_package = "nanopb_pb2"
- }
+ pw_proto_library("proto") {
+ strip_prefix = "$dir_pw_third_party_nanopb/generator/proto"
+ sources = [ "$dir_pw_third_party_nanopb/generator/proto/nanopb.proto" ]
+ python_module_as_package = "nanopb_pb2"
+ }
In Python, this makes ``nanopb.proto`` available as ``import nanopb_pb2`` via
the ``nanopb_pb2`` Python package. In C++, ``nanopb.proto`` is accessed as
@@ -270,6 +270,17 @@ The ``python_module_as_package`` feature should only be used when absolutely
necessary --- for example, to support proto files that include
``import "nanopb.proto"``.
+Specifying a custom ``protoc``
+------------------------------
+If your build needs to use a custom build of ``protoc`` rather than the one
+supplied by pigweed it can be specified by setting
+``pw_protobuf_compiler_PROTOC_TARGET`` to a GN target that produces a ``protoc``
+executable and ``pw_protobuf_compiler_PROTOC_BINARY`` to the path, relative to
+``root_build_dir``, of the ``protoc`` executable.
+
+For all ``protoc`` invocations, the build will add a dependency on that target
+and will invoke that executable.
+
CMake
=====
CMake provides a ``pw_proto_library`` function with similar features as the
@@ -288,56 +299,56 @@ GN template. The CMake build only supports building firmware code, so
**Example**
- .. code-block:: cmake
+.. code-block:: cmake
- include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
- include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
+ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+ include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
- pw_proto_library(my_module.my_protos
- SOURCES
- my_protos/foo.proto
- my_protos/bar.proto
- )
+ pw_proto_library(my_module.my_protos
+ SOURCES
+ my_protos/foo.proto
+ my_protos/bar.proto
+ )
- pw_proto_library(my_module.my_protos
- SOURCES
- my_protos/foo.proto
- my_protos/bar.proto
- )
+ pw_proto_library(my_module.my_protos
+ SOURCES
+ my_protos/foo.proto
+ my_protos/bar.proto
+ )
- pw_proto_library(my_module.my_other_protos
- SOURCES
- some/other/path/baz.proto # imports foo.proto
+ pw_proto_library(my_module.my_other_protos
+ SOURCES
+ some/other/path/baz.proto # imports foo.proto
- # This removes the "some/other/path" prefix from the proto files.
- STRIP_PREFIX
- some/other/path
+ # This removes the "some/other/path" prefix from the proto files.
+ STRIP_PREFIX
+ some/other/path
- # This adds the "my_other_protos/" prefix to the proto files.
- PREFIX
- my_other_protos
+ # This adds the "my_other_protos/" prefix to the proto files.
+ PREFIX
+ my_other_protos
- # Proto libraries depend on other proto libraries directly.
- DEPS
- my_module.my_protos
- )
+ # Proto libraries depend on other proto libraries directly.
+ DEPS
+ my_module.my_protos
+ )
- add_library(my_module.my_cc_code
- foo.cc
- bar.cc
- baz.cc
- )
+ add_library(my_module.my_cc_code
+ foo.cc
+ bar.cc
+ baz.cc
+ )
- # When depending on protos in a source_set, specify the generator suffix.
- target_link_libraries(my_module.my_cc_code PUBLIC
- my_module.my_other_protos.pwpb
- )
+ # When depending on protos in a source_set, specify the generator suffix.
+ target_link_libraries(my_module.my_cc_code PUBLIC
+ my_module.my_other_protos.pwpb
+ )
These proto files are accessed in C++ the same as in the GN build:
.. code-block:: cpp
- #include "my_other_protos/baz.pwpb.h"
+ #include "my_other_protos/baz.pwpb.h"
**Supported Codegen**
@@ -369,97 +380,96 @@ into your Bazel WORKSPACE file. e.g.
.. code-block:: python
- # WORKSPACE ...
- load("@pigweed//pw_protobuf_compiler:deps.bzl", "pw_protobuf_dependencies")
- pw_protobuf_dependencies()
+ # WORKSPACE ...
+ load("@pigweed//pw_protobuf_compiler:deps.bzl", "pw_protobuf_dependencies")
+ pw_protobuf_dependencies()
Bazel uses a different set of rules to manage proto files than it does to
compile them. e.g.
.. code-block:: python
- # BUILD ...
- load("@rules_proto//proto:defs.bzl", "proto_library")
- load("@pigweed//pw_protobuf_compiler:proto.bzl",
- "nanopb_proto_library",
- "nanopb_rpc_proto_library",
- "pwpb_proto_library",
- "raw_rpc_proto_library",
- )
-
- # Manages proto sources and dependencies.
- proto_library(
- name = "my_proto",
- srcs = [
- "my_protos/foo.proto",
- "my_protos/bar.proto",
- ]
- )
-
- # Compiles dependent protos to C++.
- pwpb_proto_library(
- name = "my_proto_pwpb",
- deps = [":my_proto"],
- )
-
- nanopb_proto_library(
- name = "my_proto_nanopb",
- deps = [":my_proto"],
- )
-
- raw_rpc_proto_library(
- name = "my_proto_raw_rpc",
- deps = [":my_proto"],
- )
-
- nanopb_rpc_proto_library(
- name = "my_proto_nanopb_rpc",
- nanopb_proto_library_deps = [":my_proto_nanopb"],
- deps = [":my_proto"],
- )
-
- # Library that depends on only pw_protobuf generated proto targets.
- pw_cc_library(
- name = "my_proto_only_lib",
- srcs = ["my/proto_only.cc"],
- deps = [":my_proto_pwpb"],
- )
-
- # Library that depends on only Nanopb generated proto targets.
- pw_cc_library(
- name = "my_nanopb_only_lib",
- srcs = ["my/nanopb_only.cc"],
- deps = [":my_proto_nanopb"],
- )
-
- # Library that depends on pw_protobuf and pw_rpc/raw.
- pw_cc_library(
- name = "my_raw_rpc_lib",
- srcs = ["my/raw_rpc.cc"],
- deps = [
- ":my_proto_pwpb",
- ":my_proto_raw_rpc",
- ],
- )
- pw_cc_library(
- name = "my_nanopb_rpc_lib",
- srcs = ["my/proto_only.cc"],
- deps = [
- ":my_proto_nanopb_rpc",
- ],
- )
-
+ # BUILD ...
+ load("@rules_proto//proto:defs.bzl", "proto_library")
+ load("@pigweed//pw_protobuf_compiler:pw_proto_library.bzl",
+ "nanopb_proto_library",
+ "nanopb_rpc_proto_library",
+ "pwpb_proto_library",
+ "raw_rpc_proto_library",
+ )
+
+ # Manages proto sources and dependencies.
+ proto_library(
+ name = "my_proto",
+ srcs = [
+ "my_protos/foo.proto",
+ "my_protos/bar.proto",
+ ]
+ )
+
+ # Compiles dependent protos to C++.
+ pwpb_proto_library(
+ name = "my_proto_pwpb",
+ deps = [":my_proto"],
+ )
+
+ nanopb_proto_library(
+ name = "my_proto_nanopb",
+ deps = [":my_proto"],
+ )
+
+ raw_rpc_proto_library(
+ name = "my_proto_raw_rpc",
+ deps = [":my_proto"],
+ )
+
+ nanopb_rpc_proto_library(
+ name = "my_proto_nanopb_rpc",
+ nanopb_proto_library_deps = [":my_proto_nanopb"],
+ deps = [":my_proto"],
+ )
+
+ # Library that depends on only pw_protobuf generated proto targets.
+ pw_cc_library(
+ name = "my_proto_only_lib",
+ srcs = ["my/proto_only.cc"],
+ deps = [":my_proto_pwpb"],
+ )
+
+ # Library that depends on only Nanopb generated proto targets.
+ pw_cc_library(
+ name = "my_nanopb_only_lib",
+ srcs = ["my/nanopb_only.cc"],
+ deps = [":my_proto_nanopb"],
+ )
+
+ # Library that depends on pw_protobuf and pw_rpc/raw.
+ pw_cc_library(
+ name = "my_raw_rpc_lib",
+ srcs = ["my/raw_rpc.cc"],
+ deps = [
+ ":my_proto_pwpb",
+ ":my_proto_raw_rpc",
+ ],
+ )
+ pw_cc_library(
+ name = "my_nanopb_rpc_lib",
+ srcs = ["my/proto_only.cc"],
+ deps = [
+ ":my_proto_nanopb_rpc",
+ ],
+ )
From ``my/lib.cc`` you can now include the generated headers.
e.g.
-.. code:: cpp
+.. code-block:: cpp
- #include "my_protos/bar.pwpb.h"
- // and/or RPC headers
- #include "my_protos/bar.raw_rpc.pb.h
- // or
- #include "my_protos/bar.nanopb_rpc.pb.h"
+ #include "my_protos/bar.pwpb.h"
+ // and/or RPC headers
+ #include "my_protos/bar.raw_rpc.pb.h
+ // or
+ #include "my_protos/bar.nanopb_rpc.pb.h"
Why isn't there one rule to generate all the code?
@@ -482,7 +492,7 @@ The ``pw_proto_library`` target has a number of disadvantages:
the ``pw_proto_library`` macro, and bazel will attempt to build them when
you run ``bazel build //...``. This may cause build breakages, and has
forced us to implement `awkward workarounds
- <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/96980>`_.
+ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/96980>`_.
----------------------
Python proto libraries
diff --git a/pw_protobuf_compiler/proto.bzl b/pw_protobuf_compiler/proto.bzl
deleted file mode 100644
index 4b07323ca..000000000
--- a/pw_protobuf_compiler/proto.bzl
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright 2022 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Embedded-friendly replacement for native.cc_proto_library."""
-
-load(
- "//third_party/rules_proto_grpc:internal_proto.bzl",
- _nanopb_proto_library = "nanopb_proto_library",
- _nanopb_rpc_proto_library = "nanopb_rpc_proto_library",
- _pw_proto_library = "pw_proto_library",
- _pwpb_proto_library = "pwpb_proto_library",
- _pwpb_rpc_proto_library = "pwpb_rpc_proto_library",
- _raw_rpc_proto_library = "raw_rpc_proto_library",
-)
-
-# Export internal symbols.
-nanopb_proto_library = _nanopb_proto_library
-nanopb_rpc_proto_library = _nanopb_rpc_proto_library
-pw_proto_library = _pw_proto_library
-pwpb_proto_library = _pwpb_proto_library
-pwpb_rpc_proto_library = _pwpb_rpc_proto_library
-raw_rpc_proto_library = _raw_rpc_proto_library
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index 8a628010c..305a05f4a 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -13,6 +13,7 @@
# the License.
import("//build_overrides/pigweed.gni")
+import("//build_overrides/pigweed_environment.gni")
import("$dir_pw_build/error.gni")
import("$dir_pw_build/input_group.gni")
@@ -31,6 +32,29 @@ _forwarded_vars = [
"visibility",
]
+declare_args() {
+ # To override the protobuf compiler used set this to the GN target that builds
+ # the protobuf compiler.
+ pw_protobuf_compiler_PROTOC_TARGET = ""
+}
+
+if (pw_protobuf_compiler_PROTOC_TARGET == "" &&
+ defined(pw_env_setup_CIPD_PIGWEED)) {
+ _default_protc =
+ rebase_path("$pw_env_setup_CIPD_PIGWEED/bin/protoc", root_build_dir)
+ if (host_os == "win") {
+ _default_protc += ".exe"
+ }
+} else {
+ _default_protc = ""
+}
+
+declare_args() {
+ # To override the protobuf compiler used set this to the path, relative to the
+ # root_build_dir, to the protoc binary.
+ pw_protobuf_compiler_PROTOC_BINARY = _default_protc
+}
+
# Internal template that invokes protoc with a pw_python_action. This should not
# be used outside of this file; use pw_proto_library instead.
#
@@ -89,6 +113,21 @@ template("_pw_invoke_protoc") {
if (!pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES) {
args += [ "--exclude-pwpb-legacy-snake-case-field-name-enums" ]
}
+ if (pw_protobuf_compiler_PROTOC_TARGET != "") {
+ assert(
+ pw_protobuf_compiler_PROTOC_BINARY != "",
+ "if pw_protobuf_compiler_PROTOC_TARGET is set then pw_protobuf_compiler_PROTOC_BINARY must be set to the path to protoc, relative to the root_build_dir.")
+ _protoc_src_dir =
+ get_label_info(pw_protobuf_compiler_PROTOC_TARGET, "dir") + "/src"
+
+ args += [
+ "--protoc",
+ pw_protobuf_compiler_PROTOC_BINARY,
+ "--proto-path",
+ rebase_path(_protoc_src_dir, root_build_dir),
+ ]
+ deps += [ pw_protobuf_compiler_PROTOC_TARGET ]
+ }
args += [
"--language",
invoker.language,
diff --git a/pw_protobuf_compiler/pw_nanopb_cc_library.bzl b/pw_protobuf_compiler/pw_nanopb_cc_library.bzl
deleted file mode 100644
index c2bda47c3..000000000
--- a/pw_protobuf_compiler/pw_nanopb_cc_library.bzl
+++ /dev/null
@@ -1,37 +0,0 @@
-# Copyright 2022 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""WORK IN PROGRESS!
-
-Nanopb C++ library generating targets.
-"""
-
-# TODO(b/234873954) Enable unused variable check.
-# buildifier: disable=unused-variable
-def pw_nanopb_cc_library(
- name,
- deps,
- options = None,
- **kwargs):
- """Generates the nanopb C++ library.
-
- deps: proto_library targets to convert using nanopb.
- options: Path to the nanopb .options file. See
- https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
- for the syntax.
- """
-
- # TODO(tpudlik): Implement this rule. Just a placeholder for now.
- native.cc_library(
- name = name,
- )
diff --git a/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel b/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel
index 92a3073c3..41383ed15 100644
--- a/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel
+++ b/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel
@@ -13,7 +13,7 @@
# the License.
load("@rules_proto//proto:defs.bzl", "proto_library")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
diff --git a/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel b/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel
index 9d9fa4186..ef0308077 100644
--- a/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel
@@ -13,7 +13,7 @@
# the License.
load("@rules_proto//proto:defs.bzl", "proto_library")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
diff --git a/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel
index 12c7aadd2..d9d3902c4 100644
--- a/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel
@@ -13,7 +13,7 @@
# the License.
load("@rules_proto//proto:defs.bzl", "proto_library")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
diff --git a/pw_protobuf_compiler/pw_proto_library.bzl b/pw_protobuf_compiler/pw_proto_library.bzl
index 58d07bdc9..3f73689a6 100644
--- a/pw_protobuf_compiler/pw_proto_library.bzl
+++ b/pw_protobuf_compiler/pw_proto_library.bzl
@@ -13,42 +13,41 @@
# the License.
"""WORK IN PROGRESS!
-This is intended to be a replacement for the proto codegen in proto.bzl, which
-relies on the transitive proto compilation support removed from newer versions
-of rules_proto_grpc. However, the version checked in here does not yet support,
-
-1. Proto libraries with dependencies in external repositories.
-2. Proto libraries with strip_import_prefix or import_prefix attributes.
-
-In addition, nanopb proto files are not yet generated.
-
-TODO(b/234873954): Close these gaps and start using this implementation.
-
# Overview of implementation
(If you just want to use the macros, see their docstrings; this section is
intended to orient future maintainers.)
-Proto code generation is carried out by the _pw_proto_library,
-_pw_raw_rpc_proto_library and _pw_nanopb_rpc_proto_library rules using aspects
-(https://docs.bazel.build/versions/main/skylark/aspects.html). A
-_pw_proto_library has a single proto_library as a dependency, but that
-proto_library may depend on other proto_library targets; as a result, the
-generated .pwpb.h file #include's .pwpb.h files generated from the dependency
-proto_libraries. The aspect propagates along the proto_library dependency
-graph, running the proto compiler on each proto_library in the original
-target's transitive dependencies, ensuring that we're not missing any .pwpb.h
-files at C++ compile time.
+Proto code generation is carried out by the _pwpb_proto_library,
+_nanopb_proto_library, _pw_raw_rpc_proto_library and
+_pw_nanopb_rpc_proto_library rules using aspects
+(https://docs.bazel.build/versions/main/skylark/aspects.html).
+
+As an example, _pwpb_proto_library has a single proto_library as a dependency,
+but that proto_library may depend on other proto_library targets; as a result,
+the generated .pwpb.h file #include's .pwpb.h files generated from the
+dependency proto_libraries. The aspect propagates along the proto_library
+dependency graph, running the proto compiler on each proto_library in the
+original target's transitive dependencies, ensuring that we're not missing any
+.pwpb.h files at C++ compile time.
Although we have a separate rule for each protocol compiler plugin
-(_pw_proto_library, _pw_raw_rpc_proto_library, _pw_nanopb_rpc_proto_library),
-they actually share an implementation (_pw _impl_pw_proto_library) and use
-similar aspects, all generated by _proto_compiler_aspect.
+(_pwpb_proto_library, _nanopb_proto_library, _pw_raw_rpc_proto_library,
+_pw_nanopb_rpc_proto_library), they actually share an implementation
+(_impl_pw_proto_library) and use similar aspects, all generated by
+_proto_compiler_aspect.
"""
-load("//pw_build:pigweed.bzl", "pw_cc_library")
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "use_cpp_toolchain")
+load(
+ "@pigweed//pw_build/bazel_internal:pigweed_internal.bzl",
+ _compile_cc = "compile_cc",
+)
load("@rules_proto//proto:defs.bzl", "ProtoInfo")
-load("//pw_protobuf_compiler:pw_nanopb_cc_library.bzl", "pw_nanopb_cc_library")
+
+# For Copybara use only
+ADDITIONAL_PWPB_DEPS = []
def pwpb_proto_library(name, deps, tags = None, visibility = None):
"""A C++ proto library generated using pw_protobuf.
@@ -56,27 +55,19 @@ def pwpb_proto_library(name, deps, tags = None, visibility = None):
Attributes:
deps: proto_library targets for which to generate this library.
"""
- name_pb = name + ".pb"
-
- _pw_proto_library(
- name = name_pb,
- deps = deps,
- )
-
- pw_cc_library(
+ _pwpb_proto_library(
name = name,
- hdrs = [":" + name_pb],
+ protos = deps,
deps = [
- "//pw_assert:facade",
- "//pw_containers:vector",
- "//pw_preprocessor",
- "//pw_protobuf",
- "//pw_result",
- "//pw_span",
- "//pw_status",
- "//pw_string:string",
- ],
- linkstatic = 1,
+ Label("//pw_assert"),
+ Label("//pw_containers:vector"),
+ Label("//pw_preprocessor"),
+ Label("//pw_protobuf"),
+ Label("//pw_result"),
+ Label("//pw_span"),
+ Label("//pw_status"),
+ Label("//pw_string:string"),
+ ] + ADDITIONAL_PWPB_DEPS,
tags = tags,
visibility = visibility,
)
@@ -89,50 +80,75 @@ def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, tags = None, vis
pwpb_proto_library_deps: A pwpb_proto_library generated
from the same proto_library. Required.
"""
- name_pb = name + ".pb"
-
_pw_pwpb_rpc_proto_library(
- name = name_pb,
- deps = deps,
- )
-
- pw_cc_library(
name = name,
- hdrs = [":" + name_pb],
+ protos = deps,
deps = [
- "//pw_protobuf",
- "//pw_rpc",
- "//pw_rpc/pwpb:client_api",
- "//pw_rpc/pwpb:server_api",
+ Label("//pw_protobuf"),
+ Label("//pw_rpc"),
+ Label("//pw_rpc/pwpb:client_api"),
+ Label("//pw_rpc/pwpb:server_api"),
] + pwpb_proto_library_deps,
- linkstatic = 1,
tags = tags,
visibility = visibility,
)
def raw_rpc_proto_library(name, deps, tags = None, visibility = None):
"""A raw C++ RPC proto library."""
- name_pb = name + ".pb"
-
_pw_raw_rpc_proto_library(
- name = name_pb,
- deps = deps,
+ name = name,
+ protos = deps,
+ deps = [
+ Label("//pw_rpc"),
+ Label("//pw_rpc/raw:client_api"),
+ Label("//pw_rpc/raw:server_api"),
+ ],
+ tags = tags,
+ visibility = visibility,
)
- pw_cc_library(
+# TODO: b/234873954 - Enable unused variable check.
+# buildifier: disable=unused-variable
+def nanopb_proto_library(name, deps, tags = [], visibility = None, options = None):
+ """A C++ proto library generated using pw_protobuf.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ """
+
+ # TODO(tpudlik): Find a way to get Nanopb to generate nested structs.
+ # Otherwise add the manual tag to the resulting library, preventing it
+ # from being built unless directly depended on. e.g. The 'Pigweed'
+ # message in
+ # pw_protobuf/pw_protobuf_test_protos/full_test.proto will fail to
+ # compile as it has a self referring nested message. According to
+ # the docs
+ # https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
+ # and https://github.com/nanopb/nanopb/issues/433 it seems like it
+ # should be possible to configure nanopb to generate nested structs via
+ # flags in .options files.
+ #
+ # One issue is nanopb doesn't silently ignore unknown options in .options
+ # files so we can't share .options files with pwpb.
+ extra_tags = ["manual"]
+ _nanopb_proto_library(
name = name,
- hdrs = [":" + name_pb],
+ protos = deps,
deps = [
- "//pw_rpc",
- "//pw_rpc/raw:client_api",
- "//pw_rpc/raw:server_api",
+ "@com_github_nanopb_nanopb//:nanopb",
+ Label("//pw_assert"),
+ Label("//pw_containers:vector"),
+ Label("//pw_preprocessor"),
+ Label("//pw_result"),
+ Label("//pw_span"),
+ Label("//pw_status"),
+ Label("//pw_string:string"),
],
- linkstatic = 1,
- tags = tags,
+ tags = tags + extra_tags,
visibility = visibility,
)
-def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = None, visibility = None):
+def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = [], visibility = None):
"""A C++ RPC proto library using nanopb.
Attributes:
@@ -140,23 +156,18 @@ def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = None,
nanopb_proto_library_deps: A pw_nanopb_cc_library generated
from the same proto_library. Required.
"""
- name_pb = name + ".pb"
+ # See comment in nanopb_proto_library.
+ extra_tags = ["manual"]
_pw_nanopb_rpc_proto_library(
- name = name_pb,
- deps = deps,
- )
-
- pw_cc_library(
name = name,
- hdrs = [":" + name_pb],
+ protos = deps,
deps = [
- "//pw_rpc",
- "//pw_rpc/nanopb:client_api",
- "//pw_rpc/nanopb:server_api",
+ Label("//pw_rpc"),
+ Label("//pw_rpc/nanopb:client_api"),
+ Label("//pw_rpc/nanopb:server_api"),
] + nanopb_proto_library_deps,
- linkstatic = 1,
- tags = tags,
+ tags = tags + extra_tags,
visibility = visibility,
)
@@ -164,7 +175,7 @@ def pw_proto_library(
name,
deps,
visibility = None,
- tags = None,
+ tags = [],
nanopb_options = None,
enabled_targets = None):
"""Generate Pigweed proto C++ code.
@@ -228,11 +239,11 @@ def pw_proto_library(
if is_plugin_enabled("nanopb"):
# Use nanopb to generate the pb.h and pb.c files, and the target
# exposing them.
- pw_nanopb_cc_library(
+ nanopb_proto_library(
name = name + ".nanopb",
deps = deps,
- visibility = visibility,
tags = tags,
+ visibility = visibility,
options = nanopb_options,
)
@@ -273,7 +284,9 @@ def pw_proto_library(
PwProtoInfo = provider(
"Returned by PW proto compilation aspect",
fields = {
- "genfiles": "generated C++ header files",
+ "hdrs": "generated C++ header files",
+ "srcs": "generated C++ src files",
+ "includes": "include paths for generated C++ header files",
},
)
@@ -287,19 +300,48 @@ PwProtoOptionsInfo = provider(
},
)
-def _get_short_path(source):
- return source.short_path
-
-def _get_path(file):
- return file.path
-
def _proto_compiler_aspect_impl(target, ctx):
# List the files we will generate for this proto_library target.
- genfiles = []
-
- for src in target[ProtoInfo].direct_sources:
- path = src.basename[:-len("proto")] + ctx.attr._extension
- genfiles.append(ctx.actions.declare_file(path, sibling = src))
+ proto_info = target[ProtoInfo]
+
+ srcs = []
+ hdrs = []
+
+ # Setup the output root for the plugin to point to targets output
+ # directory. This allows us to declare the location of the files that protoc
+ # will output in a way that `ctx.actions.declare_file` will understand,
+ # since it works relative to the target.
+ out_path = ctx.bin_dir.path
+ if target.label.workspace_root:
+ out_path += "/" + target.label.workspace_root
+ if target.label.package:
+ out_path += "/" + target.label.package
+
+ # Add location of headers to cc include path.
+ # Depending on prefix rules, the include path can be directly from the
+ # output path, or underneath the package.
+ includes = [out_path]
+
+ for src in proto_info.direct_sources:
+ # Get the relative import path for this .proto file.
+ src_rel = paths.relativize(src.path, proto_info.proto_source_root)
+ proto_dir = paths.dirname(src_rel)
+
+ # Add location of headers to cc include path.
+ includes.append("{}/{}".format(out_path, src.owner.package))
+
+ for ext in ctx.attr._extensions:
+ # Declare all output files, in target package dir.
+ generated_filename = src.basename[:-len("proto")] + ext
+ out_file = ctx.actions.declare_file("{}/{}".format(
+ proto_dir,
+ generated_filename,
+ ))
+
+ if ext.endswith(".h"):
+ hdrs.append(out_file)
+ else:
+ srcs.append(out_file)
# List the `.options` files from any `pw_proto_filegroup` targets listed
# under this target's `srcs`.
@@ -310,54 +352,84 @@ def _proto_compiler_aspect_impl(target, ctx):
for options_file in src[PwProtoOptionsInfo].options_files.to_list()
]
- # Convert include paths to a depset and back to deduplicate entries.
- # Note that this will probably evaluate to either [] or ["."] in most cases.
- options_file_include_paths = depset([
- "." if options_file.root.path == "" else options_file.root.path
- for options_file in options_files
- ]).to_list()
+ # Local repository options files.
+ options_file_include_paths = ["."]
+ for options_file in options_files:
+ # Handle .options files residing in external repositories.
+ if options_file.owner.workspace_root:
+ options_file_include_paths.append(options_file.owner.workspace_root)
+
+ # Handle generated .options files.
+ if options_file.root.path:
+ options_file_include_paths.append(options_file.root.path)
args = ctx.actions.args()
- args.add("--plugin=protoc-gen-pwpb={}".format(ctx.executable._protoc_plugin.path))
- for options_file_include_path in options_file_include_paths:
- args.add("--pwpb_opt=-I{}".format(options_file_include_path))
- args.add("--pwpb_opt=--no-legacy-namespace")
- args.add("--pwpb_out={}".format(ctx.bin_dir.path))
- args.add_joined(
- "--descriptor_set_in",
- target[ProtoInfo].transitive_descriptor_sets,
- join_with = ctx.configuration.host_path_separator,
- map_each = _get_path,
- )
+ for path in proto_info.transitive_proto_path.to_list():
+ args.add("-I{}".format(path))
- args.add_all(target[ProtoInfo].direct_sources, map_each = _get_short_path)
+ args.add("--plugin=protoc-gen-custom={}".format(ctx.executable._protoc_plugin.path))
+
+ # Convert include paths to a depset and back to deduplicate entries.
+ for options_file_include_path in depset(options_file_include_paths).to_list():
+ args.add("--custom_opt=-I{}".format(options_file_include_path))
+
+ for plugin_option in ctx.attr._plugin_options:
+ args.add("--custom_opt={}".format(plugin_option))
+
+ args.add("--custom_out={}".format(out_path))
+ args.add_all(proto_info.direct_sources)
+
+ all_tools = [
+ ctx.executable._protoc,
+ ctx.executable._python_runtime,
+ ctx.executable._protoc_plugin,
+ ]
+ run_path = [tool.dirname for tool in all_tools]
ctx.actions.run(
inputs = depset(
- target[ProtoInfo].transitive_sources.to_list() + options_files,
- transitive = [target[ProtoInfo].transitive_descriptor_sets],
+ direct = proto_info.direct_sources +
+ proto_info.transitive_sources.to_list() +
+ options_files,
+ transitive = [proto_info.transitive_descriptor_sets],
),
- progress_message = "Generating %s C++ files for %s" % (ctx.attr._extension, ctx.label.name),
- tools = [ctx.executable._protoc_plugin],
- outputs = genfiles,
+ progress_message = "Generating %s C++ files for %s" % (ctx.attr._extensions, ctx.label.name),
+ tools = all_tools,
+ outputs = srcs + hdrs,
executable = ctx.executable._protoc,
arguments = [args],
+ env = {
+ "PATH": ":".join(run_path),
+
+ # The nanopb protobuf plugin likes to compile some temporary protos
+ # next to source files. This forces them to be written to Bazel's
+ # genfiles directory.
+ "NANOPB_PB2_TEMP_DIR": str(ctx.genfiles_dir),
+ },
)
- transitive_genfiles = genfiles
+ transitive_srcs = srcs
+ transitive_hdrs = hdrs
+ transitive_includes = includes
for dep in ctx.rule.attr.deps:
- transitive_genfiles += dep[PwProtoInfo].genfiles
- return [PwProtoInfo(genfiles = transitive_genfiles)]
-
-def _proto_compiler_aspect(extension, protoc_plugin):
+ transitive_srcs += dep[PwProtoInfo].srcs
+ transitive_hdrs += dep[PwProtoInfo].hdrs
+ transitive_includes += dep[PwProtoInfo].includes
+ return [PwProtoInfo(
+ srcs = transitive_srcs,
+ hdrs = transitive_hdrs,
+ includes = transitive_includes,
+ )]
+
+def _proto_compiler_aspect(extensions, protoc_plugin, plugin_options = []):
"""Returns an aspect that runs the proto compiler.
The aspect propagates through the deps of proto_library targets, running
the proto compiler with the specified plugin for each of their source
files. The proto compiler is assumed to produce one output file per input
.proto file. That file is placed under bazel-bin at the same path as the
- input file, but with the specified extension (i.e., with _extension =
- .pwpb.h, the aspect converts pw_log/log.proto into
+ input file, but with the specified extension (i.e., with _extensions = [
+ .pwpb.h], the aspect converts pw_log/log.proto into
bazel-bin/pw_log/log.pwpb.h).
The aspect returns a provider exposing all the File objects generated from
@@ -366,7 +438,7 @@ def _proto_compiler_aspect(extension, protoc_plugin):
return aspect(
attr_aspects = ["deps"],
attrs = {
- "_extension": attr.string(default = extension),
+ "_extensions": attr.string_list(default = extensions),
"_protoc": attr.label(
default = Label("@com_google_protobuf//:protoc"),
executable = True,
@@ -377,6 +449,15 @@ def _proto_compiler_aspect(extension, protoc_plugin):
executable = True,
cfg = "exec",
),
+ "_python_runtime": attr.label(
+ default = Label("//:python3_interpreter"),
+ allow_single_file = True,
+ executable = True,
+ cfg = "exec",
+ ),
+ "_plugin_options": attr.string_list(
+ default = plugin_options,
+ ),
},
implementation = _proto_compiler_aspect_impl,
provides = [PwProtoInfo],
@@ -386,7 +467,7 @@ def _impl_pw_proto_library(ctx):
"""Implementation of the proto codegen rule.
The work of actually generating the code is done by the aspect, so here we
- just gather up all the generated files and return them.
+ compile and return a CcInfo to link against.
"""
# Note that we don't distinguish between the files generated from the
@@ -398,61 +479,131 @@ def _impl_pw_proto_library(ctx):
# in srcs. We don't perform layering_check in Pigweed, so this is not a big
# deal.
#
- # TODO(b/234873954): Tidy this up.
- all_genfiles = []
- for dep in ctx.attr.deps:
- for f in dep[PwProtoInfo].genfiles:
- all_genfiles.append(f)
-
- return [DefaultInfo(files = depset(all_genfiles))]
+ # TODO: b/234873954 - Tidy this up.
+ all_srcs = []
+ all_hdrs = []
+ all_includes = []
+ for dep in ctx.attr.protos:
+ for f in dep[PwProtoInfo].hdrs:
+ all_hdrs.append(f)
+ for f in dep[PwProtoInfo].srcs:
+ all_srcs.append(f)
+ for i in dep[PwProtoInfo].includes:
+ all_includes.append(i)
+
+ return _compile_cc(
+ ctx,
+ all_srcs,
+ all_hdrs,
+ ctx.attr.deps,
+ all_includes,
+ defines = [],
+ )
# Instantiate the aspects and rules for generating code using specific plugins.
-_pw_proto_compiler_aspect = _proto_compiler_aspect("pwpb.h", "//pw_protobuf/py:plugin")
+_pwpb_proto_compiler_aspect = _proto_compiler_aspect(
+ ["pwpb.h"],
+ "//pw_protobuf/py:plugin",
+ ["--no-legacy-namespace"],
+)
-_pw_proto_library = rule(
+_pwpb_proto_library = rule(
implementation = _impl_pw_proto_library,
attrs = {
+ "protos": attr.label_list(
+ providers = [ProtoInfo],
+ aspects = [_pwpb_proto_compiler_aspect],
+ ),
"deps": attr.label_list(
+ providers = [CcInfo],
+ ),
+ },
+ fragments = ["cpp"],
+ toolchains = use_cpp_toolchain(),
+)
+
+_nanopb_proto_compiler_aspect = _proto_compiler_aspect(
+ ["pb.h", "pb.c"],
+ "@com_github_nanopb_nanopb//:protoc-gen-nanopb",
+ ["--library-include-format=quote"],
+)
+
+_nanopb_proto_library = rule(
+ implementation = _impl_pw_proto_library,
+ attrs = {
+ "protos": attr.label_list(
providers = [ProtoInfo],
- aspects = [_pw_proto_compiler_aspect],
+ aspects = [_nanopb_proto_compiler_aspect],
+ ),
+ "deps": attr.label_list(
+ providers = [CcInfo],
),
},
+ fragments = ["cpp"],
+ toolchains = use_cpp_toolchain(),
)
-_pw_pwpb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pwpb.h", "//pw_rpc/py:plugin_pwpb")
+_pw_pwpb_rpc_proto_compiler_aspect = _proto_compiler_aspect(
+ ["rpc.pwpb.h"],
+ "//pw_rpc/py:plugin_pwpb",
+ ["--no-legacy-namespace"],
+)
_pw_pwpb_rpc_proto_library = rule(
implementation = _impl_pw_proto_library,
attrs = {
- "deps": attr.label_list(
+ "protos": attr.label_list(
providers = [ProtoInfo],
aspects = [_pw_pwpb_rpc_proto_compiler_aspect],
),
+ "deps": attr.label_list(
+ providers = [CcInfo],
+ ),
},
+ fragments = ["cpp"],
+ toolchains = use_cpp_toolchain(),
)
-_pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect("raw_rpc.pb.h", "//pw_rpc/py:plugin_raw")
+_pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect(
+ ["raw_rpc.pb.h"],
+ "//pw_rpc/py:plugin_raw",
+ ["--no-legacy-namespace"],
+)
_pw_raw_rpc_proto_library = rule(
implementation = _impl_pw_proto_library,
attrs = {
- "deps": attr.label_list(
+ "protos": attr.label_list(
providers = [ProtoInfo],
aspects = [_pw_raw_rpc_proto_compiler_aspect],
),
+ "deps": attr.label_list(
+ providers = [CcInfo],
+ ),
},
+ fragments = ["cpp"],
+ toolchains = use_cpp_toolchain(),
)
-_pw_nanopb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pb.h", "//pw_rpc/py:plugin_nanopb")
+_pw_nanopb_rpc_proto_compiler_aspect = _proto_compiler_aspect(
+ ["rpc.pb.h"],
+ "//pw_rpc/py:plugin_nanopb",
+ ["--no-legacy-namespace"],
+)
_pw_nanopb_rpc_proto_library = rule(
implementation = _impl_pw_proto_library,
attrs = {
- "deps": attr.label_list(
+ "protos": attr.label_list(
providers = [ProtoInfo],
aspects = [_pw_nanopb_rpc_proto_compiler_aspect],
),
+ "deps": attr.label_list(
+ providers = [CcInfo],
+ ),
},
+ fragments = ["cpp"],
+ toolchains = use_cpp_toolchain(),
)
def _pw_proto_filegroup_impl(ctx):
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel
index 280c01fd5..a2babf575 100644
--- a/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel
+++ b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel
@@ -16,13 +16,6 @@ load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"//pw_protobuf_compiler:pw_proto_library.bzl",
"pw_proto_filegroup",
- # TODO(b/234873954): Also load `pw_proto_library` from here when possible.
- # A somewhat different Starlark macro with the same name is currently loaded
- # from `proto.bzl` (below), but we need to switch to the implementation from
- # `pw_proto_library.bzl` when possible.
-)
-load(
- "//pw_protobuf_compiler:proto.bzl",
"pw_proto_library",
)
diff --git a/pw_protobuf_compiler/py/BUILD.gn b/pw_protobuf_compiler/py/BUILD.gn
index 4616364d6..bf3ee1e68 100644
--- a/pw_protobuf_compiler/py/BUILD.gn
+++ b/pw_protobuf_compiler/py/BUILD.gn
@@ -21,7 +21,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_protobuf_compiler/__init__.py",
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
index 5d379cbb6..415611a2c 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
@@ -52,6 +52,15 @@ _LOG = logging.getLogger(__name__)
PathOrStr = Union[Path, str]
+def _find_protoc() -> str:
+ """Locates a protoc binary to use for compiling protos."""
+ if 'PROTOC' in os.environ:
+ return os.environ['PROTOC']
+
+ # Fallback is assuming `protoc` is on the system PATH.
+ return 'protoc'
+
+
def compile_protos(
output_dir: PathOrStr,
proto_files: Iterable[PathOrStr],
@@ -70,7 +79,7 @@ def compile_protos(
include_paths.add(path.parent)
cmd: Tuple[PathOrStr, ...] = (
- 'protoc',
+ _find_protoc(),
'--experimental_allow_proto3_optional',
'--python_out',
os.path.abspath(output_dir),
diff --git a/pw_protobuf_compiler/py/setup.cfg b/pw_protobuf_compiler/py/setup.cfg
index 8e3a6eca0..b679dd65f 100644
--- a/pw_protobuf_compiler/py/setup.cfg
+++ b/pw_protobuf_compiler/py/setup.cfg
@@ -23,12 +23,12 @@ packages = find:
zip_safe = False
install_requires =
# NOTE: protobuf needs to stay in sync with mypy-protobuf
- # Currently using mypy protobuf 3.20.1 and mypy-protobuf 3.3.0 (see
- # constraint.list). These requirements should stay as >= the lowest version
- # we support.
- mypy-protobuf>=3.2.0
- protobuf>=3.20.1
- types-protobuf>=3.19.22
+ # Currently using mypy protobuf 3.5.0 (see constraint.list). These
+ # requirements should stay as >= the lowest version we support.
+ mypy-protobuf>=3.5.0,<3.6.0
+ # This is also specified in //pw_protobuf/py/setup.cfg
+ protobuf~=4.24.4
+ types-protobuf>=4.24.0.0,<5.0.0
[options.package_data]
pw_protobuf_compiler = py.typed
diff --git a/pw_protobuf_compiler/py/setup.py b/pw_protobuf_compiler/py/setup.py
deleted file mode 100644
index ee28b2297..000000000
--- a/pw_protobuf_compiler/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_protobuf_compiler"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_protobuf_compiler/toolchain.gni b/pw_protobuf_compiler/toolchain.gni
index 806a96367..4256fd267 100644
--- a/pw_protobuf_compiler/toolchain.gni
+++ b/pw_protobuf_compiler/toolchain.gni
@@ -25,5 +25,5 @@ declare_args() {
# kConstantCase. Set this variable to temporarily enable legacy SNAKE_CASE
# support while you migrate your codebase to kConstantCase.
# b/266298474
- pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES = true
+ pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES = false
}
diff --git a/pw_protobuf_compiler/ts/build.ts b/pw_protobuf_compiler/ts/build.ts
index 2baa7e641..9ae903612 100644
--- a/pw_protobuf_compiler/ts/build.ts
+++ b/pw_protobuf_compiler/ts/build.ts
@@ -12,11 +12,10 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {exec, ExecException} from 'child_process';
+import { exec, ExecException } from 'child_process';
import fs from 'fs';
import path from 'path';
import generateTemplate from './codegen/template_replacement';
-// eslint-disable-next-line node/no-extraneous-import
import * as argModule from 'arg';
const arg = argModule.default;
@@ -35,18 +34,22 @@ const args = arg({
const protos = args['--proto'];
const outDir = args['--out'] || 'protos';
-fs.mkdirSync(outDir, {recursive: true});
+fs.mkdirSync(outDir, { recursive: true });
const run = function (executable: string, args: string[]) {
- return new Promise<void>(resolve => {
- exec(`${executable} ${args.join(" ")}`, {cwd: process.cwd()}, (error: ExecException | null, stdout: string | Buffer) => {
- if (error) {
- throw error;
- }
+ return new Promise<void>((resolve) => {
+ exec(
+ `${executable} ${args.join(' ')}`,
+ { cwd: process.cwd() },
+ (error: ExecException | null, stdout: string | Buffer) => {
+ if (error) {
+ throw error;
+ }
- console.log(stdout);
- resolve();
- });
+ console.log(stdout);
+ resolve();
+ },
+ );
});
};
@@ -55,7 +58,7 @@ const protoc = async function (protos: string[], outDir: string) {
path.dirname(require.resolve('ts-protoc-gen/generate.js')),
'..',
'.bin',
- 'protoc-gen-ts'
+ 'protoc-gen-ts',
);
await run('protoc', [
@@ -68,12 +71,15 @@ const protoc = async function (protos: string[], outDir: string) {
]);
// ES6 workaround: Replace google-protobuf imports with entire library.
- protos.forEach(protoPath => {
+ protos.forEach((protoPath) => {
const outPath = path.join(outDir, protoPath.replace('.proto', '_pb.js'));
if (fs.existsSync(outPath)) {
let data = fs.readFileSync(outPath, 'utf8');
- data = data.replace("var jspb = require('google-protobuf');", googProtobufModule);
+ data = data.replace(
+ "var jspb = require('google-protobuf');",
+ googProtobufModule,
+ );
data = data.replace('var goog = jspb;', '');
fs.writeFileSync(outPath, data);
}
@@ -83,16 +89,24 @@ const protoc = async function (protos: string[], outDir: string) {
const makeProtoCollection = function (
descriptorBinPath: string,
protoPath: string,
- importPath: string
+ importPath: string,
) {
- const outputCollectionName = path.extname(require.resolve("./ts_proto_collection.template")) === ".ts" ? "collection.ts" : "collection.js";
- generateTemplate(`${protoPath}/${outputCollectionName}`, descriptorBinPath, require.resolve("./ts_proto_collection.template"), importPath)
+ const outputCollectionName =
+ path.extname(require.resolve('./ts_proto_collection.template')) === '.ts'
+ ? 'collection.ts'
+ : 'collection.js';
+ generateTemplate(
+ `${protoPath}/${outputCollectionName}`,
+ descriptorBinPath,
+ require.resolve('./ts_proto_collection.template'),
+ importPath,
+ );
};
protoc(protos, outDir).then(() => {
makeProtoCollection(
path.join(outDir, 'descriptor.bin'),
outDir,
- 'pigweedjs/protos'
+ 'pigweedjs/protos',
);
});
diff --git a/pw_protobuf_compiler/ts/codegen/template_replacement.ts b/pw_protobuf_compiler/ts/codegen/template_replacement.ts
index 7f2ef0517..18d224f4e 100644
--- a/pw_protobuf_compiler/ts/codegen/template_replacement.ts
+++ b/pw_protobuf_compiler/ts/codegen/template_replacement.ts
@@ -13,20 +13,25 @@
// the License.
import * as fs from 'fs';
-import {FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
+import { FileDescriptorSet } from 'google-protobuf/google/protobuf/descriptor_pb';
function buildModulePath(rootDir: string, fileName: string): string {
const name = `${rootDir}/${fileName}`;
return name.replace(/\.proto$/, '_pb');
}
-export default function generateTemplate(outputPath: string, descriptorDataPath: string, templatePath: string, protoRootDir: string) {
+export default function generateTemplate(
+ outputPath: string,
+ descriptorDataPath: string,
+ templatePath: string,
+ protoRootDir: string,
+) {
let template = fs.readFileSync(templatePath).toString();
const descriptorSetBinary = fs.readFileSync(descriptorDataPath);
const base64DescriptorSet = descriptorSetBinary.toString('base64');
const fileDescriptorSet = FileDescriptorSet.deserializeBinary(
- Buffer.from(descriptorSetBinary)
+ Buffer.from(descriptorSetBinary),
);
const imports = [];
@@ -34,21 +39,21 @@ export default function generateTemplate(outputPath: string, descriptorDataPath:
const fileList = fileDescriptorSet.getFileList();
for (let i = 0; i < fileList.length; i++) {
const file = fileList[i];
- const modulePath = buildModulePath(".", file.getName()!);
+ const modulePath = buildModulePath('.', file.getName()!);
const moduleName = 'proto_' + i;
imports.push(`import * as ${moduleName} from '${modulePath}';`);
const key = file.getName()!;
- moduleDictionary.push(`['${key}', ${moduleName}],`);
+ moduleDictionary.push(`'${key}': ${moduleName},`);
}
template = template.replace(
'{TEMPLATE_descriptor_binary}',
- base64DescriptorSet
+ base64DescriptorSet,
);
template = template.replace('// TEMPLATE_proto_imports', imports.join('\n'));
template = template.replace(
'// TEMPLATE_module_map',
- moduleDictionary.join('\n')
+ moduleDictionary.join('\n'),
);
fs.writeFileSync(outputPath, template);
diff --git a/pw_protobuf_compiler/ts/proto_collection.ts b/pw_protobuf_compiler/ts/proto_collection.ts
index fb31b3a96..9196aab6e 100644
--- a/pw_protobuf_compiler/ts/proto_collection.ts
+++ b/pw_protobuf_compiler/ts/proto_collection.ts
@@ -14,24 +14,33 @@
/** Tools for compiling and importing Javascript protos on the fly. */
-import {Message} from 'google-protobuf';
-import {DescriptorProto, FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
+import { Message } from 'google-protobuf';
+import {
+ DescriptorProto,
+ FileDescriptorSet,
+} from 'google-protobuf/google/protobuf/descriptor_pb';
export type MessageCreator = new () => Message;
-class MessageMap extends Map<string, MessageCreator> {}
-class MessageDescriptorMap extends Map<string, DescriptorProto> { }
-export class ModuleMap extends Map<string, any> {}
+interface MessageMap {
+ [key: string | number]: MessageCreator;
+}
+interface MessageDescriptorMap {
+ [key: string | number]: DescriptorProto;
+}
+export interface ModuleMap {
+ [key: string | number]: any;
+}
/**
* A wrapper class of protocol buffer modules to provide convenience methods.
*/
export class ProtoCollection {
- private messages: MessageMap = new MessageMap();
- private messageDescriptors: MessageDescriptorMap = new MessageDescriptorMap();
+ private readonly messages: MessageMap = {};
+ private readonly messageDescriptors: MessageDescriptorMap = {};
constructor(
readonly fileDescriptorSet: FileDescriptorSet,
- modules: ModuleMap
+ modules: ModuleMap,
) {
this.mapMessages(fileDescriptorSet, modules);
}
@@ -42,13 +51,13 @@ export class ProtoCollection {
*/
private mapMessages(set: FileDescriptorSet, mods: ModuleMap): void {
for (const fileDescriptor of set.getFileList()) {
- const mod = mods.get(fileDescriptor.getName()!)!;
+ const mod = mods[fileDescriptor.getName()];
for (const messageType of fileDescriptor.getMessageTypeList()) {
const fullName =
- fileDescriptor.getPackage()! + '.' + messageType.getName();
- const message = mod[messageType.getName()!];
- this.messages.set(fullName, message);
- this.messageDescriptors.set(fullName, messageType);
+ fileDescriptor.getPackage() + '.' + messageType.getName();
+ const message = mod[messageType.getName()];
+ this.messages[fullName] = message;
+ this.messageDescriptors[fullName] = messageType;
}
}
}
@@ -60,7 +69,7 @@ export class ProtoCollection {
* "{packageName}.{messageName}" i.e: "pw.rpc.test.NewMessage".
*/
getMessageCreator(identifier: string): MessageCreator | undefined {
- return this.messages.get(identifier);
+ return this.messages[identifier];
}
/**
@@ -70,6 +79,6 @@ export class ProtoCollection {
* "{packageName}.{messageName}" i.e: "pw.rpc.test.NewMessage".
*/
getDescriptorProto(identifier: string): DescriptorProto | undefined {
- return this.messageDescriptors.get(identifier);
+ return this.messageDescriptors[identifier];
}
}
diff --git a/pw_protobuf_compiler/ts/ts_proto_collection.template.ts b/pw_protobuf_compiler/ts/ts_proto_collection.template.ts
index ff0fcefcb..1db41abba 100644
--- a/pw_protobuf_compiler/ts/ts_proto_collection.template.ts
+++ b/pw_protobuf_compiler/ts/ts_proto_collection.template.ts
@@ -14,19 +14,16 @@
/** Tools for compiling and importing Javascript protos on the fly. */
-import {
- ProtoCollection as Base,
- ModuleMap,
-} from 'pigweedjs/pw_protobuf_compiler';
-import {FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
+import { ProtoCollection as Base } from 'pigweedjs/pw_protobuf_compiler';
+import { FileDescriptorSet } from 'google-protobuf/google/protobuf/descriptor_pb';
import * as base64 from 'base64-js';
// Generated proto imports added during build
// TEMPLATE_proto_imports
-const MODULE_MAP = new ModuleMap([
+const MODULE_MAP = {
// TEMPLATE_module_map
-]);
+};
const DESCRIPTOR_BASE64_BINARY = '{TEMPLATE_descriptor_binary}';
@@ -36,7 +33,7 @@ const DESCRIPTOR_BASE64_BINARY = '{TEMPLATE_descriptor_binary}';
export class ProtoCollection extends Base {
constructor() {
const fileDescriptorSet = FileDescriptorSet.deserializeBinary(
- base64.toByteArray(DESCRIPTOR_BASE64_BINARY)
+ base64.toByteArray(DESCRIPTOR_BASE64_BINARY),
);
super(fileDescriptorSet, MODULE_MAP);
}
diff --git a/pw_protobuf_compiler/ts/ts_proto_collection_test.ts b/pw_protobuf_compiler/ts/ts_proto_collection_test.ts
index d7206c027..87a801184 100644
--- a/pw_protobuf_compiler/ts/ts_proto_collection_test.ts
+++ b/pw_protobuf_compiler/ts/ts_proto_collection_test.ts
@@ -14,9 +14,9 @@
/* eslint-env browser */
-import {Message} from 'pigweedjs/protos/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test_pb';
+import { Message } from 'pigweedjs/protos/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test_pb';
-import {ProtoCollection} from 'pigweedjs/protos/collection';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
describe('ProtoCollection', () => {
it('getMessageType returns message', () => {
@@ -39,7 +39,7 @@ describe('ProtoCollection', () => {
const lib = new ProtoCollection();
const fetched = lib.getDescriptorProto('pw.protobuf_compiler.test.Message');
- expect(fetched.getFieldList()[0].getName()).toEqual("field");
+ expect(fetched.getFieldList()[0].getName()).toEqual('field');
});
it('getDescriptorProto for invalid identifier returns undefined', () => {
diff --git a/pw_random/BUILD.gn b/pw_random/BUILD.gn
index 0563fc2e2..afc460b23 100644
--- a/pw_random/BUILD.gn
+++ b/pw_random/BUILD.gn
@@ -41,19 +41,17 @@ pw_source_set("fuzzer_generator") {
public = [ "public/pw_random/fuzzer.h" ]
public_deps = [
":pw_random",
- dir_pw_fuzzer,
+ "$dir_pw_fuzzer:libfuzzer",
]
}
pw_test_group("tests") {
- tests = [
- ":xor_shift_star_test",
- ":get_int_bounded_fuzzer_test",
- ]
+ tests = [ ":xor_shift_star_test" ]
+ group_deps = [ ":fuzzers" ]
}
-group("fuzzers") {
- deps = [ ":get_int_bounded_fuzzer" ]
+pw_fuzzer_group("fuzzers") {
+ fuzzers = [ ":get_int_bounded_fuzzer" ]
}
pw_test("xor_shift_star_test") {
diff --git a/pw_random/docs.rst b/pw_random/docs.rst
index c6b5fe260..2db14e062 100644
--- a/pw_random/docs.rst
+++ b/pw_random/docs.rst
@@ -1,8 +1,8 @@
.. _module-pw_random:
----------
+=========
pw_random
----------
+=========
Pigweed's ``pw_random`` module provides a generic interface for random number
generators, as well as some practical embedded-friendly implementations. While
this module does not provide drivers for hardware random number generators, it
@@ -17,8 +17,9 @@ not be available. Even if RNG hardware is present, it might not always be active
or accessible. ``pw_random`` provides libraries that make these situations
easier to manage.
+---------------------
Using RandomGenerator
-=====================
+---------------------
There's two sides to a RandomGenerator; the input, and the output. The outputs
are relatively straightforward; ``GetInt(T&)`` randomizes the passed integer
reference, ``GetInt(T&, T exclusive_upper_bound)`` produces a random integer
@@ -34,31 +35,15 @@ few bits of noise from ADCs or other highly variable inputs can be accumulated
in a RandomGenerator over time to improve randomness. Such an approach might
not be sufficient for security, but it could help for less strict uses.
-Algorithms
-==========
-xorshift*
----------
-The ``xorshift*`` algorithm is a pseudo-random number generation algorithm. It's
-very simple in principle; the state is represented as an integer that, with each
-generation, performs exclusive OR operations on different left/right bit shifts
-of itself. The "*" refers to a final multiplication that is applied to the
-output value.
-
-Pigweed's implementation augments this with an ability to inject entropy to
-reseed the generator throughout its lifetime. When entropy is injected, the
-results of the generator are no longer completely deterministic based on the
-original seed.
-
-Note that this generator is NOT cryptographically secure.
-
-For more information, see:
-
- * https://en.wikipedia.org/wiki/Xorshift
- * https://www.jstatsoft.org/article/view/v008i14
- * http://vigna.di.unimi.it/ftp/papers/xorshift.pdf
+-------------
+API reference
+-------------
+.. doxygennamespace:: pw::random
+ :members:
+-----------
Future Work
-===========
+-----------
A simple "entropy pool" implementation could buffer incoming entropy later use
instead of requiring an application to directly poll the hardware RNG peripheral
when the random data is needed. This would let a device collect entropy when
diff --git a/pw_random/public/pw_random/random.h b/pw_random/public/pw_random/random.h
index 4be398c05..884d8d22c 100644
--- a/pw_random/public/pw_random/random.h
+++ b/pw_random/public/pw_random/random.h
@@ -25,12 +25,12 @@
namespace pw::random {
-// A random generator uses injected entropy to generate random values. Many of
-// the guarantees for this interface are provided at the level of the
-// implementations. In general:
-// * DO assume a generator will always succeed.
-// * DO NOT assume a generator is cryptographically secure.
-// * DO NOT assume uniformity of generated data.
+/// A random generator uses injected entropy to generate random values. Many of
+/// the guarantees for this interface are provided at the level of the
+/// implementations. In general:
+/// * DO assume a generator will always succeed.
+/// * DO NOT assume a generator is cryptographically secure.
+/// * DO NOT assume uniformity of generated data.
class RandomGenerator {
public:
virtual ~RandomGenerator() = default;
@@ -42,11 +42,18 @@ class RandomGenerator {
Get({reinterpret_cast<std::byte*>(&dest), sizeof(T)});
}
- // Calculate a uniformly distributed random number in the range [0,
- // exclusive_upper_bound). This avoids modulo biasing. Uniformity is only
- // guaranteed if the underlying generator generates uniform data. Uniformity
- // is achieved by generating new random numbers until one is generated in the
- // desired range (with optimizations).
+ /// Calculates a uniformly distributed random number in the range
+ /// `[0, exclusive_upper_bound)`.
+ ///
+ /// This avoids modulo biasing. Uniformity is only guaranteed if the
+ /// underlying generator generates uniform data. Uniformity is achieved by
+ /// generating new random numbers until one is generated in the desired
+ /// range (with optimizations).
+ ///
+ /// @param[out] dest The destination to populate the random number into.
+ ///
+ /// @param[in] exclusive_upper_bound The largest number that can be
+ /// populated into `dest`, exclusive.
template <class T>
void GetInt(T& dest, const T& exclusive_upper_bound) {
static_assert(std::is_unsigned_v<T>, "T must be an unsigned integer");
@@ -77,15 +84,20 @@ class RandomGenerator {
}
}
- // Populates the destination buffer with a randomly generated value.
+ /// Populates the destination buffer with a randomly generated value.
+ ///
+ /// @param[out] dest The destination buffer.
virtual void Get(ByteSpan dest) = 0;
- // Injects entropy into the pool. `data` may have up to 32 bits of random
- // entropy. If the number of bits of entropy is less than 32, entropy is
- // assumed to be stored in the least significant bits of `data`.
+ /// Injects entropy into the pool.
+ ///
+ /// @param[in] data Up to 32 bits of random entropy data.
+ ///
+ /// @param[in] num_bits The number of bits of entropy. If less than `32`,
+ /// entropy is assumed to be stored in the least significant bits of `data`.
virtual void InjectEntropyBits(uint32_t data, uint_fast8_t num_bits) = 0;
- // Injects entropy into the pool byte-by-byte.
+ /// Injects entropy into the pool byte-by-byte.
void InjectEntropy(ConstByteSpan data) {
for (std::byte b : data) {
InjectEntropyBits(std::to_integer<uint32_t>(b), /*num_bits=*/8);
diff --git a/pw_random/public/pw_random/xor_shift.h b/pw_random/public/pw_random/xor_shift.h
index 2e98e9b65..a748a20eb 100644
--- a/pw_random/public/pw_random/xor_shift.h
+++ b/pw_random/public/pw_random/xor_shift.h
@@ -24,20 +24,35 @@
namespace pw::random {
-// This is the "xorshift*" algorithm which is a bit stronger than plain XOR
-// shift thanks to the nonlinear transformation at the end (multiplication).
-//
-// See: https://en.wikipedia.org/wiki/Xorshift
-//
-// This random generator is NOT cryptographically secure, and incorporates
-// pseudo-random generation to extrapolate any true injected entropy. The
-// distribution is not guaranteed to be uniform.
+/// A random generator based off the
+/// [xorshift*](https://en.wikipedia.org/wiki/Xorshift) algorithm.
+///
+/// The state is represented as an integer that, with each generation, performs
+/// exclusive OR (XOR) operations on different left/right bit shifts of itself.
+/// The `*` in `xorshift*` refers to a final multiplication that is applied to
+/// the output value. The final multiplication is essentially a nonlinear
+/// transformation that makes the algorithm stronger than a plain XOR shift.
+///
+/// Pigweed's implementation augments `xorshift*` with an ability to inject
+/// entropy to reseed the generator throughout its lifetime. When entropy is
+/// injected, the results of the generator are no longer completely
+/// deterministic based on the original seed.
+///
+/// See also [Xorshift RNGs](https://www.jstatsoft.org/article/view/v008i14)
+/// and [An experimental exploration of Marsaglia's xorshift generators,
+/// scrambled](https://vigna.di.unimi.it/ftp/papers/xorshift.pdf).
+///
+/// @warning This random generator is **NOT** cryptographically secure. It
+/// incorporates pseudo-random generation to extrapolate any true injected
+/// entropy. The distribution is not guaranteed to be uniform.
class XorShiftStarRng64 : public RandomGenerator {
public:
XorShiftStarRng64(uint64_t initial_seed) : state_(initial_seed) {}
- // This generator uses entropy-seeded PRNG to never exhaust its random number
- // pool.
+ /// Populates the destination buffer with a randomly generated value.
+ ///
+ /// This generator uses entropy-seeded PRNG to never exhaust its random
+ /// number pool.
void Get(ByteSpan dest) final {
while (!dest.empty()) {
uint64_t random = Regenerate();
@@ -47,10 +62,11 @@ class XorShiftStarRng64 : public RandomGenerator {
}
}
- // Entropy is injected by rotating the state by the number of entropy bits
- // before xoring the entropy with the current state. This ensures seeding
- // the random value with single bits will progressively fill the state with
- // more entropy.
+ /// Injects entropy by rotating the state by the number of entropy bits
+ /// before XORing the entropy with the current state.
+ ///
+ /// This technique ensures that seeding the random value with single bits
+ /// will progressively fill the state with more entropy.
void InjectEntropyBits(uint32_t data, uint_fast8_t num_bits) final {
if (num_bits == 0) {
return;
diff --git a/pw_result/BUILD.bazel b/pw_result/BUILD.bazel
index e5b05c2b2..522170f3b 100644
--- a/pw_result/BUILD.bazel
+++ b/pw_result/BUILD.bazel
@@ -28,11 +28,18 @@ pw_cc_library(
hdrs = ["public/pw_result/result.h"],
includes = ["public"],
deps = [
- "//pw_assert:facade",
+ "//pw_assert",
"//pw_status",
],
)
+pw_cc_library(
+ name = "expected",
+ srcs = ["public/pw_result/internal/expected_impl.h"],
+ hdrs = ["public/pw_result/expected.h"],
+ includes = ["public"],
+)
+
pw_cc_test(
name = "result_test",
srcs = ["result_test.cc"],
@@ -44,6 +51,15 @@ pw_cc_test(
)
pw_cc_test(
+ name = "expected_test",
+ srcs = ["expected_test.cc"],
+ deps = [
+ ":expected",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "statusor_test",
srcs = ["statusor_test.cc"],
deps = [
diff --git a/pw_result/BUILD.gn b/pw_result/BUILD.gn
index 8bb7ebdbc..44dfc04bf 100644
--- a/pw_result/BUILD.gn
+++ b/pw_result/BUILD.gn
@@ -35,7 +35,10 @@ pw_source_set("pw_result") {
}
pw_test_group("tests") {
- tests = [ ":result_test" ]
+ tests = [
+ ":result_test",
+ ":expected_test",
+ ]
}
pw_test("result_test") {
@@ -49,6 +52,17 @@ pw_test("result_test") {
]
}
+pw_source_set("expected") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_result/expected.h" ]
+ sources = [ "public/pw_result/internal/expected_impl.h" ]
+}
+
+pw_test("expected_test") {
+ deps = [ ":expected" ]
+ sources = [ "expected_test.cc" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
report_deps = [ ":result_size" ]
diff --git a/pw_result/CMakeLists.txt b/pw_result/CMakeLists.txt
index 7f1aa9bce..0228bdea5 100644
--- a/pw_result/CMakeLists.txt
+++ b/pw_result/CMakeLists.txt
@@ -24,9 +24,13 @@ pw_add_library(pw_result INTERFACE
pw_status
pw_preprocessor
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RESULT)
- zephyr_link_libraries(pw_result)
-endif()
+
+pw_add_library(pw_result.expected INTERFACE
+ HEADERS
+ public/pw_result/expected.h
+ PUBLIC_INCLUDES
+ public
+)
pw_add_test(pw_result.result_test
SOURCES
@@ -38,3 +42,13 @@ pw_add_test(pw_result.result_test
modules
pw_result
)
+
+pw_add_test(pw_result.expected_test
+ SOURCES
+ expected_test.cc
+ PRIVATE_DEPS
+ pw_result.expected
+ GROUPS
+ modules
+ pw_result
+)
diff --git a/pw_result/Kconfig b/pw_result/Kconfig
index 76e3b2197..1872b120b 100644
--- a/pw_result/Kconfig
+++ b/pw_result/Kconfig
@@ -12,7 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_result"
+
config PIGWEED_RESULT
- bool "Enable Pigweed result library (pw_result)"
+ bool "Link pw_result library"
select PIGWEED_ASSERT
select PIGWEED_STATUS
+ help
+ See :ref:`module-pw_result` for module details.
+
+endmenu
diff --git a/pw_result/docs.rst b/pw_result/docs.rst
index fa4aa9ec1..070726cd6 100644
--- a/pw_result/docs.rst
+++ b/pw_result/docs.rst
@@ -229,6 +229,17 @@ should be aware that if they provide a function that returns a ``pw::Result`` to
Result<int> x = ConvertStringToInteger("42")
.transform(MultiplyByTwo);
+-------------
+pw::expected
+-------------
+This module also includes the ``pw::expected`` type, which is either an alias
+for ``std::expected`` or a polyfill for that type if it is not available. This
+type has a similar use case to ``pw::Result``, in that it either returns a type
+``T`` or an error, but the error may be any type ``E``, not just ``pw::Status``.
+The ``PW_TRY`` and ``PW_TRY_ASSIGN`` macros do not work with ``pw::expected``
+but it should be usable in any place that ``std::expected`` from the ``C++23``
+standard could be used.
+
-----------
Size report
-----------
diff --git a/pw_result/expected_test.cc b/pw_result/expected_test.cc
new file mode 100644
index 000000000..f002fe6d8
--- /dev/null
+++ b/pw_result/expected_test.cc
@@ -0,0 +1,249 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_result/expected.h"
+
+#include "gtest/gtest.h"
+
+namespace pw {
+namespace {
+
+struct Defaults {
+ Defaults() = default;
+ Defaults(const Defaults&) = default;
+ Defaults(Defaults&&) = default;
+ Defaults& operator=(const Defaults&) = default;
+ Defaults& operator=(Defaults&&) = default;
+};
+
+struct NoDefaultConstructor {
+ NoDefaultConstructor() = delete;
+ NoDefaultConstructor(std::nullptr_t) {}
+};
+
+struct NoCopy {
+ NoCopy(const NoCopy&) = delete;
+ NoCopy(NoCopy&&) = default;
+ NoCopy& operator=(const NoCopy&) = delete;
+ NoCopy& operator=(NoCopy&&) = default;
+};
+
+struct NoCopyNoMove {
+ NoCopyNoMove(const NoCopyNoMove&) = delete;
+ NoCopyNoMove(NoCopyNoMove&&) = delete;
+ NoCopyNoMove& operator=(const NoCopyNoMove&) = delete;
+ NoCopyNoMove& operator=(NoCopyNoMove&&) = delete;
+};
+
+struct NonTrivialDestructor {
+ ~NonTrivialDestructor() {}
+};
+
+namespace test_constexpr {
+// Expected and unexpected are constexpr types.
+constexpr expected<int, int> kExpectedConstexpr1;
+constexpr expected<int, int> kExpectedConstexpr2{5};
+constexpr expected<int, int> kExpectedConstexpr3 = unexpected<int>(42);
+constexpr unexpected<int> kExpectedConstexprUnexpected{50};
+static_assert(kExpectedConstexpr1.has_value());
+static_assert(kExpectedConstexpr1.value() == 0);
+static_assert(kExpectedConstexpr2.has_value());
+static_assert(kExpectedConstexpr2.value() == 5);
+static_assert(!kExpectedConstexpr3.has_value());
+static_assert(kExpectedConstexpr3.error() == 42);
+static_assert(kExpectedConstexprUnexpected.error() == 50);
+} // namespace test_constexpr
+
+namespace test_default_construction {
+// Default constructible if and only if T is default constructible.
+static_assert(
+ std::is_default_constructible<expected<Defaults, Defaults>>::value);
+static_assert(!std::is_default_constructible<
+ expected<NoDefaultConstructor, Defaults>>::value);
+static_assert(std::is_default_constructible<
+ expected<Defaults, NoDefaultConstructor>>::value);
+static_assert(!std::is_default_constructible<
+ expected<NoDefaultConstructor, NoDefaultConstructor>>::value);
+// Never default constructible.
+static_assert(!std::is_default_constructible<unexpected<Defaults>>::value);
+static_assert(
+ !std::is_default_constructible<unexpected<NoDefaultConstructor>>::value);
+} // namespace test_default_construction
+
+namespace test_copy_construction {
+// Copy constructible if and only if both types are copy constructible.
+static_assert(std::is_copy_constructible<expected<Defaults, Defaults>>::value);
+static_assert(!std::is_copy_constructible<expected<Defaults, NoCopy>>::value);
+static_assert(!std::is_copy_constructible<expected<NoCopy, Defaults>>::value);
+static_assert(!std::is_copy_constructible<expected<NoCopy, NoCopy>>::value);
+// Copy constructible if and only if E is copy constructible.
+static_assert(std::is_copy_constructible<unexpected<Defaults>>::value);
+static_assert(!std::is_copy_constructible<unexpected<NoCopy>>::value);
+} // namespace test_copy_construction
+
+namespace test_copy_assignment {
+// Copy assignable if and only if both types are copy assignable.
+static_assert(std::is_copy_assignable<expected<Defaults, Defaults>>::value);
+static_assert(!std::is_copy_assignable<expected<Defaults, NoCopy>>::value);
+static_assert(!std::is_copy_assignable<expected<NoCopy, Defaults>>::value);
+static_assert(!std::is_copy_assignable<expected<NoCopy, NoCopy>>::value);
+// Copy assignable if and only if E is copy assignable.
+static_assert(std::is_copy_assignable<unexpected<Defaults>>::value);
+static_assert(!std::is_copy_assignable<unexpected<NoCopy>>::value);
+} // namespace test_copy_assignment
+
+namespace test_move_construction {
+// Move constructible if and only if both types are move constructible.
+static_assert(std::is_move_constructible<expected<Defaults, Defaults>>::value);
+static_assert(
+ !std::is_move_constructible<expected<Defaults, NoCopyNoMove>>::value);
+static_assert(
+ !std::is_move_constructible<expected<NoCopyNoMove, Defaults>>::value);
+static_assert(
+ !std::is_move_constructible<expected<NoCopyNoMove, NoCopyNoMove>>::value);
+// Move constructible if and only if E is move constructible.
+static_assert(std::is_move_constructible<unexpected<Defaults>>::value);
+static_assert(!std::is_move_constructible<unexpected<NoCopyNoMove>>::value);
+} // namespace test_move_construction
+
+namespace test_move_assignment {
+// Move assignable if and only if both types are move assignable.
+static_assert(std::is_move_assignable<expected<Defaults, Defaults>>::value);
+static_assert(
+ !std::is_move_assignable<expected<Defaults, NoCopyNoMove>>::value);
+static_assert(
+ !std::is_move_assignable<expected<NoCopyNoMove, Defaults>>::value);
+static_assert(
+ !std::is_move_assignable<expected<NoCopyNoMove, NoCopyNoMove>>::value);
+// Move assignable if and only if E is move assignable.
+static_assert(std::is_move_assignable<unexpected<Defaults>>::value);
+static_assert(!std::is_move_assignable<unexpected<NoCopyNoMove>>::value);
+} // namespace test_move_assignment
+
+namespace test_trivial_destructor {
+// Destructor is trivial if and only if both types are trivially destructible.
+static_assert(
+ std::is_trivially_destructible<expected<Defaults, Defaults>>::value);
+static_assert(!std::is_trivially_destructible<
+ expected<NonTrivialDestructor, Defaults>>::value);
+static_assert(!std::is_trivially_destructible<
+ expected<Defaults, NonTrivialDestructor>>::value);
+static_assert(!std::is_trivially_destructible<
+ expected<NonTrivialDestructor, NonTrivialDestructor>>::value);
+// Destructor is trivial if and only if E is trivially destructible.
+static_assert(std::is_trivially_destructible<unexpected<Defaults>>::value);
+static_assert(
+ !std::is_trivially_destructible<unexpected<NonTrivialDestructor>>::value);
+} // namespace test_trivial_destructor
+
+expected<int, const char*> FailableFunction1(bool fail, int num) {
+ if (fail) {
+ return unexpected<const char*>("FailableFunction1");
+ }
+ return num;
+}
+
+expected<std::string, const char*> FailableFunction2(bool fail, int num) {
+ if (fail) {
+ return unexpected<const char*>("FailableFunction2");
+ }
+ return std::to_string(num);
+}
+
+expected<int, const char*> FailOnOdd(int x) {
+ if (x % 2) {
+ return unexpected<const char*>("odd");
+ }
+ return x;
+}
+
+expected<std::string, const char*> ItoaFailOnNegative(int x) {
+ if (x < 0) {
+ return unexpected<const char*>("negative");
+ }
+ return std::to_string(x);
+}
+
+expected<char, const char*> GetSecondChar(const std::string& s) {
+ if (s.size() < 2) {
+ return unexpected<const char*>("string too small");
+ }
+ return s[1];
+}
+
+int Decrement(int x) { return x - 1; }
+
+template <class T, class E>
+expected<void, E> Consume(const expected<T, E>& e) {
+ return e.transform([](auto) {});
+}
+
+TEST(ExpectedTest, HoldIntValueSuccess) {
+ auto x = FailableFunction1(false, 10);
+ ASSERT_TRUE(x.has_value());
+ EXPECT_EQ(x.value(), 10);
+ EXPECT_EQ(*x, 10);
+ EXPECT_EQ(x.value_or(33), 10);
+ EXPECT_EQ(x.error_or("no error"), std::string("no error"));
+}
+
+TEST(ExpectedTest, HoldIntValueFail) {
+ auto x = FailableFunction1(true, 10);
+ ASSERT_FALSE(x.has_value());
+ EXPECT_EQ(x.error(), std::string("FailableFunction1"));
+ EXPECT_EQ(x.value_or(33), 33);
+ EXPECT_EQ(x.error_or("no error"), std::string("FailableFunction1"));
+}
+
+TEST(ExpectedTest, HoldStringValueSuccess) {
+ auto x = FailableFunction2(false, 42);
+ ASSERT_TRUE(x.has_value());
+ EXPECT_EQ(x.value(), std::string("42"));
+ EXPECT_EQ(*x, std::string("42"));
+ EXPECT_EQ(x.value_or("33"), std::string("42"));
+ EXPECT_EQ(x.error_or("no error"), std::string("no error"));
+}
+
+TEST(ExpectedTest, HoldStringValueFail) {
+ auto x = FailableFunction2(true, 42);
+ ASSERT_FALSE(x.has_value());
+ EXPECT_EQ(x.error(), std::string("FailableFunction2"));
+ EXPECT_EQ(x.value_or("33"), std::string("33"));
+ EXPECT_EQ(x.error_or("no error"), std::string("FailableFunction2"));
+}
+
+TEST(ExpectedTest, MonadicOperation) {
+ auto f = [](expected<int, const char*> value) {
+ return value.and_then(FailOnOdd)
+ .transform(Decrement)
+ .transform(Decrement)
+ .and_then(ItoaFailOnNegative)
+ .and_then(GetSecondChar);
+ };
+ EXPECT_EQ(f(26).value_or(0), '4');
+ EXPECT_EQ(f(26).error_or(nullptr), nullptr);
+ EXPECT_EQ(f(25).value_or(0), 0);
+ EXPECT_EQ(f(25).error_or(nullptr), std::string("odd"));
+ EXPECT_EQ(f(0).value_or(0), 0);
+ EXPECT_EQ(f(0).error_or(nullptr), std::string("negative"));
+ EXPECT_EQ(f(4).value_or(0), 0);
+ EXPECT_EQ(f(4).error_or(nullptr), std::string("string too small"));
+ EXPECT_TRUE(Consume(f(26)).has_value());
+ EXPECT_EQ(Consume(f(25)).error_or(nullptr), std::string("odd"));
+ EXPECT_EQ(Consume(f(0)).error_or(nullptr), std::string("negative"));
+ EXPECT_EQ(Consume(f(4)).error_or(nullptr), std::string("string too small"));
+}
+
+} // namespace
+} // namespace pw
diff --git a/pw_result/public/pw_result/expected.h b/pw_result/public/pw_result/expected.h
new file mode 100644
index 000000000..adc258555
--- /dev/null
+++ b/pw_result/public/pw_result/expected.h
@@ -0,0 +1,48 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#if __has_include(<version>)
+#include <version>
+#endif
+
+#if defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L
+// No polyfill for <expected>.
+
+#include <expected>
+
+namespace pw {
+
+using std::expected;
+using std::unexpect;
+using std::unexpect_t;
+using std::unexpected;
+
+} // namespace pw
+
+#else
+// Provide polyfill for <expected>.
+
+#include "pw_result/internal/expected_impl.h"
+
+namespace pw {
+
+using internal::expected;
+using internal::unexpect;
+using internal::unexpect_t;
+using internal::unexpected;
+
+} // namespace pw
+
+#endif // defined(__cpp_lib_expected) && __cpp_lib_expected >= 202202L
diff --git a/pw_result/public/pw_result/internal/expected_impl.h b/pw_result/public/pw_result/internal/expected_impl.h
new file mode 100644
index 000000000..fe97010da
--- /dev/null
+++ b/pw_result/public/pw_result/internal/expected_impl.h
@@ -0,0 +1,1011 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <functional>
+#include <optional>
+#include <type_traits>
+#include <utility>
+#include <variant>
+
+namespace pw::internal {
+
+// Helper type trait for removing reference and cv-qualification of a type.
+template <class T>
+struct remove_cvref {
+ typedef std::remove_cv_t<std::remove_reference_t<T>> type;
+};
+
+// Helper type trait ::type of above struct.
+template <class T>
+using remove_cvref_t = typename remove_cvref<T>::type;
+
+// Helper type trait for enabling/disabling std::expected constructors.
+template <class T, class W>
+constexpr bool converts_from_any_cvref =
+ std::disjunction_v<std::is_constructible<T, W&>,
+ std::is_convertible<W&, T>,
+ std::is_constructible<T, W>,
+ std::is_convertible<W, T>,
+ std::is_constructible<T, const W&>,
+ std::is_convertible<const W&, T>,
+ std::is_constructible<T, const W>,
+ std::is_convertible<const W, T>>;
+
+// Helper type trait for determining if a type is any specialization of a
+// template class.
+template <typename T, template <typename...> class Template>
+constexpr bool is_specialization = false;
+template <template <typename...> class Template, typename... Ts>
+constexpr bool is_specialization<Template<Ts...>, Template> = true;
+
+// Polyfill implementaion of std::unexpected.
+
+template <class E>
+class unexpected {
+ public:
+ constexpr unexpected(const unexpected&) = default;
+ constexpr unexpected(unexpected&&) = default;
+ template <class Err = E,
+ std::enable_if_t<
+ !std::is_same_v<remove_cvref_t<Err>, unexpected> &&
+ !std::is_same_v<remove_cvref_t<Err>, std::in_place_t> &&
+ std::is_constructible_v<E, Err>,
+ bool> = true>
+ constexpr explicit unexpected(Err&& e) : unex_(std::forward<Err>(e)) {}
+ template <class... Args,
+ std::enable_if_t<std::is_constructible_v<E, Args...>, bool> = true>
+ constexpr explicit unexpected(std::in_place_t, Args&&... args)
+ : unex_(std::forward<Args>(args)...) {}
+ template <
+ class U,
+ class... Args,
+ std::enable_if_t<std::is_constructible_v<E, std::initializer_list<U>>,
+ bool> = true>
+ constexpr explicit unexpected(std::in_place_t,
+ std::initializer_list<U> il,
+ Args&&... args)
+ : unex_(il, std::forward<Args>(args)...) {}
+
+ constexpr unexpected& operator=(const unexpected&) = default;
+ constexpr unexpected& operator=(unexpected&&) = default;
+
+ constexpr const E& error() const& noexcept { return unex_; }
+ constexpr E& error() & noexcept { return unex_; }
+ constexpr E&& error() && noexcept { return std::move(unex_); }
+ constexpr const E&& error() const&& noexcept { return std::move(unex_); }
+
+ constexpr void swap(unexpected& other) noexcept(
+ std::is_nothrow_swappable<E>::value) {
+ std::swap(unex_, other.unex_);
+ }
+
+ template <class E2>
+ friend constexpr bool operator==(const unexpected& x,
+ const unexpected<E2>& y) {
+ return x.error() == y.error();
+ }
+
+ private:
+ E unex_;
+};
+
+// Polyfill implementation of std::unexpect_t and std::unexpect.
+struct unexpect_t {
+ explicit unexpect_t() = default;
+};
+
+inline constexpr unexpect_t unexpect;
+
+template <class T, class E, typename Enable = void>
+class expected;
+
+// Polyfill implementation of std::expected.
+template <class T, class E>
+class expected<T, E, std::enable_if_t<!std::is_void_v<T>>> {
+ public:
+ using value_type = T;
+ using error_type = E;
+ using unexpected_type = unexpected<E>;
+
+ template <class U>
+ using rebind = expected<U, error_type>;
+
+ template <
+ class Enable = T,
+ std::enable_if_t<std::is_default_constructible_v<Enable>, bool> = true>
+ constexpr expected() : contents_(kInPlaceValue) {}
+ constexpr expected(const expected& rhs) = default;
+ constexpr expected(expected&& rhs) = default;
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_constructible_v<T, const U&> &&
+ std::is_constructible_v<E, const G&> &&
+ !converts_from_any_cvref<T, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<const U&, T> ||
+ !std::is_convertible_v<const G&, E>,
+ bool> = true>
+ constexpr explicit expected(const expected<U, G>& rhs)
+ : contents_(convert_variant(
+ std::forward<const std::variant<U, G>&>(rhs.contents_))) {}
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_constructible_v<T, const U&> &&
+ std::is_constructible_v<E, const G&> &&
+ !converts_from_any_cvref<T, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<const U&, T> &&
+ std::is_convertible_v<const G&, E>,
+ bool> = true>
+ constexpr /* implicit */ expected(const expected<U, G>& rhs)
+ : contents_(convert_variant(
+ std::forward<const std::variant<U, G>&>(rhs.contents_))) {}
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_constructible_v<T, U&&> && std::is_constructible_v<E, G&&> &&
+ !converts_from_any_cvref<T, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<U&&, T> ||
+ !std::is_convertible_v<G&&, E>,
+ bool> = true>
+ constexpr explicit expected(expected<U, G>&& rhs)
+ : contents_(
+ std::forward<std::variant<U, G>>(convert_variant(rhs.contents_))) {}
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_constructible_v<T, U&&> && std::is_constructible_v<E, G&&> &&
+ !converts_from_any_cvref<T, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<U&&, T> &&
+ std::is_convertible_v<G&&, E>,
+ bool> = true>
+ constexpr /* implicit */ expected(expected<U, G>&& rhs)
+ : contents_(
+ convert_variant(std::forward<std::variant<U, G>>(rhs.contents_))) {}
+ template <
+ class U = T,
+ // Constraints
+ std::enable_if_t<!std::is_same_v<remove_cvref_t<U>, std::in_place_t> &&
+ !std::is_same_v<remove_cvref_t<U>, expected> &&
+ !is_specialization<U, unexpected> &&
+ std::is_constructible_v<T, U>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<U, T>, bool> = true>
+ constexpr explicit expected(U&& u)
+ : contents_(kInPlaceValue, std::forward<U>(u)) {}
+ template <
+ class U = T,
+ // Constraints
+ std::enable_if_t<!std::is_same_v<remove_cvref_t<U>, std::in_place_t> &&
+ !std::is_same_v<remove_cvref_t<U>, expected> &&
+ !is_specialization<U, unexpected> &&
+ std::is_constructible_v<T, U>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<U, T>, bool> = true>
+ constexpr /* implicit */ expected(U&& u)
+ : contents_(kInPlaceValue, std::forward<U>(u)) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, const G&>, bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<const G&, E>, bool> = true>
+ constexpr explicit expected(const unexpected<G>& e)
+ : contents_(kInPlaceError, std::forward<const G&>(e.error())) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, const G&>, bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<const G&, E>, bool> = true>
+ constexpr /* implicit */ expected(const unexpected<G>& e)
+ : contents_(kInPlaceError, std::forward<const G&>(e.error())) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, G>, bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<G, E>, bool> = true>
+ constexpr explicit expected(unexpected<G>&& e)
+ : contents_(kInPlaceError, std::forward<G>(e.error())) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, G>, bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<G, E>, bool> = true>
+ constexpr /* implicit */ expected(unexpected<G>&& e)
+ : contents_(kInPlaceError, std::forward<G>(e.error())) {}
+ template <class... Args,
+ std::enable_if_t<std::is_constructible_v<T, Args...>, bool> = true>
+ constexpr explicit expected(std::in_place_t, Args&&... args)
+ : contents_(kInPlaceValue, std::forward<Args>(args)...) {}
+ template <class U,
+ class... Args,
+ std::enable_if_t<
+ std::is_constructible_v<T, std::initializer_list<U>&, Args...>,
+ bool> = true>
+ constexpr explicit expected(std::in_place_t,
+ std::initializer_list<U> il,
+ Args&&... args)
+ : contents_(kInPlaceValue, il, std::forward<Args>(args)...) {}
+ template <class... Args,
+ std::enable_if_t<std::is_constructible_v<E, Args...>, bool> = true>
+ constexpr explicit expected(unexpect_t, Args&&... args)
+ : contents_(kInPlaceError, std::forward<Args>(args)...) {}
+ template <class U,
+ class... Args,
+ std::enable_if_t<
+ std::is_constructible_v<E, std::initializer_list<U>&, Args...>,
+ bool> = true>
+ constexpr explicit expected(unexpect_t,
+ std::initializer_list<U> il,
+ Args&&... args)
+ : contents_(kInPlaceError, il, std::forward<Args>(args)...) {}
+
+ constexpr expected& operator=(const expected& rhs) = default;
+ constexpr expected& operator=(expected&& rhs) = default;
+ template <
+ class U = T,
+ std::enable_if_t<!std::is_same_v<expected, remove_cvref_t<U>> &&
+ !is_specialization<remove_cvref_t<U>, unexpected>,
+ bool> = true>
+ constexpr expected& operator=(U&& u) {
+ value() = std::forward<U>(u);
+ }
+ template <class G>
+ constexpr expected& operator=(const unexpected<G>& e) {
+ error() = std::forward<const G&>(e.error());
+ }
+ template <class G>
+ constexpr expected& operator=(unexpected<G>&& e) {
+ error() = std::forward<G>(e.error());
+ }
+
+ template <class... Args,
+ std::enable_if_t<std::is_nothrow_constructible_v<T, Args...>,
+ bool> = true>
+ constexpr T& emplace(Args&&... args) noexcept {
+ return contents_.template emplace<kValue>(std::forward<Args>(args)...);
+ }
+ template <
+ class U,
+ class... Args,
+ std::enable_if_t<
+ std::is_nothrow_constructible_v<T, std::initializer_list<U>, Args...>,
+ bool> = true>
+ constexpr T& emplace(std::initializer_list<U> il, Args&&... args) noexcept {
+ return contents_.template emplace<kValue>(il, std::forward<Args>(args)...);
+ }
+
+ constexpr void swap(expected& rhs) { std::swap(contents_, rhs.contents_); }
+
+ constexpr T* operator->() noexcept { return std::addressof(value()); }
+ constexpr const T* operator->() const noexcept {
+ return std::addressof(value());
+ }
+ constexpr T& operator*() & noexcept { return value(); }
+ constexpr T&& operator*() && noexcept { return std::move(value()); }
+ constexpr const T& operator*() const& noexcept { return value(); }
+ constexpr const T&& operator*() const&& noexcept {
+ return std::move(value());
+ }
+
+ constexpr explicit operator bool() const noexcept { return has_value(); }
+ constexpr bool has_value() const noexcept {
+ return contents_.index() == kValue;
+ }
+
+ constexpr T& value() & { return std::get<kValue>(contents_); }
+ constexpr T&& value() && { return std::move(std::get<kValue>(contents_)); }
+ constexpr const T& value() const& { return std::get<kValue>(contents_); }
+ constexpr const T&& value() const&& {
+ return std::move(std::get<kValue>(contents_));
+ }
+
+ constexpr E& error() & { return std::get<kError>(contents_); }
+ constexpr E&& error() && { return std::move(std::get<kError>(contents_)); }
+ constexpr const E& error() const& { return std::get<kError>(contents_); }
+ constexpr const E&& error() const&& {
+ return std::move(std::get<kError>(contents_));
+ }
+
+ template <class U>
+ constexpr T value_or(U&& u) && {
+ return has_value() ? std::move(value())
+ : static_cast<T>(std::forward<U>(u));
+ }
+ template <class U>
+ constexpr T value_or(U&& u) const& {
+ return has_value() ? value() : static_cast<T>(std::forward<U>(u));
+ }
+
+ template <class G>
+ constexpr E error_or(G&& g) && {
+ return has_value() ? std::forward<G>(g) : std::move(error());
+ }
+ template <class G>
+ constexpr E error_or(G&& g) const& {
+ return has_value() ? std::forward<G>(g) : error();
+ }
+
+ template <class F>
+ constexpr auto and_then(F&& f) & {
+ using U = remove_cvref_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f), value());
+ } else {
+ return U(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto and_then(F&& f) && {
+ using U = remove_cvref_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f), std::move(value()));
+ } else {
+ return U(unexpect, std::move(error()));
+ }
+ }
+ template <class F>
+ constexpr auto and_then(F&& f) const& {
+ using U = remove_cvref_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f), value());
+ } else {
+ return U(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto and_then(F&& f) const&& {
+ using U = remove_cvref_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f), std::move(value()));
+ } else {
+ return U(unexpect, std::move(error()));
+ }
+ }
+
+ template <class F>
+ constexpr auto or_else(F&& f) & {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G(std::in_place, value());
+ } else {
+ return std::invoke(std::forward<F>(f), error());
+ }
+ }
+ template <class F>
+ constexpr auto or_else(F&& f) && {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G(std::in_place, value());
+ } else {
+ return std::invoke(std::forward<F>(f), std::move(error()));
+ }
+ }
+ template <class F>
+ constexpr auto or_else(F&& f) const& {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G(std::in_place, value());
+ } else {
+ return std::invoke(std::forward<F>(f), error());
+ }
+ }
+ template <class F>
+ constexpr auto or_else(F&& f) const&& {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G(std::in_place, value());
+ } else {
+ return std::invoke(std::forward<F>(f), std::move(error()));
+ }
+ }
+
+ template <class F>
+ constexpr auto transform(F&& f) & {
+ using U = std::remove_cv_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto transform(F&& f) && {
+ using U = std::remove_cv_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, std::move(error()));
+ }
+ }
+ template <class F>
+ constexpr auto transform(F&& f) const& {
+ using U = std::remove_cv_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto transform(F&& f) const&& {
+ using U = std::remove_cv_t<std::invoke_result_t<F, decltype(value())>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, std::move(error()));
+ }
+ }
+
+ template <class F>
+ constexpr auto transform_error(F&& f) & {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>(std::in_place, value());
+ } else {
+ return expected<T, G>(unexpect, std::invoke(std::forward<F>(f), error()));
+ }
+ }
+ template <class F>
+ constexpr auto transform_error(F&& f) && {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>(std::in_place, std::move(value()));
+ } else {
+ return expected<T, G>(
+ unexpect, std::invoke(std::forward<F>(f), std::move(error())));
+ }
+ }
+ template <class F>
+ constexpr auto transform_error(F&& f) const& {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>(std::in_place, value());
+ } else {
+ return expected<T, G>(unexpect, std::invoke(std::forward<F>(f), error()));
+ }
+ }
+ template <class F>
+ constexpr auto transform_error(F&& f) const&& {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>(std::in_place, std::move(value()));
+ } else {
+ return expected<T, G>(
+ unexpect, std::invoke(std::forward<F>(f), std::move(error())));
+ }
+ }
+
+ private:
+ // Make all specializations of `expected` friends.
+ template <class U, class G, class>
+ friend class expected;
+
+ static constexpr size_t kValue = 0;
+ static constexpr size_t kError = 1;
+
+ static constexpr auto kInPlaceValue = std::in_place_index<kValue>;
+ static constexpr auto kInPlaceError = std::in_place_index<kError>;
+
+ // Helper to convert variant<U, G> -> variant<T, E>
+ template <class U, class G>
+ std::variant<T, E> convert_variant(const std::variant<U, G>& v) {
+ switch (v.index()) {
+ case kValue:
+ return std::variant<T, E>(kInPlaceValue, std::get<kValue>(v));
+ case kError:
+ return std::variant<T, E>(kInPlaceError, std::get<kError>(v));
+ default:
+ // Could only happen if valueless_by_exception, which can't be handled
+ // gracefully anyways.
+ std::abort();
+ }
+ }
+
+ // Helper to convert variant<U, G> -> variant<T, E>
+ template <class U, class G>
+ std::variant<T, E> convert_variant(std::variant<U, G>&& v) {
+ switch (v.index()) {
+ case kValue:
+ return std::variant<T, E>(kInPlaceValue,
+ std::forward<U>(std::get<kValue>(v)));
+ case kError:
+ return std::variant<T, E>(kInPlaceError,
+ std::forward<G>(std::get<kError>(v)));
+ default:
+ // Could only happen if valueless_by_exception, which can't be handled
+ // gracefully anyways.
+ std::abort();
+ }
+ }
+
+ // Helper to handle transform correctly for void(Args...) functions, non-void
+ // case.
+ template <class U, class F, std::enable_if_t<!std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) & {
+ return expected<U, E>(std::in_place,
+ std::invoke(std::forward<F>(f), value()));
+ }
+ template <class U, class F, std::enable_if_t<!std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) && {
+ return expected<U, E>(std::in_place,
+ std::invoke(std::forward<F>(f), std::move(value())));
+ }
+ template <class U, class F, std::enable_if_t<!std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) const& {
+ return expected<U, E>(std::in_place,
+ std::invoke(std::forward<F>(f), value()));
+ }
+ template <class U, class F, std::enable_if_t<!std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) const&& {
+ return expected<U, E>(std::in_place,
+ std::invoke(std::forward<F>(f), std::move(value())));
+ }
+
+ // Helper to handle transform correctly for void(Args...) functions, void
+ // case.
+ template <class U, class F, std::enable_if_t<std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) & {
+ std::invoke(std::forward<F>(f), value());
+ return expected<U, E>();
+ }
+ template <class U, class F, std::enable_if_t<std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) && {
+ std::invoke(std::forward<F>(f), std::move(value()));
+ return expected<U, E>();
+ }
+ template <class U, class F, std::enable_if_t<std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) const& {
+ std::invoke(std::forward<F>(f), value());
+ return expected<U, E>();
+ }
+ template <class U, class F, std::enable_if_t<std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) const&& {
+ std::invoke(std::forward<F>(f), std::move(value()));
+ return expected<U, E>();
+ }
+
+ std::variant<T, E> contents_;
+};
+
+template <class T,
+ class E,
+ class U,
+ class G,
+ std::enable_if_t<!std::is_void_v<U>, bool> = true>
+constexpr bool operator==(const expected<T, E>& lhs,
+ const expected<U, G>& rhs) {
+ if (lhs.has_value() != rhs.has_value()) {
+ return false;
+ }
+ if (lhs.has_value()) {
+ return lhs.value() == rhs.value();
+ } else {
+ return lhs.error() == rhs.error();
+ }
+}
+
+template <class T, class E, class U>
+constexpr bool operator==(const expected<T, E>& x, const U& u) {
+ return x.has_value() && static_cast<bool>(*x == u);
+}
+
+template <class T, class E, class G>
+constexpr bool operator==(const expected<T, E>& x, const unexpected<G> e) {
+ return !x.has_value() && static_cast<bool>(x.error() == e.error());
+}
+
+// Polyfill implementation of std::expected<void, ...>
+template <class T, class E>
+class expected<T, E, std::enable_if_t<std::is_void_v<T>>> {
+ public:
+ using value_type = T;
+ using error_type = E;
+ using unexpected_type = unexpected<E>;
+
+ template <class U>
+ using rebind = expected<U, error_type>;
+
+ constexpr expected() noexcept : error_contents_(std::nullopt) {}
+ constexpr expected(const expected& rhs) = default;
+ constexpr expected(expected&& rhs) = default;
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_void_v<U> && std::is_constructible_v<E, const G&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<const G&, E>, bool> = true>
+ constexpr /* implicit */ expected(const expected<U, G>& rhs)
+ : error_contents_(rhs.error_contents_.has_value() ? rhs.error_contents_
+ : std::nullopt) {}
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_void_v<U> && std::is_constructible_v<E, const G&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<const G&, E>, bool> = true>
+ constexpr explicit expected(const expected<U, G>& rhs)
+ : error_contents_(rhs.error_contents_.has_value() ? rhs.error_contents_
+ : std::nullopt) {}
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_void_v<U> && std::is_constructible_v<E, G> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<G, E>, bool> = true>
+ constexpr /* implicit */ expected(expected<U, G>&& rhs)
+ : error_contents_(rhs.error_contents_.has_value()
+ ? std::move(rhs.error_contents_)
+ : std::nullopt) {}
+ template <
+ class U,
+ class G,
+ // Constraints
+ std::enable_if_t<
+ std::is_void_v<U> && std::is_constructible_v<E, G> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, expected<U, G>> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>&> &&
+ !std::is_constructible_v<unexpected<E>, const expected<U, G>>,
+ bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<G, E>, bool> = true>
+ constexpr explicit expected(expected<U, G>&& rhs)
+ : error_contents_(rhs.error_contents_.has_value()
+ ? std::move(rhs.error_contents_)
+ : std::nullopt) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, const G&>, bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<const G&, E>, bool> = true>
+ constexpr explicit expected(const unexpected<G>& e)
+ : error_contents_(std::in_place, std::forward<const G&>(e.error())) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, const G&>, bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<const G&, E>, bool> = true>
+ constexpr /* implicit */ expected(const unexpected<G>& e)
+ : error_contents_(std::in_place, std::forward<const G&>(e.error())) {}
+ template <class... Args,
+ std::enable_if_t<std::is_constructible_v<E, Args...>, bool> = true>
+ constexpr explicit expected(unexpect_t, Args&&... args)
+ : error_contents_(std::in_place, std::forward<Args>(args)...) {}
+ template <class U,
+ class... Args,
+ std::enable_if_t<
+ std::is_constructible_v<E, std::initializer_list<U>&, Args...>,
+ bool> = true>
+ constexpr explicit expected(unexpect_t,
+ std::initializer_list<U> il,
+ Args&&... args)
+ : error_contents_(std::in_place, il, std::forward<Args>(args)...) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, G>, bool> = true,
+ // Explicit
+ std::enable_if_t<!std::is_convertible_v<G, E>, bool> = true>
+ constexpr explicit expected(unexpected<G>&& e)
+ : error_contents_(std::in_place, std::forward<G>(e.error())) {}
+ template <class G,
+ // Constraints
+ std::enable_if_t<std::is_constructible_v<E, G>, bool> = true,
+ // Explicit
+ std::enable_if_t<std::is_convertible_v<G, E>, bool> = true>
+ constexpr /* implicit */ expected(unexpected<G>&& e)
+ : error_contents_(std::in_place, std::forward<G>(e.error())) {}
+
+ constexpr expected& operator=(const expected& rhs) {
+ error_contents_ = rhs.error_contents_;
+ return *this;
+ }
+ constexpr expected& operator=(expected&& rhs) noexcept(
+ std::is_nothrow_move_constructible_v<E> &&
+ std::is_nothrow_move_assignable_v<E>) {
+ error_contents_ = std::move(rhs.error_contents);
+ return *this;
+ }
+ template <class G>
+ constexpr expected& operator=(const unexpected<G>& rhs) {
+ error_contents_ = rhs.error();
+ return *this;
+ }
+ template <class G>
+ constexpr expected& operator=(unexpected<G>&& rhs) {
+ error_contents_ = std::move(rhs.error());
+ return *this;
+ }
+
+ constexpr void swap(expected& rhs) {
+ error_contents_.swap(rhs.error_contents_);
+ }
+
+ constexpr explicit operator bool() const noexcept { return has_value(); }
+ constexpr bool has_value() const noexcept {
+ return !error_contents_.has_value();
+ }
+
+ constexpr void operator*() const noexcept {}
+ constexpr void value() const& {}
+ constexpr void value() && {}
+
+ constexpr E& error() & { return *error_contents_; }
+ constexpr E&& error() && { return std::move(*error_contents_); }
+ constexpr const E& error() const& { return *error_contents_; }
+ constexpr const E&& error() const&& { return std::move(*error_contents_); }
+
+ template <class G = E>
+ constexpr E error_or(G&& g) const& {
+ if (has_value()) {
+ return std::forward<G>(g);
+ } else {
+ return error();
+ }
+ }
+ template <class G = E>
+ constexpr E error_or(G&& g) const&& {
+ if (has_value()) {
+ return std::forward<G>(g);
+ } else {
+ return std::move(error());
+ }
+ }
+
+ template <class F>
+ constexpr auto and_then(F&& f) & {
+ using U = remove_cvref_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f));
+ } else {
+ return U(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto and_then(F&& f) && {
+ using U = remove_cvref_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f));
+ } else {
+ return U(unexpect, std::move(error()));
+ }
+ }
+ template <class F>
+ constexpr auto and_then(F&& f) const& {
+ using U = remove_cvref_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f));
+ } else {
+ return U(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto and_then(F&& f) const&& {
+ using U = remove_cvref_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return std::invoke(std::forward<F>(f));
+ } else {
+ return U(unexpect, std::move(error()));
+ }
+ }
+
+ template <class F>
+ constexpr auto or_else(F&& f) & {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G();
+ } else {
+ return std::invoke(std::forward<F>(f), error());
+ }
+ }
+ template <class F>
+ constexpr auto or_else(F&& f) && {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G();
+ } else {
+ return std::invoke(std::forward<F>(f), std::move(error()));
+ }
+ }
+ template <class F>
+ constexpr auto or_else(F&& f) const& {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G();
+ } else {
+ return std::invoke(std::forward<F>(f), error());
+ }
+ }
+ template <class F>
+ constexpr auto or_else(F&& f) const&& {
+ using G = remove_cvref_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return G();
+ } else {
+ return std::invoke(std::forward<F>(f), std::move(error()));
+ }
+ }
+
+ template <class F>
+ constexpr auto transform(F&& f) & {
+ using U = std::remove_cv_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto transform(F&& f) && {
+ using U = std::remove_cv_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, std::move(error()));
+ }
+ }
+ template <class F>
+ constexpr auto transform(F&& f) const& {
+ using U = std::remove_cv_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, error());
+ }
+ }
+ template <class F>
+ constexpr auto transform(F&& f) const&& {
+ using U = std::remove_cv_t<std::invoke_result_t<F>>;
+ if (has_value()) {
+ return transform_helper<U>(std::forward<F>(f));
+ } else {
+ return expected<U, E>(unexpect, std::move(error()));
+ }
+ }
+
+ template <class F>
+ constexpr auto transform_error(F&& f) & {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>();
+ } else {
+ return expected<T, G>(unexpect, std::invoke(std::forward<F>(f), error()));
+ }
+ }
+ template <class F>
+ constexpr auto transform_error(F&& f) && {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>();
+ } else {
+ return expected<T, G>(
+ unexpect, std::invoke(std::forward<F>(f), std::move(error())));
+ }
+ }
+ template <class F>
+ constexpr auto transform_error(F&& f) const& {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>();
+ } else {
+ return expected<T, G>(unexpect, std::invoke(std::forward<F>(f), error()));
+ }
+ }
+ template <class F>
+ constexpr auto transform_error(F&& f) const&& {
+ using G = std::remove_cv_t<std::invoke_result_t<F, decltype(error())>>;
+ if (has_value()) {
+ return expected<T, G>();
+ } else {
+ return expected<T, G>(
+ unexpect, std::invoke(std::forward<F>(f), std::move(error())));
+ }
+ }
+
+ private:
+ // Make all specializations of `expected` friends.
+ template <class U, class G, class>
+ friend class expected;
+
+ // Helper to handle transform correctly for void(Args...) functions, non-void
+ // case.
+ template <class U, class F, std::enable_if_t<!std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) const {
+ return expected<U, E>(std::in_place, std::invoke(std::forward<F>(f)));
+ }
+
+ // Helper to handle transform correctly for void(Args...) functions, void
+ // case.
+ template <class U, class F, std::enable_if_t<std::is_void_v<U>, bool> = true>
+ expected<U, E> transform_helper(F&& f) const {
+ std::invoke(std::forward<F>(f));
+ return expected<U, E>();
+ }
+
+ std::optional<E> error_contents_;
+};
+
+template <class T,
+ class E,
+ class U,
+ class G,
+ std::enable_if_t<std::is_void_v<U>, bool> = true>
+constexpr bool operator==(const expected<T, E>& lhs,
+ const expected<U, G>& rhs) {
+ if (lhs.has_value() != rhs.has_value())
+ return false;
+ return lhs.has_value() || static_cast<bool>(lhs.error() == rhs.error());
+}
+
+template <class T, class E, class G>
+constexpr bool operator==(const expected<T, E>& lhs, const unexpected<G>& rhs) {
+ return !lhs.has_value() && static_cast<bool>(lhs.error() == rhs.error());
+}
+
+} // namespace pw::internal
diff --git a/pw_result/public/pw_result/internal/result_internal.h b/pw_result/public/pw_result/internal/result_internal.h
index f87eb02d8..357afc0cd 100644
--- a/pw_result/public/pw_result/internal/result_internal.h
+++ b/pw_result/public/pw_result/internal/result_internal.h
@@ -349,8 +349,8 @@ struct MoveCtorBase<T, false> {
};
template <typename T,
- bool = std::is_copy_constructible<T>::value&&
- std::is_copy_assignable<T>::value>
+ bool = std::is_copy_constructible<T>::value &&
+ std::is_copy_assignable<T>::value>
struct CopyAssignBase {
CopyAssignBase() = default;
CopyAssignBase(const CopyAssignBase&) = default;
@@ -369,8 +369,8 @@ struct CopyAssignBase<T, false> {
};
template <typename T,
- bool = std::is_move_constructible<T>::value&&
- std::is_move_assignable<T>::value>
+ bool = std::is_move_constructible<T>::value &&
+ std::is_move_assignable<T>::value>
struct MoveAssignBase {
MoveAssignBase() = default;
MoveAssignBase(const MoveAssignBase&) = default;
diff --git a/pw_ring_buffer/BUILD.bazel b/pw_ring_buffer/BUILD.bazel
index 467c15681..7b13cbf4c 100644
--- a/pw_ring_buffer/BUILD.bazel
+++ b/pw_ring_buffer/BUILD.bazel
@@ -42,9 +42,7 @@ pw_cc_library(
pw_cc_test(
name = "prefixed_entry_ring_buffer_test",
- srcs = [
- "prefixed_entry_ring_buffer_test.cc",
- ],
+ srcs = ["prefixed_entry_ring_buffer_test.cc"],
deps = [
":pw_ring_buffer",
"//pw_unit_test",
diff --git a/pw_ring_buffer/BUILD.gn b/pw_ring_buffer/BUILD.gn
index 0116a68f8..870283913 100644
--- a/pw_ring_buffer/BUILD.gn
+++ b/pw_ring_buffer/BUILD.gn
@@ -19,12 +19,12 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
-config("default_config") {
+config("public_include_path") {
include_dirs = [ "public" ]
}
pw_source_set("pw_ring_buffer") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [
"$dir_pw_containers",
"$dir_pw_result",
@@ -37,6 +37,9 @@ pw_source_set("pw_ring_buffer") {
"$dir_pw_assert:pw_assert",
"$dir_pw_varint",
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test_group("tests") {
diff --git a/pw_ring_buffer/docs.rst b/pw_ring_buffer/docs.rst
index 673861042..dba41cecb 100644
--- a/pw_ring_buffer/docs.rst
+++ b/pw_ring_buffer/docs.rst
@@ -1,22 +1,25 @@
.. _module-pw_ring_buffer:
---------------
+==============
pw_ring_buffer
---------------
+==============
The ``pw_ring_buffer`` module will eventually provide several ring buffer
implementations, each with different tradeoffs.
This documentation is incomplete :)
-Compatibility
-=============
-* C++14
+-----------------------
+PrefixedEntryRingBuffer
+-----------------------
+:cpp:class:`pw::ring_buffer::PrefixedEntryRingBuffer` is a circular buffer for
+arbitrary length data entries with an optional user-defined preamble byte. It
+supports multiple independent readers.
Iterator
========
In crash contexts, it may be useful to scan through a ring buffer that may
have a mix of valid (yet to be read), stale (read), and invalid entries. The
-`PrefixedEntryRingBufferMulti::iterator` class can be used to walk through
+``PrefixedEntryRingBufferMulti::iterator`` class can be used to walk through
entries in the provided buffer.
.. code-block:: cpp
@@ -50,8 +53,8 @@ entries in the provided buffer.
In cases where a crash has caused the ring buffer to have corrupted data, the
iterator will progress until it sees the corrupted section and instead move to
-`iterator::end()`. The `iterator::status()` function returns a `pw::Status`
-indicating the reason the iterator reached it's end.
+``iterator::end()``. The ``iterator::status()`` function returns a
+:cpp:class:`pw::Status` indicating the reason the iterator reached it's end.
.. code-block:: cpp
@@ -80,8 +83,3 @@ entry to delimit the size of the entry. Unlike the iterator, the methods in
When these methods encounter data corruption, there is no generic way to
recover, and thus, the application crashes. Data corruption is indicative of
other issues.
-
-Dependencies
-============
-* ``pw_span``
-* ``pw_containers`` - for tests only
diff --git a/pw_ring_buffer/prefixed_entry_ring_buffer.cc b/pw_ring_buffer/prefixed_entry_ring_buffer.cc
index 93f70f780..99a9a0c61 100644
--- a/pw_ring_buffer/prefixed_entry_ring_buffer.cc
+++ b/pw_ring_buffer/prefixed_entry_ring_buffer.cc
@@ -98,8 +98,9 @@ Status PrefixedEntryRingBufferMulti::InternalPushBack(
user_preamble_bytes =
varint::Encode<uint32_t>(user_preamble_data, preamble_buf);
}
- size_t length_bytes = varint::Encode<uint32_t>(
- data.size_bytes(), span(preamble_buf).subspan(user_preamble_bytes));
+ size_t length_bytes =
+ varint::Encode<uint32_t>(static_cast<uint32_t>(data.size_bytes()),
+ span(preamble_buf).subspan(user_preamble_bytes));
size_t total_write_bytes =
user_preamble_bytes + length_bytes + data.size_bytes();
if (buffer_bytes_ < total_write_bytes) {
@@ -173,7 +174,7 @@ Status PrefixedEntryRingBufferMulti::InternalPeekFrontPreamble(
return OkStatus();
}
-// TODO(b/235351046): Consider whether this internal templating is required, or
+// TODO: b/235351046 - Consider whether this internal templating is required, or
// if we can simply promote GetOutput to a static function and remove the
// template. T should be similar to Status (*read_output)(span<const byte>)
template <typename T>
@@ -227,7 +228,7 @@ void PrefixedEntryRingBufferMulti::InternalPopFrontAll() {
for (Reader& reader : readers_) {
if (reader.entry_count_ == entry_count) {
reader.PopFront()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
}
@@ -263,9 +264,11 @@ Status PrefixedEntryRingBufferMulti::InternalDering(Reader& dering_reader) {
}
auto buffer_span = span(buffer_, buffer_bytes_);
- std::rotate(buffer_span.begin(),
- buffer_span.begin() + dering_reader.read_idx_,
- buffer_span.end());
+ std::rotate(
+ buffer_span.begin(),
+ buffer_span.begin() + static_cast<span<std::byte>::difference_type>(
+ dering_reader.read_idx_),
+ buffer_span.end());
// If the new index is past the end of the buffer,
// alias it back (wrap) to the start of the buffer.
@@ -359,7 +362,7 @@ PrefixedEntryRingBufferMulti::RawFrontEntryInfo(size_t source_idx) const {
EntryInfo info = {};
info.preamble_bytes = user_preamble_bytes + length_bytes;
info.user_preamble = static_cast<uint32_t>(user_preamble_data);
- info.data_bytes = entry_bytes;
+ info.data_bytes = static_cast<size_t>(entry_bytes);
return info;
}
diff --git a/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc b/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc
index f9325551e..eefda44b7 100644
--- a/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc
+++ b/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc
@@ -171,8 +171,8 @@ TEST(PrefixedEntryRingBuffer, SingleEntryWriteReadYesUserData) {
SingleEntryWriteReadTest(true);
}
-// TODO(b/234883746): Increase this to 5000 once we have a way to detect targets
-// with more computation and memory oomph.
+// TODO: b/234883746 - Increase this to 5000 once we have a way to detect
+// targets with more computation and memory oomph.
constexpr size_t kOuterCycles = 50u;
constexpr size_t kCountingUpMaxExpectedEntries =
single_entry_test_buffer_size / single_entry_total_size;
@@ -321,7 +321,7 @@ void DeringTest(bool preload) {
auto entry_data = span(single_entry_buffer);
size_t i;
- // TODO(b/234883746): Increase this to 500 once we have a way to detect
+ // TODO: b/234883746 - Increase this to 500 once we have a way to detect
// targets with more computation and memory oomph.
size_t loop_goal = preload ? 50 : 1;
diff --git a/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h b/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h
index a1209c256..c36cc2ed1 100644
--- a/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h
+++ b/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h
@@ -14,6 +14,7 @@
#pragma once
#include <cstddef>
+#include <cstdint>
#include <limits>
#include "pw_containers/intrusive_list.h"
@@ -64,7 +65,7 @@ class PrefixedEntryRingBufferMulti {
public:
constexpr Reader() : buffer_(nullptr), read_idx_(0), entry_count_(0) {}
- // TODO(b/235351035): Add locking to the internal functions. Who owns the
+ // TODO: b/235351035 - Add locking to the internal functions. Who owns the
// lock? This class? Does this class need a lock if it's not a multi-reader?
// (One doesn't exist today but presumably nothing prevents push + pop
// operations from happening on two different threads).
@@ -102,7 +103,7 @@ class PrefixedEntryRingBufferMulti {
// Same as PeekFront but includes the entry's preamble of optional user
// value and the varint of the data size.
- // TODO(b/235351847): Move all other APIs to passing bytes_read by
+ // TODO: b/235351847 - Move all other APIs to passing bytes_read by
// reference, as it is required to determine the length populated in the
// span.
Status PeekFrontWithPreamble(span<std::byte> data,
@@ -250,7 +251,7 @@ class PrefixedEntryRingBufferMulti {
const_iterator cbegin() { return begin(); }
const_iterator cend() { return end(); }
- // TODO(b/235351861): Consider changing bool to an enum, to explicitly
+ // TODO: b/235351861 - Consider changing bool to an enum, to explicitly
// enumerate what this variable means in clients.
PrefixedEntryRingBufferMulti(bool user_preamble = false)
: buffer_(nullptr),
@@ -514,7 +515,7 @@ class PrefixedEntryRingBuffer : public PrefixedEntryRingBufferMulti,
PrefixedEntryRingBuffer(bool user_preamble = false)
: PrefixedEntryRingBufferMulti(user_preamble) {
AttachReader(*this)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
};
diff --git a/pw_ring_buffer/size_report/ring_buffer_multi.cc b/pw_ring_buffer/size_report/ring_buffer_multi.cc
index c4e9232d1..aebe50c3a 100644
--- a/pw_ring_buffer/size_report/ring_buffer_multi.cc
+++ b/pw_ring_buffer/size_report/ring_buffer_multi.cc
@@ -36,7 +36,7 @@ int main() {
pw::ring_buffer::PrefixedEntryRingBufferMulti::Reader readers[kReaderCount];
for (auto& reader : readers) {
ring.AttachReader(reader)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
// Push entries until the buffer is full.
@@ -94,7 +94,7 @@ int main() {
for (auto& reader : readers) {
ring.DetachReader(reader)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
ring.Clear();
return 0;
diff --git a/pw_router/Android.bp b/pw_router/Android.bp
new file mode 100644
index 000000000..db3bc9fba
--- /dev/null
+++ b/pw_router/Android.bp
@@ -0,0 +1,77 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package{
+ default_applicable_licenses : ["external_pigweed_license"],
+}
+
+cc_library {
+ name: "pw_router_static_router",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ srcs: [
+ "static_router.cc",
+ ],
+ header_libs: [
+ "pw_log_headers",
+ "pw_router_egress_headers",
+ "pw_router_packet_parser_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_log_headers",
+ "pw_router_egress_headers",
+ "pw_router_packet_parser_headers",
+ ],
+ static_libs: [
+ "pw_base64",
+ "pw_containers",
+ "pw_metric",
+ "pw_status",
+ "pw_tokenizer_base64",
+ ],
+ export_static_lib_headers: [
+ "pw_metric",
+ "pw_status",
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
+
+cc_library_headers {
+ name: "pw_router_egress_headers",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ host_supported: true,
+ vendor_available: true,
+ static_libs: [
+ "pw_bytes",
+ ],
+ export_static_lib_headers: [
+ "pw_bytes",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_router_packet_parser_headers",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ host_supported: true,
+ vendor_available: true,
+ static_libs: [
+ "pw_bytes",
+ ],
+ export_static_lib_headers: [
+ "pw_bytes",
+ ],
+}
diff --git a/pw_router/CMakeLists.txt b/pw_router/CMakeLists.txt
index 0bc82fca3..1af3eeaa1 100644
--- a/pw_router/CMakeLists.txt
+++ b/pw_router/CMakeLists.txt
@@ -29,9 +29,6 @@ pw_add_library(pw_router.static_router STATIC
PRIVATE_DEPS
pw_log
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_STATIC_ROUTER)
- zephyr_link_libraries(pw_router.static_router)
-endif()
pw_add_library(pw_router.egress INTERFACE
HEADERS
@@ -43,9 +40,6 @@ pw_add_library(pw_router.egress INTERFACE
pw_router.packet_parser
pw_span
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_EGRESS)
- zephyr_link_libraries(pw_router.egress)
-endif()
pw_add_library(pw_router.packet_parser INTERFACE
HEADERS
@@ -56,9 +50,6 @@ pw_add_library(pw_router.packet_parser INTERFACE
pw_bytes
pw_span
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_PACKET_PARSER)
- zephyr_link_libraries(pw_router.packet_parser)
-endif()
pw_add_library(pw_router.egress_function INTERFACE
HEADERS
@@ -70,9 +61,6 @@ pw_add_library(pw_router.egress_function INTERFACE
pw_router.egress
pw_span
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_EGRESS_FUNCTION)
- zephyr_link_libraries(pw_router.egress_function)
-endif()
pw_add_test(pw_router.static_router_test
SOURCES
diff --git a/pw_router/Kconfig b/pw_router/Kconfig
index da0149796..98bda10a3 100644
--- a/pw_router/Kconfig
+++ b/pw_router/Kconfig
@@ -12,22 +12,34 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_router"
+
config PIGWEED_ROUTER_STATIC_ROUTER
- bool "Enable the Pigweed static router library (pw_router.static_router)"
+ bool "Link pw_router.static_router library"
select PIGWEED_METRIC
select PIGWEED_ROUTER_EGRESS
select PIGWEED_ROUTER_PACKET_PARSER
select PIGWEED_SYNC_MUTEX
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_router-static_router` for library details.
config PIGWEED_ROUTER_EGRESS
- bool "Enable the Pigweed router egress library (pw_router.egress)"
+ bool "Link pw_router.egress library"
select PIGWEED_BYTES
+ help
+ See :ref:`module-pw_router-egress` for library details.
config PIGWEED_ROUTER_PACKET_PARSER
- bool "Enable the Pigweed router packet parser library (pw_router.packet_parser)"
+ bool "Link pw_router.packet_parser library"
select PIGWEED_BYTES
+ help
+ See :ref:`module-pw_router-packet_parser` for library details.
config PIGWEED_ROUTER_EGRESS_FUNCTION
- bool "Enable the Pigweed router egress function library (pw_router.egress_function)"
+ bool "Link pw_router.egress_function library"
select PIGWEED_RPC_EGRESS
+ help
+ See :ref:`module-pw_router-egress` for library details.
+
+endmenu
diff --git a/pw_router/docs.rst b/pw_router/docs.rst
index aefabc9d3..c5356c745 100644
--- a/pw_router/docs.rst
+++ b/pw_router/docs.rst
@@ -9,6 +9,8 @@ over network links.
Common router interfaces
========================
+.. _module-pw_router-packet_parser:
+
PacketParser
------------
To work with arbitrary packet formats, routers require a common interface for
@@ -16,6 +18,8 @@ extracting relevant packet data, such as the destination. This interface is
``pw::router::PacketParser``, defined in ``pw_router/packet_parser.h``, which
must be implemented for the packet framing format used by the network.
+.. _module-pw_router-egress:
+
Egress
------
The Egress class is a virtual interface for sending packet data over a network
@@ -47,6 +51,8 @@ under heavy load.
Some common egress implementations are provided upstream in Pigweed.
+.. _module-pw_router-static_router:
+
StaticRouter
============
``pw::router::StaticRouter`` is a router with a static table of address to
diff --git a/pw_rpc/Android.bp b/pw_rpc/Android.bp
index e62b96157..516fb0a8c 100644
--- a/pw_rpc/Android.bp
+++ b/pw_rpc/Android.bp
@@ -75,7 +75,6 @@ filegroup {
cc_library_headers {
name: "pw_rpc_include_dirs",
- cpp_std: "c++20",
export_include_dirs: [
"public",
],
@@ -99,7 +98,7 @@ cc_library_headers {
// name: "pw_rpc_cflags_<instance_name>",
// cflags: [
// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
// "-DPW_RPC_DYNAMIC_ALLOCATION",
// ],
// }
@@ -109,8 +108,8 @@ cc_defaults {
name: "pw_rpc_defaults",
cpp_std: "c++20",
header_libs: [
- "fuschia_sdk_lib_fit",
- "fuschia_sdk_lib_stdcompat",
+ "fuchsia_sdk_lib_fit",
+ "fuchsia_sdk_lib_stdcompat",
"pw_assert_headers",
"pw_assert_log_headers",
"pw_function_headers",
@@ -126,8 +125,8 @@ cc_defaults {
"pw_toolchain",
],
export_header_lib_headers: [
- "fuschia_sdk_lib_fit",
- "fuschia_sdk_lib_stdcompat",
+ "fuchsia_sdk_lib_fit",
+ "fuchsia_sdk_lib_stdcompat",
"pw_assert_headers",
"pw_assert_log_headers",
"pw_function_headers",
@@ -167,23 +166,21 @@ cc_defaults {
"pw_rpc_internal_packet_pwpb_h",
],
srcs: [
- ":pw_rpc_src_files"
+ ":pw_rpc_src_files",
],
- host_supported: true,
- vendor_available: true,
}
genrule {
name: "pw_rpc_internal_packet_pwpb_h",
srcs: ["internal/packet.proto"],
cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--out-dir=$$(dirname $(location pw_rpc/internal/packet.pwpb.h)) " +
- "--plugin-path=$(location pw_protobuf_plugin_py) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--sources $(in) " +
- "--language pwpb " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc) ",
+ "--out-dir=$$(dirname $(location pw_rpc/internal/packet.pwpb.h)) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) ",
out: [
"pw_rpc/internal/packet.pwpb.h",
],
@@ -198,13 +195,13 @@ genrule {
name: "pw_rpc_internal_packet_py",
srcs: ["internal/packet.proto"],
cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--sources $(in) " +
- "--language python " +
- "--no-generate-type-hints " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language python " +
+ "--no-generate-type-hints " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
out: [
"packet_pb2.py",
],
@@ -218,14 +215,45 @@ genrule {
// The output file names are based on the srcs file name with a .pb.c / .pb.h extension.
genrule_defaults {
name: "pw_rpc_generate_nanopb_proto",
- cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--plugin-path=$(location protoc-gen-nanopb) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--language nanopb " +
- "--sources $(in) " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location protoc-gen-nanopb) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "protoc-gen-nanopb",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Same as pw_rpc_generate_nanopb_proto but the proto files are compiled with a
+// single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_nanopb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_nanopb_proto_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location protoc-gen-nanopb) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
tools: [
"aprotoc",
"protoc-gen-nanopb",
@@ -237,14 +265,45 @@ genrule_defaults {
// The output file name is based on the srcs file name with a .rpc.pb.h extension.
genrule_defaults {
name: "pw_rpc_generate_nanopb_rpc_header",
- cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--language nanopb_rpc " +
- "--sources $(in) " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_nanopb_py",
+ ],
+}
+
+// Same as pw_rpc_generate_nanopb_rpc_header but the proto files are compiled
+// with a single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_nanopb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_nanopb_rpc_header_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language nanopb_rpc " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
tools: [
"aprotoc",
"pw_protobuf_compiler_py",
@@ -256,14 +315,15 @@ genrule_defaults {
// The output file name is based on the srcs file name with a .raw_rpc.pb.h extension.
genrule_defaults {
name: "pw_rpc_generate_raw_rpc_header",
- cmd: "python3 $(location pw_protobuf_compiler_py) " +
- "--plugin-path=$(location pw_rpc_plugin_rawpb_py) " +
- "--out-dir=$(genDir) " +
- "--compile-dir=$$(dirname $(in)) " +
- "--language raw_rpc " +
- "--sources $(in) " +
- "--no-experimental-proto3-optional " +
- "--protoc=$(location aprotoc)",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_rawpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language raw_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
tools: [
"aprotoc",
"pw_protobuf_compiler_py",
@@ -271,6 +331,212 @@ genrule_defaults {
],
}
+// Generate header pwpb files.
+// The output file names are based on the srcs file name with a .pwpb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_proto",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Same as pw_rpc_generate_pwpb_proto but the proto files are compiled with a
+// single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_pwpb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_proto_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Generate the header pwpb RPC file.
+// The output file name is based on the srcs file name with a .rpc.pwpb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_rpc_header",
+ cmd: "in_files=($(in)); compile_dir=$$(dirname $${in_files[0]}); " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_pwpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_pwpb_py",
+ ],
+}
+
+// Same as pw_rpc_generate_pwpb_rpc_header but the proto files are compiled
+// with a single prefix, which can be added with pw_rpc_add_prefix_to_proto.
+// Since pw_rpc_add_prefix_to_proto may include .option files as an input, only
+// .proto files are passed to the compile script. Make sure .option files are
+// prefixed in the same rule as their .proto files.
+//
+// See the pw_rpc_echo_service_pwpb target for an example. The echo.proto file
+// is compiled with "pw_rpc" as the prefix.
+genrule_defaults {
+ name: "pw_rpc_generate_pwpb_rpc_header_with_prefix",
+ cmd: "in_files=($(in)); prefix_dir=$$(dirname $${in_files[0]}); " +
+ "compile_dir=$$(dirname $${prefix_dir}); proto_files=(); " +
+ "for f in \"$${in_files[@]}\"; do " +
+ "if [[ \"$${f##*.}\" == \"proto\" ]]; then " +
+ "proto_files+=(\"$${f}\"); fi; done; " +
+ "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_pwpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$${compile_dir} " +
+ "--language pwpb_rpc " +
+ "--sources $${proto_files} " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_pwpb_py",
+ ],
+}
+
+// Copies the proto files to a prefix directory to add the prefix to the
+// compiled proto. The prefix is taken from the directory name of the first
+// item listen in out.
+genrule_defaults {
+ name: "pw_rpc_add_prefix_to_proto",
+ cmd: "out_files=($(out)); prefix=$$(dirname $${out_files[0]}); " +
+ "mkdir -p $${prefix}; cp -t $${prefix} $(in);",
+}
+
+genrule {
+ name: "pw_rpc_echo_proto_with_prefix",
+ defaults: ["pw_rpc_add_prefix_to_proto"],
+ srcs: [
+ "echo.options",
+ "echo.proto",
+ ],
+ out: [
+ "pw_rpc/echo.options",
+ "pw_rpc/echo.proto",
+ ],
+}
+
+genrule {
+ name: "pw_rpc_echo_rpc_header",
+ defaults: ["pw_rpc_generate_nanopb_rpc_header_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.rpc.pb.h"],
+}
+
+genrule {
+ name: "pw_rpc_echo_proto_header",
+ defaults: ["pw_rpc_generate_nanopb_proto_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.pb.h"],
+}
+
+genrule {
+ name: "pw_rpc_echo_proto_source",
+ defaults: ["pw_rpc_generate_nanopb_proto_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.pb.c"],
+}
+
+// This is a copy of the echo.pb.h header, since the generated echo.pb.c
+// includes it by file name, while pw_rpc/nanopb/echo_service_nanopb.h includes
+// it with a prefix.
+// Soong makes it very hard to add include directories when they don't come from
+// modules, so this is a kludge to add an include directory path without a
+// prefix.
+genrule {
+ name: "pw_rpc_echo_proto_header_copy",
+ cmd: "cp $(in) $(out)",
+ srcs: [":pw_rpc_echo_proto_header"],
+ out: ["echo.pb.h"],
+}
+
+cc_library_static {
+ name: "pw_rpc_echo_service_nanopb",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ export_include_dirs: ["public/pw_rpc"],
+ generated_headers: [
+ "pw_rpc_echo_proto_header",
+ "pw_rpc_echo_proto_header_copy",
+ "pw_rpc_echo_rpc_header",
+ ],
+ export_generated_headers: [
+ "pw_rpc_echo_proto_header",
+ "pw_rpc_echo_proto_header_copy",
+ "pw_rpc_echo_rpc_header",
+ ],
+ generated_sources: ["pw_rpc_echo_proto_source"],
+ static_libs: ["libprotobuf-c-nano"],
+}
+
+genrule {
+ name: "pw_rpc_echo_pwpb_rpc_header",
+ defaults: ["pw_rpc_generate_pwpb_rpc_header_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.rpc.pwpb.h"],
+}
+
+genrule {
+ name: "pw_rpc_echo_pwpb_proto_header",
+ defaults: ["pw_rpc_generate_pwpb_proto_with_prefix"],
+ srcs: [":pw_rpc_echo_proto_with_prefix"],
+ out: ["pw_rpc/echo.pwpb.h"],
+}
+
+cc_library_static {
+ name: "pw_rpc_echo_service_pwpb",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ export_include_dirs: ["public/pw_rpc"],
+ generated_headers: [
+ "pw_rpc_echo_pwpb_proto_header",
+ "pw_rpc_echo_pwpb_rpc_header",
+ ],
+ export_generated_headers: [
+ "pw_rpc_echo_pwpb_proto_header",
+ "pw_rpc_echo_pwpb_rpc_header",
+ ],
+}
+
python_library_host {
name: "pw_rpc_internal_packet_py_lib",
srcs: [
diff --git a/pw_rpc/BUILD.bazel b/pw_rpc/BUILD.bazel
index 8574e9f53..dca4569dd 100644
--- a/pw_rpc/BUILD.bazel
+++ b/pw_rpc/BUILD.bazel
@@ -12,12 +12,10 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
-load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
-load("@rules_proto_grpc//:defs.bzl", "proto_plugin")
+load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -46,7 +44,7 @@ pw_cc_library(
],
)
-# TODO(b/242059613): Build this as a cc_binary and use it in integration tests.
+# TODO: b/242059613 - Build this as a cc_binary and use it in integration tests.
filegroup(
name = "test_rpc_server",
srcs = ["test_rpc_server.cc"],
@@ -131,6 +129,7 @@ pw_cc_library(
pw_cc_library(
name = "synchronous_client_api",
+ srcs = ["public/pw_rpc/internal/synchronous_call_impl.h"],
hdrs = [
"public/pw_rpc/synchronous_call.h",
"public/pw_rpc/synchronous_call_result.h",
@@ -201,11 +200,11 @@ pw_cc_library(
"public/pw_rpc/internal/fake_channel_output.h",
"public/pw_rpc/internal/method_impl_tester.h",
"public/pw_rpc/internal/method_info_tester.h",
- "public/pw_rpc/internal/test_method.h",
"public/pw_rpc/internal/test_method_context.h",
"public/pw_rpc/internal/test_utils.h",
"public/pw_rpc/payloads_view.h",
"pw_rpc_private/fake_server_reader_writer.h",
+ "pw_rpc_private/test_method.h",
],
includes = [
".",
@@ -242,11 +241,11 @@ pw_cc_library(
"//pw_log",
"//pw_stream:socket_stream",
"//pw_unit_test",
- "//pw_unit_test:logging_event_handler",
+ "//pw_unit_test:logging",
],
)
-# TODO(b/242059613): Add the client integration test to the build.
+# TODO: b/242059613 - Add the client integration test to the build.
filegroup(
name = "client_integration_test",
srcs = ["client_integration_test.cc"],
@@ -271,10 +270,10 @@ pw_cc_test(
":pw_rpc_test_cc.raw_rpc",
"//pw_rpc/raw:client_testing",
"//pw_sync:binary_semaphore",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:sleep",
- "//pw_thread:test_threads_header",
"//pw_thread:yield",
- "//pw_thread_stl:test_threads",
+ "//pw_thread_stl:non_portable_test_thread_options",
],
)
@@ -303,6 +302,7 @@ pw_cc_test(
],
deps = [
":pw_rpc",
+ "//pw_fuzzer:fuzztest",
],
)
@@ -313,6 +313,7 @@ pw_cc_test(
],
deps = [
":pw_rpc",
+ "//pw_fuzzer:fuzztest",
],
)
@@ -356,11 +357,20 @@ pw_cc_test(
deps = [":internal_test_utils"],
)
-# TODO(b/234874064): Add test_helpers_test when it is possible to use .options
-# in bazel build.
-filegroup(
+pw_cc_test(
name = "test_helpers_test",
srcs = ["test_helpers_test.cc"],
+ deps = [
+ ":test_helpers",
+ "//pw_result",
+ "//pw_rpc/pwpb:client_testing",
+ "//pw_rpc/pwpb:echo_service",
+ "//pw_rpc/pwpb:server_api",
+ "//pw_status",
+ "//pw_sync:interrupt_spin_lock",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:timed_thread_notification",
+ ],
)
proto_library(
@@ -398,54 +408,6 @@ pw_proto_library(
deps = [":pw_rpc_test_proto"],
)
-proto_plugin(
- name = "pw_cc_plugin_raw",
- outputs = [
- "{protopath}.raw_rpc.pb.h",
- ],
- protoc_plugin_name = "raw_rpc",
- tool = "@pigweed//pw_rpc/py:plugin_raw",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
-proto_plugin(
- name = "pw_cc_plugin_nanopb_rpc",
- outputs = [
- "{protopath}.rpc.pb.h",
- ],
- protoc_plugin_name = "nanopb_rpc",
- tool = "@pigweed//pw_rpc/py:plugin_nanopb",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
-proto_plugin(
- name = "nanopb_plugin",
- options = [
- "--library-include-format='#include\"%s\"'",
- ],
- outputs = [
- "{protopath}.pb.h",
- "{protopath}.pb.c",
- ],
- separate_options_flag = True,
- tool = "@com_github_nanopb_nanopb//:bazel_generator",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
-proto_plugin(
- name = "pw_cc_plugin_pwpb_rpc",
- outputs = [
- "{protopath}.rpc.pwpb.h",
- ],
- protoc_plugin_name = "pwpb_rpc",
- tool = "@pigweed//pw_rpc/py:plugin_pwpb",
- use_built_in_shell_environment = True,
- visibility = ["//visibility:public"],
-)
-
pw_proto_filegroup(
name = "echo_proto_and_options",
srcs = ["echo.proto"],
@@ -465,6 +427,4 @@ py_proto_library(
pw_proto_library(
name = "echo_cc",
deps = [":echo_proto"],
- # TODO(tpudlik): We should provide echo.options to nanopb here, but the
- # current proto codegen implementation provides no mechanism for doing so.
)
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 18c8778e3..97a32cec4 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/python_action_test.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
@@ -59,6 +60,16 @@ group("use_global_mutex") {
public_configs = [ ":global_mutex_config" ]
}
+config("dynamic_allocation_config") {
+ defines = [ "PW_RPC_DYNAMIC_ALLOCATION=1" ]
+ visibility = [ ":*" ]
+}
+
+# Use this for pw_rpc_CONFIG to enable dynamic allocation.
+pw_source_set("use_dynamic_allocation") {
+ public_configs = [ ":dynamic_allocation_config" ]
+}
+
pw_source_set("config") {
sources = [ "public/pw_rpc/internal/config.h" ]
public_configs = [ ":public_include_path" ]
@@ -146,6 +157,7 @@ pw_source_set("synchronous_client_api") {
"public/pw_rpc/synchronous_call.h",
"public/pw_rpc/synchronous_call_result.h",
]
+ sources = [ "public/pw_rpc/internal/synchronous_call_impl.h" ]
}
# Classes shared by the server and client.
@@ -290,10 +302,10 @@ pw_source_set("test_utils") {
"public/pw_rpc/internal/fake_channel_output.h",
"public/pw_rpc/internal/method_impl_tester.h",
"public/pw_rpc/internal/method_info_tester.h",
- "public/pw_rpc/internal/test_method.h",
"public/pw_rpc/internal/test_method_context.h",
"public/pw_rpc/internal/test_utils.h",
"pw_rpc_private/fake_server_reader_writer.h",
+ "pw_rpc_private/test_method.h",
]
public_configs = [ ":public_include_path" ]
public_deps = [
@@ -309,6 +321,7 @@ pw_source_set("test_utils") {
}
pw_source_set("integration_testing") {
+ testonly = pw_unit_test_TESTONLY
public = [
"public/pw_rpc/integration_test_socket_client.h",
"public/pw_rpc/integration_testing.h",
@@ -319,7 +332,7 @@ pw_source_set("integration_testing") {
"$dir_pw_hdlc:pw_rpc",
"$dir_pw_hdlc:rpc_channel_output",
"$dir_pw_stream:socket_stream",
- "$dir_pw_unit_test:logging_event_handler",
+ "$dir_pw_unit_test:logging",
dir_pw_assert,
dir_pw_function,
dir_pw_unit_test,
@@ -339,6 +352,7 @@ pw_executable("test_rpc_server") {
}
pw_executable("client_integration_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "client_integration_test.cc" ]
deps = [
":client",
@@ -356,7 +370,8 @@ pw_executable("client_integration_test") {
}
}
-pw_python_action("cpp_client_server_integration_test") {
+pw_python_action_test("cpp_client_server_integration_test") {
+ testonly = pw_unit_test_TESTONLY
script = "py/pw_rpc/testing.py"
args = [
"--server",
@@ -370,8 +385,7 @@ pw_python_action("cpp_client_server_integration_test") {
":client_integration_test",
":test_rpc_server",
]
-
- stamp = true
+ tags = [ "integration" ]
}
pw_proto_library("protos") {
@@ -397,16 +411,6 @@ pw_doc_group("docs") {
"benchmark.proto",
"echo.proto",
"internal/packet.proto",
- "unary_rpc.svg",
- "unary_rpc_cancelled.svg",
- "server_streaming_rpc.svg",
- "server_streaming_rpc_cancelled.svg",
- "client_streaming_rpc.svg",
- "client_streaming_rpc_cancelled.svg",
- "bidirectional_streaming_rpc.svg",
- "bidirectional_streaming_rpc_cancelled.svg",
- "request_packets.svg",
- "response_packets.svg",
]
group_deps = [
"nanopb:docs",
@@ -477,6 +481,9 @@ pw_test("call_test") {
":test_utils",
]
sources = [ "call_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("callback_test") {
@@ -486,13 +493,16 @@ pw_test("callback_test") {
":server",
":test_protos.raw_rpc",
"$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:non_portable_test_thread_options",
"$dir_pw_thread:sleep",
- "$dir_pw_thread:test_threads",
"$dir_pw_thread:yield",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
"raw:client_testing",
]
sources = [ "callback_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("channel_test") {
@@ -501,6 +511,9 @@ pw_test("channel_test") {
":test_utils",
]
sources = [ "channel_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_python_action("generate_ids_test") {
@@ -515,28 +528,36 @@ pw_python_action("generate_ids_test") {
}
pw_test("ids_test") {
- deps = [
- ":generate_ids_test",
- ":server",
- ]
- sources = get_target_outputs(":generate_ids_test")
+ deps = [ ":server" ]
+ source_gen_deps = [ ":generate_ids_test" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("packet_test") {
deps = [
":server",
+ "$dir_pw_fuzzer:fuzztest",
dir_pw_bytes,
dir_pw_protobuf,
]
sources = [ "packet_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("packet_meta_test") {
deps = [
":server",
+ "$dir_pw_fuzzer:fuzztest",
dir_pw_bytes,
]
sources = [ "packet_meta_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("service_test") {
@@ -546,6 +567,9 @@ pw_test("service_test") {
dir_pw_assert,
]
sources = [ "service_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("client_server_test") {
@@ -555,6 +579,9 @@ pw_test("client_server_test") {
"raw:server_api",
]
sources = [ "client_server_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("method_test") {
@@ -563,6 +590,9 @@ pw_test("method_test") {
":test_utils",
]
sources = [ "method_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("server_test") {
@@ -573,11 +603,17 @@ pw_test("server_test") {
dir_pw_assert,
]
sources = [ "server_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("fake_channel_output_test") {
deps = [ ":test_utils" ]
sources = [ "fake_channel_output_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("test_helpers_test") {
@@ -596,4 +632,7 @@ pw_test("test_helpers_test") {
enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "" &&
pw_sync_COUNTING_SEMAPHORE_BACKEND != "" &&
pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
diff --git a/pw_rpc/CMakeLists.txt b/pw_rpc/CMakeLists.txt
index 37b7cb0c9..4d95c180d 100644
--- a/pw_rpc/CMakeLists.txt
+++ b/pw_rpc/CMakeLists.txt
@@ -61,9 +61,6 @@ pw_add_library(pw_rpc.server STATIC
pw_log
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_SERVER)
- zephyr_link_libraries(pw_rpc.server)
-endif()
pw_add_library(pw_rpc.client STATIC
HEADERS
@@ -83,9 +80,6 @@ pw_add_library(pw_rpc.client STATIC
pw_log
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_CLIENT)
- zephyr_link_libraries(pw_rpc.client)
-endif()
pw_add_library(pw_rpc.client_server STATIC
HEADERS
@@ -98,14 +92,12 @@ pw_add_library(pw_rpc.client_server STATIC
SOURCES
client_server.cc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_CLIENT_SERVER)
- zephyr_link_libraries(pw_rpc.client_server)
-endif()
pw_add_library(pw_rpc.synchronous_client_api INTERFACE
HEADERS
public/pw_rpc/synchronous_call.h
public/pw_rpc/synchronous_call_result.h
+ public/pw_rpc/internal/synchronous_call_impl.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
@@ -170,10 +162,6 @@ if(NOT "${pw_thread.yield_BACKEND}" STREQUAL "")
pw_target_link_targets(pw_rpc.common PUBLIC pw_thread.yield)
endif()
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_COMMON)
- zephyr_link_libraries(pw_rpc.common)
-endif()
-
pw_add_library(pw_rpc.fake_channel_output STATIC
HEADERS
public/pw_rpc/internal/fake_channel_output.h
@@ -253,10 +241,10 @@ pw_add_library(pw_rpc.test_utils INTERFACE
public/pw_rpc/internal/fake_channel_output.h
public/pw_rpc/internal/method_impl_tester.h
public/pw_rpc/internal/method_info_tester.h
- public/pw_rpc/internal/test_method.h
public/pw_rpc/internal/test_method_context.h
public/pw_rpc/internal/test_utils.h
pw_rpc_private/fake_server_reader_writer.h
+ pw_rpc_private/test_method.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
@@ -343,6 +331,7 @@ pw_add_test(pw_rpc.packet_test
packet_test.cc
PRIVATE_DEPS
pw_bytes
+ pw_fuzzer.fuzztest
pw_protobuf
pw_rpc.server
GROUPS
@@ -355,6 +344,7 @@ pw_add_test(pw_rpc.packet_meta_test
packet_meta_test.cc
PRIVATE_DEPS
pw_bytes
+ pw_fuzzer.fuzztest
pw_rpc.server
GROUPS
modules
diff --git a/pw_rpc/Kconfig b/pw_rpc/Kconfig
index f01276ff7..d199dc852 100644
--- a/pw_rpc/Kconfig
+++ b/pw_rpc/Kconfig
@@ -12,26 +12,34 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_rpc"
+
rsource "nanopb/Kconfig"
config PIGWEED_RPC_SERVER
- bool "Enable Pigweed RPC server library (pw_rpc.server)"
+ bool "Link pw_rpc.server library"
select PIGWEED_RPC_COMMON
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc` for module details.
config PIGWEED_RPC_CLIENT
- bool "Enable Pigweed RPC client library (pw_rpc.client)"
+ bool "Link pw_rpc.client library"
select PIGWEED_RPC_COMMON
select PIGWEED_RESULT
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc` for module details.
config PIGWEED_RPC_CLIENT_SERVER
- bool "Enable Pigweed RPC client-server library (pw_rpc.client_server)"
+ bool "Link pw_rpc.client_server library"
select PIGWEED_RPC_CLIENT
select PIGWEED_RPC_SERVER
+ help
+ See :ref:`module-pw_rpc` for module details.
config PIGWEED_RPC_COMMON
- bool "Enable Pigweed RPC common library (pw_rpc.common)"
+ bool "Link pw_rpc.common library"
select PIGWEED_ASSERT
select PIGWEED_BYTES
select PIGWEED_CONTAINERS
@@ -40,3 +48,7 @@ config PIGWEED_RPC_COMMON
select PIGWEED_STATUS
select PIGWEED_LOG
select PIGWEED_SYNC_MUTEX
+ help
+ See :ref:`module-pw_rpc` for module details.
+
+endmenu
diff --git a/pw_rpc/benchmark.cc b/pw_rpc/benchmark.cc
index dfa11b64e..a8cec9522 100644
--- a/pw_rpc/benchmark.cc
+++ b/pw_rpc/benchmark.cc
@@ -39,17 +39,36 @@ void BenchmarkService::UnaryEcho(ConstByteSpan request,
.IgnoreError();
}
+BenchmarkService::ReaderWriterId BenchmarkService::AllocateReaderWriterId() {
+ return next_reader_writer_id_++;
+}
+
void BenchmarkService::BidirectionalEcho(
RawServerReaderWriter& new_reader_writer) {
- reader_writer_ = std::move(new_reader_writer);
-
- reader_writer_.set_on_next([this](ConstByteSpan request) {
- Status status = reader_writer_.Write(request);
- if (!status.ok()) {
- reader_writer_.Finish(status)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
- }
- });
+ auto id = AllocateReaderWriterId();
+
+ struct Captures {
+ BenchmarkService* self;
+ ReaderWriterId id;
+ };
+
+ auto captures = std::make_unique<Captures>(Captures{.self = this, .id = id});
+ new_reader_writer.set_on_next(
+ [captures = std::move(captures)](ConstByteSpan request) {
+ auto& reader_writers = captures->self->reader_writers_;
+ auto rw_id = captures->id;
+ auto reader_writer = reader_writers.find(rw_id);
+ if (reader_writer == reader_writers.end()) {
+ return;
+ }
+ Status status = reader_writer->second.Write(request);
+ if (!status.ok()) {
+ reader_writer->second.Finish(status)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
+ reader_writers.erase(rw_id);
+ }
+ });
+ reader_writers_.insert({id, std::move(new_reader_writer)});
}
} // namespace pw::rpc
diff --git a/pw_rpc/bidirectional_streaming_rpc.svg b/pw_rpc/bidirectional_streaming_rpc.svg
deleted file mode 100644
index fb18ea96c..000000000
--- a/pw_rpc/bidirectional_streaming_rpc.svg
+++ /dev/null
@@ -1,86 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="806.3000000000001" viewBox="0 0 624 733" width="686.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Bidirectional Streaming RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="558" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="231" y="153"></rect>
-<rect fill="rgb(0,0,0)" height="488" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="423" y="153"></rect>
-<polygon fill="rgb(0,0,0)" points="87,126 211,126 219,134 219,180 87,180 87,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="51,220 211,220 219,228 219,287 51,287 51,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="443,407 603,407 611,415 611,474 443,474 443,407" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="27,514 211,514 219,522 219,568 27,568 27,514" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="443,608 573,608 581,616 581,675 443,675 443,608" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="171" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="363" y="46"></rect>
-<path d="M 232 80 L 232 321" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<path d="M 232 329 L 232 353" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="2 8"></path>
-<path d="M 232 361 L 232 721" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="558" stroke="rgb(0,0,0)" width="8" x="228" y="147"></rect>
-<path d="M 424 80 L 424 321" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<path d="M 424 329 L 424 353" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="2 8"></path>
-<path d="M 424 361 L 424 721" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="488" stroke="rgb(0,0,0)" width="8" x="420" y="147"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="168" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="232.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="360" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="424.0" y="66">server</text>
-<path d="M 240 147 L 416 147" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,143 416,147 408,151" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="84,120 208,120 216,128 216,174 84,174 84,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 120 L 208 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 128 L 216 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="146.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="119.0" y="172">method ID</text>
-<path d="M 240 247 L 416 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="408,243 416,247 408,251" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="48,214 208,214 216,222 216,281 48,281 48,214" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 214 L 208 222" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 222 L 216 222" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="128.0" y="227">PacketType.CLIENT_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="240">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="253">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="83.0" y="266">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="77.0" y="279">payload</text>
-<path d="M 240 434 L 416 434" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="248,430 240,434 248,438" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="440,401 600,401 608,409 608,468 440,468 440,401" stroke="rgb(0,0,0)"></polygon>
-<path d="M 600 401 L 600 409" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 600 409 L 608 409" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="520.0" y="414">PacketType.SERVER_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="427">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="440">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="475.0" y="453">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="469.0" y="466">payload</text>
-<path d="M 240 535 L 416 535" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,531 416,535 408,539" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,508 208,508 216,516 216,562 24,562 24,508" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 508 L 208 516" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 516 L 216 516" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="168" x="116.0" y="521">PacketType.CLIENT_STREAM_END</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="534">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="547">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="560">method ID</text>
-<path d="M 240 635 L 416 635" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="248,631 240,635 248,639" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="440,602 570,602 578,610 578,669 440,669 440,602" stroke="rgb(0,0,0)"></polygon>
-<path d="M 570 602 L 570 610" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 570 610 L 578 610" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="505.0" y="615">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="628">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="641">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="475.0" y="654">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="466.0" y="667">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="30" x="259.0" y="145">start</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="313.0" y="245">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="347.0" y="432">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="256.0" y="533">done</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="404.0" y="633">done</text>
-<rect fill="white" height="19" stroke="white" width="158" x="249" y="331"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="328.0" y="346">(messages in any order)</text>
-</svg>
diff --git a/pw_rpc/bidirectional_streaming_rpc_cancelled.svg b/pw_rpc/bidirectional_streaming_rpc_cancelled.svg
deleted file mode 100644
index a6e6b7f3b..000000000
--- a/pw_rpc/bidirectional_streaming_rpc_cancelled.svg
+++ /dev/null
@@ -1,94 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 604 559" width="604px" height="559px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"/>
- </filter>
- </defs>
- <title>Cancelled Bidirectional Streaming RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "start",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID"
- ];
-
- client --&gt; server [
- noactivate,
- label = "messages (zero or more)",
- leftnote = "PacketType.CLIENT_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client &lt;-- server [
- noactivate,
- label = "messages (zero or more)",
- rightnote = "PacketType.SERVER_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- leftnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="384" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="209" y="153"/>
- <rect fill="rgb(0,0,0)" height="384" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="401" y="153"/>
- <polygon fill="rgb(0,0,0)" points="64,126 189,126 197,134 197,180 64,180 64,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="27,220 189,220 197,228 197,287 27,287 27,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="421,327 583,327 591,335 591,394 421,394 421,327" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="33,434 189,434 197,442 197,501 33,501 33,434" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="149" y="46"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="341" y="46"/>
- <path d="M 210 80 L 210 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="384" stroke="rgb(0,0,0)" width="8" x="206" y="147"/>
- <path d="M 402 80 L 402 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="384" stroke="rgb(0,0,0)" width="8" x="398" y="147"/>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="146" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="210.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="338" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="402.5" y="66">server</text>
- <path d="M 218 147 L 394 147" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,143 394,147 386,151" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="61,120 186,120 194,128 194,174 61,174 61,120" stroke="rgb(0,0,0)"/>
- <path d="M 186 120 L 186 128" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 128 L 194 128" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="123.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="96.5" y="172">method ID</text>
- <path d="M 218 247 L 394 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"/>
- <polygon fill="rgb(0,0,0)" points="386,243 394,247 386,251" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="24,214 186,214 194,222 194,281 24,281 24,214" stroke="rgb(0,0,0)"/>
- <path d="M 186 214 L 186 222" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 222 L 194 222" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="105.0" y="227">PacketType.CLIENT_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="240">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="253">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="266">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="53.5" y="279">payload</text>
- <path d="M 218 354 L 394 354" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"/>
- <polygon fill="rgb(0,0,0)" points="226,350 218,354 226,358" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="418,321 580,321 588,329 588,388 418,388 418,321" stroke="rgb(0,0,0)"/>
- <path d="M 580 321 L 580 329" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 580 329 L 588 329" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="499.0" y="334">PacketType.SERVER_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="347">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="360">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="453.5" y="373">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="447.5" y="386">payload</text>
- <path d="M 218 461 L 394 461" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,457 394,461 386,465" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="30,428 186,428 194,436 194,495 30,495 30,428" stroke="rgb(0,0,0)"/>
- <path d="M 186 428 L 186 436" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 436 L 194 436" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="108.0" y="441">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="68.5" y="454">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="68.5" y="467">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="65.5" y="480">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="86.5" y="493">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="31" x="237.5" y="145">start</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="292.0" y="245">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="324.0" y="352">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="240.5" y="459">cancel</text>
-</svg>
diff --git a/pw_rpc/call.cc b/pw_rpc/call.cc
index ffb94150b..1083f7769 100644
--- a/pw_rpc/call.cc
+++ b/pw_rpc/call.cc
@@ -83,9 +83,8 @@ Call::Call(LockedEndpoint& endpoint_ref,
id_(call_id),
service_id_(service_id),
method_id_(method_id),
- state_(kActive | (HasClientStream(properties.method_type())
- ? static_cast<uint8_t>(kClientStreamActive)
- : 0u)),
+ // Note: Bit kActive set to 1 and kClientRequestedCompletion is set to 0.
+ state_(kActive),
awaiting_cleanup_(OkStatus().code()),
callbacks_executing_(0),
properties_(properties) {
@@ -96,20 +95,22 @@ Call::Call(LockedEndpoint& endpoint_ref,
endpoint().RegisterCall(*this);
}
-Call::~Call() {
- // Note: this explicit deregistration is necessary to ensure that
- // modifications to the endpoint call list occur while holding rpc_lock.
- // Removing this explicit registration would result in unsynchronized
- // modification of the endpoint call list via the destructor of the
- // superclass `IntrusiveList<Call>::Item`.
+void Call::DestroyServerCall() {
RpcLockGuard lock;
+ // Any errors are logged in Channel::Send.
+ CloseAndSendResponseLocked(OkStatus()).IgnoreError();
+ WaitForCallbacksToComplete();
+ state_ |= kHasBeenDestroyed;
+}
- // This `active_locked()` guard is necessary to ensure that `endpoint()` is
- // still valid.
- if (active_locked()) {
- endpoint().UnregisterCall(*this);
- }
+void Call::DestroyClientCall() {
+ RpcLockGuard lock;
+ CloseClientCall();
+ WaitForCallbacksToComplete();
+ state_ |= kHasBeenDestroyed;
+}
+void Call::WaitForCallbacksToComplete() {
do {
int iterations = 0;
while (CallbacksAreRunning()) {
@@ -118,9 +119,6 @@ Call::~Call() {
}
} while (CleanUpIfRequired());
-
- // Help prevent dangling references in callbacks by waiting for callbacks to
- // complete before deleting this call.
}
void Call::MoveFrom(Call& other) {
@@ -284,6 +282,19 @@ void Call::HandlePayload(ConstByteSpan payload) {
endpoint_->CleanUpCalls();
}
+void Call::CloseClientCall() {
+ // When a client call is closed, for bidirectional and client streaming RPCs,
+ // the server may be waiting for client stream messages, so we need to notify
+ // the server that the client has requested for completion and no further
+ // requests should be expected from the client. For unary and server streaming
+ // RPCs, since the client is not sending messages, server does not need to be
+ // notified.
+ if (has_client_stream() && !client_requested_completion()) {
+ RequestCompletionLocked().IgnoreError();
+ }
+ UnregisterAndMarkClosed();
+}
+
void Call::UnregisterAndMarkClosed() {
if (active_locked()) {
endpoint().UnregisterCall(*this);
@@ -291,4 +302,36 @@ void Call::UnregisterAndMarkClosed() {
}
}
+void Call::DebugLog() const PW_NO_LOCK_SAFETY_ANALYSIS {
+ PW_LOG_INFO(
+ "Call %p\n"
+ "\tEndpoint: %p\n"
+ "\tCall ID: %8u\n"
+ "\tChannel: %8u\n"
+ "\tService: %08x\n"
+ "\tMethod: %08x\n"
+ "\tState: %8x\n"
+ "\tCleanup: %8s\n"
+ "\tBusy CBs: %8x\n"
+ "\tType: %8d\n"
+ "\tClient: %8d\n"
+ "\tWrapped: %8d\n"
+ "\ton_error: %8d\n"
+ "\ton_next: %8d\n",
+ static_cast<const void*>(this),
+ static_cast<const void*>(endpoint_),
+ static_cast<unsigned>(id_),
+ static_cast<unsigned>(channel_id_),
+ static_cast<unsigned>(service_id_),
+ static_cast<unsigned>(method_id_),
+ static_cast<int>(state_),
+ Status(static_cast<Status::Code>(awaiting_cleanup_)).str(),
+ static_cast<int>(callbacks_executing_),
+ static_cast<int>(properties_.method_type()),
+ static_cast<int>(properties_.call_type()),
+ static_cast<int>(hold_lock_while_invoking_callback_with_payload()),
+ static_cast<int>(on_error_ == nullptr),
+ static_cast<int>(on_next_ == nullptr));
+}
+
} // namespace pw::rpc::internal
diff --git a/pw_rpc/call_test.cc b/pw_rpc/call_test.cc
index b2cb44bfb..c3ae8be98 100644
--- a/pw_rpc/call_test.cc
+++ b/pw_rpc/call_test.cc
@@ -21,10 +21,10 @@
#include <optional>
#include "gtest/gtest.h"
-#include "pw_rpc/internal/test_method.h"
#include "pw_rpc/internal/test_utils.h"
#include "pw_rpc/service.h"
#include "pw_rpc_private/fake_server_reader_writer.h"
+#include "pw_rpc_private/test_method.h"
namespace pw::rpc {
@@ -188,13 +188,14 @@ TEST_F(ServerWriterTest, DefaultConstructor_NoClientStream) {
FakeServerWriter writer;
RpcLockGuard lock;
EXPECT_FALSE(writer.as_server_call().has_client_stream());
- EXPECT_FALSE(writer.as_server_call().client_stream_open());
+ EXPECT_FALSE(writer.as_server_call().client_requested_completion());
}
TEST_F(ServerWriterTest, Open_NoClientStream) {
RpcLockGuard lock;
EXPECT_FALSE(writer_.as_server_call().has_client_stream());
- EXPECT_FALSE(writer_.as_server_call().client_stream_open());
+ EXPECT_TRUE(writer_.as_server_call().has_server_stream());
+ EXPECT_FALSE(writer_.as_server_call().client_requested_completion());
}
class ServerReaderTest : public Test {
@@ -210,41 +211,41 @@ class ServerReaderTest : public Test {
FakeServerReader reader_;
};
-TEST_F(ServerReaderTest, DefaultConstructor_ClientStreamClosed) {
+TEST_F(ServerReaderTest, DefaultConstructor_StreamClosed) {
FakeServerReader reader;
EXPECT_FALSE(reader.as_server_call().active());
RpcLockGuard lock;
- EXPECT_FALSE(reader.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader.as_server_call().client_requested_completion());
}
TEST_F(ServerReaderTest, Open_ClientStreamStartsOpen) {
RpcLockGuard lock;
EXPECT_TRUE(reader_.as_server_call().has_client_stream());
- EXPECT_TRUE(reader_.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader_.as_server_call().client_requested_completion());
}
-TEST_F(ServerReaderTest, Close_ClosesClientStream) {
+TEST_F(ServerReaderTest, Close_ClosesStream) {
EXPECT_TRUE(reader_.as_server_call().active());
rpc_lock().lock();
- EXPECT_TRUE(reader_.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader_.as_server_call().client_requested_completion());
rpc_lock().unlock();
EXPECT_EQ(OkStatus(),
reader_.as_server_call().CloseAndSendResponse(OkStatus()));
EXPECT_FALSE(reader_.as_server_call().active());
RpcLockGuard lock;
- EXPECT_FALSE(reader_.as_server_call().client_stream_open());
+ EXPECT_TRUE(reader_.as_server_call().client_requested_completion());
}
-TEST_F(ServerReaderTest, EndClientStream_OnlyClosesClientStream) {
+TEST_F(ServerReaderTest, RequestCompletion_OnlyMakesClientNotReady) {
EXPECT_TRUE(reader_.active());
rpc_lock().lock();
- EXPECT_TRUE(reader_.as_server_call().client_stream_open());
- reader_.as_server_call().HandleClientStreamEnd();
+ EXPECT_FALSE(reader_.as_server_call().client_requested_completion());
+ reader_.as_server_call().HandleClientRequestedCompletion();
EXPECT_TRUE(reader_.active());
RpcLockGuard lock;
- EXPECT_FALSE(reader_.as_server_call().client_stream_open());
+ EXPECT_TRUE(reader_.as_server_call().client_requested_completion());
}
class ServerReaderWriterTest : public Test {
@@ -264,33 +265,31 @@ TEST_F(ServerReaderWriterTest, Move_MaintainsClientStream) {
FakeServerReaderWriter destination;
rpc_lock().lock();
- EXPECT_FALSE(destination.as_server_call().client_stream_open());
+ EXPECT_FALSE(destination.as_server_call().client_requested_completion());
rpc_lock().unlock();
destination = std::move(reader_writer_);
RpcLockGuard lock;
EXPECT_TRUE(destination.as_server_call().has_client_stream());
- EXPECT_TRUE(destination.as_server_call().client_stream_open());
+ EXPECT_FALSE(destination.as_server_call().client_requested_completion());
}
TEST_F(ServerReaderWriterTest, Move_MovesCallbacks) {
int calls = 0;
reader_writer_.set_on_error([&calls](Status) { calls += 1; });
reader_writer_.set_on_next([&calls](ConstByteSpan) { calls += 1; });
-
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- reader_writer_.set_on_client_stream_end([&calls]() { calls += 1; });
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ reader_writer_.set_on_completion_requested_if_enabled(
+ [&calls]() { calls += 1; });
FakeServerReaderWriter destination(std::move(reader_writer_));
rpc_lock().lock();
destination.as_server_call().HandlePayload({});
rpc_lock().lock();
- destination.as_server_call().HandleClientStreamEnd();
+ destination.as_server_call().HandleClientRequestedCompletion();
rpc_lock().lock();
destination.as_server_call().HandleError(Status::Unknown());
- EXPECT_EQ(calls, 2 + PW_RPC_CLIENT_STREAM_END_CALLBACK);
+ EXPECT_EQ(calls, 2 + PW_RPC_COMPLETION_REQUEST_CALLBACK);
}
TEST_F(ServerReaderWriterTest, Move_ClearsCallAndChannelId) {
diff --git a/pw_rpc/callback_test.cc b/pw_rpc/callback_test.cc
index 76f15a2cc..80e0cb4cc 100644
--- a/pw_rpc/callback_test.cc
+++ b/pw_rpc/callback_test.cc
@@ -16,8 +16,8 @@
#include "pw_rpc/raw/client_testing.h"
#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread/thread.h"
#include "pw_thread/yield.h"
@@ -28,6 +28,12 @@ using namespace std::chrono_literals;
using test::pw_rpc::raw::TestService;
+// These tests cover interactions between a thread moving or destroying an RPC
+// call object and a thread running callbacks for that call. In order to test
+// that the first thread waits for callbacks to complete when trying to move or
+// destroy the call, it is necessary to have the callback thread yield to the
+// other thread. There isn't a good way to synchronize these threads without
+// changing the code under test.
void YieldToOtherThread() {
// Sleep for a while and then yield just to be sure the other thread runs.
this_thread::sleep_for(100ms);
@@ -38,6 +44,8 @@ class CallbacksTest : public ::testing::Test {
protected:
CallbacksTest()
: callback_thread_(
+ // TODO: b/290860904 - Replace TestOptionsThread0 with
+ // TestThreadContext.
thread::test::TestOptionsThread0(),
[](void* arg) {
static_cast<CallbacksTest*>(arg)->SendResponseAfterSemaphore();
@@ -58,7 +66,7 @@ class CallbacksTest : public ::testing::Test {
thread::Thread callback_thread_;
- // Must be set to true by the RPC callback in each test.
+ // Must be incremented exactly once by the RPC callback in each test.
volatile int callback_executed_ = 0;
// Variables optionally used by tests. These are in this object so lambads
@@ -81,11 +89,11 @@ class CallbacksTest : public ::testing::Test {
};
TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
- // Skip this test if locks are disabled because the thread can't yield.
if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
callback_thread_sem_.release();
callback_thread_.join();
- GTEST_SKIP();
+ GTEST_SKIP()
+ << "Skipping because locks are disabled, so this thread cannot yield.";
}
{
@@ -126,11 +134,11 @@ TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
}
TEST_F(CallbacksTest, MoveActiveCall_WaitsForCallbackToComplete) {
- // Skip this test if locks are disabled because the thread can't yield.
if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
callback_thread_sem_.release();
callback_thread_.join();
- GTEST_SKIP();
+ GTEST_SKIP()
+ << "Skipping because locks are disabled, so this thread cannot yield.";
}
call_1_ = TestService::TestBidirectionalStreamRpc(
@@ -231,7 +239,8 @@ TEST_F(CallbacksTest, PacketDroppedIfOnNextIsBusy) {
main_thread_sem_.acquire(); // Confirm that the callback is running
- // Handle a few packets for this call, which should be dropped.
+ // Handle a few packets for this call, which should be dropped since on_next
+ // is busy. callback_executed_ should remain at 1.
for (int i = 0; i < 5; ++i) {
context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
{}, call_1_.id());
diff --git a/pw_rpc/client.cc b/pw_rpc/client.cc
index 006264220..4cf26dca6 100644
--- a/pw_rpc/client.cc
+++ b/pw_rpc/client.cc
@@ -42,7 +42,8 @@ Status Client::ProcessPacket(ConstByteSpan data) {
if (channel == nullptr) {
internal::rpc_lock().unlock();
- PW_LOG_WARN("RPC client received a packet for an unregistered channel");
+ PW_LOG_WARN("RPC client received a packet for an unregistered channel: %lu",
+ static_cast<unsigned long>(packet.channel_id()));
return Status::Unavailable();
}
@@ -88,7 +89,7 @@ Status Client::ProcessPacket(ConstByteSpan data) {
case PacketType::REQUEST:
case PacketType::CLIENT_STREAM:
case PacketType::CLIENT_ERROR:
- case PacketType::CLIENT_STREAM_END:
+ case PacketType::CLIENT_REQUEST_COMPLETION:
default:
internal::rpc_lock().unlock();
PW_LOG_WARN("pw_rpc client unable to handle packet of type %u",
diff --git a/pw_rpc/client_call.cc b/pw_rpc/client_call.cc
index 70ca4344a..612f13c99 100644
--- a/pw_rpc/client_call.cc
+++ b/pw_rpc/client_call.cc
@@ -16,13 +16,6 @@
namespace pw::rpc::internal {
-void ClientCall::CloseClientCall() {
- if (client_stream_open()) {
- CloseClientStreamLocked().IgnoreError();
- }
- UnregisterAndMarkClosed();
-}
-
void ClientCall::MoveClientCallFrom(ClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
WaitUntilReadyForMove(*this, other);
diff --git a/pw_rpc/client_integration_test.cc b/pw_rpc/client_integration_test.cc
index 53bff084a..8f1021176 100644
--- a/pw_rpc/client_integration_test.cc
+++ b/pw_rpc/client_integration_test.cc
@@ -14,6 +14,8 @@
#include <sys/socket.h>
+#include <algorithm>
+#include <array>
#include <cstring>
#include "gtest/gtest.h"
@@ -47,28 +49,35 @@ Benchmark::Client kServiceClient(pw::rpc::integration_test::client(),
class StringReceiver {
public:
const char* Wait() {
- PW_CHECK(sem_.try_acquire_for(1500ms));
- return buffer_;
+ PW_CHECK(sem_.try_acquire_for(10s));
+ return reinterpret_cast<const char*>(buffer_.begin());
}
Function<void(ConstByteSpan, Status)> UnaryOnCompleted() {
- return [this](ConstByteSpan data, Status) { CopyPayload(data); };
+ return [this](ConstByteSpan data, Status) { CopyStringPayload(data); };
}
Function<void(ConstByteSpan)> OnNext() {
- return [this](ConstByteSpan data) { CopyPayload(data); };
+ return [this](ConstByteSpan data) { CopyStringPayload(data); };
}
- private:
- void CopyPayload(ConstByteSpan data) {
- std::memset(buffer_, 0, sizeof(buffer_));
- PW_CHECK_UINT_LE(data.size(), sizeof(buffer_));
- std::memcpy(buffer_, data.data(), data.size());
+ void CopyStringPayload(ConstByteSpan data) {
+ std::memset(buffer_.data(), 0, buffer_.size());
+ PW_CHECK_UINT_LE(data.size(), buffer_.size());
+ std::copy(data.begin(), data.end(), buffer_.begin());
sem_.release();
}
+ void ReverseCopyStringPayload(ConstByteSpan data) {
+ std::memset(buffer_.data(), 0, buffer_.size());
+ PW_CHECK_UINT_LE(data.size(), buffer_.size());
+ std::reverse_copy(data.begin(), data.end() - 1, buffer_.begin());
+ sem_.release();
+ }
+
+ private:
pw::sync::BinarySemaphore sem_;
- char buffer_[64];
+ std::array<std::byte, 64> buffer_;
};
TEST(RawRpcIntegrationTest, Unary) {
@@ -96,6 +105,38 @@ TEST(RawRpcIntegrationTest, BidirectionalStreaming) {
}
}
+// This test sometimes fails due to a server stream packet being dropped.
+// TODO: b/290048137 - Enable this test after the flakiness is fixed.
+TEST(RawRpcIntegrationTest, DISABLED_OnNextOverwritesItsOwnCall) {
+ for (int i = 0; i < kIterations; ++i) {
+ struct {
+ StringReceiver receiver;
+ pw::rpc::RawClientReaderWriter call;
+ } ctx;
+
+ // Chain together three calls. The first and third copy the string in normal
+ // order, while the second copies the string in reverse order.
+ ctx.call = kServiceClient.BidirectionalEcho([&ctx](ConstByteSpan data) {
+ ctx.call = kServiceClient.BidirectionalEcho([&ctx](ConstByteSpan data) {
+ ctx.receiver.ReverseCopyStringPayload(data);
+ ctx.call = kServiceClient.BidirectionalEcho(ctx.receiver.OnNext());
+ });
+ ctx.receiver.CopyStringPayload(data);
+ });
+
+ ASSERT_EQ(OkStatus(), ctx.call.Write(pw::as_bytes(pw::span("Window"))));
+ EXPECT_STREQ(ctx.receiver.Wait(), "Window");
+
+ ASSERT_EQ(OkStatus(), ctx.call.Write(pw::as_bytes(pw::span("Door"))));
+ EXPECT_STREQ(ctx.receiver.Wait(), "rooD");
+
+ ASSERT_EQ(OkStatus(), ctx.call.Write(pw::as_bytes(pw::span("Roof"))));
+ EXPECT_STREQ(ctx.receiver.Wait(), "Roof");
+
+ ASSERT_EQ(OkStatus(), ctx.call.Cancel());
+ }
+}
+
} // namespace
} // namespace rpc_test
diff --git a/pw_rpc/client_streaming_rpc.svg b/pw_rpc/client_streaming_rpc.svg
deleted file mode 100644
index 37155fdae..000000000
--- a/pw_rpc/client_streaming_rpc.svg
+++ /dev/null
@@ -1,69 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="614.9000000000001" viewBox="0 0 594 559" width="653.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Client Streaming RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="384" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="231" y="153"></rect>
-<rect fill="rgb(0,0,0)" height="308" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="423" y="153"></rect>
-<polygon fill="rgb(0,0,0)" points="87,126 211,126 219,134 219,180 87,180 87,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="51,220 211,220 219,228 219,287 51,287 51,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="27,327 211,327 219,335 219,381 27,381 27,327" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="443,421 573,421 581,429 581,501 443,501 443,421" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="171" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="363" y="46"></rect>
-<path d="M 232 80 L 232 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="384" stroke="rgb(0,0,0)" width="8" x="228" y="147"></rect>
-<path d="M 424 80 L 424 547" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="308" stroke="rgb(0,0,0)" width="8" x="420" y="147"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="168" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="232.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="360" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="424.0" y="66">server</text>
-<path d="M 240 147 L 416 147" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,143 416,147 408,151" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="84,120 208,120 216,128 216,174 84,174 84,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 120 L 208 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 128 L 216 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="146.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="122.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="119.0" y="172">method ID</text>
-<path d="M 240 247 L 416 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="408,243 416,247 408,251" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="48,214 208,214 216,222 216,281 48,281 48,214" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 214 L 208 222" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 222 L 216 222" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="128.0" y="227">PacketType.CLIENT_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="240">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="86.0" y="253">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="83.0" y="266">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="77.0" y="279">payload</text>
-<path d="M 240 348 L 416 348" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="408,344 416,348 408,352" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,321 208,321 216,329 216,375 24,375 24,321" stroke="rgb(0,0,0)"></polygon>
-<path d="M 208 321 L 208 329" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 208 329 L 216 329" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="168" x="116.0" y="334">PacketType.CLIENT_STREAM_END</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="347">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="360">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="373">method ID</text>
-<path d="M 240 455 L 416 455" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="248,451 240,455 248,459" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="440,415 570,415 578,423 578,495 440,495 440,415" stroke="rgb(0,0,0)"></polygon>
-<path d="M 570 415 L 570 423" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 570 423 L 578 423" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="505.0" y="428">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="441">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="478.0" y="454">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="475.0" y="467">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="469.0" y="480">payload</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="466.0" y="493">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="30" x="259.0" y="145">start</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="313.0" y="245">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="256.0" y="346">done</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="48" x="392.0" y="453">response</text>
-</svg>
diff --git a/pw_rpc/client_streaming_rpc_cancelled.svg b/pw_rpc/client_streaming_rpc_cancelled.svg
deleted file mode 100644
index 9542ed2df..000000000
--- a/pw_rpc/client_streaming_rpc_cancelled.svg
+++ /dev/null
@@ -1,76 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 598 452" width="598px" height="452px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"/>
- </filter>
- </defs>
- <title>Cancelled Client Streaming RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "start",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID"
- ];
-
- client --&gt; server [
- noactivate,
- label = "messages (zero or more)",
- leftnote = "PacketType.CLIENT_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- rightnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="277" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="209" y="153"/>
- <rect fill="rgb(0,0,0)" height="277" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="401" y="153"/>
- <polygon fill="rgb(0,0,0)" points="64,126 189,126 197,134 197,180 64,180 64,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="27,220 189,220 197,228 197,287 27,287 27,220" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <polygon fill="rgb(0,0,0)" points="421,327 577,327 585,335 585,394 421,394 421,327" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="149" y="46"/>
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="341" y="46"/>
- <path d="M 210 80 L 210 440" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="277" stroke="rgb(0,0,0)" width="8" x="206" y="147"/>
- <path d="M 402 80 L 402 440" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"/>
- <rect fill="moccasin" height="277" stroke="rgb(0,0,0)" width="8" x="398" y="147"/>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="146" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="210.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="338" y="40"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="402.5" y="66">server</text>
- <path d="M 218 147 L 394 147" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,143 394,147 386,151" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="61,120 186,120 194,128 194,174 61,174 61,120" stroke="rgb(0,0,0)"/>
- <path d="M 186 120 L 186 128" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 128 L 194 128" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="123.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="99.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="96.5" y="172">method ID</text>
- <path d="M 218 247 L 394 247" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"/>
- <polygon fill="rgb(0,0,0)" points="386,243 394,247 386,251" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="24,214 186,214 194,222 194,281 24,281 24,214" stroke="rgb(0,0,0)"/>
- <path d="M 186 214 L 186 222" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 186 222 L 194 222" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="105.0" y="227">PacketType.CLIENT_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="240">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="253">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="266">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="53.5" y="279">payload</text>
- <path d="M 218 354 L 394 354" fill="none" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(0,0,0)" points="386,350 394,354 386,358" stroke="rgb(0,0,0)"/>
- <polygon fill="rgb(240,248,255)" points="418,321 574,321 582,329 582,388 418,388 418,321" stroke="rgb(0,0,0)"/>
- <path d="M 574 321 L 574 329" fill="none" stroke="rgb(0,0,0)"/>
- <path d="M 574 329 L 582 329" fill="none" stroke="rgb(0,0,0)"/>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="496.0" y="334">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="347">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="456.5" y="360">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="453.5" y="373">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="474.5" y="386">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="31" x="237.5" y="145">start</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="292.0" y="245">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="240.5" y="352">cancel</text>
-</svg>
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index 83bd6d558..093e52a78 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -11,20 +11,20 @@ This document discusses the ``pw_rpc`` protocol and its C++ implementation.
documents:
.. toctree::
- :maxdepth: 1
+ :maxdepth: 1
- py/docs
- ts/docs
+ py/docs
+ ts/docs
.. admonition:: Try it out!
- For a quick intro to ``pw_rpc``, see the
- :ref:`module-pw_hdlc-rpc-example` in the :ref:`module-pw_hdlc` module.
+ For a quick intro to ``pw_rpc``, see the
+ :ref:`module-pw_hdlc-rpc-example` in the :ref:`module-pw_hdlc` module.
.. warning::
- This documentation is under construction. Many sections are outdated or
- incomplete. The content needs to be reorgnanized.
+ This documentation is under construction. Many sections are outdated or
+ incomplete. The content needs to be reorgnanized.
---------------
Implementations
@@ -56,6 +56,14 @@ Pigweed provides several client and server implementations of ``pw_rpc``.
-
- in development
+.. warning::
+
+ ``pw_protobuf`` and ``nanopb`` RPC services cannot currently coexist within the
+ same RPC server. Unless you are running multiple RPC servers, you cannot
+ incrementally migrate services from one protobuf implementation to another,
+ or otherwise mix and match. See
+ `Issue 234874320 <https://issues.pigweed.dev/issues/234874320>`_.
+
-------------
RPC semantics
-------------
@@ -68,6 +76,7 @@ In ``pw_rpc``, an RPC begins when the client sends an initial packet. The server
receives the packet, looks up the relevant service method, then calls into the
RPC function. The RPC is considered active until the server sends a status to
finish the RPC. The client may terminate an ongoing RPC by cancelling it.
+Multiple RPC requests to the same method may be made simultaneously.
Depending the type of RPC, the client and server exchange zero or more protobuf
request or response payloads. There are four RPC types:
@@ -96,10 +105,10 @@ The key events in the RPC lifecycle are:
only one response and it is handled when the RPC completes.
* **Error**. The server or client terminates the RPC abnormally with a status.
The receiving endpoint calls a callback.
-* **Client stream end**. The client sends a message that it has finished sending
- requests. The server calls a callback when it receives it. Some servers may
- ignore the client stream end. The client stream end is only relevant for
- client and bidirectional streaming RPCs.
+* **Request Completion**. The client sends a message that it would like to
+ request call completion. The server calls a callback when it receives it. Some
+ servers may ignore the request completion message. In client and bidirectional
+ streaming RPCs, this also indicates that client has finished sending requests.
Status codes
============
@@ -153,65 +162,65 @@ Pigweed RPCs are declared in a protocol buffer service definition.
.. code-block:: protobuf
- syntax = "proto3";
+ syntax = "proto3";
- package foo.bar;
+ package foo.bar;
- message Request {}
+ message Request {}
- message Response {
- int32 number = 1;
- }
+ message Response {
+ int32 number = 1;
+ }
- service TheService {
- rpc MethodOne(Request) returns (Response) {}
- rpc MethodTwo(Request) returns (stream Response) {}
- }
+ service TheService {
+ rpc MethodOne(Request) returns (Response) {}
+ rpc MethodTwo(Request) returns (stream Response) {}
+ }
This protocol buffer is declared in a ``BUILD.gn`` file as follows:
.. code-block:: python
- import("//build_overrides/pigweed.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ import("//build_overrides/pigweed.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
- pw_proto_library("the_service_proto") {
- sources = [ "foo_bar/the_service.proto" ]
- }
+ pw_proto_library("the_service_proto") {
+ sources = [ "foo_bar/the_service.proto" ]
+ }
.. admonition:: proto2 or proto3 syntax?
- Always use proto3 syntax rather than proto2 for new protocol buffers. Proto2
- protobufs can be compiled for ``pw_rpc``, but they are not as well supported
- as proto3. Specifically, ``pw_rpc`` lacks support for non-zero default values
- in proto2. When using Nanopb with ``pw_rpc``, proto2 response protobufs with
- non-zero field defaults should be manually initialized to the default struct.
+ Always use proto3 syntax rather than proto2 for new protocol buffers. Proto2
+ protobufs can be compiled for ``pw_rpc``, but they are not as well supported
+ as proto3. Specifically, ``pw_rpc`` lacks support for non-zero default values
+ in proto2. When using Nanopb with ``pw_rpc``, proto2 response protobufs with
+ non-zero field defaults should be manually initialized to the default struct.
- In the past, proto3 was sometimes avoided because it lacked support for field
- presence detection. Fortunately, this has been fixed: proto3 now supports
- ``optional`` fields, which are equivalent to proto2 ``optional`` fields.
+ In the past, proto3 was sometimes avoided because it lacked support for field
+ presence detection. Fortunately, this has been fixed: proto3 now supports
+ ``optional`` fields, which are equivalent to proto2 ``optional`` fields.
- If you need to distinguish between a default-valued field and a missing field,
- mark the field as ``optional``. The presence of the field can be detected
- with ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
- depending on the library.
+ If you need to distinguish between a default-valued field and a missing field,
+ mark the field as ``optional``. The presence of the field can be detected
+ with ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
+ depending on the library.
- Optional fields have some overhead --- if using Nanopb, default-valued fields
- are included in the encoded proto, and the proto structs have a
- ``has_<field>`` flag for each optional field. Use plain fields if field
- presence detection is not needed.
+ Optional fields have some overhead --- if using Nanopb, default-valued fields
+ are included in the encoded proto, and the proto structs have a
+ ``has_<field>`` flag for each optional field. Use plain fields if field
+ presence detection is not needed.
- .. code-block:: protobuf
+ .. code-block:: protobuf
- syntax = "proto3";
+ syntax = "proto3";
- message MyMessage {
- // Leaving this field unset is equivalent to setting it to 0.
- int32 number = 1;
+ message MyMessage {
+ // Leaving this field unset is equivalent to setting it to 0.
+ int32 number = 1;
- // Setting this field to 0 is different from leaving it unset.
- optional int32 other_number = 2;
- }
+ // Setting this field to 0 is different from leaving it unset.
+ optional int32 other_number = 2;
+ }
2. RPC code generation
======================
@@ -248,64 +257,64 @@ Services may mix and match protobuf implementations within one service.
.. tip::
- The generated code includes RPC service implementation stubs. You can
- reference or copy and paste these to get started with implementing a service.
- These stub classes are generated at the bottom of the pw_rpc proto header.
+ The generated code includes RPC service implementation stubs. You can
+ reference or copy and paste these to get started with implementing a service.
+ These stub classes are generated at the bottom of the pw_rpc proto header.
- To use the stubs, do the following:
+ To use the stubs, do the following:
- #. Locate the generated RPC header in the build directory. For example:
+ #. Locate the generated RPC header in the build directory. For example:
- .. code-block:: sh
+ .. code-block:: sh
- find out/ -name <proto_name>.rpc.pwpb.h
+ find out/ -name <proto_name>.rpc.pwpb.h
- #. Scroll to the bottom of the generated RPC header.
- #. Copy the stub class declaration to a header file.
- #. Copy the member function definitions to a source file.
- #. Rename the class or change the namespace, if desired.
- #. List these files in a build target with a dependency on the
- ``pw_proto_library``.
+ #. Scroll to the bottom of the generated RPC header.
+ #. Copy the stub class declaration to a header file.
+ #. Copy the member function definitions to a source file.
+ #. Rename the class or change the namespace, if desired.
+ #. List these files in a build target with a dependency on the
+ ``pw_proto_library``.
A pw_protobuf implementation of this service would be as follows:
.. code-block:: cpp
- #include "foo_bar/the_service.rpc.pwpb.h"
+ #include "foo_bar/the_service.rpc.pwpb.h"
- namespace foo::bar {
+ namespace foo::bar {
- class TheService : public pw_rpc::pwpb::TheService::Service<TheService> {
- public:
- pw::Status MethodOne(const Request::Message& request,
- Response::Message& response) {
- // implementation
- response.number = 123;
- return pw::OkStatus();
- }
+ class TheService : public pw_rpc::pwpb::TheService::Service<TheService> {
+ public:
+ pw::Status MethodOne(const Request::Message& request,
+ Response::Message& response) {
+ // implementation
+ response.number = 123;
+ return pw::OkStatus();
+ }
- void MethodTwo(const Request::Message& request,
- ServerWriter<Response::Message>& response) {
- // implementation
- response.Write({.number = 123});
- }
- };
+ void MethodTwo(const Request::Message& request,
+ ServerWriter<Response::Message>& response) {
+ // implementation
+ response.Write({.number = 123});
+ }
+ };
- } // namespace foo::bar
+ } // namespace foo::bar
The pw_protobuf implementation would be declared in a ``BUILD.gn``:
.. code-block:: python
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_build/target_types.gni")
- pw_source_set("the_service") {
- public_configs = [ ":public" ]
- public = [ "public/foo_bar/service.h" ]
- public_deps = [ ":the_service_proto.pwpb_rpc" ]
- }
+ pw_source_set("the_service") {
+ public_configs = [ ":public" ]
+ public = [ "public/foo_bar/service.h" ]
+ public_deps = [ ":the_service_proto.pwpb_rpc" ]
+ }
4. Register the service with a server
=====================================
@@ -314,40 +323,40 @@ channel output and the example service.
.. code-block:: cpp
- // Set up the output channel for the pw_rpc server to use. This configures the
- // pw_rpc server to use HDLC over UART; projects not using UART and HDLC must
- // adapt this as necessary.
- pw::stream::SysIoWriter writer;
- pw::rpc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
- writer, pw::hdlc::kDefaultRpcAddress, "HDLC output");
+ // Set up the output channel for the pw_rpc server to use. This configures the
+ // pw_rpc server to use HDLC over UART; projects not using UART and HDLC must
+ // adapt this as necessary.
+ pw::stream::SysIoWriter writer;
+ pw::rpc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
+ writer, pw::hdlc::kDefaultRpcAddress, "HDLC output");
- // Allocate an array of channels for the server to use. If dynamic allocation
- // is enabled (PW_RPC_DYNAMIC_ALLOCATION=1), the server can be initialized
- // without any channels, and they can be added later.
- pw::rpc::Channel channels[] = {
- pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
+ // Allocate an array of channels for the server to use. If dynamic allocation
+ // is enabled (PW_RPC_DYNAMIC_ALLOCATION=1), the server can be initialized
+ // without any channels, and they can be added later.
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
- // Declare the pw_rpc server with the HDLC channel.
- pw::rpc::Server server(channels);
+ // Declare the pw_rpc server with the HDLC channel.
+ pw::rpc::Server server(channels);
- foo::bar::TheService the_service;
- pw::rpc::SomeOtherService some_other_service;
+ foo::bar::TheService the_service;
+ pw::rpc::SomeOtherService some_other_service;
- void RegisterServices() {
- // Register the foo.bar.TheService example service and another service.
- server.RegisterService(the_service, some_other_service);
- }
+ void RegisterServices() {
+ // Register the foo.bar.TheService example service and another service.
+ server.RegisterService(the_service, some_other_service);
+ }
- int main() {
- // Set up the server.
- RegisterServices();
+ int main() {
+ // Set up the server.
+ RegisterServices();
- // Declare a buffer for decoding incoming HDLC frames.
- std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+ // Declare a buffer for decoding incoming HDLC frames.
+ std::array<std::byte, kMaxTransmissionUnit> input_buffer;
- PW_LOG_INFO("Starting pw_rpc server");
- pw::hdlc::ReadAndProcessPackets(server, input_buffer);
- }
+ PW_LOG_INFO("Starting pw_rpc server");
+ pw::hdlc::ReadAndProcessPackets(server, input_buffer);
+ }
--------
Channels
@@ -360,22 +369,22 @@ assigned statically at compile time or dynamically.
.. code-block:: cpp
- // Creating a channel with the static ID 3.
- pw::rpc::Channel static_channel = pw::rpc::Channel::Create<3>(&output);
+ // Creating a channel with the static ID 3.
+ pw::rpc::Channel static_channel = pw::rpc::Channel::Create<3>(&output);
- // Grouping channel IDs within an enum can lead to clearer code.
- enum ChannelId {
- kUartChannel = 1,
- kSpiChannel = 2,
- };
+ // Grouping channel IDs within an enum can lead to clearer code.
+ enum ChannelId {
+ kUartChannel = 1,
+ kSpiChannel = 2,
+ };
- // Creating a channel with a static ID defined within an enum.
- pw::rpc::Channel another_static_channel =
- pw::rpc::Channel::Create<ChannelId::kUartChannel>(&output);
+ // Creating a channel with a static ID defined within an enum.
+ pw::rpc::Channel another_static_channel =
+ pw::rpc::Channel::Create<ChannelId::kUartChannel>(&output);
- // Creating a channel with a dynamic ID (note that no output is provided; it
- // will be set when the channel is used.
- pw::rpc::Channel dynamic_channel;
+ // Creating a channel with a dynamic ID (note that no output is provided; it
+ // will be set when the channel is used.
+ pw::rpc::Channel dynamic_channel;
Sometimes, the ID and output of a channel are not known at compile time as they
depend on information stored on the physical device. To support this use case, a
@@ -384,13 +393,13 @@ output.
.. code-block:: cpp
- // Create a dynamic channel without a compile-time ID or output.
- pw::rpc::Channel dynamic_channel;
+ // Create a dynamic channel without a compile-time ID or output.
+ pw::rpc::Channel dynamic_channel;
- void Init() {
- // Called during boot to pull the channel configuration from the system.
- dynamic_channel.Configure(GetChannelId(), some_output);
- }
+ void Init() {
+ // Called during boot to pull the channel configuration from the system.
+ dynamic_channel.Configure(GetChannelId(), some_output);
+ }
Adding and removing channels
============================
@@ -407,9 +416,9 @@ with the ``ABORTED`` status.
.. code-block:: cpp
- // When a channel is closed, any pending calls will receive
- // on_error callbacks with ABORTED status.
- client->CloseChannel(1);
+ // When a channel is closed, any pending calls will receive
+ // on_error callbacks with ABORTED status.
+ client->CloseChannel(1);
--------
Services
@@ -432,10 +441,10 @@ Protobuf library APIs
---------------------
.. toctree::
- :maxdepth: 1
+ :maxdepth: 1
- pwpb/docs
- nanopb/docs
+ pwpb/docs
+ nanopb/docs
----------------------------
Testing a pw_rpc integration
@@ -445,35 +454,35 @@ working as intended by registering the provided ``EchoService``, defined in
``echo.proto``, which echoes back a message that it receives.
.. literalinclude:: echo.proto
- :language: protobuf
- :lines: 14-
+ :language: protobuf
+ :lines: 14-
For example, in C++ with pw_protobuf:
-.. code:: c++
+.. code-block:: c++
- #include "pw_rpc/server.h"
+ #include "pw_rpc/server.h"
- // Include the apporpriate header for your protobuf library.
- #include "pw_rpc/echo_service_pwpb.h"
+ // Include the apporpriate header for your protobuf library.
+ #include "pw_rpc/echo_service_pwpb.h"
- constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
- static pw::rpc::Server server(kChannels);
+ constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
+ static pw::rpc::Server server(kChannels);
- static pw::rpc::EchoService echo_service;
+ static pw::rpc::EchoService echo_service;
- void Init() {
- server.RegisterService(echo_service);
- }
+ void Init() {
+ server.RegisterService(echo_service);
+ }
Benchmarking and stress testing
===============================
.. toctree::
- :maxdepth: 1
- :hidden:
+ :maxdepth: 1
+ :hidden:
- benchmark
+ benchmark
``pw_rpc`` provides an RPC service and Python module for stress testing and
benchmarking a ``pw_rpc`` deployment. See :ref:`module-pw_rpc-benchmark`.
@@ -507,27 +516,27 @@ For example, a service for accessing a file system should simply be named
.. code-block:: protobuf
- // file.proto
- package pw.file;
+ // file.proto
+ package pw.file;
- service FileSystem {
- rpc List(ListRequest) returns (stream ListResponse);
- }
+ service FileSystem {
+ rpc List(ListRequest) returns (stream ListResponse);
+ }
The C++ service implementation class may append "Service" to the name.
.. code-block:: cpp
- // file_system_service.h
- #include "pw_file/file.raw_rpc.pb.h"
+ // file_system_service.h
+ #include "pw_file/file.raw_rpc.pb.h"
- namespace pw::file {
+ namespace pw::file {
- class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
- void List(ConstByteSpan request, RawServerWriter& writer);
- };
+ class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
+ void List(ConstByteSpan request, RawServerWriter& writer);
+ };
- } // namespace pw::file
+ } // namespace pw::file
For upstream Pigweed services, this naming style is a requirement. Note that
some services created before this was established may use non-compliant
@@ -553,22 +562,22 @@ message payload size.
.. code-block:: cpp
- #include "pw_file/file.raw_rpc.pb.h"
- #include "pw_rpc/channel.h"
+ #include "pw_file/file.raw_rpc.pb.h"
+ #include "pw_rpc/channel.h"
- namespace pw::file {
+ namespace pw::file {
- class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
- public:
- void List(ConstByteSpan request, RawServerWriter& writer);
+ class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
+ public:
+ void List(ConstByteSpan request, RawServerWriter& writer);
- private:
- // Allocate a buffer for building proto responses.
- static constexpr size_t kEncodeBufferSize = pw::rpc::MaxSafePayloadSize();
- std::array<std::byte, kEncodeBufferSize> encode_buffer_;
- };
+ private:
+ // Allocate a buffer for building proto responses.
+ static constexpr size_t kEncodeBufferSize = pw::rpc::MaxSafePayloadSize();
+ std::array<std::byte, kEncodeBufferSize> encode_buffer_;
+ };
- } // namespace pw::file
+ } // namespace pw::file
--------------------
Protocol description
@@ -584,8 +593,8 @@ encoded as protocol buffers. The full packet format is described in
``pw_rpc/pw_rpc/internal/packet.proto``.
.. literalinclude:: internal/packet.proto
- :language: protobuf
- :lines: 14-
+ :language: protobuf
+ :lines: 14-
The packet type and RPC type determine which fields are present in a Pigweed RPC
packet. Each packet type is only sent by either the client or the server.
@@ -593,53 +602,53 @@ These tables describe the meaning of and fields included with each packet type.
Client-to-server packets
------------------------
-+-------------------+-------------------------------------+
-| packet type | description |
-+===================+=====================================+
-| REQUEST | Invoke an RPC |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - payload |
-| | (unary & server streaming only) |
-| | - call_id (optional) |
-| | |
-+-------------------+-------------------------------------+
-| CLIENT_STREAM | Message in a client stream |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - payload |
-| | - call_id (if set in REQUEST) |
-| | |
-+-------------------+-------------------------------------+
-| CLIENT_STREAM_END | Client stream is complete |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - call_id (if set in REQUEST) |
-| | |
-+-------------------+-------------------------------------+
-| CLIENT_ERROR | Abort an ongoing RPC |
-| | |
-| | .. code-block:: text |
-| | |
-| | - channel_id |
-| | - service_id |
-| | - method_id |
-| | - status |
-| | - call_id (if set in REQUEST) |
-| | |
-+-------------------+-------------------------------------+
++---------------------------+-------------------------------------+
+| packet type | description |
++===========================+=====================================+
+| REQUEST | Invoke an RPC |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - payload |
+| | (unary & server streaming only) |
+| | - call_id (optional) |
+| | |
++---------------------------+-------------------------------------+
+| CLIENT_STREAM | Message in a client stream |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - payload |
+| | - call_id (if set in REQUEST) |
+| | |
++---------------------------+-------------------------------------+
+| CLIENT_REQUEST_COMPLETION | Client requested stream completion |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - call_id (if set in REQUEST) |
+| | |
++---------------------------+-------------------------------------+
+| CLIENT_ERROR | Abort an ongoing RPC |
+| | |
+| | .. code-block:: text |
+| | |
+| | - channel_id |
+| | - service_id |
+| | - method_id |
+| | - status |
+| | - call_id (if set in REQUEST) |
+| | |
++---------------------------+-------------------------------------+
**Client errors**
@@ -730,14 +739,14 @@ client streaming, and bidirectional streaming.
The basic flow for all RPC invocations is as follows:
- * Client sends a ``REQUEST`` packet. Includes a payload for unary & server
- streaming RPCs.
- * For client and bidirectional streaming RPCs, the client may send any number
- of ``CLIENT_STREAM`` packets with payloads.
- * For server and bidirectional streaming RPCs, the server may send any number
- of ``SERVER_STREAM`` packets.
- * The server sends a ``RESPONSE`` packet. Includes a payload for unary &
- client streaming RPCs. The RPC is complete.
+* Client sends a ``REQUEST`` packet. Includes a payload for unary & server
+ streaming RPCs.
+* For client and bidirectional streaming RPCs, the client may send any number of
+ ``CLIENT_STREAM`` packets with payloads.
+* For server and bidirectional streaming RPCs, the server may send any number of
+ ``SERVER_STREAM`` packets.
+* The server sends a ``RESPONSE`` packet. Includes a payload for unary & client
+ streaming RPCs. The RPC is complete.
The client may cancel an ongoing RPC at any time by sending a ``CLIENT_ERROR``
packet with status ``CANCELLED``. The server may finish an ongoing RPC at any
@@ -748,59 +757,183 @@ Unary RPC
In a unary RPC, the client sends a single request and the server sends a single
response.
-.. image:: unary_rpc.svg
+.. mermaid::
+ :alt: Unary RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>+S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S->>-C: response
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>payload<br>status
The client may attempt to cancel a unary RPC by sending a ``CLIENT_ERROR``
packet with status ``CANCELLED``. The server sends no response to a cancelled
RPC. If the server processes the unary RPC synchronously (the handling thread
sends the response), it may not be possible to cancel the RPC.
-.. image:: unary_rpc_cancelled.svg
+.. mermaid::
+ :alt: Cancelled Unary RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>+S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: cancel
+ Note left of C: PacketType.CLIENT_ERROR<br>channel ID<br>service ID<br>method ID<br>status=CANCELLED
+
Server streaming RPC
--------------------
In a server streaming RPC, the client sends a single request and the server
sends any number of ``SERVER_STREAM`` packets followed by a ``RESPONSE`` packet.
-.. image:: server_streaming_rpc.svg
+.. mermaid::
+ :alt: Server Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>+S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S-->>C: messages (zero or more)
+ Note right of S: PacketType.SERVER_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S->>-C: done
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>status
+
The client may terminate a server streaming RPC by sending a ``CLIENT_STREAM``
packet with status ``CANCELLED``. The server sends no response.
-.. image:: server_streaming_rpc_cancelled.svg
+.. mermaid::
+ :alt: Cancelled Server Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: request
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ S-->>C: messages (zero or more)
+ Note right of S: PacketType.SERVER_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: cancel
+ Note left of C: PacketType.CLIENT_ERROR<br>channel ID<br>service ID<br>method ID<br>status=CANCELLED
+
Client streaming RPC
--------------------
In a client streaming RPC, the client starts the RPC by sending a ``REQUEST``
packet with no payload. It then sends any number of messages in
-``CLIENT_STREAM`` packets, followed by a ``CLIENT_STREAM_END``. The server sends
+``CLIENT_STREAM`` packets, followed by a ``CLIENT_REQUEST_COMPLETION``. The server sends
a single ``RESPONSE`` to finish the RPC.
-.. image:: client_streaming_rpc.svg
+.. mermaid::
+ :alt: Client Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: done
+ Note left of C: PacketType.CLIENT_REQUEST_COMPLETION<br>channel ID<br>service ID<br>method ID
+
+ S->>C: response
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>payload<br>status
The server may finish the RPC at any time by sending its ``RESPONSE`` packet,
-even if it has not yet received the ``CLIENT_STREAM_END`` packet. The client may
+even if it has not yet received the ``CLIENT_REQUEST_COMPLETION`` packet. The client may
terminate the RPC at any time by sending a ``CLIENT_ERROR`` packet with status
``CANCELLED``.
-.. image:: client_streaming_rpc_cancelled.svg
+.. mermaid::
+ :alt: Cancelled Client Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: cancel
+ Note right of S: PacketType.CLIENT_ERROR<br>channel ID<br>service ID<br>method ID<br>status=CANCELLED
Bidirectional streaming RPC
---------------------^^^^^^^
+---------------------------
In a bidirectional streaming RPC, the client sends any number of requests and
the server sends any number of responses. The client invokes the RPC by sending
-a ``REQUEST`` with no payload. It sends a ``CLIENT_STREAM_END`` packet when it
+a ``REQUEST`` with no payload. It sends a ``CLIENT_REQUEST_COMPLETION`` packet when it
has finished sending requests. The server sends a ``RESPONSE`` packet to finish
the RPC.
-.. image:: bidirectional_streaming_rpc.svg
+.. mermaid::
+ :alt: Bidirectional Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->S: (messages in any order)
+
+ S-->>C: messages (zero or more)
+ Note right of S: PacketType.SERVER_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: done
+ Note left of C: PacketType.CLIENT_REQUEST_COMPLETION<br>channel ID<br>service ID<br>method ID
+
+ S->>C: done
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>status
+
The server may finish the RPC at any time by sending the ``RESPONSE`` packet,
-even if it has not received the ``CLIENT_STREAM_END`` packet. The client may
+even if it has not received the ``CLIENT_REQUEST_COMPLETION`` packet. The client may
terminate the RPC at any time by sending a ``CLIENT_ERROR`` packet with status
``CANCELLED``.
-.. image:: bidirectional_streaming_rpc_cancelled.svg
+.. mermaid::
+ :alt: Client Streaming RPC
+ :align: center
+
+ sequenceDiagram
+ participant C as Client
+ participant S as Server
+ C->>S: start
+ Note left of C: PacketType.REQUEST<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C-->>S: messages (zero or more)
+ Note left of C: PacketType.CLIENT_STREAM<br>channel ID<br>service ID<br>method ID<br>payload
+
+ C->>S: done
+ Note left of C: PacketType.CLIENT_REQUEST_COMPLETION<br>channel ID<br>service ID<br>method ID
+
+ S->>C: response
+ Note right of S: PacketType.RESPONSE<br>channel ID<br>service ID<br>method ID<br>payload<br>status
-------
C++ API
@@ -812,7 +945,7 @@ Declare an instance of ``rpc::Server`` and register services with it.
.. admonition:: TODO
- Document the public interface
+ Document the public interface
Size report
-----------
@@ -851,12 +984,47 @@ Packet flow
Requests
........
-.. image:: request_packets.svg
+.. mermaid::
+ :alt: Request Packet Flow
+
+ flowchart LR
+ packets[Packets]
+
+ subgraph pw_rpc [pw_rpc Library]
+ direction TB
+ internalMethod[[internal::Method]]
+ Server --> Service --> internalMethod
+ end
+
+ packets --> Server
+
+ generatedServices{{generated services}}
+ userDefinedRPCs(user-defined RPCs)
+
+ generatedServices --> userDefinedRPCs
+ internalMethod --> generatedServices
Responses
.........
-.. image:: response_packets.svg
+.. mermaid::
+ :alt: Request Packet Flow
+
+ flowchart LR
+ generatedServices{{generated services}}
+ userDefinedRPCs(user-defined RPCs)
+
+ subgraph pw_rpc [pw_rpc Library]
+ direction TB
+ internalMethod[[internal::Method]]
+ internalMethod --> Server --> Channel
+ end
+
+ packets[Packets]
+ Channel --> packets
+
+ userDefinedRPCs --> generatedServices
+ generatedServices --> internalMethod
RPC client
==========
@@ -872,22 +1040,22 @@ clients cannot use the same channels.
To send incoming RPC packets from the transport layer to be processed by a
client, the client's ``ProcessPacket`` function is called with the packet data.
-.. code:: c++
+.. code-block:: c++
- #include "pw_rpc/client.h"
+ #include "pw_rpc/client.h"
- namespace {
+ namespace {
- pw::rpc::Channel my_channels[] = {
- pw::rpc::Channel::Create<1>(&my_channel_output)};
- pw::rpc::Client my_client(my_channels);
+ pw::rpc::Channel my_channels[] = {
+ pw::rpc::Channel::Create<1>(&my_channel_output)};
+ pw::rpc::Client my_client(my_channels);
- } // namespace
+ } // namespace
- // Called when the transport layer receives an RPC packet.
- void ProcessRpcPacket(ConstByteSpan packet) {
- my_client.ProcessPacket(packet);
- }
+ // Called when the transport layer receives an RPC packet.
+ void ProcessRpcPacket(ConstByteSpan packet) {
+ my_client.ProcessPacket(packet);
+ }
Note that client processing such as callbacks will be invoked within
the body of ``ProcessPacket``.
@@ -915,54 +1083,55 @@ the ongoing RPC call, and can be used to manage it. An RPC call is only active
as long as its call object is alive.
.. tip::
- Use ``std::move`` when passing around call objects to keep RPCs alive.
+
+ Use ``std::move`` when passing around call objects to keep RPCs alive.
Example
^^^^^^^
.. code-block:: c++
- #include "pw_rpc/echo_service_nanopb.h"
-
- namespace {
- // Generated clients are namespaced with their proto library.
- using EchoClient = pw_rpc::nanopb::EchoService::Client;
-
- // RPC channel ID on which to make client calls. RPC calls cannot be made on
- // channel 0 (Channel::kUnassignedChannelId).
- constexpr uint32_t kDefaultChannelId = 1;
-
- pw::rpc::NanopbUnaryReceiver<pw_rpc_EchoMessage> echo_call;
-
- // Callback invoked when a response is received. This is called synchronously
- // from Client::ProcessPacket.
- void EchoResponse(const pw_rpc_EchoMessage& response,
- pw::Status status) {
- if (status.ok()) {
- PW_LOG_INFO("Received echo response: %s", response.msg);
- } else {
- PW_LOG_ERROR("Echo failed with status %d",
- static_cast<int>(status.code()));
- }
- }
-
- } // namespace
-
- void CallEcho(const char* message) {
- // Create a client to call the EchoService.
- EchoClient echo_client(my_rpc_client, kDefaultChannelId);
-
- pw_rpc_EchoMessage request{};
- pw::string::Copy(message, request.msg);
-
- // By assigning the returned call to the global echo_call, the RPC
- // call is kept alive until it completes. When a response is received, it
- // will be logged by the handler function and the call will complete.
- echo_call = echo_client.Echo(request, EchoResponse);
- if (!echo_call.active()) {
- // The RPC call was not sent. This could occur due to, for example, an
- // invalid channel ID. Handle if necessary.
- }
- }
+ #include "pw_rpc/echo_service_nanopb.h"
+
+ namespace {
+ // Generated clients are namespaced with their proto library.
+ using EchoClient = pw_rpc::nanopb::EchoService::Client;
+
+ // RPC channel ID on which to make client calls. RPC calls cannot be made on
+ // channel 0 (Channel::kUnassignedChannelId).
+ constexpr uint32_t kDefaultChannelId = 1;
+
+ pw::rpc::NanopbUnaryReceiver<pw_rpc_EchoMessage> echo_call;
+
+ // Callback invoked when a response is received. This is called synchronously
+ // from Client::ProcessPacket.
+ void EchoResponse(const pw_rpc_EchoMessage& response,
+ pw::Status status) {
+ if (status.ok()) {
+ PW_LOG_INFO("Received echo response: %s", response.msg);
+ } else {
+ PW_LOG_ERROR("Echo failed with status %d",
+ static_cast<int>(status.code()));
+ }
+ }
+
+ } // namespace
+
+ void CallEcho(const char* message) {
+ // Create a client to call the EchoService.
+ EchoClient echo_client(my_rpc_client, kDefaultChannelId);
+
+ pw_rpc_EchoMessage request{};
+ pw::string::Copy(message, request.msg);
+
+ // By assigning the returned call to the global echo_call, the RPC
+ // call is kept alive until it completes. When a response is received, it
+ // will be logged by the handler function and the call will complete.
+ echo_call = echo_client.Echo(request, EchoResponse);
+ if (!echo_call.active()) {
+ // The RPC call was not sent. This could occur due to, for example, an
+ // invalid channel ID. Handle if necessary.
+ }
+ }
Call objects
============
@@ -974,23 +1143,23 @@ server or client.
The public call types are as follows:
.. list-table::
- :header-rows: 1
-
- * - RPC Type
- - Server call
- - Client call
- * - Unary
- - ``(Raw|Nanopb|Pwpb)UnaryResponder``
- - ``(Raw|Nanopb|Pwpb)UnaryReceiver``
- * - Server streaming
- - ``(Raw|Nanopb|Pwpb)ServerWriter``
- - ``(Raw|Nanopb|Pwpb)ClientReader``
- * - Client streaming
- - ``(Raw|Nanopb|Pwpb)ServerReader``
- - ``(Raw|Nanopb|Pwpb)ClientWriter``
- * - Bidirectional streaming
- - ``(Raw|Nanopb|Pwpb)ServerReaderWriter``
- - ``(Raw|Nanopb|Pwpb)ClientReaderWriter``
+ :header-rows: 1
+
+ * - RPC Type
+ - Server call
+ - Client call
+ * - Unary
+ - ``(Raw|Nanopb|Pwpb)UnaryResponder``
+ - ``(Raw|Nanopb|Pwpb)UnaryReceiver``
+ * - Server streaming
+ - ``(Raw|Nanopb|Pwpb)ServerWriter``
+ - ``(Raw|Nanopb|Pwpb)ClientReader``
+ * - Client streaming
+ - ``(Raw|Nanopb|Pwpb)ServerReader``
+ - ``(Raw|Nanopb|Pwpb)ClientWriter``
+ * - Bidirectional streaming
+ - ``(Raw|Nanopb|Pwpb)ServerReaderWriter``
+ - ``(Raw|Nanopb|Pwpb)ClientReaderWriter``
Client call API
---------------
@@ -998,67 +1167,68 @@ Client call objects provide a few common methods.
.. cpp:class:: pw::rpc::ClientCallType
- The ``ClientCallType`` will be one of the following types:
+ The ``ClientCallType`` will be one of the following types:
- - ``(Raw|Nanopb|Pwpb)UnaryReceiver`` for unary
- - ``(Raw|Nanopb|Pwpb)ClientReader`` for server streaming
- - ``(Raw|Nanopb|Pwpb)ClientWriter`` for client streaming
- - ``(Raw|Nanopb|Pwpb)ClientReaderWriter`` for bidirectional streaming
+ - ``(Raw|Nanopb|Pwpb)UnaryReceiver`` for unary
+ - ``(Raw|Nanopb|Pwpb)ClientReader`` for server streaming
+ - ``(Raw|Nanopb|Pwpb)ClientWriter`` for client streaming
+ - ``(Raw|Nanopb|Pwpb)ClientReaderWriter`` for bidirectional streaming
- .. cpp:function:: bool active() const
+ .. cpp:function:: bool active() const
- Returns true if the call is active.
+ Returns true if the call is active.
- .. cpp:function:: uint32_t channel_id() const
+ .. cpp:function:: uint32_t channel_id() const
- Returns the channel ID of this call, which is 0 if the call is inactive.
+ Returns the channel ID of this call, which is 0 if the call is inactive.
- .. cpp:function:: uint32_t id() const
+ .. cpp:function:: uint32_t id() const
- Returns the call ID, a unique identifier for this call.
+ Returns the call ID, a unique identifier for this call.
- .. cpp:function:: void Write(RequestType)
+ .. cpp:function:: void Write(RequestType)
- Only available on client and bidirectional streaming calls. Sends a stream
- request. Returns:
+ Only available on client and bidirectional streaming calls. Sends a stream
+ request. Returns:
- - ``OK`` - the request was successfully sent
- - ``FAILED_PRECONDITION`` - the writer is closed
- - ``INTERNAL`` - pw_rpc was unable to encode message; does not apply to raw
- calls
- - other errors - the :cpp:class:`ChannelOutput` failed to send the packet;
- the error codes are determined by the :cpp:class:`ChannelOutput`
- implementation
+ - ``OK`` - the request was successfully sent
+ - ``FAILED_PRECONDITION`` - the writer is closed
+ - ``INTERNAL`` - pw_rpc was unable to encode message; does not apply to
+ raw calls
+ - other errors - the :cpp:class:`ChannelOutput` failed to send the packet;
+ the error codes are determined by the :cpp:class:`ChannelOutput`
+ implementation
- .. cpp:function:: pw::Status CloseClientStream()
+ .. cpp:function:: pw::Status RequestCompletion()
- Only available on client and bidirectional streaming calls. Notifies the
- server that no further client stream messages will be sent.
+ Notifies the server that client has requested for call completion. On
+ client and bidirectional streaming calls no further client stream messages
+ will be sent.
- .. cpp:function:: pw::Status Cancel()
+ .. cpp:function:: pw::Status Cancel()
- Cancels this RPC. Closes the call and sends a ``CANCELLED`` error to the
- server. Return statuses are the same as :cpp:func:`Write`.
+ Cancels this RPC. Closes the call and sends a ``CANCELLED`` error to the
+ server. Return statuses are the same as :cpp:func:`Write`.
- .. cpp:function:: void Abandon()
+ .. cpp:function:: void Abandon()
- Closes this RPC locally. Sends a ``CLIENT_STREAM_END``, but no cancellation
- packet. Future packets for this RPC are dropped, and the client sends a
- ``FAILED_PRECONDITION`` error in response because the call is not active.
+ Closes this RPC locally. Sends a ``CLIENT_REQUEST_COMPLETION``, but no cancellation
+ packet. Future packets for this RPC are dropped, and the client sends a
+ ``FAILED_PRECONDITION`` error in response because the call is not active.
- .. cpp:function:: void set_on_completed(pw::Function<void(ResponseTypeIfUnaryOnly, pw::Status)>)
+ .. cpp:function:: void set_on_completed(pw::Function<void(ResponseTypeIfUnaryOnly, pw::Status)>)
- Sets the callback that is called when the RPC completes normally. The
- signature depends on whether the call has a unary or stream response.
+ Sets the callback that is called when the RPC completes normally. The
+ signature depends on whether the call has a unary or stream response.
- .. cpp:function:: void set_on_error(pw::Function<void(pw::Status)>)
+ .. cpp:function:: void set_on_error(pw::Function<void(pw::Status)>)
- Sets the callback that is called when the RPC is terminated due to an error.
+ Sets the callback that is called when the RPC is terminated due to an error.
- .. cpp:function:: void set_on_next(pw::Function<void(ResponseType)>)
+ .. cpp:function:: void set_on_next(pw::Function<void(ResponseType)>)
- Only available on server and bidirectional streaming calls. Sets the callback
- that is called for each stream response.
+ Only available on server and bidirectional streaming calls. Sets the callback
+ that is called for each stream response.
Callbacks
---------
@@ -1066,33 +1236,33 @@ The C++ call objects allow users to set callbacks that are invoked when RPC
`events`_ occur.
.. list-table::
- :header-rows: 1
-
- * - Name
- - Stream signature
- - Non-stream signature
- - Server
- - Client
- * - ``on_error``
- - ``void(pw::Status)``
- - ``void(pw::Status)``
- - ✅
- - ✅
- * - ``on_next``
- - n/a
- - ``void(const PayloadType&)``
- - ✅
- - ✅
- * - ``on_completed``
- - ``void(pw::Status)``
- - ``void(const PayloadType&, pw::Status)``
- -
- - ✅
- * - ``on_client_stream_end``
- - ``void()``
- - n/a
- - ✅ (:c:macro:`optional <PW_RPC_CLIENT_STREAM_END_CALLBACK>`)
- -
+ :header-rows: 1
+
+ * - Name
+ - Stream signature
+ - Non-stream signature
+ - Server
+ - Client
+ * - ``on_error``
+ - ``void(pw::Status)``
+ - ``void(pw::Status)``
+ - ✅
+ - ✅
+ * - ``on_next``
+ - n/a
+ - ``void(const PayloadType&)``
+ - ✅
+ - ✅
+ * - ``on_completed``
+ - ``void(pw::Status)``
+ - ``void(const PayloadType&, pw::Status)``
+ -
+ - ✅
+ * - ``on_client_requested_completion``
+ - ``void()``
+ - n/a
+ - ✅ (:c:macro:`optional <PW_RPC_COMPLETION_REQUEST_CALLBACK>`)
+ -
Limitations and restrictions
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -1146,9 +1316,9 @@ Example warning for a dropped stream packet:
.. code-block:: text
- WRN Received stream packet for 1:cc0f6de0/31e616ce before the callback for
- a previous packet completed! This packet will be dropped. This can be
- avoided by handling packets for a particular RPC on only one thread.
+ WRN Received stream packet for 1:cc0f6de0/31e616ce before the callback for
+ a previous packet completed! This packet will be dropped. This can be
+ avoided by handling packets for a particular RPC on only one thread.
RPC calls introspection
=======================
@@ -1165,110 +1335,97 @@ We have an RPC service ``SpecialService`` with ``MyMethod`` method:
.. code-block:: protobuf
- package some.package;
- service SpecialService {
- rpc MyMethod(MyMethodRequest) returns (MyMethodResponse) {}
- }
+ package some.package;
+ service SpecialService {
+ rpc MyMethod(MyMethodRequest) returns (MyMethodResponse) {}
+ }
We also have a templated Storage type alias:
.. code-block:: c++
- template <auto kMethod>
- using Storage =
- std::pair<MethodRequestType<kMethod>, MethodResponseType<kMethod>>;
+ template <auto kMethod>
+ using Storage =
+ std::pair<MethodRequestType<kMethod>, MethodResponseType<kMethod>>;
``Storage<some::package::pw_rpc::pwpb::SpecialService::MyMethod>`` will
instantiate as:
.. code-block:: c++
- std::pair<some::package::MyMethodRequest::Message,
- some::package::MyMethodResponse::Message>;
+ std::pair<some::package::MyMethodRequest::Message,
+ some::package::MyMethodResponse::Message>;
.. note::
- Only nanopb and pw_protobuf have real types as
- ``MethodRequestType<RpcMethod>``/``MethodResponseType<RpcMethod>``. Raw has
- them both set as ``void``. In reality, they are ``pw::ConstByteSpan``. Any
- helper/trait that wants to use this types for raw methods should do a custom
- implementation that copies the bytes under the span instead of copying just
- the span.
+ Only nanopb and pw_protobuf have real types as
+ ``MethodRequestType<RpcMethod>``/``MethodResponseType<RpcMethod>``. Raw has
+ them both set as ``void``. In reality, they are ``pw::ConstByteSpan``. Any
+ helper/trait that wants to use this types for raw methods should do a custom
+ implementation that copies the bytes under the span instead of copying just
+ the span.
-Client Synchronous Call wrappers
-================================
-If synchronous behavior is desired when making client calls, users can use one
-of the ``SynchronousCall<RpcMethod>`` wrapper functions to make their RPC call.
-These wrappers effectively wrap the asynchronous Client RPC call with a timed
-thread notification and return once a result is known or a timeout has occurred.
-These return a ``SynchronousCallResult<Response>`` object, which can be queried
-to determine whether any error scenarios occurred and, if not, access the
-response.
-
-``SynchronousCall<RpcMethod>`` will block indefinitely, whereas
-``SynchronousCallFor<RpcMethod>`` and ``SynchronousCallUntil<RpcMethod>`` will
-block for a given timeout or until a deadline, respectively. All wrappers work
-with both the standalone static RPC functions and the generated Client member
-methods.
+.. _module-pw_rpc-client-sync-call-wrappers:
-.. note:: Use of the SynchronousCall wrappers requires a TimedThreadNotification
- backend.
-.. note:: Only nanopb and pw_protobuf Unary RPC methods are supported.
+Client synchronous call wrappers
+================================
+.. doxygenfile:: pw_rpc/synchronous_call.h
+ :sections: detaileddescription
Example
-------
.. code-block:: c++
- #include "pw_rpc/synchronous_call.h"
-
- void InvokeUnaryRpc() {
- pw::rpc::Client client;
- pw::rpc::Channel channel;
-
- RoomInfoRequest request;
- SynchronousCallResult<RoomInfoResponse> result =
- SynchronousCall<Chat::GetRoomInformation>(client, channel.id(), request);
-
- if (result.is_rpc_error()) {
- ShutdownClient(client);
- } else if (result.is_server_error()) {
- HandleServerError(result.status());
- } else if (result.is_timeout()) {
- // SynchronousCall will block indefinitely, so we should never get here.
- PW_UNREACHABLE();
- }
- HandleRoomInformation(std::move(result).response());
- }
-
- void AnotherExample() {
- pw_rpc::nanopb::Chat::Client chat_client(client, channel);
- constexpr auto kTimeout = pw::chrono::SystemClock::for_at_least(500ms);
-
- RoomInfoRequest request;
- auto result = SynchronousCallFor<Chat::GetRoomInformation>(
- chat_client, request, kTimeout);
-
- if (result.is_timeout()) {
- RetryRoomRequest();
- } else {
- ...
- }
- }
-
-The ``SynchronousCallResult<Response>`` is also compatible with the PW_TRY
-family of macros, but users should be aware that their use will lose information
-about the type of error. This should only be used if the caller will handle all
-error scenarios the same.
+ #include "pw_rpc/synchronous_call.h"
+
+ void InvokeUnaryRpc() {
+ pw::rpc::Client client;
+ pw::rpc::Channel channel;
+
+ RoomInfoRequest request;
+ SynchronousCallResult<RoomInfoResponse> result =
+ SynchronousCall<Chat::GetRoomInformation>(client, channel.id(), request);
+
+ if (result.is_rpc_error()) {
+ ShutdownClient(client);
+ } else if (result.is_server_error()) {
+ HandleServerError(result.status());
+ } else if (result.is_timeout()) {
+ // SynchronousCall will block indefinitely, so we should never get here.
+ PW_UNREACHABLE();
+ }
+ HandleRoomInformation(std::move(result).response());
+ }
+
+ void AnotherExample() {
+ pw_rpc::nanopb::Chat::Client chat_client(client, channel);
+ constexpr auto kTimeout = pw::chrono::SystemClock::for_at_least(500ms);
+
+ RoomInfoRequest request;
+ auto result = SynchronousCallFor<Chat::GetRoomInformation>(
+ chat_client, request, kTimeout);
+
+ if (result.is_timeout()) {
+ RetryRoomRequest();
+ } else {
+ ...
+ }
+ }
+
+The ``SynchronousCallResult<Response>`` is also compatible with the
+:c:macro:`PW_TRY` family of macros, but users should be aware that their use
+will lose information about the type of error. This should only be used if the
+caller will handle all error scenarios the same.
.. code-block:: c++
- pw::Status SyncRpc() {
- const RoomInfoRequest request;
- PW_TRY_ASSIGN(const RoomInfoResponse& response,
- SynchronousCall<Chat::GetRoomInformation>(client, request));
- HandleRoomInformation(response);
- return pw::OkStatus();
- }
+ pw::Status SyncRpc() {
+ const RoomInfoRequest request;
+ PW_TRY_ASSIGN(const RoomInfoResponse& response,
+ SynchronousCall<Chat::GetRoomInformation>(client, request));
+ HandleRoomInformation(response);
+ return pw::OkStatus();
+ }
ClientServer
============
@@ -1281,17 +1438,17 @@ an RPC client and server with the same set of channels.
.. code-block:: cpp
- pw::rpc::Channel channels[] = {
- pw::rpc::Channel::Create<1>(&channel_output)};
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&channel_output)};
- // Creates both a client and a server.
- pw::rpc::ClientServer client_server(channels);
+ // Creates both a client and a server.
+ pw::rpc::ClientServer client_server(channels);
- void ProcessRpcData(pw::ConstByteSpan packet) {
- // Calls into both the client and the server, sending the packet to the
- // appropriate one.
- client_server.ProcessPacket(packet);
- }
+ void ProcessRpcData(pw::ConstByteSpan packet) {
+ // Calls into both the client and the server, sending the packet to the
+ // appropriate one.
+ client_server.ProcessPacket(packet);
+ }
Testing
=======
@@ -1305,7 +1462,7 @@ are supported. Code that uses the raw API may be tested with the raw test
helpers, and vice versa. The Nanopb and Pwpb APIs also provides a test helper
with a real client-server pair that supports testing of asynchronous messaging.
-To test sychronous code that invokes RPCs, declare a ``RawClientTestContext``,
+To test synchronous code that invokes RPCs, declare a ``RawClientTestContext``,
``PwpbClientTestContext``, or ``NanopbClientTestContext``. These test context
objects provide a preconfigured RPC client, channel, server fake, and buffer for
encoding packets.
@@ -1322,42 +1479,42 @@ the expected data was sent and then simulates a response from the server.
.. code-block:: cpp
- #include "pw_rpc/raw/client_testing.h"
+ #include "pw_rpc/raw/client_testing.h"
- class ClientUnderTest {
- public:
- // To support injecting an RPC client for testing, classes that make RPC
- // calls should take an RPC client and channel ID or an RPC service client
- // (e.g. pw_rpc::raw::MyService::Client).
- ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
+ class ClientUnderTest {
+ public:
+ // To support injecting an RPC client for testing, classes that make RPC
+ // calls should take an RPC client and channel ID or an RPC service client
+ // (e.g. pw_rpc::raw::MyService::Client).
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
- void DoSomethingThatInvokesAnRpc();
+ void DoSomethingThatInvokesAnRpc();
- bool SetToTrueWhenRpcCompletes();
- };
+ bool SetToTrueWhenRpcCompletes();
+ };
- TEST(TestAThing, InvokesRpcAndHandlesResponse) {
- RawClientTestContext context;
- ClientUnderTest thing(context.client(), context.channel().id());
+ TEST(TestAThing, InvokesRpcAndHandlesResponse) {
+ RawClientTestContext context;
+ ClientUnderTest thing(context.client(), context.channel().id());
- // Execute the code that invokes the MyService.TheMethod RPC.
- things.DoSomethingThatInvokesAnRpc();
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ things.DoSomethingThatInvokesAnRpc();
- // Find and verify the payloads sent for the MyService.TheMethod RPC.
- auto msgs = context.output().payloads<pw_rpc::raw::MyService::TheMethod>();
- ASSERT_EQ(msgs.size(), 1u);
+ // Find and verify the payloads sent for the MyService.TheMethod RPC.
+ auto msgs = context.output().payloads<pw_rpc::raw::MyService::TheMethod>();
+ ASSERT_EQ(msgs.size(), 1u);
- VerifyThatTheExpectedMessageWasSent(msgs.back());
+ VerifyThatTheExpectedMessageWasSent(msgs.back());
- // Send the response packet from the server and verify that the class reacts
- // accordingly.
- EXPECT_FALSE(thing.SetToTrueWhenRpcCompletes());
+ // Send the response packet from the server and verify that the class reacts
+ // accordingly.
+ EXPECT_FALSE(thing.SetToTrueWhenRpcCompletes());
- context_.server().SendResponse<pw_rpc::raw::MyService::TheMethod>(
- final_message, OkStatus());
+ context_.server().SendResponse<pw_rpc::raw::MyService::TheMethod>(
+ final_message, OkStatus());
- EXPECT_TRUE(thing.SetToTrueWhenRpcCompletes());
- }
+ EXPECT_TRUE(thing.SetToTrueWhenRpcCompletes());
+ }
To test client code that uses asynchronous responses, encapsulates multiple
rpc calls to one or more services, or uses a custom service implemenation,
@@ -1379,47 +1536,62 @@ response is received. It verifies that expected data was both sent and received.
.. code-block:: cpp
- #include "my_library_protos/my_service.rpc.pb.h"
- #include "pw_rpc/nanopb/client_server_testing_threaded.h"
- #include "pw_thread_stl/options.h"
-
- class ClientUnderTest {
- public:
- // To support injecting an RPC client for testing, classes that make RPC
- // calls should take an RPC client and channel ID or an RPC service client
- // (e.g. pw_rpc::raw::MyService::Client).
- ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
-
- Status BlockOnResponse(uint32_t value);
- };
-
-
- class TestService final : public MyService<TestService> {
- public:
- Status TheMethod(const pw_rpc_test_TheMethod& request,
- pw_rpc_test_TheMethod& response) {
- response.value = request.integer + 1;
- return pw::OkStatus();
- }
- };
-
- TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
- NanopbClientServerTestContextThreaded<> ctx(pw::thread::stl::Options{});
- TestService service;
- ctx.server().RegisterService(service);
- ClientUnderTest client(ctx.client(), ctx.channel().id());
-
- // Execute the code that invokes the MyService.TheMethod RPC.
- constexpr uint32_t value = 1;
- const auto result = client.BlockOnResponse(value);
- const auto request = ctx.request<MyService::TheMethod>(0);
- const auto response = ctx.resonse<MyService::TheMethod>(0);
-
- // Verify content of messages
- EXPECT_EQ(result, pw::OkStatus());
- EXPECT_EQ(request.value, value);
- EXPECT_EQ(response.value, value + 1);
- }
+ #include "my_library_protos/my_service.rpc.pb.h"
+ #include "pw_rpc/nanopb/client_server_testing_threaded.h"
+ #include "pw_thread_stl/options.h"
+
+ class ClientUnderTest {
+ public:
+ // To support injecting an RPC client for testing, classes that make RPC
+ // calls should take an RPC client and channel ID or an RPC service client
+ // (e.g. pw_rpc::raw::MyService::Client).
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
+
+ Status BlockOnResponse(uint32_t value);
+ };
+
+
+ class TestService final : public MyService<TestService> {
+ public:
+ Status TheMethod(const pw_rpc_test_TheMethod& request,
+ pw_rpc_test_TheMethod& response) {
+ response.value = request.integer + 1;
+ return pw::OkStatus();
+ }
+ };
+
+ TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
+ NanopbClientServerTestContextThreaded<> ctx(pw::thread::stl::Options{});
+ TestService service;
+ ctx.server().RegisterService(service);
+ ClientUnderTest client(ctx.client(), ctx.channel().id());
+
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ constexpr uint32_t value = 1;
+ const auto result = client.BlockOnResponse(value);
+ const auto request = ctx.request<MyService::TheMethod>(0);
+ const auto response = ctx.resonse<MyService::TheMethod>(0);
+
+ // Verify content of messages
+ EXPECT_EQ(result, pw::OkStatus());
+ EXPECT_EQ(request.value, value);
+ EXPECT_EQ(response.value, value + 1);
+ }
+
+Use the context's
+``response(uint32_t index, Response<kMethod>& response)`` to decode messages
+into a provided response object. You would use this version if decoder callbacks
+are needed to fully decode a message. For instance if it uses ``repeated``
+fields.
+
+.. code-block:: cpp
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response<test::GeneratedService::TestAnotherUnaryRpc>(0, response);
Synchronous versions of these test contexts also exist that may be used on
non-threaded systems ``NanopbClientServerTestContext`` and
@@ -1433,45 +1605,79 @@ to with a test service implemenation.
.. code-block:: cpp
- #include "my_library_protos/my_service.rpc.pb.h"
- #include "pw_rpc/nanopb/client_server_testing.h"
-
- class ClientUnderTest {
- public:
- ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
-
- Status SendRpcCall(uint32_t value);
- };
-
-
- class TestService final : public MyService<TestService> {
- public:
- Status TheMethod(const pw_rpc_test_TheMethod& request,
- pw_rpc_test_TheMethod& response) {
- response.value = request.integer + 1;
- return pw::OkStatus();
- }
- };
-
- TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
- NanopbClientServerTestContext<> ctx();
- TestService service;
- ctx.server().RegisterService(service);
- ClientUnderTest client(ctx.client(), ctx.channel().id());
-
- // Execute the code that invokes the MyService.TheMethod RPC.
- constexpr uint32_t value = 1;
- const auto result = client.SendRpcCall(value);
- // Needed after ever RPC call to trigger forward of packets
- ctx.ForwardNewPackets();
- const auto request = ctx.request<MyService::TheMethod>(0);
- const auto response = ctx.resonse<MyService::TheMethod>(0);
-
- // Verify content of messages
- EXPECT_EQ(result, pw::OkStatus());
- EXPECT_EQ(request.value, value);
- EXPECT_EQ(response.value, value + 1);
- }
+ #include "my_library_protos/my_service.rpc.pb.h"
+ #include "pw_rpc/nanopb/client_server_testing.h"
+
+ class ClientUnderTest {
+ public:
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
+
+ Status SendRpcCall(uint32_t value);
+ };
+
+
+ class TestService final : public MyService<TestService> {
+ public:
+ Status TheMethod(const pw_rpc_test_TheMethod& request,
+ pw_rpc_test_TheMethod& response) {
+ response.value = request.integer + 1;
+ return pw::OkStatus();
+ }
+ };
+
+ TEST(TestServiceTest, ReceivesUnaryRpcResponse) {
+ NanopbClientServerTestContext<> ctx();
+ TestService service;
+ ctx.server().RegisterService(service);
+ ClientUnderTest client(ctx.client(), ctx.channel().id());
+
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ constexpr uint32_t value = 1;
+ const auto result = client.SendRpcCall(value);
+ // Needed after ever RPC call to trigger forward of packets
+ ctx.ForwardNewPackets();
+ const auto request = ctx.request<MyService::TheMethod>(0);
+ const auto response = ctx.response<MyService::TheMethod>(0);
+
+ // Verify content of messages
+ EXPECT_EQ(result, pw::OkStatus());
+ EXPECT_EQ(request.value, value);
+ EXPECT_EQ(response.value, value + 1);
+ }
+
+Custom packet processing for ClientServerTestContext
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Optional constructor arguments for nanopb/pwpb ``*ClientServerTestContext`` and
+``*ClientServerTestContextThreaded`` allow allow customized packet processing.
+By default the only thing is done is ``ProcessPacket()`` call on the
+``ClientServer`` instance.
+
+For cases when additional instrumentation or offloading to separate thread is
+needed, separate client and server processors can be passed to context
+constructors. A packet processor is a function that returns ``pw::Status`` and
+accepts two arguments: ``pw::rpc::ClientServer&`` and ``pw::ConstByteSpan``.
+Default packet processing is equivalent to the next processor:
+
+.. code-block:: cpp
+
+ [](ClientServer& client_server, pw::ConstByteSpan packet) -> pw::Status {
+ return client_server.ProcessPacket(packet);
+ };
+
+The Server processor will be applied to all packets sent to the server (i.e.
+requests) and client processor will be applied to all packets sent to the client
+(i.e. responses).
+
+.. note::
+
+ The packet processor MUST call ``ClientServer::ProcessPacket()`` method.
+ Otherwise the packet won't be processed.
+
+.. note::
+
+ If the packet processor offloads processing to the separate thread, it MUST
+ copy the ``packet``. After the packet processor returns, the underlying array
+ can go out of scope or be reused for other purposes.
SendResponseIfCalled() helper
-----------------------------
@@ -1479,23 +1685,23 @@ SendResponseIfCalled() helper
have a call for the specified method and then responses to it. It supports
timeout for the waiting part (default timeout is 100ms).
-.. code:: c++
+.. code-block:: c++
- #include "pw_rpc/test_helpers.h"
+ #include "pw_rpc/test_helpers.h"
- pw::rpc::PwpbClientTestContext client_context;
- other::pw_rpc::pwpb::OtherService::Client other_service_client(
- client_context.client(), client_context.channel().id());
+ pw::rpc::PwpbClientTestContext client_context;
+ other::pw_rpc::pwpb::OtherService::Client other_service_client(
+ client_context.client(), client_context.channel().id());
- PW_PWPB_TEST_METHOD_CONTEXT(MyService, GetData)
- context(other_service_client);
- context.call({});
+ PW_PWPB_TEST_METHOD_CONTEXT(MyService, GetData)
+ context(other_service_client);
+ context.call({});
- ASSERT_OK(pw::rpc::test::SendResponseIfCalled<
- other::pw_rpc::pwpb::OtherService::GetPart>(
- client_context, {.value = 42}));
+ ASSERT_OK(pw::rpc::test::SendResponseIfCalled<
+ other::pw_rpc::pwpb::OtherService::GetPart>(
+ client_context, {.value = 42}));
- // At this point MyService::GetData handler received the GetPartResponse.
+ // At this point MyService::GetData handler received the GetPartResponse.
Integration testing with ``pw_rpc``
-----------------------------------
@@ -1509,16 +1715,16 @@ defined in ``pw_rpc/integration_testing.h``:
.. cpp:var:: constexpr uint32_t kChannelId
- The RPC channel for integration test RPCs.
+ The RPC channel for integration test RPCs.
.. cpp:function:: pw::rpc::Client& pw::rpc::integration_test::Client()
- Returns the global RPC client for integration test use.
+ Returns the global RPC client for integration test use.
.. cpp:function:: pw::Status pw::rpc::integration_test::InitializeClient(int argc, char* argv[], const char* usage_args = "PORT")
- Initializes logging and the global RPC client for integration testing. Starts
- a background thread that processes incoming.
+ Initializes logging and the global RPC client for integration testing. Starts
+ a background thread that processes incoming.
Module Configuration Options
============================
@@ -1528,7 +1734,7 @@ this module, see the
more details.
.. doxygenfile:: pw_rpc/public/pw_rpc/internal/config.h
- :sections: define
+ :sections: define
Sharing server and client code
==============================
@@ -1570,43 +1776,44 @@ interface.
.. _module-pw_rpc-ChannelOutput:
.. cpp:class:: pw::rpc::ChannelOutput
- ``pw_rpc`` endpoints use :cpp:class:`ChannelOutput` instances to send packets.
- Systems that integrate pw_rpc must use one or more :cpp:class:`ChannelOutput`
- instances.
+ ``pw_rpc`` endpoints use :cpp:class:`ChannelOutput` instances to send
+ packets. Systems that integrate pw_rpc must use one or more
+ :cpp:class:`ChannelOutput` instances.
- .. cpp:member:: static constexpr size_t kUnlimited = std::numeric_limits<size_t>::max()
+ .. cpp:member:: static constexpr size_t kUnlimited = std::numeric_limits<size_t>::max()
- Value returned from :cpp:func:`MaximumTransmissionUnit` to indicate an
- unlimited MTU.
+ Value returned from :cpp:func:`MaximumTransmissionUnit` to indicate an
+ unlimited MTU.
- .. cpp:function:: virtual size_t MaximumTransmissionUnit()
+ .. cpp:function:: virtual size_t MaximumTransmissionUnit()
- Returns the size of the largest packet the :cpp:class:`ChannelOutput` can
- send. :cpp:class:`ChannelOutput` implementations should only override this
- function if they impose a limit on the MTU. The default implementation
- returns :cpp:member:`kUnlimited`, which indicates that there is no MTU
- limit.
+ Returns the size of the largest packet the :cpp:class:`ChannelOutput` can
+ send. :cpp:class:`ChannelOutput` implementations should only override this
+ function if they impose a limit on the MTU. The default implementation
+ returns :cpp:member:`kUnlimited`, which indicates that there is no MTU
+ limit.
- .. cpp:function:: virtual pw::Status Send(span<std::byte> packet)
+ .. cpp:function:: virtual pw::Status Send(span<std::byte> packet)
- Sends an encoded RPC packet. Returns OK if further packets may be sent, even
- if the current packet could not be sent. Returns any other status if the
- Channel is no longer able to send packets.
+ Sends an encoded RPC packet. Returns OK if further packets may be sent,
+ even if the current packet could not be sent. Returns any other status if
+ the Channel is no longer able to send packets.
- The RPC system's internal lock is held while this function is called. Avoid
- long-running operations, since these will delay any other users of the RPC
- system.
+ The RPC system's internal lock is held while this function is
+ called. Avoid long-running operations, since these will delay any other
+ users of the RPC system.
- .. danger::
+ .. danger::
- No ``pw_rpc`` APIs may be accessed in this function! Implementations MUST
- NOT access any RPC endpoints (:cpp:class:`pw::rpc::Client`,
- :cpp:class:`pw::rpc::Server`) or call objects
- (:cpp:class:`pw::rpc::ServerReaderWriter`,
- :cpp:class:`pw::rpc::ClientReaderWriter`, etc.) inside the :cpp:func:`Send`
- function or any descendent calls. Doing so will result in deadlock! RPC APIs
- may be used by other threads, just not within :cpp:func:`Send`.
+ No ``pw_rpc`` APIs may be accessed in this function! Implementations
+ MUST NOT access any RPC endpoints (:cpp:class:`pw::rpc::Client`,
+ :cpp:class:`pw::rpc::Server`) or call objects
+ (:cpp:class:`pw::rpc::ServerReaderWriter`,
+ :cpp:class:`pw::rpc::ClientReaderWriter`, etc.) inside the
+ :cpp:func:`Send` function or any descendent calls. Doing so will result
+ in deadlock! RPC APIs may be used by other threads, just not within
+ :cpp:func:`Send`.
- The buffer provided in ``packet`` must NOT be accessed outside of this
- function. It must be sent immediately or copied elsewhere before the
- function returns.
+ The buffer provided in ``packet`` must NOT be accessed outside of this
+ function. It must be sent immediately or copied elsewhere before the
+ function returns.
diff --git a/pw_rpc/fake_channel_output.cc b/pw_rpc/fake_channel_output.cc
index 6404e68d6..3ef04346a 100644
--- a/pw_rpc/fake_channel_output.cc
+++ b/pw_rpc/fake_channel_output.cc
@@ -79,7 +79,7 @@ Status FakeChannelOutput::HandlePacket(span<const std::byte> buffer) {
packet.status().str());
return OkStatus();
case pwpb::PacketType::SERVER_STREAM:
- case pwpb::PacketType::CLIENT_STREAM_END:
+ case pwpb::PacketType::CLIENT_REQUEST_COMPLETION:
return OkStatus();
}
PW_CRASH("Unhandled PacketType %d", static_cast<int>(result.value().type()));
diff --git a/pw_rpc/fuzz/BUILD.gn b/pw_rpc/fuzz/BUILD.gn
index bec42fe44..b2fd728b7 100644
--- a/pw_rpc/fuzz/BUILD.gn
+++ b/pw_rpc/fuzz/BUILD.gn
@@ -65,7 +65,10 @@ pw_source_set("argparse") {
pw_test("argparse_test") {
sources = [ "argparse_test.cc" ]
- deps = [ ":argparse" ]
+ deps = [
+ ":argparse",
+ "$dir_pw_string:builder",
+ ]
}
pw_source_set("engine") {
@@ -95,15 +98,17 @@ pw_test("engine_test") {
sources = [ "engine_test.cc" ]
deps = [
":engine",
+ "$dir_pw_rpc:client_server_testing",
"$dir_pw_rpc:client_server_testing_threaded",
- "$dir_pw_thread:test_threads",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
dir_pw_log,
pw_chrono_SYSTEM_TIMER_BACKEND,
]
}
pw_executable("client_fuzzer") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "client_fuzzer.cc" ]
deps = [
":argparse",
@@ -114,6 +119,7 @@ pw_executable("client_fuzzer") {
}
pw_python_action("cpp_client_server_fuzz_test") {
+ testonly = pw_unit_test_TESTONLY
script = "../py/pw_rpc/testing.py"
args = [
"--server",
diff --git a/pw_rpc/fuzz/alarm_timer_test.cc b/pw_rpc/fuzz/alarm_timer_test.cc
index ce8395a6b..a64156996 100644
--- a/pw_rpc/fuzz/alarm_timer_test.cc
+++ b/pw_rpc/fuzz/alarm_timer_test.cc
@@ -37,7 +37,7 @@ TEST(AlarmTimerTest, Restart) {
timer.Start(50ms);
for (size_t i = 0; i < 10; ++i) {
timer.Restart();
- EXPECT_FALSE(sem.try_acquire_for(10us));
+ EXPECT_FALSE(sem.try_acquire_for(chrono::SystemClock::for_at_least(10us)));
}
sem.acquire();
}
@@ -47,7 +47,7 @@ TEST(AlarmTimerTest, Cancel) {
AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
timer.Start(50ms);
timer.Cancel();
- EXPECT_FALSE(sem.try_acquire_for(100us));
+ EXPECT_FALSE(sem.try_acquire_for(chrono::SystemClock::for_at_least(100us)));
}
TEST(AlarmTimerTest, Destroy) {
@@ -57,7 +57,7 @@ TEST(AlarmTimerTest, Destroy) {
[&sem](chrono::SystemClock::time_point) { sem.release(); });
timer.Start(50ms);
}
- EXPECT_FALSE(sem.try_acquire_for(100us));
+ EXPECT_FALSE(sem.try_acquire_for(chrono::SystemClock::for_at_least(100us)));
}
} // namespace
diff --git a/pw_rpc/fuzz/argparse.cc b/pw_rpc/fuzz/argparse.cc
index 39a5dd694..7df102a20 100644
--- a/pw_rpc/fuzz/argparse.cc
+++ b/pw_rpc/fuzz/argparse.cc
@@ -183,7 +183,7 @@ ParseStatus UnsignedParserBase::Parse(std::string_view arg0,
result = kParsedTwo;
}
char* endptr;
- auto value = strtoull(arg0.data(), &endptr, 0);
+ unsigned long long value = strtoull(arg0.data(), &endptr, 0);
if (*endptr) {
PW_LOG_ERROR("Failed to parse number from '%s'", arg0.data());
return kParseFailure;
@@ -192,7 +192,7 @@ ParseStatus UnsignedParserBase::Parse(std::string_view arg0,
PW_LOG_ERROR("Parsed value is too large: %llu", value);
return kParseFailure;
}
- set_value(value);
+ set_value(static_cast<uint64_t>(value));
return result;
}
diff --git a/pw_rpc/fuzz/argparse_test.cc b/pw_rpc/fuzz/argparse_test.cc
index 6f0ab3889..93adea4d6 100644
--- a/pw_rpc/fuzz/argparse_test.cc
+++ b/pw_rpc/fuzz/argparse_test.cc
@@ -18,6 +18,7 @@
#include <limits>
#include "gtest/gtest.h"
+#include "pw_string/string_builder.h"
namespace pw::rpc::fuzz {
namespace {
@@ -132,17 +133,17 @@ void CheckArgs(Vector<ArgParserVariant>& parsers,
bool verbose,
size_t runs,
uint16_t port) {
- bool actual_verbose;
+ bool actual_verbose = false;
EXPECT_EQ(GetArg(parsers, "--verbose", &actual_verbose), OkStatus());
EXPECT_EQ(verbose, actual_verbose);
EXPECT_EQ(ResetArg(parsers, "--verbose"), OkStatus());
- size_t actual_runs;
+ size_t actual_runs = 0u;
EXPECT_EQ(GetArg(parsers, "--runs", &actual_runs), OkStatus());
EXPECT_EQ(runs, actual_runs);
EXPECT_EQ(ResetArg(parsers, "--runs"), OkStatus());
- uint16_t actual_port;
+ uint16_t actual_port = 0u;
EXPECT_EQ(GetArg(parsers, "port", &actual_port), OkStatus());
EXPECT_EQ(port, actual_port);
EXPECT_EQ(ResetArg(parsers, "port"), OkStatus());
diff --git a/pw_rpc/fuzz/engine.cc b/pw_rpc/fuzz/engine.cc
index b19dfa398..3c46b299b 100644
--- a/pw_rpc/fuzz/engine.cc
+++ b/pw_rpc/fuzz/engine.cc
@@ -57,7 +57,7 @@ struct CloseClientStreamVisitor final {
result_type operator()(std::monostate&) {}
result_type operator()(pw::rpc::RawUnaryReceiver&) {}
result_type operator()(pw::rpc::RawClientReaderWriter& call) {
- call.CloseClientStream().IgnoreError();
+ call.RequestCompletion().IgnoreError();
}
};
diff --git a/pw_rpc/fuzz/engine_test.cc b/pw_rpc/fuzz/engine_test.cc
index 5ac1a49a1..b8720fcb4 100644
--- a/pw_rpc/fuzz/engine_test.cc
+++ b/pw_rpc/fuzz/engine_test.cc
@@ -20,9 +20,10 @@
#include "pw_containers/vector.h"
#include "pw_log/log.h"
#include "pw_rpc/benchmark.h"
+#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/internal/client_server_testing_threaded.h"
#include "pw_rpc/internal/fake_channel_output.h"
-#include "pw_thread/test_threads.h"
+#include "pw_thread/non_portable_test_thread_options.h"
namespace pw::rpc::fuzz {
namespace {
@@ -56,7 +57,11 @@ using FuzzerChannelOutputBase =
/// Channel output that can be waited on by the server.
class FuzzerChannelOutput : public FuzzerChannelOutputBase {
public:
- FuzzerChannelOutput() : FuzzerChannelOutputBase() {}
+ explicit FuzzerChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : FuzzerChannelOutputBase(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
};
using FuzzerContextBase =
@@ -68,7 +73,13 @@ class FuzzerContext : public FuzzerContextBase {
public:
static constexpr uint32_t kChannelId = 1;
- FuzzerContext() : FuzzerContextBase(thread::test::TestOptionsThread0()) {}
+ explicit FuzzerContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
+ : FuzzerContextBase(thread::test::TestOptionsThread0(),
+ std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
};
class RpcFuzzTestingTest : public testing::Test {
@@ -92,13 +103,16 @@ class RpcFuzzTestingTest : public testing::Test {
fuzzer.Run(actions_);
}
+ void TearDown() override { context_.server().UnregisterService(service_); }
+
private:
FuzzerContext context_;
BenchmarkService service_;
Vector<uint32_t, Fuzzer::kMaxActions> actions_;
};
-TEST_F(RpcFuzzTestingTest, SequentialRequests) {
+// TODO: b/274437709 - Re-enable.
+TEST_F(RpcFuzzTestingTest, DISABLED_SequentialRequests) {
// Callback thread
Add(Action::kWriteStream, 1, 'B', 1);
Add(Action::kSkip, 0, 0);
@@ -126,7 +140,7 @@ TEST_F(RpcFuzzTestingTest, SequentialRequests) {
Run();
}
-// TODO(b/274437709): Re-enable.
+// TODO: b/274437709 - Re-enable.
TEST_F(RpcFuzzTestingTest, DISABLED_SimultaneousRequests) {
// Callback thread
NextThread();
@@ -149,7 +163,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_SimultaneousRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_CanceledRequests) {
@@ -177,7 +191,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_CanceledRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_AbandonedRequests) {
@@ -205,7 +219,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_AbandonedRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_SwappedRequests) {
@@ -232,7 +246,7 @@ TEST_F(RpcFuzzTestingTest, DISABLED_SwappedRequests) {
Run();
}
-// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// TODO: b/274437709 - This test currently does not pass as it exhausts the fake
// channel. It will be re-enabled when the underlying stream is swapped for
// a pw_ring_buffer-based approach.
TEST_F(RpcFuzzTestingTest, DISABLED_DestroyedRequests) {
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
index 05a7633e6..e8e4b6f3d 100644
--- a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
@@ -57,6 +57,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
#include <string_view>
#include <variant>
diff --git a/pw_rpc/internal/packet.proto b/pw_rpc/internal/packet.proto
index 606efb9ef..104966357 100644
--- a/pw_rpc/internal/packet.proto
+++ b/pw_rpc/internal/packet.proto
@@ -27,14 +27,16 @@ enum PacketType {
REQUEST = 0;
// A message in a client stream. Always sent after a REQUEST and before a
- // CLIENT_STREAM_END.
+ // CLIENT_REQUEST_COMPLETION.
CLIENT_STREAM = 2;
// The client received a packet for an RPC it did not request.
CLIENT_ERROR = 4;
- // A client stream has completed.
- CLIENT_STREAM_END = 8;
+ // Client has requested for call completion. In client streaming and
+ // bi-directional streaming RPCs, this also indicates that the client is done
+ // with sending requests.
+ CLIENT_REQUEST_COMPLETION = 8;
// Server-to-client packets
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
index cdbfc07de..5e682fba1 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
@@ -188,7 +188,7 @@ public class Client {
throw new InvalidRpcServiceException(serviceId);
}
- Method method = service.methods().get(methodId);
+ Method method = service.method(methodId);
if (method == null) {
throw new InvalidRpcServiceMethodException(service, methodId);
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
index ff80cbb61..c306147f2 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
@@ -20,7 +20,6 @@ import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import com.google.protobuf.Parser;
-import java.lang.reflect.InvocationTargetException;
/**
* Information about an RPC service method.
@@ -37,9 +36,17 @@ public abstract class Method {
public abstract Type type();
- public abstract Class<? extends MessageLite> request();
+ abstract Parser<? extends MessageLite> requestParser();
- public abstract Class<? extends MessageLite> response();
+ abstract Parser<? extends MessageLite> responseParser();
+
+ public final Class<? extends MessageLite> request() {
+ return getProtobufClass(requestParser());
+ }
+
+ public final Class<? extends MessageLite> response() {
+ return getProtobufClass(responseParser());
+ }
final int id() {
return Ids.calculate(name());
@@ -57,11 +64,11 @@ public abstract class Method {
return createFullName(service().name(), name());
}
- public static String createFullName(String serviceName, String methodName) {
+ static String createFullName(String serviceName, String methodName) {
return serviceName + '/' + methodName;
}
- /* package */ static Builder builder() {
+ static Builder builder() {
return new AutoValue_Method.Builder();
}
@@ -79,29 +86,26 @@ public abstract class Method {
abstract Builder setName(String value);
- abstract Builder setRequest(Class<? extends MessageLite> value);
+ abstract Builder setRequestParser(Parser<? extends MessageLite> parser);
- abstract Builder setResponse(Class<? extends MessageLite> value);
+ abstract Builder setResponseParser(Parser<? extends MessageLite> parser);
abstract Method build();
}
/** Decodes a response payload according to the method's response type. */
final MessageLite decodeResponsePayload(ByteString data) throws InvalidProtocolBufferException {
- return decodeProtobuf(response(), data);
+ return responseParser().parseFrom(data);
}
- /** Deserializes a protobuf using the provided class. */
- @SuppressWarnings("unchecked")
- static MessageLite decodeProtobuf(Class<? extends MessageLite> messageType, ByteString data)
- throws InvalidProtocolBufferException {
+ private static Class<? extends MessageLite> getProtobufClass(
+ Parser<? extends MessageLite> parser) {
try {
- Parser<? extends MessageLite> parser =
- (Parser<? extends MessageLite>) messageType.getMethod("parser").invoke(null);
- return parser.parseFrom(data);
- } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
- throw new LinkageError(
- "Method was created with a protobuf class with an invalid or missing parser() method", e);
+ return parser.parseFrom(ByteString.EMPTY).getClass();
+ } catch (InvalidProtocolBufferException e) {
+ throw new AssertionError("Failed to parse zero bytes as a protobuf! "
+ + "It was assumed that zero bytes is always a valid protobuf.",
+ e);
}
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
index 6d7fb2b42..3a5b57a53 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
@@ -26,9 +26,9 @@ import javax.annotation.Nullable;
*
* Invoking an RPC with a method client may throw exceptions:
*
- * TODO(hepler): This class should be split into four types -- one for each method type. The call
- * type checks should be done when the object is created. Also, the client should be typed on
- * the request/response.
+ * TODO: b/301644223 - This class should be split into four types -- one for each method type. The
+ * call type checks should be done when the object is created. Also, the client should be typed
+ * on the request/response.
*/
public class MethodClient {
private final Client client;
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java
index 3260bb13f..44e89b6e0 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Packets.java
@@ -69,7 +69,7 @@ import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
public static byte[] clientStreamEnd(PendingRpc rpc) {
return RpcPacket.newBuilder()
- .setType(PacketType.CLIENT_STREAM_END)
+ .setType(PacketType.CLIENT_REQUEST_COMPLETION)
.setChannelId(rpc.channel().id())
.setServiceId(rpc.service().id())
.setMethodId(rpc.method().id())
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
index eeffd2af8..a239f10ef 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
@@ -14,8 +14,11 @@
package dev.pigweed.pw_rpc;
+import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableMap;
import com.google.protobuf.MessageLite;
+import com.google.protobuf.Parser;
+import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.stream.Collectors;
@@ -34,20 +37,26 @@ public class Service {
.collect(Collectors.toMap(Method::id, m -> m)));
}
- public String name() {
+ /** Returns the fully qualified name of this service (package.Service). */
+ public final String name() {
return name;
}
- int id() {
+ /** Returns the methods in this service. */
+ public final ImmutableCollection<Method> getMethods() {
+ return methods.values();
+ }
+
+ final int id() {
return id;
}
- public ImmutableMap<Integer, Method> methods() {
- return methods;
+ final Method method(String name) {
+ return methods.get(Ids.calculate(name));
}
- public final Method method(String name) {
- return methods().get(Ids.calculate(name));
+ final Method method(int id) {
+ return methods.get(id);
}
@Override
@@ -55,39 +64,139 @@ public class Service {
return name();
}
+ // TODO: b/293361955 - Remove deprecated methods.
+
+ /**
+ * Declares a unary service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
public static Method.Builder unaryMethod(
- String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.UNARY)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
}
- public static Method.Builder serverStreamingMethod(
+ /**
+ * Declares a unary service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder unaryMethod(
String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return unaryMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Declares a server streaming service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
+ public static Method.Builder serverStreamingMethod(
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.SERVER_STREAMING)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
}
- public static Method.Builder clientStreamingMethod(
+ /**
+ * Declares a server streaming service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder serverStreamingMethod(
String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return serverStreamingMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Declares a client streaming service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
+ public static Method.Builder clientStreamingMethod(
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.CLIENT_STREAMING)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
}
- public static Method.Builder bidirectionalStreamingMethod(
+ /**
+ * Declares a client streaming service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder clientStreamingMethod(
String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return clientStreamingMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Declares a bidirectional streaming service method.
+ *
+ * @param name The method name within the service, e.g. "MyMethod" for my_pkg.MyService.MyMethod.
+ * @param request Parser for the request protobuf, e.g. MyRequestProto.parser()
+ * @param response Parser for the response protobuf, e.g. MyResponseProto.parser()
+ * @return Method.Builder, for internal use by the Service class only
+ */
+ public static Method.Builder bidirectionalStreamingMethod(
+ String name, Parser<? extends MessageLite> request, Parser<? extends MessageLite> response) {
return Method.builder()
.setType(Method.Type.BIDIRECTIONAL_STREAMING)
.setName(name)
- .setRequest(request)
- .setResponse(response);
+ .setRequestParser(request)
+ .setResponseParser(response);
+ }
+
+ /**
+ * Declares a bidirectional streaming service method.
+ *
+ * @deprecated Pass `ProtobufType.parser()` instead of `ProtobufType.class`.
+ */
+ @Deprecated
+ public static Method.Builder bidirectionalStreamingMethod(
+ String name, Class<? extends MessageLite> request, Class<? extends MessageLite> response) {
+ return bidirectionalStreamingMethod(name, getParser(request), getParser(response));
+ }
+
+ /**
+ * Gets the Parser from a protobuf class using reflection.
+ *
+ * This function is provided for backwards compatibility with the deprecated service API that
+ * takes a class object instead of a protobuf parser object.
+ */
+ @SuppressWarnings("unchecked")
+ private static Parser<? extends MessageLite> getParser(Class<? extends MessageLite> messageType) {
+ try {
+ return (Parser<? extends MessageLite>) messageType.getMethod("parser").invoke(null);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ throw new LinkageError(
+ String.format(
+ "Service method created with %s is missing parser() method, likely optimized out. "
+ + "Pass MyMessage.parser() instead of MyMessage.class in service declarations. "
+ + "The class-based API is deprecated and will be removed. "
+ + "See b/293361955.",
+ messageType),
+ e);
+ }
}
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel b/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
index 532255da9..5e16cc71e 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
@@ -109,6 +109,20 @@ java_test(
)
java_test(
+ name = "ServiceTest",
+ size = "small",
+ srcs = ["ServiceTest.java"],
+ test_class = "dev.pigweed.pw_rpc.ServiceTest",
+ deps = [
+ ":test_proto_java_proto_lite",
+ "//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "@com_google_protobuf//java/lite",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_truth_truth",
+ ],
+)
+
+java_test(
name = "StreamObserverCallTest",
size = "small",
srcs = ["StreamObserverCallTest.java"],
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
index b3e31984b..8fb4b9d55 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
@@ -43,11 +43,13 @@ public final class ClientTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidiStreaming", SomeMessage.class, AnotherMessage.class));
+ "SomeBidiStreaming", SomeMessage.parser(), AnotherMessage.parser()));
private static final Method UNARY_METHOD = SERVICE.method("SomeUnary");
private static final Method SERVER_STREAMING_METHOD = SERVICE.method("SomeServerStreaming");
@@ -135,7 +137,7 @@ public final class ClientTest {
InvalidRpcServiceException.class, () -> client.method(CHANNEL_ID, "abc.Service/Method"));
Service service = new Service("throwaway.NotRealService",
- Service.unaryMethod("NotAnRpc", SomeMessage.class, AnotherMessage.class));
+ Service.unaryMethod("NotAnRpc", SomeMessage.parser(), AnotherMessage.parser()));
assertThrows(InvalidRpcServiceException.class,
() -> client.method(CHANNEL_ID, service.method("NotAnRpc")));
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
index 91722ee70..041f69398 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
@@ -36,11 +36,13 @@ public final class EndpointTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, SomeMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, SomeMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, SomeMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), SomeMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), SomeMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), SomeMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidiStreaming", SomeMessage.class, SomeMessage.class));
+ "SomeBidiStreaming", SomeMessage.parser(), SomeMessage.parser()));
private static final Method METHOD = SERVICE.method("SomeUnary");
@@ -195,7 +197,7 @@ public final class EndpointTest {
.build()))
.isTrue();
assertThat(endpoint.processClientPacket(call.rpc().method(),
- packetBuilder().setType(PacketType.CLIENT_STREAM_END).build()))
+ packetBuilder().setType(PacketType.CLIENT_REQUEST_COMPLETION).build()))
.isTrue();
assertThat(endpoint.processClientPacket(call.rpc().method(),
packetBuilder()
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
index a1f11997a..ca7196287 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
@@ -41,10 +41,10 @@ public final class FutureCallTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClient", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod("SomeClient", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectional", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectional", SomeMessage.parser(), AnotherMessage.parser()));
private static final Method METHOD = SERVICE.method("SomeUnary");
private static final int CHANNEL_ID = 555;
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java
new file mode 100644
index 000000000..1d0ee0607
--- /dev/null
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/ServiceTest.java
@@ -0,0 +1,70 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package dev.pigweed.pw_rpc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public final class ServiceTest {
+ private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidiStreaming", SomeMessage.parser(), AnotherMessage.parser()));
+
+ private static final Method METHOD_1 = SERVICE.method("SomeUnary");
+ private static final Method METHOD_2 = SERVICE.method("SomeServerStreaming");
+ private static final Method METHOD_3 = SERVICE.method("SomeClientStreaming");
+ private static final Method METHOD_4 = SERVICE.method("SomeBidiStreaming");
+
+ @Test
+ public void getMethods_includesAllMethods() {
+ assertThat(SERVICE.getMethods()).containsExactly(METHOD_1, METHOD_2, METHOD_3, METHOD_4);
+ }
+
+ @Test
+ public void serviceDeclaration_deprecatedClassBasedEquivalentToParserBased() {
+ final Service declaredWithClassObjects = new Service(SERVICE.name(),
+ Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidiStreaming", SomeMessage.class, AnotherMessage.class));
+
+ assertThat(declaredWithClassObjects.method("SomeUnary").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeUnary").responseParser());
+ assertThat(declaredWithClassObjects.method("SomeServerStreaming").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeServerStreaming").responseParser());
+ assertThat(declaredWithClassObjects.method("SomeClientStreaming").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeClientStreaming").responseParser());
+ assertThat(declaredWithClassObjects.method("SomeBidiStreaming").responseParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeBidiStreaming").responseParser());
+
+ assertThat(declaredWithClassObjects.method("SomeUnary").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeUnary").requestParser());
+ assertThat(declaredWithClassObjects.method("SomeServerStreaming").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeServerStreaming").requestParser());
+ assertThat(declaredWithClassObjects.method("SomeClientStreaming").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeClientStreaming").requestParser());
+ assertThat(declaredWithClassObjects.method("SomeBidiStreaming").requestParser())
+ .isSameInstanceAs(declaredWithClassObjects.method("SomeBidiStreaming").requestParser());
+ }
+}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
index 76d86c162..d1260de4d 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
@@ -32,10 +32,10 @@ public final class StreamObserverCallTest {
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClient", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod("SomeClient", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectional", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectional", SomeMessage.parser(), AnotherMessage.parser()));
private static final Method METHOD = SERVICE.method("SomeUnary");
private static final int CHANNEL_ID = 555;
@@ -130,7 +130,7 @@ public final class StreamObserverCallTest {
streamObserverCall.finish();
verify(mockOutput)
- .send(packetBuilder().setType(PacketType.CLIENT_STREAM_END).build().toByteArray());
+ .send(packetBuilder().setType(PacketType.CLIENT_REQUEST_COMPLETION).build().toByteArray());
}
@Test
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
index 36102f447..a1bc59c64 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
@@ -34,11 +34,13 @@ import org.mockito.junit.MockitoRule;
public final class StreamObserverMethodClientTest {
private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectionalStreaming", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectionalStreaming", SomeMessage.parser(), AnotherMessage.parser()));
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@@ -212,7 +214,8 @@ public final class StreamObserverMethodClientTest {
@Test
public void invalidService_throwsException() {
Service otherService = new Service("something.Else",
- Service.clientStreamingMethod("ClientStream", SomeMessage.class, AnotherMessage.class));
+ Service.clientStreamingMethod(
+ "ClientStream", SomeMessage.parser(), AnotherMessage.parser()));
MethodClient methodClient = new MethodClient(
client, channel.id(), otherService.method("ClientStream"), defaultObserver);
@@ -222,13 +225,13 @@ public final class StreamObserverMethodClientTest {
@Test
public void invalidMethod_throwsException() {
Service serviceWithDifferentUnaryMethod = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", AnotherMessage.class, AnotherMessage.class),
+ Service.unaryMethod("SomeUnary", AnotherMessage.parser(), AnotherMessage.parser()),
Service.serverStreamingMethod(
- "SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
+ "SomeServerStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.clientStreamingMethod(
- "SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ "SomeClientStreaming", SomeMessage.parser(), AnotherMessage.parser()),
Service.bidirectionalStreamingMethod(
- "SomeBidirectionalStreaming", SomeMessage.class, AnotherMessage.class));
+ "SomeBidirectionalStreaming", SomeMessage.parser(), AnotherMessage.parser()));
MethodClient methodClient = new MethodClient(
client, 999, serviceWithDifferentUnaryMethod.method("SomeUnary"), defaultObserver);
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
index a58d51901..5e989a76b 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
@@ -175,9 +175,10 @@ public class TestClient {
private <T extends MessageLite> T parseRequestPayload(Class<T> payloadType, RpcPacket packet) {
try {
- return payloadType.cast(Method.decodeProtobuf(
- client.method(CHANNEL_ID, packet.getServiceId(), packet.getMethodId()).method().request(),
- packet.getPayload()));
+ return payloadType.cast(client.method(CHANNEL_ID, packet.getServiceId(), packet.getMethodId())
+ .method()
+ .requestParser()
+ .parseFrom(packet.getPayload()));
} catch (InvalidProtocolBufferException e) {
throw new AssertionError("Decoding sent packet payload failed", e);
}
diff --git a/pw_rpc/method_test.cc b/pw_rpc/method_test.cc
index b1403d8b0..39cd87c6c 100644
--- a/pw_rpc/method_test.cc
+++ b/pw_rpc/method_test.cc
@@ -18,9 +18,9 @@
#include "gtest/gtest.h"
#include "pw_rpc/internal/packet.h"
-#include "pw_rpc/internal/test_method.h"
#include "pw_rpc/method_type.h"
#include "pw_rpc/server.h"
+#include "pw_rpc_private/test_method.h"
namespace pw::rpc::internal {
namespace {
diff --git a/pw_rpc/nanopb/Android.bp b/pw_rpc/nanopb/Android.bp
index da2de4d04..c210b2998 100644
--- a/pw_rpc/nanopb/Android.bp
+++ b/pw_rpc/nanopb/Android.bp
@@ -59,7 +59,7 @@ cc_library_headers {
// name: "pw_rpc_cflags_<instance_name>",
// cflags: [
// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
// "-DPW_RPC_DYNAMIC_ALLOCATION",
// ],
// }
@@ -80,6 +80,4 @@ cc_defaults {
srcs: [
":pw_rpc_nanopb_src_files",
],
- host_supported: true,
- vendor_available: true,
-} \ No newline at end of file
+}
diff --git a/pw_rpc/nanopb/BUILD.bazel b/pw_rpc/nanopb/BUILD.bazel
index 32c0573bb..e28f6bf59 100644
--- a/pw_rpc/nanopb/BUILD.bazel
+++ b/pw_rpc/nanopb/BUILD.bazel
@@ -131,7 +131,22 @@ pw_cc_library(
],
)
-# TODO(b/242059613): Enable this library when logging_event_handler can be used.
+pw_cc_test(
+ name = "callback_test",
+ srcs = ["callback_test.cc"],
+ deps = [
+ ":client_testing",
+ "//pw_rpc",
+ "//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ "//pw_sync:binary_semaphore",
+ "//pw_thread:non_portable_test_thread_options",
+ "//pw_thread:sleep",
+ "//pw_thread:yield",
+ "//pw_thread_stl:non_portable_test_thread_options",
+ ],
+)
+
+# TODO: b/242059613 - Enable this library when logging_event_handler can be used.
filegroup(
name = "client_integration_test",
srcs = [
@@ -178,6 +193,7 @@ pw_cc_test(
":client_api",
":client_server_testing",
"//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ "//pw_sync:mutex",
],
)
@@ -192,8 +208,9 @@ pw_cc_test(
":client_server_testing_threaded",
"//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
"//pw_sync:binary_semaphore",
- "//pw_thread:test_threads_header",
- "//pw_thread_stl:test_threads",
+ "//pw_sync:mutex",
+ "//pw_thread:non_portable_test_thread_options",
+ "//pw_thread_stl:non_portable_test_thread_options",
],
)
@@ -267,7 +284,7 @@ pw_cc_test(
],
)
-# TODO(b/234874064): Requires nanopb options file support to compile.
+# TODO: b/234874064 - Requires nanopb options file support to compile.
filegroup(
name = "echo_service_test",
srcs = ["echo_service_test.cc"],
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index d04fa8923..1fb9852d2 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -135,6 +135,7 @@ pw_source_set("echo_service") {
}
pw_source_set("client_integration_test") {
+ testonly = pw_unit_test_TESTONLY
public_configs = [ ":public" ]
public_deps = [
"$dir_pw_sync:binary_semaphore",
@@ -152,6 +153,7 @@ pw_doc_group("docs") {
pw_test_group("tests") {
tests = [
+ ":callback_test",
":client_call_test",
":client_reader_writer_test",
":client_server_context_test",
@@ -171,6 +173,23 @@ pw_test_group("tests") {
]
}
+pw_test("callback_test") {
+ enable_if = dir_pw_third_party_nanopb != "" &&
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ deps = [
+ ":client_testing",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:yield",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
+ "..:client",
+ "..:server",
+ "..:test_protos.nanopb_rpc",
+ ]
+ sources = [ "callback_test.cc" ]
+}
+
pw_test("client_call_test") {
deps = [
":client_api",
@@ -196,10 +215,11 @@ pw_test("client_server_context_test") {
deps = [
":client_api",
":client_server_testing",
+ "$dir_pw_sync:mutex",
"..:test_protos.nanopb_rpc",
]
sources = [ "client_server_context_test.cc" ]
- enable_if = dir_pw_third_party_nanopb != ""
+ enable_if = dir_pw_third_party_nanopb != "" && pw_sync_MUTEX_BACKEND != ""
}
_stl_threading_and_nanopb_enabled =
@@ -212,8 +232,9 @@ pw_test("client_server_context_threaded_test") {
":client_api",
":client_server_testing_threaded",
"$dir_pw_sync:binary_semaphore",
- "$dir_pw_thread:test_threads",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
"..:test_protos.nanopb_rpc",
]
sources = [ "client_server_context_threaded_test.cc" ]
diff --git a/pw_rpc/nanopb/CMakeLists.txt b/pw_rpc/nanopb/CMakeLists.txt
index a3a73e5db..b2665c382 100644
--- a/pw_rpc/nanopb/CMakeLists.txt
+++ b/pw_rpc/nanopb/CMakeLists.txt
@@ -35,18 +35,12 @@ pw_add_library(pw_rpc.nanopb.server_api STATIC
pw_log
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_METHOD)
- zephyr_link_libraries(pw_rpc.nanopb.method)
-endif()
# TODO(hepler): Deprecate this once no one depends on it.
pw_add_library(pw_rpc.nanopb.method_union INTERFACE
PUBLIC_DEPS
pw_rpc.nanopb.server_api
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_METHOD_UNION)
- zephyr_link_libraries(pw_rpc.nanopb.method_union)
-endif()
pw_add_library(pw_rpc.nanopb.client_api INTERFACE
HEADERS
@@ -58,9 +52,6 @@ pw_add_library(pw_rpc.nanopb.client_api INTERFACE
pw_rpc.client
pw_rpc.nanopb.common
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_CLIENT)
- zephyr_link_libraries(pw_rpc.nanopb.client_api)
-endif()
pw_add_library(pw_rpc.nanopb.common STATIC
HEADERS
@@ -78,9 +69,6 @@ pw_add_library(pw_rpc.nanopb.common STATIC
pw_rpc.client
pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_COMMON)
- zephyr_link_libraries(pw_rpc.nanopb.common)
-endif()
pw_add_library(pw_rpc.nanopb.test_method_context INTERFACE
HEADERS
@@ -142,9 +130,6 @@ pw_add_library(pw_rpc.nanopb.echo_service INTERFACE
PUBLIC_DEPS
pw_rpc.protos.nanopb_rpc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_ECHO_SERVICE)
- zephyr_link_libraries(pw_rpc.nanopb.echo_service)
-endif()
pw_add_library(pw_rpc.nanopb.client_integration_test STATIC
SOURCES
@@ -182,17 +167,20 @@ pw_add_test(pw_rpc.nanopb.client_reader_writer_test
pw_rpc.nanopb
)
-pw_add_test(pw_rpc.nanopb.client_server_context_test
- SOURCES
- client_server_context_test.cc
- PRIVATE_DEPS
- pw_rpc.nanopb.client_api
- pw_rpc.nanopb.client_server_testing
- pw_rpc.test_protos.nanopb_rpc
- GROUPS
- modules
- pw_rpc.nanopb
-)
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+ pw_add_test(pw_rpc.nanopb.client_server_context_test
+ SOURCES
+ client_server_context_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.client_server_testing
+ pw_rpc.test_protos.nanopb_rpc
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_rpc.nanopb
+ )
+endif()
if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
@@ -205,7 +193,8 @@ if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
pw_rpc.nanopb.client_server_testing_threaded
pw_rpc.test_protos.nanopb_rpc
pw_sync.binary_semaphore
- pw_thread.test_threads
+ pw_sync.mutex
+ pw_thread.non_portable_test_thread_options
pw_thread.thread
pw_thread_stl.test_threads
GROUPS
@@ -351,7 +340,7 @@ pw_add_test(pw_rpc.nanopb.stub_generation_test
pw_rpc.nanopb
)
-# TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
+# TODO: b/231950909 - Test disabled as pw_work_queue lacks CMakeLists.txt
if((TARGET pw_work_queue.pw_work_queue) AND
("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL ""))
diff --git a/pw_rpc/nanopb/Kconfig b/pw_rpc/nanopb/Kconfig
index 649707fbb..3b7ea3dcf 100644
--- a/pw_rpc/nanopb/Kconfig
+++ b/pw_rpc/nanopb/Kconfig
@@ -12,42 +12,44 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_RPC_NANOPB
- bool "Pigweed RPC nanobp"
-
-if PIGWEED_RPC_NANOPB
-
-config PIGWEED_RPC_NANOPB_DIR
- string "Optional 3rd party directory for nanopb"
- help
- The directory for the custom nanopb build rules to integrate with pigweed.
+menu "nanopb"
config PIGWEED_RPC_NANOPB_METHOD
- bool "Enable Pigweed RPC/Nanopb method library (pw_rpc.nanopb.method)"
+ bool "Link pw_rpc.nanopb.method library"
select PIGWEED_RPC_NANOPB_COMMON
select PIGWEED_RPC_SERVER
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_METHOD_UNION
- bool "Enable Pigweed RPC/Nanopb method union library (pw_rpc.nanopb.method_union)"
+ bool "Link pw_rpc.nanopb.method_union library"
select PIGWEED_RPC_NANOPB_METHOD
select PIGWEED_RPC_RAW
select PIGWEED_RPC_SERVER
select PIGWEED_LOG
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_CLIENT
- bool "Enable Pigweed RPC/Nanopb client library (pw_rpc.nanopb.client)"
+ bool "Link pw_rpc.nanopb.client library"
select PIGWEED_FUNCTION
select PIGWEED_RPC_NANOPB_COMMON
select PIGWEED_RPC_COMMON
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_COMMON
- bool "Enable Pigweed RPC/Nanopb common library (pw_rpc.nanopb.common)"
+ bool "Link pw_rpc.nanopb.common library"
select PIGWEED_BYTES
select PIGWEED_LOG
select PIGWEED_RPC_COMMON
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
config PIGWEED_RPC_NANOPB_ECHO_SERVICE
- bool "Enable Pigweed RPC/Nanopb echo service library (pw_rpc.nanopb.echo_service)"
+ bool "Link pw_rpc.nanopb.echo_service library"
+ help
+ See :ref:`module-pw_rpc_nanopb` for module details.
-endif # PIGWEED_RPC_NANOPB \ No newline at end of file
+endmenu
diff --git a/pw_rpc/nanopb/callback_test.cc b/pw_rpc/nanopb/callback_test.cc
new file mode 100644
index 000000000..bfd5088f6
--- /dev/null
+++ b/pw_rpc/nanopb/callback_test.cc
@@ -0,0 +1,269 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "gtest/gtest.h"
+#include "pw_rpc/nanopb/client_testing.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/non_portable_test_thread_options.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/yield.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::chrono_literals;
+
+using test::pw_rpc::nanopb::TestService;
+
+using ClientReaderWriter =
+ NanopbClientReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>;
+
+// These tests cover interactions between a thread moving or destroying an RPC
+// call object and a thread running callbacks for that call. In order to test
+// that the first thread waits for callbacks to complete when trying to move or
+// destroy the call, it is necessary to have the callback thread yield to the
+// other thread. There isn't a good way to synchronize these threads without
+// changing the code under test.
+void YieldToOtherThread() {
+ // Sleep for a while and then yield just to be sure the other thread runs.
+ this_thread::sleep_for(100ms);
+ this_thread::yield();
+}
+
+class CallbacksTest : public ::testing::Test {
+ protected:
+ CallbacksTest()
+ : callback_thread_(
+ // TODO: b/290860904 - Replace TestOptionsThread0 with
+ // TestThreadContext.
+ thread::test::TestOptionsThread0(),
+ [](void* arg) {
+ static_cast<CallbacksTest*>(arg)->SendResponseAfterSemaphore();
+ },
+ this) {}
+
+ ~CallbacksTest() override {
+ EXPECT_FALSE(callback_thread_.joinable()); // Tests must join the thread!
+ }
+
+ void RespondToCall(const ClientReaderWriter& call) {
+ respond_to_call_ = &call;
+ }
+
+ NanopbClientTestContext<> context_;
+ sync::BinarySemaphore callback_thread_sem_;
+ sync::BinarySemaphore main_thread_sem_;
+
+ thread::Thread callback_thread_;
+
+ // Must be incremented exactly once by the RPC callback in each test.
+ volatile int callback_executed_ = 0;
+
+ // Variables optionally used by tests. These are in this object so lambads
+ // only need to capture [this] to access them.
+ volatile bool call_is_in_scope_ = false;
+
+ ClientReaderWriter call_1_;
+ ClientReaderWriter call_2_;
+
+ private:
+ void SendResponseAfterSemaphore() {
+ // Wait until the main thread says to send the response.
+ callback_thread_sem_.acquire();
+
+ context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {}, respond_to_call_->id());
+ }
+
+ const ClientReaderWriter* respond_to_call_ = &call_1_;
+};
+
+TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
+ // Skip this test if locks are disabled because the thread can't yield.
+ if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
+ callback_thread_sem_.release();
+ callback_thread_.join();
+ GTEST_SKIP();
+ }
+
+ {
+ ClientReaderWriter local_call = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id());
+ RespondToCall(local_call);
+
+ call_is_in_scope_ = true;
+
+ local_call.set_on_next([this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release();
+
+ // Wait for a while so the main thread tries to destroy the call.
+ YieldToOtherThread();
+
+ // Now, make sure the call is still in scope. The main thread should
+ // block in the call's destructor until this callback completes.
+ EXPECT_TRUE(call_is_in_scope_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread so it can invoke the callback.
+ callback_thread_sem_.release();
+
+ // Wait until the callback thread starts.
+ main_thread_sem_.acquire();
+ }
+
+ // The callback thread will sleep for a bit. Meanwhile, let the call go out
+ // of scope, and mark it as such.
+ call_is_in_scope_ = false;
+
+ // Wait for the callback thread to finish.
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+}
+
+TEST_F(CallbacksTest, MoveActiveCall_WaitsForCallbackToComplete) {
+ // Skip this test if locks are disabled because the thread can't yield.
+ if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
+ callback_thread_sem_.release();
+ callback_thread_.join();
+ GTEST_SKIP();
+ }
+
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ YieldToOtherThread();
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread so it can invoke the callback.
+ callback_thread_sem_.release();
+
+ // Confirm that the callback thread started.
+ main_thread_sem_.acquire();
+
+ // Move the call object. This thread should wait until the on_completed
+ // callback is done.
+ EXPECT_TRUE(call_1_.active());
+ call_2_ = std::move(call_1_);
+
+ // The callback should already have finished. This thread should have waited
+ // for it to finish during the move.
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_FALSE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ callback_thread_.join();
+}
+
+TEST_F(CallbacksTest, MoveOtherCallIntoOwnCallInCallback) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ call_1_ = std::move(call_2_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ call_2_ = TestService::TestBidirectionalStreamRpc(context_.client(),
+ context_.channel().id());
+
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ // Start the callback thread and wait for it to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_FALSE(call_2_.active());
+}
+
+TEST_F(CallbacksTest, MoveOwnCallInCallback) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ // Cancel this call first, or the move will deadlock, since the moving
+ // thread will wait for the callback thread (both this thread) to
+ // terminate if the call is active.
+ EXPECT_EQ(OkStatus(), call_1_.Cancel());
+ call_2_ = std::move(call_1_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ call_2_ = TestService::TestBidirectionalStreamRpc(context_.client(),
+ context_.channel().id());
+
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ // Start the callback thread and wait for it to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_FALSE(call_1_.active());
+ EXPECT_FALSE(call_2_.active());
+}
+
+TEST_F(CallbacksTest, PacketDroppedIfOnNextIsBusy) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(),
+ context_.channel().id(),
+ [this](const pw_rpc_test_TestStreamResponse&) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ callback_thread_sem_.acquire(); // Wait for the main thread to release
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread.
+ callback_thread_sem_.release();
+
+ main_thread_sem_.acquire(); // Confirm that the callback is running
+
+ // Handle a few packets for this call, which should be dropped since on_next
+ // is busy. callback_executed_ should remain at 1.
+ for (int i = 0; i < 5; ++i) {
+ context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {}, call_1_.id());
+ }
+
+ // Wait for the callback thread to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/client_reader_writer_test.cc b/pw_rpc/nanopb/client_reader_writer_test.cc
index 2e8b8fccf..66da9ce43 100644
--- a/pw_rpc/nanopb/client_reader_writer_test.cc
+++ b/pw_rpc/nanopb/client_reader_writer_test.cc
@@ -59,7 +59,7 @@ TEST(NanopbClientWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const pw_rpc_test_TestStreamResponse&, Status) {});
call.set_on_error([](Status) {});
@@ -72,6 +72,7 @@ TEST(NanopbClientReader, DefaultConstructed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
@@ -88,7 +89,75 @@ TEST(NanopbClientReaderWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(NanopbClientWriter, RequestCompletion) {
+ NanopbClientTestContext ctx;
+ NanopbClientWriter<pw_rpc_test_TestRequest, pw_rpc_test_TestStreamResponse>
+ call = TestService::TestClientStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnCompletedCalled<pw_rpc_test_TestStreamResponse>,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](const pw_rpc_test_TestStreamResponse&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(NanopbClientReader, RequestCompletion) {
+ NanopbClientTestContext ctx;
+ NanopbClientReader<pw_rpc_test_TestStreamResponse> call =
+ TestService::TestServerStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled<pw_rpc_test_TestStreamResponse>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(NanopbClientReaderWriter, RequestCompletion) {
+ NanopbClientTestContext ctx;
+ NanopbClientReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>
+ call = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled<pw_rpc_test_TestStreamResponse>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
@@ -130,7 +199,7 @@ TEST(NanopbClientWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const pw_rpc_test_TestStreamResponse&, Status) {});
call.set_on_error([](Status) {});
@@ -152,6 +221,7 @@ TEST(NanopbClientReader, Closed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
@@ -175,7 +245,7 @@ TEST(NanopbClientReaderWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const pw_rpc_test_TestStreamResponse&) {});
diff --git a/pw_rpc/nanopb/client_server_context_test.cc b/pw_rpc/nanopb/client_server_context_test.cc
index b023fdf4f..710efb311 100644
--- a/pw_rpc/nanopb/client_server_context_test.cc
+++ b/pw_rpc/nanopb/client_server_context_test.cc
@@ -11,10 +11,12 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+#include <array>
#include "gtest/gtest.h"
#include "pw_rpc/nanopb/client_server_testing.h"
#include "pw_rpc_test_protos/test.rpc.pb.h"
+#include "pw_sync/mutex.h"
namespace pw::rpc {
namespace {
@@ -29,8 +31,28 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const pw_rpc_test_TestRequest&,
- NanopbUnaryResponder<pw_rpc_test_TestResponse>&) {}
+ Status TestAnotherUnaryRpc(const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response) {
+ typedef std::array<uint32_t, 3> ArgType;
+ // The values array needs to be kept in memory until after this method call
+ // returns since the response is not encoded until after returning from this
+ // method.
+ static const ArgType values = {7, 8, 9};
+ response.repeated_field.funcs.encode = +[](pb_ostream_t* stream,
+ const pb_field_t* field,
+ void* const* arg) -> bool {
+ // Note: nanopb passes the pointer to the repeated_filed.arg member as
+ // arg, not its contents.
+ for (auto elem : *static_cast<const ArgType*>(*arg)) {
+ if (!pb_encode_tag_for_field(stream, field) ||
+ !pb_encode_varint(stream, elem))
+ return false;
+ }
+ return true;
+ };
+ response.repeated_field.arg = const_cast<ArgType*>(&values);
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(
const pw_rpc_test_TestRequest&,
@@ -44,7 +66,7 @@ class TestService final : public GeneratedService::Service<TestService> {
pw_rpc_test_TestStreamResponse>&) {}
};
-TEST(NanopbClientServerTestContext, ReceivesUnaryRpcReponse) {
+TEST(NanopbClientServerTestContext, ReceivesUnaryRpcResponse) {
NanopbClientServerTestContext<> ctx;
TestService service;
ctx.server().RegisterService(service);
@@ -70,7 +92,7 @@ TEST(NanopbClientServerTestContext, ReceivesUnaryRpcReponse) {
EXPECT_EQ(request.integer, sent_request.integer);
}
-TEST(NanopbClientServerTestContext, ReceivesMultipleReponses) {
+TEST(NanopbClientServerTestContext, ReceivesMultipleResponses) {
NanopbClientServerTestContext<> ctx;
TestService service;
ctx.server().RegisterService(service);
@@ -112,5 +134,121 @@ TEST(NanopbClientServerTestContext, ReceivesMultipleReponses) {
EXPECT_EQ(request2.integer, sent_request2.integer);
}
+TEST(NanopbClientServerTestContext,
+ ReceivesMultipleResponsesWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ NanopbClientServerTestContext<> ctx(server_processor, client_processor);
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ pw_rpc_test_TestResponse response1 pw_rpc_test_TestResponse_init_default;
+ pw_rpc_test_TestResponse response2 pw_rpc_test_TestResponse_init_default;
+ auto handler1 = [&response1](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response1 = server_response; };
+ auto handler2 = [&response2](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response2 = server_response; };
+
+ pw_rpc_test_TestRequest request1{.integer = 1,
+ .status_code = OkStatus().code()};
+ pw_rpc_test_TestRequest request2{.integer = 2,
+ .status_code = OkStatus().code()};
+ const auto call1 = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request1, handler1);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+ const auto call2 = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request2, handler2);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request1 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_request2 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+ const auto sent_response1 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_response2 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(response1.value, request1.integer + 1);
+ EXPECT_EQ(response2.value, request2.integer + 1);
+ EXPECT_EQ(response1.value, sent_response1.value);
+ EXPECT_EQ(response2.value, sent_response2.value);
+ EXPECT_EQ(request1.integer, sent_request1.integer);
+ EXPECT_EQ(request2.integer, sent_request2.integer);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(NanopbClientServerTestContext, ResponseWithCallbacks) {
+ NanopbClientServerTestContext<> ctx;
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ const auto call = GeneratedService::TestAnotherUnaryRpc(
+ ctx.client(), ctx.channel().id(), pw_rpc_test_TestRequest_init_default);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ // To decode a response object that requires to set pb_callback_t members,
+ // pass it to the response() method as a parameter.
+ constexpr size_t kMaxNumValues = 4;
+ struct DecoderContext {
+ uint32_t num_calls = 0;
+ uint32_t values[kMaxNumValues];
+ bool failed = false;
+ } decoder_context;
+
+ pw_rpc_test_TestResponse response = pw_rpc_test_TestResponse_init_default;
+ response.repeated_field.funcs.decode = +[](pb_istream_t* stream,
+ const pb_field_t* /* field */,
+ void** arg) -> bool {
+ DecoderContext* dec_ctx = static_cast<DecoderContext*>(*arg);
+ uint64_t value;
+ if (!pb_decode_varint(stream, &value)) {
+ dec_ctx->failed = true;
+ return false;
+ }
+ if (dec_ctx->num_calls < kMaxNumValues) {
+ dec_ctx->values[dec_ctx->num_calls] = value;
+ }
+ dec_ctx->num_calls++;
+ return true;
+ };
+ response.repeated_field.arg = &decoder_context;
+ ctx.response<GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_FALSE(decoder_context.failed);
+ EXPECT_EQ(3u, decoder_context.num_calls);
+ EXPECT_EQ(7u, decoder_context.values[0]);
+ EXPECT_EQ(8u, decoder_context.values[1]);
+ EXPECT_EQ(9u, decoder_context.values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/client_server_context_threaded_test.cc b/pw_rpc/nanopb/client_server_context_threaded_test.cc
index 68be69960..0ebefa48d 100644
--- a/pw_rpc/nanopb/client_server_context_threaded_test.cc
+++ b/pw_rpc/nanopb/client_server_context_threaded_test.cc
@@ -12,11 +12,14 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <atomic>
+
#include "gtest/gtest.h"
#include "pw_rpc/nanopb/client_server_testing_threaded.h"
#include "pw_rpc_test_protos/test.rpc.pb.h"
#include "pw_sync/binary_semaphore.h"
-#include "pw_thread/test_threads.h"
+#include "pw_sync/mutex.h"
+#include "pw_thread/non_portable_test_thread_options.h"
namespace pw::rpc {
namespace {
@@ -31,8 +34,28 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const pw_rpc_test_TestRequest&,
- NanopbUnaryResponder<pw_rpc_test_TestResponse>&) {}
+ Status TestAnotherUnaryRpc(const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response) {
+ typedef std::array<uint32_t, 3> ArgType;
+ // The values array needs to be kept in memory until after this method call
+ // returns since the response is not encoded until after returning from this
+ // method.
+ static const ArgType values = {7, 8, 9};
+ response.repeated_field.funcs.encode = +[](pb_ostream_t* stream,
+ const pb_field_t* field,
+ void* const* arg) -> bool {
+ // Note: nanopb passes the pointer to the repeated_filed.arg member as
+ // arg, not its contents.
+ for (auto elem : *static_cast<const ArgType*>(*arg)) {
+ if (!pb_encode_tag_for_field(stream, field) ||
+ !pb_encode_varint(stream, elem))
+ return false;
+ }
+ return true;
+ };
+ response.repeated_field.arg = const_cast<ArgType*>(&values);
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(
const pw_rpc_test_TestRequest&,
@@ -48,34 +71,44 @@ class TestService final : public GeneratedService::Service<TestService> {
class RpcCaller {
public:
- void BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ template <auto kMethod = GeneratedService::TestUnaryRpc>
+ Status BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ response_status_ = OkStatus();
pw_rpc_test_TestRequest request{.integer = i,
.status_code = OkStatus().code()};
- auto call = GeneratedService::TestUnaryRpc(
+ auto call = kMethod(
client,
channel_id,
request,
- [this](const pw_rpc_test_TestResponse&, Status) {
+ [this](const pw_rpc_test_TestResponse&, Status status) {
+ response_status_ = status;
semaphore_.release();
},
- [](Status) {});
+ [this](Status status) {
+ response_status_ = status;
+ semaphore_.release();
+ });
semaphore_.acquire();
+ return response_status_;
}
private:
pw::sync::BinarySemaphore semaphore_;
+ Status response_status_ = OkStatus();
};
-TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
+TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcResponseThreaded) {
NanopbClientServerTestContextThreaded<> ctx(
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
thread::test::TestOptionsThread0());
TestService service;
ctx.server().RegisterService(service);
RpcCaller caller;
constexpr auto value = 1;
- caller.BlockOnResponse(value, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request =
ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
@@ -86,7 +119,7 @@ TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
EXPECT_EQ(value + 1, response.value);
}
-TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
+TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleResponsesThreaded) {
NanopbClientServerTestContextThreaded<> ctx(
thread::test::TestOptionsThread0());
TestService service;
@@ -95,8 +128,10 @@ TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
RpcCaller caller;
constexpr auto value1 = 1;
constexpr auto value2 = 2;
- caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id());
- caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request1 =
ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
@@ -113,5 +148,109 @@ TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
EXPECT_EQ(value2 + 1, response2.value);
}
+TEST(NanopbClientServerTestContextThreaded,
+ ReceivesMultipleResponsesThreadedWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ NanopbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0(), server_processor, client_processor);
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value1 = 1;
+ constexpr auto value2 = 2;
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
+
+ const auto request1 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto request2 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+ const auto response1 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto response2 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(value1, request1.integer);
+ EXPECT_EQ(value2, request2.integer);
+ EXPECT_EQ(value1 + 1, response1.value);
+ EXPECT_EQ(value2 + 1, response2.value);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(NanopbClientServerTestContextThreaded, ResponseWithCallbacks) {
+ NanopbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0());
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ EXPECT_EQ(caller.BlockOnResponse<GeneratedService::TestAnotherUnaryRpc>(
+ 0, ctx.client(), ctx.channel().id()),
+ OkStatus());
+
+ // To decode a response object that requires to set pb_callback_t members,
+ // pass it to the response() method as a parameter.
+ constexpr size_t kMaxNumValues = 4;
+ struct DecoderContext {
+ uint32_t num_calls = 0;
+ uint32_t values[kMaxNumValues];
+ bool failed = false;
+ } decoder_context;
+
+ pw_rpc_test_TestResponse response = pw_rpc_test_TestResponse_init_default;
+ response.repeated_field.funcs.decode = +[](pb_istream_t* stream,
+ const pb_field_t* /* field */,
+ void** arg) -> bool {
+ DecoderContext* dec_ctx = static_cast<DecoderContext*>(*arg);
+ uint64_t value;
+ if (!pb_decode_varint(stream, &value)) {
+ dec_ctx->failed = true;
+ return false;
+ }
+ if (dec_ctx->num_calls < kMaxNumValues) {
+ dec_ctx->values[dec_ctx->num_calls] = value;
+ }
+ dec_ctx->num_calls++;
+ return true;
+ };
+ response.repeated_field.arg = &decoder_context;
+ ctx.response<GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_FALSE(decoder_context.failed);
+ EXPECT_EQ(3u, decoder_context.num_calls);
+ EXPECT_EQ(7u, decoder_context.values[0]);
+ EXPECT_EQ(8u, decoder_context.values[1]);
+ EXPECT_EQ(9u, decoder_context.values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/docs.rst b/pw_rpc/nanopb/docs.rst
index 993f7c5fb..4ae7dedba 100644
--- a/pw_rpc/nanopb/docs.rst
+++ b/pw_rpc/nanopb/docs.rst
@@ -16,25 +16,25 @@ Define a ``pw_proto_library`` containing the .proto file defining your service
(and optionally other related protos), then depend on the ``nanopb_rpc``
version of that library in the code implementing the service.
-.. code::
+.. code-block::
- # chat/BUILD.gn
+ # chat/BUILD.gn
- import("$dir_pw_build/target_types.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
- pw_proto_library("chat_protos") {
- sources = [ "chat_protos/chat_service.proto" ]
- }
+ pw_proto_library("chat_protos") {
+ sources = [ "chat_protos/chat_service.proto" ]
+ }
- # Library that implements the Chat service.
- pw_source_set("chat_service") {
- sources = [
- "chat_service.cc",
- "chat_service.h",
- ]
- public_deps = [ ":chat_protos.nanopb_rpc" ]
- }
+ # Library that implements the Chat service.
+ pw_source_set("chat_service") {
+ sources = [
+ "chat_service.cc",
+ "chat_service.h",
+ ]
+ public_deps = [ ":chat_protos.nanopb_rpc" ]
+ }
A C++ header file is generated for each input .proto file, with the ``.proto``
extension replaced by ``.rpc.pb.h``. For example, given the input file
@@ -45,7 +45,7 @@ Generated code API
==================
All examples in this document use the following RPC service definition.
-.. code:: protobuf
+.. code-block:: protobuf
// chat/chat_protos/chat_service.proto
@@ -74,7 +74,7 @@ located within a special ``pw_rpc::nanopb`` sub-namespace of the file's package.
The generated class is a base class which must be derived to implement the
service's methods. The base class is templated on the derived class.
-.. code:: c++
+.. code-block:: c++
#include "chat_protos/chat_service.rpc.pb.h"
@@ -89,7 +89,7 @@ A unary RPC is implemented as a function which takes in the RPC's request struct
and populates a response struct to send back, with a status indicating whether
the request succeeded.
-.. code:: c++
+.. code-block:: c++
pw::Status GetRoomInformation(pw::rpc::
const RoomInfoRequest& request,
@@ -100,7 +100,7 @@ Server streaming RPC
A server streaming RPC receives the client's request message alongside a
``ServerWriter``, used to stream back responses.
-.. code:: c++
+.. code-block:: c++
void ListUsersInRoom(pw::rpc::
const ListUsersRequest& request,
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
index 3aac5a576..bc3fbe814 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
@@ -55,6 +55,8 @@ class NanopbUnaryResponseClientCall : public UnaryResponseClientCall {
return call;
}
+ ~NanopbUnaryResponseClientCall() { DestroyClientCall(); }
+
protected:
constexpr NanopbUnaryResponseClientCall() = default;
@@ -146,6 +148,8 @@ class NanopbStreamResponseClientCall : public StreamResponseClientCall {
return call;
}
+ ~NanopbStreamResponseClientCall() { DestroyClientCall(); }
+
protected:
constexpr NanopbStreamResponseClientCall() = default;
@@ -220,6 +224,8 @@ class NanopbClientReaderWriter
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Writes a response struct. Returns the following Status codes:
//
// OK - the response was successfully sent
@@ -233,16 +239,18 @@ class NanopbClientReaderWriter
&request);
}
- // Notifies the server that no further client stream messages will be sent.
- using internal::ClientCall::CloseClientStream;
+ // Notifies the server that the client has requested to stop communication by
+ // sending CLIENT_REQUEST_COMPLETION.
+ using internal::ClientCall::RequestCompletion;
// Cancels this RPC. Closes the call locally and sends a CANCELLED error to
// the server.
using internal::Call::Cancel;
- // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
- // packet. Future packets for this RPC are dropped, and the client sends a
- // FAILED_PRECONDITION error in response because the call is not active.
+ // Closes this RPC locally. Sends a CLIENT_REQUEST_COMPLETION, but no
+ // cancellation packet. Future packets for this RPC are dropped, and the
+ // client sends a FAILED_PRECONDITION error in response because the call is
+ // not active.
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
@@ -284,12 +292,15 @@ class NanopbClientReader
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting RPC event callbacks.
using internal::NanopbStreamResponseClientCall<Response>::set_on_next;
using internal::Call::set_on_error;
using internal::StreamResponseClientCall::set_on_completed;
using internal::Call::Cancel;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
private:
@@ -325,6 +336,8 @@ class NanopbClientWriter
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting RPC event callbacks.
using internal::NanopbUnaryResponseClientCall<Response>::set_on_completed;
using internal::Call::set_on_error;
@@ -335,7 +348,7 @@ class NanopbClientWriter
}
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
private:
@@ -371,6 +384,8 @@ class NanopbUnaryReceiver
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting RPC event callbacks.
using internal::NanopbUnaryResponseClientCall<Response>::set_on_completed;
using internal::Call::set_on_error;
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
index 429cb719c..c1dec95b9 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
@@ -15,6 +15,8 @@
#include <cinttypes>
+#include "pb_decode.h"
+#include "pb_encode.h"
#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/nanopb/fake_channel_output.h"
@@ -45,7 +47,11 @@ class NanopbForwardingChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr NanopbForwardingChannelOutput() = default;
+ constexpr NanopbForwardingChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index) {
@@ -54,6 +60,17 @@ class NanopbForwardingChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index) {
PW_ASSERT(Base::PacketCount() >= index);
return Base::output_.template requests<kMethod>(channel_id)[index];
@@ -90,7 +107,11 @@ class NanopbClientServerTestContext final
kPayloadsBufferSizeBytes>;
public:
- NanopbClientServerTestContext() = default;
+ NanopbClientServerTestContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -99,12 +120,21 @@ class NanopbClientServerTestContext final
index);
}
- // Retrieve copy of resonse indexed by order of occurance
+ // Retrieve copy of response indexed by order of occurance
template <auto kMethod>
Response<kMethod> response(uint32_t index) {
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using nanopb. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
index 0e08f0b38..187d267f5 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
@@ -15,6 +15,8 @@
#include <cinttypes>
+#include "pb_decode.h"
+#include "pb_encode.h"
#include "pw_rpc/internal/client_server_testing_threaded.h"
#include "pw_rpc/nanopb/fake_channel_output.h"
@@ -45,7 +47,11 @@ class NanopbWatchableChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr NanopbWatchableChannelOutput() = default;
+ constexpr NanopbWatchableChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index)
@@ -56,6 +62,18 @@ class NanopbWatchableChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index)
PW_LOCKS_EXCLUDED(Base::mutex_) {
std::lock_guard lock(Base::mutex_);
@@ -94,8 +112,13 @@ class NanopbClientServerTestContextThreaded final
kPayloadsBufferSizeBytes>;
public:
- NanopbClientServerTestContextThreaded(const thread::Options& options)
- : Base(options) {}
+ NanopbClientServerTestContextThreaded(
+ const thread::Options& options,
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(options,
+ std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -104,12 +127,21 @@ class NanopbClientServerTestContextThreaded final
index);
}
- // Retrieve copy of resonse indexed by order of occurance
+ // Retrieve copy of response indexed by order of occurance
template <auto kMethod>
Response<kMethod> response(uint32_t index) {
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using nanopb. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
index a06d53798..05c80af1b 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
@@ -15,6 +15,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
#include "pw_bytes/span.h"
#include "pw_rpc/client.h"
@@ -24,7 +25,7 @@
namespace pw::rpc {
-// TODO(b/234878467): Document the client testing APIs.
+// TODO: b/234878467 - Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server. Accepts
// payloads as Nanopb structs.
@@ -39,28 +40,32 @@ class NanopbFakeServer : public FakeServer {
// Sends a response packet for a server or bidirectional streaming RPC to the
// client.
template <auto kMethod>
- void SendResponse(Status status) const {
- FakeServer::SendResponse<kMethod>(status);
+ void SendResponse(Status status,
+ std::optional<uint32_t> call_id = std::nullopt) const {
+ FakeServer::SendResponse<kMethod>(status, call_id);
}
// Sends a response packet for a unary or client streaming streaming RPC to
// the client.
template <auto kMethod,
size_t kEncodeBufferSizeBytes = 2 * sizeof(Response<kMethod>)>
- void SendResponse(const Response<kMethod>& payload, Status status) const {
+ void SendResponse(const Response<kMethod>& payload,
+ Status status,
+ std::optional<uint32_t> call_id = std::nullopt) const {
std::byte buffer[kEncodeBufferSizeBytes] = {};
- FakeServer::SendResponse<kMethod>(EncodeResponse<kMethod>(&payload, buffer),
- status);
+ FakeServer::SendResponse<kMethod>(
+ EncodeResponse<kMethod>(&payload, buffer), status, call_id);
}
// Sends a stream packet for a server or bidirectional streaming RPC to the
// client.
template <auto kMethod,
size_t kEncodeBufferSizeBytes = 2 * sizeof(Response<kMethod>)>
- void SendServerStream(const Response<kMethod>& payload) const {
+ void SendServerStream(const Response<kMethod>& payload,
+ std::optional<uint32_t> call_id = std::nullopt) const {
std::byte buffer[kEncodeBufferSizeBytes] = {};
FakeServer::SendServerStream<kMethod>(
- EncodeResponse<kMethod>(&payload, buffer));
+ EncodeResponse<kMethod>(&payload, buffer), call_id);
}
private:
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
index f71c87795..150a42811 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
@@ -86,6 +86,9 @@ class NanopbPayloadsView {
iterator begin() const { return iterator(view_.begin(), serde_); }
iterator end() const { return iterator(view_.end(), serde_); }
+ PayloadsView& payloads() { return view_; }
+ internal::NanopbSerde& serde() { return serde_; }
+
private:
using Base =
containers::WrappedIterator<iterator, PayloadsView::iterator, Payload>;
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
index 06bb538a4..ebb80c289 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
@@ -46,6 +46,8 @@ class NanopbServerCall : public ServerCall {
NanopbServerCall(const LockedCallContext& context, MethodType type)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ ~NanopbServerCall() { DestroyServerCall(); }
+
Status SendUnaryResponse(const void* payload, Status status)
PW_LOCKS_EXCLUDED(rpc_lock()) {
return SendFinalResponse(*this, payload, status);
@@ -93,6 +95,8 @@ class BaseNanopbServerReader : public NanopbServerCall {
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: NanopbServerCall(context, type) {}
+ ~BaseNanopbServerReader() { DestroyServerCall(); }
+
protected:
constexpr BaseNanopbServerReader() = default;
@@ -192,7 +196,8 @@ class NanopbServerReaderWriter
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
using internal::BaseNanopbServerReader<Request>::set_on_next;
private:
@@ -249,7 +254,8 @@ class NanopbServerReader : private internal::BaseNanopbServerReader<Request> {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
using internal::BaseNanopbServerReader<Request>::set_on_next;
Status Finish(const Response& response, Status status = OkStatus()) {
@@ -322,7 +328,8 @@ class NanopbServerWriter : private internal::NanopbServerCall {
}
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
private:
friend class internal::NanopbMethod;
@@ -382,7 +389,6 @@ class NanopbUnaryResponder : private internal::NanopbServerCall {
}
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
private:
friend class internal::NanopbMethod;
diff --git a/pw_rpc/packet.cc b/pw_rpc/packet.cc
index 3e9803c23..0bd452190 100644
--- a/pw_rpc/packet.cc
+++ b/pw_rpc/packet.cc
@@ -14,6 +14,7 @@
#include "pw_rpc/internal/packet.h"
+#include "pw_log/log.h"
#include "pw_protobuf/decoder.h"
namespace pw::rpc::internal {
@@ -131,4 +132,25 @@ size_t Packet::MinEncodedSizeBytes() const {
return reserved_size;
}
+void Packet::DebugLog() const {
+ PW_LOG_INFO(
+ "Packet {\n"
+ " Type : %s (%d)\n"
+ " Channel: %u\n"
+ " Service: %08x\n"
+ " Method : %08x\n"
+ " ID : %08x\n"
+ " Payload: %u B\n"
+ " Status : %s\n"
+ "}",
+ PacketTypeToString(type_),
+ static_cast<int>(type_),
+ static_cast<unsigned>(channel_id_),
+ static_cast<unsigned>(service_id_),
+ static_cast<unsigned>(method_id_),
+ static_cast<unsigned>(call_id_),
+ static_cast<unsigned>(payload_.size()),
+ status_.str());
+}
+
} // namespace pw::rpc::internal
diff --git a/pw_rpc/packet_meta.cc b/pw_rpc/packet_meta.cc
index 5de0ba4b9..d0192d36f 100644
--- a/pw_rpc/packet_meta.cc
+++ b/pw_rpc/packet_meta.cc
@@ -36,4 +36,4 @@ Result<PacketMeta> PacketMeta::FromBuffer(ConstByteSpan data) {
return PacketMeta(packet);
}
-} // namespace pw::rpc \ No newline at end of file
+} // namespace pw::rpc
diff --git a/pw_rpc/packet_meta_test.cc b/pw_rpc/packet_meta_test.cc
index d21f3a48d..0f8a9683a 100644
--- a/pw_rpc/packet_meta_test.cc
+++ b/pw_rpc/packet_meta_test.cc
@@ -15,21 +15,22 @@
#include "pw_rpc/packet_meta.h"
#include "gtest/gtest.h"
+#include "pw_fuzzer/fuzztest.h"
#include "pw_rpc/internal/packet.h"
namespace pw::rpc {
namespace {
-TEST(PacketMeta, FromBufferDecodesValidMinimalPacket) {
- const uint32_t kChannelId = 12;
- const ServiceId kServiceId = internal::WrapServiceId(0xdeadbeef);
- const uint32_t kMethodId = 44;
+using namespace fuzzer;
+void FromBufferDecodesValidMinimalPacket(uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id) {
internal::Packet packet;
- packet.set_channel_id(kChannelId);
- packet.set_service_id(internal::UnwrapServiceId(kServiceId));
+ packet.set_channel_id(channel_id);
+ packet.set_service_id(service_id);
packet.set_type(internal::pwpb::PacketType::RESPONSE);
- packet.set_method_id(kMethodId);
+ packet.set_method_id(method_id);
std::byte buffer[128];
Result<ConstByteSpan> encode_result = packet.Encode(buffer);
@@ -37,11 +38,21 @@ TEST(PacketMeta, FromBufferDecodesValidMinimalPacket) {
Result<PacketMeta> decode_result = PacketMeta::FromBuffer(*encode_result);
ASSERT_EQ(decode_result.status(), OkStatus());
- EXPECT_EQ(decode_result->channel_id(), kChannelId);
- EXPECT_EQ(decode_result->service_id(), kServiceId);
+ EXPECT_EQ(decode_result->channel_id(), channel_id);
+ EXPECT_EQ(decode_result->service_id(), internal::WrapServiceId(service_id));
EXPECT_TRUE(decode_result->destination_is_client());
}
+TEST(PacketMeta, FromBufferDecodesValidMinimalPacketConst) {
+ const uint32_t kChannelId = 12;
+ const uint32_t kServiceId = 0xdeadbeef;
+ const uint32_t kMethodId = 44;
+ FromBufferDecodesValidMinimalPacket(kChannelId, kServiceId, kMethodId);
+}
+
+FUZZ_TEST(PacketMeta, FromBufferDecodesValidMinimalPacket)
+ .WithDomains(NonZero<uint32_t>(), NonZero<uint32_t>(), NonZero<uint32_t>());
+
TEST(PacketMeta, FromBufferFailsOnIncompletePacket) {
internal::Packet packet;
diff --git a/pw_rpc/packet_test.cc b/pw_rpc/packet_test.cc
index a0d7da3b6..b1051950f 100644
--- a/pw_rpc/packet_test.cc
+++ b/pw_rpc/packet_test.cc
@@ -16,6 +16,7 @@
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
+#include "pw_fuzzer/fuzztest.h"
#include "pw_protobuf/wire_format.h"
namespace pw::rpc::internal {
@@ -24,6 +25,7 @@ namespace {
using protobuf::FieldKey;
using ::pw::rpc::internal::pwpb::PacketType;
using std::byte;
+using namespace fuzzer;
constexpr auto kPayload = bytes::Array<0x82, 0x02, 0xff, 0xff>();
@@ -116,16 +118,19 @@ TEST(Packet, Decode_InvalidPacket) {
EXPECT_EQ(Status::DataLoss(), Packet::FromBuffer(bad_data).status());
}
-TEST(Packet, EncodeDecode) {
- constexpr byte payload[]{byte(0x00), byte(0x01), byte(0x02), byte(0x03)};
-
+void EncodeDecode(uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ uint32_t call_id,
+ ConstByteSpan payload,
+ Status status) {
Packet packet;
- packet.set_channel_id(12);
- packet.set_service_id(0xdeadbeef);
- packet.set_method_id(0x03a82921);
- packet.set_call_id(33);
+ packet.set_channel_id(channel_id);
+ packet.set_service_id(service_id);
+ packet.set_method_id(method_id);
+ packet.set_call_id(call_id);
packet.set_payload(payload);
- packet.set_status(Status::Unavailable());
+ packet.set_status(status);
byte buffer[128];
Result result = packet.Encode(buffer);
@@ -146,9 +151,22 @@ TEST(Packet, EncodeDecode) {
packet.payload().data(),
packet.payload().size()),
0);
- EXPECT_EQ(decoded.status(), Status::Unavailable());
+ EXPECT_EQ(decoded.status(), status);
+}
+
+TEST(Packet, EncodeDecodeFixed) {
+ constexpr byte payload[]{byte(0x00), byte(0x01), byte(0x02), byte(0x03)};
+ EncodeDecode(12, 0xdeadbeef, 0x03a82921, 33, payload, Status::Unavailable());
}
+FUZZ_TEST(Packet, EncodeDecode)
+ .WithDomains(NonZero<uint32_t>(),
+ NonZero<uint32_t>(),
+ NonZero<uint32_t>(),
+ NonZero<uint32_t>(),
+ VectorOf<100>(Arbitrary<byte>()),
+ Arbitrary<Status>());
+
constexpr size_t kReservedSize = 2 /* type */ + 2 /* channel */ +
5 /* service */ + 5 /* method */ +
2 /* payload key */ + 2 /* status */;
diff --git a/pw_rpc/public/pw_rpc/benchmark.h b/pw_rpc/public/pw_rpc/benchmark.h
index 7ac45133c..ea40b7708 100644
--- a/pw_rpc/public/pw_rpc/benchmark.h
+++ b/pw_rpc/public/pw_rpc/benchmark.h
@@ -13,12 +13,17 @@
// the License.
#pragma once
+#include <cstdint>
+#include <unordered_map>
+
#include "pw_rpc/benchmark.raw_rpc.pb.h"
namespace pw::rpc {
// RPC service with low-level RPCs for transmitting data. Used for benchmarking
// and testing.
+//
+// NOTE: the implementation of `BidirectionalEcho` is *not* thread-safe.
class BenchmarkService
: public pw_rpc::raw::Benchmark::Service<BenchmarkService> {
public:
@@ -27,7 +32,11 @@ class BenchmarkService
void BidirectionalEcho(RawServerReaderWriter& reader_writer);
private:
- RawServerReaderWriter reader_writer_;
+ using ReaderWriterId = uint64_t;
+ ReaderWriterId AllocateReaderWriterId();
+
+ ReaderWriterId next_reader_writer_id_ = 0;
+ std::unordered_map<ReaderWriterId, RawServerReaderWriter> reader_writers_;
};
} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/channel.h b/pw_rpc/public/pw_rpc/channel.h
index 19c65b428..956548a93 100644
--- a/pw_rpc/public/pw_rpc/channel.h
+++ b/pw_rpc/public/pw_rpc/channel.h
@@ -117,7 +117,7 @@ class Channel {
// Creates a dynamically assignable channel without a set ID or output.
constexpr Channel() : id_(kUnassignedChannelId), output_(nullptr) {}
- // TODO(b/234876441): Remove the Configure and set_channel_output functions.
+ // TODO: b/234876441 - Remove the Configure and set_channel_output functions.
// Users should call CloseChannel() / OpenChannel() to change a channel.
// This ensures calls are properly update and works consistently between
// static and dynamic channel allocation.
diff --git a/pw_rpc/public/pw_rpc/internal/call.h b/pw_rpc/public/pw_rpc/internal/call.h
index ca9597a04..bb6cdc7e1 100644
--- a/pw_rpc/public/pw_rpc/internal/call.h
+++ b/pw_rpc/public/pw_rpc/internal/call.h
@@ -64,9 +64,10 @@ class CallProperties {
constexpr CallProperties(MethodType method_type,
CallType call_type,
CallbackProtoType callback_proto_type)
- : bits_((static_cast<uint8_t>(method_type) << 0) |
- (static_cast<uint8_t>(call_type) << 2) |
- (static_cast<uint8_t>(callback_proto_type) << 3)) {}
+ : bits_(static_cast<uint8_t>(
+ (static_cast<uint8_t>(method_type) << 0) |
+ (static_cast<uint8_t>(call_type) << 2) |
+ (static_cast<uint8_t>(callback_proto_type) << 3))) {}
constexpr CallProperties(const CallProperties&) = default;
@@ -100,8 +101,20 @@ inline constexpr uint32_t kOpenCallId = std::numeric_limits<uint32_t>::max();
//
// Private inheritance is used in place of composition or more complex
// inheritance hierarchy so that these objects all inherit from a common
-// IntrusiveList::Item object. Private inheritance also gives the derived classs
+// IntrusiveList::Item object. Private inheritance also gives the derived class
// full control over their interfaces.
+//
+// IMPLEMENTATION NOTE:
+//
+// Subclasses of `Call` must include a destructor which calls
+// `DestroyServerCall` or `DestroyClientCall` (as appropriate) if the subclass
+// contains any fields which might be referenced by the call's callbacks. This
+// ensures that the callbacks do not reference fields which may have already
+// been destroyed.
+//
+// At the top level, `ServerCall` and `ClientCall` invoke `DestroyServerCall`
+// `DestroyClientCall` respectively to perform cleanup in the case where no
+// subclass carries additional state.
class Call : public IntrusiveList<Call>::Item {
public:
Call(const Call&) = delete;
@@ -112,7 +125,12 @@ class Call : public IntrusiveList<Call>::Item {
Call& operator=(const Call&) = delete;
Call& operator=(Call&&) = delete;
- ~Call() PW_LOCKS_EXCLUDED(rpc_lock());
+ ~Call() {
+ // Ensure that calls have already been closed and unregistered.
+ // See class IMPLEMENTATION NOTE for further details.
+ PW_DASSERT((state_ & kHasBeenDestroyed) != 0);
+ PW_DASSERT(!active_locked() && !CallbacksAreRunning());
+ }
// True if the Call is active and ready to send responses.
[[nodiscard]] bool active() const PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -183,16 +201,24 @@ class Call : public IntrusiveList<Call>::Item {
pwpb::PacketType::SERVER_ERROR, {}, error);
}
- // Public function that ends the client stream for a client call.
- Status CloseClientStream() PW_LOCKS_EXCLUDED(rpc_lock()) {
+ // Public function that indicates that the client requests completion of the
+ // RPC, but is still active and listening to responses. For client streaming
+ // and bi-directional streaming RPCs, this also closes the client stream. If
+ // PW_RPC_COMPLETION_REQUEST_CALLBACK is enabled and
+ // on_client_requested_completion callback is set using the
+ // set_on_completion_requested_if_enabled, then the callback will be invoked
+ // on the server side. The server may then take an appropriate action to
+ // cleanup and stop server streaming.
+ Status RequestCompletion() PW_LOCKS_EXCLUDED(rpc_lock()) {
RpcLockGuard lock;
- return CloseClientStreamLocked();
+ return RequestCompletionLocked();
}
- // Internal function that closes the client stream.
- Status CloseClientStreamLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- MarkClientStreamCompleted();
- return SendPacket(pwpb::PacketType::CLIENT_STREAM_END, {}, {});
+ // Internal function that closes the client stream (if applicable) and sends
+ // CLIENT_REQUEST_COMPLETION packet to request call completion.
+ Status RequestCompletionLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MarkStreamCompleted();
+ return SendPacket(pwpb::PacketType::CLIENT_REQUEST_COMPLETION, {}, {});
}
// Sends a payload in either a server or client stream packet.
@@ -254,8 +280,10 @@ class Call : public IntrusiveList<Call>::Item {
return HasServerStream(properties_.method_type());
}
- bool client_stream_open() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return (state_ & kClientStreamActive) != 0;
+ // Returns true if the client has already requested completion.
+ bool client_requested_completion() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return (state_ & kClientRequestedCompletion) != 0;
}
// Closes a call without doing anything else. Called from the Endpoint
@@ -266,6 +294,9 @@ class Call : public IntrusiveList<Call>::Item {
endpoint_ = nullptr;
}
+ // Logs detailed info about this call at INFO level. NOT for production use!
+ void DebugLog() const;
+
protected:
// Creates an inactive Call.
constexpr Call()
@@ -290,6 +321,11 @@ class Call : public IntrusiveList<Call>::Item {
uint32_t method_id,
CallProperties properties) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ // Closes the call and waits for their callbacks to complete so destructors
+ // can run safely.
+ void DestroyServerCall() PW_LOCKS_EXCLUDED(rpc_lock());
+ void DestroyClientCall() PW_LOCKS_EXCLUDED(rpc_lock());
+
void CallbackStarted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
callbacks_executing_ += 1;
}
@@ -331,10 +367,15 @@ class Call : public IntrusiveList<Call>::Item {
on_error_ = std::move(on_error);
}
- void MarkClientStreamCompleted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- state_ &= ~kClientStreamActive;
+ void MarkStreamCompleted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ state_ |= kClientRequestedCompletion;
}
+ // Closes a client call. Sends a CLIENT_REQUEST_COMPLETION for client /
+ // bidirectional streaming RPCs if not sent yet.
+ void CloseClientCall() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
+ // Closes a server call.
Status CloseAndSendResponseLocked(Status status)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return CloseAndSendFinalPacketLocked(
@@ -438,8 +479,9 @@ class Call : public IntrusiveList<Call>::Item {
private:
enum State : uint8_t {
- kActive = 0b01,
- kClientStreamActive = 0b10,
+ kActive = 0b001,
+ kClientRequestedCompletion = 0b010,
+ kHasBeenDestroyed = 0b100,
};
// Common constructor for server & client calls.
@@ -468,7 +510,7 @@ class Call : public IntrusiveList<Call>::Item {
void MarkClosed() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
channel_id_ = Channel::kUnassignedChannelId;
id_ = 0;
- state_ = 0;
+ state_ = kClientRequestedCompletion;
}
// Calls the on_error callback without closing the RPC. This is used when the
@@ -498,6 +540,9 @@ class Call : public IntrusiveList<Call>::Item {
return callbacks_executing_ != 0u;
}
+ // Waits for callbacks to complete so that a call object can be destroyed.
+ void WaitForCallbacksToComplete() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
Endpoint* endpoint_ PW_GUARDED_BY(rpc_lock());
uint32_t channel_id_ PW_GUARDED_BY(rpc_lock());
uint32_t id_ PW_GUARDED_BY(rpc_lock());
@@ -508,7 +553,7 @@ class Call : public IntrusiveList<Call>::Item {
//
// bit 0: call is active
// bit 1: client stream is active
- //
+ // bit 2: call has been destroyed
uint8_t state_ PW_GUARDED_BY(rpc_lock());
// If non-OK, indicates that the call was closed and needs to have its
diff --git a/pw_rpc/public/pw_rpc/internal/client_call.h b/pw_rpc/public/pw_rpc/internal/client_call.h
index 793d581e4..e30dbc3b1 100644
--- a/pw_rpc/public/pw_rpc/internal/client_call.h
+++ b/pw_rpc/public/pw_rpc/internal/client_call.h
@@ -26,8 +26,6 @@ namespace pw::rpc::internal {
// A Call object, as used by an RPC client.
class ClientCall : public Call {
public:
- ~ClientCall() PW_LOCKS_EXCLUDED(rpc_lock()) { Abandon(); }
-
uint32_t id() const PW_LOCKS_EXCLUDED(rpc_lock()) {
RpcLockGuard lock;
return Call::id();
@@ -53,6 +51,8 @@ class ClientCall : public Call {
CallProperties properties) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: Call(client, channel_id, service_id, method_id, properties) {}
+ ~ClientCall() { DestroyClientCall(); }
+
// Public function that closes a call client-side without cancelling it on the
// server.
void Abandon() PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -60,9 +60,6 @@ class ClientCall : public Call {
CloseClientCall();
}
- // Sends CLIENT_STREAM_END if applicable and marks the call as closed.
- void CloseClientCall() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
-
void MoveClientCallFrom(ClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
};
@@ -71,6 +68,8 @@ class ClientCall : public Call {
// on_completed callback. The on_next callback is not used.
class UnaryResponseClientCall : public ClientCall {
public:
+ ~UnaryResponseClientCall() { DestroyClientCall(); }
+
// Start call for raw unary response RPCs.
template <typename CallType>
static CallType Start(Endpoint& client,
@@ -143,6 +142,8 @@ class UnaryResponseClientCall : public ClientCall {
// callback. Payloads are sent through the on_next callback.
class StreamResponseClientCall : public ClientCall {
public:
+ ~StreamResponseClientCall() { DestroyClientCall(); }
+
// Start call for raw stream response RPCs.
template <typename CallType>
static CallType Start(Endpoint& client,
@@ -152,7 +153,7 @@ class StreamResponseClientCall : public ClientCall {
Function<void(ConstByteSpan)>&& on_next,
Function<void(Status)>&& on_completed,
Function<void(Status)>&& on_error,
- ConstByteSpan request) {
+ ConstByteSpan request) PW_LOCKS_EXCLUDED(rpc_lock()) {
rpc_lock().lock();
CallType call(client.ClaimLocked(), channel_id, service_id, method_id);
diff --git a/pw_rpc/public/pw_rpc/internal/client_server_testing.h b/pw_rpc/public/pw_rpc/internal/client_server_testing.h
index 01cfd16a3..f9523e61f 100644
--- a/pw_rpc/public/pw_rpc/internal/client_server_testing.h
+++ b/pw_rpc/public/pw_rpc/internal/client_server_testing.h
@@ -16,14 +16,19 @@
#include <cinttypes>
#include <mutex>
+#include "pw_function/function.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/client_server.h"
#include "pw_rpc/internal/fake_channel_output.h"
#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/packet_meta.h"
#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::rpc {
+using TestPacketProcessor =
+ pw::Function<pw::Status(ClientServer&, pw::ConstByteSpan)>;
+
namespace internal {
// Expands on a Fake Channel Output implementation to allow for forwarding of
@@ -50,14 +55,29 @@ class ForwardingChannelOutput : public ChannelOutput {
return false;
}
++sent_packets_;
- const auto process_result = client_server.ProcessPacket(*result);
+
+ Result<PacketMeta> meta = pw::rpc::PacketMeta::FromBuffer(*result);
+ PW_ASSERT(meta.ok());
+
+ pw::Status process_result = pw::Status::Internal();
+ if (meta->destination_is_server() && server_packet_processor_) {
+ process_result = server_packet_processor_(client_server, *result);
+ } else if (meta->destination_is_client() && client_packet_processor_) {
+ process_result = client_packet_processor_(client_server, *result);
+ } else {
+ process_result = client_server.ProcessPacket(*result);
+ }
PW_ASSERT(process_result.ok());
return true;
}
protected:
- constexpr ForwardingChannelOutput()
- : ChannelOutput("testing::FakeChannelOutput") {}
+ explicit ForwardingChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : ChannelOutput("testing::FakeChannelOutput"),
+ server_packet_processor_(std::move(server_packet_processor)),
+ client_packet_processor_(std::move(client_packet_processor)) {}
FakeChannelOutputImpl output_;
@@ -76,6 +96,9 @@ class ForwardingChannelOutput : public ChannelOutput {
}
uint16_t sent_packets_ = 0;
+
+ const TestPacketProcessor server_packet_processor_;
+ const TestPacketProcessor client_packet_processor_;
};
// Provides a testing context with a real client and server
@@ -97,8 +120,12 @@ class ClientServerTestContext {
}
protected:
- explicit ClientServerTestContext()
- : channel_(Channel::Create<1>(&channel_output_)),
+ explicit ClientServerTestContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : channel_output_(std::move(server_packet_processor),
+ std::move(client_packet_processor)),
+ channel_(Channel::Create<1>(&channel_output_)),
client_server_({&channel_, 1}) {}
~ClientServerTestContext() = default;
diff --git a/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h b/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
index ff838a28a..d10f19256 100644
--- a/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
+++ b/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
@@ -15,6 +15,7 @@
#include <cinttypes>
+#include "pw_function/function.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/client_server.h"
#include "pw_rpc/internal/client_server_testing.h"
@@ -73,7 +74,11 @@ class WatchableChannelOutput
}
protected:
- constexpr WatchableChannelOutput() = default;
+ explicit WatchableChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
size_t PacketCount() const PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) override {
return Base::PacketCount();
@@ -119,8 +124,13 @@ class ClientServerTestContextThreaded
}
protected:
- explicit ClientServerTestContextThreaded(const thread::Options& options)
- : thread_(options, Instance::Run, this) {}
+ explicit ClientServerTestContextThreaded(
+ const thread::Options& options,
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)),
+ thread_(options, Instance::Run, this) {}
private:
using Base::ForwardNewPackets;
diff --git a/pw_rpc/public/pw_rpc/internal/config.h b/pw_rpc/public/pw_rpc/internal/config.h
index 24a227159..dec868b05 100644
--- a/pw_rpc/public/pw_rpc/internal/config.h
+++ b/pw_rpc/public/pw_rpc/internal/config.h
@@ -18,18 +18,30 @@
#include <cstddef>
#include <type_traits>
-/// In client and bidirectional RPCs, pw_rpc clients may signal that they have
-/// finished sending requests with a `CLIENT_STREAM_END` packet. While this can
-/// be useful in some circumstances, it is often not necessary.
+#if defined(PW_RPC_CLIENT_STREAM_END_CALLBACK) && \
+ PW_RPC_CLIENT_STREAM_END_CALLBACK
+#pragma message( \
+ "Warning PW_RPC_CLIENT_STREAM_END_CALLBACK is deprecated! " \
+ "Use PW_RPC_COMPLETION_REQUEST_CALLBACK instead.")
+#define PW_RPC_COMPLETION_REQUEST_CALLBACK 1
+#endif
+
+#undef PW_RPC_CLIENT_STREAM_END_CALLBACK
+
+/// pw_rpc clients may request call completion by sending
+/// `CLIENT_REQUEST_COMPLETION` packet. For client streaming or bi-direction
+/// RPCs, this also indicates that the client is done sending requests. While
+/// this can be useful in some circumstances, it is often not necessary.
///
/// This option controls whether or not include a callback that is called when
-/// the client stream ends. The callback is included in all ServerReader/Writer
-/// objects as a @cpp_type{pw::Function}, so may have a significant cost.
+/// the client stream requests for completion. The callback is included in all
+/// ServerReader/Writer objects as a @cpp_type{pw::Function}, so may have a
+/// significant cost.
///
/// This is disabled by default.
-#ifndef PW_RPC_CLIENT_STREAM_END_CALLBACK
-#define PW_RPC_CLIENT_STREAM_END_CALLBACK 0
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+#ifndef PW_RPC_COMPLETION_REQUEST_CALLBACK
+#define PW_RPC_COMPLETION_REQUEST_CALLBACK 0
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
/// The Nanopb-based pw_rpc implementation allocates memory to use for Nanopb
/// structs for the request and response protobufs. The template function that
@@ -96,11 +108,13 @@
// When building for a desktop operating system, use a 1ms sleep by default.
// 1-tick duration sleeps can result in spurious timeouts.
-#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+#if defined(_WIN32) || defined(__APPLE__) || \
+ defined(__linux__) && !defined(__ZEPHYR__)
#define PW_RPC_YIELD_SLEEP_DURATION std::chrono::milliseconds(1)
#else
#define PW_RPC_YIELD_SLEEP_DURATION pw::chrono::SystemClock::duration(1)
#endif // defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+ // && !defined(__ZEPHYR__)
#endif // PW_RPC_YIELD_SLEEP_DURATION
@@ -185,6 +199,22 @@ static_assert(
#define PW_RPC_DYNAMIC_CONTAINER_INCLUDE <vector>
#endif // PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this macro must expand to
+/// a statement that creates a `std::unique_ptr`-like smart pointer.
+/// @param type The type of the object to construct (e.g. with `new`)
+/// @param ... Arguments to pass to the constructor, if any
+#ifndef PW_RPC_MAKE_UNIQUE_PTR
+#define PW_RPC_MAKE_UNIQUE_PTR(type, ...) \
+ std::unique_ptr<type>(new type(__VA_ARGS__))
+#endif // PW_RPC_DYNAMIC_CONTAINER
+
+/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this header file is
+/// included in files that use @c_macro{PW_RPC_MAKE_UNIQUE_PTR}. Defaults to
+/// `<memory>` for `std::make_unique`.
+#ifndef PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+#define PW_RPC_MAKE_UNIQUE_PTR_INCLUDE <memory>
+#endif // PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+
/// Size of the global RPC packet encoding buffer in bytes. If dynamic
/// allocation is enabled, this value is only used for test helpers that
/// allocate RPC encoding buffers.
@@ -205,7 +235,7 @@ static_assert(
namespace pw::rpc::cfg {
template <typename...>
-constexpr std::bool_constant<PW_RPC_CLIENT_STREAM_END_CALLBACK>
+constexpr std::bool_constant<PW_RPC_COMPLETION_REQUEST_CALLBACK>
kClientStreamEndCallbackEnabled;
template <typename...>
diff --git a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
index b60f5c598..b0e1e6795 100644
--- a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
+++ b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
@@ -158,8 +158,8 @@ class FakeChannelOutput : public ChannelOutput {
return internal::test::PacketsView(
packets_,
internal::test::PacketFilter(
- internal::pwpb::PacketType::CLIENT_STREAM_END,
- internal::pwpb::PacketType::CLIENT_STREAM_END,
+ internal::pwpb::PacketType::CLIENT_REQUEST_COMPLETION,
+ internal::pwpb::PacketType::CLIENT_REQUEST_COMPLETION,
channel_id,
MethodInfo<kMethod>::kServiceId,
MethodInfo<kMethod>::kMethodId))
diff --git a/pw_rpc/public/pw_rpc/internal/hash.h b/pw_rpc/public/pw_rpc/internal/hash.h
index 57fee5287..ae4efe903 100644
--- a/pw_rpc/public/pw_rpc/internal/hash.h
+++ b/pw_rpc/public/pw_rpc/internal/hash.h
@@ -30,14 +30,14 @@ constexpr uint32_t Hash(std::string_view string)
constexpr uint32_t kHashConstant = 65599;
// The length is hashed as if it were the first character.
- uint32_t hash = string.size();
+ uint32_t hash = static_cast<uint32_t>(string.size());
uint32_t coefficient = kHashConstant;
// Hash all of the characters in the string as unsigned ints.
// The coefficient calculation is done modulo 0x100000000, so the unsigned
// integer overflows are intentional.
- for (uint8_t ch : string) {
- hash += coefficient * ch;
+ for (char ch : string) {
+ hash += coefficient * static_cast<uint8_t>(ch);
coefficient *= kHashConstant;
}
diff --git a/pw_rpc/public/pw_rpc/internal/method_lookup.h b/pw_rpc/public/pw_rpc/internal/method_lookup.h
index 232e0c4db..13fc226ff 100644
--- a/pw_rpc/public/pw_rpc/internal/method_lookup.h
+++ b/pw_rpc/public/pw_rpc/internal/method_lookup.h
@@ -52,8 +52,14 @@ class MethodLookup {
template <typename Service, uint32_t kMethodId>
static constexpr const auto& GetMethodUnion() {
constexpr auto method = GetMethodUnionPointer<Service>(kMethodId);
+// TODO: b/285367496 - Remove this #ifndef guard when the static assert
+// compiles correctly when using the Andestech RISC-V GCC 10.3.0 toolchain.
+#if !(defined(__riscv) && defined(__nds_v5) && (__GNUC__ == 10) && \
+ (__GNUC_MINOR__ == 3) && (__GNUC_PATCHLEVEL__ == 0))
static_assert(method != nullptr,
"The selected function is not an RPC service method");
+#endif // !(defined(__riscv) && defined(__nds_v5) && (__GNUC__ == 10)
+ // && (__GNUC_MINOR__ == 3) && (__GNUC_PATCHLEVEL__ == 0))
return *method;
}
diff --git a/pw_rpc/public/pw_rpc/internal/packet.h b/pw_rpc/public/pw_rpc/internal/packet.h
index 64fe6b2e4..237356164 100644
--- a/pw_rpc/public/pw_rpc/internal/packet.h
+++ b/pw_rpc/public/pw_rpc/internal/packet.h
@@ -28,7 +28,7 @@ class Packet {
public:
static constexpr uint32_t kUnassignedId = 0;
- // TODO(b/236156534): This can use the pwpb generated
+ // TODO: b/236156534 - This can use the pwpb generated
// pw::rpc::internal::pwpb::RpcPacket::kMaxEncodedSizeBytes once the max value
// of enums is properly accounted for and when `status` is changed from a
// uint32 to a StatusCode.
@@ -141,6 +141,9 @@ class Packet {
constexpr void set_payload(ConstByteSpan payload) { payload_ = payload; }
constexpr void set_status(Status status) { status_ = status; }
+ // Logs detailed info about this packet at INFO level. NOT for production use!
+ void DebugLog() const;
+
private:
pwpb::PacketType type_;
uint32_t channel_id_;
diff --git a/pw_rpc/public/pw_rpc/internal/server_call.h b/pw_rpc/public/pw_rpc/internal/server_call.h
index 65cb7375d..f6cccb0f9 100644
--- a/pw_rpc/public/pw_rpc/internal/server_call.h
+++ b/pw_rpc/public/pw_rpc/internal/server_call.h
@@ -23,21 +23,22 @@ namespace pw::rpc::internal {
// A Call object, as used by an RPC server.
class ServerCall : public Call {
public:
- void HandleClientStreamEnd() PW_UNLOCK_FUNCTION(rpc_lock()) {
- MarkClientStreamCompleted();
+ void HandleClientRequestedCompletion() PW_UNLOCK_FUNCTION(rpc_lock()) {
+ MarkStreamCompleted();
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- auto on_client_stream_end_local = std::move(on_client_stream_end_);
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ auto on_client_requested_completion_local =
+ std::move(on_client_requested_completion_);
CallbackStarted();
rpc_lock().unlock();
- if (on_client_stream_end_local) {
- on_client_stream_end_local();
+ if (on_client_requested_completion_local) {
+ on_client_requested_completion_local();
}
rpc_lock().lock();
CallbackFinished();
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
rpc_lock().unlock();
}
@@ -46,10 +47,7 @@ class ServerCall : public Call {
ServerCall(ServerCall&& other) { *this = std::move(other); }
- ~ServerCall() PW_LOCKS_EXCLUDED(rpc_lock()) {
- // Any errors are logged in Channel::Send.
- CloseAndSendResponse(OkStatus()).IgnoreError();
- }
+ ~ServerCall() { DestroyServerCall(); }
// Version of operator= used by the raw call classes.
ServerCall& operator=(ServerCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -65,28 +63,43 @@ class ServerCall : public Call {
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: Call(context, properties) {}
- // set_on_client_stream_end is templated so that it can be conditionally
- // disabled with a helpful static_assert message.
+ // set_on_completion_requested is templated so that it can be
+ // conditionally disabled with a helpful static_assert message.
template <typename UnusedType = void>
- void set_on_client_stream_end(
- [[maybe_unused]] Function<void()>&& on_client_stream_end)
+ void set_on_completion_requested(
+ [[maybe_unused]] Function<void()>&& on_client_requested_completion)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ static_assert(cfg::kClientStreamEndCallbackEnabled<UnusedType>,
+ "The client stream end callback is disabled, so "
+ "set_on_completion_requested cannot be called. To "
+ "enable the client end "
+ "callback, set PW_RPC_REQUEST_COMPLETION_CALLBACK to 1.");
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ RpcLockGuard lock;
+ on_client_requested_completion_ = std::move(on_client_requested_completion);
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
+ }
+
+ // Sets the provided on_client_requested_completion callback if
+ // PW_RPC_COMPLETION_REQUEST_CALLBACK is defined. Unlike
+ // set_on_completion_requested this API will not raise a static_assert
+ // message at compile time even when the macro is not defined.
+ void set_on_completion_requested_if_enabled(
+ Function<void()>&& on_client_requested_completion)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- static_assert(
- cfg::kClientStreamEndCallbackEnabled<UnusedType>,
- "The client stream end callback is disabled, so "
- "set_on_client_stream_end cannot be called. To enable the client end "
- "callback, set PW_RPC_CLIENT_STREAM_END_CALLBACK to 1.");
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
RpcLockGuard lock;
- on_client_stream_end_ = std::move(on_client_stream_end);
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ on_client_requested_completion_ = std::move(on_client_requested_completion);
+#else
+ on_client_requested_completion = nullptr;
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
}
private:
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
// Called when a client stream completes.
- Function<void()> on_client_stream_end_ PW_GUARDED_BY(rpc_lock());
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ Function<void()> on_client_requested_completion_ PW_GUARDED_BY(rpc_lock());
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
};
} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h b/pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h
new file mode 100644
index 000000000..7ca7b991e
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/synchronous_call_impl.h
@@ -0,0 +1,200 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <utility>
+
+#include "pw_rpc/internal/config.h"
+#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/synchronous_call_result.h"
+#include "pw_sync/timed_thread_notification.h"
+
+#if PW_RPC_DYNAMIC_ALLOCATION
+#include PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+namespace pw::rpc::internal {
+
+template <typename Response>
+struct SynchronousCallState {
+ auto OnCompletedCallback() {
+ return [this](const Response& response, Status status) {
+ result = SynchronousCallResult<Response>(status, response);
+ notify.release();
+ };
+ }
+
+ auto OnRpcErrorCallback() {
+ return [this](Status status) {
+ result = SynchronousCallResult<Response>::RpcError(status);
+ notify.release();
+ };
+ }
+
+ SynchronousCallResult<Response> result;
+ sync::TimedThreadNotification notify;
+};
+
+class RawSynchronousCallState {
+ public:
+ RawSynchronousCallState(Function<void(ConstByteSpan, Status)> on_completed)
+ : on_completed_(std::move(on_completed)) {}
+
+ auto OnCompletedCallback() {
+ return [this](ConstByteSpan response, Status status) {
+ if (on_completed_) {
+ on_completed_(response, status);
+ }
+ notify.release();
+ };
+ }
+
+ auto OnRpcErrorCallback() {
+ return [this](Status status) {
+ error = status;
+ notify.release();
+ };
+ }
+
+ Status error;
+ sync::TimedThreadNotification notify;
+
+ private:
+ Function<void(ConstByteSpan, Status)> on_completed_;
+};
+
+// Overloaded function to choose detween timeout and deadline APIs.
+inline bool AcquireNotification(sync::TimedThreadNotification& notification,
+ chrono::SystemClock::duration timeout) {
+ return notification.try_acquire_for(timeout);
+}
+
+inline bool AcquireNotification(sync::TimedThreadNotification& notification,
+ chrono::SystemClock::time_point timeout) {
+ return notification.try_acquire_until(timeout);
+}
+
+template <auto kRpcMethod,
+ typename Response = typename MethodInfo<kRpcMethod>::Response,
+ typename DoCall,
+ typename... TimeoutArg>
+SynchronousCallResult<Response> StructSynchronousCall(
+ DoCall&& do_call, TimeoutArg... timeout_arg) {
+ static_assert(MethodInfo<kRpcMethod>::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ // If dynamic allocation is enabled, heap-allocate the call_state.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ auto call_state_ptr = PW_RPC_MAKE_UNIQUE_PTR(SynchronousCallState<Response>);
+ SynchronousCallState<Response>& call_state(*call_state_ptr);
+#else
+ SynchronousCallState<Response> call_state;
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+ auto call = std::forward<DoCall>(do_call)(call_state);
+
+ // Wait for the notification based on the type of the timeout argument.
+ if constexpr (sizeof...(TimeoutArg) == 0) {
+ call_state.notify.acquire(); // Wait forever, since no timeout was given.
+ } else if (!AcquireNotification(call_state.notify, timeout_arg...)) {
+ return SynchronousCallResult<Response>::Timeout();
+ }
+
+ return std::move(call_state.result);
+}
+
+// Template for a raw synchronous call. Used for SynchronousCall,
+// SynchronousCallFor, and SynchronousCallUntil. The type of the timeout
+// argument is used to determine the behavior.
+template <auto kRpcMethod, typename DoCall, typename... TimeoutArg>
+Status RawSynchronousCall(Function<void(ConstByteSpan, Status)>&& on_completed,
+ DoCall&& do_call,
+ TimeoutArg... timeout_arg) {
+ static_assert(MethodInfo<kRpcMethod>::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ RawSynchronousCallState call_state{std::move(on_completed)};
+
+ auto call = std::forward<DoCall>(do_call)(call_state);
+
+ // Wait for the notification based on the type of the timeout argument.
+ if constexpr (sizeof...(TimeoutArg) == 0) {
+ call_state.notify.acquire(); // Wait forever, since no timeout was given.
+ } else if (!AcquireNotification(call_state.notify, timeout_arg...)) {
+ return Status::DeadlineExceeded();
+ }
+
+ return call_state.error;
+}
+
+// Choose which call state object to use (raw or struct).
+template <auto kRpcMethod,
+ typename Response =
+ typename internal::MethodInfo<kRpcMethod>::Response>
+using CallState = std::conditional_t<
+ std::is_same_v<typename MethodInfo<kRpcMethod>::Request, void>,
+ RawSynchronousCallState,
+ SynchronousCallState<Response>>;
+
+// Invokes the RPC method free function using a call_state.
+template <auto kRpcMethod, typename Request>
+constexpr auto CallFreeFunction(Client& client,
+ uint32_t channel_id,
+ const Request& request) {
+ return [&client, channel_id, &request](CallState<kRpcMethod>& call_state) {
+ return kRpcMethod(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+ };
+}
+
+// Invokes the RPC method free function using a call_state and a custom
+// response.
+template <
+ auto kRpcMethod,
+ typename Response = typename internal::MethodInfo<kRpcMethod>::Response,
+ typename Request>
+constexpr auto CallFreeFunctionWithCustomResponse(Client& client,
+ uint32_t channel_id,
+ const Request& request) {
+ return [&client, channel_id, &request](
+ CallState<kRpcMethod, Response>& call_state) {
+ constexpr auto kMemberFunction =
+ MethodInfo<kRpcMethod>::template FunctionTemplate<
+ typename MethodInfo<kRpcMethod>::ServiceClass,
+ Response>();
+ return (*kMemberFunction)(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+ };
+}
+
+// Invokes the RPC function on the generated service client using a call_state.
+template <auto kRpcMethod, typename GeneratedClient, typename Request>
+constexpr auto CallGeneratedClient(const GeneratedClient& client,
+ const Request& request) {
+ return [&client, &request](CallState<kRpcMethod>& call_state) {
+ constexpr auto kMemberFunction =
+ MethodInfo<kRpcMethod>::template Function<GeneratedClient>();
+ return (client.*kMemberFunction)(request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+ };
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/test_method_context.h b/pw_rpc/public/pw_rpc/internal/test_method_context.h
index 2cc49953c..aee85e6af 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method_context.h
+++ b/pw_rpc/public/pw_rpc/internal/test_method_context.h
@@ -135,7 +135,7 @@ class InvocationContext {
std::byte packet[kNoPayloadPacketSizeBytes];
PW_ASSERT(server_
- .ProcessPacket(Packet(PacketType::CLIENT_STREAM_END,
+ .ProcessPacket(Packet(PacketType::CLIENT_REQUEST_COMPLETION,
channel_.id(),
UnwrapServiceId(service_.service_id()),
kMethodId)
diff --git a/pw_rpc/public/pw_rpc/packet_meta.h b/pw_rpc/public/pw_rpc/packet_meta.h
index efa5e5dba..e6dce0269 100644
--- a/pw_rpc/public/pw_rpc/packet_meta.h
+++ b/pw_rpc/public/pw_rpc/packet_meta.h
@@ -16,6 +16,7 @@
#include <cstdint>
#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/internal/packet.pwpb.h"
#include "pw_rpc/method_id.h"
#include "pw_rpc/service_id.h"
#include "pw_span/span.h"
@@ -40,6 +41,12 @@ class PacketMeta {
constexpr bool destination_is_server() const {
return destination_ == internal::Packet::kServer;
}
+ constexpr bool type_is_client_error() const {
+ return type_ == internal::pwpb::PacketType::CLIENT_ERROR;
+ }
+ constexpr bool type_is_server_error() const {
+ return type_ == internal::pwpb::PacketType::SERVER_ERROR;
+ }
// Note: this `payload` is only valid so long as the original `data` buffer
// passed to `PacketMeta::FromBuffer` remains valid.
constexpr ConstByteSpan payload() const { return payload_; }
@@ -50,11 +57,13 @@ class PacketMeta {
service_id_(internal::WrapServiceId(packet.service_id())),
method_id_(internal::WrapMethodId(packet.method_id())),
destination_(packet.destination()),
+ type_(packet.type()),
payload_(packet.payload()) {}
uint32_t channel_id_;
ServiceId service_id_;
MethodId method_id_;
internal::Packet::Destination destination_;
+ internal::pwpb::PacketType type_;
ConstByteSpan payload_;
};
diff --git a/pw_rpc/public/pw_rpc/server.h b/pw_rpc/public/pw_rpc/server.h
index 19c9333c5..2369c46a1 100644
--- a/pw_rpc/public/pw_rpc/server.h
+++ b/pw_rpc/public/pw_rpc/server.h
@@ -171,6 +171,11 @@ class Server : public internal::Endpoint {
const internal::Packet& packet)
PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock());
+ void HandleCompletionRequest(const internal::Packet& packet,
+ internal::Channel& channel,
+ IntrusiveList<internal::Call>::iterator call)
+ const PW_UNLOCK_FUNCTION(internal::rpc_lock());
+
void HandleClientStreamPacket(const internal::Packet& packet,
internal::Channel& channel,
IntrusiveList<internal::Call>::iterator call)
diff --git a/pw_rpc/public/pw_rpc/synchronous_call.h b/pw_rpc/public/pw_rpc/synchronous_call.h
index 5dc58b1c8..ec02cac95 100644
--- a/pw_rpc/public/pw_rpc/synchronous_call.h
+++ b/pw_rpc/public/pw_rpc/synchronous_call.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -18,145 +18,171 @@
#include "pw_chrono/system_clock.h"
#include "pw_rpc/client.h"
#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/internal/synchronous_call_impl.h"
#include "pw_rpc/synchronous_call_result.h"
-#include "pw_sync/timed_thread_notification.h"
-
-// Synchronous Call wrappers
-//
-// Wraps an asynchronous RPC client call, converting it to a synchronous
-// interface.
-//
-// WARNING! This should not be called from any context that cannot be blocked!
-// This method will block the calling thread until the RPC completes, and
-// translate the response into a pw::rpc::SynchronousCallResult that contains
-// the error type and status or the proto response.
-//
-// Example:
-//
-// pw_rpc_EchoMessage request{.msg = "hello" };
-// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
-// pw::rpc::SynchronousCall<EchoService::Echo>(rpc_client,
-// channel_id,
-// request);
-// if (result.ok()) {
-// printf("%s", result.response().msg);
-// }
-//
-// Note: The above example will block indefinitely. If you'd like to include a
-// timeout for how long the call should block for, use the
-// `SynchronousCallFor()` or `SynchronousCallUntil()` variants.
-//
-// Additionally, the use of a generated Client object is supported:
-//
-// pw_rpc::nanopb::EchoService::client client;
-// pw_rpc_EchoMessage request{.msg = "hello" };
-// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
-// pw::rpc::SynchronousCall<EchoService::Echo>(client, request);
-//
-// if (result.ok()) {
-// printf("%s", result.response().msg);
-// }
+/// @file pw_rpc/synchronous_call.h
+///
+/// `pw_rpc` provides wrappers that convert the asynchronous client API to a
+/// synchronous API. The `SynchronousCall<RpcMethod>` functions wrap the
+/// asynchronous client RPC call with a timed thread notification and returns
+/// once a result is known or a timeout has occurred. Only unary methods are
+/// supported.
+///
+/// The Nanopb and pwpb APIs return a `SynchronousCallResult<Response>` object,
+/// which can be queried to determine whether any error scenarios occurred and,
+/// if not, access the response. The raw API executes a function when the call
+/// completes or returns a `pw::Status` if it does not.
+///
+/// `SynchronousCall<RpcMethod>` blocks indefinitely, whereas
+/// `SynchronousCallFor<RpcMethod>` and `SynchronousCallUntil<RpcMethod>` block
+/// for a given timeout or until a deadline, respectively. All wrappers work
+/// with either the standalone static RPC functions or the generated service
+/// client member methods.
+///
+/// @note Use of the SynchronousCall wrappers requires a
+/// @cpp_class{pw::sync::TimedThreadNotification} backend.
+///
+/// The following examples use the Nanopb API to make a call that blocks
+/// indefinitely. If you'd like to include a timeout for how long the call
+/// should block for, use the `SynchronousCallFor()` or `SynchronousCallUntil()`
+/// variants.
+///
+/// @code{.cpp}
+/// pw_rpc_EchoMessage request{.msg = "hello" };
+/// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
+/// pw::rpc::SynchronousCall<EchoService::Echo>(rpc_client,
+/// channel_id,
+/// request);
+/// if (result.ok()) {
+/// PW_LOG_INFO("%s", result.response().msg);
+/// }
+/// @endcode
+///
+/// Additionally, the use of a generated `Client` object is supported:
+///
+/// @code{.cpp}
+/// pw_rpc::nanopb::EchoService::Client client(rpc_client, channel_id);
+/// pw_rpc_EchoMessage request{.msg = "hello" };
+/// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
+/// pw::rpc::SynchronousCall<EchoService::Echo>(client, request);
+///
+/// if (result.ok()) {
+/// PW_LOG_INFO("%s", result.response().msg);
+/// }
+/// @endcode
+///
+/// `SynchronousCall<RpcMethod>` also supports using an optional custom response
+/// message class, `SynchronousCall<RpcMethod, Response>`. This enables the use
+/// of response messages with variable-length fields.
+///
+/// @code{.cpp}
+/// pw_rpc_MyMethodRequestMessage request{};
+/// class CustomResponse : public pw_rpc_MyMethodResponseMessage {
+/// public:
+/// CustomResponse() {
+/// repeated_field.SetDecoder([this](
+/// MyMethodResponse::StreamDecoder& decoder) {
+/// return decoder.ReadRepeatedField(values);
+/// }
+/// }
+/// pw::Vector<uint32_t, 4> values();
+/// };
+/// pw::rpc::SynchronousCallResult<CustomResponse> result =
+/// pw::rpc::SynchronousCall<EchoService::Echo, CustomResponse>(rpc_client,
+/// channel_id,
+/// request);
+/// if (result.ok()) {
+/// PW_LOG_INFO("%d", result.response().values[0]);
+/// }
+/// };
+/// @endcode
+///
+/// The raw API works similarly to the Nanopb API, but takes a
+/// @cpp_type{pw::Function} and returns a @cpp_class{pw::Status}. If the RPC
+/// completes, the @cpp_type{pw::Function} is called with the response and
+/// returned status, and the `SynchronousCall` invocation returns
+/// @pw_status{OK}. If the RPC fails, `SynchronousCall` returns an error.
+///
+/// @code{.cpp}
+/// pw::Status rpc_status = pw::rpc::SynchronousCall<EchoService::Echo>(
+/// rpc_client, channel_id, encoded_request,
+/// [](pw::ConstByteSpan reply, pw::Status status) {
+/// PW_LOG_INFO("Received %zu bytes with status %s",
+/// reply.size(),
+/// status.str());
+/// });
+/// @endcode
+///
+/// @warning These wrappers should not be used from any context that cannot be
+/// blocked! This method will block the calling thread until the RPC completes,
+/// and translate the response into a `pw::rpc::SynchronousCallResult` that
+/// contains the error type and status or the proto response.
namespace pw::rpc {
-namespace internal {
-
-template <typename Response>
-struct SynchronousCallState {
- auto OnCompletedCallback() {
- return [this](const Response& response, Status status) {
- result = SynchronousCallResult<Response>(status, response);
- notify.release();
- };
- }
- auto OnRpcErrorCallback() {
- return [this](Status status) {
- result = SynchronousCallResult<Response>::RpcError(status);
- notify.release();
- };
- }
-
- SynchronousCallResult<Response> result;
- sync::TimedThreadNotification notify;
-};
-
-} // namespace internal
-
-// SynchronousCall
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The pw::rpc::Client to use for the call
-// channel_id: The ID of the RPC channel to make the call on
-// request: The proto struct to send as the request
-template <auto kRpcMethod>
-SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
-SynchronousCall(
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks indefinitely
+/// until a response is received.
+///
+/// @param client The `pw::rpc::Client` to use for the call
+/// @param channel_id The ID of the RPC channel to make the call on
+/// @param request The proto struct to send as the request
+template <
+ auto kRpcMethod,
+ typename Response = typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCallResult<Response> SynchronousCall(
Client& client,
uint32_t channel_id,
const typename internal::MethodInfo<kRpcMethod>::Request& request) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = kRpcMethod(client,
- channel_id,
- request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
-
- call_state.notify.acquire();
-
- return std::move(call_state.result);
+ return internal::StructSynchronousCall<kRpcMethod, Response>(
+ internal::CallFreeFunctionWithCustomResponse<kRpcMethod, Response>(
+ client, channel_id, request));
}
-// SynchronousCall
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The service Client to use for the call
-// request: The proto struct to send as the request
-template <auto kRpcMethod>
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks indefinitely
+/// until a response is received.
+///
+/// @param client The generated service client to use for the call
+/// @param request The proto struct to send as the request
+template <auto kRpcMethod, typename GeneratedClient>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCall(
- const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const GeneratedClient& client,
const typename internal::MethodInfo<kRpcMethod>::Request& request) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- constexpr auto Function =
- Info::template Function<typename Info::GeneratedClient>();
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = (client.*Function)(request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallGeneratedClient<kRpcMethod>(client, request));
+}
- call_state.notify.acquire();
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received.
+template <auto kRpcMethod>
+Status SynchronousCall(Client& client,
+ uint32_t channel_id,
+ ConstByteSpan request,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request));
+}
- return std::move(call_state.result);
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received.
+template <auto kRpcMethod>
+Status SynchronousCall(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ ConstByteSpan request,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallGeneratedClient<kRpcMethod>(client, request));
}
-// SynchronousCallFor
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The pw::rpc::Client to use for the call
-// channel_id: The ID of the RPC channel to make the call on
-// request: The proto struct to send as the request
-// timeout: Duration to block for before returning with Timeout
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided timeout passes.
+///
+/// @param client The `pw::rpc::Client` to use for the call
+/// @param channel_id The ID of the RPC channel to make the call on
+/// @param request The proto struct to send as the request
+/// @param timeout Duration to block for before returning with Timeout
template <auto kRpcMethod>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallFor(
@@ -164,72 +190,63 @@ SynchronousCallFor(
uint32_t channel_id,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::duration timeout) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = kRpcMethod(client,
- channel_id,
- request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
-
- if (!call_state.notify.try_acquire_for(timeout)) {
- return SynchronousCallResult<Response>::Timeout();
- }
-
- return std::move(call_state.result);
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ timeout);
}
-// SynchronousCallFor
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The service Client to use for the call
-// request: The proto struct to send as the request
-// timeout: Duration to block for before returning with Timeout
-template <auto kRpcMethod>
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided timeout passes.
+///
+/// @param client The generated service client to use for the call
+/// @param request The proto struct to send as the request
+/// @param timeout Duration to block for before returning with Timeout
+template <auto kRpcMethod, typename GeneratedClient>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallFor(
- const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const GeneratedClient& client,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::duration timeout) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- constexpr auto Function =
- Info::template Function<typename Info::GeneratedClient>();
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = (client.*Function)(request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallGeneratedClient<kRpcMethod>(client, request), timeout);
+}
- if (!call_state.notify.try_acquire_for(timeout)) {
- return SynchronousCallResult<Response>::Timeout();
- }
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided timeout passes.
+template <auto kRpcMethod>
+Status SynchronousCallFor(
+ Client& client,
+ uint32_t channel_id,
+ ConstByteSpan request,
+ chrono::SystemClock::duration timeout,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ timeout);
+}
- return std::move(call_state.result);
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided timeout passes.
+template <auto kRpcMethod>
+Status SynchronousCallFor(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ ConstByteSpan request,
+ chrono::SystemClock::duration timeout,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallGeneratedClient<kRpcMethod>(client, request),
+ timeout);
}
-// SynchronousCallUntil
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The pw::rpc::Client to use for the call
-// channel_id: The ID of the RPC channel to make the call on
-// request: The proto struct to send as the request
-// deadline: Timepoint to block until before returning with Timeout
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided deadline arrives.
+///
+/// @param client The `pw::rpc::Client` to use for the call
+/// @param channel_id The ID of the RPC channel to make the call on
+/// @param request The proto struct to send as the request
+/// @param deadline Timepoint to block until before returning with Timeout
template <auto kRpcMethod>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallUntil(
@@ -237,59 +254,54 @@ SynchronousCallUntil(
uint32_t channel_id,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::time_point deadline) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = kRpcMethod(client,
- channel_id,
- request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
-
- if (!call_state.notify.try_acquire_until(deadline)) {
- return SynchronousCallResult<Response>::Timeout();
- }
-
- return std::move(call_state.result);
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ deadline);
}
-// SynchronousCallUntil
-//
-// Template arguments:
-// kRpcMethod: The RPC Method to invoke
-//
-// Arguments:
-// client: The service Client to use for the call
-// request: The proto struct to send as the request
-// deadline: Timepoint to block until before returning with Timeout
+/// Invokes a unary RPC synchronously using Nanopb or pwpb. Blocks until a
+/// response is received or the provided deadline arrives.
+///
+/// @param client The generated service client to use for the call
+/// @param request The proto struct to send as the request
+/// @param deadline Timepoint to block until before returning with Timeout
template <auto kRpcMethod>
SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
SynchronousCallUntil(
const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
const typename internal::MethodInfo<kRpcMethod>::Request& request,
chrono::SystemClock::time_point deadline) {
- using Info = internal::MethodInfo<kRpcMethod>;
- using Response = typename Info::Response;
- static_assert(Info::kType == MethodType::kUnary,
- "Only unary methods can be used with synchronous calls");
-
- constexpr auto Function =
- Info::template Function<typename Info::GeneratedClient>();
-
- internal::SynchronousCallState<Response> call_state;
-
- auto call = (client.*Function)(request,
- call_state.OnCompletedCallback(),
- call_state.OnRpcErrorCallback());
+ return internal::StructSynchronousCall<kRpcMethod>(
+ internal::CallGeneratedClient<kRpcMethod>(client, request), deadline);
+}
- if (!call_state.notify.try_acquire_until(deadline)) {
- return SynchronousCallResult<Response>::Timeout();
- }
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided deadline arrives.
+template <auto kRpcMethod>
+Status SynchronousCallUntil(
+ Client& client,
+ uint32_t channel_id,
+ ConstByteSpan request,
+ chrono::SystemClock::time_point deadline,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallFreeFunction<kRpcMethod>(client, channel_id, request),
+ deadline);
+}
- return std::move(call_state.result);
+/// Invokes a unary RPC synchronously using the raw API. Blocks until a
+/// response is received or the provided deadline arrives.
+template <auto kRpcMethod>
+Status SynchronousCallUntil(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ ConstByteSpan request,
+ chrono::SystemClock::time_point deadline,
+ Function<void(ConstByteSpan, Status)>&& on_completed) {
+ return internal::RawSynchronousCall<kRpcMethod>(
+ std::move(on_completed),
+ internal::CallGeneratedClient<kRpcMethod>(client, request),
+ deadline);
}
+
} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/writer.h b/pw_rpc/public/pw_rpc/writer.h
index e7e7c4155..57b1e63d2 100644
--- a/pw_rpc/public/pw_rpc/writer.h
+++ b/pw_rpc/public/pw_rpc/writer.h
@@ -20,7 +20,7 @@ namespace pw::rpc {
// The Writer class allows writing requests or responses to a streaming RPC.
// ClientWriter, ClientReaderWriter, ServerWriter, and ServerReaderWriter
// classes can be used as a generic Writer.
-class Writer : private internal::Call {
+class Writer final : private internal::Call {
public:
// Writers cannot be created directly. They may only be used as a reference to
// an existing call object.
diff --git a/pw_rpc/pw_rpc_private/fake_server_reader_writer.h b/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
index 89c5d91c3..ef24c126e 100644
--- a/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
+++ b/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
@@ -57,7 +57,8 @@ class FakeServerReaderWriter : private ServerCall {
using Call::active;
using Call::set_on_error;
using Call::set_on_next;
- using ServerCall::set_on_client_stream_end;
+ using ServerCall::set_on_completion_requested;
+ using ServerCall::set_on_completion_requested_if_enabled;
Status Finish(Status status = OkStatus()) {
return CloseAndSendResponse(status);
@@ -68,6 +69,7 @@ class FakeServerReaderWriter : private ServerCall {
// Expose a few additional methods for test use.
ServerCall& as_server_call() { return *this; }
using Call::channel_id_locked;
+ using Call::DebugLog;
using Call::id;
using Call::set_id;
};
@@ -85,6 +87,8 @@ class FakeServerWriter : private FakeServerReaderWriter {
// Common reader/writer functions.
using FakeServerReaderWriter::active;
using FakeServerReaderWriter::Finish;
+ using FakeServerReaderWriter::set_on_completion_requested;
+ using FakeServerReaderWriter::set_on_completion_requested_if_enabled;
using FakeServerReaderWriter::set_on_error;
using FakeServerReaderWriter::Write;
diff --git a/pw_rpc/public/pw_rpc/internal/test_method.h b/pw_rpc/pw_rpc_private/test_method.h
index cf06bb338..1c4ea5b0a 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method.h
+++ b/pw_rpc/pw_rpc_private/test_method.h
@@ -16,6 +16,7 @@
#include <cstdint>
#include <cstring>
+#include "fake_server_reader_writer.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/method_union.h"
@@ -32,19 +33,6 @@ namespace pw::rpc::internal {
// channel ID, request, and payload buffer, and optionally provides a response.
class TestMethod : public Method {
public:
- class FakeServerCall : public ServerCall {
- public:
- constexpr FakeServerCall() = default;
- FakeServerCall(const LockedCallContext& context, MethodType type)
- PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
- : ServerCall(context, CallProperties(type, kServerCall, kRawProto)) {}
-
- FakeServerCall(FakeServerCall&&) = default;
- FakeServerCall& operator=(FakeServerCall&&) = default;
-
- using internal::Call::set_on_error;
- };
-
constexpr TestMethod(uint32_t id, MethodType type = MethodType::kUnary)
: Method(id, GetInvoker(type)),
last_channel_id_(0),
@@ -58,7 +46,7 @@ class TestMethod : public Method {
// Sets a call object into which to move the call object when the RPC is
// invoked. This keeps the RPC active until the provided call object is
// finished or goes out of scope.
- void keep_call_active(FakeServerCall& move_to_call) const {
+ void keep_call_active(test::FakeServerReaderWriter& move_to_call) const {
move_to_call_ = &move_to_call;
}
@@ -72,7 +60,7 @@ class TestMethod : public Method {
test_method.invocations_ += 1;
// Create a call object so it registers / unregisters with the server.
- FakeServerCall fake_call(context.ClaimLocked(), kType);
+ test::FakeServerReaderWriter fake_call(context.ClaimLocked(), kType);
context.server().CleanUpCalls();
@@ -100,7 +88,7 @@ class TestMethod : public Method {
mutable uint32_t last_channel_id_;
mutable Packet last_request_;
mutable size_t invocations_;
- mutable FakeServerCall* move_to_call_;
+ mutable test::FakeServerReaderWriter* move_to_call_;
span<const std::byte> response_;
Status response_status_;
diff --git a/pw_rpc/pwpb/Android.bp b/pw_rpc/pwpb/Android.bp
new file mode 100644
index 000000000..ad4c8bb65
--- /dev/null
+++ b/pw_rpc/pwpb/Android.bp
@@ -0,0 +1,48 @@
+// Copyright 2022 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+filegroup {
+ name: "pw_rpc_pwpb_src_files",
+ srcs: [
+ "server_reader_writer.cc",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_rpc_pwpb_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+cc_defaults {
+ name: "pw_rpc_pwpb_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_rpc_pwpb_include_dirs",
+ ],
+ export_header_lib_headers: [
+ "pw_rpc_pwpb_include_dirs",
+ ],
+ srcs: [
+ ":pw_rpc_pwpb_src_files",
+ ],
+}
diff --git a/pw_rpc/pwpb/BUILD.bazel b/pw_rpc/pwpb/BUILD.bazel
index 786b03cd0..0fd8ff726 100644
--- a/pw_rpc/pwpb/BUILD.bazel
+++ b/pw_rpc/pwpb/BUILD.bazel
@@ -105,6 +105,7 @@ pw_cc_library(
includes = ["public"],
deps = [
":test_method_context",
+ "//pw_assert",
"//pw_rpc:client_server_testing",
],
)
@@ -135,7 +136,7 @@ pw_cc_library(
],
)
-# TODO(b/242059613): Enable this library when logging_event_handler can be used.
+# TODO: b/242059613 - Enable this library when logging_event_handler can be used.
filegroup(
name = "client_integration_test",
srcs = [
@@ -182,6 +183,7 @@ pw_cc_test(
":client_api",
":client_server_testing",
"//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ "//pw_sync:mutex",
],
)
@@ -196,8 +198,9 @@ pw_cc_test(
":client_server_testing_threaded",
"//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
"//pw_sync:binary_semaphore",
- "//pw_thread:test_threads_header",
- "//pw_thread_stl:test_threads",
+ "//pw_sync:mutex",
+ "//pw_thread:non_portable_test_thread_options",
+ "//pw_thread_stl:non_portable_test_thread_options",
],
)
@@ -271,14 +274,13 @@ pw_cc_test(
],
)
-# TODO(b/234874064): Requires pwpb options file support to compile.
-filegroup(
+pw_cc_test(
name = "echo_service_test",
srcs = ["echo_service_test.cc"],
- # deps = [
- # ":echo_service",
- # ":test_method_context",
- # ],
+ deps = [
+ ":echo_service",
+ ":test_method_context",
+ ],
)
pw_cc_test(
@@ -326,6 +328,7 @@ pw_cc_test(
":test_method_context",
"//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
"//pw_rpc:synchronous_client_api",
+ "//pw_rpc_transport:test_loopback_service_registry",
"//pw_work_queue",
"//pw_work_queue:stl_test_thread",
"//pw_work_queue:test_thread_header",
diff --git a/pw_rpc/pwpb/BUILD.gn b/pw_rpc/pwpb/BUILD.gn
index 0c611bb99..e036a8019 100644
--- a/pw_rpc/pwpb/BUILD.gn
+++ b/pw_rpc/pwpb/BUILD.gn
@@ -105,6 +105,7 @@ pw_source_set("client_server_testing") {
public = [ "public/pw_rpc/pwpb/client_server_testing.h" ]
public_deps = [
":test_method_context",
+ "$dir_pw_assert",
"..:client_server_testing",
]
}
@@ -132,6 +133,7 @@ pw_source_set("echo_service") {
}
pw_source_set("client_integration_test") {
+ testonly = pw_unit_test_TESTONLY
public_configs = [ ":public" ]
public_deps = [
"$dir_pw_sync:binary_semaphore",
@@ -182,8 +184,10 @@ pw_test("client_reader_writer_test") {
deps = [
":client_api",
":client_testing",
+ "$dir_pw_sync:mutex",
"..:test_protos.pwpb_rpc",
]
+ enable_if = pw_sync_MUTEX_BACKEND != ""
sources = [ "client_reader_writer_test.cc" ]
}
@@ -205,8 +209,9 @@ pw_test("client_server_context_threaded_test") {
":client_api",
":client_server_testing_threaded",
"$dir_pw_sync:binary_semaphore",
- "$dir_pw_thread:test_threads",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_thread:non_portable_test_thread_options",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
"..:test_protos.pwpb_rpc",
]
sources = [ "client_server_context_threaded_test.cc" ]
@@ -318,6 +323,8 @@ pw_test("stub_generation_test") {
pw_test("synchronous_call_test") {
deps = [
":test_method_context",
+ "$dir_pw_rpc_transport:test_loopback_service_registry",
+ "$dir_pw_thread:thread",
"$dir_pw_work_queue:pw_work_queue",
"$dir_pw_work_queue:stl_test_thread",
"$dir_pw_work_queue:test_thread",
diff --git a/pw_rpc/pwpb/CMakeLists.txt b/pw_rpc/pwpb/CMakeLists.txt
index ce75e479c..ba4a3a3f7 100644
--- a/pw_rpc/pwpb/CMakeLists.txt
+++ b/pw_rpc/pwpb/CMakeLists.txt
@@ -162,17 +162,20 @@ pw_add_test(pw_rpc.pwpb.client_reader_writer_test
pw_rpc.pwpb
)
-pw_add_test(pw_rpc.pwpb.client_server_context_test
- SOURCES
- client_server_context_test.cc
- PRIVATE_DEPS
- pw_rpc.pwpb.client_api
- pw_rpc.pwpb.client_server_testing
- pw_rpc.test_protos.pwpb_rpc
- GROUPS
- modules
- pw_rpc.pwpb
-)
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+ pw_add_test(pw_rpc.pwpb.client_server_context_test
+ SOURCES
+ client_server_context_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.client_server_testing
+ pw_rpc.test_protos.pwpb_rpc
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_rpc.pwpb
+ )
+endif()
if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
@@ -185,7 +188,8 @@ if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
pw_rpc.pwpb.client_server_testing_threaded
pw_rpc.test_protos.pwpb_rpc
pw_sync.binary_semaphore
- pw_thread.test_threads
+ pw_sync.mutex
+ pw_thread.non_portable_test_thread_options
pw_thread.thread
pw_thread_stl.test_threads
GROUPS
@@ -331,7 +335,7 @@ pw_add_test(pw_rpc.pwpb.stub_generation_test
pw_rpc.pwpb
)
-# TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
+# TODO: b/231950909 - Test disabled as pw_work_queue lacks CMakeLists.txt
if((TARGET pw_work_queue.pw_work_queue) AND
("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL
@@ -343,6 +347,7 @@ if((TARGET pw_work_queue.pw_work_queue) AND
pw_rpc.pwpb.test_method_context
pw_rpc.synchronous_client_api
pw_rpc.test_protos.pwpb_rpc
+ pw_rpc_transport.test_loopback_service_registry
pw_thread.thread
pw_work_queue.pw_work_queue
pw_work_queue.stl_test_thread
diff --git a/pw_rpc/pwpb/client_reader_writer_test.cc b/pw_rpc/pwpb/client_reader_writer_test.cc
index 540f0106d..f22313d82 100644
--- a/pw_rpc/pwpb/client_reader_writer_test.cc
+++ b/pw_rpc/pwpb/client_reader_writer_test.cc
@@ -62,7 +62,7 @@ TEST(PwpbClientWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
call.set_on_error([](Status) {});
@@ -75,6 +75,7 @@ TEST(PwpbClientReader, DefaultConstructed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
@@ -90,7 +91,74 @@ TEST(PwpbClientReaderWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientWriter, RequestCompletion) {
+ PwpbClientTestContext ctx;
+ PwpbClientWriter<TestRequest::Message, TestStreamResponse::Message> call =
+ TestService::TestClientStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnCompletedCalled<TestStreamResponse::Message>,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReader, RequestCompletion) {
+ PwpbClientTestContext ctx;
+ PwpbClientReader<TestStreamResponse::Message> call =
+ TestService::TestServerStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled<TestStreamResponse::Message>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReaderWriter, RequestCompletion) {
+ PwpbClientTestContext ctx;
+ PwpbClientReaderWriter<TestRequest::Message, TestStreamResponse::Message>
+ call = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled<TestStreamResponse::Message>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
@@ -131,7 +199,7 @@ TEST(PwpbClientWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
call.set_on_error([](Status) {});
@@ -153,6 +221,7 @@ TEST(PwpbClientReader, Closed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
@@ -175,7 +244,7 @@ TEST(PwpbClientReaderWriter, Closed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](const TestStreamResponse::Message&) {});
diff --git a/pw_rpc/pwpb/client_server_context_test.cc b/pw_rpc/pwpb/client_server_context_test.cc
index bd45154ef..184bc3948 100644
--- a/pw_rpc/pwpb/client_server_context_test.cc
+++ b/pw_rpc/pwpb/client_server_context_test.cc
@@ -12,9 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <mutex>
+#include <utility>
+
#include "gtest/gtest.h"
#include "pw_rpc/pwpb/client_server_testing.h"
#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_sync/mutex.h"
namespace pw::rpc {
namespace {
@@ -37,8 +41,16 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const TestRequest::Message&,
- PwpbUnaryResponder<TestResponse::Message>&) {}
+ Status TestAnotherUnaryRpc(const TestRequest::Message& request,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(const TestRequest::Message&,
ServerWriter<TestStreamResponse::Message>&) {}
@@ -54,7 +66,7 @@ class TestService final : public GeneratedService::Service<TestService> {
namespace {
-TEST(PwpbClientServerTestContext, ReceivesUnaryRpcReponse) {
+TEST(PwpbClientServerTestContext, ReceivesUnaryRpcResponse) {
PwpbClientServerTestContext<> ctx;
test::TestService service;
ctx.server().RegisterService(service);
@@ -79,7 +91,7 @@ TEST(PwpbClientServerTestContext, ReceivesUnaryRpcReponse) {
EXPECT_EQ(request.integer, sent_request.integer);
}
-TEST(PwpbClientServerTestContext, ReceivesMultipleReponses) {
+TEST(PwpbClientServerTestContext, ReceivesMultipleResponses) {
PwpbClientServerTestContext<> ctx;
test::TestService service;
ctx.server().RegisterService(service);
@@ -119,5 +131,104 @@ TEST(PwpbClientServerTestContext, ReceivesMultipleReponses) {
EXPECT_EQ(request2.integer, sent_request2.integer);
}
+TEST(PwpbClientServerTestContext,
+ ReceivesMultipleResponsesWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ PwpbClientServerTestContext<> ctx(server_processor, client_processor);
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ TestResponse::Message response1 = {};
+ TestResponse::Message response2 = {};
+ auto handler1 = [&response1](const TestResponse::Message& server_response,
+ pw::Status) { response1 = server_response; };
+ auto handler2 = [&response2](const TestResponse::Message& server_response,
+ pw::Status) { response2 = server_response; };
+
+ TestRequest::Message request1{.integer = 1, .status_code = OkStatus().code()};
+ TestRequest::Message request2{.integer = 2, .status_code = OkStatus().code()};
+ const auto call1 = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request1, handler1);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+ const auto call2 = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request2, handler2);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request1 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_request2 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+ const auto sent_response1 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_response2 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(response1.value, request1.integer + 1);
+ EXPECT_EQ(response2.value, request2.integer + 1);
+ EXPECT_EQ(response1.value, sent_response1.value);
+ EXPECT_EQ(response2.value, sent_response2.value);
+ EXPECT_EQ(request1.integer, sent_request1.integer);
+ EXPECT_EQ(request2.integer, sent_request2.integer);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(PwpbClientServerTestContext, ResponseWithCallbacks) {
+ PwpbClientServerTestContext<> ctx;
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ TestRequest::Message request{};
+ const auto call = test::GeneratedService::TestAnotherUnaryRpc(
+ ctx.client(), ctx.channel().id(), request);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ // To decode a response object that requires to set callbacks, pass it to the
+ // response() method as a parameter.
+ pw::Vector<uint32_t, 4> values{};
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response<test::GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_EQ(42, response.value);
+
+ EXPECT_EQ(3u, values.size());
+ EXPECT_EQ(7u, values[0]);
+ EXPECT_EQ(8u, values[1]);
+ EXPECT_EQ(9u, values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/client_server_context_threaded_test.cc b/pw_rpc/pwpb/client_server_context_threaded_test.cc
index ae1306df3..d9d6f7f65 100644
--- a/pw_rpc/pwpb/client_server_context_threaded_test.cc
+++ b/pw_rpc/pwpb/client_server_context_threaded_test.cc
@@ -12,11 +12,15 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <atomic>
+#include <iostream>
+
#include "gtest/gtest.h"
+#include "pw_function/function.h"
#include "pw_rpc/pwpb/client_server_testing_threaded.h"
#include "pw_rpc_test_protos/test.rpc.pwpb.h"
#include "pw_sync/binary_semaphore.h"
-#include "pw_thread/test_threads.h"
+#include "pw_thread/non_portable_test_thread_options.h"
namespace pw::rpc {
namespace {
@@ -39,8 +43,16 @@ class TestService final : public GeneratedService::Service<TestService> {
return static_cast<Status::Code>(request.status_code);
}
- void TestAnotherUnaryRpc(const TestRequest::Message&,
- PwpbUnaryResponder<TestResponse::Message>&) {}
+ Status TestAnotherUnaryRpc(const TestRequest::Message& request,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return static_cast<Status::Code>(request.status_code);
+ }
static void TestServerStreamRpc(const TestRequest::Message&,
ServerWriter<TestStreamResponse::Message>&) {}
@@ -58,31 +70,43 @@ namespace {
class RpcCaller {
public:
- void BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ template <auto kMethod = test::GeneratedService::TestUnaryRpc>
+ Status BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
TestRequest::Message request{.integer = i,
.status_code = OkStatus().code()};
- auto call = test::GeneratedService::TestUnaryRpc(
+ response_status_ = OkStatus();
+ auto call = kMethod(
client,
channel_id,
request,
- [this](const TestResponse::Message&, Status) { semaphore_.release(); },
- [](Status) {});
+ [this](const TestResponse::Message&, Status status) {
+ response_status_ = status;
+ semaphore_.release();
+ },
+ [this](Status status) {
+ response_status_ = status;
+ semaphore_.release();
+ });
semaphore_.acquire();
+ return response_status_;
}
private:
+ Status response_status_ = OkStatus();
pw::sync::BinarySemaphore semaphore_;
};
-TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
+TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcResponseThreaded) {
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
test::TestService service;
ctx.server().RegisterService(service);
RpcCaller caller;
constexpr auto value = 1;
- caller.BlockOnResponse(value, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request =
ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
@@ -93,7 +117,7 @@ TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
EXPECT_EQ(value + 1, response.value);
}
-TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
+TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleResponsesThreaded) {
PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
test::TestService service;
ctx.server().RegisterService(service);
@@ -101,8 +125,10 @@ TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
RpcCaller caller;
constexpr auto value1 = 1;
constexpr auto value2 = 2;
- caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id());
- caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id());
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
const auto request1 =
ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
@@ -119,5 +145,94 @@ TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
EXPECT_EQ(value2 + 1, response2.value);
}
+TEST(PwpbClientServerTestContextThreaded,
+ ReceivesMultipleResponsesThreadedWithPacketProcessor) {
+ using ProtectedInt = std::pair<int, pw::sync::Mutex>;
+ ProtectedInt server_counter{};
+ auto server_processor = [&server_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ server_counter.second.lock();
+ ++server_counter.first;
+ server_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ ProtectedInt client_counter{};
+ auto client_processor = [&client_counter](
+ ClientServer& client_server,
+ pw::ConstByteSpan packet) -> pw::Status {
+ client_counter.second.lock();
+ ++client_counter.first;
+ client_counter.second.unlock();
+ return client_server.ProcessPacket(packet);
+ };
+
+ PwpbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0(), server_processor, client_processor);
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value1 = 1;
+ constexpr auto value2 = 2;
+ EXPECT_EQ(caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id()),
+ OkStatus());
+ EXPECT_EQ(caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id()),
+ OkStatus());
+
+ const auto request1 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto request2 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+ const auto response1 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto response2 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(value1, request1.integer);
+ EXPECT_EQ(value2, request2.integer);
+ EXPECT_EQ(value1 + 1, response1.value);
+ EXPECT_EQ(value2 + 1, response2.value);
+
+ server_counter.second.lock();
+ EXPECT_EQ(server_counter.first, 2);
+ server_counter.second.unlock();
+ client_counter.second.lock();
+ EXPECT_EQ(client_counter.first, 2);
+ client_counter.second.unlock();
+}
+
+TEST(PwpbClientServerTestContextThreaded, ResponseWithCallbacks) {
+ PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ // DataLoss expected on initial response, since pwpb provides no way to
+ // populate response callback. We setup callbacks on response packet below.
+ EXPECT_EQ(caller.BlockOnResponse<test::GeneratedService::TestAnotherUnaryRpc>(
+ 0, ctx.client(), ctx.channel().id()),
+ Status::DataLoss());
+
+ // To decode a response object that requires to set callbacks, pass it to the
+ // response() method as a parameter.
+ pw::Vector<uint32_t, 4> values{};
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response<test::GeneratedService::TestAnotherUnaryRpc>(0, response);
+
+ EXPECT_EQ(42, response.value);
+
+ EXPECT_EQ(3u, values.size());
+ EXPECT_EQ(7u, values[0]);
+ EXPECT_EQ(8u, values[1]);
+ EXPECT_EQ(9u, values[2]);
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/codegen_test.cc b/pw_rpc/pwpb/codegen_test.cc
index d85760350..cd5b29b19 100644
--- a/pw_rpc/pwpb/codegen_test.cc
+++ b/pw_rpc/pwpb/codegen_test.cc
@@ -258,6 +258,52 @@ TEST(PwpbCodegen, Client_InvokesUnaryRpcWithCallback) {
EXPECT_FALSE(call.active());
}
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+TEST(PwpbCodegen, DynamicClient_InvokesUnaryRpcWithCallback) {
+ constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
+ constexpr uint32_t kMethodId = internal::Hash("TestUnaryRpc");
+
+ ClientContextForTest<128, 99, kServiceId, kMethodId> context;
+
+ test::pw_rpc::pwpb::TestService::DynamicClient test_client(
+ context.client(), context.channel().id());
+
+ struct {
+ Status last_status = Status::Unknown();
+ int response_value = -1;
+ } result;
+
+ auto call = test_client.TestUnaryRpc(
+ {.integer = 123, .status_code = 0},
+ [&result](const test::pwpb::TestResponse::Message& response,
+ Status status) {
+ result.last_status = status;
+ result.response_value = response.value;
+ });
+
+ EXPECT_TRUE(call->active());
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet =
+ static_cast<const internal::test::FakeChannelOutput&>(context.output())
+ .last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kMethodId);
+ PW_DECODE_PB(test::pwpb::TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+
+ PW_ENCODE_PB(test::pwpb::TestResponse, response, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), response));
+ EXPECT_EQ(result.last_status, OkStatus());
+ EXPECT_EQ(result.response_value, 42);
+
+ EXPECT_FALSE(call->active());
+}
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
TEST(PwpbCodegen, Client_InvokesServerStreamingRpcWithCallback) {
constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
constexpr uint32_t kMethodId = internal::Hash("TestServerStreamRpc");
diff --git a/pw_rpc/pwpb/docs.rst b/pw_rpc/pwpb/docs.rst
index a1332a5a7..28dd4e991 100644
--- a/pw_rpc/pwpb/docs.rst
+++ b/pw_rpc/pwpb/docs.rst
@@ -12,25 +12,25 @@ Define a ``pw_proto_library`` containing the .proto file defining your service
(and optionally other related protos), then depend on the ``pwpb_rpc``
version of that library in the code implementing the service.
-.. code::
+.. code-block::
- # chat/BUILD.gn
+ # chat/BUILD.gn
- import("$dir_pw_build/target_types.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
+ import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
- pw_proto_library("chat_protos") {
- sources = [ "chat_protos/chat_service.proto" ]
- }
+ pw_proto_library("chat_protos") {
+ sources = [ "chat_protos/chat_service.proto" ]
+ }
- # Library that implements the Chat service.
- pw_source_set("chat_service") {
- sources = [
- "chat_service.cc",
- "chat_service.h",
- ]
- public_deps = [ ":chat_protos.pwpb_rpc" ]
- }
+ # Library that implements the Chat service.
+ pw_source_set("chat_service") {
+ sources = [
+ "chat_service.cc",
+ "chat_service.h",
+ ]
+ public_deps = [ ":chat_protos.pwpb_rpc" ]
+ }
A C++ header file is generated for each input .proto file, with the ``.proto``
extension replaced by ``.rpc.pwpb.h``. For example, given the input file
@@ -41,7 +41,7 @@ Generated code API
==================
All examples in this document use the following RPC service definition.
-.. code:: protobuf
+.. code-block:: protobuf
// chat/chat_protos/chat_service.proto
@@ -70,7 +70,7 @@ located within a special ``pw_rpc::pwpb`` sub-namespace of the file's package.
The generated class is a base class which must be derived to implement the
service's methods. The base class is templated on the derived class.
-.. code:: c++
+.. code-block:: c++
#include "chat_protos/chat_service.rpc.pwpb.h"
@@ -85,7 +85,7 @@ A unary RPC is implemented as a function which takes in the RPC's request struct
and populates a response struct to send back, with a status indicating whether
the request succeeded.
-.. code:: c++
+.. code-block:: c++
pw::Status GetRoomInformation(const RoomInfoRequest::Message& request,
RoomInfoResponse::Message& response);
@@ -95,7 +95,7 @@ Server streaming RPC
A server streaming RPC receives the client's request message alongside a
``ServerWriter``, used to stream back responses.
-.. code:: c++
+.. code-block:: c++
void ListUsersInRoom(const ListUsersRequest::Message& request,
pw::rpc::ServerWriter<ListUsersResponse::Message>& writer);
@@ -127,6 +127,9 @@ Bidirectional streaming RPC
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. attention:: Supported, but the documentation is still under construction.
+
+.. _module-pw_rpc_pw_protobuf-client:
+
Client-side
-----------
A corresponding client class is generated for every service defined in the proto
@@ -166,6 +169,19 @@ the type of RPC. Each method returns a client call object which stores the
context of the ongoing RPC call. For more information on call objects, refer to
the :ref:`core RPC docs <module-pw_rpc-making-calls>`.
+If dynamic allocation is enabled (:c:macro:`PW_RPC_DYNAMIC_ALLOCATION` is 1), a
+``DynamicClient`` is generated, which dynamically allocates the call object with
+:c:macro:`PW_RPC_MAKE_UNIQUE_PTR`. For example:
+
+.. code-block:: c++
+
+ my_namespace::pw_rpc::pwpb::ServiceName::DynamicClient dynamic_client(
+ client, channel_id);
+ auto call = dynamic_client.TestUnaryRpc(request, response_callback);
+
+ if (call->active()) { // Access the call as a std::unique_ptr
+ // ...
+
.. admonition:: Callback invocation
RPC callbacks are invoked synchronously from ``Client::ProcessPacket``.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
index e66c42092..d47582165 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
@@ -21,8 +21,13 @@
#include "pw_function/function.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/internal/client_call.h"
+#include "pw_rpc/internal/config.h"
#include "pw_rpc/pwpb/internal/common.h"
+#if PW_RPC_DYNAMIC_ALLOCATION
+#include PW_RPC_MAKE_UNIQUE_PTR_INCLUDE
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
namespace pw::rpc {
namespace internal {
@@ -47,17 +52,38 @@ class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
rpc_lock().lock();
CallType call(
client.ClaimLocked(), channel_id, service_id, method_id, serde);
+ SetCallbacksAndSendRequest(call,
+ client,
+ serde,
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
+ return call;
+ }
- call.set_pwpb_on_completed_locked(std::move(on_completed));
- call.set_on_error_locked(std::move(on_error));
-
- if constexpr (sizeof...(Request) == 0u) {
- call.SendInitialClientRequest({});
- } else {
- PwpbSendInitialRequest(call, serde.request(), request...);
- }
-
- client.CleanUpCalls();
+ template <typename CallType, typename... Request>
+ static auto StartDynamic(
+ Endpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&, Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request) PW_LOCKS_EXCLUDED(rpc_lock()) {
+ rpc_lock().lock();
+ auto call = PW_RPC_MAKE_UNIQUE_PTR(CallType,
+ client.ClaimLocked(),
+ channel_id,
+ service_id,
+ method_id,
+ serde);
+ SetCallbacksAndSendRequest(*call,
+ client,
+ serde,
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
return call;
}
@@ -91,6 +117,8 @@ class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
return *this;
}
+ ~PwpbUnaryResponseClientCall() { DestroyClientCall(); }
+
// Implement moving by copying the serde pointer and on_completed function.
void MovePwpbUnaryResponseClientCallFrom(PwpbUnaryResponseClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -123,6 +151,26 @@ class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
}
private:
+ template <typename CallType, typename... Request>
+ static void SetCallbacksAndSendRequest(
+ CallType& call,
+ Endpoint& client,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&, Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request) PW_UNLOCK_FUNCTION(rpc_lock()) {
+ call.set_pwpb_on_completed_locked(std::move(on_completed));
+ call.set_on_error_locked(std::move(on_error));
+
+ if constexpr (sizeof...(Request) == 0u) {
+ call.SendInitialClientRequest({});
+ } else {
+ PwpbSendInitialRequest(call, serde.request(), request...);
+ }
+
+ client.CleanUpCalls();
+ }
+
void set_pwpb_on_completed_locked(
Function<void(const Response& response, Status)>&& on_completed)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -163,17 +211,41 @@ class PwpbStreamResponseClientCall : public StreamResponseClientCall {
rpc_lock().lock();
CallType call(
client.ClaimLocked(), channel_id, service_id, method_id, serde);
+ SetCallbacksAndSendRequest(call,
+ client,
+ serde,
+ std::move(on_next),
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
+ return call;
+ }
- call.set_pwpb_on_next_locked(std::move(on_next));
- call.set_on_completed_locked(std::move(on_completed));
- call.set_on_error_locked(std::move(on_error));
-
- if constexpr (sizeof...(Request) == 0u) {
- call.SendInitialClientRequest({});
- } else {
- PwpbSendInitialRequest(call, serde.request(), request...);
- }
- client.CleanUpCalls();
+ template <typename CallType, typename... Request>
+ static auto StartDynamic(Endpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&)>&& on_next,
+ Function<void(Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ rpc_lock().lock();
+ auto call = PW_RPC_MAKE_UNIQUE_PTR(CallType,
+ client.ClaimLocked(),
+ channel_id,
+ service_id,
+ method_id,
+ serde);
+ SetCallbacksAndSendRequest(*call,
+ client,
+ serde,
+ std::move(on_next),
+ std::move(on_completed),
+ std::move(on_error),
+ request...);
return call;
}
@@ -207,6 +279,8 @@ class PwpbStreamResponseClientCall : public StreamResponseClientCall {
return *this;
}
+ ~PwpbStreamResponseClientCall() { DestroyClientCall(); }
+
// Implement moving by copying the serde pointer and on_next function.
void MovePwpbStreamResponseClientCallFrom(PwpbStreamResponseClientCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -238,6 +312,27 @@ class PwpbStreamResponseClientCall : public StreamResponseClientCall {
}
private:
+ template <typename CallType, typename... Request>
+ static void SetCallbacksAndSendRequest(
+ CallType& call,
+ Endpoint& client,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&)>&& on_next,
+ Function<void(Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request) PW_UNLOCK_FUNCTION(rpc_lock()) {
+ call.set_pwpb_on_next_locked(std::move(on_next));
+ call.set_on_completed_locked(std::move(on_completed));
+ call.set_on_error_locked(std::move(on_error));
+
+ if constexpr (sizeof...(Request) == 0u) {
+ call.SendInitialClientRequest({});
+ } else {
+ PwpbSendInitialRequest(call, serde.request(), request...);
+ }
+ client.CleanUpCalls();
+ }
+
void set_pwpb_on_next_locked(
Function<void(const Response& response)>&& on_next)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -288,16 +383,18 @@ class PwpbClientReaderWriter
request);
}
- // Notifies the server that no further client stream messages will be sent.
- using internal::ClientCall::CloseClientStream;
+ // Notifies the server that the client has requested to stop communication by
+ // sending CLIENT_REQUEST_COMPLETION.
+ using internal::ClientCall::RequestCompletion;
// Cancels this RPC. Closes the call locally and sends a CANCELLED error to
// the server.
using internal::Call::Cancel;
- // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
- // packet. Future packets for this RPC are dropped, and the client sends a
- // FAILED_PRECONDITION error in response because the call is not active.
+ // Closes this RPC locally. Sends a CLIENT_REQUEST_COMPLETION, but no
+ // cancellation packet. Future packets for this RPC are dropped, and the
+ // client sends a FAILED_PRECONDITION error in response because the call is
+ // not active.
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
@@ -343,6 +440,7 @@ class PwpbClientReader
using internal::StreamResponseClientCall::channel_id;
using internal::Call::Cancel;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
@@ -401,7 +499,7 @@ class PwpbClientWriter
}
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
index 394c49f0d..425f0b960 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
@@ -15,6 +15,7 @@
#include <cinttypes>
+#include "pw_assert/assert.h"
#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/pwpb/fake_channel_output.h"
@@ -45,7 +46,11 @@ class PwpbForwardingChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr PwpbForwardingChannelOutput() = default;
+ explicit PwpbForwardingChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index) {
@@ -54,6 +59,17 @@ class PwpbForwardingChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index) {
PW_ASSERT(Base::PacketCount() >= index);
return Base::output_.template requests<kMethod>(channel_id)[index];
@@ -90,7 +106,11 @@ class PwpbClientServerTestContext final
kPayloadsBufferSizeBytes>;
public:
- PwpbClientServerTestContext() = default;
+ PwpbClientServerTestContext(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -99,12 +119,21 @@ class PwpbClientServerTestContext final
index);
}
- // Retrieve copy of resonse indexed by order of occurance
+ // Retrieve copy of response indexed by order of occurance
template <auto kMethod>
Response<kMethod> response(uint32_t index) {
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using pw_protobuf. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
index f37d3ecf0..64808f09f 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
@@ -15,6 +15,7 @@
#include <cinttypes>
+#include "pw_rpc/internal/client_server_testing.h"
#include "pw_rpc/internal/client_server_testing_threaded.h"
#include "pw_rpc/pwpb/fake_channel_output.h"
@@ -45,7 +46,11 @@ class PwpbWatchableChannelOutput final
kPayloadsBufferSizeBytes>;
public:
- constexpr PwpbWatchableChannelOutput() = default;
+ explicit PwpbWatchableChannelOutput(
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
template <auto kMethod>
Response<kMethod> response(uint32_t channel_id, uint32_t index)
@@ -56,6 +61,18 @@ class PwpbWatchableChannelOutput final
}
template <auto kMethod>
+ void response(uint32_t channel_id,
+ uint32_t index,
+ Response<kMethod>& response) PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ auto payloads_view = Base::output_.template responses<kMethod>(channel_id);
+ PW_ASSERT(payloads_view.serde()
+ .Decode(payloads_view.payloads()[index], response)
+ .ok());
+ }
+
+ template <auto kMethod>
Request<kMethod> request(uint32_t channel_id, uint32_t index)
PW_LOCKS_EXCLUDED(Base::mutex_) {
std::lock_guard lock(Base::mutex_);
@@ -94,8 +111,13 @@ class PwpbClientServerTestContextThreaded final
kPayloadsBufferSizeBytes>;
public:
- PwpbClientServerTestContextThreaded(const thread::Options& options)
- : Base(options) {}
+ PwpbClientServerTestContextThreaded(
+ const thread::Options& options,
+ TestPacketProcessor&& server_packet_processor = nullptr,
+ TestPacketProcessor&& client_packet_processor = nullptr)
+ : Base(options,
+ std::move(server_packet_processor),
+ std::move(client_packet_processor)) {}
// Retrieve copy of request indexed by order of occurance
template <auto kMethod>
@@ -110,6 +132,15 @@ class PwpbClientServerTestContextThreaded final
return Base::channel_output_.template response<kMethod>(
Base::channel().id(), index);
}
+
+ // Gives access to the RPC's indexed by order of occurance using passed
+ // Response object to parse using pw_protobuf. Use this version when you need
+ // to set callback fields in the Response object before parsing.
+ template <auto kMethod>
+ void response(uint32_t index, Response<kMethod>& response) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index, response);
+ }
};
} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
index d91ebc6d3..272d3dd97 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
@@ -24,7 +24,7 @@
namespace pw::rpc {
-// TODO(b/234878467): Document the client testing APIs.
+// TODO: b/234878467 - Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server. Accepts
// payloads as pw_protobuf message structs.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
index c164f689a..1dde53455 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
@@ -89,6 +89,9 @@ class PwpbPayloadsView {
iterator begin() const { return iterator(view_.begin(), serde_); }
iterator end() const { return iterator(view_.end(), serde_); }
+ PayloadsView& payloads() { return view_; }
+ PwpbSerde& serde() { return serde_; }
+
private:
template <size_t, size_t>
friend class PwpbFakeChannelOutput;
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
index a0b484d59..a8a7b9903 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
@@ -70,7 +70,7 @@ class PwpbMethod : public Method {
}
// Creates a PwpbMethod for a synchronous unary RPC.
- // TODO(b/234874001): Find a way to reduce the number of monomorphized copies
+ // TODO: b/234874001 - Find a way to reduce the number of monomorphized copies
// of this method.
template <auto kMethod>
static constexpr PwpbMethod SynchronousUnary(uint32_t id,
@@ -97,7 +97,7 @@ class PwpbMethod : public Method {
}
// Creates a PwpbMethod for an asynchronous unary RPC.
- // TODO(b/234874001): Find a way to reduce the number of monomorphized copies
+ // TODO: b/234874001 - Find a way to reduce the number of monomorphized copies
// of this method.
template <auto kMethod>
static constexpr PwpbMethod AsynchronousUnary(uint32_t id,
@@ -365,7 +365,7 @@ class PwpbMethod : public Method {
};
// MethodTraits specialization for a static synchronous unary method.
-// TODO(b/234874320): Further qualify this (and nanopb) definition so that they
+// TODO: b/234874320 - Further qualify this (and nanopb) definition so that they
// can co-exist in the same project.
template <typename Req, typename Res>
struct MethodTraits<PwpbSynchronousUnary<Req, Res>*> {
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
index 92572de84..6b1306592 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
@@ -44,16 +44,16 @@ class PwpbSerde {
// actually encoding it.
template <typename Message>
StatusWithSize EncodedSizeBytes(const Message& message) const {
- // TODO(b/269515470): Use kScratchBufferSizeBytes instead of a fixed size.
+ // TODO: b/269515470 - Use kScratchBufferSizeBytes instead of a fixed size.
std::array<std::byte, 64> scratch_buffer;
stream::CountingNullStream output;
StreamEncoder encoder(output, scratch_buffer);
const Status result = encoder.Write(as_bytes(span(&message, 1)), *table_);
- // TODO(b/269633514): Add 1 to the encoded size because pw_protobuf
+ // TODO: b/269633514 - Add 16 to the encoded size because pw_protobuf
// sometimes fails to encode to buffers that exactly fit the output.
- return StatusWithSize(result, output.bytes_written() + 1);
+ return StatusWithSize(result, output.bytes_written() + 16);
}
// Decodes a serialized protobuf into a pw_protobuf message struct.
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
index 4d80c7176..b8d42d075 100644
--- a/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
@@ -48,6 +48,8 @@ class PwpbServerCall : public ServerCall {
PwpbServerCall(const LockedCallContext& context, MethodType type)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ ~PwpbServerCall() { DestroyServerCall(); }
+
// Sends a unary response.
// Returns the following Status codes:
//
@@ -134,6 +136,8 @@ class BasePwpbServerReader : public PwpbServerCall {
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: PwpbServerCall(context, type) {}
+ ~BasePwpbServerReader() { DestroyServerCall(); }
+
protected:
// Allow default construction so that users can declare a variable into
// which to move server reader/writers from RPC calls.
@@ -228,7 +232,8 @@ class PwpbServerReaderWriter : private internal::BasePwpbServerReader<Request> {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
using internal::BasePwpbServerReader<Request>::set_on_next;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Writes a response. Returns the following Status codes:
//
@@ -303,7 +308,8 @@ class PwpbServerReader : private internal::BasePwpbServerReader<Request> {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
using internal::BasePwpbServerReader<Request>::set_on_next;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Sends the response. Returns the following Status codes:
//
@@ -370,7 +376,8 @@ class PwpbServerWriter : private internal::PwpbServerCall {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Writes a response. Returns the following Status codes:
//
@@ -440,7 +447,6 @@ class PwpbUnaryResponder : private internal::PwpbServerCall {
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
- using internal::ServerCall::set_on_client_stream_end;
// Sends the response. Returns the following Status codes:
//
diff --git a/pw_rpc/pwpb/synchronous_call_test.cc b/pw_rpc/pwpb/synchronous_call_test.cc
index 33a639658..b5f18e658 100644
--- a/pw_rpc/pwpb/synchronous_call_test.cc
+++ b/pw_rpc/pwpb/synchronous_call_test.cc
@@ -22,6 +22,7 @@
#include "pw_rpc/internal/packet.h"
#include "pw_rpc/pwpb/fake_channel_output.h"
#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_rpc_transport/test_loopback_service_registry.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_thread/thread.h"
@@ -36,6 +37,32 @@ using MethodInfo = internal::MethodInfo<TestService::TestUnaryRpc>;
namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+class TestServiceImpl final
+ : public pw::rpc::test::pw_rpc::pwpb::TestService::Service<
+ TestServiceImpl> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message& /*request*/,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return OkStatus();
+ }
+ Status TestAnotherUnaryRpc(const TestRequest::Message& /*request*/,
+ TestResponse::Message& /*response*/) {
+ return OkStatus();
+ }
+ void TestServerStreamRpc(const TestRequest::Message&,
+ ServerWriter<TestStreamResponse::Message>&) {}
+ void TestClientStreamRpc(RawServerReader&) {}
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<TestRequest::Message, TestStreamResponse::Message>&) {}
+};
class SynchronousCallTest : public ::testing::Test {
public:
@@ -49,7 +76,11 @@ class SynchronousCallTest : public ::testing::Test {
void TearDown() override {
work_queue_.RequestStop();
+#if PW_THREAD_JOINING_ENABLED
work_thread_.join();
+#else
+ work_thread_.detach();
+#endif // PW_THREAD_JOINING_ENABLED
}
protected:
@@ -172,6 +203,33 @@ TEST_F(SynchronousCallTest, SynchronousCallUntilTimeoutError) {
EXPECT_EQ(result.status(), Status::DeadlineExceeded());
}
+TEST_F(SynchronousCallTest, SynchronousCallCustomResponse) {
+ TestServiceImpl test_service;
+ TestLoopbackServiceRegistry service_registry;
+ service_registry.RegisterService(test_service);
+
+ class CustomResponse : public TestResponse::Message {
+ public:
+ CustomResponse() {
+ repeated_field.SetDecoder([this](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ }
+ pw::Vector<uint32_t, 4> values{};
+ };
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc, CustomResponse>(
+ service_registry.client_server().client(),
+ service_registry.channel_id(),
+ {.integer = 5, .status_code = 0});
+ EXPECT_EQ(result.status(), OkStatus());
+
+ EXPECT_EQ(3u, result.response().values.size());
+ EXPECT_EQ(7u, result.response().values[0]);
+ EXPECT_EQ(8u, result.response().values[1]);
+ EXPECT_EQ(9u, result.response().values[2]);
+}
+
TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
TestRequest::Message request{.integer = 5, .status_code = 0};
TestResponse::Message response{.value = 42, .repeated_field{}};
@@ -184,6 +242,23 @@ TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
EXPECT_EQ(result.response().value, 42);
}
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+TEST_F(SynchronousCallTest, GeneratedDynamicClientSynchronousCallSuccess) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+ TestResponse::Message response{.value = 42, .repeated_field{}};
+
+ set_response(response, OkStatus());
+
+ TestService::DynamicClient dynamic_client(client(), channel().id());
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(dynamic_client, request);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallServerError) {
TestRequest::Message request{.integer = 5, .status_code = 0};
TestResponse::Message response{.value = 42, .repeated_field{}};
diff --git a/pw_rpc/py/Android.bp b/pw_rpc/py/Android.bp
index 10b3aedbe..ada8d3796 100644
--- a/pw_rpc/py/Android.bp
+++ b/pw_rpc/py/Android.bp
@@ -44,4 +44,19 @@ python_binary_host {
"pw_rpc_internal_packet_py_lib",
"pw_status_py_lib",
],
+}
+
+python_binary_host {
+ name: "pw_rpc_plugin_pwpb_py",
+ main: "pw_rpc/plugin_pwpb.py",
+ srcs: [
+ "pw_rpc/**/*.py",
+ ],
+ libs: [
+ "libprotobuf-python",
+ "pw_protobuf_compiler_py_lib",
+ "pw_protobuf_plugin_py_lib",
+ "pw_rpc_internal_packet_py_lib",
+ "pw_status_py_lib",
+ ],
} \ No newline at end of file
diff --git a/pw_rpc/py/BUILD.bazel b/pw_rpc/py/BUILD.bazel
index 64c9c8b29..ed78e3065 100644
--- a/pw_rpc/py/BUILD.bazel
+++ b/pw_rpc/py/BUILD.bazel
@@ -107,6 +107,12 @@ py_test(
srcs = [
"tests/callback_client_test.py",
],
+ data = [
+ "@com_google_protobuf//:protoc",
+ ],
+ env = {
+ "PROTOC": "$(location @com_google_protobuf//:protoc)",
+ },
deps = [
":pw_rpc",
"//pw_protobuf_compiler:pw_protobuf_compiler_protos",
@@ -121,6 +127,12 @@ py_test(
srcs = [
"tests/client_test.py",
],
+ data = [
+ "@com_google_protobuf//:protoc",
+ ],
+ env = {
+ "PROTOC": "$(location @com_google_protobuf//:protoc)",
+ },
deps = [
":pw_rpc",
"//pw_rpc:internal_packet_proto_pb2",
@@ -134,6 +146,12 @@ py_test(
srcs = [
"tests/descriptors_test.py",
],
+ data = [
+ "@com_google_protobuf//:protoc",
+ ],
+ env = {
+ "PROTOC": "$(location @com_google_protobuf//:protoc)",
+ },
deps = [
":pw_rpc",
"//pw_protobuf_compiler:pw_protobuf_compiler_protos",
diff --git a/pw_rpc/py/BUILD.gn b/pw_rpc/py/BUILD.gn
index 2c091a70f..16ddc6f4a 100644
--- a/pw_rpc/py/BUILD.gn
+++ b/pw_rpc/py/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action_test.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_rpc/internal/integration_test_ports.gni")
@@ -25,7 +26,10 @@ pw_python_package("py") {
version = "0.0.1"
}
options = {
- install_requires = [ "protobuf" ]
+ install_requires = [
+ "protobuf",
+ "pyserial",
+ ]
}
}
@@ -84,7 +88,7 @@ pw_doc_group("docs") {
other_deps = [ ":py" ]
}
-pw_python_script("python_client_cpp_server_test") {
+pw_python_action_test("python_client_cpp_server_test") {
sources = [ "tests/python_client_cpp_server_test.py" ]
python_deps = [
":py",
@@ -94,17 +98,11 @@ pw_python_script("python_client_cpp_server_test") {
"$dir_pw_status/py",
"$dir_pw_tokenizer/py:test_proto.python",
]
-
- pylintrc = "$dir_pigweed/.pylintrc"
- mypy_ini = "$dir_pigweed/.mypy.ini"
-
- action = {
- args = [
- "--port=$pw_rpc_PYTHON_CLIENT_CPP_SERVER_TEST_PORT",
- "--test-server-command",
- "<TARGET_FILE(..:test_rpc_server)>",
- ]
- deps = [ "..:test_rpc_server" ]
- stamp = true
- }
+ args = [
+ "--port=$pw_rpc_PYTHON_CLIENT_CPP_SERVER_TEST_PORT",
+ "--test-server-command",
+ "<TARGET_FILE(..:test_rpc_server)>",
+ ]
+ deps = [ "..:test_rpc_server" ]
+ tags = [ "integration" ]
}
diff --git a/pw_rpc/py/pw_rpc/callback_client/call.py b/pw_rpc/py/pw_rpc/callback_client/call.py
index 6cb09aaef..581e4baf0 100644
--- a/pw_rpc/py/pw_rpc/callback_client/call.py
+++ b/pw_rpc/py/pw_rpc/callback_client/call.py
@@ -141,6 +141,10 @@ class Call:
_LOG.warning('%s terminated due to an error: %s', self._rpc, error)
@property
+ def call_id(self) -> int:
+ return self._rpc.call_id
+
+ @property
def method(self) -> Method:
return self._rpc.method
@@ -318,6 +322,11 @@ class ServerStreamingCall(Call):
) -> Iterator:
return self._get_responses(count=count, timeout_s=timeout_s)
+ def request_completion(self) -> None:
+ """Sends client completion packet to server."""
+ if not self.completed():
+ self._rpcs.send_client_stream_end(self._rpc)
+
def __iter__(self) -> Iterator:
return self.get_responses()
diff --git a/pw_rpc/py/pw_rpc/callback_client/impl.py b/pw_rpc/py/pw_rpc/callback_client/impl.py
index 474757373..df9129861 100644
--- a/pw_rpc/py/pw_rpc/callback_client/impl.py
+++ b/pw_rpc/py/pw_rpc/callback_client/impl.py
@@ -18,6 +18,7 @@ import logging
import textwrap
from typing import Any, Callable, Dict, Iterable, Optional, Type
+from dataclasses import dataclass
from pw_status import Status
from google.protobuf.message import Message
@@ -44,6 +45,15 @@ from pw_rpc.callback_client.call import (
_LOG = logging.getLogger(__package__)
+@dataclass(eq=True, frozen=True)
+class CallInfo:
+ method: Method
+
+ @property
+ def service(self) -> Service:
+ return self.method.service
+
+
class _MethodClient:
"""A method that can be invoked for a particular channel."""
@@ -57,20 +67,21 @@ class _MethodClient:
) -> None:
self._impl = client_impl
self._rpcs = rpcs
- self._rpc = PendingRpc(channel, method.service, method)
+ self._channel = channel
+ self._method = method
self.default_timeout_s: Optional[float] = default_timeout_s
@property
def channel(self) -> Channel:
- return self._rpc.channel
+ return self._channel
@property
def method(self) -> Method:
- return self._rpc.method
+ return self._method
@property
def service(self) -> Service:
- return self._rpc.service
+ return self._method.service
@property
def request(self) -> type:
@@ -118,8 +129,17 @@ class _MethodClient:
if timeout_s is UseDefault.VALUE:
timeout_s = self.default_timeout_s
+ if self._impl.on_call_hook:
+ self._impl.on_call_hook(CallInfo(self._method))
+
+ rpc = PendingRpc(
+ self._channel,
+ self.service,
+ self.method,
+ self._rpcs.allocate_call_id(),
+ )
call = call_type(
- self._rpcs, self._rpc, timeout_s, on_next, on_completed, on_error
+ self._rpcs, rpc, timeout_s, on_next, on_completed, on_error
)
call._invoke(request, ignore_errors) # pylint: disable=protected-access
return call
@@ -378,18 +398,24 @@ asynchronously using the invoke method.
class Impl(client.ClientImpl):
- """Callback-based ClientImpl, for use with pw_rpc.Client."""
+ """Callback-based ClientImpl, for use with pw_rpc.Client.
+
+ Args:
+ on_call_hook: A callable object to handle RPC method calls.
+ If hook is set, it will be called before RPC execution.
+ """
def __init__(
self,
default_unary_timeout_s: Optional[float] = None,
default_stream_timeout_s: Optional[float] = None,
+ on_call_hook: Optional[Callable[[CallInfo], Any]] = None,
cancel_duplicate_calls: Optional[bool] = True,
) -> None:
super().__init__()
self._default_unary_timeout_s = default_unary_timeout_s
self._default_stream_timeout_s = default_stream_timeout_s
-
+ self.on_call_hook = on_call_hook
# Temporary workaround for clients that rely on mulitple in-flight
# instances of an RPC on the same channel, which is not supported.
# TODO(hepler): Remove this option when clients have updated.
diff --git a/pw_rpc/py/pw_rpc/client.py b/pw_rpc/py/pw_rpc/client.py
index d8c3390d7..df7413751 100644
--- a/pw_rpc/py/pw_rpc/client.py
+++ b/pw_rpc/py/pw_rpc/client.py
@@ -23,7 +23,6 @@ from typing import (
Dict,
Iterable,
Iterator,
- NamedTuple,
Optional,
)
@@ -36,20 +35,41 @@ from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
_LOG = logging.getLogger(__package__)
+# Calls with ID of `kOpenCallId` were unrequested, and are updated to have the
+# call ID of the first matching request.
+OPEN_CALL_ID: int = (2**32) - 1
+
+_MAX_CALL_ID: int = 1 << 14
+
class Error(Exception):
"""Error from incorrectly using the RPC client classes."""
-class PendingRpc(NamedTuple):
- """Uniquely identifies an RPC call."""
+class PendingRpc(packets.RpcIds):
+ """Uniquely identifies an RPC call.
- channel: Channel
- service: Service
- method: Method
+ Attributes:
+ channel: Channel
+ service: Service
+ method: Method
+ channel_id: int
+ service_id: int
+ method_id: int
+ call_id: int
+ """
- def __str__(self) -> str:
- return f'PendingRpc(channel={self.channel.id}, method={self.method})'
+ def __init__(
+ self,
+ channel: Channel,
+ service: Service,
+ method: Method,
+ call_id: int,
+ ) -> None:
+ super().__init__(channel.id, service.id, method.id, call_id)
+ self.channel = channel
+ self.service = service
+ self.method = method
class _PendingRpcMetadata:
@@ -60,8 +80,14 @@ class _PendingRpcMetadata:
class PendingRpcs:
"""Tracks pending RPCs and encodes outgoing RPC packets."""
- def __init__(self):
+ def __init__(self) -> None:
self._pending: Dict[PendingRpc, _PendingRpcMetadata] = {}
+ self._next_call_id: int = 0
+
+ def allocate_call_id(self) -> int:
+ call_id = self._next_call_id
+ self._next_call_id = (self._next_call_id + 1) % _MAX_CALL_ID
+ return call_id
def request(
self,
@@ -179,6 +205,17 @@ class PendingRpcs:
def get_pending(self, rpc: PendingRpc, status: Optional[Status]):
"""Gets the pending RPC's context. If status is set, clears the RPC."""
+ if rpc.call_id == OPEN_CALL_ID:
+ # Calls with ID `OPEN_CALL_ID` were unrequested, and are updated to
+ # have the call ID of the first matching request.
+ for pending in self._pending:
+ if (
+ pending.channel == rpc.channel
+ and pending.service == rpc.service
+ and pending.method == rpc.method
+ ):
+ rpc = pending
+
if status is None:
return self._pending[rpc].context
@@ -193,7 +230,7 @@ class ClientImpl(abc.ABC):
client.
"""
- def __init__(self):
+ def __init__(self) -> None:
self.client: Optional['Client'] = None
self.rpcs: Optional[PendingRpcs] = None
@@ -611,7 +648,9 @@ class Client:
f'No method ID {method_id} in service {service.name}'
)
- return PendingRpc(channel_client.channel, service, method)
+ return PendingRpc(
+ channel_client.channel, service, method, packet.call_id
+ )
def __repr__(self) -> str:
return (
diff --git a/pw_rpc/py/pw_rpc/codegen.py b/pw_rpc/py/pw_rpc/codegen.py
index dba811bca..03c784ce2 100644
--- a/pw_rpc/py/pw_rpc/codegen.py
+++ b/pw_rpc/py/pw_rpc/codegen.py
@@ -113,7 +113,9 @@ class CodeGenerator(abc.ABC):
"""Generates code for a service method."""
@abc.abstractmethod
- def client_member_function(self, method: ProtoServiceMethod) -> None:
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
"""Generates the client code for the Client member functions."""
@abc.abstractmethod
@@ -149,6 +151,7 @@ def generate_package(
gen.line('#include <type_traits>\n')
include_lines = [
+ '#include "pw_rpc/internal/config.h"',
'#include "pw_rpc/internal/method_info.h"',
'#include "pw_rpc/internal/method_lookup.h"',
'#include "pw_rpc/internal/service_client.h"',
@@ -225,6 +228,14 @@ def _generate_service_and_client(
_generate_client(gen, service)
+ # DynamicClient is only generated for pwpb for now.
+ if gen.name() == 'pwpb':
+ gen.line('#if PW_RPC_DYNAMIC_ALLOCATION')
+ _generate_client(gen, service, dynamic=True)
+ gen.line('#endif // PW_RPC_DYNAMIC_ALLOCATION')
+
+ _generate_client_free_functions(gen, service)
+
gen.line(' private:')
with gen.indent():
@@ -253,17 +264,21 @@ def _check_method_name(method: ProtoServiceMethod) -> None:
)
-def _generate_client(gen: CodeGenerator, service: ProtoService) -> None:
+def _generate_client(
+ gen: CodeGenerator, service: ProtoService, *, dynamic: bool = False
+) -> None:
+ class_name = 'DynamicClient' if dynamic else 'Client'
+
gen.line('// The Client is used to invoke RPCs for this service.')
gen.line(
- f'class Client final : public {RPC_NAMESPACE}::internal::'
+ f'class {class_name} final : public {RPC_NAMESPACE}::internal::'
'ServiceClient {'
)
gen.line(' public:')
with gen.indent():
gen.line(
- f'constexpr Client({RPC_NAMESPACE}::Client& client,'
+ f'constexpr {class_name}({RPC_NAMESPACE}::Client& client,'
' uint32_t channel_id)'
)
gen.line(' : ServiceClient(client, channel_id) {}')
@@ -272,11 +287,15 @@ def _generate_client(gen: CodeGenerator, service: ProtoService) -> None:
for method in service.methods():
gen.line()
- gen.client_member_function(method)
+ gen.client_member_function(method, dynamic=dynamic)
gen.line('};')
gen.line()
+
+def _generate_client_free_functions(
+ gen: CodeGenerator, service: ProtoService
+) -> None:
gen.line(
'// Static functions for invoking RPCs on a pw_rpc server. '
'These functions are '
@@ -324,12 +343,28 @@ def _generate_info(
gen.line('}')
+ if gen.name() in ['pwpb', 'nanopb']:
+ gen.line('template <typename ServiceImpl, typename Response>')
+ gen.line('static constexpr auto FunctionTemplate() {')
+
+ with gen.indent():
+ template_name = method.name() + 'Template<Response>'
+ gen.line(f'return &ServiceImpl::template {template_name};')
+
+ gen.line('}')
+
gen.line(
'using GeneratedClient = '
f'{"::" + namespace if namespace else ""}'
f'::pw_rpc::{gen.name()}::{service.name()}::Client;'
)
+ gen.line(
+ 'using ServiceClass = '
+ f'{"::" + namespace if namespace else ""}'
+ f'::pw_rpc::{gen.name()}::{service.name()};'
+ )
+
gen.method_info_specialization(method)
gen.line('};')
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 35a119418..a1c171dc4 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -52,26 +52,41 @@ def _proto_filename_to_generated_header(proto_file: str) -> str:
return f'{filename}.rpc{PROTO_H_EXTENSION}'
-def _client_call(method: ProtoServiceMethod) -> str:
+def _client_call(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> str:
template_args = []
if method.client_streaming():
template_args.append(method.request_type().nanopb_struct())
- template_args.append(method.response_type().nanopb_struct())
+ if response is None:
+ response = method.response_type().nanopb_struct()
+
+ template_args.append(response)
return f'{client_call_type(method, "Nanopb")}<{", ".join(template_args)}>'
-def _function(method: ProtoServiceMethod) -> str:
- return f'{_client_call(method)} {method.name()}'
+def _function(
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+) -> str:
+ if name is None:
+ name = method.name()
+
+ return f'{_client_call(method, response)} {name}'
-def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
+def _user_args(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> Iterable[str]:
if not method.client_streaming():
yield f'const {method.request_type().nanopb_struct()}& request'
- response = method.response_type().nanopb_struct()
+ if response is None:
+ response = method.response_type().nanopb_struct()
if method.server_streaming():
yield f'::pw::Function<void(const {response}&)>&& on_next = nullptr'
@@ -134,20 +149,28 @@ class NanopbCodeGenerator(CodeGenerator):
self.line(f'{get_id(method)}, // Hash of "{method.name()}"')
self.line(f'{_serde(method)}),')
- def client_member_function(self, method: ProtoServiceMethod) -> None:
- """Outputs client code for a single RPC method."""
+ def _client_member_function(
+ self,
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> None:
+ if response is None:
+ response = method.response_type().nanopb_struct()
+
+ if name is None:
+ name = method.name()
- self.line(f'{_function(method)}(')
- self.indented_list(*_user_args(method), end=') const {')
+ self.line(f'{_function(method, response, name)}(')
+ self.indented_list(*_user_args(method, response), end=') const {')
with self.indent():
- client_call = _client_call(method)
+ client_call = _client_call(method, response)
base = 'Stream' if method.server_streaming() else 'Unary'
self.line(
f'return {RPC_NAMESPACE}::internal::'
- f'Nanopb{base}ResponseClientCall<'
- f'{method.response_type().nanopb_struct()}>::'
- f'Start<{client_call}>('
+ f'Nanopb{base}ResponseClientCall<{response}>::'
+ f'template Start<{client_call}>('
)
service_client = RPC_NAMESPACE + '::internal::ServiceClient'
@@ -172,17 +195,46 @@ class NanopbCodeGenerator(CodeGenerator):
self.line('}')
- def client_static_function(self, method: ProtoServiceMethod) -> None:
- self.line(f'static {_function(method)}(')
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
+ """Outputs client code for a single RPC method."""
+ if dynamic:
+ self.line('// DynamicClient is not implemented for Nanopb')
+ return
+
+ self._client_member_function(method)
+
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().nanopb_struct()}>'
+ )
+ self._client_member_function(
+ method, 'Response', method.name() + 'Template'
+ )
+
+ def _client_static_function(
+ self,
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> None:
+ if response is None:
+ response = method.response_type().nanopb_struct()
+
+ if name is None:
+ name = method.name()
+
+ self.line(f'static {_function(method, response, name)}(')
self.indented_list(
f'{RPC_NAMESPACE}::Client& client',
'uint32_t channel_id',
- *_user_args(method),
+ *_user_args(method, response),
end=') {',
)
with self.indent():
- self.line(f'return Client(client, channel_id).{method.name()}(')
+ self.line(f'return Client(client, channel_id).{name}(')
args = []
@@ -201,6 +253,17 @@ class NanopbCodeGenerator(CodeGenerator):
self.line('}')
+ def client_static_function(self, method: ProtoServiceMethod) -> None:
+ self._client_static_function(method)
+
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().nanopb_struct()}>'
+ )
+ self._client_static_function(
+ method, 'Response', method.name() + 'Template'
+ )
+
def method_info_specialization(self, method: ProtoServiceMethod) -> None:
self.line()
self.line(f'using Request = {method.request_type().nanopb_struct()};')
diff --git a/pw_rpc/py/pw_rpc/codegen_pwpb.py b/pw_rpc/py/pw_rpc/codegen_pwpb.py
index a9bf2383b..abf8bd80f 100644
--- a/pw_rpc/py/pw_rpc/codegen_pwpb.py
+++ b/pw_rpc/py/pw_rpc/codegen_pwpb.py
@@ -14,7 +14,7 @@
"""This module generates the code for pw_protobuf pw_rpc services."""
import os
-from typing import Iterable
+from typing import Iterable, Optional
from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoServiceMethod
@@ -52,26 +52,37 @@ def _serde(method: ProtoServiceMethod) -> str:
)
-def _client_call(method: ProtoServiceMethod) -> str:
+def _client_call(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> str:
template_args = []
if method.client_streaming():
template_args.append(method.request_type().pwpb_struct())
- template_args.append(method.response_type().pwpb_struct())
+ if response is None:
+ response = method.response_type().pwpb_struct()
+
+ template_args.append(response)
return f'{client_call_type(method, "Pwpb")}<{", ".join(template_args)}>'
-def _function(method: ProtoServiceMethod) -> str:
- return f'{_client_call(method)} {method.name()}'
+def _function(
+ method: ProtoServiceMethod,
+ name: Optional[str] = None,
+) -> str:
+ return f'auto {name or method.name()}'
-def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
+def _user_args(
+ method: ProtoServiceMethod, response: Optional[str] = None
+) -> Iterable[str]:
if not method.client_streaming():
yield f'const {method.request_type().pwpb_struct()}& request'
- response = method.response_type().pwpb_struct()
+ if response is None:
+ response = method.response_type().pwpb_struct()
if method.server_streaming():
yield f'::pw::Function<void(const {response}&)>&& on_next = nullptr'
@@ -135,20 +146,31 @@ class PwpbCodeGenerator(CodeGenerator):
self.line(f'{get_id(method)}, // Hash of "{method.name()}"')
self.line(f'{_serde(method)}),')
- def client_member_function(self, method: ProtoServiceMethod) -> None:
- """Outputs client code for a single RPC method."""
+ def _client_member_function(
+ self,
+ method: ProtoServiceMethod,
+ *,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ dynamic: bool,
+ ) -> None:
+ if response is None:
+ response = method.response_type().pwpb_struct()
- self.line(f'{_function(method)}(')
- self.indented_list(*_user_args(method), end=') const {')
+ if name is None:
+ name = method.name()
+
+ self.line(f'{_function(method, name)}(')
+ self.indented_list(*_user_args(method, response), end=') const {')
with self.indent():
- client_call = _client_call(method)
+ client_call = _client_call(method, response)
base = 'Stream' if method.server_streaming() else 'Unary'
self.line(
f'return {RPC_NAMESPACE}::internal::'
- f'Pwpb{base}ResponseClientCall<'
- f'{method.response_type().pwpb_struct()}>::'
- f'Start<{client_call}>('
+ f'Pwpb{base}ResponseClientCall<{response}>::'
+ f'template Start{"Dynamic" if dynamic else ""}'
+ f'<{client_call}>('
)
service_client = RPC_NAMESPACE + '::internal::ServiceClient'
@@ -173,17 +195,49 @@ class PwpbCodeGenerator(CodeGenerator):
self.line('}')
- def client_static_function(self, method: ProtoServiceMethod) -> None:
- self.line(f'static {_function(method)}(')
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
+ """Outputs client code for a single RPC method."""
+ self._client_member_function(method, dynamic=dynamic)
+
+ if dynamic: # Skip custom response overload
+ return
+
+ # Generate functions that allow specifying a custom response struct.
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().pwpb_struct()}>'
+ )
+ self._client_member_function(
+ method,
+ response='Response',
+ name=method.name() + 'Template',
+ dynamic=dynamic,
+ )
+
+ def _client_static_function(
+ self,
+ method: ProtoServiceMethod,
+ response: Optional[str] = None,
+ name: Optional[str] = None,
+ ) -> None:
+ if response is None:
+ response = method.response_type().pwpb_struct()
+
+ if name is None:
+ name = method.name()
+
+ self.line(f'static {_function(method, name)}(')
self.indented_list(
f'{RPC_NAMESPACE}::Client& client',
'uint32_t channel_id',
- *_user_args(method),
+ *_user_args(method, response),
end=') {',
)
with self.indent():
- self.line(f'return Client(client, channel_id).{method.name()}(')
+ self.line(f'return Client(client, channel_id).{name}(')
args = []
@@ -202,6 +256,17 @@ class PwpbCodeGenerator(CodeGenerator):
self.line('}')
+ def client_static_function(self, method: ProtoServiceMethod) -> None:
+ self._client_static_function(method)
+
+ self.line(
+ 'template <typename Response ='
+ + f'{method.response_type().pwpb_struct()}>'
+ )
+ self._client_static_function(
+ method, 'Response', method.name() + 'Template'
+ )
+
def method_info_specialization(self, method: ProtoServiceMethod) -> None:
self.line()
self.line(f'using Request = {method.request_type().pwpb_struct()};')
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 01e03466a..6f5aeb77c 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -93,7 +93,13 @@ class RawCodeGenerator(CodeGenerator):
)
self.line(f' {get_id(method)}), // Hash of "{method.name()}"')
- def client_member_function(self, method: ProtoServiceMethod) -> None:
+ def client_member_function(
+ self, method: ProtoServiceMethod, *, dynamic: bool
+ ) -> None:
+ if dynamic:
+ self.line('// DynamicClient is not implemented for raw RPC')
+ return
+
self.line(f'{_function(method)}(')
self.indented_list(*_user_args(method), end=') const {')
diff --git a/pw_rpc/py/pw_rpc/console_tools/console.py b/pw_rpc/py/pw_rpc/console_tools/console.py
index 51544f6f9..361ed7a4d 100644
--- a/pw_rpc/py/pw_rpc/console_tools/console.py
+++ b/pw_rpc/py/pw_rpc/console_tools/console.py
@@ -176,10 +176,9 @@ class Context:
.. code-block:: python
- context = console_tools.Context(
- clients, default_client, protos, help_header=WELCOME_MESSAGE)
- IPython.terminal.embed.InteractiveShellEmbed().mainloop(
- module=types.SimpleNamespace(**context.variables()))
+ context = console_tools.Context(
+ clients, default_client, protos, help_header=WELCOME_MESSAGE)
+ IPython.start_ipython(argv=[], user_ns=dict(**context.variables()))
"""
def __init__(
diff --git a/pw_rpc/py/pw_rpc/descriptors.py b/pw_rpc/py/pw_rpc/descriptors.py
index f3daf8787..728061e88 100644
--- a/pw_rpc/py/pw_rpc/descriptors.py
+++ b/pw_rpc/py/pw_rpc/descriptors.py
@@ -46,7 +46,7 @@ from pw_rpc import ids
@dataclass(frozen=True)
class Channel:
id: int
- output: Callable[[bytes], Any]
+ output: Optional[Callable[[bytes], Any]]
def __repr__(self) -> str:
return f'Channel({self.id})'
@@ -85,10 +85,14 @@ class ChannelManipulator(abc.ABC):
channels = tuple(Channel(_DEFAULT_CHANNEL, packet_logger))
# Create a RPC client.
- client = HdlcRpcClient(socket.read, protos, channels, stdout)
+ reader = SocketReader(socket)
+ with reader:
+ client = HdlcRpcClient(reader, protos, channels, stdout)
+ with client:
+ # Do something with client
"""
- def __init__(self):
+ def __init__(self) -> None:
self.send_packet: Callable[[bytes], Any] = lambda _: None
@abc.abstractmethod
@@ -388,21 +392,29 @@ class ServiceAccessor(Collection[T]):
if not isinstance(members, dict):
members = {m: m for m in members}
- by_name = {_name(k): v for k, v in members.items()}
+ by_name: Dict[str, Any] = {_name(k): v for k, v in members.items()}
self._by_id = {k.id: v for k, v in members.items()}
+ # Note: a dictionary is used rather than `setattr` in order to
+ # (1) Hint to the type checker that there will be extra fields
+ # (2) Ensure that built-in attributes such as `_by_id`` are not
+ # overwritten.
+ self._attrs: Dict[str, Any] = {}
if as_attrs == 'members':
for name, member in by_name.items():
- setattr(self, name, member)
+ self._attrs[name] = member
elif as_attrs == 'packages':
for package in python_protos.as_packages(
(m.package, _AccessByName(m.name, members[m])) for m in members
).packages:
- setattr(self, str(package), package)
+ self._attrs[str(package)] = package
elif as_attrs:
raise ValueError(f'Unexpected value {as_attrs!r} for as_attrs')
- def __getitem__(self, name_or_id: Union[str, int]):
+ def __getattr__(self, name: str) -> Any:
+ return self._attrs[name]
+
+ def __getitem__(self, name_or_id: Union[str, int]) -> Any:
"""Accesses a service/method by the string name or ID."""
try:
return self._by_id[_id(name_or_id)]
diff --git a/pw_rpc/py/pw_rpc/packets.py b/pw_rpc/py/pw_rpc/packets.py
index ddcc03e74..54d4b5085 100644
--- a/pw_rpc/py/pw_rpc/packets.py
+++ b/pw_rpc/py/pw_rpc/packets.py
@@ -13,6 +13,7 @@
# the License.
"""Functions for working with pw_rpc packets."""
+import dataclasses
from typing import Optional
from google.protobuf import message
@@ -33,43 +34,47 @@ def decode_payload(packet, payload_type):
return payload
-def _ids(rpc: tuple) -> tuple:
- return tuple(item if isinstance(item, int) else item.id for item in rpc)
+@dataclasses.dataclass(eq=True, frozen=True)
+class RpcIds:
+ """Integer IDs that uniquely identify a remote procedure call."""
+ channel_id: int
+ service_id: int
+ method_id: int
+ call_id: int
-def encode_request(rpc: tuple, request: Optional[message.Message]) -> bytes:
- channel, service, method = _ids(rpc)
+
+def encode_request(rpc: RpcIds, request: Optional[message.Message]) -> bytes:
payload = request.SerializeToString() if request is not None else bytes()
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.REQUEST,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
payload=payload,
).SerializeToString()
-def encode_response(rpc: tuple, response: message.Message) -> bytes:
- channel, service, method = _ids(rpc)
-
+def encode_response(rpc: RpcIds, response: message.Message) -> bytes:
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.RESPONSE,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
payload=response.SerializeToString(),
).SerializeToString()
-def encode_client_stream(rpc: tuple, request: message.Message) -> bytes:
- channel, service, method = _ids(rpc)
-
+def encode_client_stream(rpc: RpcIds, request: message.Message) -> bytes:
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.CLIENT_STREAM,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
payload=request.SerializeToString(),
).SerializeToString()
@@ -80,29 +85,29 @@ def encode_client_error(packet: packet_pb2.RpcPacket, status: Status) -> bytes:
channel_id=packet.channel_id,
service_id=packet.service_id,
method_id=packet.method_id,
+ call_id=packet.call_id,
status=status.value,
).SerializeToString()
-def encode_cancel(rpc: tuple) -> bytes:
- channel, service, method = _ids(rpc)
+def encode_cancel(rpc: RpcIds) -> bytes:
return packet_pb2.RpcPacket(
type=packet_pb2.PacketType.CLIENT_ERROR,
status=Status.CANCELLED.value,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
).SerializeToString()
-def encode_client_stream_end(rpc: tuple) -> bytes:
- channel, service, method = _ids(rpc)
-
+def encode_client_stream_end(rpc: RpcIds) -> bytes:
return packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.CLIENT_STREAM_END,
- channel_id=channel,
- service_id=service,
- method_id=method,
+ type=packet_pb2.PacketType.CLIENT_REQUEST_COMPLETION,
+ channel_id=rpc.channel_id,
+ service_id=rpc.service_id,
+ method_id=rpc.method_id,
+ call_id=rpc.call_id,
).SerializeToString()
diff --git a/pw_rpc/py/pw_rpc/testing.py b/pw_rpc/py/pw_rpc/testing.py
index de8ab6223..f8b129df4 100644
--- a/pw_rpc/py/pw_rpc/testing.py
+++ b/pw_rpc/py/pw_rpc/testing.py
@@ -128,7 +128,7 @@ def execute_integration_test(
client_cmdline += [*common_args]
server_process = subprocess.Popen(server_cmdline)
- # TODO(b/234879791): Replace this delay with some sort of IPC.
+ # TODO: b/234879791 - Replace this delay with some sort of IPC.
time.sleep(setup_time_s)
result = subprocess.run(client_cmdline).returncode
diff --git a/pw_rpc/py/tests/callback_client_test.py b/pw_rpc/py/tests/callback_client_test.py
index 406182983..c014e3eee 100755
--- a/pw_rpc/py/tests/callback_client_test.py
+++ b/pw_rpc/py/tests/callback_client_test.py
@@ -21,7 +21,7 @@ from typing import Any, List, Optional, Tuple
from pw_protobuf_compiler import python_protos
from pw_status import Status
-from pw_rpc import callback_client, client, packets
+from pw_rpc import callback_client, client, descriptors, packets
from pw_rpc.internal import packet_pb2
TEST_PROTO_1 = """\
@@ -52,6 +52,8 @@ service PublicService {
}
"""
+CLIENT_CHANNEL_ID: int = 489
+
def _message_bytes(msg) -> bytes:
return msg if isinstance(msg, bytes) else msg.SerializeToString()
@@ -66,10 +68,12 @@ class _CallbackClientImplTestBase(unittest.TestCase):
self._client = client.Client.from_modules(
callback_client.Impl(),
- [client.Channel(1, self._handle_packet)],
+ [client.Channel(CLIENT_CHANNEL_ID, self._handle_packet)],
self._protos.modules(),
)
- self._service = self._client.channel(1).rpcs.pw.test1.PublicService
+ self._service = self._client.channel(
+ CLIENT_CHANNEL_ID
+ ).rpcs.pw.test1.PublicService
self.requests: List[packet_pb2.RpcPacket] = []
self._next_packets: List[Tuple[bytes, Status]] = []
@@ -83,13 +87,14 @@ class _CallbackClientImplTestBase(unittest.TestCase):
def _enqueue_response(
self,
- channel_id: int,
- method=None,
+ channel_id: int = CLIENT_CHANNEL_ID,
+ method: Optional[descriptors.Method] = None,
status: Status = Status.OK,
- payload=b'',
+ payload: bytes = b'',
*,
ids: Optional[Tuple[int, int]] = None,
- process_status=Status.OK,
+ process_status: Status = Status.OK,
+ call_id: int = client.OPEN_CALL_ID,
) -> None:
if method:
assert ids is None
@@ -105,6 +110,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
channel_id=channel_id,
service_id=service_id,
method_id=method_id,
+ call_id=call_id,
status=status.value,
payload=_message_bytes(payload),
).SerializeToString(),
@@ -113,7 +119,12 @@ class _CallbackClientImplTestBase(unittest.TestCase):
)
def _enqueue_server_stream(
- self, channel_id: int, method, response, process_status=Status.OK
+ self,
+ channel_id: int,
+ method,
+ response,
+ process_status=Status.OK,
+ call_id: int = client.OPEN_CALL_ID,
) -> None:
self._next_packets.append(
(
@@ -122,6 +133,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
channel_id=channel_id,
service_id=method.service.id,
method_id=method.id,
+ call_id=call_id,
payload=_message_bytes(response),
).SerializeToString(),
process_status,
@@ -135,6 +147,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
method,
status: Status,
process_status=Status.OK,
+ call_id: int = client.OPEN_CALL_ID,
) -> None:
self._next_packets.append(
(
@@ -145,6 +158,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
if isinstance(service, int)
else service.id,
method_id=method if isinstance(method, int) else method.id,
+ call_id=call_id,
status=status.value,
).SerializeToString(),
process_status,
@@ -181,13 +195,17 @@ class _CallbackClientImplTestBase(unittest.TestCase):
return message
+# Disable docstring requirements for test functions.
+# pylint: disable=missing-function-docstring
+
+
class CallbackClientImplTest(_CallbackClientImplTestBase):
"""Tests the callback_client.Impl client implementation."""
def test_callback_exceptions_suppressed(self) -> None:
stub = self._service.SomeUnary
- self._enqueue_response(1, stub.method)
+ self._enqueue_response(CLIENT_CHANNEL_ID, stub.method)
exception_msg = 'YOU BROKE IT O-]-<'
with self.assertLogs(callback_client.__package__, 'ERROR') as logs:
@@ -198,7 +216,7 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
self.assertIn(exception_msg, ''.join(logs.output))
# Make sure we can still invoke the RPC.
- self._enqueue_response(1, stub.method, Status.UNKNOWN)
+ self._enqueue_response(CLIENT_CHANNEL_ID, stub.method, Status.UNKNOWN)
status, _ = stub()
self.assertIs(status, Status.UNKNOWN)
@@ -210,20 +228,22 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
self._enqueue_response(999, method, process_status=Status.NOT_FOUND)
# Bad service
self._enqueue_response(
- 1, ids=(999, method.id), process_status=Status.OK
+ CLIENT_CHANNEL_ID, ids=(999, method.id), process_status=Status.OK
)
# Bad method
self._enqueue_response(
- 1, ids=(service_id, 999), process_status=Status.OK
+ CLIENT_CHANNEL_ID, ids=(service_id, 999), process_status=Status.OK
)
# For RPC not pending (is Status.OK because the packet is processed)
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
ids=(service_id, self._service.SomeBidiStreaming.method.id),
process_status=Status.OK,
)
- self._enqueue_response(1, method, process_status=Status.OK)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, method, process_status=Status.OK
+ )
status, response = self._service.SomeUnary(magic_number=6)
self.assertIs(Status.OK, status)
@@ -242,12 +262,16 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
process_status=Status.NOT_FOUND,
)
# Bad service
- self._enqueue_error(1, 999, method.id, Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ CLIENT_CHANNEL_ID, 999, method.id, Status.INVALID_ARGUMENT
+ )
# Bad method
- self._enqueue_error(1, service_id, 999, Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ CLIENT_CHANNEL_ID, service_id, 999, Status.INVALID_ARGUMENT
+ )
# For RPC not pending
self._enqueue_error(
- 1,
+ CLIENT_CHANNEL_ID,
service_id,
self._service.SomeBidiStreaming.method.id,
Status.NOT_FOUND,
@@ -261,7 +285,11 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
method = self._service.SomeUnary.method
self._enqueue_response(
- 1, method, Status.OK, b'INVALID DATA!!!', process_status=Status.OK
+ CLIENT_CHANNEL_ID,
+ method,
+ Status.OK,
+ b'INVALID DATA!!!',
+ process_status=Status.OK,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -282,10 +310,10 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
def test_default_timeouts_set_for_all_rpcs(self) -> None:
rpc_client = client.Client.from_modules(
callback_client.Impl(99, 100),
- [client.Channel(1, lambda *a, **b: None)],
+ [client.Channel(CLIENT_CHANNEL_ID, lambda *a, **b: None)],
self._protos.modules(),
)
- rpcs = rpc_client.channel(1).rpcs
+ rpcs = rpc_client.channel(CLIENT_CHANNEL_ID).rpcs
self.assertEqual(
rpcs.pw.test1.PublicService.SomeUnary.default_timeout_s, 99
@@ -326,7 +354,7 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_blocking_call(self) -> None:
for _ in range(3):
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
self.method,
Status.ABORTED,
self.method.response_type(payload='0_o'),
@@ -345,17 +373,19 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_nonblocking_call(self) -> None:
for _ in range(3):
+ callback = mock.Mock()
+ call = self.rpc.invoke(
+ self._request(magic_number=5), callback, callback
+ )
+
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
self.method,
Status.ABORTED,
self.method.response_type(payload='0_o'),
+ call_id=call.call_id,
)
-
- callback = mock.Mock()
- call = self.rpc.invoke(
- self._request(magic_number=5), callback, callback
- )
+ self._process_enqueued_packets()
callback.assert_has_calls(
[
@@ -368,12 +398,50 @@ class UnaryTest(_CallbackClientImplTestBase):
5, self._sent_payload(self.method.request_type).magic_number
)
+ def test_concurrent_nonblocking_calls(self) -> None:
+ # Start several calls to the same method
+ callbacks_and_calls: List[
+ Tuple[mock.Mock, callback_client.call.Call]
+ ] = []
+ for _ in range(3):
+ callback = mock.Mock()
+ call = self.rpc.invoke(self._request(magic_number=5), callback)
+ callbacks_and_calls.append((callback, call))
+
+ # Respond only to the last call
+ last_callback, last_call = callbacks_and_calls.pop()
+ last_payload = self.method.response_type(payload='last payload')
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID,
+ self.method,
+ payload=last_payload,
+ call_id=last_call.call_id,
+ )
+ self._process_enqueued_packets()
+
+ # Assert that only the last caller received a response
+ last_callback.assert_called_once_with(last_call, last_payload)
+ for remaining_callback, _ in callbacks_and_calls:
+ remaining_callback.assert_not_called()
+
+ # Respond to the other callers and check for receipt
+ other_payload = self.method.response_type(payload='other payload')
+ for callback, call in callbacks_and_calls:
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID,
+ self.method,
+ payload=other_payload,
+ call_id=call.call_id,
+ )
+ self._process_enqueued_packets()
+ callback.assert_called_once_with(call, other_payload)
+
def test_open(self) -> None:
self.output_exception = IOError('something went wrong sending!')
for _ in range(3):
self._enqueue_response(
- 1,
+ CLIENT_CHANNEL_ID,
self.method,
Status.ABORTED,
self.method.response_type(payload='0_o'),
@@ -397,7 +465,10 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_blocking_server_error(self) -> None:
for _ in range(3):
self._enqueue_error(
- 1, self.method.service, self.method, Status.NOT_FOUND
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.NOT_FOUND,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -444,19 +515,19 @@ class UnaryTest(_CallbackClientImplTestBase):
with self.assertRaises(callback_client.RpcTimeout):
self._service.SomeUnary()
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
def test_nonblocking_exception_in_callback(self) -> None:
- exception = ValueError('something went wrong!')
+ exception = ValueError('something went wrong! (intentionally)')
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
call = self.rpc.invoke(on_completed=mock.Mock(side_effect=exception))
@@ -476,6 +547,28 @@ class UnaryTest(_CallbackClientImplTestBase):
'(Status.OK, None)',
)
+ def test_on_call_hook(self) -> None:
+ hook_function = mock.Mock()
+
+ self._client = client.Client.from_modules(
+ callback_client.Impl(on_call_hook=hook_function),
+ [client.Channel(CLIENT_CHANNEL_ID, self._handle_packet)],
+ self._protos.modules(),
+ )
+
+ self._service = self._client.channel(
+ CLIENT_CHANNEL_ID
+ ).rpcs.pw.test1.PublicService
+
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
+ self._service.SomeUnary(self.method.request_type(magic_number=6))
+
+ hook_function.assert_called_once()
+ self.assertEqual(
+ hook_function.call_args[0][0].method.full_name,
+ self.method.full_name,
+ )
+
class ServerStreamingTest(_CallbackClientImplTestBase):
"""Tests for server streaming RPCs."""
@@ -490,9 +583,11 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.ABORTED)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.ABORTED
+ )
self.assertEqual(
[rep1, rep2],
@@ -508,9 +603,11 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.ABORTED)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.ABORTED
+ )
callback = mock.Mock()
call = self.rpc.invoke(
@@ -535,9 +632,11 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.ABORTED)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.ABORTED
+ )
callback = mock.Mock()
call = self.rpc.open(
@@ -557,7 +656,7 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_cancel(self) -> None:
resp = self.rpc.method.response_type(payload='!!!')
- self._enqueue_server_stream(1, self.rpc.method, resp)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.rpc.method, resp)
callback = mock.Mock()
call = self.rpc.invoke(self._request(magic_number=3), callback)
@@ -575,8 +674,42 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
self.assertEqual(self.last_request().status, Status.CANCELLED.value)
# Ensure the RPC can be called after being cancelled.
- self._enqueue_server_stream(1, self.method, resp)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, resp)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
+
+ call = self.rpc.invoke(
+ self._request(magic_number=3), callback, callback
+ )
+
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='!!!')),
+ mock.call(call, Status.OK),
+ ]
+ )
+
+ def test_request_completion(self) -> None:
+ resp = self.rpc.method.response_type(payload='!!!')
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.rpc.method, resp)
+
+ callback = mock.Mock()
+ call = self.rpc.invoke(self._request(magic_number=3), callback)
+ callback.assert_called_once_with(
+ call, self.rpc.method.response_type(payload='!!!')
+ )
+
+ callback.reset_mock()
+
+ call.request_completion()
+
+ self.assertEqual(
+ self.last_request().type,
+ packet_pb2.PacketType.CLIENT_REQUEST_COMPLETION,
+ )
+
+ # Ensure the RPC can be called after being completed.
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, resp)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
call = self.rpc.invoke(
self._request(magic_number=3), callback, callback
@@ -605,20 +738,20 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
for _ in call:
pass
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
def test_nonblocking_iterate_over_count(self) -> None:
reply = self.method.response_type(payload='!?')
for _ in range(4):
- self._enqueue_server_stream(1, self.method, reply)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
call = self.rpc.invoke()
@@ -628,8 +761,8 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_iterate_after_completed_doesnt_block(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_server_stream(1, self.method, reply)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
call = self.rpc.invoke()
@@ -655,7 +788,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self.send_responses_after_packets = 3
response = self.method.response_type(payload='yo')
- self._enqueue_response(1, self.method, Status.OK, response)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, response
+ )
results = self.rpc(requests)
self.assertIs(results.status, Status.OK)
@@ -666,7 +801,10 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.NOT_FOUND
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.NOT_FOUND,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -692,7 +830,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
# Enqueue the server response to be sent after the next message.
- self._enqueue_response(1, self.method, Status.OK, payload_1)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, payload_1
+ )
stream.send(magic_number=32)
self.assertIs(
@@ -712,7 +852,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
payload = self.method.response_type(payload='-_-')
for _ in range(3):
- self._enqueue_response(1, self.method, Status.OK, payload)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, payload
+ )
callback = mock.Mock()
call = self.rpc.open(callback, callback, callback)
@@ -745,11 +887,13 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
# Enqueue the server response to be sent after the next message.
- self._enqueue_response(1, self.method, Status.OK, payload_1)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.OK, payload_1
+ )
stream.finish_and_wait()
self.assertIs(
- packet_pb2.PacketType.CLIENT_STREAM_END,
+ packet_pb2.PacketType.CLIENT_REQUEST_COMPLETION,
self.last_request().type,
)
@@ -778,7 +922,10 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
stream = self._service.SomeClientStreaming.invoke()
self._enqueue_error(
- 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.INVALID_ARGUMENT,
)
stream.send(magic_number=2**32 - 1)
@@ -791,9 +938,13 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
stream = self._service.SomeClientStreaming.invoke()
- # Error will be sent in response to the CLIENT_STREAM_END packet.
+ # Error will be sent in response to the CLIENT_REQUEST_COMPLETION
+ # packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.INVALID_ARGUMENT,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -812,7 +963,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_completed(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_response(1, self.method, Status.UNAVAILABLE, reply)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.UNAVAILABLE, reply
+ )
call = self.rpc.invoke()
result = call.finish_and_wait()
@@ -823,7 +976,10 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_error(self) -> None:
self._enqueue_error(
- 1, self.method.service, self.method, Status.UNAVAILABLE
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.UNAVAILABLE,
)
call = self.rpc.invoke()
@@ -836,14 +992,14 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertIs(call.error, Status.UNAVAILABLE)
self.assertIsNone(call.response)
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
class BidirectionalStreamingTest(_CallbackClientImplTestBase):
@@ -862,7 +1018,7 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self.send_responses_after_packets = 3
- self._enqueue_response(1, self.method, Status.NOT_FOUND)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.NOT_FOUND)
results = self.rpc(requests)
self.assertIs(results.status, Status.NOT_FOUND)
@@ -873,7 +1029,10 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
# Send after len(requests) and the client stream end packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.NOT_FOUND
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.NOT_FOUND,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -903,8 +1062,8 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
self.assertEqual([], responses)
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
stream.send(magic_number=66)
self.assertIs(
@@ -916,7 +1075,7 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
self.assertEqual([rep1, rep2], responses)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
stream.send(magic_number=77)
self.assertTrue(stream.completed())
@@ -931,9 +1090,9 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
rep2 = self.method.response_type(payload='?')
for _ in range(3):
- self._enqueue_server_stream(1, self.method, rep1)
- self._enqueue_server_stream(1, self.method, rep2)
- self._enqueue_response(1, self.method, Status.OK)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep2)
+ self._enqueue_response(CLIENT_CHANNEL_ID, self.method, Status.OK)
callback = mock.Mock()
call = self.rpc.open(callback, callback, callback)
@@ -953,7 +1112,7 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking(self, callback) -> None:
"""Tests a bidirectional streaming RPC ended by the server."""
reply = self.method.response_type(payload='This is the payload!')
- self._enqueue_server_stream(1, self.method, reply)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
self._service.SomeBidiStreaming.invoke()
@@ -969,14 +1128,17 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
)
self.assertFalse(stream.completed())
- self._enqueue_server_stream(1, self.method, rep1)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, rep1)
stream.send(magic_number=55)
self.assertFalse(stream.completed())
self.assertEqual([rep1], responses)
self._enqueue_error(
- 1, self.method.service, self.method, Status.OUT_OF_RANGE
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.OUT_OF_RANGE,
)
stream.send(magic_number=99999)
@@ -994,9 +1156,13 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
stream = self._service.SomeBidiStreaming.invoke()
- # Error will be sent in response to the CLIENT_STREAM_END packet.
+ # Error will be sent in response to the CLIENT_REQUEST_COMPLETION
+ # packet.
self._enqueue_error(
- 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.INVALID_ARGUMENT,
)
with self.assertRaises(callback_client.RpcError) as context:
@@ -1015,8 +1181,10 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_completed(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_server_stream(1, self.method, reply)
- self._enqueue_response(1, self.method, Status.UNAVAILABLE)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
+ self._enqueue_response(
+ CLIENT_CHANNEL_ID, self.method, Status.UNAVAILABLE
+ )
call = self.rpc.invoke()
result = call.finish_and_wait()
@@ -1027,9 +1195,12 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_error(self) -> None:
reply = self.method.response_type(payload='!?')
- self._enqueue_server_stream(1, self.method, reply)
+ self._enqueue_server_stream(CLIENT_CHANNEL_ID, self.method, reply)
self._enqueue_error(
- 1, self.method.service, self.method, Status.UNAVAILABLE
+ CLIENT_CHANNEL_ID,
+ self.method.service,
+ self.method,
+ Status.UNAVAILABLE,
)
call = self.rpc.invoke()
@@ -1042,14 +1213,14 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertIs(call.error, Status.UNAVAILABLE)
self.assertEqual(call.responses, [reply])
- def test_nonblocking_duplicate_calls_first_is_cancelled(self) -> None:
+ def test_nonblocking_duplicate_calls_not_cancelled(self) -> None:
first_call = self.rpc.invoke()
self.assertFalse(first_call.completed())
second_call = self.rpc.invoke()
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertFalse(second_call.completed())
+ self.assertIs(first_call.error, None)
+ self.assertIs(second_call.error, None)
def test_stream_response(self) -> None:
proto = self._protos.packages.pw.test1.SomeMessage(magic_number=123)
diff --git a/pw_rpc/py/tests/client_test.py b/pw_rpc/py/tests/client_test.py
index 92a1f8236..28bea9069 100755
--- a/pw_rpc/py/tests/client_test.py
+++ b/pw_rpc/py/tests/client_test.py
@@ -15,7 +15,7 @@
"""Tests creating pw_rpc client."""
import unittest
-from typing import Optional
+from typing import Any, Callable, Optional
from pw_protobuf_compiler import python_protos
from pw_status import Status
@@ -24,6 +24,8 @@ from pw_rpc import callback_client, client, packets
import pw_rpc.ids
from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
+RpcIds = packets.RpcIds
+
TEST_PROTO_1 = """\
syntax = "proto3";
@@ -73,13 +75,30 @@ service Bravo {
}
"""
+SOME_CHANNEL_ID: int = 237
+SOME_SERVICE_ID: int = 193
+SOME_METHOD_ID: int = 769
+SOME_CALL_ID: int = 452
+
+CLIENT_FIRST_CHANNEL_ID: int = 557
+CLIENT_SECOND_CHANNEL_ID: int = 474
+
+
+def create_protos() -> Any:
+ return python_protos.Library.from_strings([TEST_PROTO_1, TEST_PROTO_2])
-def _test_setup(output=None):
- protos = python_protos.Library.from_strings([TEST_PROTO_1, TEST_PROTO_2])
- return protos, client.Client.from_modules(
+
+def create_client(
+ proto_modules: Any,
+ first_channel_output_fn: Optional[Callable[[bytes], Any]] = None,
+) -> client.Client:
+ return client.Client.from_modules(
callback_client.Impl(),
- [client.Channel(1, output), client.Channel(2, lambda _: None)],
- protos.modules(),
+ [
+ client.Channel(CLIENT_FIRST_CHANNEL_ID, first_channel_output_fn),
+ client.Channel(CLIENT_SECOND_CHANNEL_ID, lambda _: None),
+ ],
+ proto_modules,
)
@@ -87,7 +106,10 @@ class ChannelClientTest(unittest.TestCase):
"""Tests the ChannelClient."""
def setUp(self) -> None:
- self._channel_client = _test_setup()[1].channel(1)
+ client_instance = create_client(create_protos().modules())
+ self._channel_client: client.ChannelClient = client_instance.channel(
+ CLIENT_FIRST_CHANNEL_ID
+ )
def test_access_service_client_as_attribute_or_index(self) -> None:
self.assertIs(
@@ -182,7 +204,8 @@ class ClientTest(unittest.TestCase):
def setUp(self) -> None:
self._last_packet_sent_bytes: Optional[bytes] = None
- self._protos, self._client = _test_setup(self._save_packet)
+ self._protos = create_protos()
+ self._client = create_client(self._protos.modules(), self._save_packet)
def _save_packet(self, packet) -> None:
self._last_packet_sent_bytes = packet
@@ -194,11 +217,19 @@ class ClientTest(unittest.TestCase):
return packet
def test_channel(self) -> None:
- self.assertEqual(self._client.channel(1).channel.id, 1)
- self.assertEqual(self._client.channel(2).channel.id, 2)
+ self.assertEqual(
+ self._client.channel(CLIENT_FIRST_CHANNEL_ID).channel.id,
+ CLIENT_FIRST_CHANNEL_ID,
+ )
+ self.assertEqual(
+ self._client.channel(CLIENT_SECOND_CHANNEL_ID).channel.id,
+ CLIENT_SECOND_CHANNEL_ID,
+ )
def test_channel_default_is_first_listed(self) -> None:
- self.assertEqual(self._client.channel().channel.id, 1)
+ self.assertEqual(
+ self._client.channel().channel.id, CLIENT_FIRST_CHANNEL_ID
+ )
def test_channel_invalid(self) -> None:
with self.assertRaises(KeyError):
@@ -259,7 +290,13 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (123, 456, 789), self._protos.packages.pw.test2.Request()
+ RpcIds(
+ SOME_CHANNEL_ID,
+ SOME_SERVICE_ID,
+ SOME_METHOD_ID,
+ SOME_CALL_ID,
+ ),
+ self._protos.packages.pw.test2.Request(),
)
),
Status.NOT_FOUND,
@@ -269,7 +306,13 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, 456, 789), self._protos.packages.pw.test2.Request()
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ SOME_SERVICE_ID,
+ SOME_METHOD_ID,
+ SOME_CALL_ID,
+ ),
+ self._protos.packages.pw.test2.Request(),
)
),
Status.OK,
@@ -279,9 +322,10 @@ class ClientTest(unittest.TestCase):
self._last_packet_sent(),
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=456,
- method_id=789,
+ channel_id=CLIENT_FIRST_CHANNEL_ID,
+ service_id=SOME_SERVICE_ID,
+ method_id=SOME_METHOD_ID,
+ call_id=SOME_CALL_ID,
status=Status.NOT_FOUND.value,
),
)
@@ -292,7 +336,12 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, service.id, 789),
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ service.id,
+ SOME_METHOD_ID,
+ SOME_CALL_ID,
+ ),
self._protos.packages.pw.test2.Request(),
)
),
@@ -303,9 +352,10 @@ class ClientTest(unittest.TestCase):
self._last_packet_sent(),
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
+ channel_id=CLIENT_FIRST_CHANNEL_ID,
service_id=service.id,
- method_id=789,
+ method_id=SOME_METHOD_ID,
+ call_id=SOME_CALL_ID,
status=Status.NOT_FOUND.value,
),
)
@@ -317,7 +367,12 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, service.id, method.id),
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ service.id,
+ method.id,
+ SOME_CALL_ID,
+ ),
self._protos.packages.pw.test2.Request(),
)
),
@@ -328,9 +383,10 @@ class ClientTest(unittest.TestCase):
self._last_packet_sent(),
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
+ channel_id=CLIENT_FIRST_CHANNEL_ID,
service_id=service.id,
method_id=method.id,
+ call_id=SOME_CALL_ID,
status=Status.FAILED_PRECONDITION.value,
),
)
@@ -340,12 +396,17 @@ class ClientTest(unittest.TestCase):
reply = method.response_type(payload='hello')
def response_callback(
- rpc: client.PendingRpc, message, status: Optional[Status]
+ rpc: client.PendingRpc,
+ message,
+ status: Optional[Status],
) -> None:
self.assertEqual(
rpc,
client.PendingRpc(
- self._client.channel(1).channel, method.service, method
+ self._client.channel(CLIENT_FIRST_CHANNEL_ID).channel,
+ method.service,
+ method,
+ call_id=SOME_CALL_ID,
),
)
self.assertEqual(message, reply)
@@ -355,7 +416,15 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
- packets.encode_response((1, method.service, method), reply)
+ packets.encode_response(
+ RpcIds(
+ CLIENT_FIRST_CHANNEL_ID,
+ method.service.id,
+ method.id,
+ SOME_CALL_ID,
+ ),
+ reply,
+ )
),
Status.OK,
)
diff --git a/pw_rpc/py/tests/packets_test.py b/pw_rpc/py/tests/packets_test.py
index d6fa87935..3edded35e 100755
--- a/pw_rpc/py/tests/packets_test.py
+++ b/pw_rpc/py/tests/packets_test.py
@@ -21,12 +21,23 @@ from pw_status import Status
from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
from pw_rpc import packets
+_TEST_IDS = packets.RpcIds(1, 2, 3, 4)
+_TEST_STATUS = 321
_TEST_REQUEST = RpcPacket(
type=PacketType.REQUEST,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString(),
+ channel_id=_TEST_IDS.channel_id,
+ service_id=_TEST_IDS.service_id,
+ method_id=_TEST_IDS.method_id,
+ call_id=_TEST_IDS.call_id,
+ payload=RpcPacket(status=_TEST_STATUS).SerializeToString(),
+)
+_TEST_RESPONSE = RpcPacket(
+ type=PacketType.RESPONSE,
+ channel_id=_TEST_IDS.channel_id,
+ service_id=_TEST_IDS.service_id,
+ method_id=_TEST_IDS.method_id,
+ call_id=_TEST_IDS.call_id,
+ payload=RpcPacket(status=_TEST_STATUS).SerializeToString(),
)
@@ -34,29 +45,23 @@ class PacketsTest(unittest.TestCase):
"""Tests for packet encoding and decoding."""
def test_encode_request(self):
- data = packets.encode_request((1, 2, 3), RpcPacket(status=321))
+ data = packets.encode_request(_TEST_IDS, RpcPacket(status=_TEST_STATUS))
packet = RpcPacket()
packet.ParseFromString(data)
self.assertEqual(_TEST_REQUEST, packet)
def test_encode_response(self):
- response = RpcPacket(
- type=PacketType.RESPONSE,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString(),
+ data = packets.encode_response(
+ _TEST_IDS, RpcPacket(status=_TEST_STATUS)
)
-
- data = packets.encode_response((1, 2, 3), RpcPacket(status=321))
packet = RpcPacket()
packet.ParseFromString(data)
- self.assertEqual(response, packet)
+ self.assertEqual(_TEST_RESPONSE, packet)
def test_encode_cancel(self):
- data = packets.encode_cancel((9, 8, 7))
+ data = packets.encode_cancel(packets.RpcIds(9, 8, 7, 6))
packet = RpcPacket()
packet.ParseFromString(data)
@@ -68,6 +73,7 @@ class PacketsTest(unittest.TestCase):
channel_id=9,
service_id=8,
method_id=7,
+ call_id=6,
status=Status.CANCELLED.value,
),
)
@@ -82,9 +88,10 @@ class PacketsTest(unittest.TestCase):
packet,
RpcPacket(
type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=2,
- method_id=3,
+ channel_id=_TEST_IDS.channel_id,
+ service_id=_TEST_IDS.service_id,
+ method_id=_TEST_IDS.method_id,
+ call_id=_TEST_IDS.call_id,
status=Status.NOT_FOUND.value,
),
)
@@ -96,18 +103,7 @@ class PacketsTest(unittest.TestCase):
def test_for_server(self):
self.assertTrue(packets.for_server(_TEST_REQUEST))
-
- self.assertFalse(
- packets.for_server(
- RpcPacket(
- type=PacketType.RESPONSE,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString(),
- )
- )
- )
+ self.assertFalse(packets.for_server(_TEST_RESPONSE))
if __name__ == '__main__':
diff --git a/pw_rpc/py/tests/python_client_cpp_server_test.py b/pw_rpc/py/tests/python_client_cpp_server_test.py
index ea180b2eb..fad61d5a3 100755
--- a/pw_rpc/py/tests/python_client_cpp_server_test.py
+++ b/pw_rpc/py/tests/python_client_cpp_server_test.py
@@ -14,6 +14,7 @@
# the License.
"""Tests using the callback client for pw_rpc."""
+import contextlib
from typing import List, Tuple
import unittest
@@ -62,28 +63,32 @@ class RpcIntegrationTest(unittest.TestCase):
rpc = self.rpcs.pw.rpc.Benchmark.BidirectionalEcho
for _ in range(ITERATIONS):
- first_call = rpc.invoke()
- first_call.send(payload=b'abc')
- self.assertEqual(
- next(iter(first_call)), rpc.response(payload=b'abc')
- )
- self.assertFalse(first_call.completed())
-
- second_call = rpc.invoke()
- second_call.send(payload=b'123')
- self.assertEqual(
- next(iter(second_call)), rpc.response(payload=b'123')
- )
-
- self.assertIs(first_call.error, Status.CANCELLED)
- self.assertEqual(
- first_call.responses, [rpc.response(payload=b'abc')]
- )
-
- self.assertFalse(second_call.completed())
- self.assertEqual(
- second_call.responses, [rpc.response(payload=b'123')]
- )
+ with contextlib.ExitStack() as stack:
+ first_call = stack.enter_context(rpc.invoke())
+ first_call_responses = first_call.get_responses()
+ first_call.send(payload=b'abc')
+ self.assertEqual(
+ next(first_call_responses), rpc.response(payload=b'abc')
+ )
+ self.assertFalse(first_call.completed())
+
+ second_call = stack.enter_context(rpc.invoke())
+ second_call_responses = second_call.get_responses()
+ second_call.send(payload=b'123')
+ self.assertEqual(
+ next(second_call_responses), rpc.response(payload=b'123')
+ )
+ self.assertFalse(second_call.completed())
+
+ # Check that issuing `second_call` did not cancel `first call`.
+ self.assertFalse(first_call.completed())
+ self.assertIs(first_call.error, None)
+
+ # Send to `first_call` again and check for a response.
+ first_call.send(payload=b'def')
+ self.assertEqual(
+ next(first_call_responses), rpc.response(payload=b'def')
+ )
def _main(
diff --git a/pw_rpc/raw/Android.bp b/pw_rpc/raw/Android.bp
index bcd39d4d1..5ed876706 100644
--- a/pw_rpc/raw/Android.bp
+++ b/pw_rpc/raw/Android.bp
@@ -52,7 +52,7 @@ cc_library_headers {
// name: "pw_rpc_cflags_<instance_name>",
// cflags: [
// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
-// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
// "-DPW_RPC_DYNAMIC_ALLOCATION",
// ],
// }
@@ -70,6 +70,4 @@ cc_defaults {
srcs: [
":pw_rpc_raw_src_files",
],
- host_supported: true,
- vendor_available: true,
}
diff --git a/pw_rpc/raw/BUILD.bazel b/pw_rpc/raw/BUILD.bazel
index ac9c10e8c..7d9033296 100644
--- a/pw_rpc/raw/BUILD.bazel
+++ b/pw_rpc/raw/BUILD.bazel
@@ -195,3 +195,16 @@ pw_cc_test(
"//pw_rpc:pw_rpc_test_cc.raw_rpc",
],
)
+
+pw_cc_test(
+ name = "synchronous_call_test",
+ srcs = ["synchronous_call_test.cc"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:pw_rpc_test_cc.raw_rpc",
+ "//pw_rpc:synchronous_client_api",
+ "//pw_work_queue",
+ "//pw_work_queue:stl_test_thread",
+ "//pw_work_queue:test_thread_header",
+ ],
+)
diff --git a/pw_rpc/raw/BUILD.gn b/pw_rpc/raw/BUILD.gn
index 48b35bf50..ca43d6f1c 100644
--- a/pw_rpc/raw/BUILD.gn
+++ b/pw_rpc/raw/BUILD.gn
@@ -17,6 +17,8 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
import("$dir_pw_unit_test/test.gni")
config("public") {
@@ -85,6 +87,7 @@ pw_test_group("tests") {
":method_union_test",
":server_reader_writer_test",
":stub_generation_test",
+ ":synchronous_call_test",
]
}
@@ -163,6 +166,20 @@ pw_test("stub_generation_test") {
sources = [ "stub_generation_test.cc" ]
}
+pw_test("synchronous_call_test") {
+ deps = [
+ ":test_method_context",
+ "$dir_pw_thread:thread",
+ "$dir_pw_work_queue:pw_work_queue",
+ "$dir_pw_work_queue:stl_test_thread",
+ "$dir_pw_work_queue:test_thread",
+ "..:synchronous_client_api",
+ "..:test_protos.raw_rpc",
+ ]
+ sources = [ "synchronous_call_test.cc" ]
+ enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != ""
+}
+
pw_cc_negative_compilation_test("service_nc_test") {
sources = [ "service_nc_test.cc" ]
deps = [ "..:test_protos.raw_rpc" ]
diff --git a/pw_rpc/raw/client_reader_writer_test.cc b/pw_rpc/raw/client_reader_writer_test.cc
index 8c9553c09..c8dd07aa2 100644
--- a/pw_rpc/raw/client_reader_writer_test.cc
+++ b/pw_rpc/raw/client_reader_writer_test.cc
@@ -50,7 +50,7 @@ TEST(RawClientWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](ConstByteSpan, Status) {});
call.set_on_error([](Status) {});
@@ -63,6 +63,7 @@ TEST(RawClientReader, DefaultConstructed) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -77,7 +78,67 @@ TEST(RawClientReaderWriter, DefaultConstructed) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](ConstByteSpan) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(RawClientWriter, RequestCompletion) {
+ RawClientTestContext ctx;
+ RawClientWriter call = TestService::TestClientStreamRpc(
+ ctx.client(), ctx.channel().id(), FailIfOnCompletedCalled, FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](ConstByteSpan, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(RawClientReader, RequestCompletion) {
+ RawClientTestContext ctx;
+ RawClientReader call = TestService::TestServerStreamRpc(ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](ConstByteSpan) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(RawClientReaderWriter, RequestCompletion) {
+ RawClientTestContext ctx;
+ RawClientReaderWriter call =
+ TestService::TestBidirectionalStreamRpc(ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.RequestCompletion());
+
+ ASSERT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.channel().id());
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
+ EXPECT_EQ(OkStatus(), call.Cancel());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -119,7 +180,7 @@ TEST(RawClientWriter, Cancel) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](ConstByteSpan, Status) {});
call.set_on_error([](Status) {});
@@ -141,6 +202,7 @@ TEST(RawClientReader, Cancel) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -164,7 +226,7 @@ TEST(RawClientReaderWriter, Cancel) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
@@ -201,7 +263,7 @@ TEST(RawClientWriter, Abandon) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
EXPECT_EQ(ctx.output().total_packets(), 2u); // request & client stream end
}
@@ -220,6 +282,7 @@ TEST(RawClientReader, Abandon) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
EXPECT_EQ(ctx.output().total_packets(), 1u); // request only
}
@@ -239,7 +302,7 @@ TEST(RawClientReaderWriter, Abandon) {
EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
- EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+ EXPECT_EQ(Status::FailedPrecondition(), call.RequestCompletion());
EXPECT_EQ(ctx.output().total_packets(), 2u); // request & client stream end
}
@@ -260,7 +323,8 @@ TEST(RawClientReaderWriter, Move_InactiveToActive_EndsClientStream) {
active_call = std::move(inactive_call);
- EXPECT_EQ(ctx.output().total_packets(), 2u); // Sent CLIENT_STREAM_END
+ EXPECT_EQ(ctx.output().total_packets(),
+ 2u); // Sent CLIENT_REQUEST_COMPLETION
EXPECT_EQ(
ctx.output()
.client_stream_end_packets<TestService::TestBidirectionalStreamRpc>(),
@@ -356,7 +420,8 @@ TEST(RawClientWriter, WithClientStream_OutOfScope_SendsClientStreamEnd) {
ASSERT_EQ(ctx.output().total_packets(), 1u); // Sent the request
}
- EXPECT_EQ(ctx.output().total_packets(), 2u); // Sent CLIENT_STREAM_END
+ EXPECT_EQ(ctx.output().total_packets(),
+ 2u); // Sent CLIENT_REQUEST_COMPLETION
EXPECT_EQ(ctx.output()
.client_stream_end_packets<TestService::TestClientStreamRpc>(),
1u);
diff --git a/pw_rpc/raw/client_test.cc b/pw_rpc/raw/client_test.cc
index a7009d649..b78fa2307 100644
--- a/pw_rpc/raw/client_test.cc
+++ b/pw_rpc/raw/client_test.cc
@@ -19,6 +19,7 @@
#include "gtest/gtest.h"
#include "pw_rpc/internal/client_call.h"
#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/raw/client_reader_writer.h"
#include "pw_rpc/raw/client_testing.h"
namespace pw::rpc {
@@ -42,113 +43,103 @@ struct internal::MethodInfo<BidirectionalStreamMethod> {
namespace {
-template <auto kMethod, typename Call, typename Context>
-Call StartCall(Context& context,
- std::optional<uint32_t> channel_id = std::nullopt)
- PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
- internal::rpc_lock().lock();
- Call call(static_cast<internal::Endpoint&>(context.client()).ClaimLocked(),
- channel_id.value_or(context.channel().id()),
- internal::MethodInfo<kMethod>::kServiceId,
- internal::MethodInfo<kMethod>::kMethodId,
- internal::MethodInfo<kMethod>::kType);
- call.SendInitialClientRequest({});
- // As in the real implementations, immediately clean up aborted calls.
- static_cast<internal::Endpoint&>(context.client()).CleanUpCalls();
- return call;
-}
-
-class TestStreamCall : public internal::StreamResponseClientCall {
- public:
- TestStreamCall(internal::LockedEndpoint& client,
- uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id,
- MethodType type)
- PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
- : StreamResponseClientCall(
- client,
- channel_id,
- service_id,
- method_id,
- internal::CallProperties(
- type, internal::kClientCall, internal::kRawProto)),
- payload(nullptr) {
- set_on_next_locked([this](ConstByteSpan string) {
+// Captures payload from on_next and statuses from on_error and on_completed.
+// Payloads are assumed to be null-terminated strings.
+template <typename CallType>
+struct CallContext {
+ auto OnNext() {
+ return [this](ConstByteSpan string) {
payload = reinterpret_cast<const char*>(string.data());
- });
- set_on_completed_locked([this](Status status) { completed = status; });
- set_on_error_locked([this](Status status) { error = status; });
+ };
}
- const char* payload;
- std::optional<Status> completed;
- std::optional<Status> error;
-};
-
-class TestUnaryCall : public internal::UnaryResponseClientCall {
- public:
- TestUnaryCall() = default;
-
- TestUnaryCall(internal::LockedEndpoint& client,
- uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id,
- MethodType type)
- PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
- : UnaryResponseClientCall(
- client,
- channel_id,
- service_id,
- method_id,
- internal::CallProperties(
- type, internal::kClientCall, internal::kRawProto)),
- payload(nullptr) {
- set_on_completed_locked([this](ConstByteSpan string, Status status) {
+ auto UnaryOnCompleted() {
+ return [this](ConstByteSpan string, Status status) {
payload = reinterpret_cast<const char*>(string.data());
completed = status;
- });
- set_on_error_locked([this](Status status) { error = status; });
+ };
+ }
+
+ auto StreamOnCompleted() {
+ return [this](Status status) { completed = status; };
}
- using Call::set_on_error;
- using UnaryResponseClientCall::set_on_completed;
+ auto OnError() {
+ return [this](Status status) { error = status; };
+ }
+
+ CallType call;
const char* payload;
std::optional<Status> completed;
std::optional<Status> error;
};
+template <auto kMethod, typename Context>
+CallContext<RawUnaryReceiver> StartUnaryCall(
+ Context& context, std::optional<uint32_t> channel_id = std::nullopt)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ CallContext<RawUnaryReceiver> call_context;
+ call_context.call =
+ internal::UnaryResponseClientCall::Start<RawUnaryReceiver>(
+ context.client(),
+ channel_id.value_or(context.channel().id()),
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId,
+ call_context.UnaryOnCompleted(),
+ call_context.OnError(),
+ {});
+ return call_context;
+}
+
+template <auto kMethod, typename Context>
+CallContext<RawClientReaderWriter> StartStreamCall(
+ Context& context, std::optional<uint32_t> channel_id = std::nullopt)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ CallContext<RawClientReaderWriter> call_context;
+ call_context.call =
+ internal::StreamResponseClientCall::Start<RawClientReaderWriter>(
+ context.client(),
+ channel_id.value_or(context.channel().id()),
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId,
+ call_context.OnNext(),
+ call_context.StreamOnCompleted(),
+ call_context.OnError(),
+ {});
+ return call_context;
+}
+
TEST(Client, ProcessPacket_InvokesUnaryCallbacks) {
RawClientTestContext context;
- TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(context);
+ CallContext call_context = StartUnaryCall<UnaryMethod>(context);
- ASSERT_NE(call.completed, OkStatus());
+ ASSERT_NE(call_context.completed, OkStatus());
context.server().SendResponse<UnaryMethod>(as_bytes(span("you nary?!?")),
OkStatus());
- ASSERT_NE(call.payload, nullptr);
- EXPECT_STREQ(call.payload, "you nary?!?");
- EXPECT_EQ(call.completed, OkStatus());
- EXPECT_FALSE(call.active());
+ ASSERT_NE(call_context.payload, nullptr);
+ EXPECT_STREQ(call_context.payload, "you nary?!?");
+ EXPECT_EQ(call_context.completed, OkStatus());
+ EXPECT_FALSE(call_context.call.active());
}
TEST(Client, ProcessPacket_NoCallbackSet) {
RawClientTestContext context;
- TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(context);
- call.set_on_completed(nullptr);
+ CallContext call_context = StartUnaryCall<UnaryMethod>(context);
+ call_context.call.set_on_completed(nullptr);
- ASSERT_NE(call.completed, OkStatus());
+ ASSERT_NE(call_context.completed, OkStatus());
context.server().SendResponse<UnaryMethod>(as_bytes(span("you nary?!?")),
OkStatus());
- EXPECT_FALSE(call.active());
+ EXPECT_FALSE(call_context.call.active());
}
TEST(Client, ProcessPacket_InvokesStreamCallbacks) {
RawClientTestContext context;
- auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+ auto call = StartStreamCall<BidirectionalStreamMethod>(context);
context.server().SendServerStream<BidirectionalStreamMethod>(
as_bytes(span("<=>")));
@@ -163,7 +154,7 @@ TEST(Client, ProcessPacket_InvokesStreamCallbacks) {
TEST(Client, ProcessPacket_UnassignedChannelId_ReturnsDataLoss) {
RawClientTestContext context;
- auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+ auto call_cts = StartStreamCall<BidirectionalStreamMethod>(context);
std::byte encoded[64];
Result<span<const std::byte>> result =
@@ -180,7 +171,7 @@ TEST(Client, ProcessPacket_UnassignedChannelId_ReturnsDataLoss) {
TEST(Client, ProcessPacket_InvokesErrorCallback) {
RawClientTestContext context;
- auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+ auto call = StartStreamCall<BidirectionalStreamMethod>(context);
context.server().SendServerError<BidirectionalStreamMethod>(
Status::Aborted());
@@ -247,9 +238,9 @@ TEST(Client, CloseChannel_UnknownChannel) {
TEST(Client, CloseChannel_CallsErrorCallback) {
RawClientTestContext ctx;
- TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(ctx);
+ CallContext call_ctx = StartUnaryCall<UnaryMethod>(ctx);
- ASSERT_NE(call.completed, OkStatus());
+ ASSERT_NE(call_ctx.completed, OkStatus());
ASSERT_EQ(1u,
static_cast<internal::Endpoint&>(ctx.client()).active_call_count());
@@ -257,25 +248,25 @@ TEST(Client, CloseChannel_CallsErrorCallback) {
EXPECT_EQ(0u,
static_cast<internal::Endpoint&>(ctx.client()).active_call_count());
- ASSERT_EQ(call.error, Status::Aborted()); // set by the on_error callback
+ ASSERT_EQ(call_ctx.error, Status::Aborted()); // set by the on_error callback
}
TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForCallOnClosedChannel) {
struct {
RawClientTestContext<> ctx;
- TestUnaryCall call;
+ CallContext<RawUnaryReceiver> call_ctx;
} context;
- context.call = StartCall<UnaryMethod, TestUnaryCall>(context.ctx);
- context.call.set_on_error([&context](Status error) {
- context.call = StartCall<UnaryMethod, TestUnaryCall>(context.ctx, 1);
- context.call.error = error;
+ context.call_ctx = StartUnaryCall<UnaryMethod>(context.ctx);
+ context.call_ctx.call.set_on_error([&context](Status error) {
+ context.call_ctx = StartUnaryCall<UnaryMethod>(context.ctx, 1);
+ context.call_ctx.error = error;
});
EXPECT_EQ(OkStatus(), context.ctx.client().CloseChannel(1));
- EXPECT_EQ(context.call.error, Status::Aborted());
+ EXPECT_EQ(context.call_ctx.error, Status::Aborted());
- EXPECT_FALSE(context.call.active());
+ EXPECT_FALSE(context.call_ctx.call.active());
EXPECT_EQ(0u,
static_cast<internal::Endpoint&>(context.ctx.client())
.active_call_count());
@@ -293,7 +284,12 @@ TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForActiveCall) {
Channel& channel() { return channels_[0]; }
Client& client() { return client_; }
- TestUnaryCall& call() { return call_; }
+ CallContext<RawUnaryReceiver>& call_ctx() { return call_context_; }
+ RawUnaryReceiver& call() { return call_context_.call; }
+
+ void StartCall(uint32_t channel_id) {
+ call_context_ = StartUnaryCall<UnaryMethod>(*this, channel_id);
+ }
private:
RawFakeChannelOutput<10, 256> channel_output_;
@@ -302,17 +298,17 @@ TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForActiveCall) {
std::byte packet_buffer[64];
FakeServer fake_server_;
- TestUnaryCall call_;
+ CallContext<RawUnaryReceiver> call_context_;
} context;
- context.call() = StartCall<UnaryMethod, TestUnaryCall>(context, 1);
+ context.StartCall(1);
context.call().set_on_error([&context](Status error) {
- context.call() = StartCall<UnaryMethod, TestUnaryCall>(context, 2);
- context.call().error = error;
+ context.StartCall(2);
+ context.call_ctx().error = error;
});
EXPECT_EQ(OkStatus(), context.client().CloseChannel(1));
- EXPECT_EQ(context.call().error, Status::Aborted());
+ EXPECT_EQ(context.call_ctx().error, Status::Aborted());
EXPECT_TRUE(context.call().active());
EXPECT_EQ(
diff --git a/pw_rpc/raw/codegen_test.cc b/pw_rpc/raw/codegen_test.cc
index 765dfdcda..072f87139 100644
--- a/pw_rpc/raw/codegen_test.cc
+++ b/pw_rpc/raw/codegen_test.cc
@@ -579,7 +579,7 @@ TEST_F(RawCodegenClientTest, ClientStream_Move) {
UnaryOnCompleted(),
OnError());
- EXPECT_EQ(OkStatus(), call.CloseClientStream());
+ EXPECT_EQ(OkStatus(), call.RequestCompletion());
RawClientWriter call_2;
diff --git a/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
index 2d780279b..e4ae19f3f 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
@@ -48,16 +48,18 @@ class RawClientReaderWriter : private internal::StreamResponseClientCall {
// Sends a stream request packet with the given raw payload.
using internal::Call::Write;
- // Notifies the server that no further client stream messages will be sent.
- using internal::ClientCall::CloseClientStream;
+ // Notifies the server that the client has requested to stop communication by
+ // sending CLIENT_REQUEST_COMPLETION.
+ using internal::ClientCall::RequestCompletion;
// Cancels this RPC. Closes the call locally and sends a CANCELLED error to
// the server.
using internal::Call::Cancel;
- // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
- // packet. Future packets for this RPC are dropped, and the client sends a
- // FAILED_PRECONDITION error in response because the call is not active.
+ // Closes this RPC locally. Sends a CLIENT_REQUEST_COMPLETION, but no
+ // cancellation packet. Future packets for this RPC are dropped, and the
+ // client sends a FAILED_PRECONDITION error in response because the call is
+ // not active.
using internal::ClientCall::Abandon;
// Allow use as a generic RPC Writer.
@@ -96,6 +98,7 @@ class RawClientReader : private internal::StreamResponseClientCall {
using internal::StreamResponseClientCall::set_on_next;
using internal::Call::Cancel;
+ using internal::Call::RequestCompletion;
using internal::ClientCall::Abandon;
private:
@@ -121,6 +124,10 @@ class RawClientWriter : private internal::UnaryResponseClientCall {
RawClientWriter(RawClientWriter&&) = default;
RawClientWriter& operator=(RawClientWriter&&) = default;
+ ~RawClientWriter() PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ DestroyClientCall();
+ }
+
using internal::UnaryResponseClientCall::active;
using internal::UnaryResponseClientCall::channel_id;
@@ -128,7 +135,7 @@ class RawClientWriter : private internal::UnaryResponseClientCall {
using internal::UnaryResponseClientCall::set_on_error;
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+ using internal::Call::RequestCompletion;
using internal::Call::Write;
using internal::ClientCall::Abandon;
@@ -159,6 +166,10 @@ class RawUnaryReceiver : private internal::UnaryResponseClientCall {
RawUnaryReceiver(RawUnaryReceiver&&) = default;
RawUnaryReceiver& operator=(RawUnaryReceiver&&) = default;
+ ~RawUnaryReceiver() PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ DestroyClientCall();
+ }
+
using internal::UnaryResponseClientCall::active;
using internal::UnaryResponseClientCall::channel_id;
diff --git a/pw_rpc/raw/public/pw_rpc/raw/client_testing.h b/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
index dbda24d58..9402e5275 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
@@ -15,6 +15,7 @@
#include <cstddef>
#include <cstdint>
+#include <optional>
#include <type_traits>
#include "pw_bytes/span.h"
@@ -26,7 +27,7 @@
namespace pw::rpc {
-// TODO(b/234878467): Document the client testing APIs.
+// TODO: b/234878467 - Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server.
class FakeServer {
diff --git a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
index 4de87fc03..91a85e552 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
@@ -75,7 +75,8 @@ class RawServerReaderWriter : private internal::ServerCall {
// Functions for setting the callbacks.
using internal::Call::set_on_error;
using internal::Call::set_on_next;
- using internal::ServerCall::set_on_client_stream_end;
+ using internal::ServerCall::set_on_completion_requested;
+ using internal::ServerCall::set_on_completion_requested_if_enabled;
// Sends a response packet with the given raw payload.
using internal::Call::Write;
@@ -140,7 +141,8 @@ class RawServerReader : private RawServerReaderWriter {
using RawServerReaderWriter::active;
using RawServerReaderWriter::channel_id;
- using RawServerReaderWriter::set_on_client_stream_end;
+ using RawServerReaderWriter::set_on_completion_requested;
+ using RawServerReaderWriter::set_on_completion_requested_if_enabled;
using RawServerReaderWriter::set_on_error;
using RawServerReaderWriter::set_on_next;
@@ -188,6 +190,8 @@ class RawServerWriter : private RawServerReaderWriter {
using RawServerReaderWriter::active;
using RawServerReaderWriter::channel_id;
+ using RawServerReaderWriter::set_on_completion_requested;
+ using RawServerReaderWriter::set_on_completion_requested_if_enabled;
using RawServerReaderWriter::set_on_error;
using RawServerReaderWriter::Finish;
diff --git a/pw_rpc/raw/synchronous_call_test.cc b/pw_rpc/raw/synchronous_call_test.cc
new file mode 100644
index 000000000..8e00631a2
--- /dev/null
+++ b/pw_rpc/raw/synchronous_call_test.cc
@@ -0,0 +1,241 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc/synchronous_call.h"
+
+#include <chrono>
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/raw/fake_channel_output.h"
+#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_thread/thread.h"
+#include "pw_work_queue/test_thread.h"
+#include "pw_work_queue/work_queue.h"
+
+namespace pw::rpc::test {
+namespace {
+
+using ::pw::rpc::test::pw_rpc::raw::TestService;
+using MethodInfo = internal::MethodInfo<TestService::TestUnaryRpc>;
+
+class RawSynchronousCallTest : public ::testing::Test {
+ public:
+ RawSynchronousCallTest()
+ : channels_({{Channel::Create<42>(&fake_output_)}}), client_(channels_) {}
+
+ void SetUp() override {
+ work_thread_ =
+ thread::Thread(work_queue::test::WorkQueueThreadOptions(), work_queue_);
+ }
+
+ void TearDown() override {
+ work_queue_.RequestStop();
+#if PW_THREAD_JOINING_ENABLED
+ work_thread_.join();
+#else
+ work_thread_.detach();
+#endif // PW_THREAD_JOINING_ENABLED
+ }
+
+ protected:
+ void OnSend(span<const std::byte> buffer, Status status) {
+ if (!status.ok()) {
+ return;
+ }
+ auto result = internal::Packet::FromBuffer(buffer);
+ EXPECT_TRUE(result.ok());
+ request_packet_ = *result;
+
+ EXPECT_TRUE(work_queue_.PushWork([this]() { SendResponse(); }).ok());
+ }
+
+ void SendResponse() {
+ std::array<std::byte, 256> buffer;
+ std::array<char, 32> payload_buffer;
+
+ PW_CHECK_UINT_LE(response_.size(), payload_buffer.size());
+ size_t size = response_.copy(payload_buffer.data(), payload_buffer.size());
+
+ auto response =
+ internal::Packet::Response(request_packet_, response_status_);
+ response.set_payload(as_bytes(span(payload_buffer.data(), size)));
+ EXPECT_TRUE(client_.ProcessPacket(response.Encode(buffer).value()).ok());
+ }
+
+ void set_response(std::string_view response,
+ Status response_status = OkStatus()) {
+ response_ = response;
+ response_status_ = response_status;
+ output().set_on_send([this](span<const std::byte> buffer, Status status) {
+ OnSend(buffer, status);
+ });
+ }
+
+ MethodInfo::GeneratedClient generated_client() {
+ return MethodInfo::GeneratedClient(client(), channel().id());
+ }
+
+ RawFakeChannelOutput<2>& output() { return fake_output_; }
+ const Channel& channel() const { return channels_.front(); }
+ Client& client() { return client_; }
+
+ private:
+ RawFakeChannelOutput<2> fake_output_;
+ std::array<Channel, 1> channels_;
+ Client client_;
+ thread::Thread work_thread_;
+ work_queue::WorkQueueWithBuffer<1> work_queue_;
+ std::string_view response_;
+ Status response_status_ = OkStatus();
+ internal::Packet request_packet_;
+};
+
+template <Status::Code kExpectedStatus = OkStatus().code()>
+auto CopyReply(InlineString<32>& reply) {
+ return [&reply](ConstByteSpan response, Status status) {
+ EXPECT_EQ(Status(kExpectedStatus), status);
+ reply.assign(reinterpret_cast<const char*>(response.data()),
+ response.size());
+ };
+}
+
+void ExpectNoReply(ConstByteSpan, Status) { FAIL(); }
+
+TEST_F(RawSynchronousCallTest, SynchronousCallSuccess) {
+ set_response("jicama", OkStatus());
+
+ InlineString<32> reply;
+ ASSERT_EQ(OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), {}, CopyReply(reply)));
+ EXPECT_EQ("jicama", reply);
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallServerError) {
+ set_response("raddish", Status::Internal());
+
+ InlineString<32> reply;
+ ASSERT_EQ(OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ CopyReply<Status::Internal().code()>(reply)));
+ // We should still receive the response
+ EXPECT_EQ("raddish", reply);
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallRpcError) {
+ // Internally, if Channel receives a non-ok status from the
+ // ChannelOutput::Send, it will always return Unknown.
+ output().set_send_status(Status::Unknown());
+
+ EXPECT_EQ(Status::Unknown(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), {}, ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallFor) {
+ set_response("broccoli", Status::NotFound());
+
+ InlineString<32> reply;
+ ASSERT_EQ(OkStatus(),
+ SynchronousCallFor<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ chrono::SystemClock::for_at_least(std::chrono::seconds(1)),
+ [&reply](ConstByteSpan response, Status status) {
+ EXPECT_EQ(Status::NotFound(), status);
+ reply.assign(reinterpret_cast<const char*>(response.data()),
+ response.size());
+ }));
+ EXPECT_EQ("broccoli", reply);
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallForTimeoutError) {
+ ASSERT_EQ(Status::DeadlineExceeded(),
+ SynchronousCallFor<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)),
+ ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, SynchronousCallUntilTimeoutError) {
+ EXPECT_EQ(Status::DeadlineExceeded(),
+ SynchronousCallUntil<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ {},
+ chrono::SystemClock::now(),
+ ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
+ set_response("lettuce", OkStatus());
+
+ InlineString<32> reply;
+ EXPECT_EQ(OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ generated_client(), {}, CopyReply(reply)));
+ EXPECT_EQ("lettuce", reply);
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallServerError) {
+ set_response("cabbage", Status::Internal());
+
+ InlineString<32> reply;
+ EXPECT_EQ(
+ OkStatus(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ generated_client(), {}, CopyReply<Status::Internal().code()>(reply)));
+ EXPECT_EQ("cabbage", reply);
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallRpcError) {
+ output().set_send_status(Status::Unknown());
+
+ EXPECT_EQ(Status::Unknown(),
+ SynchronousCall<TestService::TestUnaryRpc>(
+ generated_client(), {}, ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest, GeneratedClientSynchronousCallForTimeoutError) {
+ EXPECT_EQ(Status::DeadlineExceeded(),
+ SynchronousCallFor<TestService::TestUnaryRpc>(
+ generated_client(),
+ {},
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)),
+ ExpectNoReply));
+}
+
+TEST_F(RawSynchronousCallTest,
+ GeneratedClientSynchronousCallUntilTimeoutError) {
+ EXPECT_EQ(
+ Status::DeadlineExceeded(),
+ SynchronousCallUntil<TestService::TestUnaryRpc>(
+ generated_client(), {}, chrono::SystemClock::now(), ExpectNoReply));
+}
+
+} // namespace
+} // namespace pw::rpc::test
diff --git a/pw_rpc/request_packets.svg b/pw_rpc/request_packets.svg
deleted file mode 100644
index 7a54d8d09..000000000
--- a/pw_rpc/request_packets.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="280" viewBox="0 0 640 280" width="640" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>pw_rpc Requests</title>
-<desc></desc>
-<rect fill="rgb(243,152,0)" height="60" style="filter:url(#filter_blur)" width="528" x="56" y="110"></rect>
-<ellipse cx="131" cy="66" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="126"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="126"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="126"></rect>
-<ellipse cx="131" cy="226" fill="rgb(0,0,0)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<path d="M 267 206 L 379 206 A8,8 0 0 1 387 214 L 387 238 A8,8 0 0 1 379 246 L 267 246 A8,8 0 0 1 259 238 L 259 214 A8,8 0 0 1 267 206" fill="rgb(0,0,0)" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></path>
-<ellipse cx="128" cy="60" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="157.0" y="56">packets</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="120"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="128.0" y="146">Server</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="120"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="320.0" y="146">Service</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="120"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="96" x="512.0" y="146">internal::Method</text>
-<ellipse cx="128" cy="220" fill="rgb(255,255,255)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="128.0" y="226">generated services</text>
-<path d="M 264 200 L 376 200 A8,8 0 0 1 384 208 L 384 232 A8,8 0 0 1 376 240 L 264 240 A8,8 0 0 1 256 232 L 256 208 A8,8 0 0 1 264 200" fill="rgb(255,255,255)" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="102" x="320.0" y="226">user-defined RPCs</text>
-<path d="M 128 68 L 128 112" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,119 124,112 132,112 128,119" stroke="rgb(0,0,0)"></polygon>
-<path d="M 192 220 L 248 220" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="255,220 248,216 248,224 255,220" stroke="rgb(0,0,0)"></polygon>
-<path d="M 192 140 L 248 140" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="255,140 248,136 248,144 255,140" stroke="rgb(0,0,0)"></polygon>
-<path d="M 384 140 L 440 140" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="447,140 440,136 440,144 447,140" stroke="rgb(0,0,0)"></polygon>
-<path d="M 512 160 L 512 180" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 180 L 512 180" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 180 L 128 192" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,199 124,192 132,192 128,199" stroke="rgb(0,0,0)"></polygon>
-<rect class="highlighted" width="68.9688" x="285.766" y="106" height="12"></rect><text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="84" x="320.0" y="116"><tspan>pw_rpc</tspan> library</text>
-</svg>
diff --git a/pw_rpc/response_packets.svg b/pw_rpc/response_packets.svg
deleted file mode 100644
index 065132e94..000000000
--- a/pw_rpc/response_packets.svg
+++ /dev/null
@@ -1,42 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="360" viewBox="0 0 640 360" width="640" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>pw_rpc Responses</title>
-<desc></desc>
-<rect fill="rgb(243,152,0)" height="60" style="filter:url(#filter_blur)" width="528" x="56" y="190"></rect>
-<path d="M 75 46 L 187 46 A8,8 0 0 1 195 54 L 195 78 A8,8 0 0 1 187 86 L 75 86 A8,8 0 0 1 67 78 L 67 54 A8,8 0 0 1 75 46" fill="rgb(0,0,0)" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></path>
-<ellipse cx="131" cy="146" fill="rgb(0,0,0)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="259" y="206"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="67" y="206"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="451" y="206"></rect>
-<ellipse cx="131" cy="306" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></ellipse>
-<path d="M 72 40 L 184 40 A8,8 0 0 1 192 48 L 192 72 A8,8 0 0 1 184 80 L 72 80 A8,8 0 0 1 64 72 L 64 48 A8,8 0 0 1 72 40" fill="rgb(255,255,255)" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="102" x="128.0" y="66">user-defined RPCs</text>
-<ellipse cx="128" cy="140" fill="rgb(255,255,255)" rx="64.0" ry="20.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="128.0" y="146">generated services</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="256" y="200"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="320.0" y="226">Server</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="64" y="200"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="96" x="128.0" y="226">internal::Method</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="448" y="200"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="512.0" y="226">Channel</text>
-<ellipse cx="128" cy="300" fill="rgb(0,0,0)" rx="8.0" ry="8.0" stroke="rgb(0,0,0)"></ellipse>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="157.0" y="296">packets</text>
-<path d="M 128 80 L 128 112" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,119 124,112 132,112 128,119" stroke="rgb(0,0,0)"></polygon>
-<path d="M 128 160 L 128 192" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,199 124,192 132,192 128,199" stroke="rgb(0,0,0)"></polygon>
-<path d="M 384 220 L 440 220" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="447,220 440,216 440,224 447,220" stroke="rgb(0,0,0)"></polygon>
-<path d="M 192 220 L 248 220" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="255,220 248,216 248,224 255,220" stroke="rgb(0,0,0)"></polygon>
-<path d="M 512 240 L 512 260" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 260 L 512 260" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 128 260 L 128 284" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="128,291 124,284 132,284 128,291" stroke="rgb(0,0,0)"></polygon>
-<rect class="highlighted" width="68.9688" x="285.766" y="186" height="12"></rect><text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="84" x="320.0" y="196"><tspan>pw_rpc</tspan> library</text>
-</svg>
diff --git a/pw_rpc/server.cc b/pw_rpc/server.cc
index 4af9ff4e6..8822a0fd0 100644
--- a/pw_rpc/server.cc
+++ b/pw_rpc/server.cc
@@ -92,8 +92,8 @@ Status Server::ProcessPacket(ConstByteSpan packet_data) {
internal::rpc_lock().unlock();
}
break;
- case PacketType::CLIENT_STREAM_END:
- HandleClientStreamPacket(packet, *channel, call);
+ case PacketType::CLIENT_REQUEST_COMPLETION:
+ HandleCompletionRequest(packet, *channel, call);
break;
case PacketType::REQUEST: // Handled above
case PacketType::RESPONSE:
@@ -122,6 +122,35 @@ std::tuple<Service*, const internal::Method*> Server::FindMethod(
return {&(*service), service->FindMethod(packet.method_id())};
}
+void Server::HandleCompletionRequest(
+ const internal::Packet& packet,
+ internal::Channel& channel,
+ IntrusiveList<internal::Call>::iterator call) const {
+ if (call == calls_end()) {
+ channel.Send(Packet::ServerError(packet, Status::FailedPrecondition()))
+ .IgnoreError(); // Errors are logged in Channel::Send.
+ internal::rpc_lock().unlock();
+ PW_LOG_DEBUG(
+ "Received a request completion packet for %u:%08x/%08x, which is not a"
+ "pending call",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
+ return;
+ }
+
+ if (call->client_requested_completion()) {
+ internal::rpc_lock().unlock();
+ PW_LOG_DEBUG("Received multiple completion requests for %u:%08x/%08x",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
+ return;
+ }
+
+ static_cast<internal::ServerCall&>(*call).HandleClientRequestedCompletion();
+}
+
void Server::HandleClientStreamPacket(
const internal::Packet& packet,
internal::Channel& channel,
@@ -151,7 +180,7 @@ void Server::HandleClientStreamPacket(
return;
}
- if (!call->client_stream_open()) {
+ if (call->client_requested_completion()) {
channel.Send(Packet::ServerError(packet, Status::FailedPrecondition()))
.IgnoreError(); // Errors are logged in Channel::Send.
internal::rpc_lock().unlock();
@@ -164,11 +193,7 @@ void Server::HandleClientStreamPacket(
return;
}
- if (packet.type() == PacketType::CLIENT_STREAM) {
- call->HandlePayload(packet.payload());
- } else { // Handle PacketType::CLIENT_STREAM_END.
- static_cast<internal::ServerCall&>(*call).HandleClientStreamEnd();
- }
+ call->HandlePayload(packet.payload());
}
} // namespace pw::rpc
diff --git a/pw_rpc/server_call.cc b/pw_rpc/server_call.cc
index c665595ba..ced7958a1 100644
--- a/pw_rpc/server_call.cc
+++ b/pw_rpc/server_call.cc
@@ -26,9 +26,10 @@ void ServerCall::MoveServerCallFrom(ServerCall& other) {
MoveFrom(other);
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- on_client_stream_end_ = std::move(other.on_client_stream_end_);
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ on_client_requested_completion_ =
+ std::move(other.on_client_requested_completion_);
+#endif // PW_RPC_COMPLETION_REQUEST_CALLBACK
}
} // namespace pw::rpc::internal
diff --git a/pw_rpc/server_streaming_rpc.svg b/pw_rpc/server_streaming_rpc.svg
deleted file mode 100644
index e153a0a82..000000000
--- a/pw_rpc/server_streaming_rpc.svg
+++ /dev/null
@@ -1,58 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="511.50000000000006" viewBox="0 0 564 465" width="620.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Server Streaming RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="284" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="171" y="159"></rect>
-<rect fill="rgb(0,0,0)" height="214" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="363" y="159"></rect>
-<polygon fill="rgb(0,0,0)" points="27,126 151,126 159,134 159,193 27,193 27,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="383,233 543,233 551,241 551,300 383,300 383,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="383,340 513,340 521,348 521,407 383,407 383,340" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="111" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="303" y="46"></rect>
-<path d="M 172 80 L 172 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="284" stroke="rgb(0,0,0)" width="8" x="168" y="153"></rect>
-<path d="M 364 80 L 364 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="214" stroke="rgb(0,0,0)" width="8" x="360" y="153"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="108" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="172.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="300" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="364.0" y="66">server</text>
-<path d="M 180 153 L 356 153" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="348,149 356,153 348,157" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,120 148,120 156,128 156,187 24,187 24,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 148 120 L 148 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 148 128 L 156 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="86.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="172">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="53.0" y="185">payload</text>
-<path d="M 180 260 L 356 260" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4"></path>
-<polygon fill="rgb(0,0,0)" points="188,256 180,260 188,264" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="380,227 540,227 548,235 548,294 380,294 380,227" stroke="rgb(0,0,0)"></polygon>
-<path d="M 540 227 L 540 235" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 540 235 L 548 235" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="144" x="460.0" y="240">PacketType.SERVER_STREAM</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="253">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="266">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="415.0" y="279">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="409.0" y="292">payload</text>
-<path d="M 180 367 L 356 367" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="188,363 180,367 188,371" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="380,334 510,334 518,342 518,401 380,401 380,334" stroke="rgb(0,0,0)"></polygon>
-<path d="M 510 334 L 510 342" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 510 342 L 518 342" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="445.0" y="347">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="360">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="373">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="415.0" y="386">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="406.0" y="399">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="205.0" y="151">request</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="138" x="287.0" y="258">messages (zero or more)</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="24" x="344.0" y="365">done</text>
-</svg>
diff --git a/pw_rpc/server_streaming_rpc_cancelled.svg b/pw_rpc/server_streaming_rpc_cancelled.svg
deleted file mode 100644
index d7432da7f..000000000
--- a/pw_rpc/server_streaming_rpc_cancelled.svg
+++ /dev/null
@@ -1,77 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg viewBox="0 0 598 465" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" width="598px" height="465px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" />
- </filter>
- </defs>
- <title>Cancelled Server Streaming RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "request",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client &lt;-- server [
- noactivate,
- label = "messages (zero or more)",
- rightnote = "PacketType.SERVER_STREAM\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- leftnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="284" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="203" y="159" />
- <rect fill="rgb(0,0,0)" height="284" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="395" y="159" />
- <polygon fill="rgb(0,0,0)" points="58,126 183,126 191,134 191,193 58,193 58,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <polygon fill="rgb(0,0,0)" points="415,233 577,233 585,241 585,300 415,300 415,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <polygon fill="rgb(0,0,0)" points="27,340 183,340 191,348 191,407 27,407 27,340" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="143" y="46" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="335" y="46" />
- <path d="M 204 80 L 204 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="284" stroke="rgb(0,0,0)" width="8" x="200" y="153" />
- <path d="M 396 80 L 396 453" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="284" stroke="rgb(0,0,0)" width="8" x="392" y="153" />
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="140" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="204.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="332" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="396.5" y="66">server</text>
- <path d="M 212 153 L 388 153" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,149 388,153 380,157" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="55,120 180,120 188,128 188,187 55,187 55,120" stroke="rgb(0,0,0)" />
- <path d="M 180 120 L 180 128" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 128 L 188 128" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="117.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="90.5" y="172">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="84.5" y="185">payload</text>
- <path d="M 212 260 L 388 260" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="4" />
- <polygon fill="rgb(0,0,0)" points="220,256 212,260 220,264" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="412,227 574,227 582,235 582,294 412,294 412,227" stroke="rgb(0,0,0)" />
- <path d="M 574 227 L 574 235" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 574 235 L 582 235" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="493.0" y="240">PacketType.SERVER_STREAM</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="450.5" y="253">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="450.5" y="266">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="447.5" y="279">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="441.5" y="292">payload</text>
- <path d="M 212 367 L 388 367" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,363 388,367 380,371" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="24,334 180,334 188,342 188,401 24,401 24,334" stroke="rgb(0,0,0)" />
- <path d="M 180 334 L 180 342" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 342 L 188 342" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="102.0" y="347">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="360">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="373">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="386">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="80.5" y="399">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="237.5" y="151">request</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="318.0" y="258">messages (zero or more)</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="234.5" y="365">cancel</text>
-</svg>
diff --git a/pw_rpc/server_test.cc b/pw_rpc/server_test.cc
index 20e91e954..bbdb62421 100644
--- a/pw_rpc/server_test.cc
+++ b/pw_rpc/server_test.cc
@@ -22,10 +22,10 @@
#include "pw_rpc/internal/call.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/packet.h"
-#include "pw_rpc/internal/test_method.h"
#include "pw_rpc/internal/test_utils.h"
#include "pw_rpc/service.h"
#include "pw_rpc_private/fake_server_reader_writer.h"
+#include "pw_rpc_private/test_method.h"
namespace pw::rpc {
namespace {
@@ -344,7 +344,7 @@ TEST_F(BasicServer, CloseChannel_PendingCall) {
EXPECT_NE(nullptr, GetChannel(server_, 1));
EXPECT_EQ(static_cast<internal::Endpoint&>(server_).active_call_count(), 0u);
- internal::TestMethod::FakeServerCall call;
+ internal::test::FakeServerReaderWriter call;
service_42_.method(100).keep_call_active(call);
EXPECT_EQ(
@@ -617,26 +617,45 @@ TEST_F(BidiMethod, UnregsiterService_AbortsActiveCalls) {
EXPECT_EQ(Status::Aborted(), on_error_status);
}
-#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+TEST_F(BidiMethod, ClientRequestedCompletion_CallsCallback) {
+ bool called = false;
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ responder_.set_on_completion_requested([&called]() { called = true; });
+#endif
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
+}
-TEST_F(BidiMethod, ClientStreamEnd_CallsCallback) {
+TEST_F(BidiMethod, ClientRequestedCompletion_CallsCallbackIfEnabled) {
bool called = false;
- responder_.set_on_client_stream_end([&called]() { called = true; });
+ responder_.set_on_completion_requested_if_enabled(
+ [&called]() { called = true; });
ASSERT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END)));
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
EXPECT_EQ(output_.total_packets(), 0u);
- EXPECT_TRUE(called);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
}
-TEST_F(BidiMethod, ClientStreamEnd_ErrorWhenClosed) {
- const auto end = PacketForRpc(PacketType::CLIENT_STREAM_END);
+TEST_F(BidiMethod, ClientRequestedCompletion_ErrorWhenClosed) {
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
- bool called = false;
- responder_.set_on_client_stream_end([&called]() { called = true; });
+ ASSERT_EQ(output_.total_packets(), 0u);
+}
+TEST_F(BidiMethod, ClientRequestedCompletion_ErrorWhenAlreadyClosed) {
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
+ EXPECT_FALSE(responder_.active());
+
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(output_.total_packets(), 1u);
@@ -646,8 +665,6 @@ TEST_F(BidiMethod, ClientStreamEnd_ErrorWhenClosed) {
EXPECT_EQ(packet.status(), Status::FailedPrecondition());
}
-#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
-
class ServerStreamingMethod : public BasicServer {
protected:
ServerStreamingMethod() {
@@ -677,15 +694,55 @@ TEST_F(ServerStreamingMethod, ClientStream_InvalidArgumentError) {
EXPECT_EQ(packet.status(), Status::InvalidArgument());
}
-TEST_F(ServerStreamingMethod, ClientStreamEnd_InvalidArgumentError) {
+TEST_F(ServerStreamingMethod, ClientRequestedCompletion_CallsCallback) {
+ bool called = false;
+#if PW_RPC_COMPLETION_REQUEST_CALLBACK
+ responder_.set_on_completion_requested([&called]() { called = true; });
+#endif
+
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
+}
+
+TEST_F(ServerStreamingMethod,
+ ClientRequestedCompletion_CallsCallbackIfEnabled) {
+ bool called = false;
+ responder_.set_on_completion_requested_if_enabled(
+ [&called]() { called = true; });
+
ASSERT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END)));
+ server_.ProcessPacket(
+ PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_EQ(called, PW_RPC_COMPLETION_REQUEST_CALLBACK);
+}
+
+TEST_F(ServerStreamingMethod, ClientRequestedCompletion_ErrorWhenClosed) {
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
+
+ ASSERT_EQ(output_.total_packets(), 0u);
+}
+
+TEST_F(ServerStreamingMethod,
+ ClientRequestedCompletion_ErrorWhenAlreadyClosed) {
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
+ EXPECT_FALSE(responder_.active());
+
+ const auto end = PacketForRpc(PacketType::CLIENT_REQUEST_COMPLETION);
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(output_.total_packets(), 1u);
const Packet& packet =
static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
EXPECT_EQ(packet.type(), PacketType::SERVER_ERROR);
- EXPECT_EQ(packet.status(), Status::InvalidArgument());
+ EXPECT_EQ(packet.status(), Status::FailedPrecondition());
}
} // namespace
diff --git a/pw_rpc/size_report/base.cc b/pw_rpc/size_report/base.cc
index 5248c8034..fa85564d8 100644
--- a/pw_rpc/size_report/base.cc
+++ b/pw_rpc/size_report/base.cc
@@ -28,9 +28,9 @@ int main() {
std::byte packet_buffer[128];
pw::sys_io::ReadBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
pw::sys_io::WriteBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/size_report/server_only.cc b/pw_rpc/size_report/server_only.cc
index a8644f778..7f7b1cbcd 100644
--- a/pw_rpc/size_report/server_only.cc
+++ b/pw_rpc/size_report/server_only.cc
@@ -46,12 +46,12 @@ int main() {
std::byte packet_buffer[128];
pw::sys_io::ReadBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
pw::sys_io::WriteBytes(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
my_product::server.ProcessPacket(packet_buffer)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/system_server/BUILD.bazel b/pw_rpc/system_server/BUILD.bazel
index 407c6da73..eca9534d0 100644
--- a/pw_rpc/system_server/BUILD.bazel
+++ b/pw_rpc/system_server/BUILD.bazel
@@ -44,13 +44,13 @@ pw_cc_library(
"//pw_span",
"//pw_status",
"//pw_stream",
- "@pigweed_config//:pw_rpc_system_server_backend",
+ "@pigweed//targets:pw_rpc_system_server_backend",
],
)
pw_cc_library(
name = "system_server_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/board:stm32f429i-disc1": ["//pw_hdlc:hdlc_sys_io_system_server"],
"//pw_build/constraints/board:mimxrt595_evk": ["//pw_hdlc:hdlc_sys_io_system_server"],
diff --git a/pw_rpc/ts/call.ts b/pw_rpc/ts/call.ts
index 11894dc8c..e418760c3 100644
--- a/pw_rpc/ts/call.ts
+++ b/pw_rpc/ts/call.ts
@@ -12,12 +12,12 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Status} from 'pigweedjs/pw_status';
-import {Message} from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
-import WaitQueue from "./queue";
+import WaitQueue from './queue';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { PendingCalls, Rpc } from './rpc_classes';
export type Callback = (a: any) => any;
@@ -70,7 +70,7 @@ export class Call {
rpc: Rpc,
onNext: Callback,
onCompleted: Callback,
- onError: Callback
+ onError: Callback,
) {
this.rpcs = rpcs;
this.rpc = rpc;
@@ -86,7 +86,7 @@ export class Call {
this.rpc,
this,
ignoreErrors,
- request
+ request,
);
if (previous !== undefined && !previous.completed) {
@@ -98,13 +98,14 @@ export class Call {
return this.status !== undefined || this.error !== undefined;
}
+ // eslint-disable-next-line @typescript-eslint/ban-types
private invokeCallback(func: () => {}) {
try {
func();
} catch (err: unknown) {
if (err instanceof Error) {
console.error(
- `An exception was raised while invoking a callback: ${err}`
+ `An exception was raised while invoking a callback: ${err}`,
);
this.callbackException = err;
}
@@ -131,8 +132,9 @@ export class Call {
}
private async queuePopWithTimeout(
- timeoutMs: number
+ timeoutMs: number,
): Promise<Message | undefined> {
+ // eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
let timeoutExpired = false;
const timeoutWatcher = setTimeout(() => {
@@ -170,7 +172,7 @@ export class Call {
*/
async *getResponses(
count?: number,
- timeoutMs?: number
+ timeoutMs?: number,
): AsyncGenerator<Message> {
this.checkErrors();
@@ -213,6 +215,7 @@ export class Call {
protected async unaryWait(timeoutMs?: number): Promise<[Status, Message]> {
for await (const response of this.getResponses(1, timeoutMs)) {
+ // Do nothing.
}
if (this.status === undefined) {
throw Error('Unexpected undefined status at end of stream');
@@ -225,6 +228,7 @@ export class Call {
protected async streamWait(timeoutMs?: number): Promise<[Status, Message[]]> {
for await (const response of this.getResponses(undefined, timeoutMs)) {
+ // Do nothing.
}
if (this.status === undefined) {
throw Error('Unexpected undefined status at end of stream');
@@ -276,7 +280,7 @@ export class ClientStreamingCall extends Call {
/** Ends the client stream and waits for the RPC to complete. */
async finishAndWait(
requests: Message[] = [],
- timeoutMs?: number
+ timeoutMs?: number,
): Promise<[Status, Message[]]> {
this.finishClientStream(requests);
return await this.streamWait(timeoutMs);
@@ -300,7 +304,7 @@ export class BidirectionalStreamingCall extends Call {
/** Ends the client stream and waits for the RPC to complete. */
async finishAndWait(
requests: Array<Message> = [],
- timeoutMs?: number
+ timeoutMs?: number,
): Promise<[Status, Array<Message>]> {
this.finishClientStream(requests);
return await this.streamWait(timeoutMs);
diff --git a/pw_rpc/ts/call_test.ts b/pw_rpc/ts/call_test.ts
index 47af6dab8..98590c88a 100644
--- a/pw_rpc/ts/call_test.ts
+++ b/pw_rpc/ts/call_test.ts
@@ -14,11 +14,11 @@
/* eslint-env browser */
-import {SomeMessage} from 'pigweedjs/protos/pw_rpc/ts/test2_pb';
+import { SomeMessage } from 'pigweedjs/protos/pw_rpc/ts/test2_pb';
-import {Call} from './call';
-import {Channel, Method, Service} from './descriptors';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { Call } from './call';
+import { Channel, Method, Service } from './descriptors';
+import { PendingCalls, Rpc } from './rpc_classes';
class FakeRpc {
readonly channel: any = undefined;
@@ -33,7 +33,9 @@ describe('Call', () => {
let call: Call;
beforeEach(() => {
- const noop = () => { };
+ const noop = () => {
+ // Do nothing.
+ };
const pendingCalls = new PendingCalls();
const rpc = new FakeRpc();
call = new Call(pendingCalls, rpc, noop, noop, noop);
diff --git a/pw_rpc/ts/client.ts b/pw_rpc/ts/client.ts
index e7a97176d..e99554877 100644
--- a/pw_rpc/ts/client.ts
+++ b/pw_rpc/ts/client.ts
@@ -14,18 +14,18 @@
/** Provides a pw_rpc client for TypeScript. */
-import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
-import {Status} from 'pigweedjs/pw_status';
-import {Message} from 'google-protobuf';
+import { ProtoCollection } from 'pigweedjs/pw_protobuf_compiler';
+import { Status } from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Channel, Service} from './descriptors';
-import {MethodStub, methodStubFactory} from './method';
+import { Channel, Service } from './descriptors';
+import { MethodStub, methodStubFactory } from './method';
import * as packets from './packets';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { PendingCalls, Rpc } from './rpc_classes';
/**
* Object for managing RPC service and contained methods.
@@ -38,7 +38,7 @@ export class ServiceClient {
constructor(client: Client, channel: Channel, service: Service) {
this.service = service;
const methods = service.methods;
- methods.forEach(method => {
+ methods.forEach((method) => {
const stub = methodStubFactory(client.rpcs, channel, method);
this.methods.push(stub);
this.methodsByName.set(method.name, stub);
@@ -67,7 +67,7 @@ export class ChannelClient {
constructor(client: Client, channel: Channel, services: Service[]) {
this.channel = channel;
- services.forEach(service => {
+ services.forEach((service) => {
const serviceClient = new ServiceClient(client, this.channel, service);
this.services.set(service.name, serviceClient);
});
@@ -122,14 +122,14 @@ export class Client {
constructor(channels: Channel[], services: Service[]) {
this.rpcs = new PendingCalls();
- services.forEach(service => {
+ services.forEach((service) => {
this.services.set(service.id, service);
});
- channels.forEach(channel => {
+ channels.forEach((channel) => {
this.channelsById.set(
channel.id,
- new ChannelClient(this, channel, services)
+ new ChannelClient(this, channel, services),
);
});
}
@@ -145,11 +145,11 @@ export class Client {
static fromProtoSet(channels: Channel[], protoSet: ProtoCollection): Client {
let services: Service[] = [];
const descriptors = protoSet.fileDescriptorSet.getFileList();
- descriptors.forEach(fileDescriptor => {
+ descriptors.forEach((fileDescriptor) => {
const packageName = fileDescriptor.getPackage()!;
- fileDescriptor.getServiceList().forEach(serviceDescriptor => {
+ fileDescriptor.getServiceList().forEach((serviceDescriptor) => {
services = services.concat(
- new Service(serviceDescriptor, protoSet, packageName)
+ new Service(serviceDescriptor, protoSet, packageName),
);
});
});
@@ -176,7 +176,7 @@ export class Client {
*/
private rpc(
packet: RpcPacket,
- channelClient: ChannelClient
+ channelClient: ChannelClient,
): Rpc | undefined {
const service = this.services.get(packet.getServiceId());
if (service == undefined) {
@@ -215,7 +215,7 @@ export class Client {
private sendClientError(
client: ChannelClient,
packet: RpcPacket,
- error: Status
+ error: Status,
) {
client.channel.send(packets.encodeClientError(packet, error));
}
@@ -290,10 +290,10 @@ export class Client {
if (packet.getType() === PacketType.SERVER_ERROR) {
if (status === Status.OK) {
- throw 'Unexpected OK status on SERVER_ERROR';
+ throw new Error('Unexpected OK status on SERVER_ERROR');
}
if (status === undefined) {
- throw 'Missing status on SERVER_ERROR';
+ throw new Error('Missing status on SERVER_ERROR');
}
console.warn(`${rpc}: invocation failed with status: ${Status[status]}`);
call.handleError(status);
diff --git a/pw_rpc/ts/client_test.ts b/pw_rpc/ts/client_test.ts
index 0dfed21b1..0535fddb9 100644
--- a/pw_rpc/ts/client_test.ts
+++ b/pw_rpc/ts/client_test.ts
@@ -14,21 +14,18 @@
/* eslint-env browser */
-import {Status} from 'pigweedjs/pw_status';
-import {MessageCreator} from 'pigweedjs/pw_protobuf_compiler';
-import {Message} from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
+import { MessageCreator } from 'pigweedjs/pw_protobuf_compiler';
+import { Message } from 'google-protobuf';
import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {ProtoCollection} from 'pigweedjs/protos/collection';
-import {
- Request,
- Response,
-} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
+import { Request, Response } from 'pigweedjs/protos/pw_rpc/ts/test_pb';
-import {Client} from './client';
-import {Channel, Method} from './descriptors';
+import { Client } from './client';
+import { Channel, Method } from './descriptors';
import {
BidirectionalStreamingMethodStub,
ClientStreamingMethodStub,
@@ -77,10 +74,10 @@ describe('Client', () => {
const channel = client.channel()!;
expect(channel.methodStub('')).toBeUndefined();
expect(
- channel.methodStub('pw.rpc.test1.Garbage.SomeUnary')
+ channel.methodStub('pw.rpc.test1.Garbage.SomeUnary'),
).toBeUndefined();
expect(
- channel.methodStub('pw.rpc.test1.TheTestService.Garbage')
+ channel.methodStub('pw.rpc.test1.TheTestService.Garbage'),
).toBeUndefined();
});
@@ -134,7 +131,7 @@ describe('Client', () => {
const packet = packets.encodeResponse(
[1, service.id, method.id],
- new Request()
+ new Request(),
);
const status = client.processPacket(packet);
expect(client.processPacket(packet)).toEqual(Status.OK);
@@ -159,7 +156,12 @@ describe('RPC', () => {
beforeEach(async () => {
protoCollection = new ProtoCollection();
- const channels = [new Channel(1, handlePacket), new Channel(2, () => { })];
+ const channels = [
+ new Channel(1, handlePacket),
+ new Channel(2, () => {
+ // Do nothing.
+ }),
+ ];
client = Client.fromProtoSet(channels, protoCollection);
lastPacketSent = undefined;
requests = [];
@@ -185,7 +187,7 @@ describe('RPC', () => {
channelId: number,
method: Method,
status: Status,
- response?: Message
+ response?: Message,
) {
const packet = new RpcPacket();
packet.setType(PacketType.RESPONSE);
@@ -194,7 +196,7 @@ describe('RPC', () => {
packet.setMethodId(method.id);
packet.setStatus(status);
if (response === undefined) {
- packet.setPayload(new Uint8Array());
+ packet.setPayload(new Uint8Array(0));
} else {
packet.setPayload(response.serializeBinary());
}
@@ -205,7 +207,7 @@ describe('RPC', () => {
channelId: number,
method: Method,
response: Message,
- status: Status = Status.OK
+ status: Status = Status.OK,
) {
const packet = new RpcPacket();
packet.setType(PacketType.SERVER_STREAM);
@@ -221,7 +223,7 @@ describe('RPC', () => {
channelId: number,
method: Method,
status: Status,
- processStatus: Status
+ processStatus: Status,
) {
const packet = new RpcPacket();
packet.setType(PacketType.SERVER_ERROR);
@@ -276,8 +278,8 @@ describe('RPC', () => {
unaryStub = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeUnary'
- )! as UnaryMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeUnary',
+ ) as UnaryMethodStub;
});
it('blocking call', async () => {
@@ -286,7 +288,7 @@ describe('RPC', () => {
1,
unaryStub.method,
Status.ABORTED,
- newResponse('0_o')
+ newResponse('0_o'),
);
const [status, response] = await unaryStub.call(newRequest(6));
@@ -308,7 +310,7 @@ describe('RPC', () => {
newRequest(5),
onNext,
onCompleted,
- onError
+ onError,
);
expect(sentPayload(Request).getMagicNumber()).toEqual(5);
@@ -406,8 +408,8 @@ describe('RPC', () => {
serverStreaming = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeServerStreaming'
- )! as ServerStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeServerStreaming',
+ ) as ServerStreamingMethodStub;
});
it('non-blocking call', () => {
@@ -430,7 +432,7 @@ describe('RPC', () => {
expect(onCompleted).toHaveBeenCalledWith(Status.ABORTED);
expect(
- sentPayload(serverStreaming.method.requestType).getMagicNumber()
+ sentPayload(serverStreaming.method.requestType).getMagicNumber(),
).toEqual(4);
}
});
@@ -452,7 +454,7 @@ describe('RPC', () => {
newRequest(3),
onNext,
onCompleted,
- onError
+ onError,
);
expect(requests).toHaveLength(0);
@@ -507,8 +509,8 @@ describe('RPC', () => {
clientStreaming = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeClientStreaming'
- )! as ClientStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeClientStreaming',
+ ) as ClientStreamingMethodStub;
});
it('non-blocking call', () => {
@@ -584,7 +586,9 @@ describe('RPC', () => {
enqueueResponse(1, clientStreaming.method, Status.OK, testResponse);
stream.finishAndWait();
- expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM_END);
+ expect(lastRequest().getType()).toEqual(
+ PacketType.CLIENT_REQUEST_COMPLETION,
+ );
expect(onNext).toHaveBeenCalledWith(testResponse);
expect(stream.completed).toBe(true);
@@ -614,7 +618,7 @@ describe('RPC', () => {
1,
clientStreaming.method,
Status.INVALID_ARGUMENT,
- Status.OK
+ Status.OK,
);
stream.send(newRequest());
@@ -624,7 +628,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.INVALID_ARGUMENT);
});
}
@@ -633,12 +637,12 @@ describe('RPC', () => {
it('non-blocking call server error after stream end', async () => {
for (let i = 0; i < 3; i++) {
const stream = clientStreaming.invoke();
- // Error will be sent in response to the CLIENT_STREAM_END packet.
+ // Error will be sent in response to the CLIENT_REQUEST_COMPLETION packet.
enqueueError(
1,
clientStreaming.method,
Status.INVALID_ARGUMENT,
- Status.OK
+ Status.OK,
);
await stream
@@ -646,7 +650,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.INVALID_ARGUMENT);
});
}
@@ -659,8 +663,7 @@ describe('RPC', () => {
try {
stream.send(newRequest());
- }
- catch (e) {
+ } catch (e) {
console.log(e);
expect(e.status).toEqual(Status.CANCELLED);
}
@@ -675,7 +678,7 @@ describe('RPC', () => {
1,
clientStreaming.method,
Status.UNAVAILABLE,
- enqueuedResponse
+ enqueuedResponse,
);
const stream = clientStreaming.invoke();
@@ -696,7 +699,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.UNAVAILABLE);
expect(stream.error).toEqual(Status.UNAVAILABLE);
expect(stream.response).toBeUndefined();
@@ -721,8 +724,8 @@ describe('RPC', () => {
bidiStreaming = client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeBidiStreaming'
- )! as BidirectionalStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeBidiStreaming',
+ ) as BidirectionalStreamingMethodStub;
});
it('blocking call', async () => {
@@ -745,7 +748,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.NOT_FOUND);
});
});
@@ -756,7 +759,7 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
const testResponses: Array<Message> = [];
- const stream = bidiStreaming.invoke(response => {
+ const stream = bidiStreaming.invoke((response) => {
testResponses.push(response);
});
expect(stream.completed).toBe(false);
@@ -825,7 +828,7 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
const testResponses: Array<Message> = [];
- const stream = bidiStreaming.invoke(response => {
+ const stream = bidiStreaming.invoke((response) => {
testResponses.push(response);
});
expect(stream.completed).toBe(false);
@@ -849,7 +852,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.OUT_OF_RANGE);
});
}
@@ -858,12 +861,12 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
const stream = bidiStreaming.invoke();
- // Error is sent in response to CLIENT_STREAM_END packet.
+ // Error is sent in response to CLIENT_REQUEST_COMPLETION packet.
enqueueError(
1,
bidiStreaming.method,
Status.INVALID_ARGUMENT,
- Status.OK
+ Status.OK,
);
await stream
@@ -871,7 +874,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.INVALID_ARGUMENT);
});
}
@@ -915,7 +918,7 @@ describe('RPC', () => {
.then(() => {
fail('Promise should not be resolved');
})
- .catch(reason => {
+ .catch((reason) => {
expect(reason.status).toEqual(Status.UNAVAILABLE);
expect(stream.error).toEqual(Status.UNAVAILABLE);
});
diff --git a/pw_rpc/ts/descriptors.ts b/pw_rpc/ts/descriptors.ts
index eb4581f3e..bd0d470de 100644
--- a/pw_rpc/ts/descriptors.ts
+++ b/pw_rpc/ts/descriptors.ts
@@ -12,13 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
+import { ProtoCollection } from 'pigweedjs/pw_protobuf_compiler';
import {
MethodDescriptorProto,
ServiceDescriptorProto,
} from 'google-protobuf/google/protobuf/descriptor_pb';
-import {hash} from './hash';
+import { hash } from './hash';
interface ChannelOutput {
(data: Uint8Array): void;
@@ -28,7 +28,12 @@ export class Channel {
readonly id: number;
private output: ChannelOutput;
- constructor(id: number, output: ChannelOutput = () => {}) {
+ constructor(
+ id: number,
+ output: ChannelOutput = () => {
+ /* do nothing. */
+ },
+ ) {
this.id = id;
this.output = output;
}
@@ -48,7 +53,7 @@ export class Service {
constructor(
descriptor: ServiceDescriptorProto,
protoCollection: ProtoCollection,
- packageName: string
+ packageName: string,
) {
this.name = packageName + '.' + descriptor.getName()!;
this.id = hash(this.name);
@@ -83,7 +88,7 @@ export class Method {
constructor(
descriptor: MethodDescriptorProto,
protoCollection: ProtoCollection,
- service: Service
+ service: Service,
) {
this.name = descriptor.getName()!;
this.id = hash(this.name);
@@ -97,10 +102,10 @@ export class Method {
// Remove leading period if it exists.
this.requestType = protoCollection.getMessageCreator(
- requestTypePath.replace(/^\./, '')
+ requestTypePath.replace(/^\./, ''),
)!;
this.responseType = protoCollection.getMessageCreator(
- responseTypePath.replace(/^\./, '')
+ responseTypePath.replace(/^\./, ''),
)!;
}
diff --git a/pw_rpc/ts/descriptors_test.ts b/pw_rpc/ts/descriptors_test.ts
index a6b11e26a..c09d3d3fa 100644
--- a/pw_rpc/ts/descriptors_test.ts
+++ b/pw_rpc/ts/descriptors_test.ts
@@ -14,11 +14,8 @@
/* eslint-env browser */
-import {ProtoCollection} from 'pigweedjs/protos/collection';
-import {
- Request,
- Response,
-} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
+import { Request, Response } from 'pigweedjs/protos/pw_rpc/ts/test_pb';
import * as descriptors from './descriptors';
@@ -27,13 +24,14 @@ const TEST_PROTO_PATH = 'pw_rpc/ts/test_protos-descriptor-set.proto.bin';
describe('Descriptors', () => {
it('parses from ServiceDescriptor binary', async () => {
const protoCollection = new ProtoCollection();
- const fd = protoCollection.fileDescriptorSet.getFileList()
- .find((file: any) => file.array[1].indexOf("pw.rpc.test1") !== -1);
+ const fd = protoCollection.fileDescriptorSet
+ .getFileList()
+ .find((file: any) => file.array[1].indexOf('pw.rpc.test1') !== -1);
const sd = fd.getServiceList()[0];
const service = new descriptors.Service(
sd,
protoCollection,
- fd.getPackage()!
+ fd.getPackage()!,
);
expect(service.name).toEqual('pw.rpc.test1.TheTestService');
diff --git a/pw_rpc/ts/method.ts b/pw_rpc/ts/method.ts
index 18cf6db07..f67d9cfb0 100644
--- a/pw_rpc/ts/method.ts
+++ b/pw_rpc/ts/method.ts
@@ -12,8 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Status} from 'pigweedjs/pw_status';
-import {Message} from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
import {
BidirectionalStreamingCall,
@@ -23,13 +23,13 @@ import {
ServerStreamingCall,
UnaryCall,
} from './call';
-import {Channel, Method, MethodType, Service} from './descriptors';
-import {PendingCalls, Rpc} from './rpc_classes';
+import { Channel, Method, MethodType, Service } from './descriptors';
+import { PendingCalls, Rpc } from './rpc_classes';
export function methodStubFactory(
rpcs: PendingCalls,
channel: Channel,
- method: Method
+ method: Method,
): MethodStub {
switch (method.type) {
case MethodType.BIDIRECTIONAL_STREAMING:
@@ -64,16 +64,22 @@ export abstract class MethodStub {
export class UnaryMethodStub extends MethodStub {
invoke(
request: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): UnaryCall {
const call = new UnaryCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request);
return call;
@@ -81,16 +87,22 @@ export class UnaryMethodStub extends MethodStub {
open(
request: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): UnaryCall {
const call = new UnaryCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request, true);
return call;
@@ -104,16 +116,22 @@ export class UnaryMethodStub extends MethodStub {
export class ServerStreamingMethodStub extends MethodStub {
invoke(
request?: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): ServerStreamingCall {
const call = new ServerStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request);
return call;
@@ -121,16 +139,22 @@ export class ServerStreamingMethodStub extends MethodStub {
open(
request: Message,
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): UnaryCall {
const call = new UnaryCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(request, true);
return call;
@@ -143,32 +167,44 @@ export class ServerStreamingMethodStub extends MethodStub {
export class ClientStreamingMethodStub extends MethodStub {
invoke(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): ClientStreamingCall {
const call = new ClientStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke();
return call;
}
open(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): ClientStreamingCall {
const call = new ClientStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(undefined, true);
return call;
@@ -181,32 +217,44 @@ export class ClientStreamingMethodStub extends MethodStub {
export class BidirectionalStreamingMethodStub extends MethodStub {
invoke(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): BidirectionalStreamingCall {
const call = new BidirectionalStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke();
return call;
}
open(
- onNext: Callback = () => {},
- onCompleted: Callback = () => {},
- onError: Callback = () => {}
+ onNext: Callback = () => {
+ // Do nothing.
+ },
+ onCompleted: Callback = () => {
+ // Do nothing.
+ },
+ onError: Callback = () => {
+ // Do nothing.
+ },
): BidirectionalStreamingCall {
const call = new BidirectionalStreamingCall(
this.rpcs,
this.rpc,
onNext,
onCompleted,
- onError
+ onError,
);
call.invoke(undefined, true);
return call;
diff --git a/pw_rpc/ts/packets.ts b/pw_rpc/ts/packets.ts
index 68cf80393..b6cb71395 100644
--- a/pw_rpc/ts/packets.ts
+++ b/pw_rpc/ts/packets.ts
@@ -14,33 +14,34 @@
/** Functions for working with pw_rpc packets. */
-import {Message} from 'google-protobuf';
-import {MethodDescriptorProto} from 'google-protobuf/google/protobuf/descriptor_pb';
-import * as packetPb from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Status} from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
+import {
+ RpcPacket,
+ PacketType,
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import { Status } from 'pigweedjs/pw_status';
// Channel, Service, Method
type idSet = [number, number, number];
-export function decode(data: Uint8Array): packetPb.RpcPacket {
- return packetPb.RpcPacket.deserializeBinary(data);
+export function decode(data: Uint8Array): RpcPacket {
+ return RpcPacket.deserializeBinary(data);
}
-export function decodePayload(payload: Uint8Array, payloadType: any): Message {
- const message = payloadType.deserializeBinary(payload);
- return message;
+export function decodePayload(payload: Uint8Array, payloadType: any): any {
+ return payloadType['deserializeBinary'](payload);
}
-export function forServer(packet: packetPb.RpcPacket): boolean {
+export function forServer(packet: RpcPacket): boolean {
return packet.getType() % 2 == 0;
}
export function encodeClientError(
- packet: packetPb.RpcPacket,
- status: Status
+ packet: RpcPacket,
+ status: Status,
): Uint8Array {
- const errorPacket = new packetPb.RpcPacket();
- errorPacket.setType(packetPb.PacketType.CLIENT_ERROR);
+ const errorPacket = new RpcPacket();
+ errorPacket.setType(PacketType.CLIENT_ERROR);
errorPacket.setChannelId(packet.getChannelId());
errorPacket.setMethodId(packet.getMethodId());
errorPacket.setServiceId(packet.getServiceId());
@@ -49,18 +50,19 @@ export function encodeClientError(
}
export function encodeClientStream(ids: idSet, message: Message): Uint8Array {
- const streamPacket = new packetPb.RpcPacket();
- streamPacket.setType(packetPb.PacketType.CLIENT_STREAM);
+ const streamPacket = new RpcPacket();
+ streamPacket.setType(PacketType.CLIENT_STREAM);
streamPacket.setChannelId(ids[0]);
streamPacket.setServiceId(ids[1]);
streamPacket.setMethodId(ids[2]);
- streamPacket.setPayload(message.serializeBinary());
+ const msgSerialized = (message as any)['serializeBinary']();
+ streamPacket.setPayload(msgSerialized);
return streamPacket.serializeBinary();
}
export function encodeClientStreamEnd(ids: idSet): Uint8Array {
- const streamEnd = new packetPb.RpcPacket();
- streamEnd.setType(packetPb.PacketType.CLIENT_STREAM_END);
+ const streamEnd = new RpcPacket();
+ streamEnd.setType(PacketType.CLIENT_REQUEST_COMPLETION);
streamEnd.setChannelId(ids[0]);
streamEnd.setServiceId(ids[1]);
streamEnd.setMethodId(ids[2]);
@@ -70,11 +72,11 @@ export function encodeClientStreamEnd(ids: idSet): Uint8Array {
export function encodeRequest(ids: idSet, request?: Message): Uint8Array {
const payload: Uint8Array =
typeof request !== 'undefined'
- ? request.serializeBinary()
- : new Uint8Array();
+ ? (request as any)['serializeBinary']()
+ : new Uint8Array(0);
- const packet = new packetPb.RpcPacket();
- packet.setType(packetPb.PacketType.REQUEST);
+ const packet = new RpcPacket();
+ packet.setType(PacketType.REQUEST);
packet.setChannelId(ids[0]);
packet.setServiceId(ids[1]);
packet.setMethodId(ids[2]);
@@ -83,18 +85,19 @@ export function encodeRequest(ids: idSet, request?: Message): Uint8Array {
}
export function encodeResponse(ids: idSet, response: Message): Uint8Array {
- const packet = new packetPb.RpcPacket();
- packet.setType(packetPb.PacketType.RESPONSE);
+ const packet = new RpcPacket();
+ packet.setType(PacketType.RESPONSE);
packet.setChannelId(ids[0]);
packet.setServiceId(ids[1]);
packet.setMethodId(ids[2]);
- packet.setPayload(response.serializeBinary());
+ const msgSerialized = (response as any)['serializeBinary']();
+ packet.setPayload(msgSerialized);
return packet.serializeBinary();
}
export function encodeCancel(ids: idSet): Uint8Array {
- const packet = new packetPb.RpcPacket();
- packet.setType(packetPb.PacketType.CLIENT_ERROR);
+ const packet = new RpcPacket();
+ packet.setType(PacketType.CLIENT_ERROR);
packet.setStatus(Status.CANCELLED);
packet.setChannelId(ids[0]);
packet.setServiceId(ids[1]);
diff --git a/pw_rpc/ts/packets_test.ts b/pw_rpc/ts/packets_test.ts
index b399e2578..50cdb3f1a 100644
--- a/pw_rpc/ts/packets_test.ts
+++ b/pw_rpc/ts/packets_test.ts
@@ -17,7 +17,7 @@ import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Status} from 'pigweedjs/pw_status';
+import { Status } from 'pigweedjs/pw_status';
import * as packets from './packets';
@@ -31,7 +31,9 @@ function addTestData(packet: RpcPacket) {
}
describe('Packets', () => {
- beforeEach(() => { });
+ beforeEach(() => {
+ // Do nothing.
+ });
it('encodeRequest sets packet fields', () => {
const goldenRequest = new RpcPacket();
@@ -95,7 +97,7 @@ describe('Packets', () => {
addTestData(request);
expect(request.toObject()).toEqual(
- packets.decode(request.serializeBinary()).toObject()
+ packets.decode(request.serializeBinary()).toObject(),
);
});
diff --git a/pw_rpc/ts/queue.ts b/pw_rpc/ts/queue.ts
index 0bd2b8c6c..44dd48cbd 100644
--- a/pw_rpc/ts/queue.ts
+++ b/pw_rpc/ts/queue.ts
@@ -32,7 +32,7 @@ export default class Queue<T> {
}
shift(): Promise<T> {
- return new Promise(resolve => {
+ return new Promise((resolve) => {
if (this.length > 0) {
return resolve(this.queue.shift()!);
} else {
diff --git a/pw_rpc/ts/rpc_classes.ts b/pw_rpc/ts/rpc_classes.ts
index 703612e19..83001e268 100644
--- a/pw_rpc/ts/rpc_classes.ts
+++ b/pw_rpc/ts/rpc_classes.ts
@@ -12,11 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Message} from 'google-protobuf';
-import {Status} from 'pigweedjs/pw_status';
+import { Message } from 'google-protobuf';
+import { Status } from 'pigweedjs/pw_status';
-import {Call} from './call';
-import {Channel, Method, Service} from './descriptors';
+import { Call } from './call';
+import { Channel, Method, Service } from './descriptors';
import * as packets from './packets';
/** Data class for a pending RPC call. */
@@ -70,7 +70,7 @@ export class PendingCalls {
rpc: Rpc,
call: Call,
ignoreError: boolean,
- request?: Message
+ request?: Message,
): Call | undefined {
const previous = this.open(rpc, call);
const packet = packets.encodeRequest(rpc.idSet, request);
diff --git a/pw_rpc/unary_rpc.svg b/pw_rpc/unary_rpc.svg
deleted file mode 100644
index 1a0a28d24..000000000
--- a/pw_rpc/unary_rpc.svg
+++ /dev/null
@@ -1,47 +0,0 @@
-<!-- Originally created with blockdiag. See the Git history for the source. -->
-<svg height="408.1" viewBox="0 0 534 371" width="587.4000000000001" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" style="background-color:white">
-<defs id="defs_block">
-<filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
-<feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2"></feGaussianBlur>
-</filter>
-</defs>
-<title>Unary RPC</title>
-<desc></desc>
-<rect fill="rgb(0,0,0)" height="190" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="171" y="159"></rect>
-<rect fill="rgb(0,0,0)" height="114" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="363" y="159"></rect>
-<polygon fill="rgb(0,0,0)" points="27,126 151,126 159,134 159,193 27,193 27,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<polygon fill="rgb(0,0,0)" points="383,233 513,233 521,241 521,313 383,313 383,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1"></polygon>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="111" y="46"></rect>
-<rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="303" y="46"></rect>
-<path d="M 172 80 L 172 359" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="190" stroke="rgb(0,0,0)" width="8" x="168" y="153"></rect>
-<path d="M 364 80 L 364 359" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4"></path>
-<rect fill="moccasin" height="114" stroke="rgb(0,0,0)" width="8" x="360" y="153"></rect>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="108" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="172.0" y="66">client</text>
-<rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="300" y="40"></rect>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="364.0" y="66">server</text>
-<path d="M 180 153 L 356 153" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="348,149 356,153 348,157" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="24,120 148,120 156,128 156,187 24,187 24,120" stroke="rgb(0,0,0)"></polygon>
-<path d="M 148 120 L 148 128" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 148 128 L 156 128" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="108" x="86.0" y="133">PacketType.REQUEST</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="146">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="62.0" y="159">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="59.0" y="172">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="53.0" y="185">payload</text>
-<path d="M 180 267 L 356 267" fill="none" stroke="rgb(0,0,0)"></path>
-<polygon fill="rgb(0,0,0)" points="188,263 180,267 188,271" stroke="rgb(0,0,0)"></polygon>
-<polygon fill="rgb(240,248,255)" points="380,227 510,227 518,235 518,307 380,307 380,227" stroke="rgb(0,0,0)"></polygon>
-<path d="M 510 227 L 510 235" fill="none" stroke="rgb(0,0,0)"></path>
-<path d="M 510 235 L 518 235" fill="none" stroke="rgb(0,0,0)"></path>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="114" x="445.0" y="240">PacketType.RESPONSE</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="253">channel ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="60" x="418.0" y="266">service ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="54" x="415.0" y="279">method ID</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="409.0" y="292">payload</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="36" x="406.0" y="305">status</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="42" x="205.0" y="151">request</text>
-<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="48" x="332.0" y="265">response</text>
-</svg>
diff --git a/pw_rpc/unary_rpc_cancelled.svg b/pw_rpc/unary_rpc_cancelled.svg
deleted file mode 100644
index c253780f8..000000000
--- a/pw_rpc/unary_rpc_cancelled.svg
+++ /dev/null
@@ -1,59 +0,0 @@
-<!-- Originally created with blockdiag. -->
-<svg viewBox="0 0 524 358" xmlns="http://www.w3.org/2000/svg" xmlns:inkspace="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" width="524px" height="358px">
- <defs id="defs_block">
- <filter height="1.504" id="filter_blur" inkspace:collect="always" width="1.1575" x="-0.07875" y="-0.252">
- <feGaussianBlur id="feGaussianBlur3780" inkspace:collect="always" stdDeviation="4.2" />
- </filter>
- </defs>
- <title>Cancelled Unary RPC</title>
- <desc>seqdiag {
- default_note_color = aliceblue;
-
- client -&gt; server [
- label = "request",
- leftnote = "PacketType.REQUEST\nchannel ID\nservice ID\nmethod ID\npayload"
- ];
-
- client -&gt; server [
- noactivate,
- label = "cancel",
- leftnote = "PacketType.CLIENT_ERROR\nchannel ID\nservice ID\nmethod ID\nstatus=CANCELLED"
- ];
-}</desc>
- <rect fill="rgb(0,0,0)" height="177" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="203" y="159" />
- <rect fill="rgb(0,0,0)" height="177" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="8" x="395" y="159" />
- <polygon fill="rgb(0,0,0)" points="58,126 183,126 191,134 191,193 58,193 58,126" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <polygon fill="rgb(0,0,0)" points="27,233 183,233 191,241 191,300 27,300 27,233" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="143" y="46" />
- <rect fill="rgb(0,0,0)" height="40" stroke="rgb(0,0,0)" style="filter:url(#filter_blur);opacity:0.7;fill-opacity:1" width="128" x="335" y="46" />
- <path d="M 204 80 L 204 346" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="177" stroke="rgb(0,0,0)" width="8" x="200" y="153" />
- <path d="M 396 80 L 396 346" fill="none" stroke="rgb(0,0,0)" stroke-dasharray="8 4" />
- <rect fill="moccasin" height="177" stroke="rgb(0,0,0)" width="8" x="392" y="153" />
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="140" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="204.5" y="66">client</text>
- <rect fill="rgb(255,255,255)" height="40" stroke="rgb(0,0,0)" width="128" x="332" y="40" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="396.5" y="66">server</text>
- <path d="M 212 153 L 388 153" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,149 388,153 380,157" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="55,120 180,120 188,128 188,187 55,187 55,120" stroke="rgb(0,0,0)" />
- <path d="M 180 120 L 180 128" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 128 L 188 128" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="109" x="117.5" y="133">PacketType.REQUEST</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="146">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="93.5" y="159">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="90.5" y="172">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="84.5" y="185">payload</text>
- <path d="M 212 260 L 388 260" fill="none" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(0,0,0)" points="380,256 388,260 380,264" stroke="rgb(0,0,0)" />
- <polygon fill="rgb(240,248,255)" points="24,227 180,227 188,235 188,294 24,294 24,227" stroke="rgb(0,0,0)" />
- <path d="M 180 227 L 180 235" fill="none" stroke="rgb(0,0,0)" />
- <path d="M 180 235 L 188 235" fill="none" stroke="rgb(0,0,0)" />
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="102.0" y="240">PacketType.CLIENT_ERROR</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="253">channel ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="61" x="62.5" y="266">service ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="59.5" y="279">method ID</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="97" x="80.5" y="292">status=CANCELLED</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="43" x="237.5" y="151">request</text>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="234.5" y="258">cancel</text>
-</svg>
diff --git a/pw_rpc_transport/Android.bp b/pw_rpc_transport/Android.bp
new file mode 100644
index 000000000..240d19e9e
--- /dev/null
+++ b/pw_rpc_transport/Android.bp
@@ -0,0 +1,153 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_rpc_transport_rpc_transport_headers",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_function_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_function_headers",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_status",
+ ],
+ export_static_lib_headers: [
+ "pw_bytes",
+ "pw_status",
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
+
+cc_library_static {
+ name: "pw_rpc_transport_local_rpc_egress",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ header_libs: [
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_rpc_transport_rpc_transport_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_log_headers",
+ "pw_result_headers",
+ "pw_rpc_transport_rpc_transport_headers",
+ ],
+ static_libs: [
+ "pw_containers",
+ ],
+ export_static_lib_headers: [
+ "pw_containers",
+ ],
+ srcs: [
+ "local_rpc_egress.cc",
+ ],
+}
+
+cc_library_static {
+ name: "pw_rpc_transport_simple_framing",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_rpc_transport_rpc_transport_headers",
+ "pw_span_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_result_headers",
+ "pw_rpc_transport_rpc_transport_headers",
+ "pw_span_headers",
+ ],
+ srcs: [
+ "simple_framing.cc",
+ ],
+}
+
+filegroup {
+ name: "pw_rpc_transport_egress_ingress_src_files",
+ srcs: [
+ "egress_ingress.cc",
+ ],
+}
+
+// This rule must be instantiated, e.g.
+//
+// cc_library_static {
+// name: "<instance_name>",
+// cflags: [
+// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
+// "-DPW_RPC_COMPLETION_REQUEST_CALLBACK",
+// "-DPW_RPC_DYNAMIC_ALLOCATION",
+// ],
+// defaults: [
+// "pw_rpc_transport_egress_ingress_defaults",
+// ],
+// }
+cc_defaults {
+ name: "pw_rpc_transport_egress_ingress_defaults",
+ cpp_std: "c++20",
+ defaults: [
+ "pw_chrono_defaults",
+ "pw_chrono_stl_defaults",
+ "pw_rpc_defaults",
+ "pw_sync_stl_defaults",
+ "pw_thread_defaults",
+ "pw_thread_stl_defaults",
+ ],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_rpc_transport_rpc_transport_headers",
+ "pw_sync_headers",
+ "pw_sync_binary_semaphore_thread_notification_backend_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_rpc_transport_rpc_transport_headers",
+ "pw_sync_headers",
+ ],
+ static_libs: [
+ "pw_hdlc",
+ "pw_metric",
+ ],
+ export_static_lib_headers: [
+ "pw_hdlc",
+ "pw_metric",
+ ],
+ srcs: [
+ ":pw_rpc_transport_egress_ingress_src_files",
+ ],
+}
diff --git a/pw_rpc_transport/BUILD.bazel b/pw_rpc_transport/BUILD.bazel
new file mode 100644
index 000000000..2fa24197e
--- /dev/null
+++ b/pw_rpc_transport/BUILD.bazel
@@ -0,0 +1,338 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
+load(
+ "//pw_build:selects.bzl",
+ "TARGET_COMPATIBLE_WITH_HOST_SELECT",
+)
+load(
+ "//pw_protobuf_compiler:pw_proto_library.bzl",
+ "pw_proto_filegroup",
+ "pwpb_proto_library",
+ "pwpb_rpc_proto_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "rpc_transport",
+ hdrs = ["public/pw_rpc_transport/rpc_transport.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_bytes",
+ "//pw_function",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "service_registry",
+ hdrs = ["public/pw_rpc_transport/service_registry.h"],
+ includes = ["public"],
+ deps = [
+ ":rpc_transport",
+ "//pw_rpc:client_server",
+ "//pw_span",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "test_loopback_service_registry",
+ hdrs = ["public/pw_rpc_transport/test_loopback_service_registry.h"],
+ includes = ["public"],
+ deps = [
+ ":egress_ingress",
+ ":service_registry",
+ "//pw_work_queue",
+ "//pw_work_queue:test_thread_header",
+ ],
+)
+
+pw_cc_library(
+ name = "packet_buffer_queue",
+ hdrs = ["public/pw_rpc_transport/internal/packet_buffer_queue.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_containers",
+ "//pw_log",
+ "//pw_result",
+ "//pw_status",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ ],
+)
+
+pw_cc_test(
+ name = "packet_buffer_queue_test",
+ srcs = [
+ "internal/packet_buffer_queue_test.cc",
+ ],
+ deps = [
+ ":packet_buffer_queue",
+ "//pw_bytes",
+ "//pw_containers",
+ "//pw_result",
+ "//pw_status",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ ],
+)
+
+pw_cc_library(
+ name = "local_rpc_egress",
+ srcs = ["local_rpc_egress.cc"],
+ hdrs = ["public/pw_rpc_transport/local_rpc_egress.h"],
+ includes = ["public"],
+ deps = [
+ ":packet_buffer_queue",
+ ":rpc_transport",
+ ":test_protos_pwpb_rpc",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_result",
+ "//pw_rpc:client_server",
+ "//pw_status",
+ "//pw_sync:thread_notification",
+ "//pw_thread:thread_core",
+ ],
+)
+
+pw_cc_test(
+ name = "local_rpc_egress_test",
+ srcs = [
+ "local_rpc_egress_test.cc",
+ ],
+ deps = [
+ ":local_rpc_egress",
+ ":rpc_transport",
+ ":service_registry",
+ ":test_protos_pwpb_rpc",
+ "//pw_bytes",
+ "//pw_chrono:system_clock",
+ "//pw_rpc:client_server",
+ "//pw_status",
+ "//pw_sync:counting_semaphore",
+ "//pw_sync:thread_notification",
+ "//pw_thread:sleep",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_library(
+ name = "hdlc_framing",
+ hdrs = [
+ "public/pw_rpc_transport/hdlc_framing.h",
+ ],
+ deps = [
+ ":rpc_transport",
+ "//pw_bytes",
+ "//pw_hdlc",
+ "//pw_hdlc:pw_rpc",
+ "//pw_result",
+ "//pw_status",
+ "//pw_stream",
+ ],
+)
+
+pw_cc_test(
+ name = "hdlc_framing_test",
+ srcs = [
+ "hdlc_framing_test.cc",
+ ],
+ deps = [
+ ":hdlc_framing",
+ "//pw_bytes",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "simple_framing",
+ srcs = [
+ "simple_framing.cc",
+ ],
+ hdrs = ["public/pw_rpc_transport/simple_framing.h"],
+ deps = [
+ ":rpc_transport",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_status",
+ ],
+)
+
+pw_cc_test(
+ name = "simple_framing_test",
+ srcs = ["simple_framing_test.cc"],
+ deps = [
+ ":simple_framing",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "egress_ingress",
+ srcs = [
+ "egress_ingress.cc",
+ ],
+ hdrs = ["public/pw_rpc_transport/egress_ingress.h"],
+ deps = [
+ ":hdlc_framing",
+ ":rpc_transport",
+ ":simple_framing",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_metric:metric",
+ "//pw_rpc:client_server",
+ "//pw_status",
+ "//pw_sync:mutex",
+ ],
+)
+
+pw_cc_test(
+ name = "egress_ingress_test",
+ srcs = ["egress_ingress_test.cc"],
+ deps = [
+ ":egress_ingress",
+ ":service_registry",
+ ":test_protos_pwpb_rpc",
+ "//pw_bytes",
+ "//pw_metric:metric",
+ "//pw_status",
+ "//pw_sync:thread_notification",
+ ],
+)
+
+pw_cc_library(
+ name = "socket_rpc_transport",
+ srcs = ["socket_rpc_transport.cc"],
+ hdrs = ["public/pw_rpc_transport/socket_rpc_transport.h"],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":rpc_transport",
+ "//pw_assert",
+ "//pw_chrono:system_clock",
+ "//pw_log",
+ "//pw_status",
+ "//pw_stream",
+ "//pw_stream:socket_stream",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ "//pw_sync:thread_notification",
+ "//pw_sync_stl:condition_variable",
+ "//pw_thread:sleep",
+ "//pw_thread:thread_core",
+ ],
+)
+
+pw_cc_library(
+ name = "stream_rpc_frame_sender",
+ hdrs = ["public/pw_rpc_transport/stream_rpc_frame_sender.h"],
+ deps = [
+ ":rpc_transport",
+ "//pw_status",
+ "//pw_stream",
+ ],
+)
+
+pw_cc_library(
+ name = "stream_rpc_dispatcher",
+ hdrs = ["public/pw_rpc_transport/stream_rpc_dispatcher.h"],
+ deps = [
+ ":egress_ingress",
+ "//pw_metric:metric",
+ "//pw_status",
+ "//pw_stream",
+ ],
+)
+
+pw_cc_test(
+ name = "socket_rpc_transport_test",
+ srcs = ["socket_rpc_transport_test.cc"],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":socket_rpc_transport",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_status",
+ "//pw_sync:mutex",
+ "//pw_sync:thread_notification",
+ "//pw_thread:sleep",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_test(
+ name = "stream_rpc_dispatcher_test",
+ srcs = ["stream_rpc_dispatcher_test.cc"],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":stream_rpc_dispatcher",
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_status",
+ "//pw_sync:thread_notification",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_test(
+ name = "rpc_integration_test",
+ srcs = ["rpc_integration_test.cc"],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":egress_ingress",
+ ":local_rpc_egress",
+ ":service_registry",
+ ":socket_rpc_transport",
+ ":test_protos_pwpb_rpc",
+ "//pw_chrono:system_clock",
+ "//pw_log",
+ "//pw_rpc:client_server",
+ "//pw_rpc:synchronous_client_api",
+ "//pw_string",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_proto_filegroup(
+ name = "test_protos_and_options",
+ srcs = ["internal/test.proto"],
+ options_files = ["internal/test.options"],
+)
+
+proto_library(
+ name = "test_protos",
+ srcs = [":test_protos_and_options"],
+)
+
+pwpb_proto_library(
+ name = "test_protos_pwpb",
+ deps = [":test_protos"],
+)
+
+pwpb_rpc_proto_library(
+ name = "test_protos_pwpb_rpc",
+ pwpb_proto_library_deps = [":test_protos_pwpb"],
+ deps = [":test_protos"],
+)
diff --git a/pw_rpc_transport/BUILD.gn b/pw_rpc_transport/BUILD.gn
new file mode 100644
index 000000000..575bc51fe
--- /dev/null
+++ b/pw_rpc_transport/BUILD.gn
@@ -0,0 +1,314 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":egress_ingress_test",
+ ":hdlc_framing_test",
+ ":local_rpc_egress_test",
+ ":packet_buffer_queue_test",
+ ":rpc_integration_test",
+ ":simple_framing_test",
+ ":socket_rpc_transport_test",
+ ":stream_rpc_dispatcher_test",
+ ]
+}
+
+pw_source_set("rpc_transport") {
+ public = [ "public/pw_rpc_transport/rpc_transport.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_bytes",
+ "$dir_pw_function",
+ "$dir_pw_status",
+ ]
+}
+
+pw_source_set("service_registry") {
+ public = [ "public/pw_rpc_transport/service_registry.h" ]
+ public_deps = [
+ ":rpc_transport",
+ "$dir_pw_rpc:client_server",
+ "$dir_pw_span",
+ "$dir_pw_status",
+ ]
+}
+
+pw_source_set("test_loopback_service_registry") {
+ public = [ "public/pw_rpc_transport/test_loopback_service_registry.h" ]
+ public_deps = [
+ ":egress_ingress",
+ ":service_registry",
+ "$dir_pw_work_queue:pw_work_queue",
+ "$dir_pw_work_queue:test_thread",
+ ]
+}
+
+pw_source_set("packet_buffer_queue") {
+ public = [ "public/pw_rpc_transport/internal/packet_buffer_queue.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_bytes",
+ "$dir_pw_containers",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ ]
+ deps = [
+ "$dir_pw_assert:check",
+ "$dir_pw_log",
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_test("packet_buffer_queue_test") {
+ sources = [ "internal/packet_buffer_queue_test.cc" ]
+ deps = [
+ ":packet_buffer_queue",
+ "$dir_pw_bytes",
+ "$dir_pw_containers",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ ]
+}
+
+pw_source_set("local_rpc_egress") {
+ public = [ "public/pw_rpc_transport/local_rpc_egress.h" ]
+ sources = [ "local_rpc_egress.cc" ]
+ public_deps = [
+ ":packet_buffer_queue",
+ ":rpc_transport",
+ "$dir_pw_bytes",
+ "$dir_pw_result",
+ "$dir_pw_rpc:client",
+ "$dir_pw_status",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_thread:thread_core",
+ ]
+ deps = [ "$dir_pw_log" ]
+}
+
+pw_test("local_rpc_egress_test") {
+ sources = [ "local_rpc_egress_test.cc" ]
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ deps = [
+ ":local_rpc_egress",
+ ":rpc_transport",
+ ":service_registry",
+ ":test_protos.pwpb_rpc",
+ "$dir_pw_bytes",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_rpc:client_server",
+ "$dir_pw_status",
+ "$dir_pw_sync:counting_semaphore",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:thread",
+ "$dir_pw_thread_stl:thread",
+ ]
+}
+
+pw_source_set("hdlc_framing") {
+ public = [ "public/pw_rpc_transport/hdlc_framing.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":rpc_transport",
+ "$dir_pw_bytes",
+ "$dir_pw_hdlc:decoder",
+ "$dir_pw_hdlc:encoder",
+ "$dir_pw_hdlc:pw_rpc",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_stream:pw_stream",
+ ]
+}
+
+pw_test("hdlc_framing_test") {
+ sources = [ "hdlc_framing_test.cc" ]
+ deps = [
+ ":hdlc_framing",
+ "$dir_pw_bytes",
+ "$dir_pw_status",
+ ]
+}
+
+pw_source_set("simple_framing") {
+ public = [ "public/pw_rpc_transport/simple_framing.h" ]
+ public_configs = [ ":public_include_path" ]
+ sources = [ "simple_framing.cc" ]
+ public_deps = [
+ ":rpc_transport",
+ "$dir_pw_assert",
+ "$dir_pw_bytes",
+ "$dir_pw_status",
+ ]
+ deps = [ "$dir_pw_log" ]
+}
+
+pw_test("simple_framing_test") {
+ sources = [ "simple_framing_test.cc" ]
+ deps = [
+ ":simple_framing",
+ "$dir_pw_bytes",
+ "$dir_pw_log",
+ "$dir_pw_status",
+ ]
+}
+
+pw_source_set("egress_ingress") {
+ public = [ "public/pw_rpc_transport/egress_ingress.h" ]
+ sources = [ "egress_ingress.cc" ]
+ public_deps = [
+ ":hdlc_framing",
+ ":rpc_transport",
+ ":simple_framing",
+ "$dir_pw_bytes",
+ "$dir_pw_metric",
+ "$dir_pw_rpc:client",
+ "$dir_pw_status",
+ "$dir_pw_sync:mutex",
+ ]
+ deps = [ "$dir_pw_log" ]
+}
+
+pw_test("egress_ingress_test") {
+ sources = [ "egress_ingress_test.cc" ]
+ enable_if = pw_thread_THREAD_BACKEND != ""
+ deps = [
+ ":egress_ingress",
+ ":service_registry",
+ ":test_protos.pwpb_rpc",
+ "$dir_pw_bytes",
+ "$dir_pw_metric",
+ "$dir_pw_status",
+ "$dir_pw_sync:thread_notification",
+ ]
+}
+
+pw_source_set("socket_rpc_transport") {
+ public = [ "public/pw_rpc_transport/socket_rpc_transport.h" ]
+ sources = [ "socket_rpc_transport.cc" ]
+ public_deps = [
+ ":rpc_transport",
+ "$dir_pw_assert",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_status",
+ "$dir_pw_stream:pw_stream",
+ "$dir_pw_stream:socket_stream",
+ "$dir_pw_sync:condition_variable",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:thread_core",
+ ]
+ deps = [ "$dir_pw_log" ]
+}
+
+pw_source_set("stream_rpc_frame_sender") {
+ public = [ "public/pw_rpc_transport/stream_rpc_frame_sender.h" ]
+ public_deps = [
+ ":rpc_transport",
+ "$dir_pw_status",
+ "$dir_pw_stream:pw_stream",
+ ]
+}
+
+pw_source_set("stream_rpc_dispatcher") {
+ public = [ "public/pw_rpc_transport/stream_rpc_dispatcher.h" ]
+ public_deps = [
+ ":egress_ingress",
+ "$dir_pw_status",
+ "$dir_pw_stream:pw_stream",
+ ]
+}
+
+pw_test("socket_rpc_transport_test") {
+ sources = [ "socket_rpc_transport_test.cc" ]
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ host_os != "win" && pw_sync_CONDITION_VARIABLE_BACKEND != ""
+ deps = [
+ ":socket_rpc_transport",
+ "$dir_pw_bytes",
+ "$dir_pw_log",
+ "$dir_pw_status",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:thread",
+ "$dir_pw_thread_stl:thread",
+ ]
+}
+
+pw_test("stream_rpc_dispatcher_test") {
+ sources = [ "stream_rpc_dispatcher_test.cc" ]
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ deps = [
+ ":stream_rpc_dispatcher",
+ "$dir_pw_bytes",
+ "$dir_pw_log",
+ "$dir_pw_status",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_thread:thread",
+ "$dir_pw_thread_stl:thread",
+ ]
+}
+
+pw_test("rpc_integration_test") {
+ sources = [ "rpc_integration_test.cc" ]
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ host_os != "win" && pw_sync_CONDITION_VARIABLE_BACKEND != ""
+ deps = [
+ ":egress_ingress",
+ ":local_rpc_egress",
+ ":service_registry",
+ ":socket_rpc_transport",
+ ":test_protos.pwpb_rpc",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_log",
+ "$dir_pw_rpc:client_server",
+ "$dir_pw_rpc:synchronous_client_api",
+ "$dir_pw_string",
+ "$dir_pw_thread:thread",
+ "$dir_pw_thread_stl:thread",
+ ]
+}
+
+pw_proto_library("test_protos") {
+ sources = [ "internal/test.proto" ]
+ inputs = [ "internal/test.options" ]
+ prefix = "pw_rpc_transport"
+}
diff --git a/pw_rpc_transport/CMakeLists.txt b/pw_rpc_transport/CMakeLists.txt
new file mode 100644
index 000000000..ee52f3d57
--- /dev/null
+++ b/pw_rpc_transport/CMakeLists.txt
@@ -0,0 +1,221 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
+
+pw_add_library(pw_rpc_transport.rpc_transport INTERFACE
+ HEADERS
+ public/pw_rpc_transport/rpc_transport.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_function
+ pw_status
+)
+
+pw_add_library(pw_rpc_transport.service_registry INTERFACE
+ HEADERS
+ public/pw_rpc_transport/service_registry.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc_transport.rpc_transport
+ pw_rpc.client_server
+ pw_span
+ pw_status
+)
+
+pw_add_library(pw_rpc_transport.test_loopback_service_registry INTERFACE
+ HEADERS
+ public/pw_rpc_transport/test_loopback_service_registry.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc_transport.egress_ingress
+ pw_rpc_transport.service_registry
+ pw_work_queue
+ pw_work_queue.test_thread
+)
+
+pw_add_library(pw_rpc_transport.packet_buffer_queue INTERFACE
+ HEADERS
+ public/pw_rpc_transport/internal/packet_buffer_queue.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_containers
+ pw_result
+ pw_status
+ pw_sync.lock_annotations
+ pw_sync.mutex
+)
+
+pw_add_test(pw_rpc_transport.packet_buffer_queue_test
+ SOURCES
+ internal/packet_buffer_queue_test.cc
+ PRIVATE_DEPS
+ pw_rpc_transport.packet_buffer_queue
+ pw_bytes
+ pw_containers
+ pw_result
+ pw_status
+ pw_sync.lock_annotations
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_rpc_transport
+)
+
+pw_add_library(pw_rpc_transport.local_rpc_egress STATIC
+ HEADERS
+ public/pw_rpc_transport/local_rpc_egress.h
+ SOURCES
+ local_rpc_egress.cc
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc_transport.packet_buffer_queue
+ pw_rpc_transport.rpc_transport
+ pw_bytes
+ pw_result
+ pw_rpc.client
+ pw_status
+ pw_sync.thread_notification
+ pw_thread.thread_core
+ PRIVATE_DEPS
+ pw_log
+)
+
+pw_add_test(pw_rpc_transport.local_rpc_egress_test
+ SOURCES
+ local_rpc_egress_test.cc
+ PRIVATE_DEPS
+ pw_rpc_transport.local_rpc_egress
+ pw_rpc_transport.rpc_transport
+ pw_rpc_transport.service_registry
+ pw_rpc_transport.test_protos.pwpb_rpc
+ pw_bytes
+ pw_chrono.system_clock
+ pw_rpc.client_server
+ pw_status
+ pw_sync.counting_semaphore
+ pw_sync.thread_notification
+ pw_thread.sleep
+ pw_thread.thread
+ GROUPS
+ modules
+ pw_rpc_transport
+)
+
+pw_add_library(pw_rpc_transport.hdlc_framing INTERFACE
+ HEADERS
+ public/pw_rpc_transport/hdlc_framing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc_transport.rpc_transport
+ pw_bytes
+ pw_hdlc.decoder
+ pw_hdlc.encoder
+ pw_hdlc.pw_rpc
+ pw_result
+ pw_status
+ pw_stream
+)
+
+pw_add_test(pw_rpc_transport.hdlc_framing_test
+ SOURCES
+ hdlc_framing_test.cc
+ PRIVATE_DEPS
+ pw_rpc_transport.hdlc_framing
+ pw_bytes
+ pw_status
+ GROUPS
+ modules
+ pw_rpc_transport
+)
+
+pw_add_library(pw_rpc_transport.simple_framing STATIC
+ HEADERS
+ public/pw_rpc_transport/simple_framing.h
+ SOURCES
+ simple_framing.cc
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc_transport.rpc_transport
+ pw_assert
+ pw_bytes
+ pw_status
+ PRIVATE_DEPS
+ pw_log
+)
+
+pw_add_test(pw_rpc_transport.simple_framing_test
+ SOURCES
+ simple_framing_test.cc
+ PRIVATE_DEPS
+ pw_rpc_transport.simple_framing
+ pw_bytes
+ pw_log
+ pw_status
+ GROUPS
+ modules
+ pw_rpc_transport
+)
+
+pw_add_library(pw_rpc_transport.egress_ingress STATIC
+ HEADERS
+ public/pw_rpc_transport/egress_ingress.h
+ SOURCES
+ egress_ingress.cc
+ PUBLIC_DEPS
+ pw_rpc_transport.hdlc_framing
+ pw_rpc_transport.simple_framing
+ pw_bytes
+ pw_metric
+ pw_rpc.client
+ pw_status
+ pw_sync.mutex
+ PRIVATE_DEPS
+ pw_log
+)
+
+pw_add_test(pw_rpc_transport.egress_ingress_test
+ SOURCES
+ egress_ingress_test.cc
+ PRIVATE_DEPS
+ pw_rpc_transport.egress_ingress
+ pw_rpc_transport.service_registry
+ pw_rpc_transport.test_protos.pwpb_rpc
+ pw_bytes
+ pw_metric
+ pw_status
+ pw_sync.thread_notification
+ GROUPS
+ modules
+ pw_rpc_transport
+)
+
+pw_proto_library(pw_rpc_transport.test_protos
+ SOURCES
+ internal/test.proto
+ INPUTS
+ internal/test.options
+ PREFIX
+ pw_rpc_transport
+)
diff --git a/pw_rpc_transport/OWNERS b/pw_rpc_transport/OWNERS
new file mode 100644
index 000000000..8b979b8ca
--- /dev/null
+++ b/pw_rpc_transport/OWNERS
@@ -0,0 +1,2 @@
+cachinchilla@google.com
+frolv@google.com
diff --git a/pw_rpc_transport/docs.rst b/pw_rpc_transport/docs.rst
new file mode 100644
index 000000000..8e4eb2ed1
--- /dev/null
+++ b/pw_rpc_transport/docs.rst
@@ -0,0 +1,249 @@
+.. _module-pw_rpc_transport:
+
+.. warning::
+ This is an experimental module currently under development. APIs and
+ functionality may change at any time.
+
+================
+pw_rpc_transport
+================
+The ``pw_rpc_transport`` provides a transport layer for ``pw_rpc``.
+
+``pw_rpc`` provides a system for defining and invoking remote procedure calls
+(RPCs) on a device. It does not include any transports for sending these RPC
+calls. On a real device there could be multiple ways of inter-process and/or
+inter-core communication: hardware mailboxes, shared memory, network sockets,
+Unix domain sockets. ``pw_rpc_transport`` provides means to implement various
+transports and integrate them with ``pw_rpc`` services.
+
+``pw_rpc_transport`` relies on the assumption that a ``pw_rpc`` channel ID
+uniquely identifies both sides of an RPC conversation. It allows developers to
+define transports, egresses and ingresses for various channel IDs and choose
+what framing will be used to send RPC packets over those transports.
+
+RpcFrame
+--------
+Framed RPC data ready to be sent via ``RpcFrameSender``. Consists of a header
+and a payload. Some RPC transport encodings may not require a header and put
+all of the framed data into the payload (in which case the header can be
+an empty span).
+
+A single RPC packet can be split into multiple ``RpcFrame``'s depending on the
+MTU of the transport.
+
+All frames for an RPC packet are expected to be sent and received in order
+without being interleaved by other packets' frames.
+
+RpcFrameSender
+--------------
+Sends RPC frames over some communication channel (e.g. a hardware mailbox,
+shared memory, or a socket). It exposes its MTU size and generally only knows
+how to send an ``RpcFrame`` of a size that doesn't exceed that MTU.
+
+RpcPacketEncoder / RpcPacketDecoder
+-----------------------------------
+``RpcPacketEncoder`` is used to split and frame an RPC packet.
+``RpcPacketDecoder`` then does the opposite e.g. stitches together received
+frames and removes any framing added by the encoder.
+
+RpcEgressHandler
+----------------
+Provides means of sending an RPC packet to its destination. Typically it ties
+together an ``RpcPacketEncoder`` and ``RpcFrameSender``.
+
+RpcIngressHandler
+-----------------
+Provides means of receiving RPC packets over some transport. Typically it has
+logic for reading RPC frames from some transport (a network connection,
+shared memory, or a hardware mailbox), stitching and decoding them with
+``RpcPacketDecoder`` and passing full RPC packets to their intended processor
+via ``RpcPacketProcessor``.
+
+RpcPacketProcessor
+------------------
+Used by ``RpcIngressHandler`` to send the received RPC packet to its intended
+handler (e.g. a pw_rpc ``Service``).
+
+--------------------
+Creating a transport
+--------------------
+RPC transports implement ``pw::rpc::RpcFrameSender``. The transport exposes its
+maximum transmission unit (MTU) and only knows how to send packets of up to the
+size of that MTU.
+
+.. code-block:: cpp
+
+ class MyRpcTransport : public RpcFrameSender {
+ public:
+ size_t mtu() const override { return 128; }
+
+ Status Send(RpcFrame frame) override {
+ // Send the frame via mailbox, shared memory or some other mechanism...
+ }
+ };
+
+--------------------------
+Integration with pw_stream
+--------------------------
+An RpcFrameSender implementaion that wraps a ``pw::stream::Writer`` is provided
+by ``pw::rpc::StreamRpcFrameSender``. As the stream interface doesn't know
+about MTU's, it's up to the user to select one.
+
+.. code-block:: cpp
+
+ stream::SysIoWriter writer;
+ StreamRpcFrameSender<kMtu> sender(writer);
+
+A thread to feed data to a ``pw::rpc::RpcIngressHandler`` from a
+``pw::stream::Reader`` is provided by ``pw::rpc::StreamRpcDispatcher``.
+
+.. code-block:: cpp
+
+ rpc::HdlcRpcIngress<kMaxRpcPacketSize> hdlc_ingress(...);
+ stream::SysIoReader reader;
+
+ // Feed Hdlc ingress with bytes from sysio.
+ rpc::StreamRpcDispatcher<kMaxSysioRead> sysio_dispatcher(reader,
+ hdlc_ingress);
+
+ thread::DetachedThread(SysioDispatcherThreadOptions(),
+ sysio_dispatcher);
+
+-------------------------------------------
+Using transports: a sample three-node setup
+-------------------------------------------
+
+A transport must be properly registered in order for ``pw_rpc`` to correctly
+route its packets. Below is an example of using a ``SocketRpcTransport`` and
+a (hypothetical) ``SharedMemoryRpcTransport`` to set up RPC connectivity between
+three endpoints.
+
+Node A runs ``pw_rpc`` clients who want to talk to nodes B and C using
+``kChannelAB`` and ``kChannelAC`` respectively. However there is no direct
+connectivity from A to C: only B can talk to C over shared memory while A can
+talk to B over a socket connection. Also, some services on A are self-hosted
+and accessed from the same process on ``kChannelAA``:
+
+.. code-block:: cpp
+
+ // Set up A->B transport over a network socket where B is a server
+ // and A is a client.
+ SocketRpcTransport<kSocketReadBufferSize> a_to_b_transport(
+ SocketRpcTransport<kSocketReadBufferSize>::kAsClient, "localhost",
+ kNodeBPortNumber);
+
+ // LocalRpcEgress handles RPC packets received from other nodes and destined
+ // to this node.
+ LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress;
+ // HdlcRpcEgress applies HDLC framing to all packets outgoing over the A->B
+ // transport.
+ HdlcRpcEgress<kMaxPacketSize> a_to_b_egress("a->b", a_to_b_transport);
+
+ // List of channels for all packets originated locally at A.
+ std::array tx_channels = {
+ // Self-destined packets go directly to local egress.
+ Channel::Create<kChannelAA>(&local_egress),
+ // Packets to B and C go over A->B transport.
+ Channel::Create<kChannelAB>(&a_to_b_egress),
+ Channel::Create<kChannelAC>(&a_to_b_egress),
+ };
+
+ // Here we list all egresses for the packets _incoming_ from B.
+ std::array b_rx_channels = {
+ // Packets on both AB and AC channels are destined locally; hence sending
+ // to the local egress.
+ ChannelEgress{kChannelAB, local_egress},
+ ChannelEgress{kChannelAC, local_egress},
+ };
+
+ // HdlcRpcIngress complements HdlcRpcEgress: all packets received on
+ // `b_rx_channels` are assumed to have HDLC framing.
+ HdlcRpcIngress<kMaxPacketSize> b_ingress(b_rx_channels);
+
+ // Local egress needs to know how to send received packets to their target
+ // pw_rpc service.
+ ServiceRegistry registry(tx_channels);
+ local_egress.set_packet_processor(registry);
+ // Socket transport needs to be aware of what ingress it's handling.
+ a_to_b_transport.set_ingress(b_ingress);
+
+ // Both RpcSocketTransport and LocalRpcEgress are ThreadCore's and
+ // need to be started in order for packet processing to start.
+ DetachedThread(/*...*/, a_to_b_transport);
+ DetachedThread(/*...*/, local_egress);
+
+Node B setup is the most complicated since it needs to deal with egress
+and ingress from both A and B and needs to support two kinds of transports. Note
+that A is unaware of which transport and framing B is using when talking to C:
+
+.. code-block:: cpp
+
+ // This is the server counterpart to A's client socket.
+ SocketRpcTransport<kSocketReadBufferSize> b_to_a_transport(
+ SocketRpcTransport<kSocketReadBufferSize>::kAsServer, "localhost",
+ kNodeBPortNumber);
+
+ SharedMemoryRpcTransport b_to_c_transport(/*...*/);
+
+ LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress;
+ HdlcRpcEgress<kMaxPacketSize> b_to_a_egress("b->a", b_to_a_transport);
+ // SimpleRpcEgress applies a very simple length-prefixed framing to B->C
+ // traffic (because HDLC adds unnecessary overhead over shared memory).
+ SimpleRpcEgress<kMaxPacketSize> b_to_c_egress("b->c", b_to_c_transport);
+
+ // List of channels for all packets originated locally at B (note that in
+ // this example B doesn't need to talk to C directly; it only proxies for A).
+ std::array tx_channels = {
+ Channel::Create<kChannelAB>(&b_to_a_egress),
+ };
+
+ // Here we list all egresses for the packets _incoming_ from A.
+ std::array a_rx_channels = {
+ ChannelEgress{kChannelAB, local_egress},
+ ChannelEgress{kChannelAC, b_to_c_egress},
+ };
+
+ // Here we list all egresses for the packets _incoming_ from C.
+ std::array c_rx_channels = {
+ ChannelEgress{kChannelAC, b_to_a_egress},
+ };
+
+ HdlcRpcIngress<kMaxPacketSize> b_ingress(b_rx_channels);
+ SimpleRpcIngress<kMaxPacketSize> c_ingress(c_rx_channels);
+
+ ServiceRegistry registry(tx_channels);
+ local_egress.set_packet_processor(registry);
+
+ b_to_a_transport.set_ingress(a_ingress);
+ b_to_c_transport.set_ingress(c_ingress);
+
+ DetachedThread({}, b_to_a_transport);
+ DetachedThread({}, b_to_c_transport);
+ DetachedThread({}, local_egress);
+
+Node C setup is straightforward since it only needs to handle ingress from B:
+
+.. code-block:: cpp
+
+ SharedMemoryRpcTransport c_to_b_transport(/*...*/);
+ LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress;
+ SimpleRpcEgress<kMaxPacketSize> c_to_b_egress("c->b", c_to_b_transport);
+
+ std::array tx_channels = {
+ Channel::Create<kChannelAC>(&c_to_b_egress),
+ };
+
+ // Here we list all egresses for the packets _incoming_ from B.
+ std::array b_rx_channels = {
+ ChannelEgress{kChannelAC, local_egress},
+ };
+
+ SimpleRpcIngress<kMaxPacketSize> b_ingress(b_rx_channels);
+
+ ServiceRegistry registry(tx_channels);
+ local_egress.set_packet_processor(registry);
+
+ c_to_b_transport.set_ingress(b_ingress);
+
+ DetachedThread(/*...*/, c_to_b_transport);
+ DetachedThread(/*...*/, local_egress);
diff --git a/pw_rpc_transport/egress_ingress.cc b/pw_rpc_transport/egress_ingress.cc
new file mode 100644
index 000000000..3842ab15f
--- /dev/null
+++ b/pw_rpc_transport/egress_ingress.cc
@@ -0,0 +1,49 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#define PW_LOG_MODULE_NAME "PW_RPC"
+
+#include "pw_rpc_transport/egress_ingress.h"
+
+#include <cinttypes>
+
+#include "pw_log/log.h"
+
+namespace pw::rpc::internal {
+
+void LogBadPacket() { PW_LOG_ERROR("Received malformed RPC packet"); }
+
+void LogChannelIdOverflow(uint32_t channel_id, uint32_t max_channel_id) {
+ PW_LOG_ERROR(
+ "Received RPC packet for channel ID %d, max supported channel ID %d",
+ static_cast<int>(channel_id),
+ static_cast<int>(max_channel_id));
+}
+
+void LogMissingEgressForChannel(uint32_t channel_id) {
+ PW_LOG_ERROR(
+ "Received RPC packet for channel ID %d"
+ " which doesn't have a registered egress",
+ static_cast<int>(channel_id));
+}
+
+void LogIngressSendFailure(uint32_t channel_id, pw::Status status) {
+ PW_LOG_ERROR(
+ "Failed to send RPC packet received on channel ID %d"
+ " to its configured egress. Status %d",
+ static_cast<int>(channel_id),
+ static_cast<int>(status.code()));
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc_transport/egress_ingress_test.cc b/pw_rpc_transport/egress_ingress_test.cc
new file mode 100644
index 000000000..5c3f8d317
--- /dev/null
+++ b/pw_rpc_transport/egress_ingress_test.cc
@@ -0,0 +1,406 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/egress_ingress.h"
+
+#include <random>
+
+#include "gtest/gtest.h"
+#include "public/pw_rpc_transport/rpc_transport.h"
+#include "pw_bytes/span.h"
+#include "pw_metric/metric.h"
+#include "pw_rpc/client_server.h"
+#include "pw_rpc/packet_meta.h"
+#include "pw_rpc_transport/hdlc_framing.h"
+#include "pw_rpc_transport/internal/test.rpc.pwpb.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_rpc_transport/service_registry.h"
+#include "pw_rpc_transport/simple_framing.h"
+#include "pw_status/status.h"
+#include "pw_string/string.h"
+#include "pw_sync/thread_notification.h"
+
+namespace pw::rpc {
+namespace {
+
+constexpr size_t kMaxPacketSize = 256;
+
+class TestService final
+ : public pw_rpc_transport::testing::pw_rpc::pwpb::TestService::Service<
+ TestService> {
+ public:
+ Status Echo(
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& request,
+ pw_rpc_transport::testing::pwpb::EchoMessage::Message& response) {
+ response.msg = request.msg;
+ return OkStatus();
+ }
+};
+
+// A transport that stores all received frames so they can be manually retrieved
+// by the ingress later.
+class TestTransport : public RpcFrameSender {
+ public:
+ explicit TestTransport(size_t mtu, bool is_faulty = false)
+ : mtu_(mtu), is_faulty_(is_faulty) {}
+
+ size_t MaximumTransmissionUnit() const override { return mtu_; }
+
+ Status Send(RpcFrame frame) override {
+ if (is_faulty_) {
+ return Status::Internal();
+ }
+ std::copy(
+ frame.header.begin(), frame.header.end(), std::back_inserter(buffer_));
+ std::copy(frame.payload.begin(),
+ frame.payload.end(),
+ std::back_inserter(buffer_));
+ return OkStatus();
+ }
+
+ ByteSpan buffer() { return buffer_; }
+
+ private:
+ size_t mtu_;
+ bool is_faulty_ = false;
+ std::vector<std::byte> buffer_;
+};
+
+// An egress handler that passes the received RPC packet to the service
+// registry.
+class TestLocalEgress : public RpcEgressHandler {
+ public:
+ Status SendRpcPacket(ConstByteSpan packet) override {
+ if (!registry_) {
+ return Status::FailedPrecondition();
+ }
+ return registry_->ProcessRpcPacket(packet);
+ }
+
+ void set_registry(ServiceRegistry& registry) { registry_ = &registry; }
+
+ private:
+ ServiceRegistry* registry_ = nullptr;
+};
+
+TEST(RpcEgressIngressTest, SimpleFramingRoundtrip) {
+ constexpr uint32_t kChannelAtoB = 1;
+ constexpr size_t kMaxMessageLength = 200;
+ constexpr size_t kAtoBMtu = 33;
+ constexpr size_t kBtoAMtu = 72;
+
+ TestTransport transport_a_to_b(kAtoBMtu);
+ TestTransport transport_b_to_a(kBtoAMtu);
+
+ SimpleRpcEgress<kMaxPacketSize> egress_a_to_b("a->b", transport_a_to_b);
+ SimpleRpcEgress<kMaxPacketSize> egress_b_to_a("b->a", transport_b_to_a);
+
+ std::array a_tx_channels = {
+ rpc::Channel::Create<kChannelAtoB>(&egress_a_to_b)};
+ std::array b_tx_channels = {
+ rpc::Channel::Create<kChannelAtoB>(&egress_b_to_a)};
+
+ ServiceRegistry registry_a(a_tx_channels);
+ ServiceRegistry registry_b(b_tx_channels);
+
+ TestService test_service;
+ registry_b.RegisterService(test_service);
+
+ TestLocalEgress local_egress_a;
+ local_egress_a.set_registry(registry_a);
+
+ TestLocalEgress local_egress_b;
+ local_egress_b.set_registry(registry_b);
+
+ std::array a_rx_channels = {
+ ChannelEgress{kChannelAtoB, local_egress_a},
+ };
+ std::array b_rx_channels = {
+ ChannelEgress{kChannelAtoB, local_egress_b},
+ };
+
+ SimpleRpcIngress<kMaxPacketSize> ingress_a(a_rx_channels);
+ SimpleRpcIngress<kMaxPacketSize> ingress_b(b_rx_channels);
+
+ auto client =
+ registry_a
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kChannelAtoB);
+
+ sync::ThreadNotification receiver1_done;
+ sync::ThreadNotification receiver2_done;
+
+ struct ReceiverState {
+ InlineString<kMaxMessageLength> message;
+ sync::ThreadNotification done;
+ };
+
+ ReceiverState receiver1;
+ ReceiverState receiver2;
+ receiver1.message.append(2 * transport_a_to_b.MaximumTransmissionUnit(), '*');
+ receiver2.message.append(2 * transport_b_to_a.MaximumTransmissionUnit(), '>');
+
+ auto call1 = client.Echo(
+ {.msg = receiver1.message},
+ [&receiver1](
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& response,
+ Status status) {
+ EXPECT_EQ(status, OkStatus());
+ EXPECT_EQ(response.msg, receiver1.message);
+ receiver1.done.release();
+ },
+ [&receiver1](Status status) {
+ EXPECT_EQ(status, OkStatus());
+ receiver1.done.release();
+ });
+
+ auto call2 = client.Echo(
+ {.msg = receiver2.message},
+ [&receiver2](
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& response,
+ Status status) {
+ EXPECT_EQ(status, OkStatus());
+ EXPECT_EQ(response.msg, receiver2.message);
+ receiver2.done.release();
+ },
+ [&receiver2](Status status) {
+ EXPECT_EQ(status, OkStatus());
+ receiver2.done.release();
+ });
+
+ // Calling `ingress_b.ProcessIncomingData` reads all packets from the
+ // transport and dispatches them according to the ingress configuration.
+ // Dispatching a packet generates a reply message: we then read it back at the
+ // sender by calling `ingress_a.ProcessIncomingData`.
+ EXPECT_EQ(ingress_b.ProcessIncomingData(transport_a_to_b.buffer()),
+ OkStatus());
+ EXPECT_EQ(ingress_a.ProcessIncomingData(transport_b_to_a.buffer()),
+ OkStatus());
+
+ receiver1.done.acquire();
+ receiver2.done.acquire();
+}
+
+TEST(RpcEgressIngressTest, HdlcFramingRoundtrip) {
+ constexpr uint32_t kChannelAtoB = 1;
+ constexpr size_t kMaxMessageLength = 200;
+ constexpr size_t kAtoBMtu = 33;
+ constexpr size_t kBtoAMtu = 72;
+
+ TestTransport transport_a_to_b(kAtoBMtu);
+ TestTransport transport_b_to_a(kBtoAMtu);
+
+ HdlcRpcEgress<kMaxPacketSize> egress_a_to_b("a->b", transport_a_to_b);
+ HdlcRpcEgress<kMaxPacketSize> egress_b_to_a("b->a", transport_b_to_a);
+
+ std::array a_tx_channels = {
+ rpc::Channel::Create<kChannelAtoB>(&egress_a_to_b)};
+ std::array b_tx_channels = {
+ rpc::Channel::Create<kChannelAtoB>(&egress_b_to_a)};
+
+ ServiceRegistry registry_a(a_tx_channels);
+ ServiceRegistry registry_b(b_tx_channels);
+
+ TestService test_service;
+ registry_b.RegisterService(test_service);
+
+ TestLocalEgress local_egress_a;
+ local_egress_a.set_registry(registry_a);
+
+ TestLocalEgress local_egress_b;
+ local_egress_b.set_registry(registry_b);
+
+ std::array a_rx_channels = {
+ ChannelEgress{kChannelAtoB, local_egress_a},
+ };
+ std::array b_rx_channels = {
+ ChannelEgress{kChannelAtoB, local_egress_b},
+ };
+
+ HdlcRpcIngress<kMaxPacketSize> ingress_a(a_rx_channels);
+ HdlcRpcIngress<kMaxPacketSize> ingress_b(b_rx_channels);
+
+ auto client =
+ registry_a
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kChannelAtoB);
+
+ sync::ThreadNotification receiver1_done;
+ sync::ThreadNotification receiver2_done;
+
+ struct ReceiverState {
+ InlineString<kMaxMessageLength> message;
+ sync::ThreadNotification done;
+ };
+
+ ReceiverState receiver1;
+ ReceiverState receiver2;
+ receiver1.message.append(2 * transport_a_to_b.MaximumTransmissionUnit(), '*');
+ receiver2.message.append(2 * transport_b_to_a.MaximumTransmissionUnit(), '>');
+
+ auto call1 = client.Echo(
+ {.msg = receiver1.message},
+ [&receiver1](
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& response,
+ Status status) {
+ EXPECT_EQ(status, OkStatus());
+ EXPECT_EQ(response.msg, receiver1.message);
+ receiver1.done.release();
+ },
+ [&receiver1](Status status) {
+ EXPECT_EQ(status, OkStatus());
+ receiver1.done.release();
+ });
+
+ auto call2 = client.Echo(
+ {.msg = receiver2.message},
+ [&receiver2](
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& response,
+ Status status) {
+ EXPECT_EQ(status, OkStatus());
+ EXPECT_EQ(response.msg, receiver2.message);
+ receiver2.done.release();
+ },
+ [&receiver2](Status status) {
+ EXPECT_EQ(status, OkStatus());
+ receiver2.done.release();
+ });
+
+ // Calling `ingress_b.ProcessIncomingData` reads all packets from the
+ // transport and dispatches them according to the ingress configuration.
+ // Dispatching a packet generates a reply message: we then read it back at the
+ // sender by calling `ingress_a.ProcessIncomingData`.
+ EXPECT_EQ(ingress_b.ProcessIncomingData(transport_a_to_b.buffer()),
+ OkStatus());
+ EXPECT_EQ(ingress_a.ProcessIncomingData(transport_b_to_a.buffer()),
+ OkStatus());
+
+ receiver1.done.acquire();
+ receiver2.done.acquire();
+}
+
+TEST(RpcEgressIngressTest, MalformedRpcPacket) {
+ constexpr uint32_t kTestChannel = 1;
+ constexpr size_t kMtu = 33;
+ std::vector<std::byte> kMalformedPacket = {std::byte{0x42}, std::byte{0x74}};
+
+ TestTransport transport(kMtu);
+ SimpleRpcEgress<kMaxPacketSize> egress("test", transport);
+
+ TestLocalEgress local_egress;
+ std::array rx_channels = {
+ ChannelEgress{kTestChannel, local_egress},
+ };
+
+ SimpleRpcIngress<kMaxPacketSize> ingress(rx_channels);
+
+ EXPECT_EQ(egress.Send(kMalformedPacket), OkStatus());
+ EXPECT_EQ(ingress.ProcessIncomingData(transport.buffer()), OkStatus());
+
+ EXPECT_EQ(ingress.num_bad_packets(), 1u);
+ EXPECT_EQ(ingress.num_overflow_channel_ids(), 0u);
+ EXPECT_EQ(ingress.num_missing_egresses(), 0u);
+ EXPECT_EQ(ingress.num_egress_errors(), 0u);
+}
+
+TEST(RpcEgressIngressTest, ChannelIdOverflow) {
+ constexpr uint32_t kInvalidChannelId = 65;
+ constexpr size_t kMtu = 128;
+
+ TestTransport transport(kMtu);
+ SimpleRpcEgress<kMaxPacketSize> egress("test", transport);
+
+ std::array sender_tx_channels = {
+ rpc::Channel::Create<kInvalidChannelId>(&egress)};
+
+ ServiceRegistry registry(sender_tx_channels);
+ auto client =
+ registry
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kInvalidChannelId);
+
+ SimpleRpcIngress<kMaxPacketSize> ingress;
+
+ auto receiver = client.Echo({.msg = "test"});
+
+ EXPECT_EQ(ingress.ProcessIncomingData(transport.buffer()), OkStatus());
+
+ EXPECT_EQ(ingress.num_bad_packets(), 0u);
+ EXPECT_EQ(ingress.num_overflow_channel_ids(), 1u);
+ EXPECT_EQ(ingress.num_missing_egresses(), 0u);
+ EXPECT_EQ(ingress.num_egress_errors(), 0u);
+}
+
+TEST(RpcEgressIngressTest, MissingEgressForIncomingPacket) {
+ constexpr uint32_t kChannelA = 22;
+ constexpr uint32_t kChannelB = 33;
+ constexpr size_t kMtu = 128;
+
+ TestTransport transport(kMtu);
+ SimpleRpcEgress<kMaxPacketSize> egress("test", transport);
+
+ std::array sender_tx_channels = {rpc::Channel::Create<kChannelA>(&egress)};
+
+ ServiceRegistry registry(sender_tx_channels);
+ auto client =
+ registry
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kChannelA);
+
+ std::array ingress_channels = {ChannelEgress(kChannelB, egress)};
+ SimpleRpcIngress<kMaxPacketSize> ingress(ingress_channels);
+
+ auto receiver = client.Echo({.msg = "test"});
+
+ EXPECT_EQ(ingress.ProcessIncomingData(transport.buffer()), OkStatus());
+
+ EXPECT_EQ(ingress.num_bad_packets(), 0u);
+ EXPECT_EQ(ingress.num_overflow_channel_ids(), 0u);
+ EXPECT_EQ(ingress.num_missing_egresses(), 1u);
+ EXPECT_EQ(ingress.num_egress_errors(), 0u);
+}
+
+TEST(RpcEgressIngressTest, EgressSendFailureForIncomingPacket) {
+ constexpr uint32_t kChannelId = 22;
+ constexpr size_t kMtu = 128;
+
+ TestTransport good_transport(kMtu, /*is_faulty=*/false);
+ TestTransport bad_transport(kMtu, /*is_faulty=*/true);
+ SimpleRpcEgress<kMaxPacketSize> good_egress("test", good_transport);
+ SimpleRpcEgress<kMaxPacketSize> bad_egress("test", bad_transport);
+
+ std::array sender_tx_channels = {
+ rpc::Channel::Create<kChannelId>(&good_egress)};
+
+ ServiceRegistry registry(sender_tx_channels);
+ auto client =
+ registry
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kChannelId);
+
+ std::array ingress_channels = {ChannelEgress(kChannelId, bad_egress)};
+ SimpleRpcIngress<kMaxPacketSize> ingress(ingress_channels);
+
+ auto receiver = client.Echo({.msg = "test"});
+
+ EXPECT_EQ(ingress.ProcessIncomingData(good_transport.buffer()), OkStatus());
+
+ EXPECT_EQ(ingress.num_bad_packets(), 0u);
+ EXPECT_EQ(ingress.num_overflow_channel_ids(), 0u);
+ EXPECT_EQ(ingress.num_missing_egresses(), 0u);
+ EXPECT_EQ(ingress.num_egress_errors(), 1u);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/hdlc_framing_test.cc b/pw_rpc_transport/hdlc_framing_test.cc
new file mode 100644
index 000000000..69772593b
--- /dev/null
+++ b/pw_rpc_transport/hdlc_framing_test.cc
@@ -0,0 +1,193 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/hdlc_framing.h"
+
+#include <algorithm>
+#include <array>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc {
+namespace {
+
+TEST(HdlcRpc, EncodeThenDecode) {
+ constexpr size_t kMaxPacketSize = 256;
+ constexpr size_t kPacketSize = 100;
+ constexpr size_t kMaxFrameSize = 20;
+ // Expecting six frames due to HDLC overhead.
+ constexpr size_t kNumFramesExpected = 6;
+
+ HdlcRpcPacketEncoder<kMaxPacketSize> encoder;
+ std::array<std::byte, kPacketSize> packet{};
+
+ struct EncodeState {
+ size_t num_frames = 0;
+ size_t offset = 0;
+ std::array<std::byte, 2 * kMaxPacketSize> encoded{};
+ } state;
+
+ std::fill(packet.begin(), packet.end(), std::byte{0x42});
+ ASSERT_EQ(encoder.Encode(packet,
+ kMaxFrameSize,
+ [&state](RpcFrame& frame) {
+ state.num_frames++;
+ EXPECT_TRUE(frame.header.empty());
+ std::copy(frame.payload.begin(),
+ frame.payload.end(),
+ state.encoded.begin() + state.offset);
+ state.offset += frame.payload.size();
+ return OkStatus();
+ }),
+ OkStatus());
+
+ EXPECT_EQ(state.num_frames, kNumFramesExpected);
+
+ std::array<std::byte, kMaxPacketSize> decoded{};
+ HdlcRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ ASSERT_EQ(decoder.Decode(state.encoded,
+ [&decoded](ConstByteSpan packet_to_decode) {
+ std::copy(packet_to_decode.begin(),
+ packet_to_decode.end(),
+ decoded.begin());
+ }),
+ OkStatus());
+
+ EXPECT_TRUE(std::equal(packet.begin(), packet.end(), decoded.begin()));
+}
+
+TEST(HdlcRpc, PacketTooLong) {
+ constexpr size_t kMaxPacketSize = 256;
+ constexpr size_t kMaxFrameSize = 100;
+
+ std::array<std::byte, kMaxPacketSize + 1> packet{};
+ HdlcRpcPacketEncoder<kMaxPacketSize> encoder;
+
+ EXPECT_EQ(encoder.Encode(
+ packet, kMaxFrameSize, [](RpcFrame&) { return OkStatus(); }),
+ Status::FailedPrecondition());
+}
+
+TEST(HdlcRpcFrame, MaxFrameSizeIsZero) {
+ constexpr size_t kMaxPacketSize = 256;
+ constexpr size_t kMaxFrameSize = 0;
+
+ std::array<std::byte, kMaxPacketSize> packet{};
+ HdlcRpcPacketEncoder<kMaxPacketSize> encoder;
+
+ EXPECT_EQ(encoder.Encode(
+ packet, kMaxFrameSize, [](RpcFrame&) { return OkStatus(); }),
+ Status::FailedPrecondition());
+}
+
+TEST(HdlcRpcFrame, MaxSizeHdlcPayload) {
+ constexpr size_t kMaxPacketSize = 256;
+ constexpr size_t kPacketSize = 256;
+ constexpr size_t kMaxFrameSize = 20;
+ constexpr auto kHdlcEscapeByte = std::byte{0x7e};
+
+ std::array<std::byte, kPacketSize> packet{};
+ std::fill(packet.begin(), packet.end(), kHdlcEscapeByte);
+
+ struct EncodeState {
+ size_t offset = 0;
+ std::array<std::byte, 2 * kMaxPacketSize + kHdlcProtocolOverheadBytes>
+ encoded{};
+ } state;
+
+ HdlcRpcPacketEncoder<kMaxPacketSize> encoder;
+ ASSERT_EQ(encoder.Encode(packet,
+ kMaxFrameSize,
+ [&state](RpcFrame& frame) {
+ EXPECT_TRUE(frame.header.empty());
+ std::copy(frame.payload.begin(),
+ frame.payload.end(),
+ state.encoded.begin() + state.offset);
+ state.offset += frame.payload.size();
+ return OkStatus();
+ }),
+ OkStatus());
+
+ std::array<std::byte, kMaxPacketSize> decoded{};
+ HdlcRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ ASSERT_EQ(decoder.Decode(state.encoded,
+ [&decoded](ConstByteSpan packet_to_decode) {
+ std::copy(packet_to_decode.begin(),
+ packet_to_decode.end(),
+ decoded.begin());
+ }),
+ OkStatus());
+
+ EXPECT_TRUE(std::equal(packet.begin(), packet.end(), decoded.begin()));
+}
+
+TEST(HdlcRpc, CallbackErrorPropagation) {
+ constexpr size_t kMaxPacketSize = 256;
+ constexpr size_t kPacketSize = 256;
+ constexpr size_t kMaxFrameSize = 20;
+
+ std::array<std::byte, kPacketSize> packet{};
+ std::fill(packet.begin(), packet.end(), std::byte{0x42});
+
+ HdlcRpcPacketEncoder<kMaxPacketSize> encoder;
+ EXPECT_EQ(
+ encoder.Encode(packet,
+ kMaxFrameSize,
+ [](RpcFrame&) { return Status::PermissionDenied(); }),
+ Status::PermissionDenied());
+}
+
+TEST(HdlcRpcFrame, OneByteAtTimeDecoding) {
+ constexpr size_t kMaxPacketSize = 256;
+ constexpr size_t kPacketSize = 100;
+ constexpr size_t kMaxFrameSize = 8;
+
+ HdlcRpcPacketEncoder<kMaxPacketSize> encoder;
+ std::array<std::byte, kPacketSize> packet{};
+ std::vector<std::byte> encoded;
+
+ std::fill(packet.begin(), packet.end(), std::byte{0x42});
+ ASSERT_EQ(encoder.Encode(packet,
+ kMaxFrameSize,
+ [&encoded](RpcFrame& frame) {
+ std::copy(frame.payload.begin(),
+ frame.payload.end(),
+ std::back_inserter(encoded));
+ return OkStatus();
+ }),
+ OkStatus());
+
+ std::array<std::byte, kMaxPacketSize> decoded{};
+ HdlcRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ for (std::byte b : encoded) {
+ auto buffer_span = span(&b, 1);
+ ASSERT_EQ(decoder.Decode(buffer_span,
+ [&decoded](ConstByteSpan packet_to_decode) {
+ std::copy(packet_to_decode.begin(),
+ packet_to_decode.end(),
+ decoded.begin());
+ }),
+ OkStatus());
+ }
+
+ EXPECT_TRUE(std::equal(packet.begin(), packet.end(), decoded.begin()));
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/internal/packet_buffer_queue_test.cc b/pw_rpc_transport/internal/packet_buffer_queue_test.cc
new file mode 100644
index 000000000..0e9a33857
--- /dev/null
+++ b/pw_rpc_transport/internal/packet_buffer_queue_test.cc
@@ -0,0 +1,83 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/internal/packet_buffer_queue.h"
+
+#include <array>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+constexpr size_t kMaxPacketSize = 256;
+
+TEST(PacketBufferQueueTest, CopyAndGetPacket) {
+ PacketBufferQueue<kMaxPacketSize>::PacketBuffer packet_buffer = {};
+ std::array<std::byte, 42> input{};
+ std::fill(input.begin(), input.end(), std::byte{0x42});
+
+ auto packet = packet_buffer.GetPacket();
+ ASSERT_EQ(packet.status(), OkStatus());
+ EXPECT_EQ(packet->size(), 0ul);
+
+ ASSERT_EQ(packet_buffer.CopyPacket(input), OkStatus());
+
+ packet = packet_buffer.GetPacket();
+ ASSERT_EQ(packet.status(), OkStatus());
+ EXPECT_EQ(packet->size(), 42ul);
+
+ EXPECT_TRUE(std::equal(input.begin(), input.end(), packet->begin()));
+
+ std::array<std::byte, 300> long_input{};
+ EXPECT_EQ(packet_buffer.CopyPacket(long_input), Status::ResourceExhausted());
+}
+
+TEST(PacketBufferQueueTest, PopWhenEmptyFails) {
+ PacketBufferQueue<kMaxPacketSize> queue;
+ EXPECT_EQ(queue.Pop().status(), Status::ResourceExhausted());
+}
+
+TEST(PacketBufferQueueTest, PopAllSucceeds) {
+ constexpr auto kPacketQueueSize = 3;
+
+ std::array<PacketBufferQueue<kMaxPacketSize>::PacketBuffer, kPacketQueueSize>
+ packets;
+ PacketBufferQueue<kMaxPacketSize> queue(packets);
+
+ for (size_t i = 0; i < kPacketQueueSize; ++i) {
+ EXPECT_EQ(queue.Pop().status(), OkStatus());
+ }
+
+ EXPECT_EQ(queue.Pop().status(), Status::ResourceExhausted());
+}
+
+TEST(PacketQueueTest, PushPopSucceeds) {
+ PacketBufferQueue<kMaxPacketSize>::PacketBuffer packet_buffer;
+ PacketBufferQueue<kMaxPacketSize> queue;
+
+ pw::Result<PacketBufferQueue<kMaxPacketSize>::PacketBuffer*>
+ popped_packet_buffer = queue.Pop();
+ EXPECT_EQ(popped_packet_buffer.status(), Status::ResourceExhausted());
+
+ queue.Push(packet_buffer);
+ popped_packet_buffer = queue.Pop();
+ EXPECT_EQ(popped_packet_buffer.status(), OkStatus());
+}
+
+} // namespace
+} // namespace pw::rpc::internal
diff --git a/.prettierrc.js b/pw_rpc_transport/internal/test.options
index 312c397bc..3e7d9ae47 100644
--- a/.prettierrc.js
+++ b/pw_rpc_transport/internal/test.options
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -12,4 +12,4 @@
// License for the specific language governing permissions and limitations under
// the License.
-module.exports = {...require('gts/.prettierrc.json')}
+pw_rpc_transport.testing.EchoMessage.msg max_size:1024
diff --git a/pw_rpc_transport/internal/test.proto b/pw_rpc_transport/internal/test.proto
new file mode 100644
index 000000000..927b6cc18
--- /dev/null
+++ b/pw_rpc_transport/internal/test.proto
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+syntax = "proto3";
+
+package pw_rpc_transport.testing;
+
+service TestService {
+ rpc Echo(EchoMessage) returns (EchoMessage) {}
+}
+
+message EchoMessage {
+ string msg = 1;
+}
diff --git a/pw_rpc_transport/local_rpc_egress.cc b/pw_rpc_transport/local_rpc_egress.cc
new file mode 100644
index 000000000..e8a183027
--- /dev/null
+++ b/pw_rpc_transport/local_rpc_egress.cc
@@ -0,0 +1,47 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#define PW_LOG_MODULE_NAME "PW_RPC"
+
+#include <cstddef>
+
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc::internal {
+
+void LogNoRpcServiceRegistryError() {
+ PW_LOG_ERROR("LocalRpcEgress: service registry not configured");
+}
+
+void LogPacketSizeTooLarge(size_t packet_size, size_t max_packet_size) {
+ PW_LOG_ERROR("LocalRpcEgress: packet too large (%d > %d)",
+ static_cast<int>(packet_size),
+ static_cast<int>(max_packet_size));
+}
+
+void LogEgressThreadNotRunningError() {
+ PW_LOG_ERROR("LocalRpcEgress: egress thread is not running");
+}
+
+void LogFailedToProcessPacket(pw::Status status) {
+ PW_LOG_ERROR("LocalRpcEgress: failed to process packet. Status %d",
+ static_cast<int>(status.code()));
+}
+
+void LogFailedToAccessPacket(pw::Status status) {
+ PW_LOG_ERROR("LocalRpcEgress: failed to access packet buffer. Status %d",
+ static_cast<int>(status.code()));
+}
+} // namespace pw::rpc::internal
diff --git a/pw_rpc_transport/local_rpc_egress_test.cc b/pw_rpc_transport/local_rpc_egress_test.cc
new file mode 100644
index 000000000..96e041ef3
--- /dev/null
+++ b/pw_rpc_transport/local_rpc_egress_test.cc
@@ -0,0 +1,224 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/local_rpc_egress.h"
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_log/log.h"
+#include "pw_rpc/client_server.h"
+#include "pw_rpc/packet_meta.h"
+#include "pw_rpc_transport/internal/test.rpc.pwpb.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_rpc_transport/service_registry.h"
+#include "pw_status/status.h"
+#include "pw_sync/counting_semaphore.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::literals::chrono_literals;
+using namespace std::literals::string_view_literals;
+
+const auto kTestMessage = "I hope that someone gets my message in a bottle"sv;
+
+class TestEchoService final
+ : public pw_rpc_transport::testing::pw_rpc::pwpb::TestService::Service<
+ TestEchoService> {
+ public:
+ Status Echo(
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& request,
+ pw_rpc_transport::testing::pwpb::EchoMessage::Message& response) {
+ response.msg = request.msg;
+ return OkStatus();
+ }
+};
+
+// Test service that can be controlled from the test, e.g. the test can tell the
+// service when it's OK to proceed. Useful for testing packet queue exhaustion.
+class ControlledTestEchoService final
+ : public pw_rpc_transport::testing::pw_rpc::pwpb::TestService::Service<
+ ControlledTestEchoService> {
+ public:
+ Status Echo(
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& request,
+ pw_rpc_transport::testing::pwpb::EchoMessage::Message& response) {
+ start_.release();
+ process_.acquire();
+ response.msg = request.msg;
+ return OkStatus();
+ }
+
+ void Wait() { start_.acquire(); }
+ void Proceed() { process_.release(); }
+
+ private:
+ sync::ThreadNotification start_;
+ sync::ThreadNotification process_;
+};
+
+TEST(LocalRpcEgressTest, PacketsGetDeliveredToPacketProcessor) {
+ constexpr size_t kMaxPacketSize = 100;
+ constexpr size_t kNumRequests = 10;
+ // Size the queue so we don't exhaust it (we don't want this test to flake;
+ // exhaustion is tested separately).
+ constexpr size_t kPacketQueueSize = 2 * kNumRequests;
+ constexpr uint32_t kChannelId = 1;
+
+ LocalRpcEgress<kPacketQueueSize, kMaxPacketSize> egress;
+ std::array channels = {rpc::Channel::Create<kChannelId>(&egress)};
+ ServiceRegistry registry(channels);
+
+ TestEchoService service;
+ registry.RegisterService(service);
+
+ egress.set_packet_processor(registry);
+ auto egress_thread = thread::Thread(thread::stl::Options(), egress);
+
+ auto client =
+ registry
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kChannelId);
+
+ std::vector<rpc::PwpbUnaryReceiver<
+ pw_rpc_transport::testing::pwpb::EchoMessage::Message>>
+ receivers;
+
+ struct State {
+ // Stash the receivers to keep the calls alive.
+ std::atomic<uint32_t> successes = 0;
+ std::atomic<uint32_t> errors = 0;
+ sync::CountingSemaphore sem;
+ } state;
+
+ for (size_t i = 0; i < kNumRequests; i++) {
+ receivers.push_back(client.Echo(
+ {.msg = kTestMessage},
+ [&state](const pw_rpc_transport::testing::pwpb::EchoMessage::Message&
+ response,
+ Status status) {
+ EXPECT_EQ(status, OkStatus());
+ EXPECT_EQ(response.msg, kTestMessage);
+ state.successes++;
+ state.sem.release();
+ },
+ [&state](Status) {
+ state.errors++;
+ state.sem.release();
+ }));
+ }
+
+ for (size_t i = 0; i < kNumRequests; i++) {
+ state.sem.acquire();
+ }
+
+ EXPECT_EQ(state.successes.load(), kNumRequests);
+ EXPECT_EQ(state.errors.load(), 0u);
+
+ egress.Stop();
+ egress_thread.join();
+}
+
+TEST(LocalRpcEgressTest, PacketQueueExhausted) {
+ constexpr size_t kMaxPacketSize = 100;
+ constexpr size_t kPacketQueueSize = 1;
+ constexpr uint32_t kChannelId = 1;
+
+ LocalRpcEgress<kPacketQueueSize, kMaxPacketSize> egress;
+ std::array channels = {rpc::Channel::Create<kChannelId>(&egress)};
+ ServiceRegistry registry(channels);
+
+ ControlledTestEchoService service;
+ registry.RegisterService(service);
+
+ egress.set_packet_processor(registry);
+ auto egress_thread = thread::Thread(thread::stl::Options(), egress);
+
+ auto client =
+ registry
+ .CreateClient<pw_rpc_transport::testing::pw_rpc::pwpb::TestService>(
+ kChannelId);
+
+ auto receiver = client.Echo({.msg = kTestMessage});
+ service.Wait();
+
+ // echo_call is blocked in ServiceRegistry waiting for the Proceed() call.
+ // Since there is only one packet queue buffer available at a time, other
+ // packets will get rejected with RESOURCE_EXHAUSTED error until the first
+ // one is handled.
+ EXPECT_EQ(egress.Send({}), Status::ResourceExhausted());
+ service.Proceed();
+
+ // Expecting egress to return the packet queue buffer within a reasonable
+ // amount of time; currently there is no way to explicitly synchronize on
+ // its availability, so we give it few seconds to recover.
+ auto deadline = chrono::SystemClock::now() + 5s;
+ bool egress_ok = false;
+ while (chrono::SystemClock::now() <= deadline) {
+ if (egress.Send({}).ok()) {
+ egress_ok = true;
+ break;
+ }
+ }
+
+ EXPECT_TRUE(egress_ok);
+
+ egress.Stop();
+ egress_thread.join();
+}
+
+TEST(LocalRpcEgressTest, NoPacketProcessor) {
+ constexpr size_t kPacketQueueSize = 10;
+ constexpr size_t kMaxPacketSize = 10;
+ LocalRpcEgress<kPacketQueueSize, kMaxPacketSize> egress;
+ EXPECT_EQ(egress.Send({}), Status::FailedPrecondition());
+}
+
+TEST(LocalRpcEgressTest, PacketTooBig) {
+ constexpr size_t kPacketQueueSize = 10;
+ constexpr size_t kMaxPacketSize = 10;
+ constexpr uint32_t kChannelId = 1;
+ LocalRpcEgress<kPacketQueueSize, kMaxPacketSize> egress;
+
+ std::array<std::byte, kMaxPacketSize + 1> packet{};
+ std::array channels = {rpc::Channel::Create<kChannelId>(&egress)};
+ ServiceRegistry registry(channels);
+ egress.set_packet_processor(registry);
+
+ EXPECT_EQ(egress.Send(packet), Status::InvalidArgument());
+}
+
+TEST(LocalRpcEgressTest, EgressStopped) {
+ constexpr size_t kPacketQueueSize = 10;
+ constexpr size_t kMaxPacketSize = 10;
+ constexpr uint32_t kChannelId = 1;
+ LocalRpcEgress<kPacketQueueSize, kMaxPacketSize> egress;
+
+ std::array channels = {rpc::Channel::Create<kChannelId>(&egress)};
+ ServiceRegistry registry(channels);
+ egress.set_packet_processor(registry);
+
+ auto egress_thread = thread::Thread(thread::stl::Options(), egress);
+ EXPECT_EQ(egress.Send({}), OkStatus());
+ egress.Stop();
+ EXPECT_EQ(egress.Send({}), Status::FailedPrecondition());
+
+ egress_thread.join();
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/egress_ingress.h b/pw_rpc_transport/public/pw_rpc_transport/egress_ingress.h
new file mode 100644
index 000000000..d263081c6
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/egress_ingress.h
@@ -0,0 +1,170 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <sys/types.h>
+
+#include <mutex>
+
+#include "pw_bytes/span.h"
+#include "pw_metric/metric.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/packet_meta.h"
+#include "pw_rpc_transport/hdlc_framing.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_rpc_transport/simple_framing.h"
+#include "pw_status/status.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+#include "rpc_transport.h"
+
+namespace pw::rpc {
+namespace internal {
+
+void LogBadPacket();
+void LogChannelIdOverflow(uint32_t channel_id, uint32_t max_channel_id);
+void LogMissingEgressForChannel(uint32_t channel_id);
+void LogIngressSendFailure(uint32_t channel_id, Status status);
+
+} // namespace internal
+
+// Ties RPC transport and RPC frame encoder together.
+template <typename Encoder>
+class RpcEgress : public RpcEgressHandler, public ChannelOutput {
+ public:
+ RpcEgress(std::string_view channel_name, RpcFrameSender& transport)
+ : ChannelOutput(channel_name.data()), transport_(transport) {}
+
+ // Implements both rpc::ChannelOutput and RpcEgressHandler. Encodes the
+ // provided packet using the target transport's MTU as max frame size and
+ // sends it over that transport.
+ //
+ // Sending a packet may result in multiple RpcTransport::Write calls which
+ // must not be interleaved in order for the packet to be successfully
+ // reassembled from the transport-level frames by the receiver. RpcEgress
+ // is using a mutex to ensure this. Technically we could just rely on pw_rpc
+ // global lock but that would unnecessarily couple transport logic to pw_rpc
+ // internals.
+ Status SendRpcPacket(ConstByteSpan rpc_packet) override {
+ std::lock_guard lock(mutex_);
+ return encoder_.Encode(rpc_packet,
+ transport_.MaximumTransmissionUnit(),
+ [this](RpcFrame& frame) {
+ // Encoders must call this callback inline so that
+ // we're still holding `mutex_` here. Unfortunately
+ // the lock annotations cannot be used on
+ // `transport_` to enforce this.
+ return transport_.Send(frame);
+ });
+ }
+
+ // Implements ChannelOutput.
+ Status Send(ConstByteSpan buffer) override { return SendRpcPacket(buffer); }
+
+ private:
+ sync::Mutex mutex_;
+ RpcFrameSender& transport_;
+ Encoder encoder_ PW_GUARDED_BY(mutex_);
+};
+
+// Ties a channel id and the egress that packets on that channel should be sent
+// to.
+struct ChannelEgress {
+ ChannelEgress(uint32_t id, RpcEgressHandler& egress_handler)
+ : channel_id(id), egress(&egress_handler) {}
+
+ const uint32_t channel_id;
+ RpcEgressHandler* const egress = nullptr;
+};
+
+// Handler for incoming RPC packets. RpcIngress is not thread-safe and must be
+// accessed from a single thread (typically the RPC RX thread).
+template <typename Decoder>
+class RpcIngress : public RpcIngressHandler {
+ public:
+ static constexpr size_t kMaxChannelId = 64;
+ RpcIngress() = default;
+
+ explicit RpcIngress(span<ChannelEgress> channel_egresses) {
+ for (auto& channel : channel_egresses) {
+ PW_ASSERT(channel.channel_id <= kMaxChannelId);
+ channel_egresses_[channel.channel_id] = channel.egress;
+ }
+ }
+
+ const metric::Group& metrics() const { return metrics_; }
+
+ uint32_t num_bad_packets() const { return bad_packets_.value(); }
+
+ uint32_t num_overflow_channel_ids() const {
+ return overflow_channel_ids_.value();
+ }
+
+ uint32_t num_missing_egresses() const { return missing_egresses_.value(); }
+
+ uint32_t num_egress_errors() const { return egress_errors_.value(); }
+
+ // Finds RPC packets in `buffer`, extracts pw_rpc channel ID from each
+ // packet and sends the packet to the egress registered for that channel.
+ Status ProcessIncomingData(ConstByteSpan buffer) override {
+ return decoder_.Decode(buffer, [this](ConstByteSpan packet) {
+ const auto packet_meta = rpc::PacketMeta::FromBuffer(packet);
+ if (!packet_meta.ok()) {
+ bad_packets_.Increment();
+ internal::LogBadPacket();
+ return;
+ }
+ if (packet_meta->channel_id() > kMaxChannelId) {
+ overflow_channel_ids_.Increment();
+ internal::LogChannelIdOverflow(packet_meta->channel_id(),
+ kMaxChannelId);
+ return;
+ }
+ auto* egress = channel_egresses_[packet_meta->channel_id()];
+ if (egress == nullptr) {
+ missing_egresses_.Increment();
+ internal::LogMissingEgressForChannel(packet_meta->channel_id());
+ return;
+ }
+ const auto status = egress->SendRpcPacket(packet);
+ if (!status.ok()) {
+ egress_errors_.Increment();
+ internal::LogIngressSendFailure(packet_meta->channel_id(), status);
+ }
+ });
+ }
+
+ private:
+ std::array<RpcEgressHandler*, kMaxChannelId + 1> channel_egresses_{};
+ Decoder decoder_;
+ PW_METRIC_GROUP(metrics_, "pw_rpc_transport");
+ PW_METRIC(metrics_, bad_packets_, "bad_packets", 0u);
+ PW_METRIC(metrics_, overflow_channel_ids_, "overflow_channel_ids", 0u);
+ PW_METRIC(metrics_, missing_egresses_, "missing_egresses", 0u);
+ PW_METRIC(metrics_, egress_errors_, "egress_errors", 0u);
+};
+
+template <size_t kMaxPacketSize>
+using HdlcRpcEgress = RpcEgress<HdlcRpcPacketEncoder<kMaxPacketSize>>;
+
+template <size_t kMaxPacketSize>
+using HdlcRpcIngress = RpcIngress<HdlcRpcPacketDecoder<kMaxPacketSize>>;
+
+template <size_t kMaxPacketSize>
+using SimpleRpcEgress = RpcEgress<SimpleRpcPacketEncoder<kMaxPacketSize>>;
+
+template <size_t kMaxPacketSize>
+using SimpleRpcIngress = RpcIngress<SimpleRpcPacketDecoder<kMaxPacketSize>>;
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/hdlc_framing.h b/pw_rpc_transport/public/pw_rpc_transport/hdlc_framing.h
new file mode 100644
index 000000000..985510474
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/hdlc_framing.h
@@ -0,0 +1,111 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+
+#include "pw_bytes/span.h"
+#include "pw_hdlc/decoder.h"
+#include "pw_hdlc/encoder.h"
+#include "pw_hdlc/rpc_packets.h"
+#include "pw_result/result.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+#include "pw_stream/memory_stream.h"
+#include "rpc_transport.h"
+
+namespace pw::rpc {
+
+inline constexpr size_t kHdlcProtocolOverheadBytes = 14;
+
+template <size_t kMaxPacketSize>
+class HdlcRpcPacketEncoder
+ : public RpcPacketEncoder<HdlcRpcPacketEncoder<kMaxPacketSize>> {
+ public:
+ // Encodes `packet` as HDLC UI frame and splits the resulting frame into
+ // chunks of `RpcFrame`s where every `RpcFrame` is no longer than
+ // `max_frame_size`. Calls `callback` for each of the resulting `RpcFrame`s.
+ //
+ // Returns:
+ // * FAILED_PRECONDITION if `packet` is too long or `max_frame_size` is zero.
+ // * The underlying HDLC encoding error if it fails to generate a UI frame.
+ // * The underlying callback invocation error from the first failed callback.
+ //
+ Status Encode(ConstByteSpan packet,
+ size_t max_frame_size,
+ OnRpcFrameEncodedCallback&& callback,
+ unsigned rpc_address = hdlc::kDefaultRpcAddress) {
+ if (packet.size() > kMaxPacketSize) {
+ return Status::FailedPrecondition();
+ }
+ if (max_frame_size == 0) {
+ return Status::FailedPrecondition();
+ }
+ stream::MemoryWriter writer(buffer_);
+ PW_TRY(hdlc::WriteUIFrame(rpc_address, packet, writer));
+
+ auto remaining = writer.WrittenData();
+ while (!remaining.empty()) {
+ auto next_fragment_size = std::min(max_frame_size, remaining.size());
+ auto fragment = remaining.first(next_fragment_size);
+ // No header needed for HDLC: frame payload is already HDLC-encoded and
+ // includes frame delimiters.
+ RpcFrame frame{.header = {}, .payload = fragment};
+ PW_TRY(callback(frame));
+ remaining = remaining.subspan(next_fragment_size);
+ }
+
+ return OkStatus();
+ }
+
+ private:
+ // Buffer for HDLC-encoded data. Must be 2x of the max packet size to
+ // accommodate HDLC escape bytes for the worst case where each payload byte
+ // must be escaped, plus 14 bytes for the HDLC protocol overhead.
+ static constexpr size_t kEncodeBufferSize =
+ 2 * kMaxPacketSize + kHdlcProtocolOverheadBytes;
+ std::array<std::byte, kEncodeBufferSize> buffer_;
+};
+
+template <size_t kMaxPacketSize>
+class HdlcRpcPacketDecoder
+ : public RpcPacketDecoder<HdlcRpcPacketDecoder<kMaxPacketSize>> {
+ public:
+ HdlcRpcPacketDecoder() : decoder_(decode_buffer_) {}
+
+ // Finds and decodes HDLC frames in `buffer` and calls `callback` for each
+ // well-formed frame. Malformed frames are ignored and dropped quietly.
+ Status Decode(ConstByteSpan buffer, OnRpcPacketDecodedCallback&& callback) {
+ decoder_.Process(
+ buffer,
+ [callback = std::move(callback)](Result<hdlc::Frame> hdlc_frame) {
+ if (hdlc_frame.ok()) {
+ callback(hdlc_frame->data());
+ }
+ });
+ return OkStatus();
+ }
+
+ private:
+ // decode_buffer_ is used to store a decoded HDLC packet, including the
+ // payload (of up to kMaxPacketSize), address (varint that is always 0 in our
+ // case), control flag and checksum. The total size of the non-payload
+ // components is kMinContentSizeBytes.
+ std::array<std::byte, kMaxPacketSize + hdlc::Frame::kMinContentSizeBytes>
+ decode_buffer_{};
+ hdlc::Decoder decoder_;
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/internal/packet_buffer_queue.h b/pw_rpc_transport/public/pw_rpc_transport/internal/packet_buffer_queue.h
new file mode 100644
index 000000000..1cd9ee027
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/internal/packet_buffer_queue.h
@@ -0,0 +1,92 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <mutex>
+
+#include "pw_bytes/span.h"
+#include "pw_containers/intrusive_list.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+
+namespace pw::rpc::internal {
+
+// A simple thread-safe FIFO for queueing packets. Used by LocalRpcEgress to
+// decouple receiving locally-destined RPC packets from their processing.
+template <size_t kMaxPacketSize>
+class PacketBufferQueue {
+ public:
+ class PacketBuffer : public IntrusiveList<PacketBuffer>::Item {
+ public:
+ Status CopyPacket(ConstByteSpan packet) {
+ if (packet.size() > buffer_.size()) {
+ return Status::ResourceExhausted();
+ }
+ std::copy(packet.begin(), packet.end(), buffer_.begin());
+ size_ = packet.size();
+ return OkStatus();
+ }
+
+ Result<ConstByteSpan> GetPacket() {
+ auto buffer_span = span(buffer_);
+ return buffer_span.first(size_);
+ }
+
+ protected:
+ friend PacketBufferQueue;
+
+ private:
+ std::array<std::byte, kMaxPacketSize> buffer_ = {};
+ size_t size_ = 0;
+ };
+
+ PacketBufferQueue() = default;
+ explicit PacketBufferQueue(span<PacketBuffer> packets) {
+ for (auto& packet : packets) {
+ packet_list_.push_back(packet);
+ }
+ }
+
+ // Push a packet to the end of the queue.
+ void Push(PacketBuffer& packet) {
+ const LockGuard guard(lock_);
+ packet_list_.push_back(packet);
+ }
+
+ // Pop a packet from the head of the queue.
+ // Returns a pointer to the packet popped from the queue, or
+ // ResourceExhausted() if the queue is empty.
+ Result<PacketBuffer*> Pop() {
+ const LockGuard lock(lock_);
+
+ if (packet_list_.empty()) {
+ return Status::ResourceExhausted();
+ }
+
+ // Return the first available packet in the list.
+ PacketBufferQueue::PacketBuffer& front = packet_list_.front();
+ packet_list_.pop_front();
+ return &front;
+ }
+
+ private:
+ using LockGuard = ::std::lock_guard<sync::Mutex>;
+ sync::Mutex lock_;
+ IntrusiveList<PacketBuffer> packet_list_ PW_GUARDED_BY(lock_);
+};
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc_transport/public/pw_rpc_transport/local_rpc_egress.h b/pw_rpc_transport/public/pw_rpc_transport/local_rpc_egress.h
new file mode 100644
index 000000000..3492a8dbe
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/local_rpc_egress.h
@@ -0,0 +1,144 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <atomic>
+#include <cstddef>
+
+#include "pw_bytes/span.h"
+#include "pw_result/result.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc_transport/internal/packet_buffer_queue.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_status/status.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/thread_core.h"
+#include "rpc_transport.h"
+
+namespace pw::rpc {
+
+namespace internal {
+void LogNoRpcServiceRegistryError();
+void LogPacketSizeTooLarge(size_t packet_size, size_t max_packet_size);
+void LogEgressThreadNotRunningError();
+void LogFailedToProcessPacket(Status status);
+void LogFailedToAccessPacket(Status status);
+} // namespace internal
+
+// Handles RPC packets destined for the local receiver.
+template <size_t kPacketQueueSize, size_t kMaxPacketSize>
+class LocalRpcEgress : public RpcEgressHandler,
+ public ChannelOutput,
+ public thread::ThreadCore {
+ using PacketBuffer =
+ typename internal::PacketBufferQueue<kMaxPacketSize>::PacketBuffer;
+
+ public:
+ LocalRpcEgress() : ChannelOutput("RPC local egress") {}
+ ~LocalRpcEgress() override { Stop(); }
+
+ // Packet processor cannot be passed as a construction dependency as it would
+ // create a circular dependency in the RPC transport configuration.
+ void set_packet_processor(RpcPacketProcessor& packet_processor) {
+ packet_processor_ = &packet_processor;
+ }
+
+ // Adds the packet to the transmit queue. The queue is continuously processed
+ // by another thread. Implements RpcEgressHandler.
+ Status SendRpcPacket(ConstByteSpan rpc_packet) override;
+
+ // Implements ChannelOutput.
+ Status Send(ConstByteSpan buffer) override { return SendRpcPacket(buffer); }
+
+ // Once stopped, LocalRpcEgress will no longer process data and
+ // will report errors on SendPacket().
+ void Stop() {
+ if (stopped_) {
+ return;
+ }
+ stopped_ = true;
+ // Unblock the processing thread and let it finish gracefully.
+ process_queue_.release();
+ }
+
+ private:
+ void Run() override;
+
+ sync::ThreadNotification process_queue_;
+ RpcPacketProcessor* packet_processor_ = nullptr;
+ std::array<PacketBuffer, kPacketQueueSize> packet_storage_;
+ internal::PacketBufferQueue<kMaxPacketSize> packet_queue_{packet_storage_};
+ internal::PacketBufferQueue<kMaxPacketSize> transmit_queue_ = {};
+ std::atomic<bool> stopped_ = false;
+};
+
+template <size_t kPacketQueueSize, size_t kMaxPacketSize>
+Status LocalRpcEgress<kPacketQueueSize, kMaxPacketSize>::SendRpcPacket(
+ ConstByteSpan packet) {
+ if (!packet_processor_) {
+ internal::LogNoRpcServiceRegistryError();
+ return Status::FailedPrecondition();
+ }
+ if (packet.size() > kMaxPacketSize) {
+ internal::LogPacketSizeTooLarge(packet.size(), kMaxPacketSize);
+ return Status::InvalidArgument();
+ }
+ if (stopped_) {
+ internal::LogEgressThreadNotRunningError();
+ return Status::FailedPrecondition();
+ }
+
+ // Grab a free packet from the egress' pool, copy incoming frame and
+ // push it the queue for processing.
+ PW_TRY_ASSIGN(auto packet_buffer, packet_queue_.Pop());
+ PW_TRY(packet_buffer->CopyPacket(packet));
+
+ transmit_queue_.Push(*packet_buffer);
+
+ process_queue_.release();
+
+ if (stopped_) {
+ internal::LogEgressThreadNotRunningError();
+ return Status::DataLoss();
+ }
+
+ return OkStatus();
+}
+
+template <size_t kPacketQueueSize, size_t kMaxPacketSize>
+void LocalRpcEgress<kPacketQueueSize, kMaxPacketSize>::Run() {
+ while (!stopped_) {
+ // Wait until a client has signaled that there is data in the packet queue.
+ process_queue_.acquire();
+
+ while (true) {
+ Result<PacketBuffer*> packet_buffer = transmit_queue_.Pop();
+ if (!packet_buffer.ok()) {
+ break;
+ }
+ Result<ConstByteSpan> packet = (*packet_buffer)->GetPacket();
+ if (packet.ok()) {
+ if (const auto status = packet_processor_->ProcessRpcPacket(*packet);
+ !status.ok()) {
+ internal::LogFailedToProcessPacket(status);
+ }
+ } else {
+ internal::LogFailedToAccessPacket(packet.status());
+ }
+ packet_queue_.Push(**packet_buffer);
+ }
+ }
+}
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/rpc_transport.h b/pw_rpc_transport/public/pw_rpc_transport/rpc_transport.h
new file mode 100644
index 000000000..f21cd7689
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/rpc_transport.h
@@ -0,0 +1,108 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_bytes/span.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc {
+
+// pw_rpc transport layer interfaces.
+
+// Framed RPC data ready to be sent via `RpcFrameSender`. Consists of a header
+// and a payload. Some RPC transport encodings may not require a header and put
+// all of the framed data into the payload (in which case the header can be
+// an empty span).
+//
+// A single RPC packet can be split into multiple RpcFrame's depending on the
+// MTU of the transport.
+//
+// All frames for an RPC packet are expected to be sent and received in order
+// without being interleaved by other packets' frames.
+struct RpcFrame {
+ ConstByteSpan header;
+ ConstByteSpan payload;
+};
+
+// RpcFrameSender encapsulates the details of sending the packet over
+// some communication channel (e.g. a hardware mailbox, shared memory, or a
+// socket). It exposes its maximum transmission unit (MTU) size and generally
+// should know how to send an `RpcFrame` of a size that is smaller or equal than
+// the MTU.
+class RpcFrameSender {
+ public:
+ virtual ~RpcFrameSender() = default;
+ virtual size_t MaximumTransmissionUnit() const = 0;
+ virtual Status Send(RpcFrame frame) = 0;
+};
+
+// Gets called by `RpcPacketEncoder` for each frame that it emits.
+using OnRpcFrameEncodedCallback = pw::Function<Status(RpcFrame&)>;
+
+// Gets called by `RpcPacketDecoder` for each RPC packet that it detects.
+using OnRpcPacketDecodedCallback = pw::Function<void(ConstByteSpan)>;
+
+// RpcPacketEncoder takes an RPC packet, the max frame size, splits the packet
+// into frames not exceeding that size and calls the provided callback with
+// each produced frame.
+template <class Encoder>
+class RpcPacketEncoder {
+ public:
+ Status Encode(ConstByteSpan rpc_packet,
+ size_t max_frame_size,
+ OnRpcFrameEncodedCallback&& callback) {
+ return static_cast<Encoder*>(this)->Encode(
+ rpc_packet, max_frame_size, std::move(callback));
+ }
+};
+
+// RpcPacketDecoder finds and decodes RPC frames in the provided buffer. Once
+// all frames for an RPC packet are decoded, the callback is invoked with a
+// decoded RPC packet as an argument.
+//
+// Frames from the same RPC packet are expected to be received in order and
+// without being interleaved with frames from any other packets.
+template <class Decoder>
+class RpcPacketDecoder {
+ public:
+ Status Decode(ConstByteSpan buffer, OnRpcPacketDecodedCallback&& callback) {
+ return static_cast<Decoder*>(this)->Decode(buffer, std::move(callback));
+ }
+};
+
+// Provides means of sending an RPC packet. A typical implementation ties
+// transport and encoder together, although some implementations may not require
+// any encoding (e.g. LocalRpcEgress).
+class RpcEgressHandler {
+ public:
+ virtual ~RpcEgressHandler() = default;
+ virtual Status SendRpcPacket(ConstByteSpan rpc_packet) = 0;
+};
+
+// Provides means of receiving a stream of RPC packets. A typical implementation
+// ties transport and decoder together.
+class RpcIngressHandler {
+ public:
+ virtual ~RpcIngressHandler() = default;
+ virtual Status ProcessIncomingData(ConstByteSpan buffer) = 0;
+};
+
+// A decoded RPC packet is passed to RpcPacketProcessor for further handling.
+class RpcPacketProcessor {
+ public:
+ virtual ~RpcPacketProcessor() = default;
+ virtual Status ProcessRpcPacket(ConstByteSpan rpc_packet) = 0;
+};
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/service_registry.h b/pw_rpc_transport/public/pw_rpc_transport/service_registry.h
new file mode 100644
index 000000000..e31315e84
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/service_registry.h
@@ -0,0 +1,57 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_rpc/client_server.h"
+#include "pw_rpc/packet_meta.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc {
+
+// An RpcPacketProcessor implementation that uses an incoming RPC packet
+// metadata to find its target service and sends the packet to that service for
+// processing.
+class ServiceRegistry : public RpcPacketProcessor {
+ public:
+ explicit ServiceRegistry(span<Channel> channels) : client_server_(channels) {}
+
+ ClientServer& client_server() { return client_server_; }
+
+ template <typename Service>
+ typename Service::Client CreateClient(uint32_t channel_id) {
+ return typename Service::Client(client_server_.client(), channel_id);
+ }
+
+ void RegisterService(Service& service) {
+ client_server_.server().RegisterService(service);
+ }
+
+ Status ProcessRpcPacket(ConstByteSpan rpc_packet) override {
+ PW_TRY_ASSIGN(const auto meta, PacketMeta::FromBuffer(rpc_packet));
+ if (meta.destination_is_client()) {
+ return client_server_.client().ProcessPacket(rpc_packet);
+ }
+ if (meta.destination_is_server()) {
+ return client_server_.server().ProcessPacket(rpc_packet);
+ }
+ return Status::DataLoss();
+ }
+
+ private:
+ ClientServer client_server_;
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/simple_framing.h b/pw_rpc_transport/public/pw_rpc_transport/simple_framing.h
new file mode 100644
index 000000000..71c466550
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/simple_framing.h
@@ -0,0 +1,252 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+#include "rpc_transport.h"
+
+namespace pw::rpc {
+
+// The following encoder and decoder implement a very simple RPC framing
+// protocol where the first frame contains the total packet size in the header
+// and up to max frame size bytes in the payload. The subsequent frames of the
+// same packet have an empty header and the rest of the packet in their payload.
+//
+// First frame header also contains a special marker as an attempt to
+// resynchronize the receiver if some frames were not sent (although we expect
+// all transports using this framing type to be reliable, it's still possible
+// that some random transport write timeout result in only the first few frames
+// being sent and others dropped; in that case we attempt best effort recovery
+// by effectively skipping the input until we see something that resembles a
+// valid header).
+//
+// Both encoder and decoder are not thread-safe. The caller must ensure their
+// correct use in a multi-threaded environment.
+
+namespace internal {
+
+void LogReceivedRpcPacketTooLarge(size_t packet_size, size_t max_packet_size);
+void LogMalformedRpcFrameHeader();
+
+} // namespace internal
+
+template <size_t kMaxPacketSize>
+class SimpleRpcPacketEncoder
+ : public RpcPacketEncoder<SimpleRpcPacketEncoder<kMaxPacketSize>> {
+ static_assert(kMaxPacketSize <= 1 << 16);
+
+ public:
+ static constexpr size_t kHeaderSize = 4;
+ static constexpr uint16_t kFrameMarker = 0x27f1;
+
+ // Encodes `packet` with a simple framing protocol and split the resulting
+ // frame into chunks of `RpcFrame`s where every `RpcFrame` is no longer than
+ // `max_frame_size`. Calls `callback` for for each of the resulting
+ // `RpcFrame`s.
+ Status Encode(ConstByteSpan rpc_packet,
+ size_t max_frame_size,
+ OnRpcFrameEncodedCallback&& callback) {
+ if (rpc_packet.size() > kMaxPacketSize) {
+ return Status::FailedPrecondition();
+ }
+ if (max_frame_size <= kHeaderSize) {
+ return Status::FailedPrecondition();
+ }
+
+ // First frame. This is the only frame that contains a header.
+ const auto first_frame_size =
+ std::min(max_frame_size - kHeaderSize, rpc_packet.size());
+
+ std::array<std::byte, kHeaderSize> header{
+ std::byte{kFrameMarker & 0xff},
+ std::byte{(kFrameMarker >> 8) & 0xff},
+ static_cast<std::byte>(rpc_packet.size() & 0xff),
+ static_cast<std::byte>((rpc_packet.size() >> 8) & 0xff),
+ };
+
+ RpcFrame frame{.header = span(header),
+ .payload = rpc_packet.first(first_frame_size)};
+ PW_TRY(callback(frame));
+ auto remaining = rpc_packet.subspan(first_frame_size);
+
+ // Second and subsequent frames (if any).
+ while (!remaining.empty()) {
+ auto fragment_size = std::min(max_frame_size, remaining.size());
+ RpcFrame next_frame{.header = {},
+ .payload = remaining.first(fragment_size)};
+ PW_TRY(callback(next_frame));
+ remaining = remaining.subspan(fragment_size);
+ }
+
+ return OkStatus();
+ }
+};
+
+template <size_t kMaxPacketSize>
+class SimpleRpcPacketDecoder
+ : public RpcPacketDecoder<SimpleRpcPacketDecoder<kMaxPacketSize>> {
+ using Encoder = SimpleRpcPacketEncoder<kMaxPacketSize>;
+
+ public:
+ SimpleRpcPacketDecoder() { ExpectHeader(); }
+
+ // Find and decodes `RpcFrame`s in `buffer`. `buffer` may contain zero or
+ // more frames for zero or more packets. Calls `callback` for each
+ // well-formed packet. Malformed packets are ignored and dropped.
+ Status Decode(ConstByteSpan buffer, OnRpcPacketDecodedCallback&& callback) {
+ while (!buffer.empty()) {
+ switch (state_) {
+ case State::kReadingHeader: {
+ buffer = buffer.subspan(ReadHeader(buffer));
+ break;
+ }
+ case State::kReadingPayload: {
+ // Payload can only follow a valid header, reset the flag here so
+ // that next invalid header logs again.
+ already_logged_invalid_header_ = false;
+ buffer = buffer.subspan(ReadPayload(buffer, callback));
+ break;
+ }
+ }
+ }
+ return OkStatus();
+ }
+
+ private:
+ enum class State {
+ kReadingHeader,
+ kReadingPayload,
+ };
+
+ size_t ReadHeader(ConstByteSpan buffer);
+
+ size_t ReadPayload(ConstByteSpan buffer,
+ const OnRpcPacketDecodedCallback& callback);
+
+ void ExpectHeader() {
+ state_ = State::kReadingHeader;
+ bytes_read_ = 0;
+ bytes_remaining_ = Encoder::kHeaderSize;
+ }
+
+ void ExpectPayload(size_t size) {
+ state_ = State::kReadingPayload;
+ bytes_read_ = 0;
+ bytes_remaining_ = size;
+ }
+
+ std::array<std::byte, kMaxPacketSize> packet_{};
+ std::array<std::byte, Encoder::kHeaderSize> header_{};
+
+ // Current decoder state.
+ State state_;
+ // How many bytes were read in the current state.
+ size_t bytes_read_ = 0;
+ // How many bytes remain to read in the current state.
+ size_t bytes_remaining_ = 0;
+ // When true, discard the received payload instead of buffering it (because
+ // it's too big to buffer).
+ bool discard_payload_ = false;
+ // When true, skip logging on invalid header if we already logged. This is
+ // to prevent logging on every payload byte of a malformed frame.
+ bool already_logged_invalid_header_ = false;
+};
+
+template <size_t kMaxPacketSize>
+size_t SimpleRpcPacketDecoder<kMaxPacketSize>::ReadHeader(
+ ConstByteSpan buffer) {
+ const auto read_size = std::min(buffer.size(), bytes_remaining_);
+ bool header_available = false;
+ PW_DASSERT(read_size <= Encoder::kHeaderSize);
+
+ std::memcpy(header_.data() + bytes_read_, buffer.data(), read_size);
+ bytes_read_ += read_size;
+ bytes_remaining_ -= read_size;
+ header_available = bytes_remaining_ == 0;
+
+ if (header_available) {
+ uint16_t marker = (static_cast<uint16_t>(header_[1]) << 8) |
+ static_cast<uint16_t>(header_[0]);
+ uint16_t packet_size = (static_cast<uint16_t>(header_[3]) << 8) |
+ static_cast<uint16_t>(header_[2]);
+
+ if (marker != Encoder::kFrameMarker) {
+ // We expected a header but received some data that is definitely not
+ // a header. Skip it and keep waiting for the next header. This could
+ // also be a false positive, e.g. in the worst case we treat some
+ // random data as a header: even then we should eventually be able to
+ // stumble upon a real header and start processing packets again.
+ ExpectHeader();
+ // Consume only a single byte since we're looking for a header in a
+ // broken stream and it could start at the next byte.
+ if (!already_logged_invalid_header_) {
+ internal::LogMalformedRpcFrameHeader();
+ already_logged_invalid_header_ = true;
+ }
+ return 1;
+ }
+ if (packet_size > kMaxPacketSize) {
+ // Consume both header and packet without saving it, as it's too big
+ // for the buffer. This is likely due to max packet size mismatch
+ // between the encoder and the decoder.
+ internal::LogReceivedRpcPacketTooLarge(packet_size, kMaxPacketSize);
+ discard_payload_ = true;
+ }
+ ExpectPayload(packet_size);
+ }
+
+ return read_size;
+}
+
+template <size_t kMaxPacketSize>
+size_t SimpleRpcPacketDecoder<kMaxPacketSize>::ReadPayload(
+ ConstByteSpan buffer, const OnRpcPacketDecodedCallback& callback) {
+ if (buffer.size() >= bytes_remaining_ && bytes_read_ == 0) {
+ const auto read_size = bytes_remaining_;
+ // We have the whole packet available in the buffer, no need to copy
+ // it into an internal buffer.
+ callback(buffer.first(read_size));
+ ExpectHeader();
+ return read_size;
+ }
+ // Frame has been split between multiple inputs: assembling it in
+ // an internal buffer.
+ const auto read_size = std::min(buffer.size(), bytes_remaining_);
+
+ // We could be discarding the payload if it was too big to fit into our
+ // packet buffer.
+ if (!discard_payload_) {
+ PW_DASSERT(bytes_read_ + read_size <= packet_.size());
+ std::memcpy(packet_.data() + bytes_read_, buffer.data(), read_size);
+ }
+
+ bytes_read_ += read_size;
+ bytes_remaining_ -= read_size;
+ if (bytes_remaining_ == 0) {
+ if (discard_payload_) {
+ discard_payload_ = false;
+ } else {
+ auto packet_span = span(packet_);
+ callback(packet_span.first(bytes_read_));
+ }
+ ExpectHeader();
+ }
+ return read_size;
+}
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/socket_rpc_transport.h b/pw_rpc_transport/public/pw_rpc_transport/socket_rpc_transport.h
new file mode 100644
index 000000000..e3a84c5ea
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/socket_rpc_transport.h
@@ -0,0 +1,232 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <signal.h>
+
+#include <atomic>
+#include <mutex>
+
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+#include "pw_stream/socket_stream.h"
+#include "pw_sync/condition_variable.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/thread_core.h"
+
+namespace pw::rpc {
+
+namespace internal {
+
+void LogSocketListenError(Status);
+void LogSocketAcceptError(Status);
+void LogSocketConnectError(Status);
+void LogSocketReadError(Status);
+void LogSocketIngressHandlerError(Status);
+
+} // namespace internal
+
+template <size_t kReadBufferSize>
+class SocketRpcTransport : public RpcFrameSender, public thread::ThreadCore {
+ public:
+ struct AsServer {};
+ struct AsClient {};
+
+ static constexpr AsServer kAsServer{};
+ static constexpr AsClient kAsClient{};
+
+ SocketRpcTransport(AsServer, uint16_t port)
+ : role_(ClientServerRole::kServer), port_(port) {}
+
+ SocketRpcTransport(AsServer, uint16_t port, RpcIngressHandler& ingress)
+ : role_(ClientServerRole::kServer), port_(port), ingress_(&ingress) {}
+
+ SocketRpcTransport(AsClient, std::string_view host, uint16_t port)
+ : role_(ClientServerRole::kClient), host_(host), port_(port) {}
+
+ SocketRpcTransport(AsClient,
+ std::string_view host,
+ uint16_t port,
+ RpcIngressHandler& ingress)
+ : role_(ClientServerRole::kClient),
+ host_(host),
+ port_(port),
+ ingress_(&ingress) {}
+
+ size_t MaximumTransmissionUnit() const override { return kReadBufferSize; }
+ size_t port() const { return port_; }
+ void set_ingress(RpcIngressHandler& ingress) { ingress_ = &ingress; }
+
+ Status Send(RpcFrame frame) override {
+ std::lock_guard lock(write_mutex_);
+ PW_TRY(socket_stream_.Write(frame.header));
+ PW_TRY(socket_stream_.Write(frame.payload));
+ return OkStatus();
+ }
+
+ // Returns once the transport is connected to its peer.
+ void WaitUntilConnected() {
+ std::unique_lock lock(connected_mutex_);
+ connected_cv_.wait(lock, [this]() { return connected_; });
+ }
+
+ // Returns once the transport is ready to be used (e.g. the server is
+ // listening on the port or the client is ready to connect).
+ void WaitUntilReady() {
+ std::unique_lock lock(ready_mutex_);
+ ready_cv_.wait(lock, [this]() { return ready_; });
+ }
+
+ void Start() {
+ while (!stopped_) {
+ const auto connect_status = EstablishConnection();
+ if (!connect_status.ok()) {
+ this_thread::sleep_for(kConnectionRetryPeriod);
+ continue;
+ }
+ NotifyConnected();
+
+ while (!stopped_) {
+ const auto read_status = ReadData();
+ if (!read_status.ok()) {
+ internal::LogSocketReadError(read_status);
+ }
+ if (read_status.IsOutOfRange()) {
+ // Need to reconnect (we don't close the stream here because it's
+ // already done in SocketStream::DoRead).
+ {
+ std::lock_guard lock(connected_mutex_);
+ connected_ = false;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ void Stop() { stopped_ = true; }
+
+ private:
+ enum class ClientServerRole { kClient, kServer };
+ static constexpr chrono::SystemClock::duration kConnectionRetryPeriod =
+ std::chrono::milliseconds(100);
+
+ void Run() override { Start(); }
+
+ // Establishes or accepts a new socket connection. Returns when socket_stream_
+ // contains a valid socket connection, or when the transport is stopped.
+ Status EstablishConnection() {
+ if (role_ == ClientServerRole::kServer) {
+ return Serve();
+ }
+ return Connect();
+ }
+
+ Status Serve() {
+ PW_DASSERT(role_ == ClientServerRole::kServer);
+
+ if (!listening_) {
+ const auto listen_status = server_socket_.Listen(port_);
+ if (!listen_status.ok()) {
+ internal::LogSocketListenError(listen_status);
+ return listen_status;
+ }
+ }
+
+ listening_ = true;
+ port_ = server_socket_.port();
+ NotifyReady();
+
+ Result<stream::SocketStream> stream = server_socket_.Accept();
+ if (!stream.ok()) {
+ internal::LogSocketAcceptError(stream.status());
+ return stream.status();
+ }
+ // Ensure that the writer is done writing before updating the stream.
+ std::lock_guard lock(write_mutex_);
+ socket_stream_ = std::move(*stream);
+ return OkStatus();
+ }
+
+ Status Connect() {
+ PW_DASSERT(role_ == ClientServerRole::kClient);
+ NotifyReady();
+
+ std::lock_guard lock(write_mutex_);
+ auto connect_status = socket_stream_.Connect(host_.c_str(), port_);
+ if (!connect_status.ok()) {
+ internal::LogSocketConnectError(connect_status);
+ }
+ return connect_status;
+ }
+
+ Status ReadData() {
+ PW_DASSERT(ingress_ != nullptr);
+ PW_TRY_ASSIGN(auto buffer, socket_stream_.Read(read_buffer_));
+ const auto ingress_status = ingress_->ProcessIncomingData(buffer);
+ if (!ingress_status.ok()) {
+ internal::LogSocketIngressHandlerError(ingress_status);
+ }
+ // ReadData only returns socket stream read errors; ingress errors are only
+ // logged.
+ return OkStatus();
+ }
+
+ void NotifyConnected() {
+ {
+ std::lock_guard lock(connected_mutex_);
+ connected_ = true;
+ }
+ connected_cv_.notify_all();
+ }
+
+ void NotifyReady() {
+ {
+ std::lock_guard lock(ready_mutex_);
+ ready_ = true;
+ }
+ ready_cv_.notify_all();
+ }
+
+ ClientServerRole role_;
+ const std::string host_;
+ std::atomic<uint16_t> port_;
+ RpcIngressHandler* ingress_ = nullptr;
+
+ // write_mutex_ must be held by the thread performing socket writes.
+ sync::Mutex write_mutex_;
+ stream::SocketStream socket_stream_;
+ stream::ServerSocket server_socket_;
+
+ sync::Mutex ready_mutex_;
+ sync::ConditionVariable ready_cv_;
+ bool ready_ = false;
+
+ sync::Mutex connected_mutex_;
+ sync::ConditionVariable connected_cv_;
+ bool connected_ = false;
+
+ std::atomic<bool> stopped_ = false;
+ bool listening_ = false;
+ std::array<std::byte, kReadBufferSize> read_buffer_{};
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/stream_rpc_dispatcher.h b/pw_rpc_transport/public/pw_rpc_transport/stream_rpc_dispatcher.h
new file mode 100644
index 000000000..ac187d2f0
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/stream_rpc_dispatcher.h
@@ -0,0 +1,77 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+#include <atomic>
+#include <cstddef>
+
+#include "pw_metric/metric.h"
+#include "pw_rpc_transport/egress_ingress.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+#include "pw_thread/thread.h"
+
+namespace pw::rpc {
+
+template <size_t kReadSize>
+class StreamRpcDispatcher : public pw::thread::ThreadCore {
+ public:
+ StreamRpcDispatcher(pw::stream::Reader& reader,
+ pw::rpc::RpcIngressHandler& ingress_handler)
+ : reader_(reader), ingress_handler_(ingress_handler) {}
+ ~StreamRpcDispatcher() override { Stop(); }
+
+ const metric::Group& metrics() const { return metrics_; }
+
+ uint32_t num_read_errors() const { return read_errors_.value(); }
+ uint32_t num_egress_errors() const { return egress_errors_.value(); }
+
+ // Once stopped, will no longer process data.
+ void Stop() {
+ if (stopped_) {
+ return;
+ }
+ stopped_ = true;
+ }
+
+ protected:
+ // From pw::thread::ThreadCore.
+ void Run() final {
+ while (!stopped_) {
+ auto read = reader_.Read(read_buffer_);
+ if (!read.ok()) {
+ read_errors_.Increment();
+ continue;
+ }
+
+ if (const auto status = ingress_handler_.ProcessIncomingData(*read);
+ !status.ok()) {
+ egress_errors_.Increment();
+ continue;
+ }
+ }
+ }
+
+ private:
+ std::array<std::byte, kReadSize> read_buffer_ = {};
+ pw::stream::Reader& reader_;
+ pw::rpc::RpcIngressHandler& ingress_handler_;
+ std::atomic<bool> stopped_ = false;
+ PW_METRIC_GROUP(metrics_, "pw_rpc_stream_rpc_dispatcher");
+ PW_METRIC(metrics_, read_errors_, "read_errors", 0u);
+ PW_METRIC(metrics_, egress_errors_, "egress_errors", 0u);
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/stream_rpc_frame_sender.h b/pw_rpc_transport/public/pw_rpc_transport/stream_rpc_frame_sender.h
new file mode 100644
index 000000000..d61f954a6
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/stream_rpc_frame_sender.h
@@ -0,0 +1,40 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_rpc_transport/rpc_transport.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+#include "pw_stream/stream.h"
+
+namespace pw::rpc {
+
+// RpcFrameSender that wraps a stream::Writer.
+template <size_t kMtu>
+class StreamRpcFrameSender : public RpcFrameSender {
+ public:
+ StreamRpcFrameSender(stream::Writer& writer) : writer_(writer) {}
+
+ size_t MaximumTransmissionUnit() const override { return kMtu; }
+
+ Status Send(RpcFrame frame) override {
+ PW_TRY(writer_.Write(frame.header));
+ return writer_.Write(frame.payload);
+ }
+
+ private:
+ stream::Writer& writer_;
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/public/pw_rpc_transport/test_loopback_service_registry.h b/pw_rpc_transport/public/pw_rpc_transport/test_loopback_service_registry.h
new file mode 100644
index 000000000..3ab546b54
--- /dev/null
+++ b/pw_rpc_transport/public/pw_rpc_transport/test_loopback_service_registry.h
@@ -0,0 +1,120 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <queue>
+
+#include "pw_rpc_transport/egress_ingress.h"
+#include "pw_rpc_transport/service_registry.h"
+#include "pw_work_queue/test_thread.h"
+#include "pw_work_queue/work_queue.h"
+
+namespace pw::rpc {
+
+// A transport that loops back all received frames to a given ingress.
+class TestLoopbackTransport : public RpcFrameSender {
+ public:
+ explicit TestLoopbackTransport(size_t mtu) : mtu_(mtu) {
+ work_thread_ =
+ thread::Thread(work_queue::test::WorkQueueThreadOptions(), work_queue_);
+ }
+
+ ~TestLoopbackTransport() override {
+ work_queue_.RequestStop();
+#if PW_THREAD_JOINING_ENABLED
+ work_thread_.join();
+#else
+ work_thread_.detach();
+#endif // PW_THREAD_JOINING_ENABLED
+ }
+
+ size_t MaximumTransmissionUnit() const override { return mtu_; }
+
+ Status Send(RpcFrame frame) override {
+ buffer_queue_.emplace();
+ std::vector<std::byte>& buffer = buffer_queue_.back();
+ std::copy(
+ frame.header.begin(), frame.header.end(), std::back_inserter(buffer));
+ std::copy(
+ frame.payload.begin(), frame.payload.end(), std::back_inserter(buffer));
+
+ // Defer processing frame on ingress to avoid deadlocks.
+ return work_queue_.PushWork([this]() {
+ ingress_->ProcessIncomingData(buffer_queue_.front()).IgnoreError();
+ buffer_queue_.pop();
+ });
+ }
+
+ void SetIngress(RpcIngressHandler& ingress) { ingress_ = &ingress; }
+
+ private:
+ size_t mtu_;
+ std::queue<std::vector<std::byte>> buffer_queue_;
+ RpcIngressHandler* ingress_ = nullptr;
+ thread::Thread work_thread_;
+ work_queue::WorkQueueWithBuffer<1> work_queue_;
+};
+
+// An egress handler that passes the received RPC packet to the service
+// registry.
+class TestLocalEgress : public RpcEgressHandler {
+ public:
+ Status SendRpcPacket(ConstByteSpan packet) override {
+ if (!registry_) {
+ return Status::FailedPrecondition();
+ }
+ return registry_->ProcessRpcPacket(packet);
+ }
+
+ void SetRegistry(ServiceRegistry& registry) { registry_ = &registry; }
+
+ private:
+ ServiceRegistry* registry_ = nullptr;
+};
+
+class TestLoopbackServiceRegistry : public ServiceRegistry {
+ public:
+#if PW_RPC_DYNAMIC_ALLOCATION
+ static constexpr int kInitTxChannelCount = 0;
+#else
+ static constexpr int kInitTxChannelCount = 1;
+#endif
+ static constexpr int kTestChannelId = 1;
+ static constexpr size_t kMtu = 512;
+ static constexpr size_t kMaxPacketSize = 256;
+
+ TestLoopbackServiceRegistry() : ServiceRegistry(tx_channels_) {
+ PW_ASSERT(
+ client_server().client().OpenChannel(kTestChannelId, egress_).ok());
+#if PW_RPC_DYNAMIC_ALLOCATION
+ PW_ASSERT(
+ client_server().server().OpenChannel(kTestChannelId, egress_).ok());
+#endif
+ transport_.SetIngress(ingress_);
+ local_egress_.SetRegistry(*this);
+ }
+
+ int channel_id() const { return kTestChannelId; }
+
+ private:
+ TestLoopbackTransport transport_{kMtu};
+ TestLocalEgress local_egress_;
+ SimpleRpcEgress<kMaxPacketSize> egress_{"egress", transport_};
+ std::array<Channel, kInitTxChannelCount> tx_channels_;
+ std::array<ChannelEgress, 1> rx_channels_ = {
+ rpc::ChannelEgress{kTestChannelId, local_egress_}};
+ SimpleRpcIngress<kMaxPacketSize> ingress_{rx_channels_};
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/rpc_integration_test.cc b/pw_rpc_transport/rpc_integration_test.cc
new file mode 100644
index 000000000..c48672d3e
--- /dev/null
+++ b/pw_rpc_transport/rpc_integration_test.cc
@@ -0,0 +1,136 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/client_server.h"
+#include "pw_rpc/synchronous_call.h"
+#include "pw_rpc_transport/egress_ingress.h"
+#include "pw_rpc_transport/internal/test.rpc.pwpb.h"
+#include "pw_rpc_transport/local_rpc_egress.h"
+#include "pw_rpc_transport/service_registry.h"
+#include "pw_rpc_transport/socket_rpc_transport.h"
+#include "pw_status/status.h"
+#include "pw_string/string.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/thread_core.h"
+#include "pw_thread_stl/options.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::chrono_literals;
+
+constexpr size_t kMaxTestMessageSize = 1024;
+constexpr uint32_t kTestChannelId = 1;
+
+class TestService final
+ : public pw_rpc_transport::testing::pw_rpc::pwpb::TestService::Service<
+ TestService> {
+ public:
+ Status Echo(
+ const pw_rpc_transport::testing::pwpb::EchoMessage::Message& request,
+ pw_rpc_transport::testing::pwpb::EchoMessage::Message& response) {
+ response.msg = request.msg;
+ return OkStatus();
+ }
+};
+
+template <size_t kMaxPacketSize, size_t kLocalEgressQueueSize>
+struct SocketRpcEndpoint {
+ explicit SocketRpcEndpoint(SocketRpcTransport<kMaxPacketSize>& transport)
+ : transport(transport),
+ rpc_egress("tx", transport),
+ tx_channels({rpc::Channel::Create<kTestChannelId>(&rpc_egress)}),
+ rx_channels({ChannelEgress{kTestChannelId, local_egress}}),
+ rpc_ingress(rx_channels),
+ service_registry(tx_channels) {
+ local_egress.set_packet_processor(service_registry);
+ transport.set_ingress(rpc_ingress);
+ }
+
+ LocalRpcEgress<kLocalEgressQueueSize, kMaxPacketSize> local_egress;
+ SocketRpcTransport<kMaxPacketSize>& transport;
+ SimpleRpcEgress<kMaxPacketSize> rpc_egress;
+ std::array<rpc::Channel, 1> tx_channels;
+ std::array<ChannelEgress, 1> rx_channels;
+ SimpleRpcIngress<kMaxPacketSize> rpc_ingress;
+ ServiceRegistry service_registry;
+};
+
+TEST(RpcIntegrationTest, SocketTransport) {
+ constexpr size_t kMaxPacketSize = 512;
+ constexpr size_t kLocalEgressQueueSize = 20;
+ constexpr size_t kMessageSize = 50;
+
+ SocketRpcTransport<kMaxPacketSize> a_to_b_transport(
+ SocketRpcTransport<kMaxPacketSize>::kAsServer, /*port=*/0);
+ auto a = SocketRpcEndpoint<kMaxPacketSize, kLocalEgressQueueSize>(
+ a_to_b_transport);
+ auto a_local_egress_thread =
+ thread::Thread(thread::stl::Options(), a.local_egress);
+ auto a_transport_thread = thread::Thread(thread::stl::Options(), a.transport);
+
+ a_to_b_transport.WaitUntilReady();
+
+ SocketRpcTransport<kMaxPacketSize> b_to_a_transport(
+ SocketRpcTransport<kMaxPacketSize>::kAsClient,
+ "localhost",
+ a_to_b_transport.port());
+
+ auto b = SocketRpcEndpoint<kMaxPacketSize, kLocalEgressQueueSize>(
+ b_to_a_transport);
+ auto b_local_egress_thread =
+ thread::Thread(thread::stl::Options(), b.local_egress);
+ auto b_transport_thread = thread::Thread(thread::stl::Options(), b.transport);
+
+ TestService b_test_service;
+ b.service_registry.RegisterService(b_test_service);
+ a_to_b_transport.WaitUntilConnected();
+ b_to_a_transport.WaitUntilConnected();
+
+ for (int i = 0; i < 10; ++i) {
+ InlineString<kMaxTestMessageSize> test_message;
+ test_message.append(kMessageSize, '*');
+ auto echo_request = pw_rpc_transport::testing::pwpb::EchoMessage::Message{
+ .msg = test_message};
+ const auto echo_response = rpc::SynchronousCall<
+ pw_rpc_transport::testing::pw_rpc::pwpb::TestService::Echo>(
+ a.service_registry.client_server().client(),
+ kTestChannelId,
+ echo_request);
+ EXPECT_EQ(echo_response.status(), OkStatus());
+ EXPECT_EQ(echo_response.response().msg, test_message);
+ }
+
+ // Shut everything down.
+ a.local_egress.Stop();
+ b.local_egress.Stop();
+ a.transport.Stop();
+ b.transport.Stop();
+
+ // Unblock socket transports by sending terminator packets.
+ const std::array<std::byte, 1> terminator_bytes{std::byte{0x42}};
+ RpcFrame terminator{.header = {}, .payload = terminator_bytes};
+ EXPECT_EQ(a.transport.Send(terminator), OkStatus());
+ EXPECT_EQ(b.transport.Send(terminator), OkStatus());
+
+ a_local_egress_thread.join();
+ b_local_egress_thread.join();
+ a_transport_thread.join();
+ b_transport_thread.join();
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/simple_framing.cc b/pw_rpc_transport/simple_framing.cc
new file mode 100644
index 000000000..8a3655684
--- /dev/null
+++ b/pw_rpc_transport/simple_framing.cc
@@ -0,0 +1,37 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#define PW_LOG_MODULE_NAME "PW_RPC"
+
+#include "pw_rpc_transport/simple_framing.h"
+
+#include <cinttypes>
+
+#include "pw_log/log.h"
+
+namespace pw::rpc::internal {
+
+void LogReceivedRpcPacketTooLarge(size_t packet_size, size_t max_packet_size) {
+ PW_LOG_WARN(
+ "Received RPC packet (%d) bytes) is larger than max packet size (%d "
+ "bytes)",
+ static_cast<int>(packet_size),
+ static_cast<int>(max_packet_size));
+}
+
+void LogMalformedRpcFrameHeader() {
+ PW_LOG_WARN("Skipping malformed RPC frame header");
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc_transport/simple_framing_test.cc b/pw_rpc_transport/simple_framing_test.cc
new file mode 100644
index 000000000..2ac75a5b4
--- /dev/null
+++ b/pw_rpc_transport/simple_framing_test.cc
@@ -0,0 +1,381 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/simple_framing.h"
+
+#include <algorithm>
+#include <array>
+#include <random>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc {
+namespace {
+
+constexpr size_t kMaxPacketSize = 256;
+
+struct TestParams {
+ size_t packet_size = 0;
+ size_t max_frame_size = 0;
+};
+
+constexpr std::array<TestParams, 8> kTestCases = {
+ // Packet fits in one frame.
+ TestParams{.packet_size = 5, .max_frame_size = 100},
+ // Typical parameters for RPC packet and mailbox frame size.
+ TestParams{.packet_size = 100, .max_frame_size = 128},
+ // Smallest packet.
+ TestParams{.packet_size = 1, .max_frame_size = 16},
+ // Small packet, small frame.
+ TestParams{.packet_size = 16, .max_frame_size = 5},
+ // Odd-sized packet, small frame.
+ TestParams{.packet_size = 77, .max_frame_size = 16},
+ // Frame size and packet size off by one.
+ TestParams{.packet_size = 11, .max_frame_size = 10},
+ // Almost at the limit.
+ TestParams{.packet_size = kMaxPacketSize - 1,
+ .max_frame_size = kMaxPacketSize - 2},
+ // At the limit.
+ TestParams{.packet_size = kMaxPacketSize,
+ .max_frame_size = kMaxPacketSize}};
+
+void MakePacket(ByteSpan dst_buffer) {
+ static uint32_t rg_seed = 0x123;
+ unsigned char c = 0;
+ for (auto& i : dst_buffer) {
+ i = std::byte{c++};
+ }
+ std::mt19937 rg(rg_seed++);
+ std::shuffle(dst_buffer.begin(), dst_buffer.end(), rg);
+}
+
+void CopyFrame(RpcFrame frame, std::vector<std::byte>& dst) {
+ std::copy(frame.header.begin(), frame.header.end(), std::back_inserter(dst));
+ std::copy(
+ frame.payload.begin(), frame.payload.end(), std::back_inserter(dst));
+}
+
+TEST(SimpleRpcFrameEncodeDecodeTest, EncodeThenDecode) {
+ for (auto test_case : kTestCases) {
+ const size_t packet_size = test_case.packet_size;
+ const size_t max_frame_size = test_case.max_frame_size;
+ PW_LOG_INFO("EncodeThenDecode: packet_size = %d, max_frame_size = %d",
+ static_cast<int>(packet_size),
+ static_cast<int>(max_frame_size));
+
+ std::vector<std::byte> src(packet_size);
+ MakePacket(src);
+
+ std::vector<std::byte> encoded;
+ std::vector<std::byte> decoded;
+
+ SimpleRpcPacketEncoder<kMaxPacketSize> encoder;
+
+ ASSERT_EQ(encoder.Encode(src,
+ max_frame_size,
+ [&encoded](RpcFrame& frame) {
+ CopyFrame(frame, encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ SimpleRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ ASSERT_EQ(decoder.Decode(encoded,
+ [&decoded](ConstByteSpan packet) {
+ std::copy(packet.begin(),
+ packet.end(),
+ std::back_inserter(decoded));
+ }),
+ OkStatus());
+
+ EXPECT_TRUE(std::equal(src.begin(), src.end(), decoded.begin()));
+ }
+}
+
+TEST(SimpleRpcFrameEncodeDecodeTest, OneByteAtTimeDecoding) {
+ for (auto test_case : kTestCases) {
+ const size_t packet_size = test_case.packet_size;
+ const size_t max_frame_size = test_case.max_frame_size;
+ PW_LOG_INFO("EncodeThenDecode: packet_size = %d, max_frame_size = %d",
+ static_cast<int>(packet_size),
+ static_cast<int>(max_frame_size));
+
+ std::vector<std::byte> src(packet_size);
+ MakePacket(src);
+
+ std::vector<std::byte> encoded;
+ std::vector<std::byte> decoded;
+
+ SimpleRpcPacketEncoder<kMaxPacketSize> encoder;
+
+ ASSERT_EQ(encoder.Encode(src,
+ max_frame_size,
+ [&encoded](RpcFrame& frame) {
+ CopyFrame(frame, encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ SimpleRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ for (std::byte b : encoded) {
+ auto buffer_span = span(&b, 1);
+ ASSERT_EQ(decoder.Decode(buffer_span,
+ [&decoded](ConstByteSpan packet) {
+ std::copy(packet.begin(),
+ packet.end(),
+ std::back_inserter(decoded));
+ }),
+ OkStatus());
+ }
+
+ EXPECT_TRUE(std::equal(src.begin(), src.end(), decoded.begin()));
+ }
+}
+
+TEST(SimpleRpcFrameTest, MissingFirstFrame) {
+ // Sends two packets, the first packet is missing its first frame. The decoder
+ // ignores the remaining frames of the first packet but still picks up the
+ // second packet.
+ constexpr size_t kPacketSize = 77;
+ constexpr size_t kMaxFrameSize = 16;
+
+ std::vector<std::byte> src1(kPacketSize);
+ MakePacket(src1);
+
+ std::vector<std::byte> src2(kPacketSize);
+ MakePacket(src2);
+
+ std::vector<std::byte> decoded;
+
+ SimpleRpcPacketEncoder<kMaxPacketSize> encoder;
+ struct EncodeState {
+ size_t frame_counter = 0;
+ std::vector<std::byte> encoded;
+ } state;
+
+ ASSERT_EQ(encoder.Encode(src1,
+ kMaxFrameSize,
+ [&state](RpcFrame& frame) {
+ state.frame_counter++;
+ if (state.frame_counter > 1) {
+ // Skip the first frame.
+ CopyFrame(frame, state.encoded);
+ }
+ return OkStatus();
+ }),
+ OkStatus());
+
+ ASSERT_EQ(encoder.Encode(src2,
+ kMaxFrameSize,
+ [&state](RpcFrame& frame) {
+ CopyFrame(frame, state.encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ SimpleRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ ASSERT_EQ(decoder.Decode(state.encoded,
+ [&decoded](ConstByteSpan packet) {
+ std::copy(packet.begin(),
+ packet.end(),
+ std::back_inserter(decoded));
+ }),
+ OkStatus());
+
+ EXPECT_TRUE(std::equal(src2.begin(), src2.end(), decoded.begin()));
+}
+
+TEST(SimpleRpcFrameTest, MissingInternalFrame) {
+ // Sends two packets, the first packet is missing its second frame. The
+ // decoder ignores the remaining frames of the first packet and the second
+ // packet as well but eventually stumbles upon the frame header in the third
+ // packet and processes that packet.
+ constexpr size_t kPacketSize = 77;
+ constexpr size_t kMaxFrameSize = 16;
+
+ std::vector<std::byte> src1(kPacketSize);
+ MakePacket(src1);
+
+ std::vector<std::byte> src2(kPacketSize);
+ MakePacket(src2);
+
+ std::vector<std::byte> src3(kPacketSize);
+ MakePacket(src3);
+
+ std::vector<std::byte> decoded;
+
+ SimpleRpcPacketEncoder<kMaxPacketSize> encoder;
+ struct EncodeState {
+ size_t frame_counter = 0;
+ std::vector<std::byte> encoded;
+ } encode_state;
+
+ ASSERT_EQ(encoder.Encode(src1,
+ kMaxFrameSize,
+ [&encode_state](RpcFrame& frame) {
+ encode_state.frame_counter++;
+ if (encode_state.frame_counter != 2) {
+ // Skip the second frame.
+ CopyFrame(frame, encode_state.encoded);
+ }
+ return OkStatus();
+ }),
+ OkStatus());
+
+ ASSERT_EQ(encoder.Encode(src2,
+ kMaxFrameSize,
+ [&encode_state](RpcFrame& frame) {
+ CopyFrame(frame, encode_state.encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ ASSERT_EQ(encoder.Encode(src3,
+ kMaxFrameSize,
+ [&encode_state](RpcFrame& frame) {
+ CopyFrame(frame, encode_state.encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ SimpleRpcPacketDecoder<kMaxPacketSize> decoder;
+
+ // First packet is decoded but it doesn't have correct bytes, as one of its
+ // frames has never been received. Second packet is not received because its
+ // header has been consumed by the first packet. By that point the decoder
+ // knows that something is wrong and tries to recover as soon as it receives
+ // bytes that look as the valid header. So we eventually receive the third
+ // packet and it is correct.
+ struct DecodeState {
+ std::vector<std::byte> decoded1;
+ std::vector<std::byte> decoded2;
+ size_t packet_counter = 0;
+ } decode_state;
+
+ ASSERT_EQ(
+ decoder.Decode(encode_state.encoded,
+ [&decode_state](ConstByteSpan packet) {
+ decode_state.packet_counter++;
+ if (decode_state.packet_counter == 1) {
+ std::copy(packet.begin(),
+ packet.end(),
+ std::back_inserter(decode_state.decoded1));
+ }
+ if (decode_state.packet_counter == 2) {
+ std::copy(packet.begin(),
+ packet.end(),
+ std::back_inserter(decode_state.decoded2));
+ }
+ }),
+ OkStatus());
+
+ EXPECT_EQ(decode_state.packet_counter, 2ul);
+
+ EXPECT_EQ(decode_state.decoded1.size(), src1.size());
+ EXPECT_FALSE(
+ std::equal(src1.begin(), src1.end(), decode_state.decoded1.begin()));
+
+ EXPECT_TRUE(
+ std::equal(src3.begin(), src3.end(), decode_state.decoded2.begin()));
+}
+
+TEST(SimpleRpcPacketEncoder, PacketTooBig) {
+ SimpleRpcPacketEncoder<kMaxPacketSize> encoder;
+ constexpr size_t kMaxFrameSize = 100;
+ std::array<std::byte, kMaxPacketSize + 1> src{};
+
+ EXPECT_EQ(
+ encoder.Encode(src, kMaxFrameSize, [](RpcFrame&) { return OkStatus(); }),
+ Status::FailedPrecondition());
+}
+
+TEST(SimpleRpcPacketEncoder, MaxFrameSizeTooSmall) {
+ SimpleRpcPacketEncoder<kMaxPacketSize> encoder;
+ std::array<std::byte, kMaxPacketSize> src{};
+
+ EXPECT_EQ(encoder.Encode(
+ src, encoder.kHeaderSize, [](RpcFrame&) { return OkStatus(); }),
+ Status::FailedPrecondition());
+
+ EXPECT_EQ(
+ encoder.Encode(
+ src, encoder.kHeaderSize + 1, [](RpcFrame&) { return OkStatus(); }),
+ OkStatus());
+}
+
+TEST(SimpleRpcFrameTest, EncoderBufferLargerThanDecoderBuffer) {
+ constexpr size_t kLargePacketSize = 150;
+ constexpr size_t kSmallPacketSize = 120;
+ constexpr size_t kMaxFrameSize = 16;
+
+ // Decoder isn't able to receive the whole packet because it needs to be
+ // buffered but the internal buffer is too small; the packet is thus
+ // discarded. The second packet is received without issues as it's small
+ // enough to fit in the decoder buffer.
+ constexpr size_t kEncoderMaxPacketSize = 256;
+ constexpr size_t kDecoderMaxPacketSize = 128;
+
+ std::vector<std::byte> src1(kLargePacketSize);
+ MakePacket(src1);
+
+ std::vector<std::byte> src2(kSmallPacketSize);
+ MakePacket(src1);
+
+ std::vector<std::byte> encoded;
+ std::vector<std::byte> decoded;
+
+ SimpleRpcPacketEncoder<kEncoderMaxPacketSize> encoder;
+
+ ASSERT_EQ(encoder.Encode(src1,
+ kMaxFrameSize,
+ [&encoded](RpcFrame& frame) {
+ CopyFrame(frame, encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ ASSERT_EQ(encoder.Encode(src2,
+ kMaxFrameSize,
+ [&encoded](RpcFrame& frame) {
+ CopyFrame(frame, encoded);
+ return OkStatus();
+ }),
+ OkStatus());
+
+ SimpleRpcPacketDecoder<kDecoderMaxPacketSize> decoder;
+
+ // We have to decode piecemeal here because otherwise the decoder can just
+ // pluck the packet from `encoded` without internally buffering it.
+ for (std::byte b : encoded) {
+ auto buffer_span = span(&b, 1);
+ ASSERT_EQ(decoder.Decode(buffer_span,
+ [&decoded](ConstByteSpan packet) {
+ std::copy(packet.begin(),
+ packet.end(),
+ std::back_inserter(decoded));
+ }),
+ OkStatus());
+ }
+
+ EXPECT_TRUE(std::equal(src2.begin(), src2.end(), decoded.begin()));
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/socket_rpc_transport.cc b/pw_rpc_transport/socket_rpc_transport.cc
new file mode 100644
index 000000000..d6abe892f
--- /dev/null
+++ b/pw_rpc_transport/socket_rpc_transport.cc
@@ -0,0 +1,54 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#define PW_LOG_MODULE_NAME "PW_RPC"
+
+#include "pw_rpc_transport/socket_rpc_transport.h"
+
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc::internal {
+
+void LogSocketListenError(pw::Status status) {
+ PW_LOG_ERROR("SocketRpcTransport: socket listen error. Status %d",
+ status.code());
+}
+
+void LogSocketAcceptError(pw::Status status) {
+ PW_LOG_ERROR("SocketRpcTransport: socket accept error. Status %d",
+ status.code());
+}
+
+void LogSocketConnectError(pw::Status status) {
+ PW_LOG_ERROR("SocketRpcTransport: socket connect error. Status %d",
+ status.code());
+}
+
+void LogSocketReadError(pw::Status status) {
+ if (status.IsOutOfRange()) {
+ PW_LOG_ERROR("SocketRpcTransport: read from a closed socket. Status %d",
+ status.code());
+ return;
+ }
+ PW_LOG_ERROR("SocketRpcTransport: socket read error. Status %d",
+ status.code());
+}
+
+void LogSocketIngressHandlerError(pw::Status status) {
+ PW_LOG_ERROR("SocketRpcTransport: ingress handler error. Status %d",
+ status.code());
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc_transport/socket_rpc_transport_test.cc b/pw_rpc_transport/socket_rpc_transport_test.cc
new file mode 100644
index 000000000..12c0d845c
--- /dev/null
+++ b/pw_rpc_transport/socket_rpc_transport_test.cc
@@ -0,0 +1,364 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/socket_rpc_transport.h"
+
+#include <algorithm>
+#include <random>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_log/log.h"
+#include "pw_rpc_transport/socket_rpc_transport.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_stream/socket_stream.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::chrono_literals;
+
+constexpr size_t kMaxWriteSize = 64;
+constexpr size_t kReadBufferSize = 64;
+// Let the kernel pick the port number.
+constexpr uint16_t kServerPort = 0;
+
+class TestIngress : public RpcIngressHandler {
+ public:
+ explicit TestIngress(size_t num_bytes_expected)
+ : num_bytes_expected_(num_bytes_expected) {}
+
+ Status ProcessIncomingData(ConstByteSpan buffer) override {
+ if (num_bytes_expected_ > 0) {
+ std::copy(buffer.begin(), buffer.end(), std::back_inserter(received_));
+ num_bytes_expected_ -= std::min(num_bytes_expected_, buffer.size());
+ }
+ if (num_bytes_expected_ == 0) {
+ done_.release();
+ }
+ return OkStatus();
+ }
+
+ std::vector<std::byte> received() const { return received_; }
+ void Wait() { done_.acquire(); }
+
+ private:
+ size_t num_bytes_expected_ = 0;
+ sync::ThreadNotification done_;
+ std::vector<std::byte> received_;
+};
+
+class SocketSender {
+ public:
+ SocketSender(SocketRpcTransport<kReadBufferSize>& transport)
+ : transport_(transport) {
+ unsigned char c = 0;
+ for (auto& i : data_) {
+ i = std::byte{c++};
+ }
+ std::mt19937 rg{0x12345678};
+ std::shuffle(data_.begin(), data_.end(), rg);
+ }
+
+ std::vector<std::byte> sent() { return sent_; }
+
+ RpcFrame MakeFrame(size_t max_size) {
+ std::mt19937 rg{0x12345678};
+ size_t offset = offset_dist_(rg);
+ size_t message_size = std::min(size_dist_(rg), max_size);
+ size_t header_size = message_size > 4 ? 4 : message_size;
+ size_t payload_size = message_size > 4 ? message_size - 4 : 0;
+
+ return RpcFrame{.header = span(data_).subspan(offset, header_size),
+ .payload = span(data_).subspan(offset, payload_size)};
+ }
+
+ void Send(size_t num_bytes) {
+ size_t bytes_written = 0;
+ while (bytes_written < num_bytes) {
+ auto frame = MakeFrame(num_bytes - bytes_written);
+ std::copy(
+ frame.header.begin(), frame.header.end(), std::back_inserter(sent_));
+ std::copy(frame.payload.begin(),
+ frame.payload.end(),
+ std::back_inserter(sent_));
+
+ // Tests below expect to see all data written to the socket to be received
+ // by the other end, so we keep retrying on any errors that could happen
+ // during reconnection: in reality it would be up to the higher level
+ // abstractions to do this depending on how they manage buffers etc. For
+ // the tests we just keep retrying indefinitely: if there is a
+ // non-transient problem then the test will eventually time out.
+ while (true) {
+ const auto send_status = transport_.Send(frame);
+ if (send_status.ok()) {
+ break;
+ }
+ }
+
+ bytes_written += frame.header.size() + frame.payload.size();
+ }
+ }
+
+ // stream::SocketStream doesn't support read timeouts so we have to
+ // unblock socket reads by sending more data after the transport is stopped.
+ pw::Status Terminate() { return transport_.Send(terminator_); }
+
+ private:
+ SocketRpcTransport<kReadBufferSize>& transport_;
+ std::vector<std::byte> sent_;
+ std::array<std::byte, 256> data_{};
+ std::uniform_int_distribution<size_t> offset_dist_{0, 255};
+ std::uniform_int_distribution<size_t> size_dist_{1, kMaxWriteSize};
+ std::array<std::byte, 1> terminator_bytes_{std::byte{0x42}};
+ RpcFrame terminator_{.header = {}, .payload = terminator_bytes_};
+};
+
+class SocketSenderThreadCore : public SocketSender, public thread::ThreadCore {
+ public:
+ SocketSenderThreadCore(SocketRpcTransport<kReadBufferSize>& transport,
+ size_t write_size)
+ : SocketSender(transport), write_size_(write_size) {}
+
+ private:
+ void Run() override { Send(write_size_); }
+ size_t write_size_;
+};
+
+TEST(SocketRpcTransportTest, SendAndReceiveFramesOverSocketConnection) {
+ constexpr size_t kWriteSize = 8192;
+
+ TestIngress server_ingress(kWriteSize);
+ TestIngress client_ingress(kWriteSize);
+
+ auto server = SocketRpcTransport<kReadBufferSize>(
+ SocketRpcTransport<kReadBufferSize>::kAsServer,
+ kServerPort,
+ server_ingress);
+ auto server_thread = thread::Thread(thread::stl::Options(), server);
+
+ server.WaitUntilReady();
+ auto server_port = server.port();
+
+ auto client = SocketRpcTransport<kReadBufferSize>(
+ SocketRpcTransport<kReadBufferSize>::kAsClient,
+ "localhost",
+ server_port,
+ client_ingress);
+ auto client_thread = thread::Thread(thread::stl::Options(), client);
+
+ client.WaitUntilConnected();
+ server.WaitUntilConnected();
+
+ SocketSenderThreadCore client_sender(client, kWriteSize);
+ SocketSenderThreadCore server_sender(server, kWriteSize);
+
+ auto client_sender_thread =
+ thread::Thread(thread::stl::Options(), client_sender);
+ auto server_sender_thread =
+ thread::Thread(thread::stl::Options(), server_sender);
+
+ client_sender_thread.join();
+ server_sender_thread.join();
+
+ server_ingress.Wait();
+ client_ingress.Wait();
+
+ server.Stop();
+ client.Stop();
+
+ // Unblock socket reads to propagate the stop signal.
+ EXPECT_EQ(server_sender.Terminate(), OkStatus());
+ EXPECT_EQ(client_sender.Terminate(), OkStatus());
+
+ server_thread.join();
+ client_thread.join();
+
+ auto received_by_server = server_ingress.received();
+ EXPECT_EQ(received_by_server.size(), kWriteSize);
+ EXPECT_TRUE(std::equal(received_by_server.begin(),
+ received_by_server.end(),
+ client_sender.sent().begin()));
+
+ auto received_by_client = client_ingress.received();
+ EXPECT_EQ(received_by_client.size(), kWriteSize);
+ EXPECT_TRUE(std::equal(received_by_client.begin(),
+ received_by_client.end(),
+ server_sender.sent().begin()));
+}
+
+TEST(SocketRpcTransportTest, ServerReconnects) {
+ // Set up a server and a client that reconnects multiple times. The server
+ // must accept the new connection gracefully.
+ constexpr size_t kWriteSize = 8192;
+ std::vector<std::byte> received;
+
+ TestIngress server_ingress(0);
+ auto server = SocketRpcTransport<kReadBufferSize>(
+ SocketRpcTransport<kReadBufferSize>::kAsServer,
+ kServerPort,
+ server_ingress);
+ auto server_thread = thread::Thread(thread::stl::Options(), server);
+
+ server.WaitUntilReady();
+ auto server_port = server.port();
+ SocketSender server_sender(server);
+
+ {
+ TestIngress client_ingress(kWriteSize);
+ auto client = SocketRpcTransport<kReadBufferSize>(
+ SocketRpcTransport<kReadBufferSize>::kAsClient,
+ "localhost",
+ server_port,
+ client_ingress);
+ auto client_thread = thread::Thread(thread::stl::Options(), client);
+
+ client.WaitUntilConnected();
+ server.WaitUntilConnected();
+
+ server_sender.Send(kWriteSize);
+ client_ingress.Wait();
+ auto client_received = client_ingress.received();
+ std::copy(client_received.begin(),
+ client_received.end(),
+ std::back_inserter(received));
+ EXPECT_EQ(received.size(), kWriteSize);
+
+ // Stop the client but not the server: we're re-using the same server
+ // with a new client below.
+ client.Stop();
+ EXPECT_EQ(server_sender.Terminate(), OkStatus());
+ client_thread.join();
+ }
+
+ // Reconnect to the server and keep sending frames.
+ {
+ TestIngress client_ingress(kWriteSize);
+ auto client = SocketRpcTransport<kReadBufferSize>(
+ SocketRpcTransport<kReadBufferSize>::kAsClient,
+ "localhost",
+ server_port,
+ client_ingress);
+ auto client_thread = thread::Thread(thread::stl::Options(), client);
+
+ client.WaitUntilConnected();
+ server.WaitUntilConnected();
+
+ server_sender.Send(kWriteSize);
+ client_ingress.Wait();
+ auto client_received = client_ingress.received();
+ std::copy(client_received.begin(),
+ client_received.end(),
+ std::back_inserter(received));
+
+ client.Stop();
+ EXPECT_EQ(server_sender.Terminate(), OkStatus());
+ client_thread.join();
+
+ // This time stop the server as well.
+ SocketSender client_sender(client);
+ server.Stop();
+ EXPECT_EQ(client_sender.Terminate(), OkStatus());
+ server_thread.join();
+ }
+
+ EXPECT_EQ(received.size(), 2 * kWriteSize);
+ EXPECT_EQ(server_sender.sent().size(), 2 * kWriteSize);
+ EXPECT_TRUE(std::equal(
+ received.begin(), received.end(), server_sender.sent().begin()));
+}
+
+TEST(SocketRpcTransportTest, ClientReconnects) {
+ // Set up a server and a client, then recycle the server. The client must
+ // must reconnect gracefully.
+ constexpr size_t kWriteSize = 8192;
+ uint16_t server_port = 0;
+
+ TestIngress server_ingress(0);
+ TestIngress client_ingress(2 * kWriteSize);
+
+ auto server = std::make_unique<SocketRpcTransport<kReadBufferSize>>(
+ SocketRpcTransport<kReadBufferSize>::kAsServer,
+ kServerPort,
+ server_ingress);
+ auto server_thread = thread::Thread(thread::stl::Options(), *server);
+
+ server->WaitUntilReady();
+ server_port = server->port();
+
+ auto client = SocketRpcTransport<kReadBufferSize>(
+ SocketRpcTransport<kReadBufferSize>::kAsClient,
+ "localhost",
+ server_port,
+ client_ingress);
+ auto client_thread = thread::Thread(thread::stl::Options(), client);
+
+ client.WaitUntilConnected();
+ server->WaitUntilConnected();
+
+ SocketSender client_sender(client);
+ SocketSender server1_sender(*server);
+ std::vector<std::byte> sent_by_server;
+
+ server1_sender.Send(kWriteSize);
+ server->Stop();
+ auto server1_sent = server1_sender.sent();
+ std::copy(server1_sent.begin(),
+ server1_sent.end(),
+ std::back_inserter(sent_by_server));
+
+ EXPECT_EQ(client_sender.Terminate(), OkStatus());
+ server_thread.join();
+ server = nullptr;
+
+ server = std::make_unique<SocketRpcTransport<kReadBufferSize>>(
+ SocketRpcTransport<kReadBufferSize>::kAsServer,
+ server_port,
+ server_ingress);
+ SocketSender server2_sender(*server);
+ server_thread = thread::Thread(thread::stl::Options(), *server);
+
+ client.WaitUntilConnected();
+ server->WaitUntilConnected();
+
+ server2_sender.Send(kWriteSize);
+ client_ingress.Wait();
+
+ server->Stop();
+ auto server2_sent = server2_sender.sent();
+ std::copy(server2_sent.begin(),
+ server2_sent.end(),
+ std::back_inserter(sent_by_server));
+
+ EXPECT_EQ(client_sender.Terminate(), OkStatus());
+ server_thread.join();
+
+ client.Stop();
+ EXPECT_EQ(server2_sender.Terminate(), OkStatus());
+ client_thread.join();
+ server = nullptr;
+
+ auto received_by_client = client_ingress.received();
+ EXPECT_EQ(received_by_client.size(), 2 * kWriteSize);
+ EXPECT_TRUE(std::equal(received_by_client.begin(),
+ received_by_client.end(),
+ sent_by_server.begin()));
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc_transport/stream_rpc_dispatcher_test.cc b/pw_rpc_transport/stream_rpc_dispatcher_test.cc
new file mode 100644
index 000000000..35b735afd
--- /dev/null
+++ b/pw_rpc_transport/stream_rpc_dispatcher_test.cc
@@ -0,0 +1,141 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_rpc_transport/stream_rpc_dispatcher.h"
+
+#include <algorithm>
+#include <atomic>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_log/log.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::chrono_literals;
+
+class TestIngress : public RpcIngressHandler {
+ public:
+ explicit TestIngress(size_t num_bytes_expected)
+ : num_bytes_expected_(num_bytes_expected) {}
+
+ Status ProcessIncomingData(ConstByteSpan buffer) override {
+ if (num_bytes_expected_ > 0) {
+ std::copy(buffer.begin(), buffer.end(), std::back_inserter(received_));
+ num_bytes_expected_ -= std::min(num_bytes_expected_, buffer.size());
+ }
+ if (num_bytes_expected_ == 0) {
+ done_.release();
+ }
+ return OkStatus();
+ }
+
+ std::vector<std::byte> received() const { return received_; }
+ void Wait() { done_.acquire(); }
+
+ private:
+ size_t num_bytes_expected_ = 0;
+ sync::ThreadNotification done_;
+ std::vector<std::byte> received_;
+};
+
+class TestStream : public stream::NonSeekableReader {
+ public:
+ TestStream() : position_(0) {}
+
+ void QueueData(ConstByteSpan data) {
+ std::lock_guard lock(send_mutex_);
+ std::copy(data.begin(), data.end(), std::back_inserter(to_send_));
+ available_.release();
+ }
+
+ void Stop() {
+ stopped_ = true;
+ available_.release();
+ }
+
+ private:
+ void WaitForData() {
+ while (!stopped_) {
+ {
+ std::lock_guard lock(send_mutex_);
+ if (position_ < to_send_.size()) {
+ break;
+ }
+ }
+
+ available_.acquire();
+ }
+ }
+
+ StatusWithSize DoRead(ByteSpan out) final {
+ WaitForData();
+
+ if (stopped_) {
+ return StatusWithSize(0);
+ }
+
+ std::lock_guard lock(send_mutex_);
+
+ if (position_ == to_send_.size()) {
+ return StatusWithSize::OutOfRange();
+ }
+
+ size_t to_copy = std::min(out.size(), to_send_.size() - position_);
+ std::memcpy(out.data(), to_send_.data() + position_, to_copy);
+ position_ += to_copy;
+
+ return StatusWithSize(to_copy);
+ }
+
+ sync::Mutex send_mutex_;
+ std::vector<std::byte> to_send_;
+ std::atomic<bool> stopped_ = false;
+ size_t position_;
+ sync::ThreadNotification available_;
+};
+
+TEST(StreamRpcDispatcherTest, RecvOk) {
+ constexpr size_t kWriteSize = 10;
+ constexpr std::array<std::byte, kWriteSize> kWriteBuffer = {};
+
+ TestIngress test_ingress(kWriteSize);
+ TestStream test_stream;
+
+ auto dispatcher = StreamRpcDispatcher<kWriteSize>(test_stream, test_ingress);
+ auto dispatcher_thread = thread::Thread(thread::stl::Options(), dispatcher);
+
+ test_stream.QueueData(kWriteBuffer);
+
+ test_ingress.Wait();
+
+ dispatcher.Stop();
+ test_stream.Stop();
+ dispatcher_thread.join();
+
+ auto received = test_ingress.received();
+ EXPECT_EQ(received.size(), kWriteSize);
+ EXPECT_EQ(dispatcher.num_read_errors(), 0U);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rust/BUILD.bazel b/pw_rust/BUILD.bazel
index 74efcfc3d..cf6ea0d88 100644
--- a/pw_rust/BUILD.bazel
+++ b/pw_rust/BUILD.bazel
@@ -11,3 +11,23 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
+
+load("@rules_rust//rust:defs.bzl", "rust_docs")
+
+rust_docs(
+ name = "docs",
+
+ # These need to be kept in dependency order for inter-crate linking to
+ # work.
+ #
+ # TODO: b/295227522 - Add support to `rust_docs` to automatically processs
+ # crates in topological order.
+ crates = [
+ "//pw_format/rust:pw_format",
+ "//pw_status/rust:pw_status",
+ "//pw_stream/rust:pw_stream",
+ "//pw_varint/rust:pw_varint",
+ "//pw_tokenizer/rust:pw_tokenizer_core",
+ "//pw_tokenizer/rust:pw_tokenizer",
+ ],
+)
diff --git a/pw_rust/bazel_patches/0001-rustdoc_test-Apply-prefix-stripping-to-proc_macro-de.patch b/pw_rust/bazel_patches/0001-rustdoc_test-Apply-prefix-stripping-to-proc_macro-de.patch
new file mode 100644
index 000000000..ac96bbe3d
--- /dev/null
+++ b/pw_rust/bazel_patches/0001-rustdoc_test-Apply-prefix-stripping-to-proc_macro-de.patch
@@ -0,0 +1,28 @@
+From c50a4e729812a7d10f15c4e009ee1c2f80519880 Mon Sep 17 00:00:00 2001
+From: Erik Gilling <konkers@google.com>
+Date: Tue, 2 May 2023 21:54:55 +0000
+Subject: [PATCH 1/2] rustdoc_test: Apply prefix stripping to proc_macro
+ dependencies.
+
+Without stripping the prefix, rustdoc can not find the proc macro
+shared library.
+---
+ rust/private/rustdoc_test.bzl | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl
+index 9fb73e1e..6d968f51 100644
+--- a/rust/private/rustdoc_test.bzl
++++ b/rust/private/rustdoc_test.bzl
+@@ -67,7 +67,7 @@ def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate
+ root = crate_info.output.root.path
+ if not root in roots:
+ roots.append(root)
+- for dep in crate_info.deps.to_list():
++ for dep in crate_info.deps.to_list() + crate_info.proc_macro_deps.to_list():
+ dep_crate_info = getattr(dep, "crate_info", None)
+ dep_dep_info = getattr(dep, "dep_info", None)
+ if dep_crate_info:
+--
+2.41.0.694.ge786442a9b-goog
+
diff --git a/pw_rust/bazel_patches/0002-PROTOTYPE-Add-ability-to-document-multiple-crates-at.patch b/pw_rust/bazel_patches/0002-PROTOTYPE-Add-ability-to-document-multiple-crates-at.patch
new file mode 100644
index 000000000..c457c8df4
--- /dev/null
+++ b/pw_rust/bazel_patches/0002-PROTOTYPE-Add-ability-to-document-multiple-crates-at.patch
@@ -0,0 +1,370 @@
+From 0b48b44de59c8fd85f3ec3267061dd3610503655 Mon Sep 17 00:00:00 2001
+From: Erik Gilling <konkers@google.com>
+Date: Mon, 27 Mar 2023 16:48:26 +0000
+Subject: [PATCH 2/2] PROTOTYPE: Add ability to document multiple crates at
+ once
+
+---
+ rust/defs.bzl | 4 +
+ rust/private/rustdoc.bzl | 203 ++++++++++++++++++++++++++++++
+ util/capture_args/BUILD.bazel | 8 ++
+ util/capture_args/capture_args.rs | 28 +++++
+ util/run_scripts/BUILD.bazel | 8 ++
+ util/run_scripts/run_scripts.rs | 40 ++++++
+ 6 files changed, 291 insertions(+)
+ create mode 100644 util/capture_args/BUILD.bazel
+ create mode 100644 util/capture_args/capture_args.rs
+ create mode 100644 util/run_scripts/BUILD.bazel
+ create mode 100644 util/run_scripts/run_scripts.rs
+
+diff --git a/rust/defs.bzl b/rust/defs.bzl
+index 7c972439..c0e84b80 100644
+--- a/rust/defs.bzl
++++ b/rust/defs.bzl
+@@ -56,6 +56,7 @@ load(
+ load(
+ "//rust/private:rustdoc.bzl",
+ _rust_doc = "rust_doc",
++ _rust_docs = "rust_docs",
+ )
+ load(
+ "//rust/private:rustdoc_test.bzl",
+@@ -94,6 +95,9 @@ rust_test_suite = _rust_test_suite
+ rust_doc = _rust_doc
+ # See @rules_rust//rust/private:rustdoc.bzl for a complete description.
+
++rust_docs = _rust_docs
++# See @rules_rust//rust/private:rustdoc.bzl for a complete description.
++
+ rust_doc_test = _rust_doc_test
+ # See @rules_rust//rust/private:rustdoc_test.bzl for a complete description.
+
+diff --git a/rust/private/rustdoc.bzl b/rust/private/rustdoc.bzl
+index f06d18e4..7326f59f 100644
+--- a/rust/private/rustdoc.bzl
++++ b/rust/private/rustdoc.bzl
+@@ -229,6 +229,81 @@ def _rust_doc_impl(ctx):
+ ),
+ ]
+
++def _rust_docs_impl(ctx):
++ """The implementation of the `rust_doc` rule
++
++ Args:
++ ctx (ctx): The rule's context object
++ """
++
++ output_dir = ctx.actions.declare_directory("{}.rustdoc".format(ctx.label.name))
++
++ rustdoc_scripts = []
++ rustdoc_inputs = []
++
++ for crate in ctx.attr.crates:
++ crate_info = crate[rust_common.crate_info]
++
++ # Add the current crate as an extern for the compile action
++ rustdoc_flags = [
++ "--extern",
++ "{}={}".format(crate_info.name, crate_info.output.path),
++ ]
++
++ action = rustdoc_compile_action(
++ ctx = ctx,
++ toolchain = find_toolchain(ctx),
++ crate_info = crate_info,
++ output = output_dir,
++ rustdoc_flags = rustdoc_flags,
++ )
++
++ arg_file = ctx.actions.declare_file("gendocs-{}.sh".format(crate.label.name))
++
++ dump_args = ctx.actions.args()
++ dump_args.add(action.executable)
++
++ ctx.actions.run(
++ mnemonic = "Args",
++ progress_message = "Dumping args for {}".format(crate.label),
++ outputs = [arg_file],
++ executable = ctx.executable._capture_args,
++ inputs = [],
++ env = {"OUTPUT_FILE": arg_file.path},
++ arguments = [dump_args] + action.arguments,
++ )
++
++ rustdoc_scripts.append(arg_file)
++ rustdoc_inputs += [action.inputs, depset([action.executable])]
++
++ args = ctx.actions.args()
++ for script in rustdoc_scripts:
++ args.add(script)
++
++ ctx.actions.run(
++ mnemonic = "Rustdoc",
++ progress_message = "Generating Rustdocs",
++ outputs = [output_dir],
++ executable = ctx.executable._run_scripts,
++ inputs = depset(rustdoc_scripts, transitive = rustdoc_inputs),
++ #env = action.env,
++ arguments = [args],
++ #tools = action.tools,
++ )
++
++ # This rule does nothing without a single-file output, though the directory should've sufficed.
++ _zip_action(ctx, output_dir, ctx.outputs.rust_doc_zip, crate.label)
++
++ return [
++ DefaultInfo(
++ files = depset([ctx.outputs.rust_doc_zip]),
++ ),
++ OutputGroupInfo(
++ rustdoc_dir = depset([output_dir]),
++ rustdoc_zip = depset([ctx.outputs.rust_doc_zip]),
++ ),
++ ]
++
+ rust_doc = rule(
+ doc = dedent("""\
+ Generates code documentation.
+@@ -345,3 +420,131 @@ rust_doc = rule(
+ ],
+ incompatible_use_toolchain_transition = True,
+ )
++
++rust_docs = rule(
++ doc = dedent("""\
++ Generates code documentation for multiple crates.
++
++ Example:
++ Suppose you have the following directory structure for two Rust library crates:
++
++ ```
++ [workspace]/
++ WORKSPACE
++ BUILD
++ hello_lib/
++ BUILD
++ src/
++ lib.rs
++ world_lib/
++ BUILD
++ src/
++ lib.rs
++ ```
++
++ To build [`rustdoc`][rustdoc] documentation for the `hello_lib` and `world_lib` \
++ crates, define a `rust_docs` rule that depends on the `hello_lib` and \
++ `world_lib` `rust_library` targets:
++
++ [rustdoc]: https://doc.rust-lang.org/book/documentation.html
++
++ ```python
++ package(default_visibility = ["//visibility:public"])
++
++ load("@rules_rust//rust:defs.bzl", "rust_docs")
++
++ rust_doc(
++ name = "workspace_docs",
++ crates = ["//hello_lib", "//world_lib"],
++ )
++ ```
++
++ Running `bazel build //:workspaces` will build a zip file containing \
++ the documentation for the `hello_lib` and `world_lib` library crates \
++ generated by `rustdoc`.
++ """),
++ implementation = _rust_docs_impl,
++ attrs = {
++ "crates": attr.label_list(
++ doc = (
++ "A list of lablesThe label of the target to generate code documentation for.\n" +
++ "\n" +
++ "`rust_doc` can generate HTML code documentation for the source files of " +
++ "`rust_library` or `rust_binary` targets."
++ ),
++ providers = [rust_common.crate_info],
++ mandatory = True,
++ ),
++ "html_after_content": attr.label(
++ doc = "File to add in `<body>`, after content.",
++ allow_single_file = [".html", ".md"],
++ ),
++ "html_before_content": attr.label(
++ doc = "File to add in `<body>`, before content.",
++ allow_single_file = [".html", ".md"],
++ ),
++ "html_in_header": attr.label(
++ doc = "File to add to `<head>`.",
++ allow_single_file = [".html", ".md"],
++ ),
++ "markdown_css": attr.label_list(
++ doc = "CSS files to include via `<link>` in a rendered Markdown file.",
++ allow_files = [".css"],
++ ),
++ "rustc_flags": attr.string_list(
++ doc = dedent("""\
++ List of compiler flags passed to `rustc`.
++
++ These strings are subject to Make variable expansion for predefined
++ source/output path variables like `$location`, `$execpath`, and
++ `$rootpath`. This expansion is useful if you wish to pass a generated
++ file of arguments to rustc: `@$(location //package:target)`.
++ """),
++ ),
++ "_cc_toolchain": attr.label(
++ doc = "In order to use find_cpp_toolchain, you must define the '_cc_toolchain' attribute on your rule or aspect.",
++ default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
++ ),
++ "_dir_zipper": attr.label(
++ doc = "A tool that orchestrates the creation of zip archives for rustdoc outputs.",
++ default = Label("//util/dir_zipper"),
++ cfg = "exec",
++ executable = True,
++ ),
++ "_capture_args": attr.label(
++ doc = "A tool for dumping arguments to a file",
++ default = Label("//util/capture_args"),
++ cfg = "exec",
++ executable = True,
++ ),
++ "_process_wrapper": attr.label(
++ doc = "A process wrapper for running rustdoc on all platforms",
++ default = Label("@rules_rust//util/process_wrapper"),
++ executable = True,
++ allow_single_file = True,
++ cfg = "exec",
++ ),
++ "_run_scripts": attr.label(
++ doc = "A tool for running multiple scripts in the same action",
++ default = Label("//util/run_scripts"),
++ cfg = "exec",
++ executable = True,
++ ),
++ "_zipper": attr.label(
++ doc = "A Bazel provided tool for creating archives",
++ default = Label("@bazel_tools//tools/zip:zipper"),
++ cfg = "exec",
++ executable = True,
++ ),
++ },
++ fragments = ["cpp"],
++ host_fragments = ["cpp"],
++ outputs = {
++ "rust_doc_zip": "%{name}.zip",
++ },
++ toolchains = [
++ str(Label("//rust:toolchain_type")),
++ "@bazel_tools//tools/cpp:toolchain_type",
++ ],
++ incompatible_use_toolchain_transition = True,
++)
+diff --git a/util/capture_args/BUILD.bazel b/util/capture_args/BUILD.bazel
+new file mode 100644
+index 00000000..0d7146c2
+--- /dev/null
++++ b/util/capture_args/BUILD.bazel
+@@ -0,0 +1,8 @@
++load("//rust:defs.bzl", "rust_binary")
++
++rust_binary(
++ name = "capture_args",
++ srcs = ["capture_args.rs"],
++ edition = "2018",
++ visibility = ["//visibility:public"],
++)
+diff --git a/util/capture_args/capture_args.rs b/util/capture_args/capture_args.rs
+new file mode 100644
+index 00000000..fbcc4fc1
+--- /dev/null
++++ b/util/capture_args/capture_args.rs
+@@ -0,0 +1,28 @@
++// Copyright 2023 The Bazel Authors. All rights reserved.
++//
++// Licensed under the Apache License, Version 2.0 (the "License");
++// you may not use this file except in compliance with the License.
++// You may obtain a copy of the License at
++//
++// http://www.apache.org/licenses/LICENSE-2.0
++//
++// Unless required by applicable law or agreed to in writing, software
++// distributed under the License is distributed on an "AS IS" BASIS,
++// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
++// See the License for the specific language governing permissions and
++// limitations under the License.
++
++use std::env;
++use std::fs::File;
++use std::io::Write;
++
++// Simple utility to capture command line arguments and write them to a file.
++pub fn main() {
++ let file_name = env::var("OUTPUT_FILE").expect("OUTPUT_FILE environment variable is not set");
++ let mut file = File::create(file_name).expect("Can't open OUTPUT_FILE");
++
++ // Write command line args, skipping the first (our executable path), to
++ // `OUTPUT_FILE` separated by newlines.
++ let args: Vec<String> = env::args().skip(1).collect();
++ writeln!(&mut file, "{}", args.join("\n")).expect("Unable to write to OUTPUT_FILE");
++}
+diff --git a/util/run_scripts/BUILD.bazel b/util/run_scripts/BUILD.bazel
+new file mode 100644
+index 00000000..f295fb69
+--- /dev/null
++++ b/util/run_scripts/BUILD.bazel
+@@ -0,0 +1,8 @@
++load("//rust:defs.bzl", "rust_binary")
++
++rust_binary(
++ name = "run_scripts",
++ srcs = ["run_scripts.rs"],
++ edition = "2018",
++ visibility = ["//visibility:public"],
++)
+diff --git a/util/run_scripts/run_scripts.rs b/util/run_scripts/run_scripts.rs
+new file mode 100644
+index 00000000..cc8e4796
+--- /dev/null
++++ b/util/run_scripts/run_scripts.rs
+@@ -0,0 +1,40 @@
++use std::env;
++use std::fs::File;
++use std::io::{BufRead, BufReader};
++use std::process::Command;
++
++// Simple utility to run commands from files. Each command/command line
++// argument in the file is on a separate line.
++pub fn main() {
++ // Skip the first arg (our executable path).
++ let scripts = env::args().skip(1);
++
++ for script in scripts {
++ let script_file = BufReader::new(File::open(&script).expect("unable to open file"));
++ let mut lines = script_file.lines().map(|l| {
++ l.map_err(|e| format!("{script}: error reading line: {e}"))
++ .unwrap()
++ });
++
++ // First line of the file is the command to run.
++ let command = lines
++ .next()
++ .ok_or_else(|| format!("{script}: no command in file"))
++ .unwrap();
++
++ // Subsequent lines are arguments.
++ let args = lines.collect::<Vec<_>>();
++
++ // Run the command and wait for it to finish.
++ let mut child = Command::new(&command)
++ .args(args)
++ .spawn()
++ .map_err(|e| format!("{script}: failed to spawn child process {command}: {e}"))
++ .unwrap();
++ let status = child.wait().expect("");
++
++ if !status.success() {
++ panic!("{}: error running script: {}", script, status);
++ }
++ }
++}
+--
+2.41.0.694.ge786442a9b-goog
+
diff --git a/pw_rust/bazel_patches/BUILD.bazel b/pw_rust/bazel_patches/BUILD.bazel
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_rust/bazel_patches/BUILD.bazel
diff --git a/pw_rust/docs.rst b/pw_rust/docs.rst
index b917b54ce..d2d0a3c08 100644
--- a/pw_rust/docs.rst
+++ b/pw_rust/docs.rst
@@ -19,7 +19,7 @@ and ``microbit`` QEMU machines. The example can be built and run using
the following commands where ``PLATFORM`` is one of ``lm3s6965evb`` or
``microbit``.
-.. code:: bash
+.. code-block:: bash
$ bazel build //pw_rust/examples/embedded_hello:hello \
--platforms //pw_build/platforms:${PLATFORM} \
@@ -42,13 +42,13 @@ Building
To build the sample rust targets, you need to enable
``pw_rust_ENABLE_EXPERIMENTAL_BUILD``:
-.. code:: bash
+.. code-block:: bash
$ gn gen out --args="pw_rust_ENABLE_EXPERIMENTAL_BUILD=true"
Once that is set, you can build and run the ``hello`` example:
-.. code:: bash
+.. code-block:: bash
$ ninja -C out host_clang_debug/obj/pw_rust/example/bin/hello
$ ./out/host_clang_debug/obj/pw_rust/examples/host_executable/bin/hello
diff --git a/pw_rust/examples/embedded_hello/BUILD.bazel b/pw_rust/examples/embedded_hello/BUILD.bazel
index e8309654c..28cd0f9a8 100644
--- a/pw_rust/examples/embedded_hello/BUILD.bazel
+++ b/pw_rust/examples/embedded_hello/BUILD.bazel
@@ -31,9 +31,9 @@ rust_binary(
"//pw_build/constraints/chipset:lm3s6965evb": [],
}),
deps = [
- "@rust_crates//crates:cortex-m",
- "@rust_crates//crates:cortex-m-rt",
- "@rust_crates//crates:cortex-m-semihosting",
- "@rust_crates//crates:panic-halt",
+ "@rust_crates//:cortex-m",
+ "@rust_crates//:cortex-m-rt",
+ "@rust_crates//:cortex-m-semihosting",
+ "@rust_crates//:panic-halt",
],
)
diff --git a/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld b/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld
index 7e65d2509..0e9cecfc8 100644
--- a/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld
+++ b/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld
@@ -31,7 +31,7 @@ ENTRY(Reset)
MEMORY
{
- /* TODO(b/234892223): Make it possible for projects to freely customize
+ /* TODO: b/234892223 - Make it possible for projects to freely customize
* memory regions.
*/
diff --git a/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld b/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld
index f8366acdd..9b2349fea 100644
--- a/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld
+++ b/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld
@@ -31,7 +31,7 @@ ENTRY(Reset)
MEMORY
{
- /* TODO(b/234892223): Make it possible for projects to freely customize
+ /* TODO: b/234892223 - Make it possible for projects to freely customize
* memory regions.
*/
diff --git a/pw_rust/examples/host_executable/BUILD.gn b/pw_rust/examples/host_executable/BUILD.gn
index fca1f74e1..e4cc00c8b 100644
--- a/pw_rust/examples/host_executable/BUILD.gn
+++ b/pw_rust/examples/host_executable/BUILD.gn
@@ -16,12 +16,31 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+group("host_executable") {
+ deps = [
+ ":hello",
+ ":proc_macro",
+ ":test_hello",
+ ":test_proc_macro",
+ ]
+}
+
pw_rust_executable("hello") {
sources = [
"main.rs",
"other.rs",
]
+ deps = [
+ ":a",
+ ":c",
+ ]
+}
+pw_rust_test("test_hello") {
+ sources = [
+ "main.rs",
+ "other.rs",
+ ]
deps = [
":a",
":c",
@@ -46,3 +65,13 @@ pw_rust_library("c") {
crate_root = "c/lib.rs"
sources = [ "c/lib.rs" ]
}
+
+pw_rust_proc_macro("proc_macro") {
+ crate_root = "proc_macro/lib.rs"
+ sources = [ "proc_macro/lib.rs" ]
+}
+
+pw_rust_test("test_proc_macro") {
+ sources = [ "proc_macro/test.rs" ]
+ deps = [ ":proc_macro" ]
+}
diff --git a/pw_rust/examples/host_executable/proc_macro/lib.rs b/pw_rust/examples/host_executable/proc_macro/lib.rs
new file mode 100644
index 000000000..f06005df8
--- /dev/null
+++ b/pw_rust/examples/host_executable/proc_macro/lib.rs
@@ -0,0 +1,21 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+extern crate proc_macro;
+use proc_macro::TokenStream;
+
+#[proc_macro]
+pub fn fn_like_proc_macro(input: TokenStream) -> TokenStream {
+ input
+}
diff --git a/pw_rust/examples/host_executable/proc_macro/test.rs b/pw_rust/examples/host_executable/proc_macro/test.rs
new file mode 100644
index 000000000..3371cd725
--- /dev/null
+++ b/pw_rust/examples/host_executable/proc_macro/test.rs
@@ -0,0 +1,21 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn test_proc_macro() {
+ proc_macro::fn_like_proc_macro!();
+ }
+}
diff --git a/pw_snapshot/BUILD.bazel b/pw_snapshot/BUILD.bazel
index ae98aec55..8ee969b8e 100644
--- a/pw_snapshot/BUILD.bazel
+++ b/pw_snapshot/BUILD.bazel
@@ -17,8 +17,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -53,7 +53,7 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "metadata_proto_py_pb2",
tags = ["manual"],
@@ -76,7 +76,7 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "snapshot_proto_py_pb2",
tags = ["manual"],
diff --git a/pw_snapshot/proto_format.rst b/pw_snapshot/proto_format.rst
index 14e450dda..ef1a8655e 100644
--- a/pw_snapshot/proto_format.rst
+++ b/pw_snapshot/proto_format.rst
@@ -10,19 +10,19 @@ decoded independently without any errors. An example is illustrated below:
.. code-block::
- // This message uses field numbers 1, 2, and 3.
- message BasicLog {
- string message = 1;
- LogLevel level = 2;
- int64 timestamp = 3;
- }
-
- // This message uses field numbers 16 and 17, which are mutually exclusive
- // to the numbers used in BasicLog.
- message ExtendedLog {
- string file_name = 16;
- uint32 line_nubmer = 17;
- }
+ // This message uses field numbers 1, 2, and 3.
+ message BasicLog {
+ string message = 1;
+ LogLevel level = 2;
+ int64 timestamp = 3;
+ }
+
+ // This message uses field numbers 16 and 17, which are mutually exclusive
+ // to the numbers used in BasicLog.
+ message ExtendedLog {
+ string file_name = 16;
+ uint32 line_nubmer = 17;
+ }
In the above example, a BasicLog and ExtendedLog can be encoded to the same
buffer and then be decoded without causing any problems. What breaks
@@ -32,24 +32,24 @@ field number ranges as shown below:
.. code-block::
- message BasicLog {
- string message = 1;
- LogLevel level = 2;
- int64 timestamp = 3;
+ message BasicLog {
+ string message = 1;
+ LogLevel level = 2;
+ int64 timestamp = 3;
- // ExtendedLog uses these field numbers. These field numbers should never
- // be used by BasicLog.
- reserved 16 to max;
- }
+ // ExtendedLog uses these field numbers. These field numbers should never
+ // be used by BasicLog.
+ reserved 16 to max;
+ }
- message ExtendedLog {
- // BasicLog uses these field numbers. These field numbers should never
- // be used by ExtendedLog.
- reserved 1 to 15;
+ message ExtendedLog {
+ // BasicLog uses these field numbers. These field numbers should never
+ // be used by ExtendedLog.
+ reserved 1 to 15;
- string file_name = 16;
- uint32 line_nubmer = 17;
- }
+ string file_name = 16;
+ uint32 line_nubmer = 17;
+ }
This is exactly how the Snapshot proto is set up. While a SnapshotMetadata proto
message provides a good portion of the valuable snapshot contents, the larger
@@ -69,21 +69,21 @@ Snapshot proto without explicitly depending on the Snapshot proto.
Example:
.. code-block::
- // snapshot.proto
- message Snapshot {
- ...
- // Information about allocated Thread.
- repeated pw.thread.Thread threads = 18;
- }
-
- // thread.proto
-
- // This message overlays the pw.snapshot.Snapshot proto. It's valid to encode
- // this message to the same sink that a Snapshot proto is being written to.
- message SnapshotThread {
- // Thread information.
- repeated pw.thread.Thread threads = 18;
- }
+ // snapshot.proto
+ message Snapshot {
+ ...
+ // Information about allocated Thread.
+ repeated pw.thread.Thread threads = 18;
+ }
+
+ // thread.proto
+
+ // This message overlays the pw.snapshot.Snapshot proto. It's valid to encode
+ // this message to the same sink that a Snapshot proto is being written to.
+ message SnapshotThread {
+ // Thread information.
+ repeated pw.thread.Thread threads = 18;
+ }
It is **critical** that the SnapshotThread message is in sync with the larger
Snapshot proto. If the type or field numbers are different, the proto decode
diff --git a/pw_snapshot/py/BUILD.bazel b/pw_snapshot/py/BUILD.bazel
index eeebb252b..c07d0ddf5 100644
--- a/pw_snapshot/py/BUILD.bazel
+++ b/pw_snapshot/py/BUILD.bazel
@@ -14,7 +14,7 @@
package(default_visibility = ["//visibility:public"])
-# TODO(b/241456982): Not expected to build yet due to the dependency on
+# TODO: b/241456982 - Not expected to build yet due to the dependency on
# snapshot_proto_py_pb2.
py_library(
name = "pw_snapshot",
@@ -35,7 +35,7 @@ py_library(
],
)
-# TODO(b/241456982): Not expected to build yet due to the dependency on
+# TODO: b/241456982 - Not expected to build yet due to the dependency on
# metadata_proto_py_pb2.
py_library(
name = "pw_snapshot_metadata",
@@ -51,7 +51,7 @@ py_library(
],
)
-# TODO(b/241456982): Not expected to build yet due to the dependency on
+# TODO: b/241456982 - Not expected to build yet due to the dependency on
# snapshot_proto_py_pb2.
py_test(
name = "metadata_test",
diff --git a/pw_snapshot/py/metadata_test.py b/pw_snapshot/py/metadata_test.py
index 53ed719ce..60c724018 100644
--- a/pw_snapshot/py/metadata_test.py
+++ b/pw_snapshot/py/metadata_test.py
@@ -147,6 +147,22 @@ class MetadataProcessorTest(unittest.TestCase):
)
self.assertEqual(expected, str(meta))
+ def test_no_token_db(self):
+ meta = MetadataProcessor(self.snapshot.metadata)
+ expected = '\n'.join(
+ (
+ 'Snapshot capture reason:',
+ ' $w8SbOg==',
+ '',
+ 'Reason token: 0x3a9bc4c3',
+ 'Project name: $IwkXAQ==',
+ 'Device: hyper-fast-gshoe',
+ 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
+ 'Snapshot UUID: 00000001',
+ )
+ )
+ self.assertEqual(expected, str(meta))
+
def test_serialized_snapshot(self):
self.snapshot.tags['type'] = 'obviously a crash'
expected = '\n'.join(
diff --git a/pw_snapshot/py/pw_snapshot_metadata/metadata.py b/pw_snapshot/py/pw_snapshot_metadata/metadata.py
index d11a2f403..0992c5a86 100644
--- a/pw_snapshot/py/pw_snapshot_metadata/metadata.py
+++ b/pw_snapshot/py/pw_snapshot_metadata/metadata.py
@@ -75,7 +75,7 @@ class MetadataProcessor:
def __init__(
self,
metadata: snapshot_metadata_pb2.Metadata,
- tokenizer_db: Optional[pw_tokenizer.Detokenizer],
+ tokenizer_db: Optional[pw_tokenizer.Detokenizer] = None,
):
self._metadata = metadata
self._tokenizer_db = (
diff --git a/pw_software_update/BUILD.bazel b/pw_software_update/BUILD.bazel
index f3e21f96e..fc49e2cbf 100644
--- a/pw_software_update/BUILD.bazel
+++ b/pw_software_update/BUILD.bazel
@@ -12,14 +12,14 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -62,18 +62,18 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "bundled_update_py_pb2",
srcs = ["bundled_update.proto"],
tags = ["manual"],
)
-# TODO(b/258074401): Depends on the `any` proto, which doesn't build under
+# TODO: b/258074401 - Depends on the `any` proto, which doesn't build under
# nanopb.
# pw_proto_library(
# name = "bundled_update_proto_cc",
-# # TODO(b/258074760): Adding this tag breaks the pw_proto_library rule.
+# # TODO: b/258074760 - Adding this tag breaks the pw_proto_library rule.
# tags = ["manual"],
# deps = [":bundled_update_proto"],
# )
@@ -111,7 +111,7 @@ pw_cc_library(
"public/pw_software_update/update_bundle_accessor.h",
],
includes = ["public"],
- tags = ["manual"], # TODO(b/236321905): Depends on pw_crypto.
+ tags = ["manual"], # TODO: b/236321905 - Depends on pw_crypto.
deps = [
":blob_store_openable_reader",
":openable_reader",
@@ -128,7 +128,7 @@ pw_cc_library(
],
)
-# TODO(b/258074401): Depends on bundled_update_proto_cc.nanopb_rpc, which
+# TODO: b/258074401 - Depends on bundled_update_proto_cc.nanopb_rpc, which
# doesn't build yet.
pw_cc_library(
name = "bundled_update_service",
@@ -153,7 +153,7 @@ pw_cc_library(
],
)
-# TODO(b/258074401): Depends on bundled_update_proto_cc.nanopb_rpc, which
+# TODO: b/258074401 - Depends on bundled_update_proto_cc.nanopb_rpc, which
# doesn't build yet.
pw_cc_library(
name = "bundled_update_service_pwpb",
@@ -179,7 +179,7 @@ pw_cc_library(
],
)
-# TODO(b/258222107): pw_python_action doesn't exist yet.
+# TODO: b/258222107 - pw_python_action doesn't exist yet.
# pw_python_action(
# name = "generate_test_bundle",
# outputs = ["$target_gen_dir/generate_test_bundle/test_bundles.h"],
diff --git a/pw_software_update/BUILD.gn b/pw_software_update/BUILD.gn
index 73a8caba1..f6c209b97 100644
--- a/pw_software_update/BUILD.gn
+++ b/pw_software_update/BUILD.gn
@@ -65,7 +65,13 @@ pw_proto_library("protos") {
}
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "cli.rst",
+ "design.rst",
+ "docs.rst",
+ "get_started.rst",
+ "guides.rst",
+ ]
}
if (pw_crypto_SHA256_BACKEND != "" && pw_crypto_ECDSA_BACKEND != "") {
diff --git a/pw_software_update/bundled_update.proto b/pw_software_update/bundled_update.proto
index 39ccfd1ee..afd9ebd60 100644
--- a/pw_software_update/bundled_update.proto
+++ b/pw_software_update/bundled_update.proto
@@ -134,7 +134,7 @@ message StartRequest {
optional string bundle_filename = 1;
}
-// TODO(b/235273688): Add documentation and details of the API contract.
+// TODO: b/235273688 - Add documentation and details of the API contract.
service BundledUpdate {
// TODO(keir): Offer GetCurrentManifest & GetStagedManifest() methods that
// leverage pw_transfer to upload the manifest off the device, to enable host
diff --git a/pw_software_update/bundled_update_service.cc b/pw_software_update/bundled_update_service.cc
index 62c135e89..30a2df04c 100644
--- a/pw_software_update/bundled_update_service.cc
+++ b/pw_software_update/bundled_update_service.cc
@@ -463,8 +463,11 @@ Status BundledUpdateService::Reset(const pw_protobuf_Empty&,
}
{
+ PW_MODIFY_DIAGNOSTICS_PUSH();
+ PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
*status_.acquire() = {
.state = pw_software_update_BundledUpdateState_Enum_INACTIVE};
+ PW_MODIFY_DIAGNOSTICS_POP();
}
// Reset the bundle.
diff --git a/pw_software_update/cli.rst b/pw_software_update/cli.rst
new file mode 100644
index 000000000..d0975631c
--- /dev/null
+++ b/pw_software_update/cli.rst
@@ -0,0 +1,229 @@
+.. _module-pw_software_update-cli:
+
+---------------------------------
+pw_software_update: CLI reference
+---------------------------------
+.. pigweed-module-subpage::
+ :name: pw_software_update
+ :tagline: Secure software delivery
+
+Overview
+---------
+
+Use the ``pw_software_update`` CLI to quickly learn and prototype a software
+update system on your development PC before productionizing one. In the future
+you will be able to use the CLI to update a reference
+target.
+
+.. code-block:: bash
+
+ ~$ cd pigweed
+ ~/pigweed$ source ./activate.sh
+ ~/pigweed$ pw update [-h] <command>
+
+.. csv-table::
+ :header: "Command", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``generate-key``, "generates a local signing key"
+ ``create-root-metadata``, "creates a TUF root metadata file"
+ ``sign-root-metadata``, "signs a TUF root metadata"
+ ``inspect-root-metadata``, "prints a TUF root metadata"
+ ``create-empty-bundle``, "creates an empty update bundle"
+ ``add-root-metadata-to-bundle``, "adds a root metadata to an existing bundle"
+ ``add-file-to-bundle``, "adds a target file to an existing bundle"
+ ``sign-bundle``, "signs an update bundle"
+ ``inspect-bundle``, "prints an update bundle"
+ ``verify-bundle``, "verifies an update bundle"
+
+generate-key
+------------
+
+Generates an ECDSA SHA-256 public + private keypair.
+
+.. code-block:: bash
+
+ $ pw update generate-key [-h] pathname
+
+.. csv-table::
+ :header: "Argument", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``pathname``, "output pathname for the new key pair"
+
+create-root-metadata
+--------------------
+
+Creates a root metadata.
+
+.. code-block:: bash
+
+ $ pw update create-root-metadata [-h]
+ [--version VERSION] \
+ --append-root-key ROOT_KEY \
+ --append-targets-key TARGETS_KEY \
+ -o/--out OUT
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``--append-root-key``, "path to root key (public)"
+ ``--append-targets-key``, "path to targets key (public)"
+ ``--out``, "output path of newly created root metadata"
+ ``--version``, "anti-rollback version number of the root metadata (defaults to 1)"
+
+sign-root-metadata
+------------------
+
+Signs a given root metadata.
+
+.. code-block:: bash
+
+ $ pw update sign-root-metadata [-h] \
+ --root-metadata ROOT_METADATA \
+ --root-key ROOT_KEY
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``--root-metadata``, "Path of root metadata to be signed"
+ ``--root-key``, "Path to root signing key (private)"
+
+inspect-root-metadata
+---------------------
+
+Prints the contents of a given root metadata.
+
+.. code-block:: bash
+
+ $ pw update inspect-root-metadata [-h] pathname
+
+.. csv-table::
+ :header: "Argument", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``pathname``, "Path to root metadata"
+
+create-empty-bundle
+-------------------
+
+Creates an empty update bundle.
+
+.. code-block:: bash
+
+ $ pw update create-empty-bundle [-h] \
+ [--target-metadata-version VERSION] \
+ pathname
+
+.. csv-table::
+ :header: "Argument", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``pathname``, "Path to newly created empty bundle"
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``--target-metadata-version``, "Version number for targets metadata, defaults to 1"
+
+add-root-metadata-to-bundle
+---------------------------
+
+Adds a root metadata to a bundle.
+
+.. code-block:: bash
+
+ $ pw update add-root-metadata-to-bundle [-h] \
+ --append-root-metadata ROOT_METADATA \
+ --bundle BUNDLE
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``--append-root-metadata``, "Path to root metadata"
+ ``--bundle``, "Pathname of the bundle"
+
+
+add-file-to-bundle
+------------------
+
+Adds a target file to an existing bundle.
+
+.. code-block:: bash
+
+ $ pw update add-file-to-bundle [-h] \
+ [--new-name NEW_NAME] \
+ --bundle BUNDLE \
+ --file FILE_PATH
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``--file``, "Path to a target file"
+ ``--bundle``, "Pathname of the bundle"
+ ``--new-name``, "Optional new name for target"
+
+sign-bundle
+-----------
+
+Signs an existing bundle with a dev key.
+
+.. code-block:: bash
+
+ $ pw update sign-bundle [-h] --bundle BUNDLE --key KEY
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``--key``, "The targets signing key (private)"
+ ``--bundle``, "Pathname of the bundle"
+
+inspect-bundle
+--------------
+
+Prints the contents of a given bundle.
+
+.. code-block:: bash
+
+ $ pw update inspect-bundle [-h] pathname
+
+.. csv-table::
+ :header: "Argument", "Description"
+ :widths: 30, 70
+ :align: left
+
+ ``pathname``, "Pathname of the bundle"
+
+verify-bundle
+-------------
+
+Performs verification of an existing bundle.
+
+.. code-block:: bash
+
+ $ pw update verify-bundle [-h] \
+ --bundle BUNDLE
+ --trusted-root-metadata ROOT_METADATA
+
+.. csv-table::
+ :header: "Option", "Description"
+ :widths: 30, 70
+
+ ``--trusted-root-metadata``, "Trusted root metadata(anchor)"
+ ``--bundle``, "Pathname of the bundle to be verified"
diff --git a/pw_software_update/design.rst b/pw_software_update/design.rst
new file mode 100644
index 000000000..81fa1b255
--- /dev/null
+++ b/pw_software_update/design.rst
@@ -0,0 +1,235 @@
+.. _module-pw_software_update-design:
+
+--------------------------
+pw_software_update: Design
+--------------------------
+.. pigweed-module-subpage::
+ :name: pw_software_update
+ :tagline: Secure software delivery
+
+This page explains the security framing, bundle format and update workflows of
+``pw_software_update``.
+
+Embedded TUF
+------------
+
+At the heart, the ``pw_software_update`` module leverages
+`The Update Framework <https://theupdateframework.io/>`_ (TUF),
+an industry-leading software update security framework that is open, flexible,
+and offers a balanced security and privacy treatment.
+
+The ``pw_software_update`` module implements the following building blocks
+around TUF.
+
+.. mermaid::
+
+ flowchart LR
+ A[/Source/] --> |Build| B[/Target files/]
+ B --> |Assemble & Sign| C[(Update bundle)]
+ C --> |Publish| D[(Available updates)]
+ D --> |OTA| E[Device]
+
+Update bundles
+^^^^^^^^^^^^^^
+
+Update bundles represent software releases packaged ready for delivery. A bundle
+is essentially an archived folder matching the following structure:
+
+.. code-block:: text
+
+ /
+ ├── root_metadata
+ ├── targets_metadata
+ └── targets
+ ├── release_notes.txt
+ ├── manifest.txt
+ ├── rtos.bin
+ └── app.bin
+
+Bundles are encoded as serialized "protocol buffers".
+
+Key structure
+^^^^^^^^^^^^^
+
+As an optimization and trade-off for embedded projects, ``pw_software_update``
+only supports the "root" and "targets" roles, as represented by
+``root_metadata`` and ``targets_metadata``.
+
+.. mermaid::
+
+ flowchart LR
+ A[Verified boot] --> |Embed & Verify| B[/Root key/]
+ B --> |Delegate & Rotate| C[/Targets key/]
+ C --> |Sign| D[/Target files/]
+
+
+The "root" role delegates the "targets" role to directly authorize each release.
+
+The "root" role can regularly rotate the "targets" role, in effect revoking
+older versions once a new release is available.
+
+The "root" role is the "root of trust" for software update and tied into
+verified boot. Due to security risks, ``pw_software_update`` does not use
+persistent metadata caches that are not covered by verified boot.
+
+Signing service
+^^^^^^^^^^^^^^^
+Production signing keys MUST be kept secure and clean. That means we must
+carefully control access, log usage details, and revoke the key if it was
+(accidentally) used to sign a "questionable" build.
+
+This is easier with a signing server built around a key management service.
+
+.. mermaid::
+
+ sequenceDiagram
+ actor Releaser
+
+ Releaser->>Signer: Sign my bundle with my key, please.
+
+ activate Signer
+
+ Signer->>Signer: Check permission.
+ Signer->>Signer: Validate & sign bundle.
+ Signer->>Signer: Log action. Email alerts.
+ Signer-->>Releaser: Done!
+ deactivate Signer
+
+We don't yet have a public-facing service. External users should source their
+own solution.
+
+Bundle verification
+^^^^^^^^^^^^^^^^^^^
+
+.. mermaid::
+
+ flowchart LR
+ A[(Incoming bundle)] --> |UpdateBundleAccessor| B[/Verified target files/]
+
+The :cpp:type:`UpdateBundleAccessor` decodes, verifies, and exposes the target
+files from an incoming bundle. This class hides the details of the bundle
+format and verification flow from callers.
+
+Update workflow
+^^^^^^^^^^^^^^^
+
+On the device side, :cpp:type:`BundledUpdateService` orchestrates an update
+session end-to-end. It drives the backend via a :cpp:type:`BundledUpdateBackend`
+interface.
+
+:cpp:type:`BundledUpdateService` is invoked via :ref:`module-pw_rpc` after an
+incoming bundle is staged via :ref:`module-pw_transfer`.
+
+.. mermaid::
+
+ stateDiagram-v2
+ direction LR
+
+ [*] --> Inactive
+
+ Inactive --> Transferring: Start()
+ Inactive --> Finished: Start() error
+
+ Transferring --> Transferring: GetStatus()
+ Transferring --> Transferred
+ Transferring --> Aborting: Abort()
+ Transferring --> Finished: Transfer error
+
+ Transferred --> Transferred: GetStatus()
+ Transferred --> Verifying: Verify()
+ Transferred --> Verifying: Apply()
+ Transferred --> Aborting: Abort()
+
+ Verifying --> Verifying: GetStatus()
+ Verifying --> Verified
+ Verifying --> Aborting: Abort()
+
+ Verified --> Verified: GetStatus()
+ Verified --> Applying: Apply()
+ Verified --> Aborting: Abort()
+
+ Applying --> Applying: GetStatus()
+ Applying --> Finished: Apply() OK
+ Applying --> Finished: Apply() error
+
+ Aborting --> Aborting: GetStatus()
+ Aborting --> Finished: Abort() OK
+ Aborting --> Finished: Abort() error
+
+ Finished --> Finished: GetStatus()
+ Finished --> Inactive: Reset()
+ Finished --> Finished: Reset() error
+
+
+Tooling
+^^^^^^^
+
+``pw_software_update`` provides the following tooling support for development
+and integration.
+
+The python package
+~~~~~~~~~~~~~~~~~~
+``pw_software_update`` comes with a python package of the same name, providing
+the following functionalities.
+
+- Local signing key generation for development.
+- TUF root metadata generation and signing.
+- Bundle generation, signing, and verification.
+- Signing server integration.
+
+A typical use of the package is for build system integration.
+
+.. code-block:: text
+
+ Help on package pw_software_update:
+
+ NAME
+ pw_software_update - pw_software_update
+
+ PACKAGE CONTENTS
+ bundled_update_pb2
+ cli
+ dev_sign
+ generate_test_bundle
+ keys
+ metadata
+ remote_sign
+ root_metadata
+ tuf_pb2
+ update_bundle
+ update_bundle_pb2
+ verify
+
+
+The command line utility
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The ``pw update ...`` CLI (Command Line Interface) is a user-friendly interface
+to the ``pw_software_update`` python package.
+
+You can use the CLI to quickly learn and prototype a software update system
+based on ``pw_software_update`` on your development PC before productionizing
+one. In the future you will be able to use the CLI to update a reference
+target.
+
+.. code-block:: text
+
+ usage: pw update [sub-commands]
+
+ sub-commands:
+
+ generate-key
+ create-root-metadata
+ sign-root-metadata
+ inspect-root-metadata
+ create-empty-bundle
+ add-root-metadata-to-bundle
+ add-file-to-bundle
+ sign-bundle
+ inspect-bundle
+
+ options:
+ -h, --help show this help message and exit
+
+
+To learn more, see :ref:`module-pw_software_update-cli`.
diff --git a/pw_software_update/docs.rst b/pw_software_update/docs.rst
index 48131e510..0cc292e25 100644
--- a/pw_software_update/docs.rst
+++ b/pw_software_update/docs.rst
@@ -1,444 +1,89 @@
.. _module-pw_software_update:
--------------------
-pw_software_update
--------------------
-
-This module provides the following building blocks of a high assurance software
-update system:
-
-1. A `TUF <https://theupdateframework.io>`_-based security framework.
-2. A `protocol buffer <https://developers.google.com/protocol-buffers>`_ based
- software update "bundle" format.
-3. An update bundle decoder and verification stack.
-4. An extensible software update RPC service.
-
-High assurance software update
-==============================
-
-On a high-level, a high-assurance software update system should make users feel
-safe to use and be technologically worthy of user's trust over time. In
-particular it should demonstrate the following security and privacy properties.
-
-1. The update packages are generic, sufficiently qualified, and officially
- signed with strong insider attack guardrails.
-2. The update packages are delivered over secure channels.
-3. Update checking, changelist, and installation are done with strong user
- authorization and awareness.
-4. Incoming update packages are strongly authenticated by the client.
-5. Software update requires and builds on top of verified boot.
-
-Life of a software update
-=========================
-
-The following describes a typical software update sequence of events. The focus
-is not to prescribe implementation details but to raise awareness in subtle
-security and privacy considerations.
-
-Stage 0: Product makers create and publish updates
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-A (system) software update is essentially a new version of the on-device
-software stack. Product makers create, qualify and publish new software updates
-to deliver new experiences or bug fixes to users.
-
-While not visible to end users, the product maker team is expected to follow
-widely agreed security and release engineering best practices before signing and
-publishing a new software update. A new software update should be generic for
-all devices, rather than targeting specific devices.
-
-Stage 1: Users check for updates
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-For most consumer products, software updates are "opt-in", which means users
-either manually check for new updates or opt-in for the device itself to check
-(and/or install) updates automatically on the user's behalf. The opt-in may be
-blanket or conditioned on the nature of the updates.
-
-If users have authorized automatic updates, update checking also happens on a
-regular schedule and at every reboot.
-
-.. important::
- As a critical security recovery mechanism, checking and installing software
- updates ideally should happen early in boot, where the software stack has
- been freshly verified by verified boot and minimum mutable input is taken
- into account in update checking and installation.
-
- In other words, the longer the system has been running (up), the greater
- the chance that system has been taken control by an attacker. So it is
- a good idea to remind users to reboot when the system has been running for
- "too long".
-
-Stage 2: Users install updates
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Once a new update has been determined to be available for the device, users will
-be prompted to authorize downloading and installing the update. Users can also
-opt-in to automatic downloading and installing.
-
-.. important::
- If feasible, rechecking, downloading and installing an update should be
- carried out early in a reboot -- to recover from potential temporary attacker
- control.
-
-To improve reliability and reduce disruption, modern system updates typically
-employ an A/B update mechanism, where the incoming update is installed into
-a backup boot slot, and only enacted and locked down (anti-rollback) after
-the new slot has passed boot verification and fully booted into a good state.
-
-.. important::
- While a system update is usually carried out by a user space update client,
- an incoming update may contain more than just the user space. It could
- contain firmware for the bootloader, trusted execution environment, DSP,
- sensor cores etc. which could be important components of a device's TCB (
- trusted compute base, where critical device security policies are enforced).
- When updating these components across different domains, it is best to let
- each component carry out the actual updating, some of which may require
- stronger user authorization (e.g. a test of physical presence, explicit
- authorization with an admin passcode etc.)
-
-Lastly, updates should be checked again in case there are newer updates
-available.
-
-
-Command-line Interface
-======================
-
-You can access the software update CLI by running ``pw update`` in the pigweed environment.
-
-
-.. code-block:: bash
-
- ~$ cd pigweed
- ~/pigweed$ source ./activate.sh
- ~/pigweed$ pw update
-
- usage: pw update [-h] <command>
-
- Software update related operations.
-
- positional arguments:
- generate-key
- create-root-metadata
- sign-root-metadata
- inspect-root-metadata
- create-empty-bundle
- add-root-metadata-to-bundle
- add-file-to-bundle
- sign-bundle
- inspect-bundle
- verify-bundle
-
- optional arguments:
- -h, --help show this help message and exit
-
- Learn more at: pigweed.dev/pw_software_update
-
-
-generate-key
-^^^^^^^^^^^^
-
-The ``generate-key`` subcommmand generates an ECDSA SHA-256 public + private keypair.
-
-.. code-block:: bash
-
- $ pw update generate-key -h
-
- usage: pw update generate-key [-h] pathname
-
- Generates an ecdsa-sha2-nistp256 signing key pair (private + public)
-
- positional arguments:
- pathname Path to generated key pair
-
- optional arguments:
- -h, --help show this help message and exit
-
-
-+------------+------------+----------------+
-| positional argument |
-+============+============+================+
-|``pathname``|path to the generated keypair|
-+------------+------------+----------------+
-
-create-root-metadata
-^^^^^^^^^^^^^^^^^^^^
-
-The ``create-root-metadata`` subcommand creates a root metadata.
-
-.. code-block:: bash
-
- $ pw update create-root-metadata -h
-
- usage: pw update create-root-metadata [-h] [--version VERSION] --append-root-key ROOT_KEY
- --append-targets-key TARGETS_KEY -o/--out OUT
-
- Creation of root metadata
-
- optional arguments:
- -h, --help show this help message and exit
- --version VERSION Canonical version number for rollback checks;
- Defaults to 1
-
- required arguments:
- --append-root-key ROOT_KEY Path to root key
- --append-targets-key TARGETS_KEY Path to targets key
- -o OUT, --out OUT Path to output file
-
-
-
-+--------------------------+-------------------------------------------+
-| required arguments |
-+==========================+===========================================+
-|``--append-root-key`` | path to desired root key |
-+--------------------------+-------------------------------------------+
-|``--append-targets-key`` | path to desired target key |
-+--------------------------+-------------------------------------------+
-|``--out`` | output path of newly created root metadata|
-+--------------------------+-------------------------------------------+
-
-
-+-------------+------------+------------------------------+
-| optional argument |
-+=============+============+==============================+
-|``--version``| Rollback version number(default set to 1) |
-+-------------+------------+------------------------------+
-
-sign-root-metadata
-^^^^^^^^^^^^^^^^^^
-
-The ``sign-root-metadata`` subcommand signs a given root metadata.
-
-.. code-block:: bash
+.. rst-class:: with-subtitle
- usage: pw update sign-root-metadata [-h] --root-metadata ROOT_METADATA --root-key ROOT_KEY
-
- Signing of root metadata
-
- optional arguments:
- -h, --help show this help message and exit
-
- required arguments:
- --root-metadata ROOT_METADATA Root metadata to be signed
- --root-key ROOT_KEY Root signing key
-
-
-
-+--------------------------+-------------------------------------------+
-| required arguments |
-+==========================+===========================================+
-|``--root-metadata`` | Path of root metadata to be signed |
-+--------------------------+-------------------------------------------+
-|``--root-key`` | Path to root signing key |
-+--------------------------+-------------------------------------------+
-
-inspect-root-metadata
-^^^^^^^^^^^^^^^^^^^^^
-
-The ``inspect-root-metadata`` subcommand prints the contents of a given root metadata.
-
-.. code-block:: bash
-
- $ pw update inspect-root-metadata -h
-
- usage: pw update inspect-root-metadata [-h] pathname
-
- Outputs contents of root metadata
-
- positional arguments:
- pathname Path to root metadata
-
- optional arguments:
- -h, --help show this help message and exit
-
-
-+--------------------------+-------------------------------------------+
-| positional argument |
-+==========================+===========================================+
-|``pathname`` | Path to root metadata |
-+--------------------------+-------------------------------------------+
-
-
-create-empty-bundle
-^^^^^^^^^^^^^^^^^^^
-
-The ``create-empty-bundle`` subcommand creates an empty update bundle.
-
-.. code-block:: bash
-
- $ pw update create-empty-bundle -h
-
- usage: pw update create-empty-bundle [-h] [--target-metadata-version VERSION] pathname
-
- Creation of an empty bundle
-
- positional arguments:
- pathname Path to newly created empty bundle
-
- optional arguments:
- -h, --help show this help message and exit
- --target-metadata-version VERSION Version number for targets metadata;
- Defaults to 1
-
-+--------------------------+-------------------------------------------+
-| positional argument |
-+==========================+===========================================+
-|``pathname`` | Path to newly created empty bundle |
-+--------------------------+-------------------------------------------+
-
-+------------------------------+--------------------------------------+
-| optional arguments |
-+==============================+======================================+
-|``--target-metadata-version`` | Version number for targets metadata; |
-| | Defaults to 1 |
-+------------------------------+--------------------------------------+
-
-add-root-metadata-to-bundle
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-The ``add-root-metadata-to-bundle`` subcommand adds a root metadata to a bundle.
-
-.. code-block:: bash
-
- $ pw update add-root-metadata-to-bundle -h
-
- usage: pw update add-root-metadata-to-bundle [-h] --append-root-metadata ROOT_METADATA
- --bundle BUNDLE
-
- Add root metadata to a bundle
-
- optional arguments:
- -h, --help show this help message and exit
-
- required arguments:
- --append-root-metadata ROOT_METADATA Path to root metadata
- --bundle BUNDLE Path to bundle
-
-+--------------------------+-------------------------------------------+
-| required arguments |
-+==========================+===========================================+
-|``--append-root-metadata``| Path to root metadata |
-+--------------------------+-------------------------------------------+
-|``--bundle`` | Path to bundle |
-+--------------------------+-------------------------------------------+
-
-
-add-file-to-bundle
-^^^^^^^^^^^^^^^^^^
-
-The ``add-file-to-bundle`` subcommand adds a target file to an existing bundle.
-
-.. code-block:: bash
-
- $ pw update add-file-to-bundle -h
-
- usage: pw update add-file-to-bundle [-h] [--new-name NEW_NAME] --bundle BUNDLE
- --file FILE_PATH
-
- Add a file to an existing bundle
-
- optional arguments:
- -h, --help show this help message and exit
- --new-name NEW_NAME Optional new name for target
-
- required arguments:
- --bundle BUNDLE Path to an existing bundle
- --file FILE_PATH Path to a target file
-
-+--------------------------+-------------------------------------------+
-| required arguments |
-+==========================+===========================================+
-|``--file`` | Path to a target file |
-+--------------------------+-------------------------------------------+
-|``--bundle`` | Path to bundle |
-+--------------------------+-------------------------------------------+
-
-+--------------------------+-------------------------------------------+
-| optional argument |
-+==========================+===========================================+
-|``--new-name`` | Optional new name for target |
-+--------------------------+-------------------------------------------+
-
-sign-bundle
-^^^^^^^^^^^
-
-The ``sign-bundle`` subcommand signs an existing bundle with a dev key.
-
-.. code-block:: bash
-
- $ pw update sign-bundle -h
-
- usage: pw update sign-bundle [-h] --bundle BUNDLE --key KEY
-
- Sign an existing bundle using a development key
-
- optional arguments:
- -h, --help show this help message and exit
-
- required arguments:
- --bundle BUNDLE Bundle to be signed
- --key KEY Bundle signing key
-
-+--------------------------+-------------------------------------------+
-| required arguments |
-+==========================+===========================================+
-|``--key`` | Key to sign bundle |
-+--------------------------+-------------------------------------------+
-|``--bundle`` | Path to bundle |
-+--------------------------+-------------------------------------------+
+==================
+pw_software_update
+==================
+.. pigweed-module::
+ :name: pw_software_update
+ :tagline: Secure software delivery
+ :status: experimental
+ :languages: Python, C++17
-inspect-bundle
-^^^^^^^^^^^^^^
+ The ``pw_software_update`` module offers the following building blocks for
+ setting up your own end-to-end software delivery system.
-The ``inspect-bundle`` subcommand prints the contents of a given bundle.
+ - **TUF embedded**: An underlying TUF_-based security framework tailored
+ for embedded use cases that enable safe and resilient software delivery.
+ - **One bundle**: A standard update bundle format for assembling all build
+ artifacts and release information.
+ - **Two keys**: Each product has two keys dedicated to software updates. The
+ ``targets`` key directly signs a versioned manifest of target files and
+ can be regularly rotated by the ``root`` key. The ``root`` keys are in
+ turn rotated by verified boot. No provisioning is required.
+ - **Frameworked client**: An update client that takes care of all the logic
+ of checking, staging, verifying, and installing an incoming update. The
+ framework calls into the downstream backend only when needed.
+ - **Signing service**: Integration support for your favorite production
+ signing service.
+ - **Tooling**: Python modules that assemble, sign, and inspect bundles,
+ ready to be integrated into your build and release pipeline. Plus a CLI
+ with which you can try out ``pw_software_update`` before buying into it.
+ - **Extensive guidance**: All software update systems are not equal. We
+ are building out extensive guidance for representative scenarios.
-.. code-block:: bash
+-------------
+Who is it for
+-------------
- $ pw update inspect-bundle -h
+The ``pw_software_update`` module is still in early stages. It works best if
+your software update needs checks the following boxes.
- usage: pw update inspect-bundle [-h] pathname
+✅ **I want security-by-design**!
- Outputs contents of bundle
+The ``pw_software_update`` module is built with security in mind from
+day 0. It leverages the state-of-the-art and widely used TUF_ framework.
+With relatively little expertise, you can set up and operate a software
+building, release, and delivery pipeline that is resiliently secure and
+private.
- positional arguments:
- pathname Path to bundle
+✅ **My project has verified boot.**
- optional arguments:
- -h, --help show this help message and exit
+Software update is an extension of verified boot. Security measures in
+``pw_software_update`` CANNOT replace verified boot.
+.. note::
-+------------+------------+----------------+
-| positional argument |
-+============+============+================+
-|``pathname``|Path to bundle |
-+------------+------------+----------------+
+ Verified boot, also known as secure boot, refers to the generic security
+ feature that ensures no software component is run without passing
+ integrity and authentication verification. In particular, verified boot
+ ensures the software update stack has not been tampered with.
-verify-bundle
-^^^^^^^^^^^^^
+✅ **My project DOES NOT require delta updates.**
-The ``verify-bundle`` subcommand performs verification of an existing bundle.
+``pw_software_update`` packages every new software release in a single opaque
+bundle. The bundle is the smallest granularity transferred between endpoints.
-.. code-block:: bash
+✅ **I can manage signing keys myself.**
- $ pw update verify-bundle -h
+We don't yet have a public-facing signing service.
- usage: pw update verify-bundle [-h] --bundle BUNDLE
- --trusted-root-metadata ROOT_METADATA
+✅ **I can store and serve my own updates.**
- Verify a bundle
+We don't yet have a public-facing end-to-end software delivery solution.
- optional arguments:
- -h, --help show this help message and exit
+If your project doesn't check all the boxes above but you still wish to use
+``pw_software_update``. Please `email <https://groups.google.com/g/pigweed>`_
+or `chat <https://discord.gg/M9NSeTA>`_ with us for potential workarounds.
- required arguments:
- --bundle BUNDLE Bundle to be verified
- --trusted-root-metadata ROOT_METADATA Trusted root metadata
+.. _TUF: https://theupdateframework.io/
-+---------------------------+-------------------------------------------+
-| required arguments |
-+===========================+===========================================+
-|``--trusted-root-metadata``| Trusted root metadata(anchor) |
-+---------------------------+-------------------------------------------+
-|``--bundle`` | Path of bundle to be verified |
-+---------------------------+-------------------------------------------+
+.. toctree::
+ :hidden:
+ :maxdepth: 1
-Getting started with bundles (coming soon)
-==========================================
+ get_started
+ design
+ guides
+ cli
diff --git a/pw_software_update/get_started.rst b/pw_software_update/get_started.rst
new file mode 100644
index 000000000..b7763f82b
--- /dev/null
+++ b/pw_software_update/get_started.rst
@@ -0,0 +1,153 @@
+.. _module-pw_software_update-get-started:
+
+-------------------------------
+pw_software_update: Get started
+-------------------------------
+.. pigweed-module-subpage::
+ :name: pw_software_update
+ :tagline: Secure software delivery
+
+Hello Bundles!
+--------------
+
+A bundle represents a single software release instance -- including
+all target files of the software build and metadata needed to match,
+verify and install the build on a target device.
+
+The following illustrates how a bundle is created, signed, inspected,
+and verified using ``pw update ...`` commands.
+
+1. First let's make a working directory under your Pigweed dir. The
+ ``pw update`` command is not yet visible outside the Pigweed
+ directory.
+
+.. code-block:: bash
+
+ $ cd ~/pigweed
+ $ source activate.sh
+
+ $ mkdir hello_bundles
+ $ cd hello_bundles
+
+
+2. Create signing keys for "root" and "targets" roles.
+
+.. note::
+ Here keys are generated locally for demonstration purposes only. In
+ production, you must use a proper key management service (such as
+ `Google Cloud KMS <https://cloud.google.com/security-key-management>`_)
+ to generate, control access to, and log usage of software signing keys.
+
+.. code-block:: bash
+
+ $ mkdir keys
+ $ pw update generate-key keys/root_key
+ $ pw update generate-key keys/targets_key
+ $ tree
+ .
+ └── keys
+ ├── root_key
+ ├── root_key.pub
+ ├── targets_key
+ └── targets_key.pub
+
+3. Now that we have the keys, let's find them an owner by creating the root
+ metadata.
+
+.. code-block:: bash
+
+ # Assign a single key to each "root" and "targets" roles.
+ $ pw update create-root-metadata --append-root-key keys/root_key.pub \
+ --append-targets-key keys/targets_key.pub -o root_metadata.pb
+
+ # Sign the root metadata with the root key to make it official.
+ $ pw update sign-root-metadata --root-metadata root_metadata.pb \
+ --root-key keys/root_key
+
+ # Review the generated root metadata (output omitted for brevity).
+ $ pw update inspect-root-metadata root_metadata.pb
+
+4. Now we are ready to create a bundle.
+
+.. code-block:: bash
+
+ # Start with an empty bundle.
+ $ pw update create-empty-bundle my_bundle.pb
+
+ # Add root metadata.
+ $ pw update add-root-metadata-to-bundle \
+ --append-root-metadata root_metadata.pb --bundle my_bundle.pb
+
+ # Add some files.
+ $ mkdir target_files
+ $ echo "application bytes" > target_files/application.bin
+ $ echo "rtos bytes" > target_files/rtos.bin
+ $ pw update add-file-to-bundle --bundle my_bundle.pb --file target_files/application.bin
+ $ pw update add-file-to-bundle --bundle my_bundle.pb --file target_files/rtos.bin
+ $ tree
+ .
+ ├── keys
+ │ ├── root_key
+ │ ├── root_key.pub
+ │ ├── targets_key
+ │ └── targets_key.pub
+ ├── my_bundle.pb
+ ├── root_metadata.pb
+ └── target_files
+ ├── application.bin
+ └── rtos.bin
+
+ # Sign our bundle with the "targets" key.
+ $ pw update sign-bundle --bundle my_bundle.pb --key keys/targets_key
+
+ # Review and admire our work (output omitted).
+ $> pw update inspect-bundle my_bundle.pb
+
+5. Finally we can verify the integrity of our bundle.
+
+.. note::
+ Here we are using ``python3 -m pw_software_update.verify`` because the
+ ``pw verify-bundle`` command is WIP.
+
+.. code-block:: bash
+
+ $ python3 -m pw_software_update.verify --incoming my_bundle.pb
+ Verifying: my_bundle.pb
+ (self-verification)
+ Checking content of the trusted root metadata
+ Checking role type
+ Checking keys database
+ Checking root signature requirement
+ Checking targets signature requirement
+ Checking for key sharing
+ Verifying incoming root metadata
+ Checking signatures against current root
+ Total=1, threshold=1
+ Verified: 1
+ Checking content
+ Checking role type
+ Checking keys database
+ Checking root signature requirement
+ Checking targets signature requirement
+ Checking for key sharing
+ Checking signatures against current root
+ Total=1, threshold=1
+ Verified: 1
+ Checking for version rollback
+ Targets key rotation: False
+ Upgrading trust to the incoming root metadata
+ Verifying targets metadata
+ Checking signatures: total=1, threshold=1
+ Verified signatures: 1
+ Checking content
+ Checking role type
+ Checking targets metadata for version rollback
+ Verifying target file: "application"
+ Verifying target file: "rtos"
+ Verification passed.
+
+🎉🎉
+Congratulations on creating your first ``pw_software_update`` bundle!
+🎉🎉
+
+To learn more, see :ref:`module-pw_software_update-design`.
diff --git a/pw_software_update/guides.rst b/pw_software_update/guides.rst
new file mode 100644
index 000000000..98df349b4
--- /dev/null
+++ b/pw_software_update/guides.rst
@@ -0,0 +1,134 @@
+.. _module-pw_software_update-guides:
+
+-------------------------
+pw_software_update: Guide
+-------------------------
+.. pigweed-module-subpage::
+ :name: pw_software_update
+ :tagline: Secure software delivery
+
+How you update software on an embedded system is specific to the project.
+However, there are common patterns. This section provides suggestions for
+each scenario, which you can then adjust to fit your specific needs.
+
+High-level steps
+----------------
+
+Setting up an end-to-end software delivery system can seem overwhelming, but
+generally involves the following high-level steps.
+
+#. Get familiar with ``pw_software_update``.
+#. Enable local updates for development.
+#. Enable remote updates for internal testing.
+#. Prepare for launching.
+#. Ensure smooth landing.
+
+1. Get familiar with ``pw_software_update``.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``pw_software_update`` is not yet a fully managed service like Google Play
+Store. To use it effectively, you need to have at least a basic understanding
+of how it works. The
+:ref:`Getting started <module-pw_software_update-get-started>` and
+:ref:`Design <module-pw_software_update-design>` sections can help you with
+this.
+
+2. Enable local updates for development.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This step allows developers to update a device that is connected to their
+development computer. This achieves a low-latency feedback loop for developers,
+so they can see the results of their changes quickly.
+
+.. csv-table::
+ :header: "Component", "Task", "Description"
+ :widths: 20, 20, 60
+ :align: left
+
+ *Build System*, Produce update bundles, "Use ``pw_software_update``'s CLI and
+ Python APIs to generate and check in dev keys, assemble build artifacts into
+ a bundle, and locally sign the bundle."
+
+ *Dev Tools*, Send update bundles, "Use ``pw_rpc`` to connect to the
+ :cpp:type:`BundledUpdate` service to start and progress through an update
+ session. Use ``pw_transfer`` to transfer the bundle's bytes."
+
+ *Device software*, "Implement :cpp:type:`BundledUpdateBackend`", "Respond to
+ framework callings. Supply root metadata."
+
+3. Enable remote updates for internal testing.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This step builds upon the previous step and allows developers as well as
+internal testers to receive software updates over an internal network from an
+internal release repository. This makes it easy for them to stay up-to-date with
+the latest software and fixes.
+
+.. csv-table::
+ :header: "Component", "Task", "Description"
+ :widths: 20, 20, 60
+ :align: left
+
+ *Build System*, Upload unsigned bundles, "Assemble and generate dev-key signed
+ bundles for local consumption as before. Upload the unsigned bundle to an
+ internal build artifacts repository."
+
+ *Dev Tools*, Support remote update, "In addition to local update as before,
+ optionally support updating a device with a build pulled from the build
+ server."
+
+ *Signing service*, Prepare for test-key signing, "Set up *root* and *targets*
+ test keys and their corresponding ACLs. Monitor incoming signing requests and
+ and automatically sign incoming builds with the test key."
+
+ *Release system*, Produce internal releases, "Trigger builds. Run tests.
+ Request test-key remote signing. Publish internal releases."
+
+4. Prepare for launching.
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The goal of this step is not to add new features for users, but to improve
+security at key points in the development process in preparation for launch.
+
+.. csv-table::
+ :header: "Component", "Task", "Description"
+ :widths: 20, 20, 60
+ :align: left
+
+ *Build System*, Validate and endorse builds, "In addition to previous
+ responsibilities, validate builds to make sure the builds are configured
+ and built properly per their build type (e.g. no debug features in user
+ builds), and then endorse the validated builds by signing the builds with
+ the build server's private key and uploading both the builds and signatures."
+
+ *Signing service*, Prepare for prod-key signing, "Set up *root* and *targets*
+ production keys and their corresponding ACLs. Monitor incoming signing
+ requests and only sign qualified, user builds with the production key. Verify
+ builder identity and validate build content just before signing."
+
+ *Release system*, Produce well-secured releases, "Run builds through
+ daily, internal tests, and production release candidates. Only production-sign
+ a build after all other qualifications have passed."
+
+5. Ensure smooth rollout.
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This step ensures updates are delivered to users reliably and with speed in
+cases of recoverable security bugs, over the supported lifetime of a product.
+
+.. csv-table::
+ :header: "Component", "Task", "Description"
+ :widths: 20, 20, 60
+ :align: left
+
+ *Release system*, Produce well-secured updates, "Carefully control new
+ features. Keep all dependencies up to date. Always ready for emergency
+ updates."
+
+..
+ TODO: b/273583461 - Document these topics.
+ * How to integrate with verified boot
+ * How to do A/B updates
+ * How to manage delta updates
+ * How to revoke a bad release
+ * How to do stepping-stone releases
diff --git a/pw_software_update/public/pw_software_update/bundled_update_backend.h b/pw_software_update/public/pw_software_update/bundled_update_backend.h
index c6685d5ac..03e46992c 100644
--- a/pw_software_update/public/pw_software_update/bundled_update_backend.h
+++ b/pw_software_update/public/pw_software_update/bundled_update_backend.h
@@ -25,7 +25,7 @@
namespace pw::software_update {
-// TODO(b/235273688): update documentation for backend api contract
+// TODO: b/235273688 - update documentation for backend api contract
class BundledUpdateBackend {
public:
virtual ~BundledUpdateBackend() = default;
@@ -85,9 +85,9 @@ class BundledUpdateBackend {
// handed over to update backend.
virtual int64_t GetStatus() { return 0; }
- // Update the specific target file on the device.
+ // Update the specific target file on the device, using seekable reader.
virtual Status ApplyTargetFile(std::string_view target_file_name,
- stream::Reader& target_payload,
+ stream::SeekableReader& target_payload,
size_t update_bundle_offset) = 0;
// Backend to probe the device manifest and prepare a ready-to-go reader
diff --git a/pw_software_update/public/pw_software_update/bundled_update_service.h b/pw_software_update/public/pw_software_update/bundled_update_service.h
index 1c9dbd596..e24a032da 100644
--- a/pw_software_update/public/pw_software_update/bundled_update_service.h
+++ b/pw_software_update/public/pw_software_update/bundled_update_service.h
@@ -30,6 +30,8 @@ namespace pw::software_update {
class BundledUpdateService
: public pw_rpc::nanopb::BundledUpdate::Service<BundledUpdateService> {
public:
+ PW_MODIFY_DIAGNOSTICS_PUSH();
+ PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
BundledUpdateService(UpdateBundleAccessor& bundle,
BundledUpdateBackend& backend,
work_queue::WorkQueue& work_queue)
@@ -41,6 +43,7 @@ class BundledUpdateService
bundle_open_(false),
work_queue_(work_queue),
work_enqueued_(false) {}
+ PW_MODIFY_DIAGNOSTICS_POP();
Status GetStatus(const pw_protobuf_Empty& request,
pw_software_update_BundledUpdateStatus& response);
diff --git a/pw_software_update/py/pw_software_update/generate_test_bundle.py b/pw_software_update/py/pw_software_update/generate_test_bundle.py
index 14409ae7d..3dc15fd0e 100644
--- a/pw_software_update/py/pw_software_update/generate_test_bundle.py
+++ b/pw_software_update/py/pw_software_update/generate_test_bundle.py
@@ -280,7 +280,7 @@ def parse_args():
return parser.parse_args()
-# TODO(b/237580538): Refactor the code so that each test bundle generation
+# TODO: b/237580538 - Refactor the code so that each test bundle generation
# is done in a separate function or script.
# pylint: disable=too-many-locals
def main() -> int:
@@ -518,7 +518,7 @@ def main() -> int:
return 0
-# TODO(b/237580538): Refactor the code so that each test bundle generation
+# TODO: b/237580538 - Refactor the code so that each test bundle generation
# is done in a separate function or script.
# pylint: enable=too-many-locals
diff --git a/pw_software_update/py/remote_sign_test.py b/pw_software_update/py/remote_sign_test.py
index 78d6df4fa..b030d49d7 100644
--- a/pw_software_update/py/remote_sign_test.py
+++ b/pw_software_update/py/remote_sign_test.py
@@ -75,7 +75,7 @@ FAKE_BUILDER_PUBLIC_KEY = textwrap.dedent(
# inclusive-language: enable
-# TODO(b/235240430): Improve unit test coverage.
+# TODO: b/235240430 - Improve unit test coverage.
class PathSigningTest(unittest.TestCase):
"""Tests the signing of bundles by path."""
diff --git a/pw_software_update/update_bundle_accessor.cc b/pw_software_update/update_bundle_accessor.cc
index 1544cbb93..d6376ed61 100644
--- a/pw_software_update/update_bundle_accessor.cc
+++ b/pw_software_update/update_bundle_accessor.cc
@@ -39,7 +39,7 @@ namespace {
Result<bool> VerifyEcdsaSignature(protobuf::Bytes public_key,
ConstByteSpan digest,
protobuf::Bytes signature) {
- // TODO(b/237580538): Move this logic into an variant of the API in
+ // TODO: b/237580538 - Move this logic into an variant of the API in
// pw_crypto:ecdsa that takes readers as inputs.
std::byte public_key_bytes[65];
std::byte signature_bytes[64];
@@ -446,7 +446,7 @@ Status UpdateBundleAccessor::UpgradeRoot() {
return OkStatus();
}
- // TODO(b/237580538): Check whether the bundle contains a root metadata that
+ // TODO: b/237580538 - Check whether the bundle contains a root metadata that
// is different from the on-device trusted root.
// Verify the signatures against the trusted root metadata.
@@ -457,7 +457,8 @@ Status UpdateBundleAccessor::UpgradeRoot() {
return Status::Unauthenticated();
}
- // TODO(b/237580538): Verifiy the content of the new root metadata, including:
+ // TODO: b/237580538 - Verifiy the content of the new root metadata,
+ // including:
// 1) Check role magic field.
// 2) Check signature requirement. Specifically, check that no key is
// reused across different roles and keys are unique in the same
@@ -515,7 +516,7 @@ Status UpdateBundleAccessor::UpgradeRoot() {
}
}
- // TODO(b/237580538): Implement key change detection to determine whether
+ // TODO: b/237580538 - Implement key change detection to determine whether
// rotation has occured or not. Delete the persisted targets metadata version
// if any of the targets keys has been rotated.
diff --git a/pw_software_update/update_bundle_test.cc b/pw_software_update/update_bundle_test.cc
index e35987a38..745c33f7a 100644
--- a/pw_software_update/update_bundle_test.cc
+++ b/pw_software_update/update_bundle_test.cc
@@ -45,7 +45,9 @@ class TestBundledUpdateBackend final : public BundledUpdateBackend {
Status ApplyReboot() override { return Status::Unimplemented(); }
Status PostRebootFinalize() override { return OkStatus(); }
- Status ApplyTargetFile(std::string_view, stream::Reader&, size_t) override {
+ Status ApplyTargetFile(std::string_view,
+ stream::SeekableReader&,
+ size_t) override {
return OkStatus();
}
diff --git a/pw_span/Android.bp b/pw_span/Android.bp
index 22ea3be16..5e3fbb3a3 100644
--- a/pw_span/Android.bp
+++ b/pw_span/Android.bp
@@ -18,8 +18,8 @@ package {
cc_library_headers {
name: "pw_span_headers",
- cpp_std: "c++20",
vendor_available: true,
+ cpp_std: "c++20",
export_include_dirs: ["public"],
header_libs: [],
host_supported: true,
diff --git a/pw_span/BUILD.bazel b/pw_span/BUILD.bazel
index ec57864ba..bd1723990 100644
--- a/pw_span/BUILD.bazel
+++ b/pw_span/BUILD.bazel
@@ -31,7 +31,7 @@ pw_cc_library(
hdrs = ["public/pw_span/span.h"],
includes = ["public"],
deps = [
- # TODO(b/243851191): Depending on pw_assert causes a dependency cycle.
+ # TODO: b/243851191 - Depending on pw_assert causes a dependency cycle.
],
)
diff --git a/pw_span/CMakeLists.txt b/pw_span/CMakeLists.txt
index 63cffd227..37e277ea1 100644
--- a/pw_span/CMakeLists.txt
+++ b/pw_span/CMakeLists.txt
@@ -24,7 +24,7 @@ pw_add_library(pw_span INTERFACE
public
PUBLIC_DEPS
pw_assert
- pw_polyfill.standard_library
+ pw_polyfill._standard_library
)
pw_add_test(pw_span.pw_span_test
@@ -37,7 +37,3 @@ pw_add_test(pw_span.pw_span_test
modules
pw_span
)
-
-if(Zephyr_FOUND AND CONFIG_PIGWEED_SPAN)
- zephyr_link_libraries(pw_span)
-endif()
diff --git a/pw_span/Kconfig b/pw_span/Kconfig
index 34ef65837..cdbb72240 100644
--- a/pw_span/Kconfig
+++ b/pw_span/Kconfig
@@ -12,6 +12,12 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_span"
+
config PIGWEED_SPAN
- bool "Enable the Pigweed span library (pw_span)"
+ bool "Link pw_span library"
select PIGWEED_POLYFILL
+ help
+ See :ref:`module-pw_span` for module details.
+
+endmenu
diff --git a/pw_span/docs.rst b/pw_span/docs.rst
index d1dcd5bff..18b229738 100644
--- a/pw_span/docs.rst
+++ b/pw_span/docs.rst
@@ -88,8 +88,7 @@ more details.
-------------
Compatibility
-------------
-Works with C++14, but some features require C++17. In C++20, use ``std::span``
-instead.
+Works with C++17. In C++20, use ``std::span`` instead.
------
Zephyr
diff --git a/pw_span/public/pw_span/span.h b/pw_span/public/pw_span/span.h
index 3bc9ac559..8583a682b 100644
--- a/pw_span/public/pw_span/span.h
+++ b/pw_span/public/pw_span/span.h
@@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-// pw::span is an implementation of std::span for C++14 or newer. The
+// pw::span is an implementation of std::span for C++17 or newer. The
// implementation is shared with the std::span polyfill class.
#pragma once
diff --git a/pw_spi/BUILD.bazel b/pw_spi/BUILD.bazel
index c50f61a0b..bb8335a61 100644
--- a/pw_spi/BUILD.bazel
+++ b/pw_spi/BUILD.bazel
@@ -36,6 +36,19 @@ pw_cc_library(
)
pw_cc_library(
+ name = "responder",
+ hdrs = [
+ "public/pw_spi/responder.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_bytes",
+ "//pw_function",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
name = "chip_selector",
hdrs = [
"public/pw_spi/chip_selector.h",
@@ -101,6 +114,7 @@ pw_cc_test(
],
deps = [
":device",
+ ":responder",
"//pw_sync:mutex",
"//pw_unit_test",
],
@@ -112,6 +126,9 @@ pw_cc_library(
srcs = ["linux_spi.cc"],
hdrs = ["public/pw_spi/linux_spi.h"],
includes = ["public"],
+ target_compatible_with = [
+ "@platforms//os:linux",
+ ],
deps = [
":device",
"//pw_bytes",
diff --git a/pw_spi/BUILD.gn b/pw_spi/BUILD.gn
index 5a683629d..cc1d0b2cc 100644
--- a/pw_spi/BUILD.gn
+++ b/pw_spi/BUILD.gn
@@ -43,6 +43,16 @@ pw_source_set("initiator") {
]
}
+pw_source_set("responder") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_spi/responder.h" ]
+ public_deps = [
+ "$dir_pw_bytes",
+ "$dir_pw_function",
+ "$dir_pw_status",
+ ]
+}
+
pw_source_set("chip_selector") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_spi/chip_selector.h" ]
@@ -107,6 +117,7 @@ pw_test("spi_test") {
sources = [ "spi_test.cc" ]
deps = [
":device",
+ ":responder",
"$dir_pw_sync:mutex",
]
}
diff --git a/pw_spi/CMakeLists.txt b/pw_spi/CMakeLists.txt
new file mode 100644
index 000000000..3ce4c3f1e
--- /dev/null
+++ b/pw_spi/CMakeLists.txt
@@ -0,0 +1,59 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_spi.initiator INTERFACE
+ HEADERS
+ public/pw_spi/initiator.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
+ pw_status
+)
+
+pw_add_library(pw_spi.responder INTERFACE
+ HEADERS
+ public/pw_spi/responder.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_function
+ pw_status
+)
+
+pw_add_library(pw_spi.chip_selector INTERFACE
+ HEADERS
+ public/pw_spi/chip_selector.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_status
+)
+
+pw_add_library(pw_spi.device INTERFACE
+ HEADERS
+ public/pw_spi/device.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_spi.chip_selector
+ pw_spi.initiator
+ pw_status
+ pw_sync.borrow
+)
diff --git a/pw_spi/docs.rst b/pw_spi/docs.rst
index d1ba66823..b23ae887d 100644
--- a/pw_spi/docs.rst
+++ b/pw_spi/docs.rst
@@ -4,26 +4,28 @@
pw_spi
======
Pigweed's SPI module provides a set of interfaces for communicating with SPI
-peripherals attached to a target.
+responders attached to a target. It also provides an interface for implementing
+SPI responders.
--------
Overview
--------
The ``pw_spi`` module provides a series of interfaces that facilitate the
-development of SPI peripheral drivers that are abstracted from the target's
-SPI hardware implementation. The interface consists of three main classes:
+development of SPI responder drivers that are abstracted from the target's
+SPI hardware implementation. The interface consists of these main classes:
- ``pw::spi::Initiator`` - Interface for configuring a SPI bus, and using it
to transmit and receive data.
- ``pw::spi::ChipSelector`` - Interface for enabling/disabling a SPI
- peripheral attached to the bus.
+ responder attached to the bus.
- ``pw::spi::Device`` - primary HAL interface used to interact with a SPI
- peripheral.
+ responder.
+- ``pw::spi::Responder`` - Interface for implementing a SPI responder.
``pw_spi`` relies on a target-specific implementations of
``pw::spi::Initiator`` and ``pw::spi::ChipSelector`` to be defined, and
injected into ``pw::spi::Device`` objects which are used to communicate with a
-given peripheral attached to a target's SPI bus.
+given responder attached to a target's SPI bus.
Example - Constructing a SPI Device:
@@ -37,10 +39,11 @@ Example - Constructing a SPI Device:
};
auto initiator = pw::spi::MyInitator();
+ auto mutex = pw::sync::VirtualMutex();
auto selector = pw::spi::MyChipSelector();
- auto borrowable_initiator = pw::sync::Borrowable<Initiator&>(initiator);
- auto device = pw::spi::Device(borrowable_initiator, kConfig, selector);
+ auto device = pw::spi::Device(
+ pw::sync::Borrowable<Initiator>(initiator, mutex), kConfig, selector);
This example demonstrates the construction of a ``pw::spi::Device`` from its
object dependencies and configuration data; where ``MyDevice`` and
@@ -53,7 +56,7 @@ that transactions cannot be interrupted or corrupted by other concurrent
workloads making use of the same SPI bus.
Once constructed, the ``device`` object can then be passed to functions used to
-perform SPI transfers with a target peripheral.
+perform SPI transfers with a target responder.
Example - Performing a Transfer:
@@ -117,6 +120,7 @@ The SPI API consists of the following components:
structs.
- The ``pw::spi::ChipSelector`` interface.
- The ``pw::spi::Device`` class.
+- The ``pw::spi::Responder`` interface.
pw::spi::Initiator
------------------
@@ -141,15 +145,11 @@ method.
.. Note:
- Throughout ``pw_spi``, the terms "controller" and "peripheral" are used to
+ Throughout ``pw_spi``, the terms "initiator" and "responder" are used to
describe the two roles SPI devices can implement. These terms correspond
to the "master" and "slave" roles described in legacy documentation
related to the SPI protocol.
- ``pw_spi`` only supports SPI transfers where the target implements the
- "controller" role, and does not support the target acting in the
- "peripheral" role.
-
.. inclusive-language: enable
.. cpp:class:: pw::spi::Initiator
@@ -179,7 +179,7 @@ method.
pw::spi::ChipSelector
---------------------
The ChipSelector class provides an abstract interface for controlling the
-chip-select signal associated with a specific SPI peripheral.
+chip-select signal associated with a specific SPI responder.
This interface provides a ``SetActive()`` method, which activates/deactivates
the device based on the value of the `active` parameter. The associated
@@ -187,7 +187,7 @@ the device based on the value of the `active` parameter. The associated
``SetActive(true)`` and ``SetActive(false)``, respectively.
A concrete implementation of this interface class must be provided in order to
-use the SPI HAL to communicate with a peripheral.
+use the SPI HAL to communicate with a responder.
.. Note::
@@ -250,7 +250,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
.. cpp:function:: Status Read(Bytespan read_buffer)
- Synchronously read data from the SPI peripheral until the provided
+ Synchronously read data from the SPI responder until the provided
`read_buffer` is full.
This call will configure the bus and activate/deactivate chip select
for the transfer
@@ -263,7 +263,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
.. cpp:function:: Status Write(ConstByteSpan write_buffer)
- Synchronously write the contents of `write_buffer` to the SPI peripheral.
+ Synchronously write the contents of `write_buffer` to the SPI responder.
This call will configure the bus and activate/deactivate chip select
for the transfer
@@ -275,7 +275,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
.. cpp:function:: Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer)
- Perform a synchronous read/write transfer with the SPI peripheral. Data
+ Perform a synchronous read/write transfer with the SPI responder. Data
from the `write_buffer` object is written to the bus, while the
`read_buffer` is populated with incoming data on the bus. In the event
the read buffer is smaller than the write buffer (or zero-size), any
@@ -306,7 +306,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
.. cpp:function:: Status Read(Bytespan read_buffer)
- Synchronously read data from the SPI peripheral until the provided
+ Synchronously read data from the SPI responder until the provided
`read_buffer` is full.
Returns OkStatus() on success, and implementation-specific values on
@@ -314,7 +314,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
.. cpp:function:: Status Write(ConstByteSpan write_buffer)
- Synchronously write the contents of `write_buffer` to the SPI peripheral
+ Synchronously write the contents of `write_buffer` to the SPI responder
Returns OkStatus() on success, and implementation-specific values on
failure.
@@ -371,3 +371,23 @@ list. An example of this is shown below:
// Alternatively this is also called from MockInitiator::~MockInitiator().
EXPECT_EQ(spi_mock.Finalize(), OkStatus());
+pw::spi::Responder
+------------------
+The common interface for implementing a SPI responder. It provides a way to
+respond to SPI transactions coming from a SPI initiator in a non-target specific
+way. A concrete implementation of the ``Responder`` class should be provided for
+the target hardware. Applications can then use it to implement their specific
+protocols.
+
+.. code-block:: cpp
+
+ MyResponder responder;
+ responder.SetCompletionHandler([](ByteSpan rx_data, Status status) {
+ // Handle incoming data from initiator.
+ // ...
+ // Prepare data to send back to initiator during next SPI transaction.
+ responder.WriteReadAsync(tx_data, rx_data);
+ });
+
+ // Prepare data to send back to initiator during next SPI transaction.
+ responder.WriteReadAsync(tx_data, rx_data)
diff --git a/pw_spi/public/pw_spi/chip_selector.h b/pw_spi/public/pw_spi/chip_selector.h
index 3290f507d..50588b637 100644
--- a/pw_spi/public/pw_spi/chip_selector.h
+++ b/pw_spi/public/pw_spi/chip_selector.h
@@ -30,7 +30,7 @@ enum class ChipSelectBehavior : uint8_t {
};
// The ChipSelector class provides an abstract interface for controlling the
-// chip-select signal associated with a specific SPI peripheral.
+// chip-select signal associated with a specific SPI responder.
class ChipSelector {
public:
virtual ~ChipSelector() = default;
diff --git a/pw_spi/public/pw_spi/device.h b/pw_spi/public/pw_spi/device.h
index 3f0449653..c9e50c6b0 100644
--- a/pw_spi/public/pw_spi/device.h
+++ b/pw_spi/public/pw_spi/device.h
@@ -25,21 +25,21 @@
namespace pw::spi {
-// The Device class enables data transfer with a specific SPI peripheral.
+// The Device class enables data transfer with a specific SPI responder.
// This class combines an Initiator (representing the physical SPI bus), its
// configuration data, and the ChipSelector object to uniquely address a device.
// Transfers to a selected initiator are guarded against concurrent access
// through the use of the `Borrowable` object.
class Device {
public:
- Device(sync::Borrowable<Initiator>& initiator,
+ Device(sync::Borrowable<Initiator> initiator,
const Config config,
ChipSelector& selector)
: initiator_(initiator), config_(config), selector_(selector) {}
~Device() = default;
- // Synchronously read data from the SPI peripheral until the provided
+ // Synchronously read data from the SPI responder until the provided
// `read_buffer` is full.
// This call will configure the bus and activate/deactivate chip select
// for the transfer
@@ -50,7 +50,7 @@ class Device {
// failure.
Status Read(ByteSpan read_buffer) { return WriteRead({}, read_buffer); }
- // Synchronously write the contents of `write_buffer` to the SPI peripheral.
+ // Synchronously write the contents of `write_buffer` to the SPI responder.
// This call will configure the bus and activate/deactivate chip select
// for the transfer
//
@@ -62,7 +62,7 @@ class Device {
return WriteRead(write_buffer, {});
}
- // Perform a synchronous read/write transfer with the SPI peripheral. Data
+ // Perform a synchronous read/write transfer with the SPI responder. Data
// from the `write_buffer` object is written to the bus, while the
// `read_buffer` is populated with incoming data on the bus. In the event
// the read buffer is smaller than the write buffer (or zero-size), any
@@ -93,7 +93,7 @@ class Device {
(behavior_ == ChipSelectBehavior::kPerTransaction) &&
(!first_write_read_)) {
selector_->Deactivate()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
@@ -120,14 +120,14 @@ class Device {
Transaction(const Transaction&) = delete;
Transaction& operator=(const Transaction&) = delete;
- // Synchronously read data from the SPI peripheral until the provided
+ // Synchronously read data from the SPI responder until the provided
// `read_buffer` is full.
//
// Returns OkStatus() on success, and implementation-specific values on
// failure.
Status Read(ByteSpan read_buffer) { return WriteRead({}, read_buffer); }
- // Synchronously write the contents of `write_buffer` to the SPI peripheral
+ // Synchronously write the contents of `write_buffer` to the SPI responder
//
// Returns OkStatus() on success, and implementation-specific values on
// failure.
@@ -200,7 +200,7 @@ class Device {
}
private:
- sync::Borrowable<Initiator>& initiator_;
+ sync::Borrowable<Initiator> initiator_;
const Config config_;
ChipSelector& selector_;
};
diff --git a/pw_spi/public/pw_spi/initiator.h b/pw_spi/public/pw_spi/initiator.h
index a750b03be..0935d7e92 100644
--- a/pw_spi/public/pw_spi/initiator.h
+++ b/pw_spi/public/pw_spi/initiator.h
@@ -67,6 +67,11 @@ struct Config {
ClockPhase phase;
BitsPerWord bits_per_word;
BitOrder bit_order;
+
+ bool operator==(const Config& rhs) const {
+ return polarity == rhs.polarity && phase == rhs.phase &&
+ bits_per_word() == rhs.bits_per_word() && bit_order == rhs.bit_order;
+ }
};
static_assert(sizeof(Config) == sizeof(uint32_t),
"Ensure that the config struct fits in 32-bits");
@@ -77,7 +82,7 @@ class Initiator {
public:
virtual ~Initiator() = default;
- // Configure the SPI bus to communicate with peripherals using a given set of
+ // Configure the SPI bus to communicate with responders using a given set of
// properties, including the clock polarity, clock phase, bit-order, and
// bits-per-word.
// Returns OkStatus() on success, and implementation-specific values on
diff --git a/pw_spi/public/pw_spi/responder.h b/pw_spi/public/pw_spi/responder.h
new file mode 100644
index 000000000..283c341a7
--- /dev/null
+++ b/pw_spi/public/pw_spi/responder.h
@@ -0,0 +1,56 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "pw_bytes/span.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::spi {
+
+// The Responder class provides an abstract interface used to receive and
+// transmit data on the responder side of a SPI bus.
+class Responder {
+ public:
+ virtual ~Responder() = default;
+
+ // Set `callback` to be called when SPI transaction completes. `callback` can
+ // be called in an interrupt context. `callback` should not be changed during
+ // execution of a completion.
+ //
+ // A value of CANCELLED for the Status parameter indicates Abort() was called.
+ // Partially transferred data may be passed in that case as well.
+ // Other Status values are implementer defined.
+ void SetCompletionHandler(Function<void(ByteSpan, Status)> callback);
+
+ // `tx_data` is queued for tx when called, but only transmitted when
+ // the initiator starts the next transaction. It's up to the implementer to
+ // define how stuffing bytes are handled.
+ // `rx_data` is populated as the initiator transfers data. A slice of
+ // `rx_data` is passed as a span to the completion callback.
+ //
+ // Only one outstanding request should be active. UNAVAILABLE will be returned
+ // if a transaction is already established.
+ //
+ // The completion handler will always be invoked, even in the case of an
+ // Abort(). In that case a Status value of CANCELLED will be passed.
+ Status WriteReadAsync(ConstByteSpan tx_data, ByteSpan rx_data);
+
+ // Cancel the outstanding `WriteReadAsync` call. The completion handler will
+ // be called with a Status of CANCELLED after this is called.
+ void Abort();
+};
+
+} // namespace pw::spi
diff --git a/pw_spi/spi_test.cc b/pw_spi/spi_test.cc
index f3b42ae61..9deb7d5a4 100644
--- a/pw_spi/spi_test.cc
+++ b/pw_spi/spi_test.cc
@@ -19,6 +19,7 @@
#include "pw_spi/chip_selector.h"
#include "pw_spi/device.h"
#include "pw_spi/initiator.h"
+#include "pw_spi/responder.h"
#include "pw_status/status.h"
#include "pw_sync/borrow.h"
#include "pw_sync/mutex.h"
@@ -63,6 +64,17 @@ class SpiTestDevice : public ::testing::Test {
Device device_;
};
+class SpiResponderTestDevice : public ::testing::Test {
+ public:
+ SpiResponderTestDevice() : responder_() {}
+
+ private:
+ // Stub SPI Responder, used to exercise public API surface.
+ class TestResponder : public Responder {};
+
+ TestResponder responder_;
+};
+
// Simple test ensuring the SPI HAL compiles
TEST_F(SpiTestDevice, CompilationSucceeds) {
// arrange
@@ -71,5 +83,8 @@ TEST_F(SpiTestDevice, CompilationSucceeds) {
EXPECT_TRUE(true);
}
+// Simple test ensuring the SPI Responder HAL compiles
+TEST_F(SpiResponderTestDevice, CompilationSucceeds) { EXPECT_TRUE(true); }
+
} // namespace
} // namespace pw::spi
diff --git a/pw_spi_mcuxpresso/BUILD.bazel b/pw_spi_mcuxpresso/BUILD.bazel
new file mode 100644
index 000000000..78b54f71f
--- /dev/null
+++ b/pw_spi_mcuxpresso/BUILD.bazel
@@ -0,0 +1,61 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_spi_mcuxpresso",
+ srcs = [
+ "flexspi.cc",
+ "spi.cc",
+ ],
+ hdrs = [
+ "public/pw_spi_mcuxpresso/flexspi.h",
+ "public/pw_spi_mcuxpresso/spi.h",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [
+ "//pw_digital_io:facade",
+ "//pw_preprocessor",
+ "@pigweed//targets:mcuxpresso_sdk",
+ ],
+)
+
+pw_cc_test(
+ name = "spi_test",
+ srcs = ["spi_test.cc"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [":pw_digital_io_mcuxpresso"],
+)
+
+pw_cc_test(
+ name = "flexspi_test",
+ srcs = ["flexspi_test.cc"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [":pw_digital_io_mcuxpresso"],
+)
diff --git a/pw_spi_mcuxpresso/BUILD.gn b/pw_spi_mcuxpresso/BUILD.gn
new file mode 100644
index 000000000..67889d0a5
--- /dev/null
+++ b/pw_spi_mcuxpresso/BUILD.gn
@@ -0,0 +1,114 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+group("pw_spi_mcuxpresso") {
+ deps = [
+ ":flexspi",
+ ":spi",
+ ]
+}
+
+if (pw_third_party_mcuxpresso_SDK != "") {
+ pw_source_set("spi") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_spi_mcuxpresso/spi.h" ]
+ public_deps = [
+ "$dir_pw_spi:device",
+ "$dir_pw_status",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ "$pw_third_party_mcuxpresso_SDK",
+ ]
+ deps = [
+ "$dir_pw_assert",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_log",
+ ]
+ sources = [ "spi.cc" ]
+ }
+
+ pw_source_set("flexspi") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_spi_mcuxpresso/flexspi.h" ]
+ public_deps = [
+ "$dir_pw_digital_io",
+ "$dir_pw_spi:device",
+ "$dir_pw_status",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ "$pw_third_party_mcuxpresso_SDK",
+ ]
+ deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_log",
+ ]
+ sources = [ "flexspi.cc" ]
+ }
+
+ pw_test("spi_test") {
+ enable_if =
+ pw_third_party_mcuxpresso_SDK ==
+ "//targets/mimxrt595_evk_freertos:sdk" &&
+ (pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_debug" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_size_optimized" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_speed_optimized")
+ sources = [ "spi_test.cc" ]
+ deps = [
+ ":spi",
+ "//targets/mimxrt595_evk_freertos:sdk",
+ ]
+ }
+
+ pw_test("flexspi_test") {
+ enable_if =
+ pw_third_party_mcuxpresso_SDK ==
+ "//targets/mimxrt595_evk_freertos:sdk" &&
+ (pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_debug" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_size_optimized" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_speed_optimized")
+ sources = [ "flexspi_test.cc" ]
+ deps = [
+ ":flexspi",
+ "//targets/mimxrt595_evk_freertos:sdk",
+ ]
+ }
+
+ pw_test_group("tests") {
+ tests = [
+ ":spi_test",
+ ":flexspi_test",
+ ]
+ }
+} else {
+ pw_test_group("tests") {
+ tests = []
+ }
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_spi_mcuxpresso/OWNERS b/pw_spi_mcuxpresso/OWNERS
new file mode 100644
index 000000000..a9c2709ad
--- /dev/null
+++ b/pw_spi_mcuxpresso/OWNERS
@@ -0,0 +1,2 @@
+afoxley@google.com
+tonymd@google.com
diff --git a/pw_spi_mcuxpresso/docs.rst b/pw_spi_mcuxpresso/docs.rst
new file mode 100644
index 000000000..c286dfe73
--- /dev/null
+++ b/pw_spi_mcuxpresso/docs.rst
@@ -0,0 +1,61 @@
+.. _module-pw_spi_mcuxpresso:
+
+=================
+pw_spi_mcuxpresso
+=================
+``pw_spi_mcuxpresso`` implements the :ref:`module-pw_spi` interface using the
+NXP MCUXpresso SDK.
+
+There are two implementations corresponding to the SPI and FLEXIO_SPI drivers in the
+SDK. SPI transfer can be configured to use a blocking (by polling) method or
+non-blocking under the covers. The API is synchronous regardless.
+
+-----
+Setup
+-----
+Use of this module requires setting up the MCUXpresso SDK for use with Pigweed. Follow
+the steps in :ref:`module-pw_build_mcuxpresso` to create a ``pw_source_set`` for an
+MCUXpresso SDK. Include the GPIO and PINT driver components in this SDK definition.
+
+This example shows what your SDK setup would look like if using an RT595 EVK.
+
+.. code-block:: text
+
+ import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+
+ pw_mcuxpresso_sdk("sample_project_sdk") {
+ manifest = "$dir_pw_third_party/mcuxpresso/evkmimxrt595/EVK-MIMXRT595_manifest_v3_8.xml"
+ include = [
+ "component.serial_manager_uart.MIMXRT595S",
+ "platform.drivers.flexio_spi.MIMXRT595S",
+ "platform.drivers.flexspi.MIMXRT595S",
+ "project_template.evkmimxrt595.MIMXRT595S",
+ "utility.debug_console.MIMXRT595S",
+ ]
+ }
+
+Next, specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
+the name of this source set. Edit your GN args with ``gn args out``.
+
+.. code-block:: text
+
+ pw_third_party_mcuxpresso_SDK = "//targets/mimxrt595_evk:sample_project_sdk"
+
+Then, depend on this module in your BUILD.gn to use.
+
+.. code-block:: text
+
+ deps = [ dir_pw_spi_mcuxpresso ]
+
+-------
+Example
+-------
+Example write using the FLEXIO_SPI initiator:
+
+.. code-block:: text
+
+ McuxpressoFlexIoInitiator spi(
+ flexio_spi_config, CLOCK_GetFlexioClkFreq(), baud_rate_bps, blocking);
+ spi.Configure(configuration);
+
+ spi.WriteRead(source, destination);
diff --git a/pw_spi_mcuxpresso/flexspi.cc b/pw_spi_mcuxpresso/flexspi.cc
new file mode 100644
index 000000000..d0472592e
--- /dev/null
+++ b/pw_spi_mcuxpresso/flexspi.cc
@@ -0,0 +1,221 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_spi_mcuxpresso/flexspi.h"
+
+#include <cinttypes>
+#include <mutex>
+
+#include "fsl_flexio_spi.h"
+#include "fsl_gpio.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_log/log.h"
+#include "pw_spi/initiator.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+
+namespace pw::spi {
+namespace {
+
+using namespace ::std::literals::chrono_literals;
+constexpr auto kMaxWait = chrono::SystemClock::for_at_least(1000ms);
+
+Status ToPwStatus(int32_t status) {
+ switch (status) {
+ // Intentional fall-through
+ case kStatus_Success:
+ case kStatus_FLEXIO_SPI_Idle:
+ return OkStatus();
+ case kStatus_ReadOnly:
+ return Status::PermissionDenied();
+ case kStatus_OutOfRange:
+ return Status::OutOfRange();
+ case kStatus_InvalidArgument:
+ return Status::InvalidArgument();
+ case kStatus_Timeout:
+ return Status::DeadlineExceeded();
+ case kStatus_NoTransferInProgress:
+ return Status::FailedPrecondition();
+ // Intentional fall-through
+ case kStatus_Fail:
+ default:
+ PW_LOG_ERROR("Mcuxpresso FlexSPI unknown error code: %" PRId32, status);
+ return Status::Unknown();
+ }
+}
+
+} // namespace
+
+// inclusive-language: disable
+
+McuxpressoFlexIoInitiator::~McuxpressoFlexIoInitiator() {
+ if (is_initialized()) {
+ FLEXIO_SPI_MasterDeinit(&flexio_spi_config_);
+ }
+}
+
+void McuxpressoFlexIoInitiator::ConfigureClock(
+ flexio_spi_master_config_t* masterConfig, ClockPolarity clockPolarity) {
+ flexio_timer_config_t timerConfig = {};
+ uint16_t timerDiv = 0;
+ uint16_t timerCmp = 0;
+
+ // Rather than modify the flexio_spi driver code to support negative clock
+ // polarity, we duplicate the clock setup here to add support for inverting
+ // the output for SPI mode CPOL=1.
+ timerConfig.triggerSelect =
+ FLEXIO_TIMER_TRIGGER_SEL_SHIFTnSTAT(flexio_spi_config_.shifterIndex[0]);
+ timerConfig.triggerPolarity = kFLEXIO_TimerTriggerPolarityActiveLow;
+ timerConfig.triggerSource = kFLEXIO_TimerTriggerSourceInternal;
+ timerConfig.pinConfig = kFLEXIO_PinConfigOutput;
+ timerConfig.pinSelect = flexio_spi_config_.SCKPinIndex;
+ if (clockPolarity == ClockPolarity::kActiveLow) {
+ timerConfig.pinPolarity = kFLEXIO_PinActiveLow;
+ } else {
+ timerConfig.pinPolarity = kFLEXIO_PinActiveHigh;
+ }
+
+ timerConfig.timerMode = kFLEXIO_TimerModeDual8BitBaudBit;
+ timerConfig.timerOutput = kFLEXIO_TimerOutputZeroNotAffectedByReset;
+ timerConfig.timerDecrement = kFLEXIO_TimerDecSrcOnFlexIOClockShiftTimerOutput;
+ timerConfig.timerReset = kFLEXIO_TimerResetNever;
+ timerConfig.timerDisable = kFLEXIO_TimerDisableOnTimerCompare;
+ timerConfig.timerEnable = kFLEXIO_TimerEnableOnTriggerHigh;
+ timerConfig.timerStop = kFLEXIO_TimerStopBitEnableOnTimerDisable;
+ timerConfig.timerStart = kFLEXIO_TimerStartBitEnabled;
+
+ timerDiv = static_cast<uint16_t>(src_clock_hz_ / masterConfig->baudRate_Bps);
+ timerDiv = timerDiv / 2U - 1U;
+
+ timerCmp = (static_cast<uint16_t>(masterConfig->dataMode) * 2U - 1U) << 8U;
+ timerCmp |= timerDiv;
+
+ timerConfig.timerCompare = timerCmp;
+
+ FLEXIO_SetTimerConfig(flexio_spi_config_.flexioBase,
+ flexio_spi_config_.timerIndex[0],
+ &timerConfig);
+}
+
+void McuxpressoFlexIoInitiator::SpiCallback(FLEXIO_SPI_Type*,
+ flexio_spi_master_handle_t*,
+ status_t status,
+ void* context) {
+ auto* driver = static_cast<McuxpressoFlexIoInitiator*>(context);
+ driver->last_transfer_status_ = ToPwStatus(status);
+ driver->transfer_semaphore_.release();
+}
+
+Status McuxpressoFlexIoInitiator::Configure(const Config& config) {
+ if (current_config_ && config == *current_config_) {
+ return OkStatus();
+ }
+
+ flexio_spi_master_config_t master_config = {};
+ FLEXIO_SPI_MasterGetDefaultConfig(&master_config);
+
+ RESET_ClearPeripheralReset(kFLEXIO_RST_SHIFT_RSTn);
+
+ if (config.phase == ClockPhase::kRisingEdge) {
+ master_config.phase = kFLEXIO_SPI_ClockPhaseFirstEdge;
+ } else {
+ master_config.phase = kFLEXIO_SPI_ClockPhaseSecondEdge;
+ }
+
+ master_config.enableMaster = true;
+ master_config.baudRate_Bps = baud_rate_bps_;
+
+ switch (config.bits_per_word()) {
+ case 8:
+ master_config.dataMode = kFLEXIO_SPI_8BitMode;
+ if (config.bit_order == BitOrder::kMsbFirst) {
+ transfer_flags_ = kFLEXIO_SPI_8bitMsb;
+ } else {
+ transfer_flags_ = kFLEXIO_SPI_8bitLsb;
+ }
+ break;
+ case 16:
+ master_config.dataMode = kFLEXIO_SPI_16BitMode;
+ if (config.bit_order == BitOrder::kMsbFirst) {
+ transfer_flags_ = kFLEXIO_SPI_16bitMsb;
+ } else {
+ transfer_flags_ = kFLEXIO_SPI_16bitLsb;
+ }
+ break;
+ default:
+ return Status::InvalidArgument();
+ }
+
+ std::lock_guard lock(mutex_);
+ FLEXIO_SPI_MasterInit(&flexio_spi_config_, &master_config, src_clock_hz_);
+ ConfigureClock(&master_config, config.polarity);
+
+ const auto status = ToPwStatus(FLEXIO_SPI_MasterTransferCreateHandle(
+ &flexio_spi_config_,
+ &driver_handle_,
+ McuxpressoFlexIoInitiator::SpiCallback,
+ this));
+
+ if (status == OkStatus()) {
+ current_config_.emplace(config);
+ }
+ return status;
+}
+
+Status McuxpressoFlexIoInitiator::WriteRead(ConstByteSpan write_buffer,
+ ByteSpan read_buffer) {
+ flexio_spi_transfer_t transfer = {};
+ transfer.txData =
+ reinterpret_cast<uint8_t*>(const_cast<std::byte*>(write_buffer.data()));
+ transfer.rxData = reinterpret_cast<uint8_t*>(read_buffer.data());
+ if (write_buffer.data() == nullptr && read_buffer.data() != nullptr) {
+ // Read only transaction
+ transfer.dataSize = read_buffer.size();
+ } else if (read_buffer.data() == nullptr && write_buffer.data() != nullptr) {
+ // Write only transaction
+ transfer.dataSize = write_buffer.size();
+ } else {
+ // Take smallest as size of transaction
+ transfer.dataSize = write_buffer.size() < read_buffer.size()
+ ? write_buffer.size()
+ : read_buffer.size();
+ }
+ transfer.flags = transfer_flags_;
+
+ std::lock_guard lock(mutex_);
+ if (!current_config_) {
+ PW_LOG_ERROR("Mcuxpresso FlexSPI must be configured before use.");
+ return Status::FailedPrecondition();
+ }
+ if (blocking_) {
+ return ToPwStatus(
+ FLEXIO_SPI_MasterTransferBlocking(&flexio_spi_config_, &transfer));
+ }
+
+ PW_TRY(ToPwStatus(FLEXIO_SPI_MasterTransferNonBlocking(
+ &flexio_spi_config_, &driver_handle_, &transfer)));
+
+ if (!transfer_semaphore_.try_acquire_for(kMaxWait)) {
+ return Status::DeadlineExceeded();
+ }
+ return last_transfer_status_;
+}
+
+Status McuxpressoFlexIoChipSelector::SetActive(bool active) {
+ return pin_.SetState(active ? digital_io::State::kInactive
+ : digital_io::State::kActive);
+}
+// inclusive-language: enable
+
+} // namespace pw::spi
diff --git a/pw_spi_mcuxpresso/flexspi_test.cc b/pw_spi_mcuxpresso/flexspi_test.cc
new file mode 100644
index 000000000..755c2a00d
--- /dev/null
+++ b/pw_spi_mcuxpresso/flexspi_test.cc
@@ -0,0 +1,97 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_spi_mcuxpresso/flexspi.h"
+
+#include "board.h"
+#include "gtest/gtest.h"
+#include "pw_bytes/array.h"
+#include "pw_status/status.h"
+
+namespace pw::spi {
+namespace {
+
+FLEXIO_SPI_Type flexio_spi_config = {
+ .flexioBase = reinterpret_cast<FLEXIO_Type*>(FLEXIO0),
+ .SDOPinIndex = 13,
+ .SDIPinIndex = 14,
+ .SCKPinIndex = 15,
+ .CSnPinIndex = 12,
+ .shifterIndex = {0, 2},
+ .timerIndex = {0, 1}};
+constexpr uint32_t baud_rate_bps = 500000;
+constexpr Config configuration{.polarity = ClockPolarity::kActiveLow,
+ .phase = ClockPhase::kFallingEdge,
+ .bits_per_word = BitsPerWord(8),
+ .bit_order = BitOrder::kMsbFirst};
+
+TEST(Configure, ConfigurationSuccess) {
+ McuxpressoFlexIoInitiator spi(
+ flexio_spi_config, CLOCK_GetFlexioClkFreq(), baud_rate_bps);
+ auto status = spi.Configure(configuration);
+
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(Configure, RepeatedConfigurationSuccess) {
+ McuxpressoFlexIoInitiator spi(
+ flexio_spi_config, CLOCK_GetFlexioClkFreq(), baud_rate_bps);
+ auto status = spi.Configure(configuration);
+ EXPECT_EQ(status, OkStatus());
+
+ status = spi.Configure(configuration);
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, PollingWriteSuccess) {
+ const auto blocking = true;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ static auto destination = bytes::Array<0xff, 0xff, 0xff, 0xff, 0xff>();
+
+ McuxpressoFlexIoInitiator spi(
+ flexio_spi_config, CLOCK_GetFlexioClkFreq(), baud_rate_bps, blocking);
+ auto status = spi.Configure(configuration);
+ ASSERT_EQ(status, OkStatus());
+
+ status = spi.WriteRead(source, destination);
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, IRQWriteSuccess) {
+ const auto blocking = false;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ static auto destination = bytes::Array<0xff, 0xff, 0xff, 0xff, 0xff>();
+ McuxpressoFlexIoInitiator spi(
+ flexio_spi_config, CLOCK_GetFlexioClkFreq(), baud_rate_bps, blocking);
+ auto status = spi.Configure(configuration);
+ ASSERT_EQ(status, OkStatus());
+
+ status = spi.WriteRead(source, destination);
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, WriteOnlySuccess) {
+ const auto blocking = false;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ McuxpressoFlexIoInitiator spi(
+ flexio_spi_config, CLOCK_GetFlexioClkFreq(), baud_rate_bps, blocking);
+ auto status = spi.Configure(configuration);
+ ASSERT_EQ(status, OkStatus());
+
+ status = spi.WriteRead(source, {});
+ EXPECT_EQ(status, OkStatus());
+}
+
+} // namespace
+} // namespace pw::spi
diff --git a/pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/flexspi.h b/pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/flexspi.h
new file mode 100644
index 000000000..a7f14eeb2
--- /dev/null
+++ b/pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/flexspi.h
@@ -0,0 +1,86 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include "fsl_flexio_spi.h"
+#include "pw_digital_io/digital_io.h"
+#include "pw_spi/chip_selector.h"
+#include "pw_spi/initiator.h"
+#include "pw_status/status.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+
+namespace pw::spi {
+
+// Mcuxpresso SDK implementation of the FLEXIO SPI Initiator
+class McuxpressoFlexIoInitiator : public Initiator {
+ public:
+ McuxpressoFlexIoInitiator(FLEXIO_SPI_Type flexio_spi_config,
+ uint32_t src_clock_hz,
+ uint32_t baud_rate_bps,
+ bool blocking = true)
+ : flexio_spi_config_(flexio_spi_config),
+ src_clock_hz_(src_clock_hz),
+ baud_rate_bps_(baud_rate_bps),
+ blocking_(blocking) {}
+ ~McuxpressoFlexIoInitiator();
+
+ // Implements pw::spi::Initiator
+ pw::Status Configure(const Config& config) PW_LOCKS_EXCLUDED(mutex_) override;
+ pw::Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer)
+ PW_LOCKS_EXCLUDED(mutex_) override;
+
+ private:
+ // inclusive-language: disable
+ static void SpiCallback(FLEXIO_SPI_Type*,
+ flexio_spi_master_handle_t*,
+ status_t status,
+ void* context);
+ // Add support to FLEXIO_SPI for negative clock polarity.
+ void ConfigureClock(flexio_spi_master_config_t* masterConfig,
+ ClockPolarity clockPolarity);
+
+ bool is_initialized() { return !!current_config_; }
+
+ std::optional<const Config> current_config_;
+ FLEXIO_SPI_Type flexio_spi_config_;
+ flexio_spi_master_handle_t driver_handle_;
+ // inclusive-language: enable
+ sync::BinarySemaphore transfer_semaphore_;
+ sync::Mutex mutex_;
+ Status last_transfer_status_;
+ uint32_t src_clock_hz_;
+ uint32_t baud_rate_bps_;
+ bool blocking_;
+ uint8_t transfer_flags_;
+};
+
+// Mcuxpresso userspace implementation of SPI ChipSelector. Implemented using
+// GPIO so as to support manual control of chip select. GPIO pin passed in
+// should be already initialized and ungated.
+class McuxpressoFlexIoChipSelector : public ChipSelector {
+ public:
+ explicit McuxpressoFlexIoChipSelector(digital_io::DigitalOut& pin)
+ : pin_(pin) {}
+
+ // Implements pw::spi::ChipSelector
+ pw::Status SetActive(bool active) override;
+
+ private:
+ digital_io::DigitalOut& pin_;
+};
+
+} // namespace pw::spi
diff --git a/pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/spi.h b/pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/spi.h
new file mode 100644
index 000000000..8b245f381
--- /dev/null
+++ b/pw_spi_mcuxpresso/public/pw_spi_mcuxpresso/spi.h
@@ -0,0 +1,99 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <cinttypes>
+#include <mutex>
+#include <optional>
+
+#include "fsl_spi.h"
+#include "pw_spi/chip_selector.h"
+#include "pw_spi/initiator.h"
+#include "pw_status/status.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+
+namespace pw::spi {
+
+// Mcuxpresso SDK implementation of the SPI Initiator
+class McuxpressoInitiator : public Initiator {
+ public:
+ McuxpressoInitiator(SPI_Type* register_map,
+ uint32_t max_speed_hz,
+ uint32_t baud_rate_bps,
+ bool blocking = true)
+ : register_map_(register_map),
+ max_speed_hz_(max_speed_hz),
+ baud_rate_bps_(baud_rate_bps),
+ blocking_(blocking) {}
+ ~McuxpressoInitiator();
+
+ // Implements pw::spi::Initiator
+ Status Configure(const Config& config) PW_LOCKS_EXCLUDED(mutex_) override;
+ Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer)
+ PW_LOCKS_EXCLUDED(mutex_) override;
+
+ Status SetChipSelect(uint32_t pin) PW_LOCKS_EXCLUDED(mutex_);
+
+ private:
+ // inclusive-language: disable
+ static void SpiCallback(SPI_Type* base,
+ spi_master_handle_t* driver_handle,
+ status_t status,
+ void* context);
+
+ Status DoConfigure(const Config& config,
+ const std::lock_guard<sync::Mutex>& lock);
+
+ bool is_initialized() { return !!current_config_; }
+
+ SPI_Type* register_map_;
+ spi_master_handle_t driver_handle_;
+ // inclusive-language: enable
+ sync::BinarySemaphore transfer_semaphore_;
+ sync::Mutex mutex_;
+ Status last_transfer_status_;
+ uint32_t max_speed_hz_;
+ uint32_t baud_rate_bps_;
+ bool blocking_;
+ std::optional<const Config> current_config_;
+ uint32_t pin_ = 0;
+};
+
+// Mcuxpresso userspace implementation of SPI ChipSelector
+// NOTE: This implementation deviates from the expected for this interface.
+// It only specifies which chipselect pin should be activated and does not
+// activate the pin itself. Activation of the pin is handled at a lower level by
+// the Mcuxpresso vendor driver.
+// This chipselector may only be used with a single McuxpressoInitiator
+class McuxpressoChipSelector : public ChipSelector {
+ public:
+ McuxpressoChipSelector(McuxpressoInitiator& initiator, uint32_t pin)
+ : initiator_(initiator), pin_(pin) {}
+
+ // Implements pw::spi::ChipSelector
+ // Instead of directly activating the cs line, this informs the underlying
+ // driver to do so.
+ Status SetActive(bool active) override {
+ return initiator_.SetChipSelect(pin_);
+ }
+
+ private:
+ McuxpressoInitiator& initiator_;
+ uint32_t pin_;
+};
+
+} // namespace pw::spi
diff --git a/pw_spi_mcuxpresso/spi.cc b/pw_spi_mcuxpresso/spi.cc
new file mode 100644
index 000000000..d3735521e
--- /dev/null
+++ b/pw_spi_mcuxpresso/spi.cc
@@ -0,0 +1,184 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "public/pw_spi_mcuxpresso/spi.h"
+
+#include <cinttypes>
+#include <mutex>
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_log/log.h"
+#include "pw_spi_mcuxpresso/spi.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+
+namespace pw::spi {
+namespace {
+
+using namespace ::std::literals::chrono_literals;
+constexpr auto kMaxWait = pw::chrono::SystemClock::for_at_least(1000ms);
+
+pw::Status ToPwStatus(int32_t status) {
+ switch (status) {
+ // Intentional fall-through
+ case kStatus_Success:
+ case kStatus_SPI_Idle:
+ return pw::OkStatus();
+ case kStatus_ReadOnly:
+ return pw::Status::PermissionDenied();
+ case kStatus_OutOfRange:
+ return pw::Status::OutOfRange();
+ case kStatus_InvalidArgument:
+ return pw::Status::InvalidArgument();
+ case kStatus_Timeout:
+ return pw::Status::DeadlineExceeded();
+ case kStatus_NoTransferInProgress:
+ return pw::Status::FailedPrecondition();
+ // Intentional fall-through
+ case kStatus_Fail:
+ default:
+ PW_LOG_ERROR("Mcuxpresso SPI unknown error code: %d",
+ static_cast<int>(status));
+ return pw::Status::Unknown();
+ }
+}
+
+} // namespace
+
+McuxpressoInitiator::~McuxpressoInitiator() {
+ if (is_initialized()) {
+ SPI_Deinit(register_map_);
+ }
+}
+
+// inclusive-language: disable
+void McuxpressoInitiator::SpiCallback(SPI_Type*,
+ spi_master_handle_t*,
+ status_t status,
+ void* context) {
+ auto* driver = static_cast<McuxpressoInitiator*>(context);
+ driver->last_transfer_status_ = ToPwStatus(status);
+ driver->transfer_semaphore_.release();
+}
+
+Status McuxpressoInitiator::Configure(const Config& config) {
+ std::lock_guard lock(mutex_);
+ if (current_config_ && config == *current_config_) {
+ return OkStatus();
+ }
+ return DoConfigure(config, lock);
+}
+
+Status McuxpressoInitiator::DoConfigure(const Config& config,
+ const std::lock_guard<sync::Mutex>&) {
+ spi_master_config_t master_config = {};
+ SPI_MasterGetDefaultConfig(&master_config);
+
+ if (config.polarity == ClockPolarity::kActiveLow) {
+ master_config.polarity = kSPI_ClockPolarityActiveLow;
+ } else {
+ master_config.polarity = kSPI_ClockPolarityActiveHigh;
+ }
+ if (config.phase == ClockPhase::kRisingEdge) {
+ master_config.phase = kSPI_ClockPhaseFirstEdge;
+ } else {
+ master_config.phase = kSPI_ClockPhaseSecondEdge;
+ }
+
+ if (config.bit_order == BitOrder::kMsbFirst) {
+ master_config.direction = kSPI_MsbFirst;
+ } else {
+ master_config.direction = kSPI_LsbFirst;
+ }
+
+ master_config.enableMaster = true;
+ master_config.baudRate_Bps = baud_rate_bps_;
+
+ master_config.sselNum = static_cast<spi_ssel_t>(pin_);
+ master_config.sselPol = static_cast<spi_spol_t>(kSPI_SpolActiveAllLow);
+
+ // Data width enum value is 1 value below bits_per_word. i.e. 0 = 1;
+ constexpr uint8_t kMinBitsPerWord = 4;
+ constexpr uint8_t kMaxBitsPerWord = 16;
+ PW_CHECK(config.bits_per_word() >= kMinBitsPerWord &&
+ config.bits_per_word() <= kMaxBitsPerWord);
+ master_config.dataWidth =
+ static_cast<_spi_data_width>(config.bits_per_word() - 1);
+
+ SPI_MasterInit(register_map_, &master_config, max_speed_hz_);
+ const auto status = ToPwStatus(SPI_MasterTransferCreateHandle(
+ register_map_, &driver_handle_, McuxpressoInitiator::SpiCallback, this));
+
+ if (status == OkStatus()) {
+ current_config_.emplace(config);
+ }
+ return status;
+}
+
+Status McuxpressoInitiator::WriteRead(ConstByteSpan write_buffer,
+ ByteSpan read_buffer) {
+ spi_transfer_t transfer = {};
+
+ transfer.txData =
+ reinterpret_cast<uint8_t*>(const_cast<std::byte*>(write_buffer.data()));
+ transfer.rxData = reinterpret_cast<uint8_t*>(read_buffer.data());
+ if (write_buffer.data() == nullptr && read_buffer.data() != nullptr) {
+ // Read only transaction
+ transfer.dataSize = read_buffer.size();
+ } else if (read_buffer.data() == nullptr && write_buffer.data() != nullptr) {
+ // Write only transaction
+ transfer.dataSize = write_buffer.size();
+ } else {
+ // Take the smallest as the size of transaction
+ transfer.dataSize = write_buffer.size() < read_buffer.size()
+ ? write_buffer.size()
+ : read_buffer.size();
+ }
+ transfer.configFlags = kSPI_FrameAssert;
+
+ std::lock_guard lock(mutex_);
+ if (!current_config_) {
+ PW_LOG_ERROR("Mcuxpresso SPI must be configured before use.");
+ return Status::FailedPrecondition();
+ }
+ if (blocking_) {
+ return ToPwStatus(SPI_MasterTransferBlocking(register_map_, &transfer));
+ }
+
+ PW_TRY(ToPwStatus(SPI_MasterTransferNonBlocking(
+ register_map_, &driver_handle_, &transfer)));
+
+ if (!transfer_semaphore_.try_acquire_for(kMaxWait)) {
+ return Status::DeadlineExceeded();
+ }
+ return last_transfer_status_;
+}
+// inclusive-language: enable
+
+Status McuxpressoInitiator::SetChipSelect(uint32_t pin) {
+ std::lock_guard lock(mutex_);
+ if (pin == pin_) {
+ return OkStatus();
+ }
+ pin_ = pin;
+ // As configuration has not been called, must be called prior to use, and must
+ // itself set chipselect, set will be delayed until configuration.
+ if (!current_config_) {
+ return OkStatus();
+ }
+ return DoConfigure(*current_config_, lock);
+}
+
+} // namespace pw::spi
diff --git a/pw_spi_mcuxpresso/spi_test.cc b/pw_spi_mcuxpresso/spi_test.cc
new file mode 100644
index 000000000..13d82459c
--- /dev/null
+++ b/pw_spi_mcuxpresso/spi_test.cc
@@ -0,0 +1,102 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_spi_mcuxpresso/spi.h"
+
+#include "board.h"
+#include "gtest/gtest.h"
+#include "pw_bytes/array.h"
+#include "pw_spi/device.h"
+#include "pw_status/status.h"
+
+namespace pw::spi {
+namespace {
+
+auto* spi_base = SPI14;
+constexpr auto kClockNumber = 14;
+constexpr uint32_t baud_rate_bps = 10000000;
+constexpr Config configuration{.polarity = ClockPolarity::kActiveHigh,
+ .phase = ClockPhase::kRisingEdge,
+ .bits_per_word = BitsPerWord(8),
+ .bit_order = BitOrder::kMsbFirst};
+
+TEST(Configure, ConfigurationSuccess) {
+ McuxpressoInitiator spi(
+ spi_base, CLOCK_GetFlexcommClkFreq(kClockNumber), baud_rate_bps);
+ auto status = spi.Configure(configuration);
+
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, PollingWriteSuccess) {
+ const auto blocking = true;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ static auto destination = bytes::Array<0xff, 0xff, 0xff, 0xff, 0xff>();
+
+ McuxpressoInitiator spi(spi_base,
+ CLOCK_GetFlexcommClkFreq(kClockNumber),
+ baud_rate_bps,
+ blocking);
+ auto status = spi.Configure(configuration);
+ ASSERT_EQ(status, OkStatus());
+
+ status = spi.WriteRead(source, destination);
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, IRQWriteSuccess) {
+ const auto blocking = false;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ static auto destination = bytes::Array<0xff, 0xff, 0xff, 0xff, 0xff>();
+ McuxpressoInitiator spi(spi_base,
+ CLOCK_GetFlexcommClkFreq(kClockNumber),
+ baud_rate_bps,
+ blocking);
+ auto status = spi.Configure(configuration);
+ ASSERT_EQ(status, OkStatus());
+
+ status = spi.WriteRead(source, destination);
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, WriteOnlySuccess) {
+ const auto blocking = false;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ McuxpressoInitiator spi(spi_base,
+ CLOCK_GetFlexcommClkFreq(kClockNumber),
+ baud_rate_bps,
+ blocking);
+ auto status = spi.Configure(configuration);
+ ASSERT_EQ(status, OkStatus());
+
+ status = spi.WriteRead(source, {});
+ EXPECT_EQ(status, OkStatus());
+}
+
+TEST(ReadWrite, UseDeviceWriteOnlySuccess) {
+ const auto blocking = false;
+ constexpr auto source = bytes::Array<0x01, 0x02, 0x03, 0x04, 0x05>();
+ McuxpressoInitiator initiator(spi_base,
+ CLOCK_GetFlexcommClkFreq(kClockNumber),
+ baud_rate_bps,
+ blocking);
+ McuxpressoChipSelector spi_selector(initiator, 0);
+ sync::VirtualMutex spi_lock;
+ sync::Borrowable<Initiator> spi(initiator, spi_lock);
+ Device device(spi, configuration, spi_selector);
+
+ EXPECT_EQ(device.WriteRead(source, {}), OkStatus());
+}
+} // namespace
+} // namespace pw::spi
diff --git a/pw_status/CMakeLists.txt b/pw_status/CMakeLists.txt
index 70f2be287..4b2a11803 100644
--- a/pw_status/CMakeLists.txt
+++ b/pw_status/CMakeLists.txt
@@ -37,9 +37,6 @@ pw_add_library(pw_status STATIC
SOURCES
status.cc
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_STATUS)
- zephyr_link_libraries(pw_status)
-endif()
# Use this for pw_status_CONFIG to require pw::Status objects to be used.
pw_add_library(pw_status.check_if_used INTERFACE
diff --git a/pw_status/Kconfig b/pw_status/Kconfig
index 9fd180af4..6efe64678 100644
--- a/pw_status/Kconfig
+++ b/pw_status/Kconfig
@@ -12,5 +12,11 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_status"
+
config PIGWEED_STATUS
- bool "Enable the Pigweed status library (pw_status)"
+ bool "Link pw_status library"
+ help
+ See :ref:`module-pw_status` for module details.
+
+endmenu
diff --git a/pw_status/docs.rst b/pw_status/docs.rst
index 997ca539c..3432361f4 100644
--- a/pw_status/docs.rst
+++ b/pw_status/docs.rst
@@ -1,172 +1,387 @@
.. _module-pw_status:
----------
+=========
pw_status
----------
+=========
``pw_status`` provides features for communicating the result of an operation.
The classes in ``pw_status`` are used extensively throughout Pigweed.
-pw::Status
-==========
-The primary feature of ``pw_status`` is the ``pw::Status`` class.
-``pw::Status`` (``pw_status/status.h``) is a simple, zero-overhead status
-object that wraps a status code.
+------
+Status
+------
+The primary feature of ``pw_status`` is the ``Status`` class (in C++) or enum
+(in other languages). ``pw_status`` provides an implementation of status in
+every supported Pigweed language.
-``pw::Status`` uses Google's standard status codes (see the `Google APIs
+The C++ implementation is :cpp:class:`pw::Status`, a simple, zero-overhead
+status object that wraps a status code. Other languages use an enum.
+
+Pigweed's status uses Google's standard status codes (see the `Google APIs
repository <https://github.com/googleapis/googleapis/blob/HEAD/google/rpc/code.proto>`_).
These codes are used extensively in Google projects including `Abseil
<https://abseil.io>`_ (`status/status.h
-<https://cs.opensource.google/abseil/abseil-cpp/+/HEAD:absl/status/status.h>`_
-) and `gRPC <https://grpc.io>`_ (`doc/statuscodes.md
+<https://cs.opensource.google/abseil/abseil-cpp/+/HEAD:absl/status/status.h>`_)
+and `gRPC <https://grpc.io>`_ (`doc/statuscodes.md
<https://github.com/grpc/grpc/blob/HEAD/doc/statuscodes.md>`_).
-An OK ``Status`` is created by the ``pw::OkStatus`` function or by the default
-``Status`` constructor. Non-OK ``Status`` is created with a static member
-function that corresponds with the status code.
+Status codes
+============
+.. c:enumerator:: OK = 0
+
+ ``OK`` does not indicate an error; this value is returned on success. It is
+ typical to check for this value before proceeding on any given call across an
+ API or RPC boundary. To check this value, use the ``ok()`` member function
+ rather than inspecting the raw code.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::OkStatus()``
+ * - C
+ - ``PW_STATUS_OK``
+ * - Python / Java / TypeScript
+ - ``Status.OK``
+ * - Rust
+ - ``Ok(val)``
+
+.. c:enumerator:: CANCELLED = 1
+
+ ``CANCELLED`` indicates the operation was cancelled, typically by the caller.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Cancelled()``
+ * - C
+ - ``PW_STATUS_CANCELLED``
+ * - Python / Java / TypeScript
+ - ``Status.CANCELLED``
+ * - Rust
+ - ``Error::Cancelled``
+
+.. c:enumerator:: UNKNOWN = 2
+
+ ``UNKNOWN`` indicates an unknown error occurred. In general, more specific
+ errors should be raised, if possible. Errors raised by APIs that do not
+ return enough error information may be converted to this error.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Unknown()``
+ * - C
+ - ``PW_STATUS_UNKNOWN``
+ * - Python / Java / TypeScript
+ - ``Status.UNKNOWN``
+ * - Rust
+ - ``Error::Unknown``
+
+.. c:enumerator:: INVALID_ARGUMENT = 3
+
+ ``INVALID_ARGUMENT`` indicates the caller specified an invalid argument, such
+ as a malformed filename. Note that use of such errors should be narrowly
+ limited to indicate the invalid nature of the arguments themselves. Errors
+ with validly formed arguments that may cause errors with the state of the
+ receiving system should be denoted with :c:enumerator:`FAILED_PRECONDITION`
+ instead.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::InvalidArgument()``
+ * - C
+ - ``PW_STATUS_INVALID_ARGUMENT``
+ * - Python / Java / TypeScript
+ - ``Status.INVALID_ARGUMENT``
+ * - Rust
+ - ``Error::InvalidArgument``
+
+.. c:enumerator:: DEADLINE_EXCEEDED = 4
+
+ ``DEADLINE_EXCEEDED`` indicates a deadline expired before the operation could
+ complete. For operations that may change state within a system, this error
+ may be returned even if the operation has completed successfully. For
+ example, a successful response from a server could have been delayed long
+ enough for the deadline to expire.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::DeadlineExceeded()``
+ * - C
+ - ``PW_STATUS_DEADLINE_EXCEEDED``
+ * - Python / Java / TypeScript
+ - ``Status.DEADLINE_EXCEEDED``
+ * - Rust
+ - ``Error::DeadlineExceeded``
+
+.. c:enumerator:: NOT_FOUND = 5
+
+ ``NOT_FOUND`` indicates some requested entity (such as a file or directory)
+ was not found.
+
+ :c:enumerator:`NOT_FOUND` is useful if a request should be denied for an
+ entire class of users, such as during a gradual feature rollout or
+ undocumented allowlist. If a request should be denied for specific sets of
+ users, such as through user-based access control, use
+ :c:enumerator:`PERMISSION_DENIED` instead.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::NotFound()``
+ * - C
+ - ``PW_STATUS_NOT_FOUND``
+ * - Python / Java / TypeScript
+ - ``Status.NOT_FOUND``
+ * - Rust
+ - ``Error::NotFound``
+
+.. c:enumerator:: ALREADY_EXISTS = 6
+
+ ``ALREADY_EXISTS`` indicates that the entity a caller attempted to create
+ (such as a file or directory) is already present.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::AlreadyExists()``
+ * - C
+ - ``PW_STATUS_ALREADY_EXISTS``
+ * - Python / Java / TypeScript
+ - ``Status.ALREADY_EXISTS``
+ * - Rust
+ - ``Error::AlreadyExists``
+
+.. c:enumerator:: PERMISSION_DENIED = 7
+
+ ``PERMISSION_DENIED`` indicates that the caller does not have permission to
+ execute the specified operation. Note that this error is different than an
+ error due to an unauthenticated user. This error code does not imply the
+ request is valid or the requested entity exists or satisfies any other
+ pre-conditions.
+
+ :c:enumerator:`PERMISSION_DENIED` must not be used for rejections caused by
+ exhausting some resource. Instead, use :c:enumerator:`RESOURCE_EXHAUSTED` for
+ those errors. :c:enumerator:`PERMISSION_DENIED` must not be used if the
+ caller cannot be identified. Instead, use :c:enumerator:`UNAUTHENTICATED`
+ for those errors.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::PermissionDenied()``
+ * - C
+ - ``PW_STATUS_PERMISSION_DENIED``
+ * - Python / Java / TypeScript
+ - ``Status.PERMISSION_DENIED``
+ * - Rust
+ - ``Error::PermissionDenied``
+
+.. c:enumerator:: RESOURCE_EXHAUSTED = 8
+
+ ``RESOURCE_EXHAUSTED`` indicates some resource has been exhausted, perhaps a
+ per-user quota, or perhaps the entire file system is out of space.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::ResourceExhausted()``
+ * - C
+ - ``PW_STATUS_RESOURCE_EXHAUSTED``
+ * - Python / Java / TypeScript
+ - ``Status.RESOURCE_EXHAUSTED``
+ * - Rust
+ - ``Error::ResourceExhausted``
+
+.. c:enumerator:: FAILED_PRECONDITION = 9
+
+ ``FAILED_PRECONDITION`` indicates that the operation was rejected because the
+ system is not in a state required for the operation's execution. For example,
+ a directory to be deleted may be non-empty, an ``rmdir`` operation is applied
+ to a non-directory, etc.
+
+ .. _module-pw_status-guidelines:
+
+ Some guidelines that may help a service implementer in deciding between
+ :c:enumerator:`FAILED_PRECONDITION`, :c:enumerator:`ABORTED`, and
+ :c:enumerator:`UNAVAILABLE`:
+
+ a. Use :c:enumerator:`UNAVAILABLE` if the client can retry just the failing
+ call.
+ b. Use :c:enumerator:`ABORTED` if the client should retry at a higher
+ transaction level (such as when a client-specified test-and-set fails,
+ indicating the client should restart a read-modify-write sequence).
+ c. Use :c:enumerator:`FAILED_PRECONDITION` if the client should not retry
+ until the system state has been explicitly fixed. For example, if a
+ ``rmdir`` fails because the directory is non-empty,
+ :c:enumerator:`FAILED_PRECONDITION` should be returned since the client
+ should not retry unless the files are deleted from the directory.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::FailedPrecondition()``
+ * - C
+ - ``PW_STATUS_FAILED_PRECONDITION``
+ * - Python / Java / TypeScript
+ - ``Status.FAILED_PRECONDITION``
+ * - Rust
+ - ``Error::FailedPrecondition``
+
+.. c:enumerator:: ABORTED = 10
+
+ ``ABORTED`` indicates the operation was aborted, typically due to a
+ concurrency issue such as a sequencer check failure or a failed transaction.
+
+ See the :ref:`guidelines <module-pw_status-guidelines>` above for deciding
+ between :c:enumerator:`FAILED_PRECONDITION`, :c:enumerator:`ABORTED`, and
+ :c:enumerator:`UNAVAILABLE`.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Aborted()``
+ * - C
+ - ``PW_STATUS_ABORTED``
+ * - Python / Java / TypeScript
+ - ``Status.ABORTED``
+ * - Rust
+ - ``Error::Aborted``
+
+.. c:enumerator:: OUT_OF_RANGE = 11
+
+ ``OUT_OF_RANGE`` indicates the operation was attempted past the valid range,
+ such as seeking or reading past an end-of-file.
+
+ Unlike :c:enumerator:`INVALID_ARGUMENT`, this error indicates a problem that
+ may be fixed if the system state changes. For example, a 32-bit file system
+ will generate :c:enumerator:`INVALID_ARGUMENT` if asked to read at an offset
+ that is not in the range [0,2^32-1], but it will generate
+ :c:enumerator:`OUT_OF_RANGE` if asked to read from an offset past the current
+ file size.
+
+ There is a fair bit of overlap between :c:enumerator:`FAILED_PRECONDITION`
+ and :c:enumerator:`OUT_OF_RANGE`. We recommend using
+ :c:enumerator:`OUT_OF_RANGE` (the more specific error) when it applies so
+ that callers who are iterating through a space can easily look for an
+ :c:enumerator:`OUT_OF_RANGE` error to detect when they are done.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::OutOfRange()``
+ * - C
+ - ``PW_STATUS_OUT_OF_RANGE``
+ * - Python / Java / TypeScript
+ - ``Status.OUT_OF_RANGE``
+ * - Rust
+ - ``Error::OutOfRange``
+
+.. c:enumerator:: UNIMPLEMENTED = 12
+
+ ``UNIMPLEMENTED`` indicates the operation is not implemented or supported in
+ this service. In this case, the operation should not be re-attempted.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Unimplemented()``
+ * - C
+ - ``PW_STATUS_UNIMPLEMENTED``
+ * - Python / Java / TypeScript
+ - ``Status.UNIMPLEMENTED``
+ * - Rust
+ - ``Error::Unimplemented``
+
+.. c:enumerator:: INTERNAL = 13
+
+ ``INTERNAL`` indicates an internal error has occurred and some invariants
+ expected by the underlying system have not been satisfied. This error code is
+ reserved for serious errors.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Internal()``
+ * - C
+ - ``PW_STATUS_INTERNAL``
+ * - Python / Java / TypeScript
+ - ``Status.INTERNAL``
+ * - Rust
+ - ``Error::Internal``
+
+.. c:enumerator:: UNAVAILABLE = 14
+
+ ``UNAVAILABLE`` indicates the service is currently unavailable and that this
+ is most likely a transient condition. An error such as this can be corrected
+ by retrying with a backoff scheme. Note that it is not always safe to retry
+ non-idempotent operations.
+
+ See the :ref:`guidelines <module-pw_status-guidelines>` above for deciding
+ between :c:enumerator:`FAILED_PRECONDITION`, :c:enumerator:`ABORTED`, and
+ :c:enumerator:`UNAVAILABLE`.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Unavailable()``
+ * - C
+ - ``PW_STATUS_UNAVAILABLE``
+ * - Python / Java / TypeScript
+ - ``Status.UNAVAILABLE``
+ * - Rust
+ - ``Error::Unavailable``
+
+.. c:enumerator:: DATA_LOSS = 15
+
+ ``DATA_LOSS`` indicates that unrecoverable data loss or corruption has
+ occurred. As this error is serious, proper alerting should be attached to
+ errors such as this.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::DataLoss()``
+ * - C
+ - ``PW_STATUS_DATA_LOSS``
+ * - Python / Java / TypeScript
+ - ``Status.DATA_LOSS``
+ * - Rust
+ - ``Error::DataLoss``
+
+.. c:enumerator:: UNAUTHENTICATED = 16
+
+ ``UNAUTHENTICATED`` indicates that the request does not have valid
+ authentication credentials for the operation. Correct the authentication and
+ try again.
+
+ .. list-table::
+
+ * - C++
+ - ``pw::Status::Unauthenticated()``
+ * - C
+ - ``PW_STATUS_UNAUTHENTICATED``
+ * - Python / Java / TypeScript
+ - ``Status.UNAUTHENTICATED``
+ * - Rust
+ - ``Error::Unauthenticated``
-.. code-block:: cpp
+C++ API
+=======
+.. doxygenclass:: pw::Status
+ :members:
+
+.. doxygenfunction:: pw::OkStatus
+
+.. c:enum:: pw_Status
- // Ok (gRPC code "OK") does not indicate an error; this value is returned on
- // success. It is typical to check for this value before proceeding on any
- // given call across an API or RPC boundary. To check this value, use the
- // `status.ok()` member function rather than inspecting the raw code.
- //
- // OkStatus() is provided as a free function, rather than a static member
- // function like the error statuses to avoid conflicts with the ok() member
- // function. Status::Ok() would be too similar to Status::ok().
- pw::OkStatus()
-
- // Cancelled (gRPC code "CANCELLED") indicates the operation was cancelled,
- // typically by the caller.
- pw::Status::Cancelled()
-
- // Unknown (gRPC code "UNKNOWN") indicates an unknown error occurred. In
- // general, more specific errors should be raised, if possible. Errors raised
- // by APIs that do not return enough error information may be converted to
- // this error.
- pw::Status::Unknown()
-
- // InvalidArgument (gRPC code "INVALID_ARGUMENT") indicates the caller
- // specified an invalid argument, such a malformed filename. Note that such
- // errors should be narrowly limited to indicate to the invalid nature of the
- // arguments themselves. Errors with validly formed arguments that may cause
- // errors with the state of the receiving system should be denoted with
- // `FailedPrecondition` instead.
- pw::Status::InvalidArgument()
-
- // DeadlineExceeded (gRPC code "DEADLINE_EXCEEDED") indicates a deadline
- // expired before the operation could complete. For operations that may change
- // state within a system, this error may be returned even if the operation has
- // completed successfully. For example, a successful response from a server
- // could have been delayed long enough for the deadline to expire.
- pw::Status::DeadlineExceeded()
-
- // NotFound (gRPC code "NOT_FOUND") indicates some requested entity (such as
- // a file or directory) was not found.
- //
- // `NotFound` is useful if a request should be denied for an entire class of
- // users, such as during a gradual feature rollout or undocumented allow list.
- // If, instead, a request should be denied for specific sets of users, such as
- // through user-based access control, use `PermissionDenied` instead.
- pw::Status::NotFound()
-
- // AlreadyExists (gRPC code "ALREADY_EXISTS") indicates the entity that a
- // caller attempted to create (such as file or directory) is already present.
- pw::Status::AlreadyExists()
-
- // PermissionDenied (gRPC code "PERMISSION_DENIED") indicates that the caller
- // does not have permission to execute the specified operation. Note that this
- // error is different than an error due to an *un*authenticated user. This
- // error code does not imply the request is valid or the requested entity
- // exists or satisfies any other pre-conditions.
- //
- // `PermissionDenied` must not be used for rejections caused by exhausting
- // some resource. Instead, use `ResourceExhausted` for those errors.
- // `PermissionDenied` must not be used if the caller cannot be identified.
- // Instead, use `Unauthenticated` for those errors.
- pw::Status::PermissionDenied()
-
- // ResourceExhausted (gRPC code "RESOURCE_EXHAUSTED") indicates some resource
- // has been exhausted, perhaps a per-user quota, or perhaps the entire file
- // system is out of space.
- pw::Status::ResourceExhausted()
-
- // FailedPrecondition (gRPC code "FAILED_PRECONDITION") indicates that the
- // operation was rejected because the system is not in a state required for
- // the operation's execution. For example, a directory to be deleted may be
- // non-empty, an "rmdir" operation is applied to a non-directory, etc.
- //
- // Some guidelines that may help a service implementer in deciding between
- // `FailedPrecondition`, `Aborted`, and `Unavailable`:
- //
- // (a) Use `Unavailable` if the client can retry just the failing call.
- // (b) Use `Aborted` if the client should retry at a higher transaction
- // level (such as when a client-specified test-and-set fails, indicating
- // the client should restart a read-modify-write sequence).
- // (c) Use `FailedPrecondition` if the client should not retry until
- // the system state has been explicitly fixed. For example, if an "rmdir"
- // fails because the directory is non-empty, `FailedPrecondition`
- // should be returned since the client should not retry unless
- // the files are deleted from the directory.
- pw::Status::FailedPrecondition()
-
- // Aborted (gRPC code "ABORTED") indicates the operation was aborted,
- // typically due to a concurrency issue such as a sequencer check failure or a
- // failed transaction.
- //
- // See the guidelines above for deciding between `FailedPrecondition`,
- // `Aborted`, and `Unavailable`.
- pw::Status::Aborted()
-
- // OutOfRange (gRPC code "OUT_OF_RANGE") indicates the operation was
- // attempted past the valid range, such as seeking or reading past an
- // end-of-file.
- //
- // Unlike `InvalidArgument`, this error indicates a problem that may
- // be fixed if the system state changes. For example, a 32-bit file
- // system will generate `InvalidArgument` if asked to read at an
- // offset that is not in the range [0,2^32-1], but it will generate
- // `OutOfRange` if asked to read from an offset past the current
- // file size.
- //
- // There is a fair bit of overlap between `FailedPrecondition` and
- // `OutOfRange`. We recommend using `OutOfRange` (the more specific
- // error) when it applies so that callers who are iterating through
- // a space can easily look for an `OutOfRange` error to detect when
- // they are done.
- pw::Status::OutOfRange()
-
- // Unimplemented (gRPC code "UNIMPLEMENTED") indicates the operation is not
- // implemented or supported in this service. In this case, the operation
- // should not be re-attempted.
- pw::Status::Unimplemented()
-
- // Internal (gRPC code "INTERNAL") indicates an internal error has occurred
- // and some invariants expected by the underlying system have not been
- // satisfied. This error code is reserved for serious errors.
- pw::Status::Internal()
-
- // Unavailable (gRPC code "UNAVAILABLE") indicates the service is currently
- // unavailable and that this is most likely a transient condition. An error
- // such as this can be corrected by retrying with a backoff scheme. Note that
- // it is not always safe to retry non-idempotent operations.
- //
- // See the guidelines above for deciding between `FailedPrecondition`,
- // `Aborted`, and `Unavailable`.
- pw::Status::Unavailable()
-
- // DataLoss (gRPC code "DATA_LOSS") indicates that unrecoverable data loss or
- // corruption has occurred. As this error is serious, proper alerting should
- // be attached to errors such as this.
- pw::Status::DataLoss()
-
- // Unauthenticated (gRPC code "UNAUTHENTICATED") indicates that the request
- // does not have valid authentication credentials for the operation. Correct
- // the authentication and try again.
- pw::Status::Unauthenticated()
-
-.. note::
- Status enumerations are also supported for Python and Typescript.
+ Enum to use in place of :cpp:class:`pw::Status` in C code. Always use
+ :cpp:class:`pw::Status` in C++ code.
+
+ The values of the :c:enum:`pw_Status` enum are all-caps and prefixed with
+ ``PW_STATUS_``. For example, ``PW_STATUS_DATA_LOSS`` corresponds with
+ :c:enumerator:`DATA_LOSS`.
Tracking the first error encountered
------------------------------------
@@ -175,39 +390,40 @@ allowing execution to continue. Manually writing out ``if`` statements to check
and then assign quickly becomes verbose, and doesn't explicitly highlight the
intended behavior of "latching" to the first error.
- .. code-block:: cpp
-
- Status overall_status;
- for (Sector& sector : sectors) {
- Status erase_status = sector.Erase();
- if (!overall_status.ok()) {
- overall_status = erase_status;
- }
-
- if (erase_status.ok()) {
- Status header_write_status = sector.WriteHeader();
- if (!overall_status.ok()) {
- overall_status = header_write_status;
- }
- }
- }
- return overall_status;
+.. code-block:: cpp
-``pw::Status`` has an ``Update()`` helper function that does exactly this to
-reduce visual clutter and succinctly highlight the intended behavior.
+ Status overall_status;
+ for (Sector& sector : sectors) {
+ Status erase_status = sector.Erase();
+ if (!overall_status.ok()) {
+ overall_status = erase_status;
+ }
+
+ if (erase_status.ok()) {
+ Status header_write_status = sector.WriteHeader();
+ if (!overall_status.ok()) {
+ overall_status = header_write_status;
+ }
+ }
+ }
+ return overall_status;
+
+:cpp:class:`pw::Status` has a :cpp:func:`pw::Status::Update()` helper function
+that does exactly this to reduce visual clutter and succinctly highlight the
+intended behavior.
- .. code-block:: cpp
+.. code-block:: cpp
- Status overall_status;
- for (Sector& sector : sectors) {
- Status erase_status = sector.Erase();
- overall_status.Update(erase_status);
+ Status overall_status;
+ for (Sector& sector : sectors) {
+ Status erase_status = sector.Erase();
+ overall_status.Update(erase_status);
- if (erase_status.ok()) {
- overall_status.Update(sector.WriteHeader());
- }
- }
- return overall_status;
+ if (erase_status.ok()) {
+ overall_status.Update(sector.WriteHeader());
+ }
+ }
+ return overall_status;
Unused result warnings
----------------------
@@ -222,17 +438,19 @@ be enabled unconditionally.
C compatibility
---------------
-``pw_status`` provides the C-compatible ``pw_Status`` enum for the status codes.
-For ease of use, ``pw::Status`` implicitly converts to and from ``pw_Status``.
-However, the ``pw_Status`` enum should never be used in C++; instead use the
-``Status`` class.
+``pw_status`` provides the C-compatible :c:enum:`pw_Status` enum for the status
+codes. For ease of use, :cpp:class:`pw::Status` implicitly converts to and from
+:c:enum:`pw_Status`. However, the :c:enum:`pw_Status` enum should never be used
+in C++; instead use the :cpp:class:`pw::Status` class.
-The values of the ``pw_Status`` enum are all-caps and prefixed with
-``PW_STATUS_``. For example, ``PW_STATUS_DATA_LOSS`` corresponds with the C++
-``Status::DataLoss()``.
+Rust API
+========
+``pw_status``'s Rust API is documented in our
+`rustdoc API docs </rustdoc/pw_status>`_.
+--------------
StatusWithSize
-==============
+--------------
``pw::StatusWithSize`` (``pw_status/status_with_size.h``) is a convenient,
efficient class for reporting a status along with an unsigned integer value.
It is similar to the ``pw::Result<T>`` class, but it stores both a size and a
@@ -242,61 +460,59 @@ bits).
``pw::StatusWithSize`` values may be created with functions similar to
``pw::Status``. For example,
- .. code-block:: cpp
+.. code-block:: cpp
- // An OK StatusWithSize with a size of 123.
- StatusWithSize(123)
+ // An OK StatusWithSize with a size of 123.
+ StatusWithSize(123)
- // A NOT_FOUND StatusWithSize with a size of 0.
- StatusWithSize::NotFound()
+ // A NOT_FOUND StatusWithSize with a size of 0.
+ StatusWithSize::NotFound()
- // A RESOURCE_EXHAUSTED StatusWithSize with a size of 10.
- StatusWithSize::ResourceExhausted(10)
+ // A RESOURCE_EXHAUSTED StatusWithSize with a size of 10.
+ StatusWithSize::ResourceExhausted(10)
+------
PW_TRY
-======
+------
``PW_TRY`` (``pw_status/try.h``) is a convenient set of macros for working
-with Status and StatusWithSize objects in functions that return Status or
-StatusWithSize. The PW_TRY and PW_TRY_WITH_SIZE macros call a function and
-do an early return if the function's return status is not ok.
+with Status and ``StatusWithSize`` objects in functions that return Status or
+``StatusWithSize``. The ``PW_TRY`` and ``PW_TRY_WITH_SIZE`` macros call a
+function and do an early return if the function's return status is not ok.
Example:
.. code-block:: cpp
- Status PwTryExample() {
- PW_TRY(FunctionThatReturnsStatus());
- PW_TRY(FunctionThatReturnsStatusWithSize());
+ Status PwTryExample() {
+ PW_TRY(FunctionThatReturnsStatus());
+ PW_TRY(FunctionThatReturnsStatusWithSize());
- // Do something, only executed if both functions above return OK.
- }
+ // Do something, only executed if both functions above return OK.
+ }
- StatusWithSize PwTryWithSizeExample() {
- PW_TRY_WITH_SIZE(FunctionThatReturnsStatus());
- PW_TRY_WITH_SIZE(FunctionThatReturnsStatusWithSize());
+ StatusWithSize PwTryWithSizeExample() {
+ PW_TRY_WITH_SIZE(FunctionThatReturnsStatus());
+ PW_TRY_WITH_SIZE(FunctionThatReturnsStatusWithSize());
- // Do something, only executed if both functions above return OK.
- }
+ // Do something, only executed if both functions above return OK.
+ }
-PW_TRY_ASSIGN is for working with StatusWithSize objects in in functions
-that return Status. It is similar to PW_TRY with the addition of assigning
-the size from the StatusWithSize on ok.
+``PW_TRY_ASSIGN`` is for working with ``StatusWithSize`` objects in in functions
+that return Status. It is similar to ``PW_TRY`` with the addition of assigning
+the size from the ``StatusWithSize`` on ok.
.. code-block:: cpp
- Status PwTryAssignExample() {
- size_t size_value
- PW_TRY_ASSIGN(size_value, FunctionThatReturnsStatusWithSize());
-
- // Do something that uses size_value. size_value is only assigned and this
- // following code executed if the PW_TRY_ASSIGN function above returns OK.
- }
+ Status PwTryAssignExample() {
+ size_t size_value
+ PW_TRY_ASSIGN(size_value, FunctionThatReturnsStatusWithSize());
-Compatibility
-=============
-C++14
+ // Do something that uses size_value. size_value is only assigned and this
+ // following code executed if the PW_TRY_ASSIGN function above returns OK.
+ }
+------
Zephyr
-======
+------
To enable ``pw_status`` for Zephyr add ``CONFIG_PIGWEED_STATUS=y`` to the
project's configuration.
diff --git a/pw_status/public/pw_status/status.h b/pw_status/public/pw_status/status.h
index 8ed5330ad..9f6c3e68c 100644
--- a/pw_status/public/pw_status/status.h
+++ b/pw_status/public/pw_status/status.h
@@ -29,141 +29,27 @@ extern "C" {
// pw_Status uses the canonical Google error codes. The following enum was based
// on Abseil's status/status.h. The values are all-caps and prefixed with
// PW_STATUS_ instead of using C++ constant style.
+//
+// The status codes are described at https://pigweed.dev/pw_status#status-codes.
+// Consult that guide when deciding which status code to use.
typedef enum {
- // Ok (gRPC code "OK") does not indicate an error; this value is returned on
- // success. It is typical to check for this value before proceeding on any
- // given call across an API or RPC boundary. To check this value, use the
- // `Status::ok()` member function rather than inspecting the raw code.
- PW_STATUS_OK = 0, // Use OkStatus() in C++
-
- // Cancelled (gRPC code "CANCELLED") indicates the operation was cancelled,
- // typically by the caller.
- PW_STATUS_CANCELLED = 1, // Use Status::Cancelled() in C++
-
- // Unknown (gRPC code "UNKNOWN") indicates an unknown error occurred. In
- // general, more specific errors should be raised, if possible. Errors raised
- // by APIs that do not return enough error information may be converted to
- // this error.
- PW_STATUS_UNKNOWN = 2, // Use Status::Unknown() in C++
-
- // InvalidArgument (gRPC code "INVALID_ARGUMENT") indicates the caller
- // specified an invalid argument, such a malformed filename. Note that such
- // errors should be narrowly limited to indicate to the invalid nature of the
- // arguments themselves. Errors with validly formed arguments that may cause
- // errors with the state of the receiving system should be denoted with
- // `FailedPrecondition` instead.
- PW_STATUS_INVALID_ARGUMENT = 3, // Use Status::InvalidArgument() in C++
-
- // DeadlineExceeded (gRPC code "DEADLINE_EXCEEDED") indicates a deadline
- // expired before the operation could complete. For operations that may change
- // state within a system, this error may be returned even if the operation has
- // completed successfully. For example, a successful response from a server
- // could have been delayed long enough for the deadline to expire.
- PW_STATUS_DEADLINE_EXCEEDED = 4, // Use Status::DeadlineExceeded() in C++
-
- // NotFound (gRPC code "NOT_FOUND") indicates some requested entity (such as
- // a file or directory) was not found.
- //
- // `NotFound` is useful if a request should be denied for an entire class of
- // users, such as during a gradual feature rollout or undocumented allow list.
- // If, instead, a request should be denied for specific sets of users, such as
- // through user-based access control, use `PermissionDenied` instead.
- PW_STATUS_NOT_FOUND = 5, // Use Status::NotFound() in C++
-
- // AlreadyExists (gRPC code "ALREADY_EXISTS") indicates the entity that a
- // caller attempted to create (such as file or directory) is already present.
- PW_STATUS_ALREADY_EXISTS = 6, // Use Status::AlreadyExists() in C++
-
- // PermissionDenied (gRPC code "PERMISSION_DENIED") indicates that the caller
- // does not have permission to execute the specified operation. Note that this
- // error is different than an error due to an *un*authenticated user. This
- // error code does not imply the request is valid or the requested entity
- // exists or satisfies any other pre-conditions.
- //
- // `PermissionDenied` must not be used for rejections caused by exhausting
- // some resource. Instead, use `ResourceExhausted` for those errors.
- // `PermissionDenied` must not be used if the caller cannot be identified.
- // Instead, use `Unauthenticated` for those errors.
- PW_STATUS_PERMISSION_DENIED = 7, // Use Status::PermissionDenied() in C++
-
- // ResourceExhausted (gRPC code "RESOURCE_EXHAUSTED") indicates some resource
- // has been exhausted, perhaps a per-user quota, or perhaps the entire file
- // system is out of space.
- PW_STATUS_RESOURCE_EXHAUSTED = 8, // Use Status::ResourceExhausted() in C++
-
- // FailedPrecondition (gRPC code "FAILED_PRECONDITION") indicates that the
- // operation was rejected because the system is not in a state required for
- // the operation's execution. For example, a directory to be deleted may be
- // non-empty, an "rmdir" operation is applied to a non-directory, etc.
- //
- // Some guidelines that may help a service implementer in deciding between
- // `FailedPrecondition`, `Aborted`, and `Unavailable`:
- //
- // (a) Use `Unavailable` if the client can retry just the failing call.
- // (b) Use `Aborted` if the client should retry at a higher transaction
- // level (such as when a client-specified test-and-set fails, indicating
- // the client should restart a read-modify-write sequence).
- // (c) Use `FailedPrecondition` if the client should not retry until
- // the system state has been explicitly fixed. For example, if an "rmdir"
- // fails because the directory is non-empty, `FailedPrecondition`
- // should be returned since the client should not retry unless
- // the files are deleted from the directory.
+ PW_STATUS_OK = 0, // Use OkStatus() in C++
+ PW_STATUS_CANCELLED = 1, // Use Status::Cancelled() in C++
+ PW_STATUS_UNKNOWN = 2, // Use Status::Unknown() in C++
+ PW_STATUS_INVALID_ARGUMENT = 3, // Use Status::InvalidArgument() in C++
+ PW_STATUS_DEADLINE_EXCEEDED = 4, // Use Status::DeadlineExceeded() in C++
+ PW_STATUS_NOT_FOUND = 5, // Use Status::NotFound() in C++
+ PW_STATUS_ALREADY_EXISTS = 6, // Use Status::AlreadyExists() in C++
+ PW_STATUS_PERMISSION_DENIED = 7, // Use Status::PermissionDenied() in C++
+ PW_STATUS_RESOURCE_EXHAUSTED = 8, // Use Status::ResourceExhausted() in C++
PW_STATUS_FAILED_PRECONDITION = 9, // Use Status::FailedPrecondition() in C++
-
- // Aborted (gRPC code "ABORTED") indicates the operation was aborted,
- // typically due to a concurrency issue such as a sequencer check failure or a
- // failed transaction.
- //
- // See the guidelines above for deciding between `FailedPrecondition`,
- // `Aborted`, and `Unavailable`.
- PW_STATUS_ABORTED = 10, // Use Status::Aborted() in C++
-
- // OutOfRange (gRPC code "OUT_OF_RANGE") indicates the operation was
- // attempted past the valid range, such as seeking or reading past an
- // end-of-file.
- //
- // Unlike `InvalidArgument`, this error indicates a problem that may
- // be fixed if the system state changes. For example, a 32-bit file
- // system will generate `InvalidArgument` if asked to read at an
- // offset that is not in the range [0,2^32-1], but it will generate
- // `OutOfRange` if asked to read from an offset past the current
- // file size.
- //
- // There is a fair bit of overlap between `FailedPrecondition` and
- // `OutOfRange`. We recommend using `OutOfRange` (the more specific
- // error) when it applies so that callers who are iterating through
- // a space can easily look for an `OutOfRange` error to detect when
- // they are done.
- PW_STATUS_OUT_OF_RANGE = 11, // Use Status::OutOfRange() in C++
-
- // Unimplemented (gRPC code "UNIMPLEMENTED") indicates the operation is not
- // implemented or supported in this service. In this case, the operation
- // should not be re-attempted.
- PW_STATUS_UNIMPLEMENTED = 12, // Use Status::Unimplemented() in C++
-
- // Internal (gRPC code "INTERNAL") indicates an internal error has occurred
- // and some invariants expected by the underlying system have not been
- // satisfied. This error code is reserved for serious errors.
- PW_STATUS_INTERNAL = 13, // Use Status::Internal() in C++
-
- // Unavailable (gRPC code "UNAVAILABLE") indicates the service is currently
- // unavailable and that this is most likely a transient condition. An error
- // such as this can be corrected by retrying with a backoff scheme. Note that
- // it is not always safe to retry non-idempotent operations.
- //
- // See the guidelines above for deciding between `FailedPrecondition`,
- // `Aborted`, and `Unavailable`.
- PW_STATUS_UNAVAILABLE = 14, // Use Status::Unavailable() in C++
-
- // DataLoss (gRPC code "DATA_LOSS") indicates that unrecoverable data loss or
- // corruption has occurred. As this error is serious, proper alerting should
- // be attached to errors such as this.
- PW_STATUS_DATA_LOSS = 15, // Use Status::DataLoss() in C++
-
- // Unauthenticated (gRPC code "UNAUTHENTICATED") indicates that the request
- // does not have valid authentication credentials for the operation. Correct
- // the authentication and try again.
- PW_STATUS_UNAUTHENTICATED = 16, // Use Status::Unauthenticated() in C++
+ PW_STATUS_ABORTED = 10, // Use Status::Aborted() in C++
+ PW_STATUS_OUT_OF_RANGE = 11, // Use Status::OutOfRange() in C++
+ PW_STATUS_UNIMPLEMENTED = 12, // Use Status::Unimplemented() in C++
+ PW_STATUS_INTERNAL = 13, // Use Status::Internal() in C++
+ PW_STATUS_UNAVAILABLE = 14, // Use Status::Unavailable() in C++
+ PW_STATUS_DATA_LOSS = 15, // Use Status::DataLoss() in C++
+ PW_STATUS_UNAUTHENTICATED = 16, // Use Status::Unauthenticated() in C++
// NOTE: this error code entry should not be used and you should not rely on
// its value, which may change.
@@ -178,20 +64,32 @@ typedef enum {
// Returns a null-terminated string representation of the pw_Status.
const char* pw_StatusString(pw_Status status);
+// Indicates the status code with the highest valid value.
+#define PW_STATUS_LAST PW_STATUS_UNAUTHENTICATED
+
#ifdef __cplusplus
} // extern "C"
namespace pw {
-// The Status class is a thin, zero-cost abstraction around the pw_Status enum.
-// It initializes to OkStatus() by default and adds ok() and str() methods.
-// Implicit conversions are permitted between pw_Status and pw::Status.
+/// `Status` is a thin, zero-cost abstraction around the `pw_Status` enum. It
+/// initializes to @pw_status{OK} by default and adds `ok()` and `str()`
+/// methods. Implicit conversions are permitted between `pw_Status` and
+/// `pw::Status`.
+///
+/// An @pw_status{OK} `Status` is created by the @cpp_func{pw::OkStatus}
+/// function or by the default `Status` constructor. Non-OK `Status` is created
+/// with a static member function that corresponds with the status code.
class _PW_STATUS_NO_DISCARD Status {
public:
using Code = pw_Status;
// Functions that create a Status with the specified code.
+ //
+ // The status codes are described at
+ // https://pigweed.dev/pw_status#status-codes. Consult that guide when
+ // deciding which status code to use.
// clang-format off
[[nodiscard]] static constexpr Status Cancelled() {
return PW_STATUS_CANCELLED;
@@ -249,10 +147,12 @@ class _PW_STATUS_NO_DISCARD Status {
constexpr Status(const Status&) = default;
constexpr Status& operator=(const Status&) = default;
- // Returns the Status::Code (pw_Status) for this Status.
+ /// Returns the `Status::Code` (`pw_Status`) for this `Status`.
constexpr Code code() const { return code_; }
- // True if the status is OK.
+ /// True if the status is @pw_status{OK}.
+ ///
+ /// This function is provided in place of an `IsOk()` function.
[[nodiscard]] constexpr bool ok() const { return code_ == PW_STATUS_OK; }
// Functions for checking which status this is.
@@ -305,30 +205,31 @@ class _PW_STATUS_NO_DISCARD Status {
return code_ == PW_STATUS_UNAUTHENTICATED;
}
- // Updates this Status to the provided Status IF this status is OK. This is
- // useful for tracking the first encountered error, as calls to this helper
- // will not change one error status to another error status.
+ /// Updates this `Status` to the provided `Status` IF this status is
+ /// @pw_status{OK}. This is useful for tracking the first encountered error,
+ /// as calls to this helper will not change one error status to another error
+ /// status.
constexpr void Update(Status other) {
if (ok()) {
code_ = other.code();
}
}
- // Ignores any errors. This method does nothing except potentially suppress
- // complaints from any tools that are checking that errors are not dropped on
- // the floor.
+ /// Ignores any errors. This method does nothing except potentially suppress
+ /// complaints from any tools that are checking that errors are not dropped on
+ /// the floor.
constexpr void IgnoreError() const {}
- // Returns a null-terminated string representation of the Status.
+ /// Returns a null-terminated string representation of the `Status`.
[[nodiscard]] const char* str() const { return pw_StatusString(code_); }
private:
Code code_;
};
-// Returns an OK status. Equivalent to Status() or Status(PW_STATUS_OK). This
-// function is used instead of a Status::Ok() function, which would be too
-// similar to Status::ok().
+/// Returns an @pw_status{OK} status. Equivalent to `Status()` or
+/// `Status(PW_STATUS_OK)`. This function is used instead of a `Status::Ok()`
+/// function, which would be too similar to `Status::ok()`.
[[nodiscard]] constexpr Status OkStatus() { return Status(); }
constexpr bool operator==(const Status& lhs, const Status& rhs) {
diff --git a/pw_status/public/pw_status/status_with_size.h b/pw_status/public/pw_status/status_with_size.h
index 9dfb9521e..f68baa67b 100644
--- a/pw_status/public/pw_status/status_with_size.h
+++ b/pw_status/public/pw_status/status_with_size.h
@@ -103,7 +103,8 @@ class _PW_STATUS_NO_DISCARD StatusWithSize {
// std::enable_if is used to prevent enum types (e.g. Status) from being used.
// TODO(hepler): Add debug-only assert that size <= max_size().
template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
- explicit constexpr StatusWithSize(T size) : size_(size) {}
+ explicit constexpr StatusWithSize(T size)
+ : size_(static_cast<size_t>(size)) {}
// Creates a StatusWithSize with the provided status and size.
explicit constexpr StatusWithSize(Status status, size_t size)
diff --git a/pw_status/py/BUILD.gn b/pw_status/py/BUILD.gn
index c01ee12cd..008e928ac 100644
--- a/pw_status/py/BUILD.gn
+++ b/pw_status/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [ "pw_status/__init__.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
diff --git a/pw_status/py/pw_status/__init__.py b/pw_status/py/pw_status/__init__.py
index 7fcc19e98..527afb7dc 100644
--- a/pw_status/py/pw_status/__init__.py
+++ b/pw_status/py/pw_status/__init__.py
@@ -17,6 +17,13 @@ import enum
class Status(enum.Enum):
+ """Pigweed status codes.
+
+ The status codes are described at
+ https://pigweed.dev/pw_status#status-codes. Consult that guide when deciding
+ which status code to use.
+ """
+
OK = 0
CANCELLED = 1
UNKNOWN = 2
diff --git a/pw_status/py/setup.py b/pw_status/py/setup.py
deleted file mode 100644
index 69875d95a..000000000
--- a/pw_status/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_status"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_status/rust/BUILD.bazel b/pw_status/rust/BUILD.bazel
new file mode 100644
index 000000000..e27c92877
--- /dev/null
+++ b/pw_status/rust/BUILD.bazel
@@ -0,0 +1,44 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_doc_test", "rust_library", "rust_test")
+
+rust_library(
+ name = "pw_status",
+ srcs = ["pw_status.rs"],
+ crate_features = select({
+ "@rust_crates//:std": ["std"],
+ "//conditions:default": [""],
+ }),
+ visibility = ["//visibility:public"],
+)
+
+rust_test(
+ name = "pw_status_test",
+ crate = ":pw_status",
+ crate_features = select({
+ "@rust_crates//:std": ["std"],
+ "//conditions:default": [""],
+ }),
+)
+
+rust_doc_test(
+ name = "pw_status_doc_test",
+ crate = ":pw_status",
+)
+
+rust_doc(
+ name = "pw_status_doc",
+ crate = ":pw_status",
+)
diff --git a/pw_status/rust/pw_status.rs b/pw_status/rust/pw_status.rs
new file mode 100644
index 000000000..bb20628c9
--- /dev/null
+++ b/pw_status/rust/pw_status.rs
@@ -0,0 +1,116 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! # pw_status
+//!
+//! Rust error types using error codes compatible with Pigweed's
+//! [pw_status](https://pigweed.dev/pw_status). In order to keep the interface
+//! idiomatic for Rust, `PW_STATUS_OK` is omitted from the Error enum and a
+//! `StatusCode` trait is provided to turn a `Result` into a canonical
+//! status code.
+//!
+//! For an in depth explanation of the values of the `Error` enum, see
+//! the [Pigweed status codes documentation](https://pigweed.dev/pw_status/#status-codes).
+//!
+//! # Example
+//!
+//! ```
+//! use pw_status::{Error, Result};
+//!
+//! fn div(numerator: u32, denominator: u32) -> Result<u32> {
+//! if denominator == 0 {
+//! Err(Error::FailedPrecondition)
+//! } else {
+//! Ok(numerator / denominator)
+//! }
+//! }
+//!
+//! assert_eq!(div(4, 2), Ok(2));
+//! assert_eq!(div(4, 0), Err(Error::FailedPrecondition));
+//! ```
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+/// Status code for no error.
+pub const OK: u32 = 0;
+
+#[cfg_attr(feature = "std", derive(Debug))]
+#[derive(Clone, Copy, Eq, PartialEq)]
+/// Error type compatible with Pigweed's [pw_status](https://pigweed.dev/pw_status).
+///
+/// For an in depth explanation of the values of the `Error` enum, see
+/// the [Pigweed status codes documentation](https://pigweed.dev/pw_status/#status-codes).
+pub enum Error {
+ Cancelled = 1,
+ Unknown = 2,
+ InvalidArgument = 3,
+ DeadlineExceeded = 4,
+ NotFound = 5,
+ AlreadyExists = 6,
+ PermissionDenied = 7,
+ ResourceExhausted = 8,
+ FailedPrecondition = 9,
+ Aborted = 10,
+ OutOfRange = 11,
+ Unimplemented = 12,
+ Internal = 13,
+ Unavailable = 14,
+ DataLoss = 15,
+ Unauthenticated = 16,
+}
+
+pub type Result<T> = core::result::Result<T, Error>;
+
+/// Convert a Result into an status code.
+pub trait StatusCode {
+ /// Return a pigweed compatible status code.
+ fn status_code(self) -> u32;
+}
+
+impl<T> StatusCode for Result<T> {
+ fn status_code(self) -> u32 {
+ match self {
+ Ok(_) => OK,
+ Err(e) => e as u32,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ #[test]
+ fn test_status_code() {
+ assert_eq!(Result::Ok(()).status_code(), 0);
+ assert_eq!(Result::<()>::Err(Error::Cancelled).status_code(), 1);
+ assert_eq!(Result::<()>::Err(Error::Unknown).status_code(), 2);
+ assert_eq!(Result::<()>::Err(Error::InvalidArgument).status_code(), 3);
+ assert_eq!(Result::<()>::Err(Error::DeadlineExceeded).status_code(), 4);
+ assert_eq!(Result::<()>::Err(Error::NotFound).status_code(), 5);
+ assert_eq!(Result::<()>::Err(Error::AlreadyExists).status_code(), 6);
+ assert_eq!(Result::<()>::Err(Error::PermissionDenied).status_code(), 7);
+ assert_eq!(Result::<()>::Err(Error::ResourceExhausted).status_code(), 8);
+ assert_eq!(
+ Result::<()>::Err(Error::FailedPrecondition).status_code(),
+ 9
+ );
+ assert_eq!(Result::<()>::Err(Error::Aborted).status_code(), 10);
+ assert_eq!(Result::<()>::Err(Error::OutOfRange).status_code(), 11);
+ assert_eq!(Result::<()>::Err(Error::Unimplemented).status_code(), 12);
+ assert_eq!(Result::<()>::Err(Error::Internal).status_code(), 13);
+ assert_eq!(Result::<()>::Err(Error::Unavailable).status_code(), 14);
+ assert_eq!(Result::<()>::Err(Error::DataLoss).status_code(), 15);
+ assert_eq!(Result::<()>::Err(Error::Unauthenticated).status_code(), 16);
+ }
+}
diff --git a/pw_status/ts/status.ts b/pw_status/ts/status.ts
index 5b4c0ba45..3d4742412 100644
--- a/pw_status/ts/status.ts
+++ b/pw_status/ts/status.ts
@@ -12,8 +12,12 @@
// License for the specific language governing permissions and limitations under
// the License.
-/** Pigweed Status class; mirrors pw::Status. */
-
+/** Pigweed Status class; mirrors pw::Status.
+ *
+ * The status codes are described at
+ * https://pigweed.dev/pw_status#status-codes. Consult that guide when
+ * deciding which status code to use.
+ */
export enum Status {
OK = 0,
CANCELLED = 1,
diff --git a/pw_stm32cube_build/docs.rst b/pw_stm32cube_build/docs.rst
index 632149536..6857db0d4 100644
--- a/pw_stm32cube_build/docs.rst
+++ b/pw_stm32cube_build/docs.rst
@@ -1,9 +1,8 @@
.. _module-pw_stm32cube_build:
-------------------
+==================
pw_stm32cube_build
-------------------
-
+==================
The ``pw_stm32cube_build`` module provides helper utilities for building a
target with the stm32cube HAL and/or the stm32cube initialization code.
@@ -12,13 +11,29 @@ are documented here. The rationale for keeping the build files in `third_party`
is that code depending on stm32cube can clearly see that their dependency is on
third party, not pigweed code.
-STM32Cube directory setup
-=========================
+.. _module-pw_stm32cube_build-components:
+
+------------------------
+STM32Cube MCU Components
+------------------------
Each stm32 product family (ex. F4, L5, etc.) has its own stm32cube libraries.
This integration depends on ST's 3 core `MCU Components`_ instead of their
monolithic `MCU Package`. The components are the hal_driver, cmsis_core, and
cmsis_device. All of these repos exist on `ST's GitHub page`_. Compatible
version tags are specified on the ``README.md`` of each MCU component.
+
+To use Pigweed's STM32Cube integration, you will need to acquire the three
+components. The details are build-system dependent.
+
+--------
+GN build
+--------
+The primary ``pw_source_set`` for this integration is
+``$dir_pw_third_party/stm32cube:stm32cube``. This source set includes all of
+the HAL, init code, and templates, depending on the value of the `GN args`_.
+
+Directory setup
+===============
Within a single directory, the following directory/file names are required.
=============== =============================================
@@ -40,26 +55,20 @@ generate the ``files.txt``.
pw package install stm32cube_{family}
-GN build
-========
-The primary ``pw_source_set`` for this integration is
-``$dir_pw_third_party/stm32cube:stm32cube``. This source set includes all of
-the HAL, init code, and templates, depending on value of the `GN args`_.
-
Headers
--------
+=======
``$dir_pw_third_party/stm32cube:stm32cube`` contains the following primary
headers that external targets / applications would care about.
``{family}.h``
-^^^^^^^^^^^^^^
+--------------
ex. ``stm32f4xx.h``, ``stm32l5xx.h``
This is the primary HAL header provided by stm32cube. It includes the entire
HAL and all product specific defines.
``stm32cube/stm32cube.h``
-^^^^^^^^^^^^^^^^^^^^^^^^^
+-------------------------
This is a convenience define provided by this integration. It simply includes
``{family}.h``.
@@ -70,7 +79,7 @@ header allows for stm32 family agnostic modules (ex. ``pw_sys_io_stm32``, which
could work with most, if not all families).
``stm32cube/init.h``
-^^^^^^^^^^^^^^^^^^^^
+--------------------
As described in the inject_init_ section, if you decide to use the built in
init functionality, a pre main init function call, ``pw_stm32cube_Init()``, is
injected into ST's startup scripts.
@@ -79,18 +88,18 @@ This header contains the ``pw_stm32cube_Init()`` function declaration. It
should be included and implemented by target init code.
GN args
--------
+=======
The stm32cube GN build arguments are defined in
``$dir_pw_third_party/stm32cube/stm32cube.gni``.
``dir_pw_third_party_stm32cube_xx``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-----------------------------------
These should be set to point to the stm32cube directory for each family that
you need to build for. These are optional to set and are only provided for
convenience if you need to build for multiple families in the same project.
``dir_pw_third_party_stm32cube``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-----------------------------------
This needs to point to the stm32cube directory for the current build.
For multi target projects, the standard practice to set this for each target:
@@ -101,35 +110,36 @@ For multi target projects, the standard practice to set this for each target:
``pw_third_party_stm32cube_PRODUCT``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+------------------------------------
The product specified in as much detail as possible.
ex. ``stm32f429zit``, ``stm32l552ze``, ``stm32f207zg``, etc.
``pw_third_party_stm32cube_CONFIG``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+------------------------------------
The pw_source_set that provides ``stm32{family}xx_hal_conf.h``. The default
uses the in-tree ``stm32{family}xx_hal_conf_template.h``.
``pw_third_party_stm32cube_TIMEBASE``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-------------------------------------
The pw_source_set containing the timebase. The default uses the in-tree
``stm32{family}xx_hal_timebase_tim_template.c``.
``pw_third_party_stm32cube_CMSIS_INIT``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+---------------------------------------
The pw_source_set containing the cmsis init logic. The default uses the in-tree
``system_stm32{family}xx.c``.
``pw_third_party_stm32cube_CORE_INIT``
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+--------------------------------------
pw_source_set containing the core initialization logic. This normally includes
a ``startup_stm32{...}.s`` + a dependent ``pw_linker_script``. The default
``core_init_template`` uses the upstream startup and linker script matching
``pw_third_party_stm32cube_PRODUCT``. If set to "", you must provide your own
linker/startup logic somewhere else in the build.
+-----------------
stm32cube_builder
-=================
+-----------------
``stm32cube_builder`` is utility that contains the backend scripts used by
``pw_package/stm32cube`` and the GN build scripts in ``third_party/stm32cube``
to interact with the stm32cube repos. You should only need to interact with
@@ -138,7 +148,7 @@ using git submodules instead of pw_package, forking the stm32cube libraries,
interfacing with a different build system, or using your own init.
gen_file_list
--------------
+=============
Build systems like GN are unable to depend on arbitrary directories. Instead,
they must have dependencies on specific files. The HAL for each stm32 product
family has different filenames, so ``files.txt`` was created as a workaround.
@@ -154,7 +164,7 @@ directories.
stm32cube_builder gen_file_list /path/to/stm32cube_dir
find_files
-----------
+==========
Within each stm32 family, there are specific products. Although most of the
HAL is common between products, the init code is almost always different.
``find_files`` looks for all of the files relevant to a particular product
@@ -179,7 +189,7 @@ The following variables are output: ``family``, ``product_define``,
stm32cube_builder find_files /path/to/stm32cube_dir stm32{family}{product} [--init]
inject_init
------------
+=============
ST provides init assembly files for every product in ``cmsis_device``. This is
helpful for getting up and running quickly, but they directly call into
``main()`` before initializing the hardware / peripherals. This is because they
@@ -200,7 +210,7 @@ the pre main init call. The output is printed to stdout, or to the specified
stm32cube_builder inject_init /path/to/startup.s [--out-startup-path /path/to/new_startup.s]
icf_to_ld
----------
+=========
Pigweed primarily uses GCC for its Cortex-M builds. However, ST only provides
IAR linker scripts in ``cmsis_device`` for most product families. This script
converts from ST's IAR linker script format (.icf) to a basic GCC linker
@@ -217,3 +227,131 @@ stdout or the specified ``--ld-path``.
.. _`MCU Components`: https://github.com/STMicroelectronics/STM32Cube_MCU_Overall_Offer#stm32cube-mcu-components
.. _`ST's GitHub page`: https://github.com/STMicroelectronics
+
+.. _module-pw_stm32cube_build-bazel:
+
+-----------
+Bazel build
+-----------
+
+External dependencies
+=====================
+As discussed above in :ref:`module-pw_stm32cube_build-components`, you need the
+three STM32Cube Components for your MCU family to use Pigweed's STM32Cube
+integration. You need to add the following git repositories to your workspace:
+
+* ``stm32{family}xx_hal_driver`` (e.g., `HAL driver repo for the F4 family
+ <https://github.com/STMicroelectronics/stm32f4xx_hal_driver/>`_). We provide
+ a Bazel build file which works for any family at
+ ``@pigweed//third_party/stm32cube/stm32_hal_driver.BUILD.bazel``. By default,
+ we assume this repository will be named ``@hal_driver``, but this can be
+ overriden with a label flag (discussed below).
+* ``cmsis_device_{family}`` (e.g., `CMSIS device repo for the F4 family
+ <https://github.com/STMicroelectronics/cmsis_device_f4>`_). We provide a
+ Bazel build file which works for any family at
+ ``@pigweed//third_party/stm32cube/cmsis_device.BUILD.bazel``. By default, we
+ assume this repository will be named ``@cmsis_device``, but this can be
+ overriden with a label flag (discussed below).
+* ``cmsis_core``, at https://github.com/STMicroelectronics/cmsis_core. We
+ provide a Bazel build file for it at
+ ``@pigweed//third_party/stm32cube/cmsis_core.BUILD.bazel``. By
+ default, we assume this repository will be named ``@cmsis_core``, but this
+ can be overriden with a label flag (discussed below).
+
+.. _module-pw_stm32cube_build-bazel-multifamily:
+
+Building for more than one MCU family
+-------------------------------------
+Different MCU families require different HAL driver and CMSIS device packages
+from STM. So, if your project builds firmware for more than one MCU family, you
+will need to configure separate sets of the three [#]_ STM repositories for each MCU
+family. To do so,
+
+1. Add the appropriate repositories to your WORKSPACE under different names,
+ eg. ``@stm32f4xx_hal_driver`` and ``@stm32h7xx_hal_driver``.
+2. Set the corresponding :ref:`module-pw_stm32cube_build-bazel-label-flags` as
+ part of the platform configuration for your embedded target platforms.
+ Currently, the best way to do this is via a `bazelrc config
+ <https://bazel.build/run/bazelrc#config>`_, which would look like this:
+
+ .. code-block::
+
+ build:stm32f429i --platforms=//targets/stm32f429i_disc1_stm32cube:platform
+ build:stm32f429i --@pigweed//third_party/stm32cube:stm32_hal_driver=@stm32f4xx_hal_driver//:hal_driver
+ build:stm32f429i --@stm32f4xx_hal_driver//:cmsis_device=@cmsis_device_f4//:cmsis_device
+ build:stm32f429i --@stm32f4xx_hal_driver//:cmsis_init=@cmsis_device_f4//:default_cmsis_init
+
+ However, once `platform-based flags
+ <https://github.com/bazelbuild/proposals/blob/main/designs/2023-06-08-platform-based-flags.md>`_
+ are implemented in Bazel, it will be possible to set these flags directly
+ in the platform definition.
+
+.. [#] Although CMSIS core is shared by all MCU families, different CMSIS
+ device repositories may not be compatible with the same version of CMSIS
+ core. In this case, you may need to use separate versions of CMSIS core,
+ too.
+
+Defines
+=======
+
+``STM32CUBE_HEADER``
+--------------------
+Upstream Pigweed modules that depend on the STM32Cube HAL, like
+:ref:`module-pw_sys_io_stm32cube`, include the HAL through the family-agnostic
+header ``stm32cube/stm32cube.h``. This header expects the family to be set
+through a define of ``STM32CUBE_HEADER``. So, to use these Pigweed modules, you
+need to set that define to the correct value (e.g., ``\"stm32f4xx.h\"``; note
+the backslashes) as part of your build. This is most conveniently done through
+``copts`` associated with the target platform.
+
+.. _module-pw_stm32cube_build-bazel-label-flags:
+
+Label flags
+===========
+Required
+--------
+``@hal_driver//:hal_config``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Points to the ``cc_library`` target providing a header with the HAL
+configuration. Note that this header needs an appropriate, family-specific name
+(e.g., ``stm32f4xx_hal_conf.h`` for the F4 family).
+
+Optional
+--------
+These label flags can be used to further customize the behavior of STM32Cube.
+
+``//third_party/stm32cube:stm32_hal_driver``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+This label_flag introduces a layer of indirection useful when building a
+project that requires more than one STM32Cube package (see
+:ref:`module-pw_stm32cube_build-bazel-multifamily`). It should point to the
+repository containing the HAL driver.
+
+The default value is ``@hal_driver``.
+
+``@cmsis_device//:cmsis_core``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+This label flag should point to the repository containing the CMSIS core build
+target.
+
+The default value is ``@cmsis_core``.
+
+``@hal_driver//:cmsis_device``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+This label flag should point to the repository containing the CMSIS device
+build target.
+
+The default value is ``@cmsis_device``.
+
+``@hal_driver//:cmsis_init``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+This label flag should point to the CMSIS initialization code. By default it
+points to the ``system_{family}.c`` template provided in the CMSIS device
+repository.
+
+``@hal_driver//:timebase``
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+This label flag should point to a ``cc_library`` providing a timebase
+implementation. By default it points to the template included with STM's HAL
+repository.
+
diff --git a/pw_stm32cube_build/py/BUILD.gn b/pw_stm32cube_build/py/BUILD.gn
index c69c513f8..863dfb77d 100644
--- a/pw_stm32cube_build/py/BUILD.gn
+++ b/pw_stm32cube_build/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_stm32cube_build/__init__.py",
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py b/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
index 549ca50a7..7e6930363 100644
--- a/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
@@ -239,7 +239,7 @@ def get_include_dirs(stm32cube_path: pathlib.Path) -> List[pathlib.Path]:
def get_sources_and_headers(
files: List[str], stm32cube_path: pathlib.Path
-) -> Tuple[List[str], List[str]]:
+) -> Tuple[List[pathlib.Path], List[pathlib.Path]]:
"""Gets list of all sources and headers needed to build the stm32cube hal.
Args:
@@ -265,7 +265,7 @@ def get_sources_and_headers(
files,
)
- rebase_path = lambda f: str(stm32cube_path / f)
+ rebase_path = lambda f: pathlib.Path(stm32cube_path / f)
return list(map(rebase_path, source_files)), list(
map(rebase_path, header_files)
)
@@ -304,13 +304,10 @@ def find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool):
(family, defines, name) = parse_product_str(product_str)
family_header_path = list(
- filter(lambda p: p.endswith(f'/{family}.h'), headers)
+ filter(lambda p: p.name == f'{family}.h', headers)
)[0]
- with open(family_header_path, 'rb') as family_header:
- family_header_str = family_header.read().decode(
- 'utf-8', errors='ignore'
- )
+ family_header_str = family_header_path.read_text('utf-8', errors='ignore')
define = select_define(defines, family_header_str)
diff --git a/pw_stm32cube_build/py/setup.py b/pw_stm32cube_build/py/setup.py
deleted file mode 100644
index 1e2ab2a48..000000000
--- a/pw_stm32cube_build/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_stm32cube_build"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_stm32cube_build/py/tests/find_files_test.py b/pw_stm32cube_build/py/tests/find_files_test.py
index 364e2b081..7ee6d5c65 100644
--- a/pw_stm32cube_build/py/tests/find_files_test.py
+++ b/pw_stm32cube_build/py/tests/find_files_test.py
@@ -319,8 +319,8 @@ class GetSourceAndHeadersTest(unittest.TestCase):
self.assertSetEqual(
set(
[
- str(path / 'hal_driver/Src/stm32f4xx_hal_adc.c'),
- str(path / 'hal_driver/Src/stm32f4xx_hal_eth.c'),
+ path / 'hal_driver/Src/stm32f4xx_hal_adc.c',
+ path / 'hal_driver/Src/stm32f4xx_hal_eth.c',
]
),
set(sources),
@@ -328,11 +328,11 @@ class GetSourceAndHeadersTest(unittest.TestCase):
self.assertSetEqual(
set(
[
- str(path / 'cmsis_core/Include/core_cm4.h'),
- str(path / 'cmsis_device/Include/stm32f4xx.h'),
- str(path / 'cmsis_device/Include/stm32f439xx.h'),
- str(path / 'hal_driver/Inc/stm32f4xx_hal_eth.h'),
- str(path / 'hal_driver/Inc/stm32f4xx_hal.h'),
+ path / 'cmsis_core/Include/core_cm4.h',
+ path / 'cmsis_device/Include/stm32f4xx.h',
+ path / 'cmsis_device/Include/stm32f439xx.h',
+ path / 'hal_driver/Inc/stm32f4xx_hal_eth.h',
+ path / 'hal_driver/Inc/stm32f4xx_hal.h',
]
),
set(headers),
diff --git a/pw_stream/Android.bp b/pw_stream/Android.bp
index 11f990a50..1026f7a0b 100644
--- a/pw_stream/Android.bp
+++ b/pw_stream/Android.bp
@@ -40,3 +40,31 @@ cc_library_static {
"pw_status",
],
}
+
+cc_library_static {
+ name: "pw_stream_std_file_stream",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ srcs: [
+ "std_file_stream.cc",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_status",
+ "pw_stream",
+ ],
+ export_static_lib_headers: [
+ "pw_stream",
+ ],
+}
diff --git a/pw_stream/BUILD.bazel b/pw_stream/BUILD.bazel
index 524e2082a..1d40bfd84 100644
--- a/pw_stream/BUILD.bazel
+++ b/pw_stream/BUILD.bazel
@@ -82,6 +82,24 @@ pw_cc_library(
deps = [":pw_stream"],
)
+pw_cc_library(
+ name = "mpsc_stream",
+ srcs = ["mpsc_stream.cc"],
+ hdrs = ["public/pw_stream/mpsc_stream.h"],
+ deps = [
+ ":pw_stream",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_chrono:system_clock_facade",
+ "//pw_containers:intrusive_list",
+ "//pw_function",
+ "//pw_status",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ "//pw_sync:timed_thread_notification",
+ ],
+)
+
pw_cc_test(
name = "memory_stream_test",
srcs = ["memory_stream_test.cc"],
@@ -153,3 +171,17 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "mpsc_stream_test",
+ srcs = ["mpsc_stream_test.cc"],
+ deps = [
+ ":mpsc_stream",
+ "//pw_containers:vector",
+ "//pw_fuzzer:fuzztest",
+ "//pw_random",
+ "//pw_thread:test_thread_context",
+ "//pw_thread:thread",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_stream/BUILD.gn b/pw_stream/BUILD.gn
index 9f4eb2f8f..a55fb91a0 100644
--- a/pw_stream/BUILD.gn
+++ b/pw_stream/BUILD.gn
@@ -15,7 +15,10 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_fuzzer/fuzzer.gni")
+import("$dir_pw_thread/backend.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_unit_test/test.gni")
@@ -53,6 +56,9 @@ pw_source_set("socket_stream") {
]
sources = [ "socket_stream.cc" ]
public = [ "public/pw_stream/socket_stream.h" ]
+ if (current_os == "win") {
+ libs = [ "ws2_32" ]
+ }
}
pw_source_set("sys_io_stream") {
@@ -83,6 +89,24 @@ pw_source_set("interval_reader") {
sources = [ "interval_reader.cc" ]
}
+pw_source_set("mpsc_stream") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":pw_stream",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_containers:intrusive_list",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_sync:timed_thread_notification",
+ dir_pw_assert,
+ dir_pw_bytes,
+ dir_pw_function,
+ dir_pw_status,
+ ]
+ public = [ "public/pw_stream/mpsc_stream.h" ]
+ sources = [ "mpsc_stream.cc" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
@@ -94,6 +118,7 @@ pw_test_group("tests") {
":null_stream_test",
":seek_test",
":stream_test",
+ ":mpsc_stream_test",
]
if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
@@ -150,3 +175,18 @@ pw_test("socket_stream_test") {
sources = [ "socket_stream_test.cc" ]
deps = [ ":socket_stream" ]
}
+
+pw_test("mpsc_stream_test") {
+ sources = [ "mpsc_stream_test.cc" ]
+ deps = [
+ ":mpsc_stream",
+ "$dir_pw_containers:vector",
+ "$dir_pw_fuzzer:fuzztest",
+ "$dir_pw_thread:test_thread_context",
+ "$dir_pw_thread:thread",
+ dir_pw_random,
+ ]
+ enable_if =
+ pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_THREAD_BACKEND != "" &&
+ pw_thread_TEST_THREAD_CONTEXT_BACKEND != ""
+}
diff --git a/pw_stream/CMakeLists.txt b/pw_stream/CMakeLists.txt
index dfa45823f..b04b440af 100644
--- a/pw_stream/CMakeLists.txt
+++ b/pw_stream/CMakeLists.txt
@@ -32,9 +32,6 @@ pw_add_library(pw_stream STATIC
pw_span
pw_status
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_STREAM)
- zephyr_link_libraries(pw_stream)
-endif()
pw_add_library(pw_stream.socket_stream STATIC
HEADERS
@@ -84,6 +81,26 @@ pw_add_library(pw_stream.interval_reader STATIC
interval_reader.cc
)
+pw_add_library(pw_stream.mpsc_stream STATIC
+ HEADERS
+ public/pw_stream/mpsc_stream.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
+ pw_chrono.system_clock
+ pw_containers.intrusive_list
+ pw_function
+ pw_status
+ pw_stream
+ pw_sync.lock_annotations
+ pw_sync.mutex
+ pw_sync.timed_thread_notification
+ SOURCES
+ mpsc_stream.cc
+)
+
pw_add_test(pw_stream.memory_stream_test
SOURCES
memory_stream_test.cc
@@ -133,3 +150,18 @@ pw_add_test(pw_stream.interval_reader_test
modules
pw_stream
)
+
+pw_add_test(pw_stream.mpsc_stream_test
+ SOURCES
+ mpsc_stream_test.cc
+ PRIVATE_DEPS
+ pw_stream.mpsc_stream
+ pw_containers.vector
+ pw_fuzzer.fuzztest
+ pw_random
+ pw_thread.thread
+ pw_thread.test_thread_context
+ GROUPS
+ modules
+ pw_stream
+)
diff --git a/pw_stream/Kconfig b/pw_stream/Kconfig
index a0d41dd45..c149d71c6 100644
--- a/pw_stream/Kconfig
+++ b/pw_stream/Kconfig
@@ -12,11 +12,17 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_stream"
+
config PIGWEED_STREAM
- bool "Enable Pigweed stream library (pw_stream)"
+ bool "Link pw_stream library"
select PIGWEED_ASSERT
select PIGWEED_BYTES
select PIGWEED_RESULT
select PIGWEED_SPAN
select PIGWEED_STATUS
select PIGWEED_SYS_IO
+ help
+ See :ref:`module-pw_stream` for module details.
+
+endmenu
diff --git a/pw_stream/docs.rst b/pw_stream/docs.rst
index a4aaeb913..a53f96d3d 100644
--- a/pw_stream/docs.rst
+++ b/pw_stream/docs.rst
@@ -5,7 +5,6 @@
=========
pw_stream
=========
-
``pw_stream`` provides a foundational interface for streaming data from one part
of a system to another. In the simplest use cases, this is basically a memcpy
behind a reusable interface that can be passed around the system. On the other
@@ -22,13 +21,13 @@ Example:
.. code-block:: cpp
- Status DumpSensorData(pw::stream::Writer& writer) {
- static char temp[64];
- ImuSample imu_sample;
- imu.GetSample(&info);
- size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
- return writer.Write(temp, bytes_written);
- }
+ Status DumpSensorData(pw::stream::Writer& writer) {
+ static char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ return writer.Write(temp, bytes_written);
+ }
In this example, ``DumpSensorData()`` only cares that it has access to a
:cpp:class:`Writer` that it can use to stream data to using ``Writer::Write()``.
@@ -40,9 +39,9 @@ pw::stream Interfaces
---------------------
There are three basic capabilities of a stream:
- * Reading -- Bytes can be read from the stream.
- * Writing -- Bytes can be written to the stream.
- * Seeking -- The position in the stream can be changed.
+* Reading -- Bytes can be read from the stream.
+* Writing -- Bytes can be written to the stream.
+* Seeking -- The position in the stream can be changed.
``pw_stream`` provides a family of stream classes with different capabilities.
The most basic class, :cpp:class:`Stream` guarantees no functionality, while the
@@ -51,36 +50,36 @@ and seeking.
Usage overview
==============
+.. list-table::
+ :header-rows: 1
+
+ * - pw::stream Interfaces
+ - Accept in APIs?
+ - Extend to create new stream?
+ * - :cpp:class:`pw::stream::Stream`
+ - ❌
+ - ❌
+ * - | :cpp:class:`pw::stream::Reader`
+ | :cpp:class:`pw::stream::Writer`
+ | :cpp:class:`pw::stream::ReaderWriter`
+ - ✅
+ - ❌
+ * - | :cpp:class:`pw::stream::SeekableReader`
+ | :cpp:class:`pw::stream::SeekableWriter`
+ | :cpp:class:`pw::stream::SeekableReaderWriter`
+ - ✅
+ - ✅
+ * - | :cpp:class:`pw::stream::RelativeSeekableReader`
+ | :cpp:class:`pw::stream::RelativeSeekableWriter`
+ | :cpp:class:`pw::stream::RelativeSeekableReaderWriter`
+ - ✅ (rarely)
+ - ✅
+ * - | :cpp:class:`pw::stream::NonSeekableReader`
+ | :cpp:class:`pw::stream::NonSeekableWriter`
+ | :cpp:class:`pw::stream::NonSeekableReaderWriter`
+ - ❌
+ - ✅
-+-------------------------------------------+-----------------+------------------------------+
-| pw::stream Interfaces | Accept in APIs? | Extend to create new stream? |
-+===========================================+=================+==============================+
-| :cpp:class:`Stream` | ❌ | ❌ |
-+-------------------------------------------+-----------------+------------------------------+
-| :cpp:class:`Reader` | ✅ | ❌ |
-| | | |
-| :cpp:class:`Writer` | | |
-| | | |
-| :cpp:class:`ReaderWriter` | | |
-+-------------------------------------------+-----------------+------------------------------+
-| :cpp:class:`SeekableReader` | ✅ | ✅ |
-| | | |
-| :cpp:class:`SeekableWriter` | | |
-| | | |
-| :cpp:class:`SeekableReaderWriter` | | |
-+-------------------------------------------+-----------------+------------------------------+
-| :cpp:class:`RelativeSeekableReader` | ✅ (rarely) | ✅ |
-| | | |
-| :cpp:class:`RelativeSeekableWriter` | | |
-| | | |
-| :cpp:class:`RelativeSeekableReaderWriter` | | |
-+-------------------------------------------+-----------------+------------------------------+
-| :cpp:class:`NonSeekableReader` | ❌ | ✅ |
-| | | |
-| :cpp:class:`NonSeekableWriter` | | |
-| | | |
-| :cpp:class:`NonSeekableReaderWriter` | | |
-+-------------------------------------------+-----------------+------------------------------+
Interface documentation
=======================
@@ -89,295 +88,52 @@ comments in `pw_stream/public/pw_stream/stream.h
<https://cs.pigweed.dev/pigweed/+/main:pw_stream/public/pw_stream/stream.h>`_
for full details.
-.. cpp:class:: Stream
-
- A generic stream that may support reading, writing, and seeking, but makes no
- guarantees about whether any operations are supported. Stream serves as the
- base for the Reader, Writer, and ReaderWriter interfaces.
-
- Stream cannot be extended directly. Instead, work with one of the derived
- classes that explicitly supports the required functionality. Stream should
- almost never be used in APIs; accept a derived class with the required
- capabilities instead.
-
- All Stream methods are blocking. They return when the requested operation
- completes.
-
- **Public methods**
-
- .. cpp:function:: bool readable() const
-
- True if :cpp:func:`Read` is supported.
-
- .. cpp:function:: bool writable() const
-
- True if :cpp:func:`Write` is supported.
-
- .. cpp:function:: bool seekable() const
-
- True if :cpp:func:`Seek` is supported.
-
- .. cpp:function:: Result<ByteSpan> Read(ByteSpan buffer)
- .. cpp:function:: Result<ByteSpan> Read(void* buffer, size_t size_bytes)
-
- Reads data from the stream into the provided buffer, if supported. As many
- bytes as are available up to the buffer size are copied into the buffer.
- Remaining bytes may by read in subsequent calls.
-
- Returns:
-
- * OK - Between 1 and dest.size_bytes() were successfully read. Returns
- the span of read bytes.
- * UNIMPLEMENTED - This stream does not support writing.
- * FAILED_PRECONDITION - The Reader is not in state to read data.
- * RESOURCE_EXHAUSTED - Unable to read any bytes at this time. No bytes
- read. Try again once bytes become available.
- * OUT_OF_RANGE - Reader has been exhausted, similar to EOF. No bytes were
- read, no more will be read.
-
- .. cpp:function:: Status Write(ConstByteSpan data)
-
- Writes the provided data to the stream, if supported.
-
- Returns:
-
- * OK - Data was successfully accepted by the stream.
- * UNIMPLEMENTED - This stream does not support writing.
- * FAILED_PRECONDITION - The writer is not in a state to accept data.
- * RESOURCE_EXHAUSTED - The writer was unable to write all of requested data
- at this time. No data was written.
- * OUT_OF_RANGE - The Writer has been exhausted, similar to EOF. No data was
- written; no more will be written.
-
-
- .. cpp:function:: Status Seek(ptrdiff_t offset, Whence origin = kBeginning)
-
- Changes the current read & write position in the stream, if supported.
-
- Returns:
-
- * OK - Successfully updated the position.
- * UNIMPLEMENTED - Seeking is not supported for this stream.
- * OUT_OF_RANGE - Attempted to seek beyond the bounds of the stream. The
- position is unchanged.
-
- .. cpp:function:: size_t Tell() const
-
- Returns the current read & write position in the stream, if supported.
- Returns ``Stream::kUnknownPosition`` (``size_t(-1)``) if unsupported.
-
- .. cpp:function:: size_t ConservativeReadLimit() const
-
- Likely minimum bytes available to read. Returns ``kUnlimited``
- (``size_t(-1)``) if there is no limit or it is unknown.
-
- .. cpp:function:: size_t ConservativeWriteLimit() const
-
- Likely minimum bytes available to write. Returns ``kUnlimited``
- (``size_t(-1)``) if there is no limit or it is unknown.
-
- **Private virtual methods**
-
- Stream's public methods are non-virtual. The public methods call private
- virtual methods that are implemented by derived classes.
-
- .. cpp:function:: private virtual StatusWithSize DoRead(ByteSpan destination)
-
- Virtual :cpp:func:`Read` function implemented by derived classes.
-
- .. cpp:function:: private virtual Status DoWrite(ConstByteSpan data)
-
- Virtual :cpp:func:`Write` function implemented by derived classes.
-
- .. cpp:function:: private virtual Status DoSeek(ptrdiff_t offset, Whence origin)
-
- Virtual :cpp:func:`Seek` function implemented by derived classes.
-
- .. cpp:function:: private virtual size_t DoTell() const
-
- Virtual :cpp:func:`Tell` function optionally implemented by derived classes.
- The default implementation always returns ``kUnknownPosition``.
-
- .. cpp:function:: private virtual size_t ConservativeLimit(LimitType limit_type)
-
- Virtual function optionally implemented by derived classes that is used for
- :cpp:func:`ConservativeReadLimit` and :cpp:func:`ConservativeWriteLimit`.
- The default implementation returns ``kUnlimited`` or ``0`` depending on
- whether the stream is readable/writable.
+.. doxygenclass:: pw::stream::Stream
+ :members:
+ :private-members:
Reader interfaces
-----------------
-.. cpp:class:: Reader : public Stream
-
- A Stream that supports reading but not writing. The Write() method is hidden.
-
- Use in APIs when:
- * Must read from, but not write to, a stream.
- * May or may not need seeking. Use a SeekableReader& if seeking is
- required.
-
- Inherit from when:
- * Reader cannot be extended directly. Instead, extend SeekableReader,
- NonSeekableReader, or (rarely) RelativeSeekableReader, as appropriate.
+.. doxygenclass:: pw::stream::Reader
+ :members:
- A Reader may or may not support seeking. Check seekable() or try calling
- Seek() to determine if the stream is seekable.
+.. doxygenclass:: pw::stream::SeekableReader
+ :members:
-.. cpp:class:: SeekableReader : public RelativeSeekableReader
+.. doxygenclass:: pw::stream::RelativeSeekableReader
+ :members:
- A Reader that fully supports seeking.
-
- Use in APIs when:
- * Absolute seeking is required. Use Reader& if seeking is not required or
- seek failures can be handled gracefully.
-
- Inherit from when:
- * Implementing a reader that supports absolute seeking.
-
-.. cpp:class:: RelativeSeekableReader : public Reader
-
- A Reader that at least partially supports seeking. Seeking within some range
- of the current position works, but seeking beyond that or from other origins
- may or may not be supported. The extent to which seeking is possible is NOT
- exposed by this API.
-
- Use in APIs when:
- * Relative seeking is required. Usage in APIs should be rare; generally
- Reader should be used instead.
-
- Inherit from when:
- * Implementing a Reader that can only support seeking near the current
- position.
-
- A buffered Reader that only supports seeking within its buffer is a good
- example of a RelativeSeekableReader.
-
-.. cpp:class:: NonSeekableReader : public Reader
-
- A Reader that does not support seeking. The Seek() method is hidden.
-
- Use in APIs when:
- * Do NOT use in APIs! If seeking is not required, use Reader& instead.
-
- Inherit from when:
- * Implementing a Reader that does not support seeking.
+.. doxygenclass:: pw::stream::NonSeekableReader
+ :members:
Writer interfaces
-----------------
-.. cpp:class:: Writer : public Stream
-
- A Stream that supports writing but not reading. The Read() method is hidden.
-
- Use in APIs when:
- * Must write to, but not read from, a stream.
- * May or may not need seeking. Use a SeekableWriter& if seeking is
- required.
-
- Inherit from when:
- * Writer cannot be extended directly. Instead, extend SeekableWriter,
- NonSeekableWriter, or (rarely) RelativeSeekableWriter, as appropriate.
-
- A Writer may or may not support seeking. Check seekable() or try calling
- Seek() to determine if the stream is seekable.
-
-.. cpp:class:: SeekableWriter : public RelativeSeekableWriter
-
- A Writer that fully supports seeking.
-
- Use in APIs when:
- * Absolute seeking is required. Use Writer& if seeking is not required or
- seek failures can be handled gracefully.
-
- Inherit from when:
- * Implementing a writer that supports absolute seeking.
-
-
-.. cpp:class:: RelativeSeekableWriter : public Writer
-
- A Writer that at least partially supports seeking. Seeking within some range
- of the current position works, but seeking beyond that or from other origins
- may or may not be supported. The extent to which seeking is possible is NOT
- exposed by this API.
+.. doxygenclass:: pw::stream::Writer
+ :members:
- Use in APIs when:
- * Relative seeking is required. Usage in APIs should be rare; generally
- Writer should be used instead.
+.. doxygenclass:: pw::stream::SeekableWriter
+ :members:
- Inherit from when:
- * Implementing a Writer that can only support seeking near the current
- position.
+.. doxygenclass:: pw::stream::RelativeSeekableWriter
+ :members:
- A buffered Writer that only supports seeking within its buffer is a good
- example of a RelativeSeekableWriter.
+.. doxygenclass:: pw::stream::NonSeekableWriter
+ :members:
-.. cpp:class:: NonSeekableWriter : public Writer
-
- A Writer that does not support seeking. The Seek() method is hidden.
-
- Use in APIs when:
- * Do NOT use in APIs! If seeking is not required, use Writer& instead.
-
- Inherit from when:
- * Implementing a Writer that does not support seeking.
ReaderWriter interfaces
-----------------------
-.. cpp:class:: ReaderWriter : public Stream
-
- A Stream that supports both reading and writing.
-
- Use in APIs when:
- * Must both read from and write to a stream.
- * May or may not need seeking. Use a SeekableReaderWriter& if seeking is
- required.
-
- Inherit from when:
- * Cannot extend ReaderWriter directly. Instead, extend
- SeekableReaderWriter, NonSeekableReaderWriter, or (rarely)
- RelativeSeekableReaderWriter, as appropriate.
-
- A ReaderWriter may or may not support seeking. Check seekable() or try
- calling Seek() to determine if the stream is seekable.
-
-.. cpp:class:: SeekableReaderWriter : public RelativeSeekableReaderWriter
-
- A ReaderWriter that fully supports seeking.
-
- Use in APIs when:
- * Absolute seeking is required. Use ReaderWriter& if seeking is not
- required or seek failures can be handled gracefully.
-
- Inherit from when:
- * Implementing a writer that supports absolute seeking.
+.. doxygenclass:: pw::stream::ReaderWriter
+ :members:
-.. cpp:class:: RelativeSeekableReaderWriter : public ReaderWriter
+.. doxygenclass:: pw::stream::SeekableReaderWriter
+ :members:
- A ReaderWriter that at least partially supports seeking. Seeking within some
- range of the current position works, but seeking beyond that or from other
- origins may or may not be supported. The extent to which seeking is possible
- is NOT exposed by this API.
+.. doxygenclass:: pw::stream::RelativeSeekableReaderWriter
+ :members:
- Use in APIs when:
- * Relative seeking is required. Usage in APIs should be rare; generally
- ReaderWriter should be used instead.
-
- Inherit from when:
- * Implementing a ReaderWriter that can only support seeking near the
- current position.
-
- A buffered ReaderWriter that only supports seeking within its buffer is a
- good example of a RelativeSeekableReaderWriter.
-
-.. cpp:class:: NonSeekableReaderWriter : public ReaderWriter
-
- A ReaderWriter that does not support seeking. The Seek() method is hidden.
-
- Use in APIs when:
- * Do NOT use in APIs! If seeking is not required, use ReaderWriter&
- instead.
-
- Inherit from when:
- * Implementing a ReaderWriter that does not support seeking.
+.. doxygenclass:: pw::stream::NonSeekableReaderWriter
+ :members:
---------------
Implementations
@@ -460,27 +216,27 @@ Before:
.. code-block:: cpp
- // Not reusable, depends on `Uart`.
- void DumpSensorData(Uart& uart) {
- static char temp[64];
- ImuSample imu_sample;
- imu.GetSample(&info);
- size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
- uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200);
- }
+ // Not reusable, depends on `Uart`.
+ void DumpSensorData(Uart& uart) {
+ static char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200);
+ }
After:
.. code-block:: cpp
- // Reusable; no more Uart dependency!
- Status DumpSensorData(Writer& writer) {
- static char temp[64];
- ImuSample imu_sample;
- imu.GetSample(&info);
- size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
- return writer.Write(temp, bytes_written);
- }
+ // Reusable; no more Uart dependency!
+ Status DumpSensorData(Writer& writer) {
+ static char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ return writer.Write(temp, bytes_written);
+ }
Reduce intermediate buffers
===========================
@@ -497,26 +253,26 @@ Before:
.. code-block:: cpp
- // Requires an intermediate buffer to write the data as CSV.
- void DumpSensorData(Uart& uart) {
- char temp[64];
- ImuSample imu_sample;
- imu.GetSample(&info);
- size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
- uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200);
- }
+ // Requires an intermediate buffer to write the data as CSV.
+ void DumpSensorData(Uart& uart) {
+ char temp[64];
+ ImuSample imu_sample;
+ imu.GetSample(&info);
+ size_t bytes_written = imu_sample.AsCsv(temp, sizeof(temp));
+ uart.Transmit(temp, bytes_written, /*timeout_ms=*/ 200);
+ }
After:
.. code-block:: cpp
- // Both DumpSensorData() and RawSample::AsCsv() use a Writer, eliminating the
- // need for an intermediate buffer.
- Status DumpSensorData(Writer& writer) {
- RawSample imu_sample;
- imu.GetSample(&info);
- return imu_sample.AsCsv(writer);
- }
+ // Both DumpSensorData() and RawSample::AsCsv() use a Writer, eliminating the
+ // need for an intermediate buffer.
+ Status DumpSensorData(Writer& writer) {
+ RawSample imu_sample;
+ imu.GetSample(&info);
+ return imu_sample.AsCsv(writer);
+ }
Prevent buffer overflow
=======================
@@ -534,37 +290,37 @@ Before:
.. code-block:: cpp
- Status BuildPacket(Id dest, span<const std::byte> payload,
- span<std::byte> dest) {
- Header header;
- if (dest.size_bytes() + payload.size_bytes() < sizeof(Header)) {
- return Status::ResourceExhausted();
- }
- header.dest = dest;
- header.src = DeviceId();
- header.payload_size = payload.size_bytes();
-
- memcpy(dest.data(), &header, sizeof(header));
- // Forgetting this line would clobber buffer contents. Also, using
- // a temporary span instead could leave `dest` to be misused elsewhere in
- // the function.
- dest = dest.subspan(sizeof(header));
- memcpy(dest.data(), payload.data(), payload.size_bytes());
- }
+ Status BuildPacket(Id dest, span<const std::byte> payload,
+ span<std::byte> dest) {
+ Header header;
+ if (dest.size_bytes() + payload.size_bytes() < sizeof(Header)) {
+ return Status::ResourceExhausted();
+ }
+ header.dest = dest;
+ header.src = DeviceId();
+ header.payload_size = payload.size_bytes();
+
+ memcpy(dest.data(), &header, sizeof(header));
+ // Forgetting this line would clobber buffer contents. Also, using
+ // a temporary span instead could leave `dest` to be misused elsewhere in
+ // the function.
+ dest = dest.subspan(sizeof(header));
+ memcpy(dest.data(), payload.data(), payload.size_bytes());
+ }
After:
.. code-block:: cpp
- Status BuildPacket(Id dest, span<const std::byte> payload, Writer& writer) {
- Header header;
- header.dest = dest;
- header.src = DeviceId();
- header.payload_size = payload.size_bytes();
+ Status BuildPacket(Id dest, span<const std::byte> payload, Writer& writer) {
+ Header header;
+ header.dest = dest;
+ header.src = DeviceId();
+ header.payload_size = payload.size_bytes();
- writer.Write(header);
- return writer.Write(payload);
- }
+ writer.Write(header);
+ return writer.Write(payload);
+ }
------------
Design notes
@@ -582,11 +338,11 @@ any buffered data to the sink.
``Flush()`` and ``Sync()`` were excluded from :cpp:class:`Stream` for a few
reasons:
- * The semantics of when to call ``Flush()``/``Sync()`` on the stream are
- unclear. The presence of these methods complicates using a
- :cpp:class:`Reader` or :cpp:class:`Writer`.
- * Adding one or two additional virtual calls increases the size of all
- :cpp:class:`Stream` vtables.
+* The semantics of when to call ``Flush()``/``Sync()`` on the stream are
+ unclear. The presence of these methods complicates using a :cpp:class:`Reader`
+ or :cpp:class:`Writer`.
+* Adding one or two additional virtual calls increases the size of all
+ :cpp:class:`Stream` vtables.
Class hierarchy
===============
@@ -598,7 +354,7 @@ some similarities with Python's `io module
An alternative approach is to have the reading, writing, and seeking portions of
the interface provided by different entities. This is how Go's `io
-<https://pkg.go.dev/io package>`_ and C++'s `input/output library
+package <https://pkg.go.dev/io>`_ and C++'s `input/output library
<https://en.cppreference.com/w/cpp/io>`_ are structured.
We chose to use a single base class for a few reasons:
@@ -654,17 +410,25 @@ Pigweed has not yet established a pattern for asynchronous C++ APIs. The
:cpp:class:`Stream` class may be extended in the future to add asynchronous
capabilities, or a separate ``AsyncStream`` could be created.
+.. cpp:namespace-pop::
+
------------
Dependencies
------------
- * :ref:`module-pw_assert`
- * :ref:`module-pw_preprocessor`
- * :ref:`module-pw_status`
- * :ref:`module-pw_span`
-
-.. cpp:namespace-pop::
+* :ref:`module-pw_assert`
+* :ref:`module-pw_preprocessor`
+* :ref:`module-pw_status`
+* :ref:`module-pw_span`
+------
Zephyr
-======
+------
To enable ``pw_stream`` for Zephyr add ``CONFIG_PIGWEED_STREAM=y`` to the
project's configuration.
+
+----
+Rust
+----
+Pigweed centric analogs to Rust ``std``'s ``Read``, ``Write``, ``Seek`` traits
+as well as a basic ``Cursor`` implementation are provided by the
+`pw_stream crate </rustdoc/pw_stream>`_.
diff --git a/pw_stream/mpsc_stream.cc b/pw_stream/mpsc_stream.cc
new file mode 100644
index 000000000..50a22a81d
--- /dev/null
+++ b/pw_stream/mpsc_stream.cc
@@ -0,0 +1,523 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream/mpsc_stream.h"
+
+#include <cstring>
+
+#include "pw_assert/check.h"
+
+namespace pw::stream {
+namespace {
+
+// Wait to receive a thread notification with an optional timeout.
+bool Await(sync::TimedThreadNotification& notification,
+ const std::optional<chrono::SystemClock::duration>& timeout) {
+ if (timeout.has_value()) {
+ return notification.try_acquire_for(*timeout);
+ }
+ // Block indefinitely.
+ notification.acquire();
+ return true;
+}
+
+} // namespace
+
+void CreateMpscStream(MpscReader& reader, MpscWriter& writer) {
+ reader.Close();
+ std::lock_guard rlock(reader.mutex_);
+ PW_CHECK(reader.writers_.empty());
+ std::lock_guard wlock(writer.mutex_);
+ writer.CloseLocked();
+ reader.writers_.push_front(writer);
+ reader.IncreaseLimitLocked(Stream::kUnlimited);
+ writer.reader_ = &reader;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// MpscWriter methods.
+
+MpscWriter::MpscWriter(const MpscWriter& other) { *this = other; }
+
+MpscWriter& MpscWriter::operator=(const MpscWriter& other) {
+ Close();
+
+ // Read the other object's internal state. Avoid holding both locks at once.
+ other.mutex_.lock();
+ MpscReader* reader = other.reader_;
+ duration timeout = other.timeout_;
+ size_t limit = other.limit_;
+ size_t last_write = other.last_write_;
+ other.mutex_.unlock();
+
+ // Now update this object with the other's state.
+ mutex_.lock();
+ reader_ = reader;
+ timeout_ = timeout;
+ limit_ = limit;
+ last_write_ = last_write;
+ mutex_.unlock();
+
+ // Add the writer to the reader outside the lock. If the reader was closed
+ // concurrently, this will close the writer.
+ if (reader != nullptr) {
+ std::lock_guard lock(reader->mutex_);
+ reader->writers_.push_front(*this);
+ reader->IncreaseLimitLocked(limit);
+ }
+ return *this;
+}
+
+MpscWriter::MpscWriter(MpscWriter&& other) : MpscWriter() {
+ *this = std::move(other);
+}
+
+MpscWriter& MpscWriter::operator=(MpscWriter&& other) {
+ *this = other;
+ other.Close();
+ return *this;
+}
+
+MpscWriter::~MpscWriter() { Close(); }
+
+bool MpscWriter::connected() const {
+ std::lock_guard lock(mutex_);
+ return reader_ != nullptr;
+}
+
+size_t MpscWriter::last_write() const {
+ std::lock_guard lock(mutex_);
+ return last_write_;
+}
+
+void MpscWriter::SetTimeout(const duration& timeout) {
+ std::lock_guard lock(mutex_);
+ timeout_ = timeout;
+}
+
+void MpscWriter::SetLimit(size_t limit) {
+ std::lock_guard lock(mutex_);
+ if (reader_) {
+ reader_->DecreaseLimit(limit_);
+ reader_->IncreaseLimit(limit);
+ }
+ limit_ = limit;
+ if (limit_ == 0) {
+ CloseLocked();
+ }
+}
+
+size_t MpscWriter::ConservativeLimit(LimitType type) const {
+ std::lock_guard lock(mutex_);
+ return reader_ != nullptr && type == LimitType::kWrite ? limit_ : 0;
+}
+
+Status MpscWriter::DoWrite(ConstByteSpan data) {
+ // Check some conditions to see if an early exit is possible.
+ if (data.empty()) {
+ return OkStatus();
+ }
+ std::lock_guard lock(mutex_);
+ if (reader_ == nullptr) {
+ return Status::OutOfRange();
+ }
+ if (limit_ < data.size()) {
+ return Status::ResourceExhausted();
+ }
+ if (!write_request_.unlisted()) {
+ return Status::FailedPrecondition();
+ }
+ // Subscribe to the reader. This will enqueue this object's write request,
+ // which will be used to notify the writer when the reader has space available
+ // or has closed.
+ reader_->RequestWrite(write_request_);
+ last_write_ = 0;
+
+ Status status;
+ while (!data.empty()) {
+ // Wait to be notified by the reader.
+ // Note: This manually unlocks and relocks the mutex currently held by the
+ // lock guard. It must not return while the mutex is not locked.
+ duration timeout = timeout_;
+ mutex_.unlock();
+ bool writeable = Await(write_request_.notification, timeout);
+ mutex_.lock();
+
+ // Conditions may have changed while waiting; check again.
+ if (reader_ == nullptr) {
+ return Status::OutOfRange();
+ }
+ if (!writeable || limit_ < data.size()) {
+ status = Status::ResourceExhausted();
+ break;
+ }
+
+ // Attempt to write data.
+ StatusWithSize result = reader_->WriteData(data, limit_);
+ last_write_ += result.size();
+ if (limit_ != kUnlimited) {
+ limit_ -= result.size();
+ }
+
+ // WriteData() only returns an error if the reader is closed. In that case,
+ // or if the writer has written all of its data, the writer should close.
+ if (!result.ok() || limit_ == 0) {
+ CloseLocked();
+ return result.status();
+ }
+ data = data.subspan(result.size());
+ }
+
+ // Unsubscribe from the reader.
+ reader_->CompleteWrite(write_request_);
+ return status;
+}
+
+void MpscWriter::Close() {
+ std::lock_guard lock(mutex_);
+ CloseLocked();
+}
+
+void MpscWriter::CloseLocked() {
+ if (reader_ != nullptr) {
+ std::lock_guard lock(reader_->mutex_);
+ reader_->CompleteWriteLocked(write_request_);
+ write_request_.notification.release();
+ if (reader_->writers_.remove(*this)) {
+ reader_->DecreaseLimitLocked(limit_);
+ }
+ if (reader_->writers_.empty()) {
+ reader_->readable_.release();
+ }
+ reader_ = nullptr;
+ }
+ limit_ = kUnlimited;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// MpscReader methods.
+
+MpscReader::MpscReader() { last_request_ = write_requests_.begin(); }
+
+MpscReader::~MpscReader() { Close(); }
+
+bool MpscReader::connected() const {
+ std::lock_guard lock(mutex_);
+ return !writers_.empty();
+}
+
+void MpscReader::SetBuffer(ByteSpan buffer) {
+ std::lock_guard lock(mutex_);
+ PW_CHECK(length_ == 0);
+ buffer_ = buffer;
+ offset_ = 0;
+}
+
+void MpscReader::SetTimeout(const duration& timeout) {
+ std::lock_guard lock(mutex_);
+ timeout_ = timeout;
+}
+
+void MpscReader::IncreaseLimit(size_t delta) {
+ std::lock_guard lock(mutex_);
+ IncreaseLimitLocked(delta);
+}
+
+void MpscReader::IncreaseLimitLocked(size_t delta) {
+ if (delta == kUnlimited) {
+ ++num_unlimited_;
+ PW_CHECK_UINT_NE(num_unlimited_, 0);
+ } else if (limit_ != kUnlimited) {
+ PW_CHECK_UINT_LT(limit_, kUnlimited - delta);
+ limit_ += delta;
+ }
+}
+
+void MpscReader::DecreaseLimit(size_t delta) {
+ std::lock_guard lock(mutex_);
+ DecreaseLimitLocked(delta);
+}
+
+void MpscReader::DecreaseLimitLocked(size_t delta) {
+ if (delta == kUnlimited) {
+ PW_CHECK_UINT_NE(num_unlimited_, 0);
+ --num_unlimited_;
+ } else if (limit_ != kUnlimited) {
+ PW_CHECK_UINT_LE(delta, limit_);
+ limit_ -= delta;
+ }
+}
+
+size_t MpscReader::ConservativeLimit(LimitType type) const {
+ std::lock_guard lock(mutex_);
+ if (type != LimitType::kRead) {
+ return 0;
+ }
+ if (writers_.empty()) {
+ return length_;
+ }
+ if (num_unlimited_ != 0) {
+ return kUnlimited;
+ }
+ return limit_;
+}
+
+void MpscReader::RequestWrite(MpscWriter::Request& write_request) {
+ std::lock_guard lock(mutex_);
+ last_request_ = write_requests_.insert_after(last_request_, write_request);
+ CheckWriteableLocked();
+}
+
+void MpscReader::CheckWriteableLocked() {
+ if (write_requests_.empty()) {
+ return;
+ }
+ if (writers_.empty() || written_ < destination_.size() ||
+ length_ < buffer_.size()) {
+ MpscWriter::Request& write_request = write_requests_.front();
+ write_request.notification.release();
+ }
+}
+
+StatusWithSize MpscReader::WriteData(ConstByteSpan data, size_t limit) {
+ std::lock_guard lock(mutex_);
+ if (writers_.empty()) {
+ return StatusWithSize::OutOfRange(0);
+ }
+ size_t length = 0;
+ size_t available = buffer_.size() - length_;
+ if (written_ < destination_.size()) {
+ // A read is pending; copy directly into its buffer.
+ // Note: this condition is only true when the buffer is empty, so data
+ // order is preserved.
+ length = std::min(destination_.size() - written_, data.size());
+ memcpy(&destination_[written_], &data[0], length);
+ written_ += length;
+ } else if (available > 0) {
+ // The buffer has space for more data.
+ length = std::min(available, data.size());
+ size_t offset = (offset_ + length_) % buffer_.size();
+ size_t contiguous = buffer_.size() - offset;
+ if (length <= contiguous) {
+ memcpy(&buffer_[offset], &data[0], length);
+ } else {
+ memcpy(&buffer_[offset], &data[0], contiguous);
+ memcpy(&buffer_[0], &data[contiguous], length - contiguous);
+ }
+ length_ += length;
+ } else {
+ // If there is no space available, a write request can only be notified when
+ // its writer is closing. Do not notify the reader that data is available.
+ return StatusWithSize(0);
+ }
+ data = data.subspan(length);
+ // For unlimited writers, increase the read limit as needed.
+ // Do this before waking the reader and releasing the lock.
+ if (limit == kUnlimited) {
+ IncreaseLimitLocked(length);
+ }
+ readable_.release();
+ return StatusWithSize(length);
+}
+
+void MpscReader::CompleteWrite(MpscWriter::Request& write_request) {
+ std::lock_guard lock(mutex_);
+ CompleteWriteLocked(write_request);
+}
+
+void MpscReader::CompleteWriteLocked(MpscWriter::Request& write_request) {
+ MpscWriter::Request& last_request = *last_request_;
+ write_requests_.remove(write_request);
+
+ // If the last request is removed, find the new last request. This is O(n),
+ // but the oremoved element is first unless a request is being canceled due to
+ // its writer closing. Thus in the typical case of a successful write, this is
+ // O(1).
+ if (&last_request == &write_request) {
+ last_request_ = write_requests_.begin();
+ for (size_t i = 1; i < write_requests_.size(); ++i) {
+ ++last_request_;
+ }
+ }
+
+ // The reader may have signaled this writer that it had space between the last
+ // call to WriteData() and this call. Check if that signal should be forwarded
+ // to the next write request.
+ CheckWriteableLocked();
+}
+
+StatusWithSize MpscReader::DoRead(ByteSpan destination) {
+ if (destination.empty()) {
+ return StatusWithSize(0);
+ }
+ mutex_.lock();
+ PW_CHECK(!reading_, "All reads must happen from the same thread.");
+ reading_ = true;
+ Status status = OkStatus();
+ size_t length = 0;
+
+ // Check for buffered data. Do this before checking if the reader is still
+ // connected in order to deliver data sent from a now-closed writer.
+ if (length_ != 0) {
+ length = std::min(length_, destination.size());
+ size_t contiguous = buffer_.size() - offset_;
+ if (length < contiguous) {
+ memcpy(&destination[0], &buffer_[offset_], length);
+ offset_ += length;
+ } else if (length == contiguous) {
+ memcpy(&destination[0], &buffer_[offset_], length);
+ offset_ = 0;
+ } else {
+ memcpy(&destination[0], &buffer_[offset_], contiguous);
+ offset_ = length - contiguous;
+ memcpy(&destination[contiguous], &buffer_[0], offset_);
+ }
+ length_ -= length;
+ DecreaseLimitLocked(length);
+ CheckWriteableLocked();
+
+ } else {
+ // Register the output buffer to and wait for Write() to bypass the buffer
+ // and write directly into it. Note that the buffer is only bypassed when
+ // empty, so data order is preserved.
+ PW_CHECK(written_ == 0);
+ destination_ = destination;
+ CheckWriteableLocked();
+
+ // The reader state may change while waiting, or even between acquiring the
+ // notification and acquiring the lock. As an example, the following
+ // sequence of events is possible:
+ //
+ // 1. A writer partially fills the output buffer and releases the
+ // notification.
+ // 2. The reader acquires the notification.
+ // 3. Another writer fills the remainder of the buffer and releass the
+ // notification *again*.
+ // 4. The reader acquires the lock.
+ //
+ // In this case, on the *next* read, the notification will be acquired
+ // immediately even if no data is available. As a result, this code loops
+ // until data is available.
+ while (status.ok()) {
+ bool readable = true;
+ if (!writers_.empty()) {
+ // Wait for a writer to provide data, or the reader to be closed.
+ duration timeout = timeout_;
+ mutex_.unlock();
+ readable = Await(readable_, timeout);
+ mutex_.lock();
+ }
+ if (!readable) {
+ status = Status::ResourceExhausted();
+ } else if (written_ != 0) {
+ break;
+ } else if (writers_.empty()) {
+ status = Status::OutOfRange();
+ }
+ }
+ destination_ = ByteSpan();
+ length = written_;
+ written_ = 0;
+ DecreaseLimitLocked(length);
+ CheckWriteableLocked();
+ }
+
+ reading_ = false;
+ if (writers_.empty()) {
+ closeable_.release();
+ }
+ mutex_.unlock();
+ return StatusWithSize(status, length);
+}
+
+Status MpscReader::ReadAll(ReadAllCallback callback) {
+ mutex_.lock();
+ if (buffer_.empty()) {
+ mutex_.unlock();
+ return Status::FailedPrecondition();
+ }
+ PW_CHECK(!reading_, "All reads must happen from the same thread.");
+ reading_ = true;
+
+ Status status = Status::OutOfRange();
+ while (true) {
+ // Check for buffered data. Do this before checking if the reader still has
+ // writers in order to deliver data sent from a now-closed writer.
+ if (length_ != 0) {
+ size_t length = std::min(buffer_.size() - offset_, length_);
+ ConstByteSpan data(&buffer_[offset_], length);
+ offset_ = (offset_ + length_) % buffer_.size();
+ length_ -= length;
+ DecreaseLimitLocked(data.size());
+ CheckWriteableLocked();
+ status = callback(data);
+ if (!status.ok()) {
+ break;
+ }
+ }
+ if (writers_.empty()) {
+ break;
+ }
+ // Wait for a writer to provide data.
+ duration timeout = timeout_;
+ mutex_.unlock();
+ bool readable = Await(readable_, timeout);
+ mutex_.lock();
+ if (!readable) {
+ status = Status::ResourceExhausted();
+ break;
+ }
+ }
+ reading_ = false;
+ if (writers_.empty()) {
+ closeable_.release();
+ }
+ mutex_.unlock();
+ return status;
+}
+
+void MpscReader::Close() {
+ mutex_.lock();
+ if (writers_.empty()) {
+ mutex_.unlock();
+ return;
+ }
+ IntrusiveList<MpscWriter> writers;
+ while (!writers_.empty()) {
+ MpscWriter& writer = writers_.front();
+ writers_.pop_front();
+ writers.push_front(writer);
+ }
+
+ // Wait for any pending read to finish.
+ if (reading_) {
+ mutex_.unlock();
+ readable_.release();
+ closeable_.acquire();
+ mutex_.lock();
+ }
+
+ num_unlimited_ = 0;
+ limit_ = 0;
+ written_ = 0;
+ offset_ = 0;
+ length_ = 0;
+ mutex_.unlock();
+
+ for (auto& writer : writers) {
+ writer.Close();
+ }
+}
+
+} // namespace pw::stream
diff --git a/pw_stream/mpsc_stream_test.cc b/pw_stream/mpsc_stream_test.cc
new file mode 100644
index 000000000..1a08bed2f
--- /dev/null
+++ b/pw_stream/mpsc_stream_test.cc
@@ -0,0 +1,601 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream/mpsc_stream.h"
+
+#include "gtest/gtest.h"
+#include "pw_containers/vector.h"
+#include "pw_fuzzer/fuzztest.h"
+#include "pw_random/xor_shift.h"
+#include "pw_thread/test_thread_context.h"
+#include "pw_thread/thread.h"
+
+namespace pw::stream {
+namespace {
+
+using namespace std::chrono_literals;
+using namespace pw::fuzzer;
+
+////////////////////////////////////////////////////////////////////////////////
+// Test fixtures.
+
+/// Capacity in bytes for data buffers.
+constexpr size_t kBufSize = 512;
+
+/// Fills a byte span with random data.
+void Fill(std::byte* buf, size_t len) {
+ ByteSpan data(buf, len);
+ random::XorShiftStarRng64 rng(1);
+ rng.Get(data);
+}
+
+/// FNV-1a offset basis.
+constexpr uint64_t kOffsetBasis = 0xcbf29ce484222325ULL;
+
+/// FNV-1a prime value.
+constexpr uint64_t kPrimeValue = 0x100000001b3ULL;
+
+/// Quick implementation of public-domain Fowler-Noll-Vo hashing algorithm.
+///
+/// This is used in the tests below to verify equality of two sequences of bytes
+/// that are too large to compare directly.
+///
+/// See http://www.isthe.com/chongo/tech/comp/fnv/index.html
+void fnv1a(ConstByteSpan bytes, uint64_t& hash) {
+ for (const auto& b : bytes) {
+ hash = (hash ^ static_cast<uint8_t>(b)) * kPrimeValue;
+ }
+}
+
+/// MpscStream test context that uses a generic reader.
+///
+/// This struct associates a reader and writer with their parameters and return
+/// values. This is useful for communicating with threads spawned to call a
+/// blocking method.
+struct MpscTestContext {
+ MpscWriter writer;
+ MpscReader reader;
+
+ ConstByteSpan data;
+ std::byte write_buffer[kBufSize];
+ uint64_t write_hash = kOffsetBasis;
+ Status write_status;
+
+ ByteSpan destination;
+ std::byte read_buffer[kBufSize];
+ Result<ByteSpan> read_result;
+ uint64_t read_hash = kOffsetBasis;
+ size_t total_read = 0;
+
+ MpscTestContext() {
+ data = ConstByteSpan(write_buffer);
+ destination = ByteSpan(read_buffer);
+ }
+
+ void Connect() { CreateMpscStream(reader, writer); }
+
+ // Fills a byte span with random data.
+ void Fill() { pw::stream::Fill(write_buffer, sizeof(write_buffer)); }
+
+ // Writes data using the writer.
+ void Write() {
+ fnv1a(data, write_hash);
+ write_status = writer.Write(data);
+ }
+
+ // Writes data repeatedly up to the writer's limit.
+ void WriteAll() {
+ size_t limit = writer.ConservativeWriteLimit();
+ ASSERT_NE(limit, 0U);
+ ASSERT_NE(limit, Stream::kUnlimited);
+ while (limit != 0) {
+ if (limit < kBufSize) {
+ data = data.subspan(0, limit);
+ }
+ Fill();
+ Write();
+ if (!write_status.ok()) {
+ break;
+ }
+ limit = writer.ConservativeWriteLimit();
+ }
+ }
+
+ // Reads data using the reader.
+ void Read() {
+ read_result = reader.Read(destination);
+ if (read_result.ok()) {
+ fnv1a(*read_result, write_hash);
+ total_read += read_result->size();
+ }
+ }
+
+ // Run the given function on a dedicated thread.
+ using ThreadBody = Function<void(MpscTestContext* ctx)>;
+ void Spawn(ThreadBody func) {
+ body_ = std::move(func);
+ thread_ = thread::Thread(
+ context_.options(),
+ [](void* arg) {
+ auto* base = static_cast<MpscTestContext*>(arg);
+ base->body_(base);
+ },
+ this);
+ }
+
+ // Waits for the spawned thread to complete.
+ void Join() { thread_.join(); }
+
+ private:
+ thread::Thread thread_;
+ thread::test::TestThreadContext context_;
+ ThreadBody body_;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// Unit tests.
+
+TEST(MpscStreamTest, CopyWriters) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_TRUE(ctx.writer.connected());
+
+ MpscWriter writer2(ctx.writer);
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_TRUE(ctx.writer.connected());
+ EXPECT_TRUE(writer2.connected());
+
+ MpscWriter writer3 = writer2;
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_TRUE(ctx.writer.connected());
+ EXPECT_TRUE(writer2.connected());
+ EXPECT_TRUE(writer3.connected());
+
+ ctx.writer.Close();
+ writer2.Close();
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_FALSE(ctx.writer.connected());
+ EXPECT_FALSE(writer2.connected());
+ EXPECT_TRUE(writer3.connected());
+}
+
+TEST(MpscStreamTest, MoveWriters) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_TRUE(ctx.writer.connected());
+
+ MpscWriter writer2(std::move(ctx.writer));
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_TRUE(writer2.connected());
+
+ MpscWriter writer3 = std::move(writer2);
+ EXPECT_TRUE(ctx.reader.connected());
+ EXPECT_TRUE(writer3.connected());
+
+ // Only writer3 should be connected.
+ writer3.Close();
+ EXPECT_FALSE(writer3.connected());
+ EXPECT_FALSE(ctx.reader.connected());
+}
+
+TEST(MpscStreamTest, ReadFailsIfDisconnected) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ ctx.writer.Close();
+ ctx.Read();
+ EXPECT_EQ(ctx.read_result.status(), Status::OutOfRange());
+}
+
+TEST(MpscStreamTest, ReadBlocksWhenEmpty) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.reader.SetTimeout(10ms);
+
+ auto start = chrono::SystemClock::now();
+ ctx.Read();
+ auto elapsed = chrono::SystemClock::now() - start;
+
+ EXPECT_EQ(ctx.read_result.status(), Status::ResourceExhausted());
+ EXPECT_GE(elapsed, 10ms);
+}
+
+TEST(MpscStreamTest, ReadReturnsAfterReaderClose) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ ctx.Spawn([](MpscTestContext* inner) { inner->Read(); });
+ ctx.reader.Close();
+ ctx.Join();
+
+ EXPECT_EQ(ctx.read_result.status(), Status::OutOfRange());
+}
+
+TEST(MpscStreamTest, WriteBlocksUntilTimeout) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.writer.SetTimeout(10ms);
+ ctx.Fill();
+
+ auto start = chrono::SystemClock::now();
+ ctx.Write();
+ auto elapsed = chrono::SystemClock::now() - start;
+
+ EXPECT_EQ(ctx.write_status, Status::ResourceExhausted());
+ EXPECT_GE(elapsed, 10ms);
+}
+
+TEST(MpscStreamTest, WriteReturnsAfterClose) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ ctx.Fill();
+ ctx.Spawn([](MpscTestContext* inner) { inner->Write(); });
+ ctx.reader.Close();
+ ctx.Join();
+
+ EXPECT_EQ(ctx.write_status, Status::OutOfRange());
+}
+
+void VerifyRoundtripImpl(const Vector<std::byte>& data, ByteSpan buffer) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ ctx.reader.SetBuffer(buffer);
+ ctx.data = ConstByteSpan(data.data(), data.size());
+ ctx.Spawn([](MpscTestContext* inner) { inner->Write(); });
+ size_t offset = 0;
+ while (offset < data.size()) {
+ ctx.Read();
+ ASSERT_EQ(ctx.read_result.status(), OkStatus());
+ size_t num_read = ctx.read_result->size();
+ EXPECT_EQ(memcmp(ctx.read_buffer, &data[offset], num_read), 0);
+ offset += num_read;
+ }
+ ctx.Join();
+}
+
+template <size_t kCapacity>
+void FillAndVerifyRoundtripImpl(ByteSpan buffer) {
+ Vector<std::byte, kCapacity> data;
+ Fill(data.data(), data.size());
+ VerifyRoundtripImpl(data, buffer);
+}
+
+TEST(MpscStreamTest, VerifyRoundtripWithoutBufferSmall) {
+ FillAndVerifyRoundtripImpl<kBufSize / 2>(ByteSpan());
+}
+
+TEST(MpscStreamTest, VerifyRoundtripWithoutBufferLarge) {
+ FillAndVerifyRoundtripImpl<kBufSize * 2>(ByteSpan());
+}
+
+void VerifyRoundtripWithoutBuffer(const Vector<std::byte>& data) {
+ VerifyRoundtripImpl(data, ByteSpan());
+}
+FUZZ_TEST(MpscStreamTest, VerifyRoundtripWithoutBuffer)
+ .WithDomains(VectorOf<kBufSize * 2>(Arbitrary<std::byte>()).WithMinSize(1));
+
+TEST(MpscStreamTest, VerifyRoundtripWithBufferSmall) {
+ std::byte buffer[kBufSize];
+ FillAndVerifyRoundtripImpl<kBufSize / 2>(buffer);
+}
+
+TEST(MpscStreamTest, VerifyRoundtripWithBufferLarge) {
+ std::byte buffer[kBufSize];
+ FillAndVerifyRoundtripImpl<kBufSize * 2>(buffer);
+}
+
+void VerifyRoundtripWithBuffer(const Vector<std::byte>& data) {
+ std::byte buffer[kBufSize];
+ VerifyRoundtripImpl(data, buffer);
+}
+FUZZ_TEST(MpscStreamTest, VerifyRoundtripWithBuffer)
+ .WithDomains(VectorOf<kBufSize * 2>(Arbitrary<std::byte>()).WithMinSize(1));
+
+TEST(MpscStreamTest, CanRetryAfterPartialWrite) {
+ constexpr size_t kChunk = kBufSize - 4;
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.writer.SetTimeout(10ms);
+ ByteSpan destination = ctx.destination;
+
+ ctx.Spawn([](MpscTestContext* inner) {
+ inner->Fill();
+ inner->Write();
+ });
+ ctx.destination = destination.subspan(0, kChunk);
+ ctx.Read();
+ ctx.Join();
+ EXPECT_EQ(ctx.read_result.status(), OkStatus());
+ EXPECT_EQ(ctx.read_result->size(), kChunk);
+ EXPECT_EQ(ctx.write_status, Status::ResourceExhausted());
+ EXPECT_EQ(ctx.writer.last_write(), kChunk);
+
+ ctx.Spawn([](MpscTestContext* inner) {
+ inner->data = inner->data.subspan(kChunk);
+ inner->Write();
+ });
+ ctx.destination = destination.subspan(kChunk);
+ ctx.Read();
+ ctx.Join();
+ EXPECT_EQ(ctx.read_result.status(), OkStatus());
+ EXPECT_EQ(ctx.read_result->size(), 4U);
+ EXPECT_EQ(ctx.write_status, OkStatus());
+ EXPECT_EQ(ctx.writer.last_write(), 4U);
+
+ EXPECT_EQ(memcmp(ctx.write_buffer, ctx.read_buffer, kBufSize), 0);
+}
+
+TEST(MpscStreamTest, CannotReadAfterReaderClose) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.reader.Close();
+ ctx.Read();
+ EXPECT_EQ(ctx.read_result.status(), Status::OutOfRange());
+}
+
+TEST(MpscStreamTest, CanReadAfterWriterCloses) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ std::byte buffer[kBufSize];
+ ctx.reader.SetBuffer(buffer);
+ ctx.Fill();
+ ctx.Write();
+ EXPECT_EQ(ctx.write_status, OkStatus());
+ ctx.writer.Close();
+
+ ctx.Read();
+ ASSERT_EQ(ctx.read_result.status(), OkStatus());
+ ASSERT_EQ(ctx.read_result->size(), kBufSize);
+ EXPECT_EQ(memcmp(ctx.write_buffer, ctx.read_buffer, kBufSize), 0);
+}
+
+TEST(MpscStreamTest, CannotWriteAfterWriterClose) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.Fill();
+ ctx.writer.Close();
+ ctx.Write();
+ EXPECT_EQ(ctx.write_status, Status::OutOfRange());
+}
+
+TEST(MpscStreamTest, CannotWriteAfterReaderClose) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.Fill();
+ ctx.reader.Close();
+ ctx.Write();
+ EXPECT_EQ(ctx.write_status, Status::OutOfRange());
+}
+
+TEST(MpscStreamTest, MultipleWriters) {
+ MpscTestContext ctx1;
+ ctx1.Connect();
+ Vector<std::byte, kBufSize + 1> data1(kBufSize + 1, std::byte(1));
+ ctx1.data = ByteSpan(data1.data(), data1.size());
+
+ MpscTestContext ctx2;
+ ctx2.writer = ctx1.writer;
+ Vector<std::byte, kBufSize / 2> data2(kBufSize / 2, std::byte(2));
+ ctx2.data = ByteSpan(data2.data(), data2.size());
+
+ MpscTestContext ctx3;
+ ctx3.writer = ctx1.writer;
+ Vector<std::byte, kBufSize * 3> data3(kBufSize * 3, std::byte(3));
+ ctx3.data = ByteSpan(data3.data(), data3.size());
+
+ // Start all threads.
+ ctx1.Spawn([](MpscTestContext* ctx) { ctx->Write(); });
+ ctx2.Spawn([](MpscTestContext* ctx) { ctx->Write(); });
+ ctx3.Spawn([](MpscTestContext* ctx) { ctx->Write(); });
+
+ // The loop below keeps track of how many contiguous values are read, in order
+ // to verify that writes are not split or interleaved.
+ size_t expected[4] = {0, data1.size(), data2.size(), data3.size()};
+ size_t actual[4] = {0};
+
+ size_t total_read = 0;
+ auto current = std::byte(0);
+ size_t num_current = 0;
+ while (total_read < data1.size() + data2.size() + data3.size()) {
+ ctx1.Read();
+ if (!ctx1.read_result.ok()) {
+ break;
+ }
+ size_t num_read = ctx1.read_result->size();
+ for (size_t i = 0; i < num_read; ++i) {
+ if (current == ctx1.read_buffer[i]) {
+ ++num_current;
+ continue;
+ }
+ actual[size_t(current)] = num_current;
+ current = ctx1.read_buffer[i];
+ num_current = 1;
+ }
+ actual[size_t(current)] = num_current;
+ total_read += num_read;
+ }
+ ctx1.reader.Close();
+ ctx1.Join();
+ ctx2.Join();
+ ctx3.Join();
+ ASSERT_EQ(ctx1.read_result.status(), OkStatus());
+ for (size_t i = 0; i < 4; ++i) {
+ EXPECT_EQ(actual[i], expected[i]);
+ }
+}
+
+TEST(MpscStreamTest, GetAndSetLimits) {
+ MpscReader reader;
+ EXPECT_EQ(reader.ConservativeReadLimit(), 0U);
+
+ MpscWriter writer;
+ EXPECT_EQ(writer.ConservativeWriteLimit(), 0U);
+
+ CreateMpscStream(reader, writer);
+ EXPECT_EQ(reader.ConservativeReadLimit(), Stream::kUnlimited);
+ EXPECT_EQ(writer.ConservativeWriteLimit(), Stream::kUnlimited);
+
+ writer.SetLimit(10);
+ EXPECT_EQ(reader.ConservativeReadLimit(), 10U);
+ EXPECT_EQ(writer.ConservativeWriteLimit(), 10U);
+
+ writer.Close();
+ EXPECT_EQ(reader.ConservativeReadLimit(), 0U);
+ EXPECT_EQ(writer.ConservativeWriteLimit(), 0U);
+}
+
+TEST(MpscStreamTest, ReaderAggregatesLimit) {
+ MpscTestContext ctx;
+ ctx.Connect();
+ ctx.writer.SetLimit(10);
+
+ MpscWriter writer2 = ctx.writer;
+ writer2.SetLimit(20);
+
+ EXPECT_EQ(ctx.reader.ConservativeReadLimit(), 30U);
+
+ ctx.writer.SetLimit(Stream::kUnlimited);
+ EXPECT_EQ(ctx.reader.ConservativeReadLimit(), Stream::kUnlimited);
+
+ writer2.SetLimit(40);
+ EXPECT_EQ(ctx.reader.ConservativeReadLimit(), Stream::kUnlimited);
+
+ ctx.writer.SetLimit(0);
+ EXPECT_EQ(ctx.reader.ConservativeReadLimit(), 40U);
+}
+
+TEST(MpscStreamTest, ReadingUpdatesLimit) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ constexpr size_t kChunk = kBufSize - 4;
+ std::byte buffer[kBufSize];
+ ctx.reader.SetBuffer(buffer);
+ ctx.Fill();
+ ctx.writer.SetLimit(kBufSize);
+ ctx.Write();
+ EXPECT_EQ(ctx.write_status, OkStatus());
+
+ ctx.destination = ByteSpan(ctx.read_buffer, kChunk);
+ ctx.Read();
+ EXPECT_EQ(ctx.read_result.status(), OkStatus());
+ EXPECT_EQ(ctx.read_result->size(), kChunk);
+ EXPECT_EQ(ctx.reader.ConservativeReadLimit(), kBufSize - kChunk);
+}
+
+TEST(MpscStreamTest, CannotWriteMoreThanLimit) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ std::byte buffer[kBufSize];
+ ctx.reader.SetBuffer(buffer);
+ ctx.writer.SetLimit(kBufSize - 1);
+ ctx.Fill();
+ ctx.Write();
+ EXPECT_EQ(ctx.write_status, Status::ResourceExhausted());
+}
+
+TEST(MpscStreamTest, WritersCanCloseAutomatically) {
+ MpscTestContext ctx1;
+ ctx1.Connect();
+ Vector<std::byte, kBufSize + 1> data1(kBufSize + 1, std::byte(1));
+ ctx1.writer.SetLimit(data1.size());
+ ctx1.data = ByteSpan(data1.data(), data1.size());
+
+ MpscTestContext ctx2;
+ ctx2.writer = ctx1.writer;
+ Vector<std::byte, kBufSize / 2> data2(kBufSize / 2, std::byte(2));
+ ctx2.writer.SetLimit(data2.size());
+ ctx2.data = ByteSpan(data2.data(), data2.size());
+
+ // Start all threads.
+ EXPECT_TRUE(ctx1.reader.connected());
+ EXPECT_TRUE(ctx1.writer.connected());
+ EXPECT_TRUE(ctx2.writer.connected());
+
+ ctx1.Spawn([](MpscTestContext* ctx) { ctx->Write(); });
+ ctx2.Spawn([](MpscTestContext* ctx) { ctx->Write(); });
+
+ size_t total = 0;
+ while (ctx1.reader.ConservativeReadLimit() != 0) {
+ ctx1.Read();
+ EXPECT_EQ(ctx1.read_result.status(), OkStatus());
+ if (!ctx1.read_result.ok()) {
+ ctx1.reader.Close();
+ break;
+ }
+ total += ctx1.read_result->size();
+ }
+ EXPECT_EQ(total, data1.size() + data2.size());
+ ctx1.Join();
+ ctx2.Join();
+ EXPECT_FALSE(ctx1.reader.connected());
+ EXPECT_FALSE(ctx1.writer.connected());
+ EXPECT_FALSE(ctx2.writer.connected());
+}
+
+TEST(MpscStreamTest, ReadAllWithoutBuffer) {
+ MpscTestContext ctx;
+ Status status = ctx.reader.ReadAll([](ConstByteSpan) { return OkStatus(); });
+ EXPECT_EQ(status, Status::FailedPrecondition());
+}
+
+TEST(MpscStreamTest, ReadAll) {
+ MpscTestContext ctx;
+ ctx.Connect();
+
+ std::byte buffer[kBufSize];
+ ctx.reader.SetBuffer(buffer);
+ ctx.writer.SetLimit(kBufSize * 100);
+ ctx.Spawn([](MpscTestContext* inner) { inner->WriteAll(); });
+
+ Status status = ctx.reader.ReadAll([&ctx](ConstByteSpan data) {
+ ctx.total_read += data.size();
+ fnv1a(data, ctx.read_hash);
+ return OkStatus();
+ });
+ ctx.Join();
+
+ EXPECT_EQ(status, OkStatus());
+ EXPECT_FALSE(ctx.reader.connected());
+ EXPECT_EQ(ctx.total_read, kBufSize * 100);
+ EXPECT_EQ(ctx.read_hash, ctx.write_hash);
+}
+
+TEST(MpscStreamTest, BufferedMpscReader) {
+ BufferedMpscReader<kBufSize> reader;
+ MpscWriter writer;
+ CreateMpscStream(reader, writer);
+
+ // `kBufSize` writes of 1 byte each should fit without blocking.
+ for (size_t i = 0; i < kBufSize; ++i) {
+ std::byte b{static_cast<uint8_t>(i)};
+ EXPECT_EQ(writer.Write(ConstByteSpan(&b, 1)), OkStatus());
+ }
+
+ std::byte rx_buffer[kBufSize];
+ auto result = reader.Read(ByteSpan(rx_buffer));
+ ASSERT_EQ(result.status(), OkStatus());
+ ASSERT_EQ(result->size(), kBufSize);
+ for (size_t i = 0; i < kBufSize; ++i) {
+ EXPECT_EQ(rx_buffer[i], std::byte(i));
+ }
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_stream/public/pw_stream/mpsc_stream.h b/pw_stream/public/pw_stream/mpsc_stream.h
new file mode 100644
index 000000000..ee9a0d6cb
--- /dev/null
+++ b/pw_stream/public/pw_stream/mpsc_stream.h
@@ -0,0 +1,394 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+/// @file
+/// This file defines types related to a multi-producer, single-consumer stream.
+///
+/// The single readers must be constructed in place, while writers can be moved.
+/// A reader and writer may be connected using `CreateMpscStream()`. Additional
+/// writers may be connected by copying a previously connected writer.
+///
+/// Example:
+///
+/// @code{.cpp}
+/// void WriteThreadRoutine(void* arg) {
+/// auto *writer = static_cast<MpscWriter *>(arg);
+/// ConstByteSpan data = GenerateSomeData();
+/// Status status = writer->Write(data);
+/// ...
+/// }
+/// ...
+/// MpscReader reader;
+/// MpscWriter writer;
+/// CreateMpscStream(reader, writer);
+/// thread::Thread t(MakeThreadOptions(), WriteThreadRoutine, &writer);
+/// std::byte buffer[kBufSize];
+/// if (auto status = reader.Read(ByteSpan(buffer)); status.ok()) {
+/// ProcessSomeData(buffer);
+/// }
+/// @endcode
+///
+/// See the `MpscReader::ReadAll()` for additional examples.
+///
+/// The types in the files are designed to be used across different threads,
+/// but are not completely thread-safe. Data must only be written by an
+/// MpscWriter using a single thread, and data must only be read by an
+/// MpscReader using a single thread. In other words, multiple calls to
+/// `Write()` must not be made concurrently, and multiple calls to `Read()` and
+/// `ReadAll()` must not be made concurrently. Calls to other methods, e.g.
+/// `Close()`, are thread-safe and may be made from any thread.
+
+#include <cstddef>
+
+#include "pw_bytes/span.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/intrusive_list.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_stream/stream.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/timed_thread_notification.h"
+
+namespace pw::stream {
+
+// Forward declaration.
+class MpscReader;
+class MpscWriter;
+
+/// Creates a multi-producer, single consumer stream.
+///
+/// This method creates a stream by associating a reader and writer. Both are
+/// reset before being connected. This is the only way to connect a reader.
+/// Additional writers may be connected by copying the given writer after it is
+/// connected.
+///
+/// This method is thread-safe with respect to other MpscReader and MpscWriter
+/// methods. It is not thread-safe with respect to itself, i.e. callers must
+/// not make concurrent calls to `CreateMpscStream()` from different threads
+/// with the same objects.
+///
+/// @param[out] reader The reader to connect.
+/// @param[out] writer The writer to connect.
+void CreateMpscStream(MpscReader& reader, MpscWriter& writer);
+
+/// Writer for a multi-producer, single consumer stream.
+///
+/// This class has a default constructor that only produces disconnected
+/// writers. To connect writers, use `CreateMpscStream()`. Additional connected
+/// writers can be created by copying an existing one.
+///
+/// Each thread should have its own dedicated writer. This class is thread-safe
+/// with respect to the reader, but not with respect to itself. In particular,
+/// attempting to call `Write()` concurrently on different threads may cause
+/// result in a failure.
+class MpscWriter : public NonSeekableWriter,
+ public IntrusiveList<MpscWriter>::Item {
+ public:
+ using duration = std::optional<chrono::SystemClock::duration>;
+
+ /// A per-writer thread notification that can be added to a reader's list.
+ ///
+ /// The reader maintains a list of outstanding requests to write data. As
+ /// data is read, and space to write data becomes available, it uses these
+ /// requests to signal the waiting the writers.
+ struct Request : public IntrusiveList<Request>::Item {
+ sync::TimedThreadNotification notification;
+ using IntrusiveList<Request>::Item::unlisted;
+ };
+
+ MpscWriter() = default;
+ MpscWriter(const MpscWriter& other);
+ MpscWriter& operator=(const MpscWriter& other);
+ MpscWriter(MpscWriter&& other);
+ MpscWriter& operator=(MpscWriter&& other);
+ ~MpscWriter() override;
+
+ /// Returns whether this object is connected to a reader.
+ bool connected() const PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Indicates how much data was sent in the last call to `Write()`.
+ size_t last_write() const PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Returns the optional maximum time elapsed before a `Write()` fails.
+ const duration& timeout() const PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Set the timeout for writing to this stream.
+ ///
+ /// After setting a timeout, if the given duration elapses while making a call
+ /// to `Write()`, @pw_status{RESOURCE_EXHAUSTED} will be returned. If desired,
+ /// a timeout should be set before calling `Write()`. Setting a timeout when a
+ /// writer is awaiting notification from a reader will not affect the duration
+ /// of that wait.
+ ///
+ /// Note that setting a write timeout makes partial writes possible. For
+ /// example, if a call to `Write()` of some length corresponds to 2 calls to
+ /// `Read()` of half that length with an sufficient delay between the calls
+ /// will result in the first half being written and read, but not the second.
+ /// This differs from `Stream::Write()` which stipulates that no data is
+ /// written on failure. If this happens, the length of the data written can be
+ /// retrieved using `last_write()`.
+ ///
+ /// Generally, callers should use one of three approaches:
+ /// 1. Do not set a write timeout, and let writers block arbitrarily long
+ /// until space is available or the reader is disconnected.
+ /// 2. Use only a single writer, and use `last_write()` to resend data.
+ /// 3. Structure the data being sent so that the reader can always read
+ /// complete messages and avoid blocking or performing complex work
+ /// mid-message.
+ ///
+ /// @param[in] timeout The duration to wait before returning an error.
+ void SetTimeout(const duration& timeout) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Sets the maximum amount that can be written by this writer.
+ ///
+ /// By default, writers can write an unlimited amount of data. This method can
+ /// be used to set a limit, or remove it by providing a value of
+ /// Stream::kUnlimited.
+ ///
+ /// If a limit is set, the writer will automatically close once it has written
+ /// that much data. The current number of bytes remaining until the limit is
+ /// reached can be retrieved using `ConservativeWriteLimit()`.
+ ///
+ /// @param[in] limit The maximum amount that can be written by this writer.
+ void SetLimit(size_t limit) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Disconnects this writer from its reader.
+ ///
+ /// This method does nothing if the writer is not connected.
+ void Close() PW_LOCKS_EXCLUDED(mutex_);
+
+ private:
+ // The factory method is allowed to directly modify a writer to connect it
+ // to the reader.
+ friend void CreateMpscStream(MpscReader&, MpscWriter&);
+
+ /// @copydoc Stream::ConservativeLimit
+ size_t ConservativeLimit(LimitType type) const override;
+
+ /// @copydoc Stream::DoWrite
+ ///
+ /// This method is *not* thread-safe with respect to itself. If multiple
+ /// threads attempt to write concurrently using the same writer, those calls
+ /// may fail. Instead, each thread should have its own writer.
+ ///
+ /// @pre No other thread has called `Write()` on this object.
+ Status DoWrite(ConstByteSpan data) override;
+
+ /// Locked implementation of `Close()`.
+ void CloseLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ mutable sync::Mutex mutex_;
+ MpscReader* reader_ PW_GUARDED_BY(mutex_) = nullptr;
+ size_t limit_ PW_GUARDED_BY(mutex_) = kUnlimited;
+ Request write_request_;
+ duration timeout_ PW_GUARDED_BY(mutex_);
+ size_t last_write_ PW_GUARDED_BY(mutex_) = 0;
+};
+
+/// Reader of a multi-producer, single-consumer stream.
+///
+/// The reader manages 3 aspects of the stream:
+/// * The storage used to hold written data that is to be read.
+/// * The list of connected writers.
+/// * Accounting for how much data has and can be written.
+///
+/// This class has a default constructor that can only produce a disconnected
+/// reader. To connect a reader, use `CreateMpscStream()`.
+class MpscReader : public NonSeekableReader {
+ public:
+ using duration = std::optional<chrono::SystemClock::duration>;
+
+ MpscReader();
+ ~MpscReader() override;
+
+ /// Returns whether this object has any connected writers.
+ bool connected() const PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Set the timeout for reading from this stream.
+ ///
+ /// After setting a timeout, if the given duration elapses while making a call
+ /// to `Read()`, RESOURCE_EXHAUSTED will be returned. If desired, a timeout
+ /// should be set before calling `Read()` or `ReadAll()`. Setting a timeout
+ /// when a reader is awaiting notification from a writer will not affect the
+ /// duration of that wait. `ReadUntilClose()` ignores timeouts entirely.
+ ///
+ /// @param[in] timeout The duration to wait before returning an error.
+ void SetTimeout(const duration& timeout) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Associates the reader with storage to buffer written data to be read.
+ ///
+ /// If desired, callers can use this method to buffer written data. This can
+ /// improve writer performance by allowing calls to `WriteData()` to avoid
+ /// waiting for the reader, albeit at the cost of increased memory. This can
+ /// be useful when the reader needs time to process the data it reads, or when
+ /// the volume of writes varies over time, i.e. is "bursty".
+ ///
+ /// The reader does not take ownership of the storage, which must be valid
+ /// until a call to the destructor or another call to `SetBuffer()`.
+ ///
+ /// @param[in] buffer A view to the storage.
+ void SetBuffer(ByteSpan buffer) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// @fn ReadAll
+ /// Reads data in a loop and passes it to a provided callback.
+ ///
+ /// This will read continuously until all connected writers close.
+ ///
+ /// Example usage:
+ ///
+ /// @code(.cpp}
+ /// MpscReader reader;
+ /// MpscWriter writer;
+ /// MpscStreamCreate(reader, writer);
+ /// thread::Thread t(MakeThreadOptions(), [] (void*arg) {
+ /// auto *writer = static_cast<MpscWriter *>(arg);
+ /// writer->Write(GenerateSomeData()).IgnoreError();
+ /// }, &writer);
+ /// auto status = reader.ReadAll([] (ConstByteSpan data) {
+ /// return ProcessSomeData();
+ /// });
+ /// t.join();
+ /// @endcode
+ ///
+ /// @param[in] callback A callable object to invoke on data as it is read.
+ /// @retval OK Successfully read until writers closed.
+ /// @retval FAILED_PRECONDITION The object does not have a buffer.
+ /// @retval RESOURCE_EXHAUSTED Timed out when reading data. This can only
+ /// occur if a timeout has been set.
+ /// @retval Any other error as returned by the callback.
+ using ReadAllCallback = Function<Status(ConstByteSpan data)>;
+ Status ReadAll(ReadAllCallback callback) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Disconnects all writers and drops any unread data.
+ void Close() PW_LOCKS_EXCLUDED(mutex_);
+
+ private:
+ // The factory method is allowed to directly modify the reader to connect it
+ // to a writer.
+ friend void CreateMpscStream(MpscReader&, MpscWriter&);
+
+ // The writer is allowed to call directly into the reader to:
+ // * Add/remove itself to the reader's list of writer.
+ // * Request space to write data, and to write to that space.
+ friend class MpscWriter;
+
+ /// @fn IncreaseLimit
+ /// @fn IncreaseLimitLocked
+ /// Increases the number of remaining bytes to be written.
+ ///
+ /// Used by `MpscWriter::SetLimit()` and `MpscWriter::WriteData()`.
+ ///
+ /// @param[in] delta How much to increase the number of remaining bytes.
+ void IncreaseLimit(size_t delta) PW_LOCKS_EXCLUDED(mutex_);
+ void IncreaseLimitLocked(size_t delta) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// @fn DecreaseLimit
+ /// @fn DecreaseLimitLocked
+ /// Decreases the number of remaining bytes to be written.
+ ///
+ /// Used by `MpscWriter::SetLimit()` and `MpscWriter::RemoveWriter()`.
+ ///
+ /// @param[in] delta How much to decrease the number of remaining bytes.
+ void DecreaseLimit(size_t delta) PW_LOCKS_EXCLUDED(mutex_);
+ void DecreaseLimitLocked(size_t delta) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// @copydoc Stream::ConservativeLimit
+ size_t ConservativeLimit(Stream::LimitType type) const override
+ PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Adds the write request to the reader's list of pending requests.
+ ///
+ /// Used by `MpscWriter::WriteData()`.
+ ///
+ /// @param[in] write_request A writer's request object.
+ void RequestWrite(MpscWriter::Request& write_request)
+ PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Checks if a writer can write data, and signals it if so.
+ ///
+ /// A reader may signal a writer because:
+ /// * Space to write data has become available.
+ /// * The queue of write requests has changed.
+ /// * The reader is closing. `WriteData()` will return OUT_OF_RANGE.
+ void CheckWriteableLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// Adds data from a writer to the buffer to be read.
+ ///
+ /// @param[in] data The data to be written.
+ /// @param[in] limit The writer's current write limit.
+ ///
+ /// @retval OK Data was written to the buffer.
+ /// @retval RESOURCE_EXHAUSTED Buffer has insufficent space for data.
+ /// @retval OUT_OF_RANGE Stream is shut down or closed.
+ StatusWithSize WriteData(ConstByteSpan data, size_t limit)
+ PW_LOCKS_EXCLUDED(mutex_);
+
+ /// @fn CompleteWrite
+ /// @fn CompleteWriteLocked
+ /// Removes the write request from the reader's list of pending requests.
+ ///
+ /// Used by `MpscWriter::WriteData()` and `MpscWriter::CloseLocked()`.
+ ///
+ /// @param[in] write_request A writer's request object.
+ void CompleteWrite(MpscWriter::Request& write_request)
+ PW_LOCKS_EXCLUDED(mutex_);
+ void CompleteWriteLocked(MpscWriter::Request& write_request)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// @copydoc Stream::DoRead
+ StatusWithSize DoRead(ByteSpan destination) override
+ PW_LOCKS_EXCLUDED(mutex_);
+
+ // Locked implementations.
+
+ mutable sync::Mutex mutex_;
+ IntrusiveList<MpscWriter> writers_ PW_GUARDED_BY(mutex_);
+ IntrusiveList<MpscWriter::Request> write_requests_ PW_GUARDED_BY(mutex_);
+ IntrusiveList<MpscWriter::Request>::iterator last_request_
+ PW_GUARDED_BY(mutex_);
+
+ size_t num_unlimited_ PW_GUARDED_BY(mutex_) = 0;
+ size_t limit_ PW_GUARDED_BY(mutex_) = 0;
+
+ bool reading_ PW_GUARDED_BY(mutex_) = false;
+ sync::TimedThreadNotification readable_;
+ sync::ThreadNotification closeable_;
+ duration timeout_ PW_GUARDED_BY(mutex_);
+
+ ByteSpan destination_ PW_GUARDED_BY(mutex_);
+ size_t written_ PW_GUARDED_BY(mutex_) = 0;
+
+ ByteSpan buffer_ PW_GUARDED_BY(mutex_);
+ size_t offset_ PW_GUARDED_BY(mutex_) = 0;
+ size_t length_ PW_GUARDED_BY(mutex_) = 0;
+};
+
+/// Reader for a multi-producer, single consumer stream.
+///
+/// This class includes an explicitly-sized buffer. It has a default constructor
+/// that can only produce a disconnected reader. To connect a reader, use
+/// `CreateMpscStream()`.
+template <size_t kCapacity>
+class BufferedMpscReader : public MpscReader {
+ public:
+ BufferedMpscReader() { SetBuffer(buffer_); }
+
+ private:
+ std::array<std::byte, kCapacity> buffer_;
+};
+
+} // namespace pw::stream
diff --git a/pw_stream/public/pw_stream/socket_stream.h b/pw_stream/public/pw_stream/socket_stream.h
index 0092fc808..a9b7b16a6 100644
--- a/pw_stream/public/pw_stream/socket_stream.h
+++ b/pw_stream/public/pw_stream/socket_stream.h
@@ -13,8 +13,6 @@
// the License.
#pragma once
-#include <netinet/in.h>
-
#include <cstdint>
#include "pw_result/result.h"
@@ -26,23 +24,17 @@ namespace pw::stream {
class SocketStream : public NonSeekableReaderWriter {
public:
constexpr SocketStream() = default;
+ // Construct a SocketStream directly from a file descriptor.
+ explicit SocketStream(int connection_fd) : connection_fd_(connection_fd) {}
// SocketStream objects are moveable but not copyable.
SocketStream& operator=(SocketStream&& other) {
- listen_port_ = other.listen_port_;
- socket_fd_ = other.socket_fd_;
- other.socket_fd_ = kInvalidFd;
connection_fd_ = other.connection_fd_;
other.connection_fd_ = kInvalidFd;
- sockaddr_client_ = other.sockaddr_client_;
return *this;
}
SocketStream(SocketStream&& other) noexcept
- : listen_port_(other.listen_port_),
- socket_fd_(other.socket_fd_),
- connection_fd_(other.connection_fd_),
- sockaddr_client_(other.sockaddr_client_) {
- other.socket_fd_ = kInvalidFd;
+ : connection_fd_(other.connection_fd_) {
other.connection_fd_ = kInvalidFd;
}
SocketStream(const SocketStream&) = delete;
@@ -50,12 +42,6 @@ class SocketStream : public NonSeekableReaderWriter {
~SocketStream() override { Close(); }
- // Listen to the port and return after a client is connected
- //
- // DEPRECATED: Use the ServerSocket class instead.
- // TODO(b/271323032): Remove when this method is no longer used.
- Status Serve(uint16_t port);
-
// Connect to a local or remote endpoint. Host may be either an IPv4 or IPv6
// address. If host is nullptr then the IPv4 localhost address is used
// instead.
@@ -80,10 +66,7 @@ class SocketStream : public NonSeekableReaderWriter {
StatusWithSize DoRead(ByteSpan dest) override;
- uint16_t listen_port_ = 0;
- int socket_fd_ = kInvalidFd;
int connection_fd_ = kInvalidFd;
- struct sockaddr_in sockaddr_client_ = {};
};
/// `ServerSocket` wraps a POSIX-style server socket, producing a `SocketStream`
diff --git a/pw_stream/public/pw_stream/stream.h b/pw_stream/public/pw_stream/stream.h
index a7eed528e..f8b69056b 100644
--- a/pw_stream/public/pw_stream/stream.h
+++ b/pw_stream/public/pw_stream/stream.h
@@ -15,6 +15,7 @@
#include <array>
#include <cstddef>
+#include <cstdint>
#include <limits>
#include "pw_assert/assert.h"
@@ -26,78 +27,87 @@
namespace pw::stream {
-// A generic stream that may support reading, writing, and seeking, but makes no
-// guarantees about whether any operations are supported. Unsupported functions
-// return Status::Unimplemented(). Stream serves as the base for the Reader,
-// Writer, and ReaderWriter interfaces.
-//
-// Stream should NOT be used or extended directly. Instead, work with one of the
-// derived classes that explicitly supports the required functionality.
+/// A generic stream that may support reading, writing, and seeking, but makes
+/// no guarantees about whether any operations are supported. Unsupported
+/// functions return Status::Unimplemented() Stream serves as the base for the
+/// Reader, Writer, and ReaderWriter interfaces.
+///
+/// Stream cannot be extended directly. Instead, work with one of the derived
+/// classes that explicitly supports the required functionality. Stream should
+/// almost never be used in APIs; accept a derived class with the required
+/// capabilities instead.
+///
+/// All Stream methods are blocking. They return when the requested operation
+/// completes.
class Stream {
public:
- // Positions from which to seek.
+ /// Positions from which to seek.
enum Whence : uint8_t {
- // Seek from the beginning of the stream. The offset is a direct offset into
- // the data.
+ /// Seek from the beginning of the stream. The offset is a direct offset
+ /// into the data.
kBeginning = 0b001,
- // Seek from the current position in the stream. The offset is added to the
- // current position. Use a negative offset to seek backwards.
- //
- // Implementations may only support seeking within a limited range from
- // the current position.
+ /// Seek from the current position in the stream. The offset is added to the
+ /// current position. Use a negative offset to seek backwards.
+ ///
+ /// Implementations may only support seeking within a limited range from the
+ /// current position.
kCurrent = 0b010,
- // Seek from the end of the stream. The offset is added to the end
- // position. Use a negative offset to seek backwards from the end.
+ /// Seek from the end of the stream. The offset is added to the end
+ /// position. Use a negative offset to seek backwards from the end.
kEnd = 0b100,
};
- // Value returned from read/write limit if unlimited.
+ /// Value returned from read/write limit if unlimited.
static constexpr size_t kUnlimited = std::numeric_limits<size_t>::max();
- // Returned by Tell() if getting the position is not supported.
+ /// Returned by Tell() if getting the position is not supported.
static constexpr size_t kUnknownPosition = std::numeric_limits<size_t>::max();
virtual ~Stream() = default;
- // True if reading is supported. If false, Read() returns UNIMPLEMENTED.
+ /// @retval True If reading is supported.
+ /// @retval False If Read() returns UNIMPLEMENTED.
constexpr bool readable() const { return readable_; }
- // True if writing is supported. If false, Write() returns UNIMPLEMENTED.
+ /// @retval True If writing is supported.
+ /// @retval False If Write() returns UNIMPLEMENTED.
constexpr bool writable() const { return writable_; }
- // True if the stream supports seeking.
+ /// @retval True If the stream supports seeking.
+ /// @retval False If the stream does not supports seeking.
constexpr bool seekable() const { return seekability_ != Seekability::kNone; }
- // True if the stream supports seeking from the specified origin.
+ /// True if the stream supports seeking from the specified origin.
constexpr bool seekable(Whence origin) const {
return (static_cast<uint8_t>(seekability_) & origin) != 0u;
}
- // Reads data from this stream. If any number of bytes are read returns OK
- // with a span of the bytes read.
- //
- // If the reader has been exhausted and is and can no longer read additional
- // bytes it will return OUT_OF_RANGE. This is similar to end-of-file (EOF).
- // Read will only return OUT_OF_RANGE if ConservativeReadLimit() is and will
- // remain zero. A Read operation that is successful and also exhausts the
- // reader returns OK, with all following calls returning OUT_OF_RANGE.
- //
- // Derived classes should NOT try to override these public read methods.
- // Instead, provide an implementation by overriding DoRead().
- //
- // Returns:
- //
- // OK - Between 1 and dest.size_bytes() were successfully read. Returns the
- // span of read bytes.
- // UNIMPLEMENTED - This stream does not support reading.
- // FAILED_PRECONDITION - The Reader is not in state to read data.
- // RESOURCE_EXHAUSTED - Unable to read any bytes at this time. No bytes
- // read. Try again once bytes become available.
- // OUT_OF_RANGE - Reader has been exhausted, similar to EOF. No bytes were
- // read, no more will be read.
- //
+ /// Reads data from the stream into the provided buffer, if supported. As many
+ /// bytes as are available up to the buffer size are copied into the buffer.
+ /// Remaining bytes may by read in subsequent calls. If any number of bytes
+ /// are read returns OK with a span of the bytes read.
+ ///
+ /// If the reader has been exhausted and is and can no longer read additional
+ /// bytes it will return `OUT_OF_RANGE`. This is similar to end-of-file (EOF).
+ /// Read will only return `OUT_OF_RANGE` if ConservativeReadLimit() is and
+ /// will remain zero. A Read operation that is successful and also exhausts
+ /// the reader returns OK, with all following calls returning `OUT_OF_RANGE`.
+ ///
+ /// Derived classes should NOT try to override these public read methods.
+ /// Instead, provide an implementation by overriding DoRead().
+ ///
+ /// @retval OK Between 1 and `dest.size_bytes()` were successfully
+ /// read. Returns the span of read bytes.
+ /// @retval UNIMPLEMENTED This stream does not support reading.
+ /// @retval FAILED_PRECONDITION The Reader is not in state to read data.
+ /// @retval RESOURCE_EXHAUSTED Unable to read any bytes at this time. No
+ /// bytes read. Try again once bytes become
+ /// available.
+ /// @retval OUT_OF_RANGE Reader has been exhausted, similar to EOF. No bytes
+ /// were read, no more will be read.
+ ///
Result<ByteSpan> Read(ByteSpan dest) {
PW_DASSERT(dest.empty() || dest.data() != nullptr);
StatusWithSize result = DoRead(dest);
@@ -107,96 +117,91 @@ class Stream {
}
return result.status();
}
+ /// @overload
Result<ByteSpan> Read(void* dest, size_t size_bytes) {
return Read(span(static_cast<std::byte*>(dest), size_bytes));
}
- // Writes data to this stream. Data is not guaranteed to be fully written out
- // to final resting place on Write return.
- //
- // If the writer is unable to fully accept the input data size it will abort
- // the write and return RESOURCE_EXHAUSTED.
- //
- // If the writer has been exhausted and is and can no longer accept additional
- // bytes it will return OUT_OF_RANGE. This is similar to end-of-file (EOF).
- // Write will only return OUT_OF_RANGE if ConservativeWriteLimit() is and will
- // remain zero. A Write operation that is successful and also exhausts the
- // writer returns OK, with all following calls returning OUT_OF_RANGE. When
- // ConservativeWriteLimit() is greater than zero, a Write that is a number of
- // bytes beyond what will exhaust the Write will abort and return
- // RESOURCE_EXHAUSTED rather than OUT_OF_RANGE because the writer is still
- // able to write bytes.
- //
- // Derived classes should NOT try to override the public Write methods.
- // Instead, provide an implementation by overriding DoWrite().
- //
- // Returns:
- //
- // OK - Data was successfully accepted by the stream.
- // UNIMPLEMENTED - This stream does not support writing.
- // FAILED_PRECONDITION - The writer is not in a state to accept data.
- // RESOURCE_EXHAUSTED - The writer was unable to write all of requested data
- // at this time. No data was written.
- // OUT_OF_RANGE - The Writer has been exhausted, similar to EOF. No data was
- // written; no more will be written.
- //
+ /// Writes data to this stream. Data is not guaranteed to be fully written out
+ /// to final resting place on Write return.
+ ///
+ /// If the writer is unable to fully accept the input data size it will abort
+ /// the write and return `RESOURCE_EXHAUSTED`.
+ ///
+ /// If the writer has been exhausted and is and can no longer accept
+ /// additional bytes it will return `OUT_OF_RANGE`. This is similar to
+ /// end-of-file (EOF). Write will only return `OUT_OF_RANGE` if
+ /// ConservativeWriteLimit() is and will remain zero. A Write operation that
+ /// is successful and also exhausts the writer returns OK, with all following
+ /// calls returning `OUT_OF_RANGE`. When ConservativeWriteLimit() is greater
+ /// than zero, a Write that is a number of bytes beyond what will exhaust the
+ /// Write will abort and return `RESOURCE_EXHAUSTED` rather than OUT_OF_RANGE
+ /// because the writer is still able to write bytes.
+ ///
+ /// Derived classes should NOT try to override the public Write methods.
+ /// Instead, provide an implementation by overriding DoWrite().
+ ///
+ /// @retval OK Data was successfully accepted by the stream.
+ /// @retval UNIMPLEMENTED This stream does not support writing.
+ /// @retval FAILED_PRECONDITION The writer is not in a state to accept data.
+ /// @retval RESOURCE_EXHAUSTED The writer was unable to write all of requested
+ /// data at this time. No data was written.
+ /// @retval OUT_OF_RANGE The Writer has been exhausted, similar to EOF. No
+ /// data was written; no more will be written.
Status Write(ConstByteSpan data) {
PW_DASSERT(data.empty() || data.data() != nullptr);
return DoWrite(data);
}
+ /// @overload
Status Write(const void* data, size_t size_bytes) {
return Write(span(static_cast<const std::byte*>(data), size_bytes));
}
+ /// @overload
Status Write(const std::byte b) { return Write(&b, 1); }
- // Changes the current position in the stream for both reading and writing, if
- // supported.
- //
- // Seeking to a negative offset is invalid. The behavior when seeking beyond
- // the end of a stream is determined by the implementation. The implementation
- // could fail with OUT_OF_RANGE or append bytes to the stream.
- //
- // Returns:
- //
- // OK - Successfully updated the position.
- // UNIMPLEMENTED - Seeking is not supported for this stream.
- // OUT_OF_RANGE - Attempted to seek beyond the bounds of the stream. The
- // position is unchanged.
- //
+ /// Changes the current position in the stream for both reading and writing,
+ /// if supported.
+ ///
+ /// Seeking to a negative offset is invalid. The behavior when seeking beyond
+ /// the end of a stream is determined by the implementation. The
+ /// implementation could fail with OUT_OF_RANGE or append bytes to the stream.
+ ///
+ /// @retval OK Successfully updated the position.
+ /// @retval UNIMPLEMENTED Seeking is not supported for this stream.
+ /// @retval OUT_OF_RANGE Attempted to seek beyond the bounds of the
+ /// stream. The position is unchanged.
Status Seek(ptrdiff_t offset, Whence origin = kBeginning) {
return DoSeek(offset, origin);
}
- // Returns the current position in the stream, if supported. The position is
- // the offset from the beginning of the stream. Returns
- // Stream::kUnknownPosition if the position is unknown.
- //
- // Streams that support seeking from the beginning always support Tell().
- // Other streams may or may not support Tell().
+ /// Returns the current position in the stream, if supported. The position is
+ /// the offset from the beginning of the stream. Returns
+ /// Stream::kUnknownPosition (`size_t(-1)`) if the position is unknown.
+ ///
+ /// Streams that support seeking from the beginning always support Tell().
+ /// Other streams may or may not support Tell().
size_t Tell() { return DoTell(); }
- // Liklely (not guaranteed) minimum bytes available to read at this time.
- // This number is advisory and not guaranteed to read full number of requested
- // bytes or without a RESOURCE_EXHAUSTED or OUT_OF_RANGE. As Reader
- // processes/handles/receives enqueued data or other contexts read data this
- // number can go up or down for some Readers.
- //
- // Returns zero if, in the current state, Read() would not return
- // OkStatus().
- //
- // Returns kUnlimited if the implementation imposes no limits on read sizes.
+ /// Liklely (not guaranteed) minimum bytes available to read at this time.
+ /// This number is advisory and not guaranteed to read full number of
+ /// requested bytes or without a `RESOURCE_EXHAUSTED` or `OUT_OF_RANGE`. As
+ /// Reader processes/handles/receives enqueued data or other contexts read
+ /// data this number can go up or down for some Readers.
+ ///
+ /// @retval zero if, in the current state, Read() would not return OkStatus().
+ /// @retval kUnlimited if the implementation imposes no limits on read sizes.
size_t ConservativeReadLimit() const {
return ConservativeLimit(LimitType::kRead);
}
- // Likely (not guaranteed) minimum bytes available to write at this time.
- // This number is advisory and not guaranteed to write without a
- // RESOURCE_EXHAUSTED or OUT_OF_RANGE. As Writer processes/handles enqueued of
- // other contexts write data this number can go up or down for some Writers.
- // Returns zero if, in the current state, Write() would not return
- // OkStatus().
- //
- // Returns kUnlimited if the implementation has no limits on write sizes.
+ /// Likely (not guaranteed) minimum bytes available to write at this time.
+ /// This number is advisory and not guaranteed to write without a
+ /// `RESOURCE_EXHAUSTED` or `OUT_OF_RANGE`. As Writer processes/handles
+ /// enqueued of other contexts write data this number can go up or down for
+ /// some Writers. Returns zero if, in the current state, Write() would not
+ /// return OkStatus().
+ ///
+ /// Returns kUnlimited if the implementation has no limits on write sizes.
size_t ConservativeWriteLimit() const {
return ConservativeLimit(LimitType::kWrite);
}
@@ -223,36 +228,47 @@ class Stream {
friend class SeekableReaderWriter;
friend class NonSeekableReaderWriter;
- // Seekability expresses the origins from which the stream always supports
- // seeking. Seeking from other origins may work, but is not guaranteed.
- //
- // Seekability is implemented as a bitfield of Whence values.
+ /// Seekability expresses the origins from which the stream always supports
+ /// seeking. Seeking from other origins may work, but is not guaranteed.
+ ///
+ /// Seekability is implemented as a bitfield of Whence values.
enum class Seekability : uint8_t {
- // No type of seeking is supported.
+ /// No type of seeking is supported.
kNone = 0,
- // Seeking from the current position is supported, but the range may be
- // limited. For example, a buffered stream might support seeking within the
- // buffered data, but not before or after.
+ /// Seeking from the current position is supported, but the range may be
+ /// limited. For example, a buffered stream might support seeking within the
+ /// buffered data, but not before or after.
kRelative = kCurrent,
- // The stream supports random access anywhere within the stream.
+ /// The stream supports random access anywhere within the stream.
kAbsolute = kBeginning | kCurrent | kEnd,
};
+ // These are the virtual methods that are implemented by derived classes. The
+ // public API functions call these virtual functions.
+
constexpr Stream(bool readable, bool writable, Seekability seekability)
: readable_(readable), writable_(writable), seekability_(seekability) {}
- // These are the virtual methods that are implemented by derived classes. The
- // public API functions call these virtual functions.
+ /// Virtual Read() function implemented by derived classes.
virtual StatusWithSize DoRead(ByteSpan destination) = 0;
+ /// Virtual Write() function implemented by derived classes.
virtual Status DoWrite(ConstByteSpan data) = 0;
+ /// Virtual Seek() function implemented by derived classes.
virtual Status DoSeek(ptrdiff_t offset, Whence origin) = 0;
+ /// Virtual Tell() function optionally implemented by derived classes.
+ /// The default implementation always returns kUnknownPosition.
virtual size_t DoTell() { return kUnknownPosition; }
+ /// Virtual function optionally implemented by derived classes that is used
+ /// for ConservativeReadLimit() and ConservativeWriteLimit().
+ ///
+ /// The default implementation returns kUnlimited or ``0`` depending on
+ /// whether the stream is readable/writable.
virtual size_t ConservativeLimit(LimitType limit_type) const {
if (limit_type == LimitType::kRead) {
return readable() ? kUnlimited : 0;
@@ -265,19 +281,20 @@ class Stream {
Seekability seekability_;
};
-// A Stream that supports reading but not writing. The Write() method is hidden.
-//
-// Use in APIs when:
-// * Must read from, but not write to, a stream.
-// * May or may not need seeking. Use a SeekableReader& if seeking is
-// required.
-//
-// Inherit from when:
-// * Reader cannot be extended directly. Instead, extend SeekableReader,
-// NonSeekableReader, or (rarely) RelativeSeekableReader, as appropriate.
-//
-// A Reader may or may not support seeking. Check seekable() or try calling
-// Seek() to determine if the stream is seekable.
+/// A Stream that supports reading but not writing. The Write() method is
+/// hidden.
+///
+/// Use in APIs when:
+/// * Must read from, but not write to, a stream.
+/// * May or may not need seeking. Use a SeekableReader& if seeking is
+/// required.
+///
+/// Inherit from when:
+/// * Reader cannot be extended directly. Instead, extend SeekableReader,
+/// NonSeekableReader, or (rarely) RelativeSeekableReader, as appropriate.
+///
+/// A Reader may or may not support seeking. Check seekable() or try calling
+/// Seek() to determine if the stream is seekable.
class Reader : public Stream {
private:
friend class RelativeSeekableReader;
@@ -291,21 +308,21 @@ class Reader : public Stream {
Status DoWrite(ConstByteSpan) final { return Status::Unimplemented(); }
};
-// A Reader that supports at least relative seeking within some range of the
-// current position. Seeking beyond that or from other origins may or may not be
-// supported. The extent to which seeking is possible is NOT exposed by this
-// API.
-//
-// Use in APIs when:
-// * Relative seeking is required. Usage in APIs should be rare; generally
-// Reader should be used instead.
-//
-// Inherit from when:
-// * Implementing a Reader that can only support seeking near the current
-// position.
-//
-// A buffered Reader that only supports seeking within its buffer is a good
-// example of a RelativeSeekableReader.
+/// A Reader that supports at least relative seeking within some range of the
+/// current position. Seeking beyond that or from other origins may or may not
+/// be supported. The extent to which seeking is possible is NOT exposed by this
+/// API.
+///
+/// Use in APIs when:
+/// * Relative seeking is required. Usage in APIs should be rare; generally
+/// Reader should be used instead.
+///
+/// Inherit from when:
+/// * Implementing a Reader that can only support seeking near the current
+/// position.
+///
+/// A buffered Reader that only supports seeking within its buffer is a good
+/// example of a RelativeSeekableReader.
class RelativeSeekableReader : public Reader {
protected:
constexpr RelativeSeekableReader()
@@ -318,28 +335,28 @@ class RelativeSeekableReader : public Reader {
: Reader(seekability) {}
};
-// A Reader that fully supports seeking.
-//
-// Use in APIs when:
-// * Absolute seeking is required. Use Reader& if seeking is not required or
-// seek failures can be handled gracefully.
-//
-// Inherit from when:
-// * Implementing a reader that supports absolute seeking.
-//
+/// A Reader that fully supports seeking.
+///
+/// Use in APIs when:
+/// * Absolute seeking is required. Use Reader& if seeking is not required or
+/// seek failures can be handled gracefully.
+///
+/// Inherit from when:
+/// * Implementing a reader that supports absolute seeking.
+///
class SeekableReader : public RelativeSeekableReader {
protected:
constexpr SeekableReader() : RelativeSeekableReader(Seekability::kAbsolute) {}
};
-// A Reader that does not support seeking. The Seek() method is hidden.
-//
-// Use in APIs when:
-// * Do NOT use in APIs! If seeking is not required, use Reader& instead.
-//
-// Inherit from when:
-// * Implementing a Reader that does not support seeking.
-//
+/// A Reader that does not support seeking. The Seek() method is hidden.
+///
+/// Use in APIs when:
+/// * Do NOT use in APIs! If seeking is not required, use Reader& instead.
+///
+/// Inherit from when:
+/// * Implementing a Reader that does not support seeking.
+///
class NonSeekableReader : public Reader {
protected:
constexpr NonSeekableReader() : Reader(Seekability::kNone) {}
@@ -350,19 +367,19 @@ class NonSeekableReader : public Reader {
Status DoSeek(ptrdiff_t, Whence) final { return Status::Unimplemented(); }
};
-// A Stream that supports writing but not reading. The Read() method is hidden.
-//
-// Use in APIs when:
-// * Must write to, but not read from, a stream.
-// * May or may not need seeking. Use a SeekableWriter& if seeking is
-// required.
-//
-// Inherit from when:
-// * Writer cannot be extended directly. Instead, extend SeekableWriter,
-// NonSeekableWriter, or (rarely) RelativeSeekableWriter, as appropriate.
-//
-// A Writer may or may not support seeking. Check seekable() or try calling
-// Seek() to determine if the stream is seekable.
+/// A Stream that supports writing but not reading. The Read() method is hidden.
+///
+/// Use in APIs when:
+/// * Must write to, but not read from, a stream.
+/// * May or may not need seeking. Use a SeekableWriter& if seeking is
+/// required.
+///
+/// Inherit from when:
+/// * Writer cannot be extended directly. Instead, extend SeekableWriter,
+/// NonSeekableWriter, or (rarely) RelativeSeekableWriter, as appropriate.
+///
+/// A Writer may or may not support seeking. Check seekable() or try calling
+/// Seek() to determine if the stream is seekable.
class Writer : public Stream {
private:
friend class RelativeSeekableWriter;
@@ -378,21 +395,21 @@ class Writer : public Stream {
}
};
-// A Writer that supports at least relative seeking within some range of the
-// current position. Seeking beyond that or from other origins may or may not be
-// supported. The extent to which seeking is possible is NOT exposed by this
-// API.
-//
-// Use in APIs when:
-// * Relative seeking is required. Usage in APIs should be rare; generally
-// Writer should be used instead.
-//
-// Inherit from when:
-// * Implementing a Writer that can only support seeking near the current
-// position.
-//
-// A buffered Writer that only supports seeking within its buffer is a good
-// example of a RelativeSeekableWriter.
+/// A Writer that supports at least relative seeking within some range of the
+/// current position. Seeking beyond that or from other origins may or may not
+/// be supported. The extent to which seeking is possible is NOT exposed by this
+/// API.
+///
+/// Use in APIs when:
+/// * Relative seeking is required. Usage in APIs should be rare; generally
+/// Writer should be used instead.
+///
+/// Inherit from when:
+/// * Implementing a Writer that can only support seeking near the current
+/// position.
+///
+/// A buffered Writer that only supports seeking within its buffer is a good
+/// example of a RelativeSeekableWriter.
class RelativeSeekableWriter : public Writer {
protected:
constexpr RelativeSeekableWriter()
@@ -405,28 +422,28 @@ class RelativeSeekableWriter : public Writer {
: Writer(seekability) {}
};
-// A Writer that fully supports seeking.
-//
-// Use in APIs when:
-// * Absolute seeking is required. Use Writer& if seeking is not required or
-// seek failures can be handled gracefully.
-//
-// Inherit from when:
-// * Implementing a writer that supports absolute seeking.
-//
+/// A Writer that fully supports seeking.
+///
+/// Use in APIs when:
+/// * Absolute seeking is required. Use Writer& if seeking is not required or
+/// seek failures can be handled gracefully.
+///
+/// Inherit from when:
+/// * Implementing a writer that supports absolute seeking.
+///
class SeekableWriter : public RelativeSeekableWriter {
protected:
constexpr SeekableWriter() : RelativeSeekableWriter(Seekability::kAbsolute) {}
};
-// A Writer that does not support seeking. The Seek() method is hidden.
-//
-// Use in APIs when:
-// * Do NOT use in APIs! If seeking is not required, use Writer& instead.
-//
-// Inherit from when:
-// * Implementing a Writer that does not support seeking.
-//
+/// A Writer that does not support seeking. The Seek() method is hidden.
+///
+/// Use in APIs when:
+/// * Do NOT use in APIs! If seeking is not required, use Writer& instead.
+///
+/// Inherit from when:
+/// * Implementing a Writer that does not support seeking.
+///
class NonSeekableWriter : public Writer {
protected:
constexpr NonSeekableWriter() : Writer(Seekability::kNone) {}
@@ -437,20 +454,20 @@ class NonSeekableWriter : public Writer {
Status DoSeek(ptrdiff_t, Whence) final { return Status::Unimplemented(); }
};
-// A Stream that supports both reading and writing.
-//
-// Use in APIs when:
-// * Must both read from and write to a stream.
-// * May or may not need seeking. Use a SeekableReaderWriter& if seeking is
-// required.
-//
-// Inherit from when:
-// * Cannot extend ReaderWriter directly. Instead, extend
-// SeekableReaderWriter, NonSeekableReaderWriter, or (rarely)
-// RelativeSeekableReaderWriter, as appropriate.
-//
-// A ReaderWriter may or may not support seeking. Check seekable() or try
-// calling Seek() to determine if the stream is seekable.
+/// A Stream that supports both reading and writing.
+///
+/// Use in APIs when:
+/// * Must both read from and write to a stream.
+/// * May or may not need seeking. Use a SeekableReaderWriter& if seeking is
+/// required.
+///
+/// Inherit from when:
+/// * Cannot extend ReaderWriter directly. Instead, extend
+/// SeekableReaderWriter, NonSeekableReaderWriter, or (rarely)
+/// RelativeSeekableReaderWriter, as appropriate.
+///
+/// A ReaderWriter may or may not support seeking. Check seekable() or try
+/// calling Seek() to determine if the stream is seekable.
class ReaderWriter : public Stream {
public:
// ReaderWriters may be used as Readers.
@@ -483,21 +500,21 @@ class ReaderWriter : public Stream {
: Stream(true, true, seekability) {}
};
-// A ReaderWriter that supports at least relative seeking within some range of
-// the current position. Seeking beyond that or from other origins may or may
-// not be supported. The extent to which seeking is possible is NOT exposed by
-// this API.
-//
-// Use in APIs when:
-// * Relative seeking is required. Usage in APIs should be rare; generally
-// ReaderWriter should be used instead.
-//
-// Inherit from when:
-// * Implementing a ReaderWriter that can only support seeking near the
-// current position.
-//
-// A buffered ReaderWriter that only supports seeking within its buffer is a
-// good example of a RelativeSeekableReaderWriter.
+/// A ReaderWriter that supports at least relative seeking within some range of
+/// the current position. Seeking beyond that or from other origins may or may
+/// not be supported. The extent to which seeking is possible is NOT exposed by
+/// this API.
+///
+/// Use in APIs when:
+/// * Relative seeking is required. Usage in APIs should be rare; generally
+/// ReaderWriter should be used instead.
+///
+/// Inherit from when:
+/// * Implementing a ReaderWriter that can only support seeking near the
+/// current position.
+///
+/// A buffered ReaderWriter that only supports seeking within its buffer is a
+/// good example of a RelativeSeekableReaderWriter.
class RelativeSeekableReaderWriter : public ReaderWriter {
public:
// RelativeSeekableReaderWriters may be used as RelativeSeekableReaders or
@@ -528,15 +545,15 @@ class RelativeSeekableReaderWriter : public ReaderWriter {
: ReaderWriter(seekability) {}
};
-// A ReaderWriter that fully supports seeking.
-//
-// Use in APIs when:
-// * Absolute seeking is required. Use ReaderWriter& if seeking is not
-// required or seek failures can be handled gracefully.
-//
-// Inherit from when:
-// * Implementing a writer that supports absolute seeking.
-//
+/// A ReaderWriter that fully supports seeking.
+///
+/// Use in APIs when:
+/// * Absolute seeking is required. Use ReaderWriter& if seeking is not
+/// required or seek failures can be handled gracefully.
+///
+/// Inherit from when:
+/// * Implementing a writer that supports absolute seeking.
+///
class SeekableReaderWriter : public RelativeSeekableReaderWriter {
public:
// SeekableReaderWriters may be used as SeekableReaders.
@@ -572,15 +589,15 @@ class SeekableReaderWriter : public RelativeSeekableReaderWriter {
: RelativeSeekableReaderWriter(Seekability::kAbsolute) {}
};
-// A ReaderWriter that does not support seeking. The Seek() method is hidden.
-//
-// Use in APIs when:
-// * Do NOT use in APIs! If seeking is not required, use ReaderWriter&
-// instead.
-//
-// Inherit from when:
-// * Implementing a ReaderWriter that does not support seeking.
-//
+/// A ReaderWriter that does not support seeking. The Seek() method is hidden.
+///
+/// Use in APIs when:
+/// * Do NOT use in APIs! If seeking is not required, use ReaderWriter&
+/// instead.
+///
+/// Inherit from when:
+/// * Implementing a ReaderWriter that does not support seeking.
+///
class NonSeekableReaderWriter : public ReaderWriter {
public:
// NonSeekableReaderWriters may be used as either NonSeekableReaders or
diff --git a/pw_stream/rust/BUILD.bazel b/pw_stream/rust/BUILD.bazel
new file mode 100644
index 000000000..538efd178
--- /dev/null
+++ b/pw_stream/rust/BUILD.bazel
@@ -0,0 +1,52 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_doc_test", "rust_library", "rust_test")
+
+rust_library(
+ name = "pw_stream",
+ srcs = [
+ "pw_stream/cursor.rs",
+ "pw_stream/integer.rs",
+ "pw_stream/lib.rs",
+ ],
+ crate_features = select({
+ "@rust_crates//:no_std": ["no_std"],
+ "//conditions:default": [""],
+ }),
+ proc_macro_deps = ["@rust_crates//:paste"],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_status/rust:pw_status",
+ "//pw_varint/rust:pw_varint",
+ ],
+)
+
+rust_test(
+ name = "pw_stream_test",
+ crate = ":pw_stream",
+ crate_features = select({
+ "@rust_crates//:no_std": ["no_std"],
+ "//conditions:default": [""],
+ }),
+)
+
+rust_doc_test(
+ name = "pw_stream_doc_test",
+ crate = ":pw_stream",
+)
+
+rust_doc(
+ name = "pw_stream_doc",
+ crate = ":pw_stream",
+)
diff --git a/pw_stream/rust/pw_stream/cursor.rs b/pw_stream/rust/pw_stream/cursor.rs
new file mode 100644
index 000000000..e8462db28
--- /dev/null
+++ b/pw_stream/rust/pw_stream/cursor.rs
@@ -0,0 +1,534 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use core::cmp::min;
+
+use paste::paste;
+use pw_status::{Error, Result};
+use pw_varint::{VarintDecode, VarintEncode};
+
+use super::{Read, Seek, SeekFrom, Write};
+
+/// Wraps an <code>[AsRef]<[u8]></code> in a container implementing
+/// [`Read`], [`Write`], and [`Seek`].
+///
+/// [`Write`] support requires the inner type also implement
+/// <code>[AsMut]<[u8]></code>.
+pub struct Cursor<T>
+where
+ T: AsRef<[u8]>,
+{
+ inner: T,
+ pos: usize,
+}
+
+impl<T: AsRef<[u8]>> Cursor<T> {
+ /// Create a new Cursor wrapping `inner` with an initial position of 0.
+ ///
+ /// Semantics match [`std::io::Cursor::new()`].
+ pub fn new(inner: T) -> Self {
+ Self { inner, pos: 0 }
+ }
+
+ /// Consumes the cursor and returns the inner wrapped data.
+ pub fn into_inner(self) -> T {
+ self.inner
+ }
+
+ /// Returns the number of remaining bytes in the Cursor.
+ pub fn remaining(&self) -> usize {
+ self.len() - self.pos
+ }
+
+ /// Returns the total length of the Cursor.
+ pub fn len(&self) -> usize {
+ self.inner.as_ref().len()
+ }
+
+ /// Returns current IO position of the Cursor.
+ pub fn position(&self) -> usize {
+ self.pos
+ }
+
+ fn remaining_slice(&mut self) -> &[u8] {
+ &self.inner.as_ref()[self.pos..]
+ }
+}
+
+impl<T: AsRef<[u8]> + AsMut<[u8]>> Cursor<T> {
+ fn remaining_mut(&mut self) -> &mut [u8] {
+ &mut self.inner.as_mut()[self.pos..]
+ }
+}
+
+// Implement `read()` as a concrete function to avoid extra monomorphization
+// overhead.
+fn read_impl(inner: &[u8], pos: &mut usize, buf: &mut [u8]) -> Result<usize> {
+ let remaining = inner.len() - *pos;
+ let read_len = min(remaining, buf.len());
+ buf[..read_len].copy_from_slice(&inner[*pos..(*pos + read_len)]);
+ *pos += read_len;
+ Ok(read_len)
+}
+
+impl<T: AsRef<[u8]>> Read for Cursor<T> {
+ fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+ read_impl(self.inner.as_ref(), &mut self.pos, buf)
+ }
+}
+
+// Implement `write()` as a concrete function to avoid extra monomorphization
+// overhead.
+fn write_impl(inner: &mut [u8], pos: &mut usize, buf: &[u8]) -> Result<usize> {
+ let remaining = inner.len() - *pos;
+ let write_len = min(remaining, buf.len());
+ inner[*pos..(*pos + write_len)].copy_from_slice(&buf[0..write_len]);
+ *pos += write_len;
+ Ok(write_len)
+}
+
+impl<T: AsRef<[u8]> + AsMut<[u8]>> Write for Cursor<T> {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ write_impl(self.inner.as_mut(), &mut self.pos, buf)
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ // Cursor does not provide any buffering so flush() is a noop.
+ Ok(())
+ }
+}
+
+impl<T: AsRef<[u8]>> Seek for Cursor<T> {
+ fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
+ let new_pos = match pos {
+ SeekFrom::Start(pos) => pos,
+ SeekFrom::Current(pos) => (self.pos as u64)
+ .checked_add_signed(pos)
+ .ok_or(Error::OutOfRange)?,
+ SeekFrom::End(pos) => (self.len() as u64)
+ .checked_add_signed(-pos)
+ .ok_or(Error::OutOfRange)?,
+ };
+
+ // Since Cursor operates on in memory buffers, it's limited by usize.
+ // Return an error if we are asked to seek beyond that limit.
+ let new_pos: usize = new_pos.try_into().map_err(|_| Error::OutOfRange)?;
+
+ if new_pos > self.len() {
+ Err(Error::OutOfRange)
+ } else {
+ self.pos = new_pos;
+ Ok(new_pos as u64)
+ }
+ }
+
+ // Implement more efficient versions of rewind, stream_len, stream_position.
+ fn rewind(&mut self) -> Result<()> {
+ self.pos = 0;
+ Ok(())
+ }
+
+ fn stream_len(&mut self) -> Result<u64> {
+ Ok(self.len() as u64)
+ }
+
+ fn stream_position(&mut self) -> Result<u64> {
+ Ok(self.pos as u64)
+ }
+}
+
+macro_rules! cursor_read_type_impl {
+ ($ty:ident, $endian:ident) => {
+ paste! {
+ fn [<read_ $ty _ $endian>](&mut self) -> Result<$ty> {
+ const NUM_BYTES: usize = $ty::BITS as usize / 8;
+ if NUM_BYTES > self.remaining() {
+ return Err(Error::OutOfRange);
+ }
+ let sub_slice = self
+ .inner
+ .as_ref()
+ .get(self.pos..self.pos + NUM_BYTES)
+ .ok_or_else(|| Error::InvalidArgument)?;
+ // Because we are code size conscious we want an infallible way to
+ // turn `sub_slice` into a fixed sized array as opposed to using
+ // something like `.try_into()?`.
+ //
+ // Safety: We are both bounds checking and size constraining the
+ // slice in the above lines of code.
+ let sub_array: &[u8; NUM_BYTES] = unsafe { ::core::mem::transmute(sub_slice.as_ptr()) };
+ let value = $ty::[<from_ $endian _bytes>](*sub_array);
+
+ self.pos += NUM_BYTES;
+ Ok(value)
+ }
+ }
+ };
+}
+
+macro_rules! cursor_read_bits_impl {
+ ($bits:literal) => {
+ paste! {
+ cursor_read_type_impl!([<i $bits>], le);
+ cursor_read_type_impl!([<u $bits>], le);
+ cursor_read_type_impl!([<i $bits>], be);
+ cursor_read_type_impl!([<u $bits>], be);
+ }
+ };
+}
+
+macro_rules! cursor_write_type_impl {
+ ($ty:ident, $endian:ident) => {
+ paste! {
+ fn [<write_ $ty _ $endian>](&mut self, value: &$ty) -> Result<()> {
+ const NUM_BYTES: usize = $ty::BITS as usize / 8;
+ if NUM_BYTES > self.remaining() {
+ return Err(Error::OutOfRange);
+ }
+ let value_bytes = $ty::[<to_ $endian _bytes>](*value);
+ let sub_slice = self
+ .inner
+ .as_mut()
+ .get_mut(self.pos..self.pos + NUM_BYTES)
+ .ok_or_else(|| Error::InvalidArgument)?;
+
+ sub_slice.copy_from_slice(&value_bytes[..]);
+
+ self.pos += NUM_BYTES;
+ Ok(())
+ }
+ }
+ };
+}
+
+macro_rules! cursor_write_bits_impl {
+ ($bits:literal) => {
+ paste! {
+ cursor_write_type_impl!([<i $bits>], le);
+ cursor_write_type_impl!([<u $bits>], le);
+ cursor_write_type_impl!([<i $bits>], be);
+ cursor_write_type_impl!([<u $bits>], be);
+ }
+ };
+}
+
+impl<T: AsRef<[u8]>> crate::ReadInteger for Cursor<T> {
+ cursor_read_bits_impl!(8);
+ cursor_read_bits_impl!(16);
+ cursor_read_bits_impl!(32);
+ cursor_read_bits_impl!(64);
+ cursor_read_bits_impl!(128);
+}
+
+impl<T: AsRef<[u8]> + AsMut<[u8]>> crate::WriteInteger for Cursor<T> {
+ cursor_write_bits_impl!(8);
+ cursor_write_bits_impl!(16);
+ cursor_write_bits_impl!(32);
+ cursor_write_bits_impl!(64);
+ cursor_write_bits_impl!(128);
+}
+
+impl<T: AsRef<[u8]>> crate::ReadVarint for Cursor<T> {
+ fn read_varint(&mut self) -> Result<u64> {
+ let (len, value) = u64::varint_decode(self.remaining_slice())?;
+ self.pos += len;
+ Ok(value)
+ }
+
+ fn read_signed_varint(&mut self) -> Result<i64> {
+ let (len, value) = i64::varint_decode(self.remaining_slice())?;
+ self.pos += len;
+ Ok(value)
+ }
+}
+
+impl<T: AsRef<[u8]> + AsMut<[u8]>> crate::WriteVarint for Cursor<T> {
+ fn write_varint(&mut self, value: u64) -> Result<()> {
+ let encoded_len = value.varint_encode(self.remaining_mut())?;
+ self.pos += encoded_len;
+ Ok(())
+ }
+
+ fn write_signed_varint(&mut self, value: i64) -> Result<()> {
+ let encoded_len = value.varint_encode(self.remaining_mut())?;
+ self.pos += encoded_len;
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{test_utils::*, ReadInteger, ReadVarint, WriteInteger, WriteVarint};
+
+ #[test]
+ fn cursor_len_returns_total_bytes() {
+ let cursor = Cursor {
+ inner: &[0u8; 64],
+ pos: 31,
+ };
+ assert_eq!(cursor.len(), 64);
+ }
+
+ #[test]
+ fn cursor_remaining_returns_remaining_bytes() {
+ let cursor = Cursor {
+ inner: &[0u8; 64],
+ pos: 31,
+ };
+ assert_eq!(cursor.remaining(), 33);
+ }
+
+ #[test]
+ fn cursor_position_returns_current_position() {
+ let cursor = Cursor {
+ inner: &[0u8; 64],
+ pos: 31,
+ };
+ assert_eq!(cursor.position(), 31);
+ }
+
+ #[test]
+ fn cursor_read_of_partial_buffer_reads_correct_data() {
+ let mut cursor = Cursor {
+ inner: &[1, 2, 3, 4, 5, 6, 7, 8],
+ pos: 4,
+ };
+ let mut buf = [0u8; 8];
+ assert_eq!(cursor.read(&mut buf), Ok(4));
+ assert_eq!(buf, [5, 6, 7, 8, 0, 0, 0, 0]);
+ }
+
+ #[test]
+ fn cursor_write_of_partial_buffer_writes_correct_data() {
+ let mut cursor = Cursor {
+ inner: &mut [0, 0, 0, 0, 0, 0, 0, 0],
+ pos: 4,
+ };
+ let mut buf = [1, 2, 3, 4, 5, 6, 7, 8];
+ assert_eq!(cursor.write(&mut buf), Ok(4));
+ assert_eq!(cursor.inner, &[0, 0, 0, 0, 1, 2, 3, 4]);
+ }
+
+ #[test]
+ fn cursor_rewind_resets_position_to_zero() {
+ test_rewind_resets_position_to_zero::<64, _>(Cursor::new(&[0u8; 64]));
+ }
+
+ #[test]
+ fn cursor_stream_pos_reports_correct_position() {
+ test_stream_pos_reports_correct_position::<64, _>(Cursor::new(&[0u8; 64]));
+ }
+
+ #[test]
+ fn cursor_stream_len_reports_correct_length() {
+ test_stream_len_reports_correct_length::<64, _>(Cursor::new(&[0u8; 64]));
+ }
+
+ macro_rules! cursor_read_n_bit_integers_unpacks_data_correctly {
+ ($bits:literal) => {
+ paste! {
+ #[test]
+ fn [<cursor_read_ $bits _bit_integers_unpacks_data_correctly>]() {
+ let (bytes, values) = [<integer_ $bits _bit_test_cases>]();
+ let mut cursor = Cursor::new(&bytes);
+
+ assert_eq!(cursor.[<read_i $bits _le>](), Ok(values.0));
+ assert_eq!(cursor.[<read_u $bits _le>](), Ok(values.1));
+ assert_eq!(cursor.[<read_i $bits _be>](), Ok(values.2));
+ assert_eq!(cursor.[<read_u $bits _be>](), Ok(values.3));
+ }
+ }
+ };
+ }
+
+ macro_rules! cursor_write_n_bit_integers_packs_data_correctly {
+ ($bits:literal) => {
+ paste! {
+ #[test]
+ fn [<cursor_write_ $bits _bit_integers_packs_data_correctly>]() {
+ let (expected_bytes, values) = [<integer_ $bits _bit_test_cases>]();
+ let mut cursor = Cursor::new(vec![0u8; expected_bytes.len()]);
+ cursor.[<write_i $bits _le>](&values.0).unwrap();
+ cursor.[<write_u $bits _le>](&values.1).unwrap();
+ cursor.[<write_i $bits _be>](&values.2).unwrap();
+ cursor.[<write_u $bits _be>](&values.3).unwrap();
+
+ let result_bytes: Vec<u8> = cursor.into_inner().into();
+
+ assert_eq!(result_bytes, expected_bytes);
+ }
+ }
+ };
+ }
+
+ fn integer_8_bit_test_cases() -> (Vec<u8>, (i8, u8, i8, u8)) {
+ (
+ vec![
+ 0x0, // le i8
+ 0x1, // le u8
+ 0x2, // be i8
+ 0x3, // be u8
+ ],
+ (0, 1, 2, 3),
+ )
+ }
+
+ cursor_read_n_bit_integers_unpacks_data_correctly!(8);
+ cursor_write_n_bit_integers_packs_data_correctly!(8);
+
+ fn integer_16_bit_test_cases() -> (Vec<u8>, (i16, u16, i16, u16)) {
+ (
+ vec![
+ 0x0, 0x80, // le i16
+ 0x1, 0x80, // le u16
+ 0x80, 0x2, // be i16
+ 0x80, 0x3, // be u16
+ ],
+ (
+ i16::from_le_bytes([0x0, 0x80]),
+ 0x8001,
+ i16::from_be_bytes([0x80, 0x2]),
+ 0x8003,
+ ),
+ )
+ }
+
+ cursor_read_n_bit_integers_unpacks_data_correctly!(16);
+ cursor_write_n_bit_integers_packs_data_correctly!(16);
+
+ fn integer_32_bit_test_cases() -> (Vec<u8>, (i32, u32, i32, u32)) {
+ (
+ vec![
+ 0x0, 0x1, 0x2, 0x80, // le i32
+ 0x3, 0x4, 0x5, 0x80, // le u32
+ 0x80, 0x6, 0x7, 0x8, // be i32
+ 0x80, 0x9, 0xa, 0xb, // be u32
+ ],
+ (
+ i32::from_le_bytes([0x0, 0x1, 0x2, 0x80]),
+ 0x8005_0403,
+ i32::from_be_bytes([0x80, 0x6, 0x7, 0x8]),
+ 0x8009_0a0b,
+ ),
+ )
+ }
+
+ cursor_read_n_bit_integers_unpacks_data_correctly!(32);
+ cursor_write_n_bit_integers_packs_data_correctly!(32);
+
+ fn integer_64_bit_test_cases() -> (Vec<u8>, (i64, u64, i64, u64)) {
+ (
+ vec![
+ 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x80, // le i64
+ 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0x80, // le u64
+ 0x80, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, // be i64
+ 0x80, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, // be u64
+ ],
+ (
+ i64::from_le_bytes([0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x80]),
+ 0x800d_0c0b_0a09_0807,
+ i64::from_be_bytes([0x80, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16]),
+ 0x8017_1819_1a1b_1c1d,
+ ),
+ )
+ }
+
+ cursor_read_n_bit_integers_unpacks_data_correctly!(64);
+ cursor_write_n_bit_integers_packs_data_correctly!(64);
+
+ fn integer_128_bit_test_cases() -> (Vec<u8>, (i128, u128, i128, u128)) {
+ #[rustfmt::skip]
+ let val = (
+ vec![
+ // le i128
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x8f,
+ // le u128
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x8f,
+ // be i128
+ 0x80, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
+ 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+ // be u128
+ 0x80, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
+ 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
+ ],
+ (
+ i128::from_le_bytes([
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+ 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x8f,
+ ]),
+ 0x8f1e_1d1c_1b1a_1918_1716_1514_1312_1110,
+ i128::from_be_bytes([
+ 0x80, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
+ 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+ ]),
+ 0x8031_3233_3435_3637_3839_3a3b_3c3d_3e3f,
+ ),
+ );
+ val
+ }
+
+ cursor_read_n_bit_integers_unpacks_data_correctly!(128);
+ cursor_write_n_bit_integers_packs_data_correctly!(128);
+
+ #[test]
+ pub fn read_varint_unpacks_data_correctly() {
+ let mut cursor = Cursor::new(vec![0xfe, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+ let value = cursor.read_varint().unwrap();
+ assert_eq!(value, 0xffff_fffe);
+
+ let mut cursor = Cursor::new(vec![0xff, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+ let value = cursor.read_varint().unwrap();
+ assert_eq!(value, 0xffff_ffff);
+ }
+
+ #[test]
+ pub fn read_signed_varint_unpacks_data_correctly() {
+ let mut cursor = Cursor::new(vec![0xfe, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+ let value = cursor.read_signed_varint().unwrap();
+ assert_eq!(value, i32::MAX.into());
+
+ let mut cursor = Cursor::new(vec![0xff, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+ let value = cursor.read_signed_varint().unwrap();
+ assert_eq!(value, i32::MIN.into());
+ }
+
+ #[test]
+ pub fn write_varint_packs_data_correctly() {
+ let mut cursor = Cursor::new(vec![0u8; 8]);
+ cursor.write_varint(0xffff_fffe).unwrap();
+ let buf = cursor.into_inner();
+ assert_eq!(buf, vec![0xfe, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+
+ let mut cursor = Cursor::new(vec![0u8; 8]);
+ cursor.write_varint(0xffff_ffff).unwrap();
+ let buf = cursor.into_inner();
+ assert_eq!(buf, vec![0xff, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+ }
+
+ #[test]
+ pub fn write_signed_varint_packs_data_correctly() {
+ let mut cursor = Cursor::new(vec![0u8; 8]);
+ cursor.write_signed_varint(i32::MAX.into()).unwrap();
+ let buf = cursor.into_inner();
+ assert_eq!(buf, vec![0xfe, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+
+ let mut cursor = Cursor::new(vec![0u8; 8]);
+ cursor.write_signed_varint(i32::MIN.into()).unwrap();
+ let buf = cursor.into_inner();
+ assert_eq!(buf, vec![0xff, 0xff, 0xff, 0xff, 0x0f, 0x0, 0x0, 0x0]);
+ }
+}
diff --git a/pw_stream/rust/pw_stream/integer.rs b/pw_stream/rust/pw_stream/integer.rs
new file mode 100644
index 000000000..948074193
--- /dev/null
+++ b/pw_stream/rust/pw_stream/integer.rs
@@ -0,0 +1,366 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use pw_status::Result;
+
+/// A trait for reading integers from a stream.
+///
+/// Allows reading signed and unsigned integers from 8 to 128 bits in
+/// either big or little endian byte order.
+///
+/// # Example
+///
+/// ```
+/// use pw_stream::{Cursor, ReadInteger};
+///
+/// let mut cursor = Cursor::new([0x3, 0x4, 0x5, 0x80]);
+/// let value = cursor.read_u32_le().unwrap();
+/// assert_eq!(value, 0x8005_0403);
+/// ```
+///
+/// # Future Work
+///
+/// In order to allow for optimized non-generic implementations, there is
+/// no blanket implementation over the [`crate::Read`] trait. An `IntegerReader`
+/// adapter could be written to allow this functionality.
+pub trait ReadInteger {
+ /// Reads a little-endian i8 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i8_le(&mut self) -> Result<i8>;
+
+ /// Reads a little-endian u8 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u8_le(&mut self) -> Result<u8>;
+
+ /// Reads a big-endian i8 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i8_be(&mut self) -> Result<i8>;
+
+ /// Reads a big-endian u8 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u8_be(&mut self) -> Result<u8>;
+
+ /// Reads a little-endian i16 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i16_le(&mut self) -> Result<i16>;
+
+ /// Reads a little-endian u16 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ ///
+ fn read_u16_le(&mut self) -> Result<u16>;
+
+ /// Reads a big-endian i16 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i16_be(&mut self) -> Result<i16>;
+
+ /// Reads a big-endian u16 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ ///
+ fn read_u16_be(&mut self) -> Result<u16>;
+
+ /// Reads a little-endian i32 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i32_le(&mut self) -> Result<i32>;
+
+ /// Reads a little-endian u32 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u32_le(&mut self) -> Result<u32>;
+
+ /// Reads a big-endian i32 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i32_be(&mut self) -> Result<i32>;
+
+ /// Reads a big-endian u32 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u32_be(&mut self) -> Result<u32>;
+
+ /// Reads a little-endian i64 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i64_le(&mut self) -> Result<i64>;
+
+ /// Reads a little-endian u64 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u64_le(&mut self) -> Result<u64>;
+
+ /// Reads a big-endian i64 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i64_be(&mut self) -> Result<i64>;
+
+ /// Reads a big-endian u64 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u64_be(&mut self) -> Result<u64>;
+
+ /// Reads a little-endian i128 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i128_le(&mut self) -> Result<i128>;
+
+ /// Reads a little-endian u128 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u128_le(&mut self) -> Result<u128>;
+
+ /// Reads a big-endian i128 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_i128_be(&mut self) -> Result<i128>;
+
+ /// Reads a big-endian u128 returning it's value or an Error.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_u128_be(&mut self) -> Result<u128>;
+}
+
+/// A trait for writing integers toa stream.
+///
+/// Allows writing signed and unsigned integers from 8 to 128 bits in
+/// either big or little endian byte order.
+///
+/// # Example
+///
+/// ```
+/// use pw_stream::{Cursor, WriteInteger};
+///
+/// let mut cursor = Cursor::new([0u8; 8]);
+/// cursor.write_u32_le(&0x8005_0403).unwrap();
+/// let buffer = cursor.into_inner();
+/// assert_eq!(buffer, [0x3, 0x4, 0x5, 0x80, 0x0, 0x0, 0x0, 0x0]);
+/// ```
+///
+/// # Future Work
+///
+/// In order to allow for optimized non-generic implementations, there is
+/// no blanket implementation over the [`crate::Write`] trait. An `IntegerWriter`
+/// adapter could be written to allow this functionality.
+pub trait WriteInteger {
+ /// Writes a little-endian i8.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i8_le(&mut self, value: &i8) -> Result<()>;
+
+ /// Writes a little-endian u8.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u8_le(&mut self, value: &u8) -> Result<()>;
+
+ /// Writes a big-endian i8.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i8_be(&mut self, value: &i8) -> Result<()>;
+
+ /// Writes a big-endian u8.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u8_be(&mut self, value: &u8) -> Result<()>;
+
+ /// Writes a little-endian i16.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i16_le(&mut self, value: &i16) -> Result<()>;
+
+ /// Writes a little-endian u16.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u16_le(&mut self, value: &u16) -> Result<()>;
+
+ /// Writes a big-endian i16.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i16_be(&mut self, value: &i16) -> Result<()>;
+
+ /// Writes a big-endian u16.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u16_be(&mut self, value: &u16) -> Result<()>;
+
+ /// Writes a little-endian i32.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i32_le(&mut self, value: &i32) -> Result<()>;
+
+ /// Writes a little-endian u32.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u32_le(&mut self, value: &u32) -> Result<()>;
+
+ /// Writes a big-endian i32.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i32_be(&mut self, value: &i32) -> Result<()>;
+
+ /// Writes a big-endian u32.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u32_be(&mut self, value: &u32) -> Result<()>;
+
+ /// Writes a little-endian i64.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i64_le(&mut self, value: &i64) -> Result<()>;
+
+ /// Writes a little-endian u64.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u64_le(&mut self, value: &u64) -> Result<()>;
+
+ /// Writes a big-endian i64.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i64_be(&mut self, value: &i64) -> Result<()>;
+
+ /// Writes a big-endian u64.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u64_be(&mut self, value: &u64) -> Result<()>;
+
+ /// Writes a little-endian i128.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i128_le(&mut self, value: &i128) -> Result<()>;
+
+ /// Writes a little-endian u128.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u128_le(&mut self, value: &u128) -> Result<()>;
+
+ /// Writes a big-endian i128.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_i128_be(&mut self, value: &i128) -> Result<()>;
+
+ /// Writes a big-endian u128.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_u128_be(&mut self, value: &u128) -> Result<()>;
+}
+
+/// A trait for reading varint integers from a stream.
+///
+/// The API here is explicitly limited to `u64` and `i64` in order to reduce
+/// code size.
+///
+/// # Example
+///
+/// ```
+/// use pw_stream::{Cursor, ReadVarint};
+///
+/// let mut cursor = Cursor::new(vec![0xfe, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff, 0xff, 0x0f]);
+/// let unsigned_value = cursor.read_varint().unwrap();
+/// let signed_value = cursor.read_signed_varint().unwrap();
+/// assert_eq!(unsigned_value, 0xffff_fffe);
+/// assert_eq!(signed_value, i32::MIN.into());
+/// ```
+pub trait ReadVarint {
+ /// Read an unsigned varint from the stream.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_varint(&mut self) -> Result<u64>;
+
+ /// Read a signed varint from the stream.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn read_signed_varint(&mut self) -> Result<i64>;
+}
+
+/// A trait for writing varint integers from a stream.
+///
+/// The API here is explicitly limited to `u64` and `i64` in order to reduce
+/// code size.
+///
+/// # Example
+///
+/// ```
+/// use pw_stream::{Cursor, WriteVarint};
+///
+/// let mut cursor = Cursor::new(vec![0x0; 16]);
+///
+/// cursor.write_varint(0xffff_fffe).unwrap();
+/// cursor.write_signed_varint(i32::MIN.into()).unwrap();
+///
+/// let buffer = cursor.into_inner();
+/// assert_eq!(buffer,vec![
+/// 0xfe, 0xff, 0xff, 0xff, 0x0f, 0xff, 0xff, 0xff,
+/// 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
+/// ```
+pub trait WriteVarint {
+ /// Write an unsigned varint to the stream.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_varint(&mut self, value: u64) -> Result<()>;
+
+ /// Write a signed varint to the stream.
+ ///
+ /// Errors that may be returned are dependant on the underlying
+ /// implementation.
+ fn write_signed_varint(&mut self, value: i64) -> Result<()>;
+}
diff --git a/pw_stream/rust/pw_stream/lib.rs b/pw_stream/rust/pw_stream/lib.rs
new file mode 100644
index 000000000..ba60c230f
--- /dev/null
+++ b/pw_stream/rust/pw_stream/lib.rs
@@ -0,0 +1,360 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! `pw_stream` provides `no_std` versions of Rust's [`std::io::Read`],
+//! [`std::io::Write`], and [`std::io::Seek`] traits as well as a simplified
+//! version of [`std::io::Cursor`]. One notable difference is that
+//! [`pw_status::Error`] is used to avoid needing to do error conversion or
+//! encapsulation.
+#![deny(missing_docs)]
+// Allows docs to reference `std`
+#![cfg_attr(feature = "no_std", no_std)]
+
+use pw_status::{Error, Result};
+
+#[doc(hidden)]
+mod cursor;
+mod integer;
+
+pub use cursor::Cursor;
+pub use integer::{ReadInteger, ReadVarint, WriteInteger, WriteVarint};
+
+/// A trait for objects that provide streaming read capability.
+pub trait Read {
+ /// Read from a stream into a buffer.
+ ///
+ /// Semantics match [`std::io::Read::read()`].
+ fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
+
+ /// Read exactly enough bytes to fill the buffer.
+ ///
+ /// Semantics match [`std::io::Read::read_exact()`].
+ fn read_exact(&mut self, mut buf: &mut [u8]) -> Result<()> {
+ while !buf.is_empty() {
+ let len = self.read(buf)?;
+
+ // End of stream
+ if len == 0 {
+ break;
+ }
+
+ buf = &mut buf[len..];
+ }
+
+ if !buf.is_empty() {
+ Err(Error::OutOfRange)
+ } else {
+ Ok(())
+ }
+ }
+}
+
+/// A trait for objects that provide streaming write capability.
+pub trait Write {
+ /// Write a buffer to a stream.
+ ///
+ /// Semantics match [`std::io::Write::write()`].
+ fn write(&mut self, buf: &[u8]) -> Result<usize>;
+
+ /// Commit any outstanding buffered writes to underlying storage.
+ ///
+ /// Semantics match [`std::io::Write::flush()`].
+ fn flush(&mut self) -> Result<()>;
+
+ /// Writes entire buffer to stream.
+ ///
+ /// Semantics match [`std::io::Write::write_all()`].
+ fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
+ while !buf.is_empty() {
+ let len = self.write(buf)?;
+
+ // End of stream
+ if len == 0 {
+ break;
+ }
+
+ buf = &buf[len..];
+ }
+
+ if !buf.is_empty() {
+ Err(Error::OutOfRange)
+ } else {
+ Ok(())
+ }
+ }
+}
+
+/// A description of a seek operation in a stream.
+///
+/// While `pw_stream` targets embedded platforms which are often natively
+/// 32 bit, we believe that seek operation are relatively rare and the added
+/// overhead of using 64 bit values for seeks is balanced by the ability
+/// to support objects and operations over 4 GiB.
+pub enum SeekFrom {
+ /// Seek from the start of the stream.
+ Start(u64),
+
+ /// Seek from the end of the stream.
+ End(i64),
+
+ /// Seek from the current position of the stream.
+ Current(i64),
+}
+
+/// A trait for objects that provide the ability to seek withing a stream.
+pub trait Seek {
+ /// Adjust the current position of the stream.
+ ///
+ /// Semantics match [`std::io::Seek::seek()`].
+ fn seek(&mut self, pos: SeekFrom) -> Result<u64>;
+
+ /// Set the current position of the stream to its beginning.
+ ///
+ /// Semantics match [`std::io::Seek::rewind()`].
+ fn rewind(&mut self) -> Result<()> {
+ self.seek(SeekFrom::Start(0)).map(|_| ())
+ }
+
+ /// Returns the length of the stream.
+ ///
+ /// Semantics match [`std::io::Seek::stream_len()`].
+ fn stream_len(&mut self) -> Result<u64> {
+ // Save original position.
+ let orig_pos = self.seek(SeekFrom::Current(0))?;
+
+ // Seed to the end to discover stream length.
+ let end_pos = self.seek(SeekFrom::End(0))?;
+
+ // Go back to original position.
+ self.seek(SeekFrom::Start(orig_pos))?;
+
+ Ok(end_pos)
+ }
+
+ /// Returns the current position of the stream.
+ ///
+ /// Semantics match [`std::io::Seek::stream_position()`].
+ fn stream_position(&mut self) -> Result<u64> {
+ self.seek(SeekFrom::Current(0))
+ }
+}
+
+#[cfg(test)]
+pub(crate) mod test_utils {
+ use super::{Seek, SeekFrom};
+
+ pub(crate) fn test_rewind_resets_position_to_zero<const LEN: u64, T: Seek>(mut seeker: T) {
+ seeker.seek(SeekFrom::Current(LEN as i64 / 2)).unwrap();
+ assert_eq!(seeker.stream_position().unwrap(), LEN / 2);
+ seeker.rewind().unwrap();
+ assert_eq!(seeker.stream_position().unwrap(), 0);
+ }
+
+ pub(crate) fn test_stream_pos_reports_correct_position<const LEN: u64, T: Seek>(mut seeker: T) {
+ assert_eq!(seeker.stream_position().unwrap(), 0);
+ seeker.seek(SeekFrom::Current(1)).unwrap();
+ assert_eq!(seeker.stream_position().unwrap(), 1);
+ seeker.seek(SeekFrom::Current(LEN as i64 / 2 - 1)).unwrap();
+ assert_eq!(seeker.stream_position().unwrap(), LEN / 2);
+ seeker.seek(SeekFrom::Current(0)).unwrap();
+ assert_eq!(seeker.stream_position().unwrap(), LEN / 2);
+ seeker.seek(SeekFrom::End(0)).unwrap();
+ assert_eq!(seeker.stream_position().unwrap(), LEN);
+ }
+
+ pub(crate) fn test_stream_len_reports_correct_length<const LEN: u64, T: Seek>(mut seeker: T) {
+ assert_eq!(seeker.stream_len().unwrap(), LEN);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use core::cmp::min;
+
+ use pw_status::Error;
+
+ use super::test_utils::*;
+ use super::*;
+
+ struct TestSeeker {
+ len: u64,
+ pos: u64,
+ }
+
+ impl Seek for TestSeeker {
+ fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
+ let new_pos = match pos {
+ SeekFrom::Start(pos) => pos,
+ SeekFrom::Current(pos) => {
+ self.pos.checked_add_signed(pos).ok_or(Error::OutOfRange)?
+ }
+ SeekFrom::End(pos) => self.len.checked_add_signed(-pos).ok_or(Error::OutOfRange)?,
+ };
+
+ if new_pos > self.len {
+ Err(Error::OutOfRange)
+ } else {
+ self.pos = new_pos;
+ Ok(new_pos)
+ }
+ }
+ }
+
+ // A stream wrapper that limits reads and writes to a maximum chunk size.
+ struct ChunkedStreamAdapter<S: Read + Write + Seek> {
+ inner: S,
+ chunk_size: usize,
+ num_reads: u32,
+ num_writes: u32,
+ }
+
+ impl<S: Read + Write + Seek> ChunkedStreamAdapter<S> {
+ fn new(inner: S, chunk_size: usize) -> Self {
+ Self {
+ inner,
+ chunk_size,
+ num_reads: 0,
+ num_writes: 0,
+ }
+ }
+ }
+
+ impl<S: Read + Write + Seek> Read for ChunkedStreamAdapter<S> {
+ fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
+ let read_len = min(self.chunk_size, buf.len());
+ self.num_reads += 1;
+ self.inner.read(&mut buf[..read_len])
+ }
+ }
+
+ impl<S: Read + Write + Seek> Write for ChunkedStreamAdapter<S> {
+ fn write(&mut self, buf: &[u8]) -> Result<usize> {
+ let write_len = min(self.chunk_size, buf.len());
+ self.num_writes += 1;
+ self.inner.write(&buf[..write_len])
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ self.inner.flush()
+ }
+ }
+
+ struct ErrorStream {
+ error: Error,
+ }
+
+ impl Read for ErrorStream {
+ fn read(&mut self, _buf: &mut [u8]) -> Result<usize> {
+ Err(self.error)
+ }
+ }
+
+ impl Write for ErrorStream {
+ fn write(&mut self, _buf: &[u8]) -> Result<usize> {
+ Err(self.error)
+ }
+
+ fn flush(&mut self) -> Result<()> {
+ Err(self.error)
+ }
+ }
+
+ #[test]
+ fn default_rewind_impl_resets_position_to_zero() {
+ test_rewind_resets_position_to_zero::<64, _>(TestSeeker { len: 64, pos: 0 });
+ }
+
+ #[test]
+ fn default_stream_pos_impl_reports_correct_position() {
+ test_stream_pos_reports_correct_position::<64, _>(TestSeeker { len: 64, pos: 0 });
+ }
+
+ #[test]
+ fn default_stream_len_impl_reports_correct_length() {
+ test_stream_len_reports_correct_length::<64, _>(TestSeeker { len: 64, pos: 32 });
+ }
+
+ #[test]
+ fn read_exact_reads_full_buffer_on_short_reads() {
+ let cursor = Cursor::new((0x0..=0xff).collect::<Vec<u8>>());
+ // Limit reads to 10 bytes per read.
+ let mut wrapper = ChunkedStreamAdapter::new(cursor, 10);
+ let mut read_buffer = vec![0u8; 256];
+
+ wrapper.read_exact(&mut read_buffer).unwrap();
+
+ // Ensure that the correct bytes were read.
+ assert_eq!(wrapper.inner.into_inner(), read_buffer);
+
+ // Verify that the read was broken up into the correct number of reads.
+ assert_eq!(wrapper.num_reads, 26);
+ }
+
+ #[test]
+ fn read_exact_returns_error_on_too_little_data() {
+ let cursor = Cursor::new((0x0..=0x7f).collect::<Vec<u8>>());
+ // Limit reads to 10 bytes per read.
+ let mut wrapper = ChunkedStreamAdapter::new(cursor, 10);
+ let mut read_buffer = vec![0u8; 256];
+
+ assert_eq!(wrapper.read_exact(&mut read_buffer), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn read_exact_propagates_read_errors() {
+ let mut error_stream = ErrorStream {
+ error: Error::Internal,
+ };
+ let mut read_buffer = vec![0u8; 256];
+ assert_eq!(
+ error_stream.read_exact(&mut read_buffer),
+ Err(Error::Internal)
+ );
+ }
+
+ #[test]
+ fn write_all_writes_full_buffer_on_short_writes() {
+ let cursor = Cursor::new(vec![0u8; 256]);
+ // Limit writes to 10 bytes per write.
+ let mut wrapper = ChunkedStreamAdapter::new(cursor, 10);
+ let write_buffer = (0x0..=0xff).collect::<Vec<u8>>();
+
+ wrapper.write_all(&write_buffer).unwrap();
+
+ // Ensure that the correct bytes were written.
+ assert_eq!(wrapper.inner.into_inner(), write_buffer);
+
+ // Verify that the write was broken up into the correct number of writes.
+ assert_eq!(wrapper.num_writes, 26);
+ }
+
+ #[test]
+ fn write_all_returns_error_on_too_little_data() {
+ let cursor = Cursor::new(vec![0u8; 128]);
+ // Limit writes to 10 bytes per write.
+ let mut wrapper = ChunkedStreamAdapter::new(cursor, 10);
+ let write_buffer = (0x0..=0xff).collect::<Vec<u8>>();
+
+ assert_eq!(wrapper.write_all(&write_buffer), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn write_all_propagates_write_errors() {
+ let mut error_stream = ErrorStream {
+ error: Error::Internal,
+ };
+ let write_buffer = (0x0..=0xff).collect::<Vec<u8>>();
+ assert_eq!(error_stream.write_all(&write_buffer), Err(Error::Internal));
+ }
+}
diff --git a/pw_stream/socket_stream.cc b/pw_stream/socket_stream.cc
index 3978e49ab..b3125439c 100644
--- a/pw_stream/socket_stream.cc
+++ b/pw_stream/socket_stream.cc
@@ -14,25 +14,32 @@
#include "pw_stream/socket_stream.h"
+#if defined(_WIN32) && _WIN32
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#define SHUT_RDWR SD_BOTH
+#else
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
+#endif // defined(_WIN32) && _WIN32
#include <cerrno>
#include <cstring>
#include "pw_assert/check.h"
#include "pw_log/log.h"
+#include "pw_status/status.h"
#include "pw_string/to_string.h"
namespace pw::stream {
namespace {
constexpr uint32_t kServerBacklogLength = 1;
-constexpr const char* kLocalhostAddress = "127.0.0.1";
+constexpr const char* kLocalhostAddress = "localhost";
// Set necessary options on a socket file descriptor.
void ConfigureSocket([[maybe_unused]] int socket) {
@@ -46,62 +53,30 @@ void ConfigureSocket([[maybe_unused]] int socket) {
#endif // defined(__APPLE__)
}
-} // namespace
-
-// TODO(b/240982565): Implement SocketStream for Windows.
+#if defined(_WIN32) && _WIN32
+int close(SOCKET s) { return closesocket(s); }
-// Listen to the port and return after a client is connected
-Status SocketStream::Serve(uint16_t port) {
- listen_port_ = port;
- socket_fd_ = socket(AF_INET, SOCK_STREAM, 0);
- if (socket_fd_ == kInvalidFd) {
- PW_LOG_ERROR("Failed to create socket: %s", std::strerror(errno));
- return Status::Unknown();
+class WinsockInitializer {
+ public:
+ WinsockInitializer() {
+ WSADATA data = {};
+ PW_CHECK_INT_EQ(
+ WSAStartup(MAKEWORD(2, 2), &data), 0, "Failed to initialize winsock");
}
-
- struct sockaddr_in addr = {};
- addr.sin_family = AF_INET;
- addr.sin_port = htons(listen_port_);
- addr.sin_addr.s_addr = INADDR_ANY;
-
- // Configure the socket to allow reusing the address. Closing a socket does
- // not immediately close it. Instead, the socket waits for some period of time
- // before it is actually closed. Setting SO_REUSEADDR allows this socket to
- // bind to an address that may still be in use by a recently closed socket.
- // Without this option, running a program multiple times in series may fail
- // unexpectedly.
- constexpr int value = 1;
-
- if (setsockopt(socket_fd_, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(int)) <
- 0) {
- PW_LOG_WARN("Failed to set SO_REUSEADDR: %s", std::strerror(errno));
+ ~WinsockInitializer() {
+ // TODO: b/301545011 - This currently fails, probably a cleanup race.
+ WSACleanup();
}
+};
- if (bind(socket_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
- PW_LOG_ERROR("Failed to bind socket to localhost:%hu: %s",
- listen_port_,
- std::strerror(errno));
- return Status::Unknown();
- }
+[[maybe_unused]] WinsockInitializer initializer;
- if (listen(socket_fd_, kServerBacklogLength) < 0) {
- PW_LOG_ERROR("Failed to listen to socket: %s", std::strerror(errno));
- return Status::Unknown();
- }
+#endif // defined(_WIN32) && _WIN32
- socklen_t len = sizeof(sockaddr_client_);
-
- connection_fd_ =
- accept(socket_fd_, reinterpret_cast<sockaddr*>(&sockaddr_client_), &len);
- if (connection_fd_ < 0) {
- return Status::Unknown();
- }
- ConfigureSocket(connection_fd_);
- return OkStatus();
-}
+} // namespace
Status SocketStream::SocketStream::Connect(const char* host, uint16_t port) {
- if (host == nullptr || std::strcmp(host, "localhost") == 0) {
+ if (host == nullptr) {
host = kLocalhostAddress;
}
@@ -111,35 +86,41 @@ Status SocketStream::SocketStream::Connect(const char* host, uint16_t port) {
PW_CHECK(ToString(port, port_buffer).ok());
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
- hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV | AI_PASSIVE;
+ hints.ai_flags = AI_NUMERICSERV;
if (getaddrinfo(host, port_buffer, &hints, &res) != 0) {
PW_LOG_ERROR("Failed to configure connection address for socket");
return Status::InvalidArgument();
}
- connection_fd_ = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
- ConfigureSocket(connection_fd_);
- if (connect(connection_fd_, res->ai_addr, res->ai_addrlen) < 0) {
- close(connection_fd_);
- connection_fd_ = kInvalidFd;
+ struct addrinfo* rp;
+ for (rp = res; rp != nullptr; rp = rp->ai_next) {
+ connection_fd_ = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
+ if (connection_fd_ != kInvalidFd) {
+ break;
+ }
}
- freeaddrinfo(res);
if (connection_fd_ == kInvalidFd) {
+ PW_LOG_ERROR("Failed to create a socket: %s", std::strerror(errno));
+ freeaddrinfo(res);
+ return Status::Unknown();
+ }
+
+ ConfigureSocket(connection_fd_);
+ if (connect(connection_fd_, rp->ai_addr, rp->ai_addrlen) == -1) {
+ close(connection_fd_);
+ connection_fd_ = kInvalidFd;
PW_LOG_ERROR(
"Failed to connect to %s:%d: %s", host, port, std::strerror(errno));
+ freeaddrinfo(res);
return Status::Unknown();
}
+ freeaddrinfo(res);
return OkStatus();
}
void SocketStream::Close() {
- if (socket_fd_ != kInvalidFd) {
- close(socket_fd_);
- socket_fd_ = kInvalidFd;
- }
-
if (connection_fd_ != kInvalidFd) {
close(connection_fd_);
connection_fd_ = kInvalidFd;
@@ -154,8 +135,10 @@ Status SocketStream::DoWrite(span<const std::byte> data) {
send_flags |= MSG_NOSIGNAL;
#endif // defined(__linux__)
- ssize_t bytes_sent =
- send(connection_fd_, data.data(), data.size_bytes(), send_flags);
+ ssize_t bytes_sent = send(connection_fd_,
+ reinterpret_cast<const char*>(data.data()),
+ data.size_bytes(),
+ send_flags);
if (bytes_sent < 0 || static_cast<size_t>(bytes_sent) != data.size()) {
if (errno == EPIPE) {
@@ -170,8 +153,15 @@ Status SocketStream::DoWrite(span<const std::byte> data) {
}
StatusWithSize SocketStream::DoRead(ByteSpan dest) {
- ssize_t bytes_rcvd = recv(connection_fd_, dest.data(), dest.size_bytes(), 0);
+ ssize_t bytes_rcvd = recv(connection_fd_,
+ reinterpret_cast<char*>(dest.data()),
+ dest.size_bytes(),
+ 0);
if (bytes_rcvd == 0) {
+ // Remote peer has closed the connection.
+ Close();
+ return StatusWithSize::OutOfRange();
+ } else if (bytes_rcvd < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Socket timed out when trying to read.
// This should only occur if SO_RCVTIMEO was configured to be nonzero, or
@@ -179,10 +169,6 @@ StatusWithSize SocketStream::DoRead(ByteSpan dest) {
// blocking when performing reads or writes.
return StatusWithSize::ResourceExhausted();
}
- // Remote peer has closed the connection.
- Close();
- return StatusWithSize::OutOfRange();
- } else if (bytes_rcvd < 0) {
return StatusWithSize::Unknown();
}
return StatusWithSize(bytes_rcvd);
@@ -199,7 +185,11 @@ Status ServerSocket::Listen(uint16_t port) {
// Allow binding to an address that may still be in use by a closed socket.
constexpr int value = 1;
- setsockopt(socket_fd_, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(int));
+ setsockopt(socket_fd_,
+ SOL_SOCKET,
+ SO_REUSEADDR,
+ reinterpret_cast<const char*>(&value),
+ sizeof(int));
if (port != 0) {
struct sockaddr_in6 addr = {};
@@ -221,7 +211,7 @@ Status ServerSocket::Listen(uint16_t port) {
socklen_t addr_len = sizeof(addr);
if (getsockname(socket_fd_, reinterpret_cast<sockaddr*>(&addr), &addr_len) <
0 ||
- addr_len > sizeof(addr)) {
+ static_cast<size_t>(addr_len) > sizeof(addr)) {
close(socket_fd_);
return Status::Unknown();
}
diff --git a/pw_stream_shmem_mcuxpresso/BUILD.bazel b/pw_stream_shmem_mcuxpresso/BUILD.bazel
new file mode 100644
index 000000000..9d9a307a1
--- /dev/null
+++ b/pw_stream_shmem_mcuxpresso/BUILD.bazel
@@ -0,0 +1,48 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_stream_shmem_mcuxpresso",
+ srcs = ["stream.cc"],
+ hdrs = ["public/pw_stream_shmem_mcuxpresso/stream.h"],
+ includes = ["public"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [
+ "//pw_preprocessor",
+ "//pw_stream",
+ "//pw_sync:binary_semaphore",
+ "@pigweed//targets:mcuxpresso_sdk",
+ ],
+)
+
+pw_cc_test(
+ name = "stream_test",
+ srcs = ["stream_test.cc"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [":pw_stream_shmem_mcuxpresso"],
+)
diff --git a/pw_stream_shmem_mcuxpresso/BUILD.gn b/pw_stream_shmem_mcuxpresso/BUILD.gn
new file mode 100644
index 000000000..8178d0069
--- /dev/null
+++ b/pw_stream_shmem_mcuxpresso/BUILD.gn
@@ -0,0 +1,59 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+if (pw_third_party_mcuxpresso_SDK != "") {
+ pw_source_set("pw_stream_shmem_mcuxpresso") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_stream_shmem_mcuxpresso/stream.h" ]
+ public_deps = [
+ "$dir_pw_stream",
+ "$dir_pw_sync:binary_semaphore",
+ ]
+ deps = [ pw_third_party_mcuxpresso_SDK ]
+ sources = [ "stream.cc" ]
+ }
+}
+
+pw_test("stream_test") {
+ enable_if =
+ pw_third_party_mcuxpresso_SDK == "//targets/mimxrt595_evk_freertos:sdk" &&
+ (pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_debug" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_size_optimized" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_speed_optimized")
+ sources = [ "stream_test.cc" ]
+
+ deps = [
+ ":pw_stream_shmem_mcuxpresso",
+ "//targets/mimxrt595_evk_freertos:sdk",
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":stream_test" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_stream_shmem_mcuxpresso/docs.rst b/pw_stream_shmem_mcuxpresso/docs.rst
new file mode 100644
index 000000000..570f3f5a8
--- /dev/null
+++ b/pw_stream_shmem_mcuxpresso/docs.rst
@@ -0,0 +1,128 @@
+.. _module-pw_stream_shmem_mcuxpresso:
+
+==========================
+pw_stream_shmem_mcuxpresso
+==========================
+``pw_stream_shmem_mcuxpresso`` implements the ``pw_stream`` interface for
+reading and writing between two different processor cores via shared memory
+using the NXP MCUXpresso SDK. It uses the messaging unit module (MU) to signal
+data readiness between cores.
+
+Setup
+=====
+This module requires a little setup:
+
+1. Use ``pw_build_mcuxpresso`` to create a ``pw_source_set`` for an
+ MCUXpresso SDK.
+2. Include the debug console component in this SDK definition.
+3. Specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
+ the name of this source set.
+
+The name of the SDK source set must be set in the
+"pw_third_party_mcuxpresso_SDK" GN arg
+
+Usage
+=====
+``ShmemMcuxpressoStream`` blocks on both reads and writes, as only one
+outstanding buffer can be transferred at a time in each direction. This means a
+dedicated thread should be used for both reading and writing. A typical use case
+for this class would be as the underlying transport for a pw_rpc network between
+cores. Use with the ``pw::rpc::StreamRpcFrameSender`` and
+``pw::rpc::StreamRpcDispatcher`` classes.
+
+Interrupt handlers and shared buffers on both cores must be setup before using
+this stream. The shared buffer must be mapped as uncacheable on both sides.
+
+As an example on the RT595, we connect the M33 core to the FusionF1 DSP. On the
+FusionF1 side, the MU interrupt must be explicitly routed.
+
+Initialization for the M33 Core:
+
+.. code-block:: cpp
+
+ // `kSharedBuffer` is a pointer to memory that is shared between the M33 and
+ // F1 cores, and it is at least `2 * kSharedBufferSize` in size.
+ ByteSpan read_buffer = ByteSpan{kSharedBuffer, kSharedBufferSize};
+ ByteSpan write_buffer = ByteSpan{kSharedBuffer + kSharedBufferSize, kSharedBufferSize};
+ ShmemMcuxpressoStream stream{MUA, read_buffer, write_buffer};
+
+ PW_EXTERN_C void MU_A_DriverIRQHandler() {
+ stream.HandleInterrupt();
+ }
+
+ void Init() {
+ return stream.Enable();
+ }
+
+Initialization for the FusionF1 Core:
+
+.. code-block:: cpp
+
+ ByteSpan write_buffer = ByteSpan{kSharedBuffer, kSharedBufferSize};
+ ByteSpan read_buffer = ByteSpan{kSharedBuffer + kSharedBufferSize, kSharedBufferSize};
+ ShmemMcuxpressoStream stream{MUB, read_buffer, write_buffer};
+
+ PW_EXTERN_C void MU_B_IrqHandler(void*) {
+ stream.HandleInterrupt();
+ }
+
+ void Init() {
+ // Enables the clock for the Input Mux
+ INPUTMUX_Init(INPUTMUX);
+ // MUB interrupt signal is selected for DSP interrupt input 1
+ INPUTMUX_AttachSignal(INPUTMUX, 1U, kINPUTMUX_MuBToDspInterrupt);
+ // Disables the clock for the Input Mux to save power
+ INPUTMUX_Deinit(INPUTMUX);
+
+ xt_set_interrupt_handler(kMuBIrqNum, MU_B_IrqHandler, NULL);
+ xt_interrupt_enable(kMuBIrqNum);
+ stream.Enable();
+ }
+
+Read/Write example where each core has threads for reading and writing.
+
+Core 0:
+
+.. code-block:: cpp
+
+ constexpr std::byte kCore0Value = std::byte{0xab};
+ constexpr std::byte kCore1Value = std::byte{0xcd};
+
+ void ReadThread() {
+ while(true) {
+ std::array<std::byte, 1> read = {};
+ auto status = stream.Read(read);
+ if (!status.ok() || status.size() != 1 || read[0] != kCore1Value) {
+ PW_LOG_WARN("Incorrect value read from core1");
+ }
+ }
+ }
+
+
+ void WriteThread() {
+ std::array<std::byte, 1> write = {kCore0Value};
+ while(true) {
+ stream.Write(write);
+ }
+ }
+
+Core 1:
+
+.. code-block:: cpp
+
+ void ReadThread() {
+ while(true) {
+ std::array<std::byte, 1> read = {};
+ auto status = stream.Read(read);
+ if (!status.ok() || status.size() != 1 || read[0] != kCore0Value) {
+ PW_LOG_WARN("Incorrect value read from core0");
+ }
+ }
+ }
+
+ void WriteThread() {
+ std::array<std::byte, 1> write = {kCore1Value};
+ while(true) {
+ stream.Write(write);
+ }
+ }
diff --git a/pw_stream_shmem_mcuxpresso/public/pw_stream_shmem_mcuxpresso/stream.h b/pw_stream_shmem_mcuxpresso/public/pw_stream_shmem_mcuxpresso/stream.h
new file mode 100644
index 000000000..95b889bd0
--- /dev/null
+++ b/pw_stream_shmem_mcuxpresso/public/pw_stream_shmem_mcuxpresso/stream.h
@@ -0,0 +1,64 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstdint>
+
+#include "fsl_mu.h"
+#include "pw_bytes/span.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+#include "pw_sync/binary_semaphore.h"
+
+namespace pw::stream {
+
+// Stream for reading/writing between processor cores using the Mcuxpresso SDK.
+//
+// It uses the MU module from the SDK for signaling data readiness. MU channels
+// 0 and 1 are claimed for exclusive use. Each core should have an instance of
+// this class with shared buffers pointing at the same physical memory locations
+// that is uncached on both sides.
+//
+// Interrupt setup is different between cores, so that is left to the user. An
+// example can be found in the docs. In the MU interrupt handler on each core,
+// the `HandleInterrupt` function on the stream should be called.
+class ShmemMcuxpressoStream : public NonSeekableReaderWriter {
+ public:
+ ShmemMcuxpressoStream(MU_Type* base,
+ ByteSpan shared_read_buffer,
+ ByteSpan shared_write_buffer)
+ : base_(base),
+ shared_read_buffer_(shared_read_buffer),
+ shared_write_buffer_(shared_write_buffer) {}
+ ~ShmemMcuxpressoStream();
+
+ void Enable();
+ void Disable();
+
+ // To be called when MU interrupt fires.
+ void HandleInterrupt();
+
+ private:
+ StatusWithSize DoRead(ByteSpan) override;
+ Status DoWrite(ConstByteSpan) override;
+
+ MU_Type* const base_;
+ ByteSpan shared_read_buffer_;
+ ByteSpan shared_write_buffer_;
+ sync::BinarySemaphore read_semaphore_;
+ sync::BinarySemaphore write_semaphore_;
+ sync::BinarySemaphore write_done_semaphore_;
+};
+
+} // namespace pw::stream
diff --git a/pw_stream_shmem_mcuxpresso/stream.cc b/pw_stream_shmem_mcuxpresso/stream.cc
new file mode 100644
index 000000000..cd2badd33
--- /dev/null
+++ b/pw_stream_shmem_mcuxpresso/stream.cc
@@ -0,0 +1,109 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream_shmem_mcuxpresso/stream.h"
+
+#include <atomic>
+#include <cstdint>
+
+namespace pw::stream {
+namespace {
+
+constexpr uint32_t kMuRegDataSize = 0;
+constexpr uint32_t kMuRegDataCopied = 1;
+
+} // namespace
+
+ShmemMcuxpressoStream::~ShmemMcuxpressoStream() { Disable(); }
+
+void ShmemMcuxpressoStream::Enable() {
+ MU_Init(base_);
+ MU_EnableInterrupts(base_,
+ kMU_Tx0EmptyInterruptEnable | kMU_Rx0FullInterruptEnable |
+ kMU_Rx1FullInterruptEnable);
+}
+
+void ShmemMcuxpressoStream::Disable() {
+ MU_DisableInterrupts(base_,
+ kMU_Tx0EmptyInterruptEnable |
+ kMU_Rx0FullInterruptEnable |
+ kMU_Rx1FullInterruptEnable);
+ MU_Deinit(base_);
+}
+
+StatusWithSize ShmemMcuxpressoStream::DoRead(ByteSpan data) {
+ read_semaphore_.acquire();
+
+ const uint32_t msg_len = MU_ReceiveMsgNonBlocking(base_, kMuRegDataSize);
+ StatusWithSize result(msg_len);
+
+ if (msg_len > shared_read_buffer_.size()) {
+ result = StatusWithSize::Internal();
+ } else if (msg_len > data.size()) {
+ result = StatusWithSize::InvalidArgument();
+ } else {
+ std::copy(shared_read_buffer_.begin(),
+ shared_read_buffer_.begin() + msg_len,
+ data.begin());
+ // Ensure all data is read before MU message is written.
+ std::atomic_thread_fence(std::memory_order_release);
+ }
+
+ // Ack we're done with our copy. Use blocking send as the other side will
+ // process the message directly in ISR.
+ MU_SendMsg(base_, kMuRegDataCopied, msg_len);
+
+ // Turn back on Rx0 interrupt, which will unblock next read.
+ MU_EnableInterrupts(base_, kMU_Rx0FullInterruptEnable);
+
+ return result;
+}
+
+Status ShmemMcuxpressoStream::DoWrite(ConstByteSpan data) {
+ if (data.size() > shared_write_buffer_.size()) {
+ return Status::InvalidArgument();
+ }
+ write_semaphore_.acquire();
+
+ std::copy(data.begin(), data.end(), shared_write_buffer_.begin());
+
+ // Ensure MU message is written after shared buffer is populated.
+ std::atomic_thread_fence(std::memory_order_release);
+
+ MU_SendMsgNonBlocking(base_, kMuRegDataSize, data.size());
+
+ write_done_semaphore_.acquire();
+
+ MU_EnableInterrupts(base_, kMU_Tx0EmptyInterruptEnable);
+
+ return OkStatus();
+}
+
+void ShmemMcuxpressoStream::HandleInterrupt() {
+ const uint32_t flags = MU_GetStatusFlags(base_);
+ if (flags & kMU_Tx0EmptyFlag) {
+ write_semaphore_.release();
+ MU_DisableInterrupts(base_, kMU_Tx0EmptyInterruptEnable);
+ }
+ if (flags & kMU_Rx0FullFlag) {
+ read_semaphore_.release();
+ MU_DisableInterrupts(base_, kMU_Rx0FullInterruptEnable);
+ }
+ if (flags & kMU_Rx1FullFlag) {
+ write_done_semaphore_.release();
+ MU_ReceiveMsgNonBlocking(base_, kMuRegDataCopied);
+ }
+}
+
+} // namespace pw::stream
diff --git a/pw_stream_shmem_mcuxpresso/stream_test.cc b/pw_stream_shmem_mcuxpresso/stream_test.cc
new file mode 100644
index 000000000..c4e658fac
--- /dev/null
+++ b/pw_stream_shmem_mcuxpresso/stream_test.cc
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream_shmem_mcuxpresso/stream.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::stream {
+namespace {
+
+std::array<std::byte, 20> read_buffer = {};
+std::array<std::byte, 20> write_buffer = {};
+
+TEST(ShmemMcuxpressoStream, CompilesOk) {
+ auto stream = ShmemMcuxpressoStream{MUA, read_buffer, write_buffer};
+ stream.Enable();
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_stream_uart_linux/Android.bp b/pw_stream_uart_linux/Android.bp
new file mode 100644
index 000000000..28cd99b34
--- /dev/null
+++ b/pw_stream_uart_linux/Android.bp
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+cc_library_static {
+ name: "pw_stream_uart_linux",
+ cpp_std: "c++20",
+ export_include_dirs: ["public"],
+ vendor_available: true,
+ host_supported: true,
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ srcs: [
+ "stream.cc",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_status",
+ "pw_stream",
+ ],
+ export_static_lib_headers: [
+ "pw_bytes",
+ "pw_status",
+ "pw_stream",
+ ],
+}
diff --git a/pw_stream_uart_linux/BUILD.bazel b/pw_stream_uart_linux/BUILD.bazel
new file mode 100644
index 000000000..901be32ac
--- /dev/null
+++ b/pw_stream_uart_linux/BUILD.bazel
@@ -0,0 +1,49 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_stream_uart_linux",
+ srcs = ["stream.cc"],
+ hdrs = ["public/pw_stream_uart_linux/stream.h"],
+ includes = ["public"],
+ target_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ deps = [
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_stream",
+ ],
+)
+
+pw_cc_test(
+ name = "stream_test",
+ srcs = [
+ "stream_test.cc",
+ ],
+ deps = [
+ ":pw_stream_uart_linux",
+ ],
+)
diff --git a/pw_stream_uart_linux/BUILD.gn b/pw_stream_uart_linux/BUILD.gn
new file mode 100644
index 000000000..2ff97173b
--- /dev/null
+++ b/pw_stream_uart_linux/BUILD.gn
@@ -0,0 +1,45 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_stream_uart_linux") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_stream_uart_linux/stream.h" ]
+ public_deps = [ "$dir_pw_stream" ]
+ sources = [ "stream.cc" ]
+ deps = [ "$dir_pw_log" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":stream_test" ]
+}
+
+pw_test("stream_test") {
+ enable_if = current_os == "linux"
+ sources = [ "stream_test.cc" ]
+ deps = [ ":pw_stream_uart_linux" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_stream_uart_linux/OWNERS b/pw_stream_uart_linux/OWNERS
new file mode 100644
index 000000000..685903036
--- /dev/null
+++ b/pw_stream_uart_linux/OWNERS
@@ -0,0 +1 @@
+aharp@google.com
diff --git a/pw_stream_uart_linux/docs.rst b/pw_stream_uart_linux/docs.rst
new file mode 100644
index 000000000..853da78d9
--- /dev/null
+++ b/pw_stream_uart_linux/docs.rst
@@ -0,0 +1,37 @@
+.. _module-pw_stream_uart_linux:
+
+====================
+pw_stream_uart_linux
+====================
+``pw_stream_uart_linux`` implements the
+:cpp:class:`pw::stream::NonSeekableReaderWriter` interface for reading from and
+writing to a UART using Linux TTY interfaces.
+
+.. note::
+ This module will likely be superseded by a future ``pw_uart`` interface.
+
+C++
+===
+.. doxygenclass:: pw::stream::UartStreamLinux
+ :members:
+
+Examples
+========
+A simple example illustrating writing to a UART:
+
+.. code-block:: cpp
+
+ constexpr const char* kUartPath = "/dev/ttyS0";
+ constexpr uint32_t kBaudRate = 115200;
+
+ pw::stream::UartStreamLinux stream;
+ PW_TRY(stream.Open(kUartPath, kBaudRate));
+
+ std::array<std::byte, 10> to_write = {};
+ PW_TRY(stream.Write(to_write));
+
+Caveats
+=======
+No interfaces are supplied for configuring data bits, stop bits, or parity.
+These attributes are left as they are already configured on the TTY; only the
+speed is modified.
diff --git a/pw_stream_uart_linux/public/pw_stream_uart_linux/stream.h b/pw_stream_uart_linux/public/pw_stream_uart_linux/stream.h
new file mode 100644
index 000000000..0195323a2
--- /dev/null
+++ b/pw_stream_uart_linux/public/pw_stream_uart_linux/stream.h
@@ -0,0 +1,65 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_stream/stream.h"
+
+namespace pw::stream {
+
+/// `pw::stream::NonSeekableReaderWriter` implementation for UARTs on Linux.
+class UartStreamLinux : public NonSeekableReaderWriter {
+ public:
+ constexpr UartStreamLinux() = default;
+
+ // UartStream objects are moveable but not copyable.
+ UartStreamLinux& operator=(UartStreamLinux&& other) {
+ fd_ = other.fd_;
+ other.fd_ = kInvalidFd;
+ return *this;
+ }
+ UartStreamLinux(UartStreamLinux&& other) noexcept : fd_(other.fd_) {
+ other.fd_ = kInvalidFd;
+ }
+ UartStreamLinux(const UartStreamLinux&) = delete;
+ UartStreamLinux& operator=(const UartStreamLinux&) = delete;
+
+ ~UartStreamLinux() override { Close(); }
+
+ /// Open a UART device using the specified baud rate.
+ ///
+ /// @param[in] path Path to the TTY device.
+ /// @param[in] baud_rate Baud rate to use for the device.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - The device was successfully opened and configured.
+ /// * @pw_status{INVALID_ARGUMENT} - An unsupported baud rate was supplied.
+ /// * @pw_status{FAILED_PRECONDITION} - A device was already open.
+ /// * @pw_status{UNKNOWN} - An error was returned by the operating system.
+ Status Open(const char* path, uint32_t baud_rate);
+ void Close();
+
+ private:
+ static constexpr int kInvalidFd = -1;
+
+ Status DoWrite(ConstByteSpan data) override;
+ StatusWithSize DoRead(ByteSpan dest) override;
+
+ int fd_ = kInvalidFd;
+};
+
+} // namespace pw::stream
diff --git a/pw_stream_uart_linux/stream.cc b/pw_stream_uart_linux/stream.cc
new file mode 100644
index 000000000..7bb7e7f15
--- /dev/null
+++ b/pw_stream_uart_linux/stream.cc
@@ -0,0 +1,149 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream_uart_linux/stream.h"
+
+#include <fcntl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include <cerrno>
+#include <cinttypes>
+
+#include "pw_log/log.h"
+
+namespace pw::stream {
+
+Result<speed_t> BaudRateToSpeed(uint32_t baud_rate) {
+ switch (baud_rate) {
+ case 9600:
+ return B9600;
+ case 19200:
+ return B19200;
+ case 38400:
+ return B38400;
+ case 57600:
+ return B57600;
+ case 115200:
+ return B115200;
+ case 230400:
+ return B230400;
+ case 460800:
+ return B460800;
+ case 500000:
+ return B500000;
+ case 576000:
+ return B576000;
+ case 921600:
+ return B921600;
+ case 1000000:
+ return B1000000;
+ case 1152000:
+ return B1152000;
+ case 1500000:
+ return B1500000;
+ case 2000000:
+ return B2000000;
+ case 2500000:
+ return B2500000;
+ case 3000000:
+ return B3000000;
+ case 3500000:
+ return B3500000;
+ case 4000000:
+ return B4000000;
+ default:
+ return Status::InvalidArgument();
+ }
+}
+
+Status UartStreamLinux::Open(const char* path, uint32_t baud_rate) {
+ const auto speed_result = BaudRateToSpeed(baud_rate);
+ if (!speed_result.ok()) {
+ PW_LOG_ERROR("Unsupported baud rate: %" PRIu32, baud_rate);
+ return speed_result.status();
+ }
+ speed_t speed = speed_result.value();
+
+ if (fd_ != kInvalidFd) {
+ PW_LOG_ERROR("UART device already open");
+ return Status::FailedPrecondition();
+ }
+
+ fd_ = open(path, O_RDWR);
+ if (fd_ < 0) {
+ PW_LOG_ERROR(
+ "Failed to open UART device '%s', %s", path, std::strerror(errno));
+ return Status::Unknown();
+ }
+
+ struct termios tty;
+ int result = tcgetattr(fd_, &tty);
+ if (result < 0) {
+ PW_LOG_ERROR("Failed to get TTY attributes for '%s', %s",
+ path,
+ std::strerror(errno));
+ return Status::Unknown();
+ }
+
+ cfmakeraw(&tty);
+ result = cfsetspeed(&tty, speed);
+ if (result < 0) {
+ PW_LOG_ERROR(
+ "Failed to set TTY speed for '%s', %s", path, std::strerror(errno));
+ return Status::Unknown();
+ }
+
+ result = tcsetattr(fd_, TCSANOW, &tty);
+ if (result < 0) {
+ PW_LOG_ERROR("Failed to set TTY attributes for '%s', %s",
+ path,
+ std::strerror(errno));
+ return Status::Unknown();
+ }
+
+ return OkStatus();
+}
+
+void UartStreamLinux::Close() {
+ if (fd_ != kInvalidFd) {
+ close(fd_);
+ fd_ = kInvalidFd;
+ }
+}
+
+Status UartStreamLinux::DoWrite(ConstByteSpan data) {
+ const size_t size = data.size_bytes();
+ size_t written = 0;
+ while (written < size) {
+ int bytes = write(fd_, &data[written], size - written);
+ if (bytes < 0) {
+ PW_LOG_ERROR("Failed to write to UART, %s", std::strerror(errno));
+ return Status::Unknown();
+ }
+ written += bytes;
+ }
+ return OkStatus();
+}
+
+StatusWithSize UartStreamLinux::DoRead(ByteSpan dest) {
+ int bytes = read(fd_, &dest[0], dest.size_bytes());
+ if (bytes < 0) {
+ PW_LOG_ERROR("Failed to read from UART, %s", std::strerror(errno));
+ return StatusWithSize::Unknown();
+ }
+ return StatusWithSize(bytes);
+}
+
+} // namespace pw::stream
diff --git a/pw_stream_uart_linux/stream_test.cc b/pw_stream_uart_linux/stream_test.cc
new file mode 100644
index 000000000..7f4d070b8
--- /dev/null
+++ b/pw_stream_uart_linux/stream_test.cc
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream_uart_linux/stream.h"
+
+#include "gtest/gtest.h"
+#include "public/pw_stream_uart_linux/stream.h"
+
+namespace pw::stream {
+namespace {
+
+// Path to a non-UART device.
+constexpr auto kPathNonUart = "/dev/null";
+
+TEST(UartStreamLinuxTest, TestOpenNonUartDevice) {
+ UartStreamLinux uart;
+ Status status = uart.Open(kPathNonUart, 115200);
+ EXPECT_EQ(status, Status::Unknown());
+}
+
+TEST(UartStreamLinuxTest, TestOpenInvalidBaudRate) {
+ UartStreamLinux uart;
+ Status status = uart.Open(kPathNonUart, 123456);
+ EXPECT_EQ(status, Status::InvalidArgument());
+}
+
+} // namespace
+} // namespace pw::stream \ No newline at end of file
diff --git a/pw_stream_uart_mcuxpresso/BUILD.bazel b/pw_stream_uart_mcuxpresso/BUILD.bazel
new file mode 100644
index 000000000..b4c418406
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/BUILD.bazel
@@ -0,0 +1,47 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_stream_uart_mcuxpresso",
+ srcs = ["stream.cc"],
+ hdrs = ["public/pw_stream_uart_mcuxpresso/stream.h"],
+ includes = ["public"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [
+ "//pw_preprocessor",
+ "//pw_stream",
+ "@pigweed//targets:mcuxpresso_sdk",
+ ],
+)
+
+pw_cc_test(
+ name = "stream_test",
+ srcs = ["stream_test.cc"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
+ deps = [":pw_stream_uart_mcuxpresso"],
+)
diff --git a/pw_stream_uart_mcuxpresso/BUILD.gn b/pw_stream_uart_mcuxpresso/BUILD.gn
new file mode 100644
index 000000000..b12e8f95a
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/BUILD.gn
@@ -0,0 +1,60 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+if (pw_third_party_mcuxpresso_SDK != "") {
+ pw_source_set("pw_stream_uart_mcuxpresso") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_stream_uart_mcuxpresso/stream.h" ]
+ public_deps = [ "$dir_pw_stream" ]
+ deps = [ pw_third_party_mcuxpresso_SDK ]
+ sources = [ "stream.cc" ]
+ }
+
+ pw_test("stream_test") {
+ enable_if =
+ pw_third_party_mcuxpresso_SDK ==
+ "//targets/mimxrt595_evk_freertos:sdk" &&
+ (pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_debug" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_size_optimized" ||
+ pw_toolchain_SCOPE.name == "mimxrt595_evk_freertos_speed_optimized")
+ sources = [ "stream_test.cc" ]
+ deps = [
+ ":pw_stream_uart_mcuxpresso",
+ "//targets/mimxrt595_evk_freertos:sdk",
+ ]
+ }
+
+ pw_test_group("tests") {
+ tests = [ ":stream_test" ]
+ }
+} else {
+ pw_test_group("tests") {
+ tests = []
+ }
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_stream_uart_mcuxpresso/OWNERS b/pw_stream_uart_mcuxpresso/OWNERS
new file mode 100644
index 000000000..a9c2709ad
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/OWNERS
@@ -0,0 +1,2 @@
+afoxley@google.com
+tonymd@google.com
diff --git a/pw_stream_uart_mcuxpresso/docs.rst b/pw_stream_uart_mcuxpresso/docs.rst
new file mode 100644
index 000000000..49d867c3c
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/docs.rst
@@ -0,0 +1,44 @@
+.. _module-pw_stream_uart_mcuxpresso:
+
+=========================
+pw_stream_uart_mcuxpresso
+=========================
+``pw_stream_uart_mcuxpresso`` implements the ``pw_stream`` interface for reading
+and writing to a UART using the NXP MCUXpresso SDK.
+
+.. note::
+ This module will likely be superseded by a future ``pw_uart`` interface.
+
+Setup
+=====
+This module requires a little setup:
+
+1. Use ``pw_build_mcuxpresso`` to create a ``pw_source_set`` for an
+ MCUXpresso SDK.
+2. Include the debug console component in this SDK definition.
+3. Specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
+ the name of this source set.
+4. Use a target that calls ``pw_sys_io_mcuxpresso_Init`` in
+ ``pw_boot_PreMainInit`` or similar.
+
+The name of the SDK source set must be set in the
+"pw_third_party_mcuxpresso_SDK" GN arg
+
+Usage
+=====
+.. code-block:: cpp
+
+ constexpr uint32_t kFlexcomm = 0;
+ constexpr uint32_t kBaudRate = 115200;
+ std::array<std::byte, 20> kBuffer = {};
+
+ auto stream = UartStreamMcuxpresso{USART0,
+ kBaudRate,
+ kUSART_ParityDisabled,
+ kUSART_OneStopBit,
+ kBuffer};
+
+ PW_TRY(stream.Init(CLOCK_GetFlexcommClkFreq(kFlexcomm)));
+
+ std::array<std::byte, 10> to_write = {};
+ PW_TRY(stream.Write(to_write));
diff --git a/pw_stream_uart_mcuxpresso/public/pw_stream_uart_mcuxpresso/stream.h b/pw_stream_uart_mcuxpresso/public/pw_stream_uart_mcuxpresso/stream.h
new file mode 100644
index 000000000..fbf2f3489
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/public/pw_stream_uart_mcuxpresso/stream.h
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "fsl_usart_freertos.h"
+#include "pw_bytes/span.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+
+namespace pw::stream {
+
+class UartStreamMcuxpresso : public NonSeekableReaderWriter {
+ public:
+ UartStreamMcuxpresso(USART_Type* base,
+ uint32_t baudrate,
+ usart_parity_mode_t parity,
+ usart_stop_bit_count_t stopbits,
+ ByteSpan buffer)
+ : base_(base),
+ config_{.base = base_,
+ .srcclk = 0,
+ .baudrate = baudrate,
+ .parity = parity,
+ .stopbits = stopbits,
+ .buffer = reinterpret_cast<uint8_t*>(buffer.data()),
+ .buffer_size = buffer.size()} {}
+
+ ~UartStreamMcuxpresso();
+
+ pw::Status Init(uint32_t srcclk);
+
+ private:
+ StatusWithSize DoRead(ByteSpan) override;
+ Status DoWrite(ConstByteSpan) override;
+
+ USART_Type* base_;
+ struct rtos_usart_config config_;
+ usart_rtos_handle_t handle_;
+ usart_handle_t uart_handle_;
+};
+
+} // namespace pw::stream
diff --git a/pw_stream_uart_mcuxpresso/stream.cc b/pw_stream_uart_mcuxpresso/stream.cc
new file mode 100644
index 000000000..e8b288fca
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/stream.cc
@@ -0,0 +1,56 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream_uart_mcuxpresso/stream.h"
+
+namespace pw::stream {
+
+UartStreamMcuxpresso::~UartStreamMcuxpresso() { USART_RTOS_Deinit(&handle_); }
+
+Status UartStreamMcuxpresso::Init(uint32_t srcclk) {
+ config_.srcclk = srcclk;
+
+ if (USART_RTOS_Init(&handle_, &uart_handle_, &config_) != kStatus_Success) {
+ return Status::Internal();
+ }
+
+ return OkStatus();
+}
+
+StatusWithSize UartStreamMcuxpresso::DoRead(ByteSpan data) {
+ size_t read = 0;
+ if (const auto status =
+ USART_RTOS_Receive(&handle_,
+ reinterpret_cast<uint8_t*>(data.data()),
+ data.size(),
+ &read);
+ status != kStatus_Success) {
+ USART_TransferAbortReceive(base_, &uart_handle_);
+ return StatusWithSize(Status::Internal(), 0);
+ }
+
+ return StatusWithSize(read);
+}
+
+Status UartStreamMcuxpresso::DoWrite(ConstByteSpan data) {
+ if (USART_RTOS_Send(
+ &handle_,
+ reinterpret_cast<uint8_t*>(const_cast<std::byte*>(data.data())),
+ data.size()) != kStatus_Success) {
+ return Status::Internal();
+ }
+ return OkStatus();
+}
+
+} // namespace pw::stream
diff --git a/pw_stream_uart_mcuxpresso/stream_test.cc b/pw_stream_uart_mcuxpresso/stream_test.cc
new file mode 100644
index 000000000..6ebbcd356
--- /dev/null
+++ b/pw_stream_uart_mcuxpresso/stream_test.cc
@@ -0,0 +1,33 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_stream_uart_mcuxpresso/stream.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::stream {
+namespace {
+
+constexpr uint32_t kFlexcomm = 0;
+constexpr uint32_t kBaudRate = 115200;
+std::array<std::byte, 20> kBuffer = {};
+
+TEST(UartStreamMcuxpresso, InitOk) {
+ auto stream = UartStreamMcuxpresso{
+ USART0, kBaudRate, kUSART_ParityDisabled, kUSART_OneStopBit, kBuffer};
+ EXPECT_EQ(stream.Init(CLOCK_GetFlexcommClkFreq(kFlexcomm)), OkStatus());
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_string/Android.bp b/pw_string/Android.bp
index e60a149a1..01be7887d 100644
--- a/pw_string/Android.bp
+++ b/pw_string/Android.bp
@@ -22,6 +22,7 @@ cc_library_static {
vendor_available: true,
export_include_dirs: ["public"],
header_libs: [
+ "fuchsia_sdk_lib_stdcompat",
"pw_assert_headers",
"pw_assert_log_headers",
"pw_log_headers",
@@ -31,6 +32,15 @@ cc_library_static {
"pw_result_headers",
"pw_span_headers",
],
+ export_header_lib_headers: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_polyfill_headers",
+ "pw_result_headers",
+ ],
+ export_static_lib_headers: [
+ "pw_status",
+ ],
host_supported: true,
srcs: [
"format.cc",
diff --git a/pw_string/BUILD.bazel b/pw_string/BUILD.bazel
index 420026054..c0ccfe866 100644
--- a/pw_string/BUILD.bazel
+++ b/pw_string/BUILD.bazel
@@ -59,6 +59,7 @@ pw_cc_library(
hdrs = ["public/pw_string/format.h"],
includes = ["public"],
deps = [
+ ":string",
"//pw_preprocessor",
"//pw_span",
"//pw_status",
@@ -74,7 +75,7 @@ pw_cc_library(
hdrs = ["public/pw_string/string.h"],
includes = ["public"],
deps = [
- "//pw_assert:facade",
+ "//pw_assert",
"//pw_polyfill",
],
)
@@ -92,6 +93,7 @@ pw_cc_library(
":format",
":util",
"//pw_status",
+ "//third_party/fuchsia:stdcompat",
],
)
@@ -102,7 +104,7 @@ pw_cc_library(
includes = ["public"],
deps = [
":string",
- "//pw_assert:facade",
+ "//pw_assert",
"//pw_result",
"//pw_span",
"//pw_status",
diff --git a/pw_string/BUILD.gn b/pw_string/BUILD.gn
index bc73a2504..ef3fda62c 100644
--- a/pw_string/BUILD.gn
+++ b/pw_string/BUILD.gn
@@ -66,6 +66,9 @@ pw_source_set("builder") {
dir_pw_span,
dir_pw_status,
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_source_set("format") {
@@ -73,10 +76,14 @@ pw_source_set("format") {
public = [ "public/pw_string/format.h" ]
sources = [ "format.cc" ]
public_deps = [
+ ":string",
dir_pw_preprocessor,
dir_pw_span,
dir_pw_status,
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_source_set("string") {
@@ -90,6 +97,9 @@ pw_source_set("string") {
dir_pw_assert,
dir_pw_polyfill,
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_source_set("to_string") {
@@ -106,6 +116,10 @@ pw_source_set("to_string") {
dir_pw_span,
dir_pw_status,
]
+ deps = [ "$dir_pw_third_party/fuchsia:stdcompat" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_source_set("util") {
@@ -139,17 +153,26 @@ pw_test_group("tests") {
pw_test("format_test") {
deps = [ ":format" ]
sources = [ "format_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("string_test") {
deps = [ ":string" ]
sources = [ "string_test.cc" ]
negative_compilation_tests = true
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("string_builder_test") {
deps = [ ":builder" ]
sources = [ "string_builder_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("to_string_test") {
@@ -158,21 +181,31 @@ pw_test("to_string_test") {
":pw_string",
]
sources = [ "to_string_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("type_to_string_test") {
deps = [ ":to_string" ]
sources = [ "type_to_string_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("util_test") {
deps = [ ":util" ]
sources = [ "util_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_doc_group("docs") {
sources = [
"api.rst",
+ "code_size.rst",
"design.rst",
"docs.rst",
"guide.rst",
diff --git a/pw_string/CMakeLists.txt b/pw_string/CMakeLists.txt
index 07bba6293..8299bfab2 100644
--- a/pw_string/CMakeLists.txt
+++ b/pw_string/CMakeLists.txt
@@ -58,6 +58,7 @@ pw_add_library(pw_string.format STATIC
pw_preprocessor
pw_span
pw_status
+ pw_string.string
SOURCES
format.cc
)
@@ -84,6 +85,8 @@ pw_add_library(pw_string.to_string STATIC
pw_string.format
pw_string.util
pw_status
+ PRIVATE_DEPS
+ pw_third_party.fuchsia.stdcompat
SOURCES
type_to_string.cc
)
@@ -164,7 +167,3 @@ pw_add_test(pw_string.util_test
modules
pw_string
)
-
-if(Zephyr_FOUND AND CONFIG_PIGWEED_STRING)
- zephyr_link_libraries(pw_string)
-endif()
diff --git a/pw_string/Kconfig b/pw_string/Kconfig
index fffa12a9c..9731428bc 100644
--- a/pw_string/Kconfig
+++ b/pw_string/Kconfig
@@ -12,10 +12,16 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_string"
+
config PIGWEED_STRING
- bool "Enable Pigweed string library (pw_string)"
+ bool "Link pw_string library"
select PIGWEED_ASSERT
select PIGWEED_PREPROCESSOR
select PIGWEED_RESULT
select PIGWEED_SPAN
select PIGWEED_STATUS
+ help
+ See :ref:`module-pw_string` for module details.
+
+endmenu
diff --git a/pw_string/api.rst b/pw_string/api.rst
index a526023eb..f90918591 100644
--- a/pw_string/api.rst
+++ b/pw_string/api.rst
@@ -3,11 +3,14 @@
=============
API Reference
=============
+.. pigweed-module-subpage::
+ :name: pw_string
+ :tagline: pw_string: Efficient, easy, and safe string manipulation
--------
Overview
--------
-This module provides two types of strings, and utility functions for working
+This module provides two types of strings and utility functions for working
with strings.
**pw::StringBuilder**
@@ -77,9 +80,14 @@ terminator in the vector.
pw::string::Format()
--------------------
.. doxygenfile:: pw_string/format.h
- :sections: briefdescription
-.. doxygenfunction:: pw::string::Format
-.. doxygenfunction:: pw::string::FormatVaList
+ :sections: detaileddescription
+
+.. doxygenfunction:: pw::string::Format(span<char> buffer, const char* format, ...)
+.. doxygenfunction:: pw::string::FormatVaList(span<char> buffer, const char* format, va_list args)
+.. doxygenfunction:: pw::string::Format(InlineString<>& string, const char* format, ...)
+.. doxygenfunction:: pw::string::FormatVaList(InlineString<>& string, const char* format, va_list args)
+.. doxygenfunction:: pw::string::FormatOverwrite(InlineString<>& string, const char* format, ...)
+.. doxygenfunction:: pw::string::FormatOverwriteVaList(InlineString<>& string, const char* format, va_list args)
pw::string::NullTerminatedLength()
----------------------------------
diff --git a/pw_string/code_size.rst b/pw_string/code_size.rst
new file mode 100644
index 000000000..575c1cfe9
--- /dev/null
+++ b/pw_string/code_size.rst
@@ -0,0 +1,40 @@
+.. _module-pw_string-size-reports:
+
+==================
+Code Size Analysis
+==================
+.. pigweed-module-subpage::
+ :name: pw_string
+ :tagline: pw_string: Efficient, easy, and safe string manipulation
+
+Save code space by replacing ``snprintf``
+=========================================
+The C standard library function ``snprintf`` is commonly used for string
+formatting. However, it isn't optimized for embedded systems, and using it will
+bring in a lot of other standard library code that will inflate your binary
+size.
+
+Size comparison: snprintf versus pw::StringBuilder
+--------------------------------------------------
+The fixed code size cost of :cpp:type:`pw::StringBuilder` is smaller than
+that of ``std::snprintf``. Using only :cpp:type:`pw::StringBuilder`'s ``<<`` and
+``append`` methods instead of ``snprintf`` leads to significant code size
+reductions.
+
+However, there are cases when the incremental code size cost of
+:cpp:type:`pw::StringBuilder` is similar to that of ``snprintf``. For example,
+each argument to :cpp:type:`pw::StringBuilder`'s ``<<`` method expands to a
+function call, but one or two :cpp:type:`pw::StringBuilder` appends may still
+have a smaller code size impact than a single ``snprintf`` call. Using
+:cpp:type:`pw::StringBuilder` error handling will also impact code size in a
+way that is comparable to ``snprintf``.
+
+.. include:: string_builder_size_report
+
+Size comparison: snprintf versus pw::string::Format
+---------------------------------------------------
+The ``pw::string::Format`` functions have a small, fixed code size
+cost. However, relative to equivalent ``std::snprintf`` calls, there is no
+incremental code size cost to using ``pw::string::Format``.
+
+.. include:: format_size_report
diff --git a/pw_string/design.rst b/pw_string/design.rst
index 91202e185..ae52f4187 100644
--- a/pw_string/design.rst
+++ b/pw_string/design.rst
@@ -1,16 +1,22 @@
.. _module-pw_string-design:
================
-pw_string design
+Design & Roadmap
================
+.. pigweed-module-subpage::
+ :name: pw_string
+ :tagline: pw_string: Efficient, easy, and safe string manipulation
+
``pw_string`` provides string classes and utility functions designed to
prioritize safety and static allocation. The APIs are broadly similar to those
of the string classes in the C++ standard library, so familiarity with those
classes will provide some context around ``pw_string`` design decisions.
-------------
-InlineString
-------------
+.. _module-pw_string-design-inlinestring:
+
+--------------------------
+Design of pw::InlineString
+--------------------------
:cpp:type:`pw::InlineString` / :cpp:class:`pw::InlineBasicString` are designed
to match the ``std::string`` / ``std::basic_string<T>`` API as closely as
possible, but with key differences to improve performance on embedded systems:
@@ -56,9 +62,9 @@ fail an assertion, resulting in a crash. Helpers are provided in
``pw_string/util.h`` that return ``pw::Status::ResourceExhausted()`` instead of
failing an assert when the capacity would be exceeded.
-------------------------
-String utility functions
-------------------------
+----------------------------------
+Design of string utility functions
+----------------------------------
Safe length checking
====================
@@ -73,3 +79,12 @@ null-termination.
Second, a constexpr specialized form is offered where null termination is
required through :cpp:func:`pw::string::NullTerminatedLength`. This will only
return a length if the string is null-terminated.
+
+.. _module-pw_string-roadmap:
+
+-------
+Roadmap
+-------
+* The fixed size cost of :cpp:type:`pw::StringBuilder` can be dramatically
+ reduced by limiting support for 64-bit integers.
+* ``pw_string`` may be integrated with :ref:`module-pw_tokenizer`.
diff --git a/pw_string/docs.rst b/pw_string/docs.rst
index e7b46608d..cbd8906bf 100644
--- a/pw_string/docs.rst
+++ b/pw_string/docs.rst
@@ -1,21 +1,14 @@
.. _module-pw_string:
-.. rst-class:: with-subtitle
-
=========
pw_string
=========
-
.. pigweed-module::
:name: pw_string
:tagline: Efficient, easy, and safe string manipulation
:status: stable
- :languages: C++14, C++17
+ :languages: C++17
:code-size-impact: 500 to 1500 bytes
- :get-started: module-pw_string-get-started
- :design: module-pw_string-design
- :guides: module-pw_string-guide
- :api: module-pw_string-api
- **Efficient**: No memory allocation, no pointer indirection.
- **Easy**: Use the string API you already know.
@@ -24,7 +17,7 @@ pw_string
*Pick three!* If you know how to use ``std::string``, just use
:cpp:type:`pw::InlineString` in the same way:
- .. code:: cpp
+ .. code-block:: cpp
// Create a string from a C-style char array; storage is pre-allocated!
pw::InlineString<16> my_string = "Literally";
@@ -40,7 +33,7 @@ pw_string
``std::ostringstream``, but with most of the efficiency and memory benefits
of :cpp:type:`pw::InlineString`:
- .. code:: cpp
+ .. code-block:: cpp
// Create a pw::StringBuilder with a built-in buffer
pw::StringBuffer<32> my_string_builder = "Is it really this easy?";
@@ -53,128 +46,83 @@ pw_string
Check out :ref:`module-pw_string-guide` for more code samples.
-----------
-Background
-----------
String manipulation on embedded systems can be surprisingly challenging.
-C strings are light weight but come with many pitfalls for those who don't know
-the standard library deeply. C++ provides string classes that are safe and easy
-to use, but they consume way too much code space and are designed to be used
-with dynamic memory allocation.
-
-Embedded systems need string functionality that is both safe and suitable for
-resource-constrained platforms.
-
-------------
-Our solution
-------------
-``pw_string`` provides safe string handling functionality with an API that
-closely matches that of ``std::string``, but without dynamic memory allocation
-and with a *much* smaller :ref:`binary size impact <module-pw_string-size-reports>`.
-
----------------
-Who this is for
----------------
-``pw_string`` is useful any time you need to handle strings in embedded C++.
+
+- **C strings?** They're light-weight but come with many pitfalls for those who
+ don't know the standard library deeply.
+
+- **C++ strings?** STL string classes are safe and easy to use, but they consume
+ way too much code space and are designed to be used with dynamic memory
+ allocation.
+
+- **Roll your own strings?** You don't have time! You have a product to ship!
+
+Embedded systems need string functionality that is both *safe* and *suitable*
+for resource-constrained platforms.
+
+.. rst-class:: key-text
+
+ ``pw_string`` provides safe string handling functionality with an API that
+ closely matches that of ``std::string``, but without dynamic memory
+ allocation and with a *much* smaller :ref:`binary size impact<module-pw_string-size-reports>`.
--------------------
Is it right for you?
--------------------
+.. rst-class:: key-text
+
+ ``pw_string`` is useful any time you need to handle strings in embedded C++.
+
If your project written in C, ``pw_string`` is not a good fit since we don't
currently expose a C API.
-For larger platforms where code space isn't in short supply and dynamic memory
-allocation isn't a problem, you may find that ``std::string`` meets your needs.
+On the other hand, for larger platforms where code space isn't in short supply
+and dynamic memory allocation isn't a problem, you may find that ``std::string``
+meets your needs.
.. tip::
``pw_string`` works just as well on larger embedded platforms and host
- systems. Using ``pw_string`` even when you might get away with ``std:string``
+ systems. Using ``pw_string`` even when you could get away with ``std:string``
gives you the flexibility to move to smaller platforms later with much less
rework.
-Here are some size reports that may affect whether ``pw_string`` is right for
-you.
-
-.. _module-pw_string-size-reports:
-
-Size comparison: snprintf versus pw::StringBuilder
---------------------------------------------------
-:cpp:type:`pw::StringBuilder` is safe, flexible, and results in much smaller
-code size than using ``std::ostringstream``. However, applications sensitive to
-code size should use :cpp:type:`pw::StringBuilder` with care.
-
-The fixed code size cost of :cpp:type:`pw::StringBuilder` is significant, though
-smaller than ``std::snprintf``. Using :cpp:type:`pw::StringBuilder`'s ``<<`` and
-``append`` methods exclusively in place of ``snprintf`` reduces code size, but
-``snprintf`` may be difficult to avoid.
-
-The incremental code size cost of :cpp:type:`pw::StringBuilder` is comparable to
-``snprintf`` if errors are handled. Each argument to
-:cpp:type:`pw::StringBuilder`'s ``<<`` method expands to a function call, but
-one or two :cpp:type:`pw::StringBuilder` appends may have a smaller code size
-impact than a single ``snprintf`` call.
-
-.. include:: string_builder_size_report
-
-Size comparison: snprintf versus pw::string::Format
----------------------------------------------------
-The ``pw::string::Format`` functions have a small, fixed code size
-cost. However, relative to equivalent ``std::snprintf`` calls, there is no
-incremental code size cost to using ``pw::string::Format``.
-
-.. include:: format_size_report
-
-Roadmap
--------
-* StringBuilder's fixed size cost can be dramatically reduced by limiting
- support for 64-bit integers.
-* Consider integrating with the tokenizer module.
-
-Compatibility
--------------
-C++17, C++14 (:cpp:type:`pw::InlineString`)
+.. toctree::
+ :hidden:
+ :maxdepth: 1
-.. _module-pw_string-get-started:
+ guide
+ api
+ design
+ code_size
----------------
-Getting started
----------------
+.. grid:: 2
-GN
---
+ .. grid-item-card:: :octicon:`rocket` Get Started & Guides
+ :link: module-pw_string-get-started
+ :link-type: ref
+ :class-item: sales-pitch-cta-primary
-Add ``$dir_pw_string`` to the ``deps`` list in your ``pw_executable()`` build
-target:
+ Integrate pw_string into your project and learn common use cases
-.. code::
+ .. grid-item-card:: :octicon:`code-square` API Reference
+ :link: module-pw_string-api
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
- pw_executable("...") {
- # ...
- deps = [
- # ...
- "$dir_pw_string",
- # ...
- ]
- }
+ Detailed description of the pw_string's classes and methods
-See `//source/BUILD.gn <https://pigweed.googlesource.com/pigweed/sample_project/+/refs/heads/main/source/BUILD.gn>`_
-in the Pigweed Sample Project for an example.
+.. grid:: 2
-Zephyr
-------
-Add ``CONFIG_PIGWEED_STRING=y`` to the Zephyr project's configuration.
+ .. grid-item-card:: :octicon:`code-square` Design & Roadmap
+ :link: module-pw_string-design
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
--------
-Roadmap
--------
-* The fixed size cost of :cpp:type:`pw::StringBuilder` can be dramatically
- reduced by limiting support for 64-bit integers.
-* ``pw_string`` may be integrated with :ref:`module-pw_tokenizer`.
+ Learn why pw_string is designed the way it is, and upcoming plans
-.. toctree::
- :hidden:
- :maxdepth: 1
+ .. grid-item-card:: :octicon:`code-square` Code Size Analysis
+ :link: module-pw_string-size-reports
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
- design
- guide
- api
+ Understand pw_string's code footprint and savings potential
diff --git a/pw_string/format.cc b/pw_string/format.cc
index 57dea10f2..a5754d39f 100644
--- a/pw_string/format.cc
+++ b/pw_string/format.cc
@@ -1,4 +1,4 @@
-// Copyright 2019 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -51,4 +51,36 @@ StatusWithSize FormatVaList(span<char> buffer,
return StatusWithSize(result);
}
+Status Format(InlineString<>& string, const char* format, ...) {
+ va_list args;
+ va_start(args, format);
+ const Status status = FormatVaList(string, format, args);
+ va_end(args);
+
+ return status;
+}
+
+Status FormatVaList(InlineString<>& string, const char* format, va_list args) {
+ Status status;
+ string.resize_and_overwrite([&](char* buffer, size_t capacity) {
+ // The vsnprintf buffer size includes a byte for the null terminator.
+ StatusWithSize result =
+ FormatVaList(span(buffer + string.size(), capacity + 1 - string.size()),
+ format,
+ args);
+ status = result.status();
+ return string.size() + result.size();
+ });
+ return status;
+}
+
+Status FormatOverwrite(InlineString<>& string, const char* format, ...) {
+ va_list args;
+ va_start(args, format);
+ const Status status = FormatOverwriteVaList(string, format, args);
+ va_end(args);
+
+ return status;
+}
+
} // namespace pw::string
diff --git a/pw_string/format_test.cc b/pw_string/format_test.cc
index f2bfbf545..cc7b7308b 100644
--- a/pw_string/format_test.cc
+++ b/pw_string/format_test.cc
@@ -65,6 +65,7 @@ TEST(Format, ArgumentLargerThanBuffer_ReturnsResourceExhausted) {
EXPECT_STREQ("2big", buffer);
}
+PW_PRINTF_FORMAT(2, 3)
StatusWithSize CallFormatWithVaList(span<char> buffer, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
@@ -84,5 +85,65 @@ TEST(Format, CallFormatWithVaList_CallsCorrectFormatOverload) {
EXPECT_STREQ("Yo?!", buffer);
}
+TEST(FormatString, Append) {
+ InlineString<6> string;
+ EXPECT_EQ(OkStatus(), Format(string, "-_-"));
+ EXPECT_EQ("-_-", string);
+
+ EXPECT_EQ(OkStatus(), Format(string, "%d", 123));
+ EXPECT_EQ("-_-123", string);
+
+ EXPECT_EQ(Status::ResourceExhausted(), Format(string, "%d", 1));
+ EXPECT_EQ("-_-123", string);
+}
+
+TEST(FormatString, EmptyString) {
+ InlineString<0> string;
+ EXPECT_EQ(Status::ResourceExhausted(), Format(string, "-_-"));
+ EXPECT_EQ("", string);
+}
+
+TEST(FormatString, Truncates) {
+ InlineString<5> string;
+ EXPECT_EQ(Status::ResourceExhausted(), Format(string, "1%s", "23456"));
+ EXPECT_EQ("12345", string);
+}
+
+TEST(FormatString, Overwrite) {
+ InlineString<6> string("???");
+ EXPECT_EQ(OkStatus(), FormatOverwrite(string, "-_-"));
+ EXPECT_EQ("-_-", string);
+}
+
+template <Status (&kFunction)(InlineString<>&, const char*, va_list)>
+PW_PRINTF_FORMAT(2, 3)
+Status CallInlineString(InlineString<>& string, const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+
+ Status result = kFunction(string, fmt, args);
+
+ va_end(args);
+ return result;
+}
+
+TEST(FormatString, CallFormatWithVaList_CallsCorrectFormatOverload) {
+ InlineString<8> string;
+ Status result = CallInlineString<FormatVaList>(string, "Yo%s %d", "?!", 5);
+
+ EXPECT_EQ(OkStatus(), result);
+ EXPECT_EQ(6u, string.size());
+ EXPECT_EQ("Yo?! 5", string);
+}
+
+TEST(FormatString, OverwriteVaList) {
+ InlineString<8> string("blah");
+ Status result = CallInlineString<FormatOverwriteVaList>(string, "why%c", '?');
+
+ EXPECT_EQ(OkStatus(), result);
+ EXPECT_EQ(4u, string.size());
+ EXPECT_EQ("why?", string);
+}
+
} // namespace
} // namespace pw::string
diff --git a/pw_string/guide.rst b/pw_string/guide.rst
index 87ceec03e..dfa83b39b 100644
--- a/pw_string/guide.rst
+++ b/pw_string/guide.rst
@@ -1,19 +1,103 @@
.. _module-pw_string-guide:
-================
-pw_string: Guide
-================
+====================
+Get Started & Guides
+====================
+.. pigweed-module-subpage::
+ :name: pw_string
+ :tagline: pw_string: Efficient, easy, and safe string manipulation
-InlineString and StringBuilder?
-===============================
-Use :cpp:type:`pw::InlineString` if you need:
+.. _module-pw_string-get-started:
+
+Get Started
+===========
+.. tab-set::
+
+ .. tab-item:: Bazel
+
+ Add ``@pigweed//pw_string`` to the ``deps`` list in your Bazel target:
+
+ .. code-block::
+
+ cc_library("...") {
+ # ...
+ deps = [
+ # ...
+ "@pigweed//pw_string",
+ # ...
+ ]
+ }
+
+ If only one part of the module is needed, depend only on it; for example
+ ``@pigweed//pw_string:format``.
+
+ This assumes ``@pigweed`` is the name you pulled Pigweed into your Bazel
+ ``WORKSPACE`` as.
+
+ .. tab-item:: GN
+
+ Add ``$dir_pw_string`` to the ``deps`` list in your ``pw_executable()``
+ build target:
+
+ .. code-block::
+
+ pw_executable("...") {
+ # ...
+ deps = [
+ # ...
+ "$dir_pw_string",
+ # ...
+ ]
+ }
+
+ See `//source/BUILD.gn <https://pigweed.googlesource.com/pigweed/sample_project/+/refs/heads/main/source/BUILD.gn>`_
+ in the Pigweed Sample Project for an example.
+
+ .. tab-item:: CMake
+
+ Add ``pw_string`` to your ``pw_add_library`` or similar CMake target:
+
+ .. code-block::
+
+ pw_add_library(my_library STATIC
+ HEADERS
+ ...
+ PRIVATE_DEPS
+ # ...
+ pw_string
+ # ...
+ )
+
+ For a narrower dependency, depend on subtargets like
+ ``pw_string.builder``, etc.
+
+ .. tab-item:: Zephyr
+
+ There are two ways to use ``pw_string`` from a Zephyr project:
+
+ #. Depend on ``pw_string`` in your CMake target (see CMake tab). This is
+ Pigweed Team's suggested approach since it enables precise CMake
+ dependency analysis.
+
+ #. Add ``CONFIG_PIGWEED_STRING=y`` to the Zephyr project's configuration,
+ which causes ``pw_string`` to become a global dependency and have the
+ includes exposed to all targets. Pigweed team does not recommend this
+ approach, though it is the typical Zephyr solution.
+
+Choose between pw::InlineString and pw::StringBuilder
+=====================================================
+`pw::InlineString` is intended to replace typical null terminated character
+arrays in embedded data structures. Use :cpp:type:`pw::InlineString` if you
+need:
* Compatibility with ``std::string``
* Storage internal to the object
* A string object to persist in other data structures
* Lower code size overhead
-Use :cpp:class:`pw::StringBuilder` if you need:
+`pw::StringBuilder` is intended to ease constructing strings in external data;
+typically created on the stack and disposed of in the same function. Use
+:cpp:class:`pw::StringBuilder` if you need:
* Compatibility with ``std::ostringstream``, including custom object support
* Storage external to the object
@@ -59,9 +143,10 @@ constructing a string for external use.
return sb.status();
}
+.. _module-pw_string-guide-stringbuilder:
-Building strings with pw::StringBuilder
-=======================================
+Build a string with pw::StringBuilder
+=====================================
The following shows basic use of a :cpp:class:`pw::StringBuilder`.
.. code-block:: cpp
@@ -90,8 +175,8 @@ The following shows basic use of a :cpp:class:`pw::StringBuilder`.
return sb.status();
}
-Building strings with pw::InlineString
-======================================
+Build a string with pw::InlineString
+====================================
:cpp:type:`pw::InlineString` objects must be constructed by specifying a fixed
capacity for the string.
@@ -127,8 +212,8 @@ capacity for the string.
FunctionThatTakesAnInlineString(std::string_view("1234", 4));
-Building strings inside InlineString with a StringBuilder
-=========================================================
+Build a string inside an pw::InlineString with a pw::StringBuilder
+==================================================================
:cpp:class:`pw::StringBuilder` can build a string in a
:cpp:type:`pw::InlineString`:
@@ -143,8 +228,8 @@ Building strings inside InlineString with a StringBuilder
// inline_str contains "456"
}
-Passing InlineStrings as parameters
-===================================
+Pass an pw::InlineString object as a parameter
+==============================================
:cpp:type:`pw::InlineString` objects can be passed to non-templated functions
via type erasure. This saves code size in most cases, since it avoids template
expansions triggered by string size differences.
@@ -193,8 +278,8 @@ Known size strings
return string;
}();
-Compact initialization of InlineStrings
-=======================================
+Initialization of pw::InlineString objects
+===========================================
:cpp:type:`pw::InlineBasicString` supports class template argument deduction
(CTAD) in C++17 and newer. Since :cpp:type:`pw::InlineString` is an alias, CTAD
is not supported until C++20.
@@ -208,9 +293,9 @@ is not supported until C++20.
// In C++20, CTAD may be used with the pw::InlineString alias.
pw::InlineString my_other_string("123456789");
-Supporting custom types with StringBuilder
-==========================================
-As with ``std::ostream``, StringBuilder supports printing custom types by
+Custom types with pw::StringBuilder
+===================================
+As with ``std::ostream``, pw::StringBuilder supports printing custom types by
overriding the ``<<`` operator. This is is done by defining ``operator<<`` in
the same namespace as the custom type. For example:
diff --git a/pw_string/public/pw_string/format.h b/pw_string/public/pw_string/format.h
index 205210a8c..1fa0b396e 100644
--- a/pw_string/public/pw_string/format.h
+++ b/pw_string/public/pw_string/format.h
@@ -1,4 +1,4 @@
-// Copyright 2019 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -13,24 +13,27 @@
// the License.
#pragma once
-// This provides the pw::string::Format functions, which are safer alternatives
-// to std::snprintf and std::vsnprintf. The snprintf return value is awkward to
-// interpret, and misinterpreting it can lead to serious bugs.
-//
-// These functions return a StatusWithSize. The Status is set to reflect any
-// errors and the return value is always the number of characters written before
-// the null terminator.
+/// @file pw_string/format.h
+///
+/// The `pw::string::Format` functions are safer alternatives to `std::snprintf`
+/// and `std::vsnprintf`. The `snprintf` return value is awkward to interpret,
+/// and misinterpreting it can lead to serious bugs.
+///
+/// These functions return a `pw::StatusWithSize`. The `pw::Status` is set to
+/// reflect any errors and the return value is always the number of characters
+/// written before the null terminator.
#include <cstdarg>
#include "pw_preprocessor/compiler.h"
#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
+#include "pw_string/string.h"
namespace pw::string {
-/// @brief Writes a printf-style formatted string to the provided buffer,
-/// similarly to `std::snprintf()`.
+/// Writes a printf-style formatted string to the provided buffer, similarly to
+/// `std::snprintf()`.
///
/// The `std::snprintf()` return value is awkward to interpret, and
/// misinterpreting it can lead to serious bugs.
@@ -43,8 +46,8 @@ namespace pw::string {
PW_PRINTF_FORMAT(2, 3)
StatusWithSize Format(span<char> buffer, const char* format, ...);
-/// @brief Writes a printf-style formatted string with va_list-packed arguments
-/// to the provided buffer, similarly to `std::vsnprintf()`.
+/// Writes a printf-style formatted string with va_list-packed arguments to the
+/// provided buffer, similarly to `std::vsnprintf()`.
///
/// @returns See `pw::string::Format()`.
PW_PRINTF_FORMAT(2, 0)
@@ -52,4 +55,37 @@ StatusWithSize FormatVaList(span<char> buffer,
const char* format,
va_list args);
+/// Appends a printf-style formatted string to the provided `pw::InlineString`,
+/// similarly to `std::snprintf()`.
+///
+/// @returns See `pw::string::Format()`.
+PW_PRINTF_FORMAT(2, 3)
+Status Format(InlineString<>& string, const char* format, ...);
+
+/// Appends a printf-style formatted string with va_list-packed arguments to the
+/// provided `InlineString`, similarly to `std::vsnprintf()`.
+///
+/// @returns See `pw::string::Format()`.
+PW_PRINTF_FORMAT(2, 0)
+Status FormatVaList(InlineString<>& string, const char* format, va_list args);
+
+/// Writes a printf-style format string to the provided `InlineString`,
+/// overwriting any contents.
+///
+/// @returns See `pw::string::Format()`.
+PW_PRINTF_FORMAT(2, 3)
+Status FormatOverwrite(InlineString<>& string, const char* format, ...);
+
+/// Writes a printf-style formatted string with `va_list`-packed arguments to
+/// the provided `InlineString`, overwriting any contents.
+///
+/// @returns See `pw::string::Format()`.
+PW_PRINTF_FORMAT(2, 0)
+inline Status FormatOverwriteVaList(InlineString<>& string,
+ const char* format,
+ va_list args) {
+ string.clear();
+ return FormatVaList(string, format, args);
+}
+
} // namespace pw::string
diff --git a/pw_string/public/pw_string/internal/string_common_functions.inc b/pw_string/public/pw_string/internal/string_common_functions.inc
index c2d8625b6..38ad827a7 100644
--- a/pw_string/public/pw_string/internal/string_common_functions.inc
+++ b/pw_string/public/pw_string/internal/string_common_functions.inc
@@ -35,12 +35,12 @@ PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wtype-limits");
// Assignment functions
-constexpr InlineBasicString& assign(size_type count, T ch) {
+constexpr InlineBasicString& assign(size_t count, T ch) {
return static_cast<InlineBasicString&>(Fill(data(), ch, count));
}
// Checks capacity rather than current size.
-template <size_type kOtherCapacity>
+template <size_t kOtherCapacity>
constexpr InlineBasicString& assign(
const InlineBasicString<T, kOtherCapacity>& other) {
static_assert(
@@ -51,13 +51,13 @@ constexpr InlineBasicString& assign(
}
constexpr InlineBasicString& assign(const InlineBasicString& other,
- size_type index,
- size_type count = npos) {
+ size_t index,
+ size_t count = npos) {
return static_cast<InlineBasicString&>(
CopySubstr(data(), other.data(), other.size(), index, count));
}
-constexpr InlineBasicString& assign(const T* string, size_type count) {
+constexpr InlineBasicString& assign(const T* string, size_t count) {
return static_cast<InlineBasicString&>(Copy(data(), string, count));
}
@@ -81,8 +81,7 @@ constexpr InlineBasicString& assign(const T (&array)[kCharArraySize]) {
template <typename InputIterator,
typename = string_impl::EnableIfInputIterator<InputIterator>>
constexpr InlineBasicString& assign(InputIterator start, InputIterator finish) {
- return static_cast<InlineBasicString&>(
- IteratorCopy(start, finish, data(), data() + max_size()));
+ return static_cast<InlineBasicString&>(CopyIterator(data(), start, finish));
}
constexpr InlineBasicString& assign(std::initializer_list<T> list) {
@@ -102,12 +101,12 @@ constexpr InlineBasicString& assign(const StringView& string) {
template <typename StringView,
typename = string_impl::EnableIfStringViewLike<T, StringView>>
constexpr InlineBasicString& assign(const StringView& string,
- size_type index,
- size_type count = npos) {
+ size_t index,
+ size_t count = npos) {
const std::basic_string_view<T> view = string;
PW_ASSERT(view.size() < npos);
- return static_cast<InlineBasicString&>(CopySubstr(
- data(), view.data(), static_cast<size_type>(view.size()), index, count));
+ return static_cast<InlineBasicString&>(
+ CopySubstr(data(), view.data(), view.size(), index, count));
}
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -116,17 +115,17 @@ constexpr InlineBasicString& assign(std::nullptr_t) = delete;
// Element access
-constexpr reference at(size_type index) {
+constexpr reference at(size_t index) {
PW_ASSERT(index < length());
return data()[index];
}
-constexpr const_reference at(size_type index) const {
+constexpr const_reference at(size_t index) const {
PW_ASSERT(index < length());
return data()[index];
}
-constexpr reference operator[](size_type index) { return data()[index]; }
-constexpr const_reference operator[](size_type index) const {
+constexpr reference operator[](size_t index) { return data()[index]; }
+constexpr const_reference operator[](size_t index) const {
return data()[index];
}
@@ -173,15 +172,160 @@ constexpr const_reverse_iterator crend() const noexcept {
[[nodiscard]] constexpr bool empty() const noexcept { return size() == 0u; }
// The number of characters in the string.
-constexpr size_type length() const noexcept { return size(); }
+constexpr size_t length() const noexcept { return size(); }
-constexpr size_type capacity() const noexcept { return max_size(); }
+constexpr size_t capacity() const noexcept { return max_size(); }
// Operations
constexpr void clear() { SetSizeAndTerminate(data(), 0); }
-// TODO(b/239996007): Implement insert and erase.
+constexpr InlineBasicString& insert(size_t index, size_t count, T ch) {
+ MoveExtend(data(), index, index + count);
+ return static_cast<InlineBasicString&>(FillExtend(data(), index, ch, count));
+}
+
+constexpr InlineBasicString& insert(size_t index,
+ const T* string,
+ size_t count) {
+ MoveExtend(data(), index, index + count);
+ return static_cast<InlineBasicString&>(
+ CopyExtend(data(), index, string, count));
+}
+
+template <size_t kCharArraySize>
+constexpr InlineBasicString& insert(size_t index,
+ const T (&array)[kCharArraySize]) {
+ return insert(
+ index, array, string_impl::ArrayStringLength(array, max_size()));
+}
+
+template <typename U, typename = string_impl::EnableIfNonArrayCharPointer<T, U>>
+constexpr InlineBasicString& insert(size_t index, U c_string) {
+ return insert(
+ index, c_string, string_impl::BoundedStringLength(c_string, max_size()));
+}
+
+template <size_t kOtherCapacity>
+constexpr InlineBasicString& insert(
+ size_t index, const InlineBasicString<T, kOtherCapacity>& string) {
+ return insert(index, string.data(), string.size());
+}
+
+template <size_t kOtherCapacity>
+constexpr InlineBasicString& insert(
+ size_t index,
+ const InlineBasicString<T, kOtherCapacity>& other,
+ size_t other_index,
+ size_t count = npos) {
+ PW_ASSERT(other_index <= other.size());
+ if (count == npos || count > other.size() - other_index) {
+ count = other.size() - other_index;
+ }
+ PW_ASSERT(index <= index + count);
+ MoveExtend(data(), index, index + count);
+ return static_cast<InlineBasicString&>(CopyExtendSubstr(
+ data(), index, other.data(), other.size(), other_index, count));
+}
+
+// Use a templated type rather than `const_iterator` to convince the compiler
+// to prefer `0` as an index rather than as a `nullptr`.
+template <typename U>
+constexpr iterator insert(const U* pos, size_t count, T ch) {
+ PW_ASSERT(cbegin() <= pos && pos <= cend());
+ size_t index = static_cast<size_t>(pos - cbegin());
+ PW_ASSERT(index <= index + count);
+ MoveExtend(data(), index, index + count);
+ FillExtend(data(), index, ch, count);
+ return begin() + index;
+}
+
+template <typename U>
+constexpr iterator insert(const U* pos, T ch) {
+ return insert(pos, 1, ch);
+}
+
+template <typename U, class InputIt>
+constexpr iterator insert(const U* pos, InputIt first, InputIt last) {
+ PW_ASSERT(cbegin() <= pos && pos <= cend());
+ size_t index = static_cast<size_t>(pos - cbegin());
+ size_t count = 0;
+ for (auto tmp = first; tmp != last; ++tmp) {
+ PW_ASSERT(count < max_size());
+ ++count;
+ }
+ PW_ASSERT(index <= index + count);
+ MoveExtend(data(), index, index + count);
+ CopyIteratorExtend(data(), index, first, last);
+ return begin() + index;
+}
+
+template <typename U>
+constexpr iterator insert(const U* pos, std::initializer_list<T> list) {
+ return insert(pos, list.begin(), list.end());
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+
+template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+constexpr InlineBasicString& insert(size_t index,
+ const StringView& string,
+ size_t string_index,
+ size_t count = npos) {
+ const std::basic_string_view<T> view = string;
+ PW_ASSERT(view.size() < npos);
+ PW_ASSERT(string_index <= view.size());
+ if (count == npos || count > view.size() - string_index) {
+ count = view.size() - string_index;
+ }
+ PW_ASSERT(index <= index + count);
+ MoveExtend(data(), index, index + count);
+ return static_cast<InlineBasicString&>(CopyExtendSubstr(
+ data(), index, view.data(), view.size(), string_index, count));
+}
+
+template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+constexpr InlineBasicString& insert(size_t index, const StringView& string) {
+ return insert(index, string, 0);
+}
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+constexpr InlineBasicString& erase(size_t index = 0, size_t count = npos) {
+ PW_ASSERT(index <= size());
+ size_t old_index =
+ index + std::min(count == npos ? size() : count, size() - index);
+ return static_cast<InlineBasicString&>(MoveExtend(data(), old_index, index));
+}
+
+// Use a templated type rather than `const_iterator` to convince the compiler
+// to prefer `0` as an index rather than as a `nullptr`.
+template <typename U>
+constexpr iterator erase(const U* pos) {
+ PW_ASSERT(cbegin() <= pos && pos <= cend());
+ size_t index = static_cast<size_t>(pos - cbegin());
+ size_t old_index = index + 1;
+ if (old_index <= size()) {
+ MoveExtend(data(), old_index, index);
+ }
+ return old_index <= size() ? begin() + index : end();
+}
+
+template <typename U>
+constexpr iterator erase(const U* first, const U* last) {
+ PW_ASSERT(cbegin() <= first && first <= cend());
+ size_t old_index =
+ first == cend() ? size() : static_cast<size_t>(first - cbegin());
+ size_t index = old_index;
+ for (auto tmp = first; tmp != last; ++tmp) {
+ PW_ASSERT(index < size());
+ ++old_index;
+ }
+ MoveExtend(data(), old_index, index);
+ return old_index <= size() ? begin() + index : end();
+}
constexpr void push_back(value_type ch) {
static_assert(kCapacity != 0,
@@ -195,11 +339,11 @@ constexpr void pop_back() {
PopBack(data());
}
-constexpr InlineBasicString& append(size_type count, T ch) {
- return static_cast<InlineBasicString&>(FillExtend(data(), ch, count));
+constexpr InlineBasicString& append(size_t count, T ch) {
+ return static_cast<InlineBasicString&>(FillExtend(data(), size(), ch, count));
}
-template <size_type kOtherCapacity>
+template <size_t kOtherCapacity>
constexpr InlineBasicString& append(
const InlineBasicString<T, kOtherCapacity>& string) {
static_assert(
@@ -208,17 +352,18 @@ constexpr InlineBasicString& append(
return append(string.data(), string.size());
}
-template <size_type kOtherCapacity>
+template <size_t kOtherCapacity>
constexpr InlineBasicString& append(
const InlineBasicString<T, kOtherCapacity>& other,
- size_type index,
- size_type count = npos) {
- return static_cast<InlineBasicString&>(
- CopyExtendSubstr(data(), other.data(), other.size(), index, count));
+ size_t index,
+ size_t count = npos) {
+ return static_cast<InlineBasicString&>(CopyExtendSubstr(
+ data(), size(), other.data(), other.size(), index, count));
}
-constexpr InlineBasicString& append(const T* string, size_type count) {
- return static_cast<InlineBasicString&>(CopyExtend(data(), string, count));
+constexpr InlineBasicString& append(const T* string, size_t count) {
+ return static_cast<InlineBasicString&>(
+ CopyExtend(data(), size(), string, count));
}
template <size_t kCharArraySize>
@@ -239,7 +384,7 @@ template <typename InputIterator,
typename = string_impl::EnableIfInputIterator<InputIterator>>
constexpr InlineBasicString& append(InputIterator first, InputIterator last) {
return static_cast<InlineBasicString&>(
- IteratorExtend(first, last, data() + size(), data() + max_size()));
+ CopyIteratorExtend(data(), size(), first, last));
}
constexpr InlineBasicString& append(std::initializer_list<T> list) {
@@ -259,17 +404,17 @@ constexpr InlineBasicString& append(const StringView& string) {
template <typename StringView,
typename = string_impl::EnableIfStringViewLike<T, StringView>>
constexpr InlineBasicString& append(const StringView& string,
- size_type index,
- size_type count = npos) {
+ size_t index,
+ size_t count = npos) {
const std::basic_string_view<T> view = string;
PW_ASSERT(view.size() < npos);
- return static_cast<InlineBasicString&>(CopyExtendSubstr(
- data(), view.data(), static_cast<size_type>(view.size()), index, count));
+ return static_cast<InlineBasicString&>(
+ CopyExtendSubstr(data(), size(), view.data(), view.size(), index, count));
}
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
-template <size_type kOtherCapacity>
+template <size_t kOtherCapacity>
constexpr int compare(
const InlineBasicString<T, kOtherCapacity>& other) const noexcept {
return string_impl::Compare(data(), size(), other.data(), other.size());
@@ -283,9 +428,9 @@ constexpr int compare(const T* other) const {
string_impl::BoundedStringLength(other, max_size()));
}
-// TODO(b/239996007): Implement other compare overloads.
+// TODO: b/239996007 - Implement other compare overloads.
-// TODO(b/239996007): Implement other std::string functions:
+// TODO: b/239996007 - Implement other std::string functions:
//
// - starts_with
// - ends_with
@@ -293,9 +438,9 @@ constexpr int compare(const T* other) const {
// - substr
// - copy
-constexpr void resize(size_type new_size) { resize(new_size, T()); }
+constexpr void resize(size_t new_size) { resize(new_size, T()); }
-constexpr void resize(size_type new_size, T ch) {
+constexpr void resize(size_t new_size, T ch) {
return Resize(data(), new_size, ch);
}
@@ -303,16 +448,17 @@ constexpr void resize(size_type new_size, T ch) {
// buffer has a fixed size.
template <typename Operation>
constexpr void resize_and_overwrite(Operation operation) {
- const auto new_size = std::move(operation)(data(), max_size());
- PW_ASSERT(static_cast<size_t>(new_size) <= max_size());
+ const auto new_size =
+ static_cast<size_t>(std::move(operation)(data(), max_size()));
+ PW_ASSERT(new_size <= max_size());
SetSizeAndTerminate(data(), new_size);
}
-// TODO(b/239996007): Implement swap
+// TODO: b/239996007 - Implement swap
// Search
-// TODO(b/239996007): Implement std::string search functions:
+// TODO: b/239996007 - Implement std::string search functions:
//
// - find
// - rfind
diff --git a/pw_string/public/pw_string/internal/string_impl.h b/pw_string/public/pw_string/internal/string_impl.h
index 5c2eb0de1..9672d0c17 100644
--- a/pw_string/public/pw_string/internal/string_impl.h
+++ b/pw_string/public/pw_string/internal/string_impl.h
@@ -13,6 +13,7 @@
// the License.
#pragma once
+#include <limits>
#include <string> // for std::char_traits
#include <type_traits>
@@ -55,7 +56,7 @@ using EnableIfStringViewLikeButNotStringView = std::enable_if_t<
// Reserved capacity that is used to represent a generic-length
// pw::InlineString.
-PW_INLINE_VARIABLE constexpr size_type kGeneric = size_type(-1);
+PW_INLINE_VARIABLE constexpr size_t kGeneric = size_type(-1);
#if defined(__cpp_lib_constexpr_string) && __cpp_lib_constexpr_string >= 201907L
@@ -84,8 +85,19 @@ class char_traits : private std::char_traits<T> {
using std::char_traits<T>::eq;
+ static constexpr T* move(T* dest, const T* source, size_t count) {
+ if (dest < source) {
+ char_traits<T>::copy(dest, source, count);
+ } else if (source < dest) {
+ for (size_t i = count; i != 0; --i) {
+ char_traits<T>::assign(dest[i - 1], source[i - 1]);
+ }
+ }
+ return dest;
+ }
+
static constexpr T* copy(T* dest, const T* source, size_t count) {
- for (size_type i = 0; i < count; ++i) {
+ for (size_t i = 0; i < count; ++i) {
char_traits<T>::assign(dest[i], source[i]);
}
return dest;
@@ -98,12 +110,22 @@ class char_traits : private std::char_traits<T> {
// Used in static_asserts to check that a C array fits in an InlineString.
constexpr bool NullTerminatedArrayFitsInString(
- size_t null_terminated_array_size, size_type capacity) {
+ size_t null_terminated_array_size, size_t capacity) {
return null_terminated_array_size > 0u &&
null_terminated_array_size - 1 <= capacity &&
null_terminated_array_size - 1 < kGeneric;
}
+// Used to safely convert various numeric types to `size_type`.
+template <typename T>
+constexpr size_type CheckedCastToSize(T num) {
+ static_assert(std::is_unsigned<T>::value,
+ "Attempted to convert signed value to string length, but only "
+ "unsigned types are allowed.");
+ PW_ASSERT(num < std::numeric_limits<size_type>::max());
+ return static_cast<size_type>(num);
+}
+
// Constexpr utility functions for pw::InlineString. These are NOT intended for
// general use. These mostly map directly to general purpose standard library
// utilities that are not constexpr until C++20.
@@ -113,8 +135,8 @@ constexpr bool NullTerminatedArrayFitsInString(
// std::char_traits<T>::length, which is unbounded. The string must contain at
// least one character.
template <typename T>
-constexpr size_type BoundedStringLength(const T* string, size_type capacity) {
- size_type length = 0;
+constexpr size_t BoundedStringLength(const T* string, size_t capacity) {
+ size_t length = 0;
for (; length <= capacity; ++length) {
if (char_traits<T>::eq(string[length], T())) {
break;
@@ -125,49 +147,67 @@ constexpr size_type BoundedStringLength(const T* string, size_type capacity) {
// As with std::string, InlineString treats literals and character arrays as
// null-terminated strings. ArrayStringLength checks that the array size fits
-// within size_type and asserts if no null terminator was found in the array.
+// within size_t and asserts if no null terminator was found in the array.
template <typename T>
-constexpr size_type ArrayStringLength(const T* array,
- size_type max_string_length,
- size_type capacity) {
- const size_type max_length = std::min(max_string_length, capacity);
- const size_type length = BoundedStringLength(array, max_length);
+constexpr size_t ArrayStringLength(const T* array,
+ size_t max_string_length,
+ size_t capacity) {
+ const size_t max_length = std::min(max_string_length, capacity);
+ const size_t length = BoundedStringLength(array, max_length);
PW_ASSERT(length <= max_string_length); // The array is not null terminated
return length;
}
template <typename T, size_t kCharArraySize>
-constexpr size_type ArrayStringLength(const T (&array)[kCharArraySize],
- size_type capacity) {
+constexpr size_t ArrayStringLength(const T (&array)[kCharArraySize],
+ size_t capacity) {
static_assert(kCharArraySize > 0u, "C arrays cannot have a length of 0");
static_assert(kCharArraySize - 1 < kGeneric,
"The size of this literal or character array is too large "
- "for pw::InlineString<>::size_type");
+ "for pw::InlineString<>::size_t");
return ArrayStringLength(
- array, static_cast<size_type>(kCharArraySize - 1), capacity);
+ array, static_cast<size_t>(kCharArraySize - 1), capacity);
}
// Constexpr version of std::copy that returns the number of copied characters.
+// Does NOT null-terminate the string.
template <typename InputIterator, typename T>
-constexpr size_type IteratorCopyAndTerminate(InputIterator begin,
- InputIterator end,
- T* const string_begin,
- const T* const string_end) {
+constexpr size_t IteratorCopy(InputIterator begin,
+ InputIterator end,
+ T* const string_begin,
+ const T* const string_end) {
T* current_position = string_begin;
- for (InputIterator it = begin; it != end; ++it) {
- PW_ASSERT(current_position != string_end);
- char_traits<T>::assign(*current_position++, *it);
+
+ // If `InputIterator` is a `LegacyRandomAccessIterator`, the bounds check can
+ // be done up front, allowing the compiler more flexibility in optimizing the
+ // loop.
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // constexpr-if is a C++17 feature
+ using category =
+ typename std::iterator_traits<InputIterator>::iterator_category;
+ if constexpr (std::is_same_v<category, std::random_access_iterator_tag>) {
+ PW_ASSERT(begin <= end);
+ PW_ASSERT(end - begin <= string_end - string_begin);
+ for (InputIterator it = begin; it != end; ++it) {
+ char_traits<T>::assign(*current_position++, *it);
+ }
+ } else {
+#else
+ {
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+ for (InputIterator it = begin; it != end; ++it) {
+ PW_ASSERT(current_position != string_end);
+ char_traits<T>::assign(*current_position++, *it);
+ }
}
- char_traits<T>::assign(*current_position, T()); // Null terminate
- return static_cast<size_type>(current_position - string_begin);
+ return static_cast<size_t>(current_position - string_begin);
}
// Constexpr lexicographical comparison.
template <typename T>
constexpr int Compare(const T* lhs,
- size_type lhs_size,
+ size_t lhs_size,
const T* rhs,
- size_type rhs_size) noexcept {
+ size_t rhs_size) noexcept {
int result = char_traits<T>::compare(lhs, rhs, std::min(lhs_size, rhs_size));
if (result != 0 || lhs_size == rhs_size) {
return result;
diff --git a/pw_string/public/pw_string/string.h b/pw_string/public/pw_string/string.h
index a6041f52a..7e544b192 100644
--- a/pw_string/public/pw_string/string.h
+++ b/pw_string/public/pw_string/string.h
@@ -59,7 +59,7 @@ namespace pw {
///
/// See also `pw::InlineString`, which is an alias of
/// `pw::InlineBasicString<char>` and is equivalent to `std::string`.
-template <typename T, string_impl::size_type kCapacity = string_impl::kGeneric>
+template <typename T, size_t kCapacity = string_impl::kGeneric>
class InlineBasicString final
: public InlineBasicString<T, string_impl::kGeneric> {
public:
@@ -84,19 +84,19 @@ class InlineBasicString final
constexpr InlineBasicString() noexcept
: InlineBasicString<T, string_impl::kGeneric>(kCapacity), buffer_() {}
- constexpr InlineBasicString(size_type count, T ch) : InlineBasicString() {
+ constexpr InlineBasicString(size_t count, T ch) : InlineBasicString() {
Fill(data(), ch, count);
}
- template <size_type kOtherCapacity>
+ template <size_t kOtherCapacity>
constexpr InlineBasicString(const InlineBasicString<T, kOtherCapacity>& other,
- size_type index,
- size_type count = npos)
+ size_t index,
+ size_t count = npos)
: InlineBasicString() {
CopySubstr(data(), other.data(), other.size(), index, count);
}
- constexpr InlineBasicString(const T* string, size_type count)
+ constexpr InlineBasicString(const T* string, size_t count)
: InlineBasicString() {
Copy(data(), string, count);
}
@@ -120,7 +120,7 @@ class InlineBasicString final
typename = string_impl::EnableIfInputIterator<InputIterator>>
constexpr InlineBasicString(InputIterator start, InputIterator finish)
: InlineBasicString() {
- IteratorCopy(start, finish, data(), data() + max_size());
+ CopyIterator(data(), start, finish);
}
// Use the default copy for InlineBasicString with the same capacity.
@@ -128,7 +128,7 @@ class InlineBasicString final
// When copying from an InlineBasicString with a different capacity, check
// that the destination capacity is at least as large as the source capacity.
- template <size_type kOtherCapacity>
+ template <size_t kOtherCapacity>
constexpr InlineBasicString(const InlineBasicString<T, kOtherCapacity>& other)
: InlineBasicString(other.data(), other.size()) {
static_assert(
@@ -169,8 +169,8 @@ class InlineBasicString final
template <typename StringView,
typename = string_impl::EnableIfStringViewLike<T, StringView>>
constexpr InlineBasicString(const StringView& string,
- size_type index,
- size_type count)
+ size_t index,
+ size_t count)
: InlineBasicString() {
const std::basic_string_view<T> view = string;
CopySubstr(data(), view.data(), view.size(), index, count);
@@ -185,7 +185,7 @@ class InlineBasicString final
default;
// Checks capacity rather than current size.
- template <size_type kOtherCapacity>
+ template <size_t kOtherCapacity>
constexpr InlineBasicString& operator=(
const InlineBasicString<T, kOtherCapacity>& other) {
return assign<kOtherCapacity>(other); // NOLINT
@@ -223,7 +223,7 @@ class InlineBasicString final
constexpr InlineBasicString& operator=(std::nullptr_t) = delete;
- template <size_type kOtherCapacity>
+ template <size_t kOtherCapacity>
constexpr InlineBasicString& operator+=(
const InlineBasicString<T, kOtherCapacity>& string) {
return append(string);
@@ -269,7 +269,7 @@ class InlineBasicString final
// Use the size() function from the base, but define max_size() to return the
// kCapacity template parameter instead of reading the stored capacity value.
using InlineBasicString<T, string_impl::kGeneric>::size;
- constexpr size_type max_size() const noexcept { return kCapacity; }
+ constexpr size_t max_size() const noexcept { return kCapacity; }
// Most string functions are defined in separate file so they can be shared
// between the known capacity and generic capacity versions of
@@ -282,11 +282,12 @@ class InlineBasicString final
using InlineBasicString<T, string_impl::kGeneric>::Copy;
using InlineBasicString<T, string_impl::kGeneric>::CopySubstr;
using InlineBasicString<T, string_impl::kGeneric>::Fill;
- using InlineBasicString<T, string_impl::kGeneric>::IteratorCopy;
+ using InlineBasicString<T, string_impl::kGeneric>::CopyIterator;
using InlineBasicString<T, string_impl::kGeneric>::CopyExtend;
using InlineBasicString<T, string_impl::kGeneric>::CopyExtendSubstr;
using InlineBasicString<T, string_impl::kGeneric>::FillExtend;
- using InlineBasicString<T, string_impl::kGeneric>::IteratorExtend;
+ using InlineBasicString<T, string_impl::kGeneric>::MoveExtend;
+ using InlineBasicString<T, string_impl::kGeneric>::CopyIteratorExtend;
using InlineBasicString<T, string_impl::kGeneric>::Resize;
using InlineBasicString<T, string_impl::kGeneric>::SetSizeAndTerminate;
@@ -313,7 +314,7 @@ class InlineBasicString<T, string_impl::kGeneric> {
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
- static constexpr size_type npos = string_impl::kGeneric;
+ static constexpr size_t npos = string_impl::kGeneric;
InlineBasicString() = delete; // Must specify capacity to construct a string.
@@ -328,8 +329,8 @@ class InlineBasicString<T, string_impl::kGeneric> {
return static_cast<const InlineBasicString<T, 0>*>(this)->data();
}
- constexpr size_type size() const noexcept { return length_; }
- constexpr size_type max_size() const noexcept { return capacity_; }
+ constexpr size_t size() const noexcept { return length_; }
+ constexpr size_t max_size() const noexcept { return capacity_; }
// Most string functions are defined in separate file so they can be shared
// between the known capacity and generic capacity versions of
@@ -337,8 +338,8 @@ class InlineBasicString<T, string_impl::kGeneric> {
#include "pw_string/internal/string_common_functions.inc"
protected:
- explicit constexpr InlineBasicString(size_type capacity)
- : capacity_(capacity), length_(0) {}
+ explicit constexpr InlineBasicString(size_t capacity)
+ : capacity_(string_impl::CheckedCastToSize(capacity)), length_(0) {}
// The generic-capacity InlineBasicString<T> is not copyable or movable, but
// BasicStrings can copied or assigned through a fixed capacity derived class.
@@ -353,56 +354,52 @@ class InlineBasicString<T, string_impl::kGeneric> {
SetSizeAndTerminate(data, size() - 1);
}
- constexpr InlineBasicString& Copy(T* data,
- const T* source,
- size_type new_size);
+ constexpr InlineBasicString& Copy(T* data, const T* source, size_t new_size);
- constexpr InlineBasicString& CopySubstr(T* data,
- const T* source,
- size_type source_size,
- size_type index,
- size_type count);
+ constexpr InlineBasicString& CopySubstr(
+ T* data, const T* source, size_t source_size, size_t index, size_t count);
- constexpr InlineBasicString& Fill(T* data, T fill_char, size_type new_size);
+ constexpr InlineBasicString& Fill(T* data, T fill_char, size_t new_size);
template <typename InputIterator>
- constexpr InlineBasicString& IteratorCopy(InputIterator start,
- InputIterator finish,
- T* data_start,
- const T* data_finish) {
- set_size(string_impl::IteratorCopyAndTerminate(
- start, finish, data_start, data_finish));
- return *this;
- }
+ constexpr InlineBasicString& CopyIterator(T* data_start,
+ InputIterator begin,
+ InputIterator end);
constexpr InlineBasicString& CopyExtend(T* data,
+ size_t index,
const T* source,
- size_type count);
+ size_t count);
constexpr InlineBasicString& CopyExtendSubstr(T* data,
+ size_t index,
const T* source,
- size_type source_size,
- size_type index,
- size_type count);
+ size_t source_size,
+ size_t source_index,
+ size_t count);
constexpr InlineBasicString& FillExtend(T* data,
+ size_t index,
T fill_char,
- size_type count);
+ size_t count);
template <typename InputIterator>
- constexpr InlineBasicString& IteratorExtend(InputIterator start,
- InputIterator finish,
- T* data_start,
- const T* data_finish) {
- length_ += string_impl::IteratorCopyAndTerminate(
- start, finish, data_start, data_finish);
- return *this;
- }
+ constexpr InlineBasicString& CopyIteratorExtend(T* data,
+ size_t index,
+ InputIterator begin,
+ InputIterator end);
+
+ constexpr InlineBasicString& MoveExtend(T* data,
+ size_t index,
+ size_t new_index);
- constexpr void Resize(T* data, size_type new_size, T ch);
+ constexpr void Resize(T* data, size_t new_size, T ch);
- constexpr void set_size(size_type length) { length_ = length; }
- constexpr void SetSizeAndTerminate(T* data, size_type length) {
+ constexpr void set_size(size_t length) {
+ length_ = string_impl::CheckedCastToSize(length);
+ }
+ constexpr void SetSizeAndTerminate(T* data, size_t length) {
+ PW_ASSERT(length <= max_size());
string_impl::char_traits<T>::assign(data[length], T());
set_size(length);
}
@@ -414,7 +411,7 @@ class InlineBasicString<T, string_impl::kGeneric> {
// Provide this constant for static_assert checks. If the capacity is unknown,
// use the maximum value so that compile-time capacity checks pass. If
// overflow occurs, the operation triggers a PW_ASSERT at runtime.
- static constexpr size_type kCapacity = string_impl::kGeneric;
+ static constexpr size_t kCapacity = string_impl::kGeneric;
size_type capacity_;
size_type length_;
@@ -443,141 +440,129 @@ InlineBasicString(const T (&)[kCharArraySize])
// Operators
-// TODO(b/239996007): Implement operator+
+// TODO: b/239996007 - Implement operator+
-template <typename T,
- string_impl::size_type kLhsCapacity,
- string_impl::size_type kRhsCapacity>
+template <typename T, size_t kLhsCapacity, size_t kRhsCapacity>
constexpr bool operator==(
const InlineBasicString<T, kLhsCapacity>& lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
return lhs.compare(rhs) == 0;
}
-template <typename T,
- string_impl::size_type kLhsCapacity,
- string_impl::size_type kRhsCapacity>
+template <typename T, size_t kLhsCapacity, size_t kRhsCapacity>
constexpr bool operator!=(
const InlineBasicString<T, kLhsCapacity>& lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
return lhs.compare(rhs) != 0;
}
-template <typename T,
- string_impl::size_type kLhsCapacity,
- string_impl::size_type kRhsCapacity>
+template <typename T, size_t kLhsCapacity, size_t kRhsCapacity>
constexpr bool operator<(
const InlineBasicString<T, kLhsCapacity>& lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
return lhs.compare(rhs) < 0;
}
-template <typename T,
- string_impl::size_type kLhsCapacity,
- string_impl::size_type kRhsCapacity>
+template <typename T, size_t kLhsCapacity, size_t kRhsCapacity>
constexpr bool operator<=(
const InlineBasicString<T, kLhsCapacity>& lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
return lhs.compare(rhs) <= 0;
}
-template <typename T,
- string_impl::size_type kLhsCapacity,
- string_impl::size_type kRhsCapacity>
+template <typename T, size_t kLhsCapacity, size_t kRhsCapacity>
constexpr bool operator>(
const InlineBasicString<T, kLhsCapacity>& lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
return lhs.compare(rhs) > 0;
}
-template <typename T,
- string_impl::size_type kLhsCapacity,
- string_impl::size_type kRhsCapacity>
+template <typename T, size_t kLhsCapacity, size_t kRhsCapacity>
constexpr bool operator>=(
const InlineBasicString<T, kLhsCapacity>& lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
return lhs.compare(rhs) >= 0;
}
-template <typename T, string_impl::size_type kLhsCapacity>
+template <typename T, size_t kLhsCapacity>
constexpr bool operator==(const InlineBasicString<T, kLhsCapacity>& lhs,
const T* rhs) {
return lhs.compare(rhs) == 0;
}
-template <typename T, string_impl::size_type kRhsCapacity>
+template <typename T, size_t kRhsCapacity>
constexpr bool operator==(const T* lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) {
return rhs.compare(lhs) == 0;
}
-template <typename T, string_impl::size_type kLhsCapacity>
+template <typename T, size_t kLhsCapacity>
constexpr bool operator!=(const InlineBasicString<T, kLhsCapacity>& lhs,
const T* rhs) {
return lhs.compare(rhs) != 0;
}
-template <typename T, string_impl::size_type kRhsCapacity>
+template <typename T, size_t kRhsCapacity>
constexpr bool operator!=(const T* lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) {
return rhs.compare(lhs) != 0;
}
-template <typename T, string_impl::size_type kLhsCapacity>
+template <typename T, size_t kLhsCapacity>
constexpr bool operator<(const InlineBasicString<T, kLhsCapacity>& lhs,
const T* rhs) {
return lhs.compare(rhs) < 0;
}
-template <typename T, string_impl::size_type kRhsCapacity>
+template <typename T, size_t kRhsCapacity>
constexpr bool operator<(const T* lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) {
return rhs.compare(lhs) >= 0;
}
-template <typename T, string_impl::size_type kLhsCapacity>
+template <typename T, size_t kLhsCapacity>
constexpr bool operator<=(const InlineBasicString<T, kLhsCapacity>& lhs,
const T* rhs) {
return lhs.compare(rhs) <= 0;
}
-template <typename T, string_impl::size_type kRhsCapacity>
+template <typename T, size_t kRhsCapacity>
constexpr bool operator<=(const T* lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) {
return rhs.compare(lhs) >= 0;
}
-template <typename T, string_impl::size_type kLhsCapacity>
+template <typename T, size_t kLhsCapacity>
constexpr bool operator>(const InlineBasicString<T, kLhsCapacity>& lhs,
const T* rhs) {
return lhs.compare(rhs) > 0;
}
-template <typename T, string_impl::size_type kRhsCapacity>
+template <typename T, size_t kRhsCapacity>
constexpr bool operator>(const T* lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) {
return rhs.compare(lhs) <= 0;
}
-template <typename T, string_impl::size_type kLhsCapacity>
+template <typename T, size_t kLhsCapacity>
constexpr bool operator>=(const InlineBasicString<T, kLhsCapacity>& lhs,
const T* rhs) {
return lhs.compare(rhs) >= 0;
}
-template <typename T, string_impl::size_type kRhsCapacity>
+template <typename T, size_t kRhsCapacity>
constexpr bool operator>=(const T* lhs,
const InlineBasicString<T, kRhsCapacity>& rhs) {
return rhs.compare(lhs) <= 0;
}
-// TODO(b/239996007): Implement other comparison operator overloads.
+// TODO: b/239996007 - Implement other comparison operator overloads.
// Aliases
/// @brief `pw::InlineString` is an alias of `pw::InlineBasicString<char>` and
/// is equivalent to `std::string`.
-template <string_impl::size_type kCapacity = string_impl::kGeneric>
+template <size_t kCapacity = string_impl::kGeneric>
using InlineString = InlineBasicString<char, kCapacity>;
// Function implementations
@@ -594,7 +579,7 @@ template <typename T>
constexpr InlineBasicString<T, string_impl::kGeneric>&
InlineBasicString<T, string_impl::kGeneric>::Copy(T* data,
const T* source,
- size_type new_size) {
+ size_t new_size) {
PW_ASSERT(new_size <= max_size());
string_impl::char_traits<T>::copy(data, source, new_size);
SetSizeAndTerminate(data, new_size);
@@ -603,36 +588,22 @@ InlineBasicString<T, string_impl::kGeneric>::Copy(T* data,
template <typename T>
constexpr InlineBasicString<T, string_impl::kGeneric>&
-InlineBasicString<T, string_impl::kGeneric>::CopySubstr(T* data,
- const T* source,
- size_type source_size,
- size_type index,
- size_type count) {
+InlineBasicString<T, string_impl::kGeneric>::CopySubstr(
+ T* data, const T* source, size_t source_size, size_t index, size_t count) {
PW_ASSERT(index <= source_size);
- return Copy(data,
- source + index,
- std::min(count, static_cast<size_type>(source_size - index)));
-}
-
-template <typename T>
-constexpr InlineBasicString<T, string_impl::kGeneric>&
-InlineBasicString<T, string_impl::kGeneric>::Fill(T* data,
- T fill_char,
- size_type new_size) {
- PW_ASSERT(new_size <= max_size());
- string_impl::char_traits<T>::assign(data, new_size, fill_char);
- SetSizeAndTerminate(data, new_size);
- return *this;
+ return Copy(data, source + index, std::min(count, source_size - index));
}
template <typename T>
constexpr InlineBasicString<T, string_impl::kGeneric>&
InlineBasicString<T, string_impl::kGeneric>::CopyExtend(T* data,
+ size_t index,
const T* source,
- size_type count) {
- PW_ASSERT(count <= max_size() - size());
- string_impl::char_traits<T>::copy(data + size(), source, count);
- SetSizeAndTerminate(data, size() + count);
+ size_t count) {
+ PW_ASSERT(index <= size());
+ PW_ASSERT(count <= max_size() - index);
+ string_impl::char_traits<T>::copy(data + index, source, count);
+ SetSizeAndTerminate(data, std::max(size(), index + count));
return *this;
}
@@ -640,32 +611,83 @@ template <typename T>
constexpr InlineBasicString<T, string_impl::kGeneric>&
InlineBasicString<T, string_impl::kGeneric>::CopyExtendSubstr(
T* data,
+ size_t index,
const T* source,
- size_type source_size,
- size_type index,
- size_type count) {
- PW_ASSERT(index <= source_size);
- return CopyExtend(
- data,
- source + index,
- std::min(count, static_cast<size_type>(source_size - index)));
+ size_t source_size,
+ size_t source_index,
+ size_t count) {
+ PW_ASSERT(source_index <= source_size);
+ return CopyExtend(data,
+ index,
+ source + source_index,
+ std::min(count, source_size - source_index));
+ return *this;
+}
+
+template <typename T>
+template <typename InputIterator>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::CopyIterator(T* data,
+ InputIterator begin,
+ InputIterator end) {
+ size_t length =
+ string_impl::IteratorCopy(begin, end, data, data + max_size());
+ SetSizeAndTerminate(data, length);
+ return *this;
+}
+
+template <typename T>
+template <typename InputIterator>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::CopyIteratorExtend(
+ T* data, size_t index, InputIterator begin, InputIterator end) {
+ size_t length =
+ string_impl::IteratorCopy(begin, end, data + index, data + max_size());
+ SetSizeAndTerminate(data, std::max(size(), index + length));
+ return *this;
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::Fill(T* data,
+ T fill_char,
+ size_t new_size) {
+ PW_ASSERT(new_size <= max_size());
+ string_impl::char_traits<T>::assign(data, new_size, fill_char);
+ SetSizeAndTerminate(data, new_size);
return *this;
}
template <typename T>
constexpr InlineBasicString<T, string_impl::kGeneric>&
InlineBasicString<T, string_impl::kGeneric>::FillExtend(T* data,
+ size_t index,
T fill_char,
- size_type count) {
- PW_ASSERT(count <= max_size() - size());
- string_impl::char_traits<T>::assign(data + size(), count, fill_char);
- SetSizeAndTerminate(data, size() + count);
+ size_t count) {
+ PW_ASSERT(index <= size());
+ PW_ASSERT(count <= max_size() - index);
+ string_impl::char_traits<T>::assign(data + index, count, fill_char);
+ SetSizeAndTerminate(data, std::max(size(), index + count));
+ return *this;
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::MoveExtend(T* data,
+ size_t index,
+ size_t new_index) {
+ PW_ASSERT(index <= size());
+ PW_ASSERT(new_index <= max_size());
+ PW_ASSERT(size() - index <= max_size() - new_index);
+ string_impl::char_traits<T>::move(
+ data + new_index, data + index, size() - index);
+ SetSizeAndTerminate(data, size() - index + new_index);
return *this;
}
template <typename T>
constexpr void InlineBasicString<T, string_impl::kGeneric>::Resize(
- T* data, size_type new_size, T ch) {
+ T* data, size_t new_size, T ch) {
PW_ASSERT(new_size <= max_size());
if (new_size > size()) {
diff --git a/pw_string/public/pw_string/to_string.h b/pw_string/public/pw_string/to_string.h
index 74923cd63..abbfd42b9 100644
--- a/pw_string/public/pw_string/to_string.h
+++ b/pw_string/public/pw_string/to_string.h
@@ -82,7 +82,7 @@ StatusWithSize ToString(const T& value, span<char> buffer) {
// it is available.
return string::Format(buffer, "%.3f", value);
} else {
- return string::FloatAsIntToString(value, buffer);
+ return string::FloatAsIntToString(static_cast<float>(value), buffer);
}
} else if constexpr (std::is_convertible_v<T, std::string_view>) {
return string::CopyStringOrNull(value, buffer);
diff --git a/pw_string/size_report/format_many_without_error_handling.cc b/pw_string/size_report/format_many_without_error_handling.cc
index fc460020c..6ddf0d52b 100644
--- a/pw_string/size_report/format_many_without_error_handling.cc
+++ b/pw_string/size_report/format_many_without_error_handling.cc
@@ -28,7 +28,7 @@
#define FORMAT_CASE(...) \
pw::string::Format(buffer, __VA_ARGS__) \
- .IgnoreError() // TODO(b/242598609): Handle Status properly
+ .IgnoreError() // TODO: b/242598609 - Handle Status properly
#else // std::snprintf
diff --git a/pw_string/string_builder.cc b/pw_string/string_builder.cc
index c034c4cae..576a83a07 100644
--- a/pw_string/string_builder.cc
+++ b/pw_string/string_builder.cc
@@ -64,7 +64,9 @@ StringBuilder& StringBuilder::append(const std::string_view& str,
size_t StringBuilder::ResizeAndTerminate(size_t chars_to_append) {
const size_t copied = std::min(chars_to_append, max_size() - size());
- *size_ += copied;
+ // NOTE: `+=` is not used in order to avoid implicit integer conversion which
+ // results in an error on some compilers.
+ *size_ = static_cast<InlineString<>::size_type>(copied + *size_);
NullTerminate();
if (buffer_.empty() || chars_to_append != copied) {
@@ -123,7 +125,9 @@ void StringBuilder::HandleStatusWithSize(StatusWithSize written) {
status_ = StatusCode(status);
}
- *size_ += written.size();
+ // NOTE: `+=` is not used in order to avoid implicit integer conversion which
+ // results in an error on some compilers.
+ *size_ = static_cast<InlineString<>::size_type>(written.size() + *size_);
}
void StringBuilder::SetErrorStatus(Status status) {
diff --git a/pw_string/string_builder_test.cc b/pw_string/string_builder_test.cc
index ad05c8a15..5252dc6b9 100644
--- a/pw_string/string_builder_test.cc
+++ b/pw_string/string_builder_test.cc
@@ -192,26 +192,30 @@ TEST(StringBuilder, Append_NonTerminatedString) {
TEST(StringBuilder, Append_Chars) {
StringBuffer<8> sb;
- EXPECT_TRUE(sb.append(7, '?').ok());
+ size_t count = 7;
+ EXPECT_TRUE(sb.append(count, '?').ok());
EXPECT_STREQ("???????", sb.data());
}
TEST(StringBuilder, Append_Chars_Full) {
StringBuffer<8> sb;
- EXPECT_EQ(Status::ResourceExhausted(), sb.append(8, '?').last_status());
+ size_t count = 8;
+ EXPECT_EQ(Status::ResourceExhausted(), sb.append(count, '?').last_status());
EXPECT_STREQ("???????", sb.data());
}
TEST(StringBuilder, Append_Chars_ToEmpty) {
StringBuilder sb(span<char>{});
- EXPECT_EQ(Status::ResourceExhausted(), sb.append(1, '?').last_status());
+ size_t count = 1;
+ EXPECT_EQ(Status::ResourceExhausted(), sb.append(count, '?').last_status());
}
TEST(StringBuilder, Append_PartialCString) {
StringBuffer<12> sb;
- EXPECT_TRUE(sb.append("123456", 4).ok());
+ size_t count = 4;
+ EXPECT_TRUE(sb.append("123456", count).ok());
EXPECT_EQ(4u, sb.size());
EXPECT_STREQ("1234", sb.data());
}
@@ -225,7 +229,9 @@ TEST(StringBuilder, Append_CString) {
TEST(StringBuilder, Append_CString_Full) {
auto sb = MakeString<6>("hello");
- EXPECT_EQ(Status::ResourceExhausted(), sb.append("890123", 1).last_status());
+ size_t count = 1;
+ EXPECT_EQ(Status::ResourceExhausted(),
+ sb.append("890123", count).last_status());
EXPECT_EQ(Status::ResourceExhausted(), sb.status());
EXPECT_EQ(sb.max_size(), sb.size());
EXPECT_STREQ("hello", sb.data());
@@ -239,13 +245,16 @@ TEST(StringBuilder, Append_StringView) {
TEST(StringBuilder, Append_StringView_Substring) {
auto sb = MakeString<32>("I like ");
- EXPECT_TRUE(sb.append("your shoes!!!"sv, 5, 5).ok());
+ size_t position = 5;
+ size_t count = 5;
+ EXPECT_TRUE(sb.append("your shoes!!!"sv, position, count).ok());
EXPECT_EQ("I like shoes"sv, sb);
}
TEST(StringBuilder, Append_StringView_RemainingSubstring) {
auto sb = MakeString<32>("I like ");
- EXPECT_TRUE(sb.append("your shoes!!!"sv, 5).ok());
+ size_t count = 5;
+ EXPECT_TRUE(sb.append("your shoes!!!"sv, count).ok());
EXPECT_EQ("I like shoes!!!"sv, sb);
}
diff --git a/pw_string/string_test.cc b/pw_string/string_test.cc
index 1a20f00cf..efb78cbbb 100644
--- a/pw_string/string_test.cc
+++ b/pw_string/string_test.cc
@@ -103,11 +103,11 @@ TEST(InlineString, DeduceBasicString_Char) {
}
TEST(InlineString, DeduceBasicString_Int) {
- constexpr long kLongArray[4] = {0, 1, 2, 0};
- InlineBasicString string_3 = kLongArray;
- static_assert(std::is_same_v<decltype(string_3), InlineBasicString<long, 3>>);
+ constexpr char kCharArray[4] = {0, 1, 2, 0};
+ InlineBasicString string_3 = kCharArray;
+ static_assert(std::is_same_v<decltype(string_3), InlineBasicString<char, 3>>);
- EXPECT_EQ(string_3, InlineBasicString(kLongArray));
+ EXPECT_EQ(string_3, InlineBasicString(kCharArray));
}
// Test CTAD on the InlineString alias, if supported.
@@ -1209,7 +1209,7 @@ TEST(InlineString, Size) {
static_assert(kEmptyCapacity10.size() == 0u, "0"); // NOLINT
static_assert(kSize5Capacity10.size() == 5u, "5");
- static_assert(kEmptyCapacity10.length() == 0u, "0");
+ static_assert(kEmptyCapacity10.length() == 0u, "0"); // NOLINT
static_assert(kSize5Capacity10.length() == 5u, "5");
}
@@ -1238,9 +1238,285 @@ TEST(InlineString, Clear) {
TEST_STRING(InlineString<8>("!!"), str.clear(); str.assign("?"), "?");
}
-// TODO(b/239996007): Test insert.
+// insert
-// TODO(b/239996007): Test erase.
+TEST(InlineString, Insert_CharactersAtIndex) {
+ TEST_STRING(InlineString<1>(), str.insert(0, 0, 'b'), "");
+ TEST_STRING(InlineString<1>("a"), str.insert(0, 0, 'b'), "a");
+ TEST_STRING(InlineString<10>("a"), str.insert(0, 2, 'b'), "bba");
+ TEST_STRING(InlineString<10>("a"), str.insert(1, 2, 'b'), "abb");
+ TEST_STRING(InlineString<10>(), str.insert(0, 10, 'b'), "bbbbbbbbbb");
+}
+#if PW_NC_TEST(Insert_CharactersAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(1, 2, '?');
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_PointerSizeAtIndex) {
+ TEST_STRING(InlineString<1>(), str.insert(0, "", 0), "");
+ TEST_STRING(InlineString<1>("a"), str.insert(0, "b", 0), "a");
+ TEST_STRING(InlineString<10>("a"), str.insert(0, "bb", 2), "bba");
+ TEST_STRING(InlineString<10>("a"), str.insert(1, "bb", 2), "abb");
+ TEST_STRING(
+ InlineString<10>(), str.insert(0, "bbbbbbbbbb", 10), "bbbbbbbbbb");
+}
+#if PW_NC_TEST(Insert_PointerSizeAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(1, "23", 2);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_ArrayAtIndex) {
+ TEST_STRING(InlineString<1>(), fixed_str.insert(0, ""), "");
+ TEST_STRING(InlineString<2>(), fixed_str.insert(0, "a"), "a");
+ TEST_STRING(InlineString<6>(), fixed_str.insert(0, "12345"), "12345");
+
+ TEST_STRING(InlineString<1>({'a'}), fixed_str.insert(1, ""), "a");
+ TEST_STRING(InlineString<2>("a"), fixed_str.insert(1, "a"), "aa");
+ TEST_STRING(InlineString<6>("a"), fixed_str.insert(1, "12345"), "a12345");
+}
+#if PW_NC_TEST(Insert_ArrayAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<4> str({0, 1});
+ return str.insert(1, "123");
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_PointerAtIndex) {
+ TEST_STRING(InlineString<0>(), str.insert(0, kPointer0), "");
+ TEST_STRING(InlineString<10>(), str.insert(0, kPointer10), "9876543210");
+ TEST_STRING(
+ InlineString<10>("abc"), str.insert(1, kPointer10 + 5), "a43210bc");
+ TEST_STRING(
+ InlineString<13>("abc"), str.insert(3, kPointer10), "abc9876543210");
+}
+#if PW_NC_TEST(Insert_PointerAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(1, kPointer10 + 8);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_BasicStringAtIndex) {
+ TEST_STRING(InlineString<0>(), str.insert(0, kEmptyCapacity0), "");
+ TEST_STRING(InlineString<10>(), str.insert(0, kEmptyCapacity10), "");
+ TEST_STRING(InlineString<10>(), str.insert(0, kSize5Capacity10), "12345");
+ TEST_STRING(
+ InlineString<10>(), str.insert(0, kSize10Capacity10), "1234567890");
+
+ TEST_STRING(InlineString<1>({'a'}), str.insert(0, kEmptyCapacity0), "a");
+ TEST_STRING(InlineString<11>("a"), str.insert(1, kEmptyCapacity10), "a");
+ TEST_STRING(
+ InlineString<12>("aa"), str.insert(1, kSize5Capacity10), "a12345a");
+ TEST_STRING(
+ InlineString<12>("aa"), str.insert(2, kSize10Capacity10), "aa1234567890");
+}
+
+#if PW_NC_TEST(Insert_BasicStringAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(new_index <= max_size\(\)\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(1, kSize5Capacity10);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_BasicStringSubstrAtIndex) {
+ TEST_STRING(
+ InlineString<1>({'a'}), str.insert(0, kEmptyCapacity0, 0, 0), "a");
+ TEST_STRING(
+ InlineString<11>("a"), str.insert(1, kSize10Capacity10, 10, 0), "a");
+ TEST_STRING(InlineString<12>("aa"),
+ str.insert(1, kSize10Capacity10, 3, 5),
+ "a45678a");
+ TEST_STRING(InlineString<12>("aa"),
+ str.insert(2, kSize10Capacity10, 0, 10),
+ "aa1234567890");
+}
+#if PW_NC_TEST(Insert_BasicStringSubstrAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(1, kSize5Capacity10, 1, 2);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_CharactersAtPosition) {
+ TEST_STRING(InlineString<1>(), str.insert(str.begin(), 0, 'b'), "");
+ TEST_STRING(InlineString<1>("a"), str.insert(str.begin(), 0, 'b'), "a");
+ TEST_STRING(InlineString<10>("a"), str.insert(str.begin(), 2, 'b'), "bba");
+ TEST_STRING(
+ InlineString<10>("a"), str.insert(str.begin() + 1, 2, 'b'), "abb");
+ TEST_STRING(InlineString<10>(), str.insert(str.end(), 10, 'b'), "bbbbbbbbbb");
+}
+
+#if PW_NC_TEST(Insert_CharactersAtPosition_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(str.begin() + 1, 2, '?');
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_CharacterAtPosition) {
+ TEST_STRING(InlineString<1>(), str.insert(str.begin(), 'b'), "b");
+ TEST_STRING(InlineString<10>("aa"), str.insert(str.begin(), 'b'), "baa");
+ TEST_STRING(InlineString<10>("aa"), str.insert(str.begin() + 1, 'b'), "aba");
+ TEST_STRING(InlineString<10>("aa"), str.insert(str.begin() + 2, 'b'), "aab");
+ TEST_STRING(InlineString<10>("aa"), str.insert(str.end(), 'b'), "aab");
+}
+#if PW_NC_TEST(Insert_CharacterAtPosition_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<2> str({0, 1});
+ return str.insert(str.begin() + 1, '?');
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_IteratorsAtPosition) {
+ TEST_STRING(InlineString<0>(),
+ str.insert(str.begin(), kEvenNumbers0, kEvenNumbers0),
+ "");
+ TEST_STRING(InlineString<10>(),
+ str.insert(str.end(), kEvenNumbers0, kEvenNumbers0),
+ "");
+ TEST_STRING(InlineString<10>(),
+ str.insert(str.end(), kEvenNumbers0, kEvenNumbers0),
+ "");
+ TEST_STRING(InlineString<10>(),
+ str.insert(str.begin(), kEvenNumbers0, kEvenNumbers8),
+ "\0\2\4\6");
+ TEST_STRING(InlineString<10>(),
+ str.insert(str.end(), kEvenNumbers0, kEvenNumbers8),
+ "\0\2\4\6");
+ TEST_STRING(InlineString<10>("aa"),
+ str.insert(str.begin(), kEvenNumbers0, kEvenNumbers8),
+ "\0\2\4\6aa");
+ TEST_STRING(InlineString<10>("aa"),
+ str.insert(str.begin() + 1, kEvenNumbers0, kEvenNumbers8),
+ "a\0\2\4\6a");
+ TEST_STRING(InlineString<10>("aa"),
+ str.insert(str.begin() + 2, kEvenNumbers0, kEvenNumbers8),
+ "aa\0\2\4\6");
+ TEST_STRING(InlineString<10>("aa"),
+ str.insert(str.end(), kEvenNumbers0, kEvenNumbers8),
+ "aa\0\2\4\6");
+}
+#if PW_NC_TEST(Insert_IteratorsAtPosition_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(count < max_size\(\)\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.insert(str.begin() + 1, kEvenNumbers0, kEvenNumbers8);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_InitializerListAtPosition) {
+ TEST_STRING(InlineString<0>(), str.insert(str.begin(), {}), "");
+ TEST_STRING(InlineString<10>(), str.insert(str.end(), {1, 2, 3}), "\1\2\3");
+ TEST_STRING(
+ InlineString<10>("abc"), str.insert(str.begin(), {1, 2, 3}), "\1\2\3abc");
+ TEST_STRING(InlineString<10>("abc"),
+ str.insert(str.begin() + 1, {1, 2, 3}),
+ "a\1\2\3bc");
+ TEST_STRING(InlineString<10>("abc"),
+ str.insert(str.begin() + 3, {1, 2, 3}),
+ "abc\1\2\3");
+ TEST_STRING(
+ InlineString<5>("abc"), str.insert(str.end(), {'4', '5'}), "abc45");
+}
+#if PW_NC_TEST(Insert_InitializerListAtPosition_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.insert(str.begin() + 1, {3});
+}();
+#endif // PW_NC_TEST
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+TEST(InlineString, Insert_StringViewAtIndex) {
+ TEST_STRING(InlineString<0>(), str.insert(0, ""sv), "");
+ TEST_STRING(InlineString<10>("a"), str.insert(0, ""sv), "a");
+ TEST_STRING(InlineString<10>("abc"), str.insert(0, "123"sv), "123abc");
+ TEST_STRING(InlineString<10>("abc"), str.insert(1, "123"sv), "a123bc");
+ TEST_STRING(InlineString<5>("abc"), str.insert(3, "45"sv), "abc45");
+}
+#if PW_NC_TEST(Insert_StringViewAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.insert(1, "3"sv);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Insert_StringViewSubstrAtIndex) {
+ TEST_STRING(InlineString<0>(), str.insert(0, ""sv, 0), "");
+ TEST_STRING(InlineString<0>(), str.insert(0, ""sv, 0, 0), "");
+ TEST_RUNTIME_STRING(
+ InlineString<5>("aa"), str.insert(0, "123"sv, 0), "123aa");
+ TEST_RUNTIME_STRING(
+ InlineString<10>("aa"), str.insert(1, "123"sv, 1, 0), "aa");
+ TEST_RUNTIME_STRING(
+ InlineString<10>("aa"), str.insert(1, "123"sv, 1, 1), "a2a");
+ TEST_RUNTIME_STRING(
+ InlineString<10>("aa"), str.insert(1, "123"sv, 1, 99), "a23a");
+ TEST_RUNTIME_STRING(
+ InlineString<10>("aa"), str.insert(2, "123"sv, 1, 99), "aa23");
+ TEST_RUNTIME_STRING(
+ InlineString<10>("aa"), str.insert(2, "123"sv, 3, 99), "aa");
+}
+#if PW_NC_TEST(Insert_StringViewSubstrAtIndex_DoesNotFit)
+PW_NC_EXPECT("PW_ASSERT\(size\(\) - index <= max_size\(\) - new_index\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.insert(1, "34"sv, 1);
+}();
+#endif // PW_NC_TEST
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+// erase.
+
+TEST(InlineString, Erase_CharactersAtIndex) {
+ TEST_STRING(InlineString<0>(), str.erase(), "");
+ TEST_STRING(InlineString<10>("abc"), str.erase(), "");
+ TEST_STRING(InlineString<10>("abc"), str.erase(0), "");
+ TEST_STRING(InlineString<10>("abc"), str.erase(1), "a");
+ TEST_STRING(InlineString<10>("abc"), str.erase(1, 1), "ac");
+ TEST_STRING(InlineString<10>("abc"), str.erase(1, 10), "a");
+ TEST_STRING(InlineString<10>("abc"), str.erase(3, 10), "abc");
+}
+#if PW_NC_TEST(Erase_IndexOutOfRange)
+PW_NC_EXPECT("PW_ASSERT\(index <= size\(\)\)");
+[[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str("abc");
+ return str.erase(4, 2);
+}();
+#endif // PW_NC_TEST
+
+TEST(InlineString, Erase_CharacterAtPosition) {
+ TEST_STRING(InlineString<3>(), str.erase(str.begin()), "");
+ TEST_STRING(InlineString<3>(), str.erase(str.end()), "");
+ TEST_STRING(InlineString<3>("abc"), str.erase(str.begin()), "bc");
+ TEST_STRING(InlineString<3>("abc"), str.erase(str.begin() + 1), "ac");
+ TEST_STRING(InlineString<3>("abc"), str.erase(str.begin() + 2), "ab");
+ TEST_STRING(InlineString<3>("abc"), str.erase(str.end()), "abc");
+}
+
+TEST(InlineString, Erase_CharactersInRange) {
+ TEST_STRING(
+ InlineString<3>("abc"), str.erase(str.begin(), str.begin()), "abc");
+ TEST_STRING(InlineString<3>("abc"), str.erase(str.end(), str.end()), "abc");
+ TEST_STRING(InlineString<3>("abc"), str.erase(str.begin(), str.end()), "");
+ TEST_STRING(
+ InlineString<3>("abc"), str.erase(str.begin(), str.begin() + 1), "bc");
+ TEST_STRING(
+ InlineString<3>("abc"), str.erase(str.begin() + 1, str.end()), "a");
+}
TEST(InlineString, PushBack) {
TEST_STRING(InlineString<1>(), str.push_back('#'), "#");
@@ -1289,7 +1565,7 @@ TEST(InlineString, Append_BasicString) {
InlineString<11>("a"), str.append(kSize10Capacity10), "a1234567890");
#if PW_NC_TEST(Append_BasicString_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1});
return str.append(kSize5Capacity10);
@@ -1309,7 +1585,7 @@ TEST(InlineString, Append_Characters) {
TEST_STRING(InlineString<6>("Hi"), str.append(4, '!'), "Hi!!!!");
#if PW_NC_TEST(Append_Characters_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index\)");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1});
return str.append(2, '?');
@@ -1326,7 +1602,7 @@ TEST(InlineString, Append_PointerSize) {
TEST_STRING(InlineString<10>("abc"), str.append("1234567", 3), "abc123");
#if PW_NC_TEST(Append_PointerSize_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1});
return str.append("23", 2);
@@ -1360,7 +1636,7 @@ TEST(InlineString, Append_Pointer) {
TEST_STRING(InlineString<13>("abc"), str.append(kPointer10), "abc9876543210");
#if PW_NC_TEST(Append_Pointer_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1});
return str.append(kPointer10 + 8);
@@ -1394,7 +1670,7 @@ TEST(InlineString, Append_InitializerList) {
TEST_STRING(InlineString<5>("abc"), str.append({'4', '5'}), "abc45");
#if PW_NC_TEST(Append_InitializerList_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1, 2});
return str.append({3});
@@ -1410,7 +1686,7 @@ TEST(InlineString, Append_StringView) {
TEST_STRING(InlineString<5>("abc"), str.append("45"sv), "abc45");
#if PW_NC_TEST(Append_StringView_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1, 2});
return str.append("3"sv);
@@ -1428,7 +1704,7 @@ TEST(InlineString, Append_StringViewSubstr) {
TEST_RUNTIME_STRING(InlineString<4>("a"), str.append("123"sv, 3, 99), "a");
#if PW_NC_TEST(Append_StringViewSubstr_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1, 2});
return str.append("34"sv, 1);
@@ -1451,7 +1727,7 @@ TEST(InlineString, AppendOperator_BasicString) {
TEST_STRING(InlineString<6>("Hi"), str.append(4, '!'), "Hi!!!!");
#if PW_NC_TEST(AppendOperator_BasicString_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1});
return str.append(kSize5Capacity10);
@@ -1502,7 +1778,7 @@ TEST(InlineString, AppendOperator_Pointer) {
InlineString<13>("abc"), fixed_str += kPointer10, "abc9876543210");
#if PW_NC_TEST(AppendOperator_Pointer_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1});
return str.append(kPointer10 + 8);
@@ -1517,7 +1793,7 @@ TEST(InlineString, AppendOperator_InitializerList) {
TEST_STRING(InlineString<5>("abc"), (fixed_str += {'4', '5'}), "abc45");
#if PW_NC_TEST(AppendOperator_InitializerList_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1, 2});
return str.append({3});
@@ -1533,7 +1809,7 @@ TEST(InlineString, AppendOperator_StringView) {
TEST_STRING(InlineString<5>("abc"), fixed_str += "45"sv, "abc45");
#if PW_NC_TEST(AppendOperator_StringView_DoesNotFit)
- PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - index");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<3> str({0, 1, 2});
return str.append("3"sv);
@@ -1570,19 +1846,10 @@ TEST(InlineString, Compare) {
"less");
static_assert(InlineString<5>("abc").compare(InlineString<5>("")) > 0,
"greater");
-
- constexpr InlineBasicString<unsigned long long, 3> kUllString1(
- {0, std::numeric_limits<unsigned long long>::max(), 0});
- constexpr InlineBasicString<unsigned long long, 3> kUllString2(
- {std::numeric_limits<unsigned long long>::max(), 0});
-
- static_assert(kUllString1.compare(kUllString1) == 0, "equal");
- static_assert(kUllString1.compare(kUllString2) < 0, "less");
- static_assert(kUllString2.compare(kUllString1) > 0, "greater");
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
}
-// TODO(b/239996007): Test other pw::InlineString functions:
+// TODO: b/239996007 - Test other pw::InlineString functions:
//
// - starts_with
// - ends_with
@@ -1637,14 +1904,14 @@ TEST(InlineString, ResizeAndOverwrite) {
"A?CDE5678");
#if PW_NC_TEST(ResizeAndOverwrite_LargerThanCapacity)
- PW_NC_EXPECT("PW_ASSERT\(static_cast<size_t>\(new_size\) <= max_size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<4> str("123");
str.resize_and_overwrite([](char*, size_t) { return 5; });
return str;
}();
#elif PW_NC_TEST(ResizeAndOverwrite_NegativeSize)
- PW_NC_EXPECT("PW_ASSERT\(static_cast<size_t>\(new_size\) <= max_size\(\)\)");
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
[[maybe_unused]] constexpr auto fail = [] {
InlineString<4> str("123");
str.resize_and_overwrite([](char*, size_t) { return -1; });
@@ -1653,16 +1920,16 @@ TEST(InlineString, ResizeAndOverwrite) {
#endif // PW_NC_TEST
}
-// TODO(b/239996007): Test other pw::InlineString functions:
+// TODO: b/239996007 - Test other pw::InlineString functions:
// - swap
//
// Search
//
-// TODO(b/239996007): Test search functions.
+// TODO: b/239996007 - Test search functions.
-// TODO(b/239996007): Test operator+.
+// TODO: b/239996007 - Test operator+.
TEST(InlineString, ComparisonOperators_InlineString) {
EXPECT_EQ(InlineString<10>("a"), InlineString<10>("a"));
@@ -1800,15 +2067,31 @@ TEST(InlineString, ComparisonOperators_NullTerminatedString) {
#define PW_STRING_WRAP_TEST_EXPANSION(expr)
#endif // __cpp_constexpr >= 201603L
-#define TEST_FOR_TYPES(test_macro, ...) \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(char, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(unsigned char, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(signed char, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(short, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(int, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(unsigned, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(long, __VA_ARGS__)); \
- PW_STRING_WRAP_TEST_EXPANSION(test_macro(long long, __VA_ARGS__))
+#define TEST_FOR_TYPES_BASE(test_macro, ...) \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(char, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(wchar_t, __VA_ARGS__));
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(11)
+#define TEST_FOR_TYPES_CXX11(test_macro, ...) \
+ TEST_FOR_TYPES_BASE(test_macro, __VA_ARGS__) \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(char16_t, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(char32_t, __VA_ARGS__));
+#else
+#define TEST_FOR_TYPES_CXX11(test_macro, ...) \
+ TEST_FOR_TYPES_BASE(test_macro, __VA_ARGS__)
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(11)
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(20)
+#define TEST_FOR_TYPES_CXX20(test_macro, ...) \
+ TEST_FOR_TYPES_CXX11(test_macro, __VA_ARGS__) \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(char8_t, __VA_ARGS__));
+#else
+#define TEST_FOR_TYPES_CXX20(test_macro, ...) \
+ TEST_FOR_TYPES_CXX11(test_macro, __VA_ARGS__)
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(20)
+
+#define TEST_FOR_TYPES(test_macro, ...) \
+ TEST_FOR_TYPES_CXX20(test_macro, __VA_ARGS__)
TEST(BasicStrings, Empty) {
#define BASIC_STRINGS_EMPTY(type, capacity) \
diff --git a/pw_string/to_string_test.cc b/pw_string/to_string_test.cc
index 39a09fbdd..8ec9c498d 100644
--- a/pw_string/to_string_test.cc
+++ b/pw_string/to_string_test.cc
@@ -123,7 +123,7 @@ TEST(ToString, Float) {
if (string::internal::config::kEnableDecimalFloatExpansion) {
EXPECT_EQ(5u, ToString(0.0f, buffer).size());
EXPECT_STREQ("0.000", buffer);
- EXPECT_EQ(6u, ToString(33.444, buffer).size());
+ EXPECT_EQ(6u, ToString(33.444f, buffer).size());
EXPECT_STREQ("33.444", buffer);
EXPECT_EQ(3u, ToString(INFINITY, buffer).size());
EXPECT_STREQ("inf", buffer);
@@ -141,10 +141,11 @@ TEST(ToString, Float) {
TEST(ToString, Pointer_NonNull_WritesValue) {
CustomType custom;
- const size_t length = std::snprintf(expected,
- sizeof(expected),
- "%" PRIxPTR,
- reinterpret_cast<intptr_t>(&custom));
+ const size_t length =
+ static_cast<size_t>(std::snprintf(expected,
+ sizeof(expected),
+ "%" PRIxPTR,
+ reinterpret_cast<intptr_t>(&custom)));
EXPECT_EQ(length, ToString(&custom, buffer).size());
EXPECT_STREQ(expected, buffer);
diff --git a/pw_string/type_to_string.cc b/pw_string/type_to_string.cc
index 37311c4da..bc4ce00ef 100644
--- a/pw_string/type_to_string.cc
+++ b/pw_string/type_to_string.cc
@@ -19,6 +19,8 @@
#include <cstring>
#include <limits>
+#include "lib/stdcompat/bit.h"
+
namespace pw::string {
namespace {
@@ -59,12 +61,12 @@ StatusWithSize HandleExhaustedBuffer(span<char> buffer) {
uint_fast8_t DecimalDigitCount(uint64_t integer) {
// This fancy piece of code takes the log base 2, then approximates the
// change-of-base formula by multiplying by 1233 / 4096.
- // TODO(hepler): Replace __builtin_clzll with std::countl_zeros in C++20.
const uint_fast8_t log_10 = static_cast<uint_fast8_t>(
- (64 - __builtin_clzll(integer | 1)) * 1233 >> 12);
+ (64 - cpp20::countl_zero(integer | 1)) * 1233 >> 12);
// Adjust the estimated log base 10 by comparing against the power of 10.
- return log_10 + (integer < kPowersOf10[log_10] ? 0u : 1u);
+ return static_cast<uint_fast8_t>(log_10 +
+ (integer < kPowersOf10[log_10] ? 0u : 1u));
}
// std::to_chars is available for integers in recent versions of GCC. I looked
@@ -97,14 +99,14 @@ StatusWithSize IntToString(uint64_t value, span<char> buffer) {
lower_digits = static_cast<uint32_t>(value);
digit_count = remaining;
} else {
- lower_digits = value % max_uint32_base_power;
+ lower_digits = static_cast<uint32_t>(value % max_uint32_base_power);
digit_count = max_uint32_base_power_exponent;
value /= max_uint32_base_power;
}
// Write the specified number of digits, with leading 0s.
for (uint_fast8_t i = 0; i < digit_count; ++i) {
- buffer[--remaining] = lower_digits % base + '0';
+ buffer[--remaining] = static_cast<char>(lower_digits % base + '0');
lower_digits /= base;
}
}
@@ -120,8 +122,8 @@ StatusWithSize IntToHexString(uint64_t value,
return HandleExhaustedBuffer(buffer);
}
- for (int i = digits - 1; i >= 0; --i) {
- buffer[i] = "0123456789abcdef"[value & 0xF];
+ for (int i = static_cast<int>(digits) - 1; i >= 0; --i) {
+ buffer[static_cast<size_t>(i)] = "0123456789abcdef"[value & 0xF];
value >>= 4;
}
@@ -132,12 +134,14 @@ StatusWithSize IntToHexString(uint64_t value,
template <>
StatusWithSize IntToString(int64_t value, span<char> buffer) {
if (value >= 0) {
- return IntToString<uint64_t>(value, buffer);
+ return IntToString<uint64_t>(static_cast<uint64_t>(value), buffer);
}
// Write as an unsigned number, but leave room for the leading minus sign.
+ // Do not use std::abs since it fails for the minimum value integer.
+ const uint64_t absolute_value = -static_cast<uint64_t>(value);
auto result = IntToString<uint64_t>(
- std::abs(value), buffer.empty() ? buffer : buffer.subspan(1));
+ absolute_value, buffer.empty() ? buffer : buffer.subspan(1));
if (result.ok()) {
buffer[0] = '-';
diff --git a/pw_string/type_to_string_test.cc b/pw_string/type_to_string_test.cc
index f9fe670a3..2b2e8fe9f 100644
--- a/pw_string/type_to_string_test.cc
+++ b/pw_string/type_to_string_test.cc
@@ -346,12 +346,12 @@ TEST_F(FloatAsIntToStringTest, NegativeNan) {
}
TEST_F(FloatAsIntToStringTest, RoundDown_PrintsNearestInt) {
- EXPECT_EQ(1u, FloatAsIntToString(1.23, buffer_).size());
+ EXPECT_EQ(1u, FloatAsIntToString(1.23f, buffer_).size());
EXPECT_STREQ("1", buffer_);
}
TEST_F(FloatAsIntToStringTest, RoundUp_PrintsNearestInt) {
- EXPECT_EQ(4u, FloatAsIntToString(1234.5, buffer_).size());
+ EXPECT_EQ(4u, FloatAsIntToString(1234.5f, buffer_).size());
EXPECT_STREQ("1235", buffer_);
}
@@ -366,13 +366,13 @@ TEST_F(FloatAsIntToStringTest, RoundsToPositiveZero_PrintsZero) {
}
TEST_F(FloatAsIntToStringTest, RoundDownNegative_PrintsNearestInt) {
- volatile float x = -5.9;
+ volatile float x = -5.9f;
EXPECT_EQ(2u, FloatAsIntToString(x, buffer_).size());
EXPECT_STREQ("-6", buffer_);
}
TEST_F(FloatAsIntToStringTest, RoundUpNegative_PrintsNearestInt) {
- EXPECT_EQ(9u, FloatAsIntToString(-50000000.1, buffer_).size());
+ EXPECT_EQ(9u, FloatAsIntToString(-50000000.1f, buffer_).size());
EXPECT_STREQ("-50000000", buffer_);
}
diff --git a/pw_symbolizer/docs.rst b/pw_symbolizer/docs.rst
index 5d01216c7..7b8cfcae0 100644
--- a/pw_symbolizer/docs.rst
+++ b/pw_symbolizer/docs.rst
@@ -24,7 +24,7 @@ Pigweed to provide explicit support for all possible implementations.
``Symbolizer`` Also provides a helper function for producing nicely formatted
stack trace style dumps.
-.. code:: py
+.. code-block:: py
import pw_symbolizer
@@ -63,7 +63,7 @@ The ``FakeSymbolizer`` is utility class that implements the ``Symbolizer``
interface with a fixed database of address to ``Symbol`` mappings. This is
useful for testing, or as a no-op ``Symbolizer``.
-.. code:: py
+.. code-block:: py
import pw_symbolizer
@@ -78,10 +78,15 @@ useful for testing, or as a no-op ``Symbolizer``.
LlvmSymbolizer
==============
The ``LlvmSymbolizer`` is a python layer that wraps ``llvm-symbolizer`` to
-produce symbols from provided addresses. This module will only work if
-``llvm-symbolizer`` is available on the system ``PATH``.
+produce symbols from provided addresses. This module requires either:
-.. code:: py
+* ``llvm-symbolizer`` is available on the system ``PATH``.
+* ``llvm_symbolizer_binary`` argument is specified and points to the executable.
+
+This object also defines a ``close`` to ensure the background process is
+cleaned up deterministically.
+
+.. code-block:: py
import pw_symbolizer
diff --git a/pw_symbolizer/py/BUILD.bazel b/pw_symbolizer/py/BUILD.bazel
index fdbdfe975..06dced5d3 100644
--- a/pw_symbolizer/py/BUILD.bazel
+++ b/pw_symbolizer/py/BUILD.bazel
@@ -33,7 +33,7 @@ py_test(
# This test attempts to run subprocesses directly in the source tree, which is
# incompatible with sandboxing.
-# TODO(b/241307309): Update this test to work with bazel.
+# TODO: b/241307309 - Update this test to work with bazel.
filegroup(
name = "llvm_symbolizer_test",
# size = "small",
diff --git a/pw_symbolizer/py/llvm_symbolizer_test.py b/pw_symbolizer/py/llvm_symbolizer_test.py
index d3a2f64fb..a4bc27152 100644
--- a/pw_symbolizer/py/llvm_symbolizer_test.py
+++ b/pw_symbolizer/py/llvm_symbolizer_test.py
@@ -13,6 +13,8 @@
# the License.
"""Tests for pw_symbolizer's llvm-symbolizer based symbolization."""
+import os
+import shutil
import subprocess
import tempfile
import unittest
@@ -41,8 +43,12 @@ class TestSymbolizer(unittest.TestCase):
self.assertEqual(result.file, _CPP_TEST_FILE_NAME)
self.assertEqual(result.line, expected_symbol['Line'])
- def test_symbolization(self):
+ def _parameterized_test_symbolization(self, **llvm_symbolizer_kwargs):
"""Tests that the symbolizer can symbolize addresses properly."""
+ self.assertTrue('PW_PIGWEED_CIPD_INSTALL_DIR' in os.environ)
+ sysroot = Path(os.environ['PW_PIGWEED_CIPD_INSTALL_DIR']).joinpath(
+ "clang_sysroot"
+ )
with tempfile.TemporaryDirectory() as exe_dir:
exe_file = Path(exe_dir) / 'print_expected_symbols'
@@ -53,6 +59,7 @@ class TestSymbolizer(unittest.TestCase):
_CPP_TEST_FILE_NAME,
'-gfull',
f'-ffile-prefix-map={_MODULE_PY_DIR}=',
+ '--sysroot=%s' % sysroot,
'-std=c++17',
'-fno-pic',
'-fno-pie',
@@ -79,15 +86,39 @@ class TestSymbolizer(unittest.TestCase):
for line in process.stdout.decode().splitlines()
]
- symbolizer = pw_symbolizer.LlvmSymbolizer(exe_file)
- self._test_symbolization_results(expected_symbols, symbolizer)
-
- # Test backwards compatibility with older versions of
- # llvm-symbolizer.
- symbolizer = pw_symbolizer.LlvmSymbolizer(
- exe_file, force_legacy=True
+ with self.subTest("non-legacy"):
+ symbolizer = pw_symbolizer.LlvmSymbolizer(
+ exe_file, **llvm_symbolizer_kwargs
+ )
+ self._test_symbolization_results(expected_symbols, symbolizer)
+ symbolizer.close()
+
+ with self.subTest("backwards-compability"):
+ # Test backwards compatibility with older versions of
+ # llvm-symbolizer.
+ symbolizer = pw_symbolizer.LlvmSymbolizer(
+ exe_file, force_legacy=True, **llvm_symbolizer_kwargs
+ )
+ self._test_symbolization_results(expected_symbols, symbolizer)
+ symbolizer.close()
+
+ def test_symbolization_default_binary(self):
+ self._parameterized_test_symbolization()
+
+ def test_symbolization_specified_binary(self):
+ location = Path(
+ subprocess.run(
+ ['which', 'llvm-symbolizer'], check=True, stdout=subprocess.PIPE
+ )
+ .stdout.decode()
+ .strip()
+ )
+ with tempfile.TemporaryDirectory() as copy_dir:
+ copy_location = Path(copy_dir) / "copy-llvm-symbolizer"
+ shutil.copy(location, copy_location)
+ self._parameterized_test_symbolization(
+ llvm_symbolizer_binary=copy_location
)
- self._test_symbolization_results(expected_symbols, symbolizer)
if __name__ == '__main__':
diff --git a/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py b/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py
index b36267827..f799be529 100644
--- a/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py
+++ b/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py
@@ -25,21 +25,32 @@ from pw_symbolizer import symbolizer
class LlvmSymbolizer(symbolizer.Symbolizer):
"""A symbolizer that wraps llvm-symbolizer."""
- def __init__(self, binary: Optional[Path] = None, force_legacy=False):
+ def __init__(
+ self,
+ binary: Optional[Path] = None,
+ force_legacy=False,
+ llvm_symbolizer_binary: Optional[Path] = None,
+ ):
# Lets destructor return cleanly if the binary is not found.
self._symbolizer = None
- if shutil.which('llvm-symbolizer') is None:
- raise FileNotFoundError(
- 'llvm-symbolizer not installed. Run bootstrap, or download '
- 'LLVM (https://github.com/llvm/llvm-project/releases/) and add '
- 'the tools to your system PATH'
- )
+ if llvm_symbolizer_binary:
+ self._symbolizer_binary = str(llvm_symbolizer_binary)
+ else:
+ self._symbolizer_binary = 'llvm-symbolizer'
+ if shutil.which(self._symbolizer_binary) is None:
+ raise FileNotFoundError(
+ 'llvm-symbolizer not installed. Run bootstrap, or download '
+ 'LLVM (https://github.com/llvm/llvm-project/releases/) and '
+ 'add the tools to your system PATH'
+ )
# Prefer JSON output as it's easier to decode.
if force_legacy:
self._json_mode = False
else:
- self._json_mode = LlvmSymbolizer._is_json_compatibile()
+ self._json_mode = LlvmSymbolizer._is_json_compatibile(
+ self._symbolizer_binary
+ )
if binary is not None:
if not binary.exists():
@@ -47,7 +58,7 @@ class LlvmSymbolizer(symbolizer.Symbolizer):
output_style = 'JSON' if self._json_mode else 'LLVM'
cmd = [
- 'llvm-symbolizer',
+ self._symbolizer_binary,
'--no-inlines',
'--demangle',
'--functions',
@@ -62,15 +73,22 @@ class LlvmSymbolizer(symbolizer.Symbolizer):
self._lock: threading.Lock = threading.Lock()
def __del__(self):
- if self._symbolizer:
+ self.close()
+
+ def close(self):
+ """Closes the active llvm-symbolizer process."""
+ if self._symbolizer is not None:
self._symbolizer.terminate()
self._symbolizer.wait()
+ self._symbolizer.stdin.close()
+ self._symbolizer.stdout.close()
+ self._symbolizer = None
@staticmethod
- def _is_json_compatibile() -> bool:
+ def _is_json_compatibile(symbolizer_binary: str) -> bool:
"""Checks llvm-symbolizer to ensure compatibility"""
result = subprocess.run(
- ('llvm-symbolizer', '--help'),
+ (symbolizer_binary, '--help'),
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
)
diff --git a/pw_sync/Android.bp b/pw_sync/Android.bp
index 55751b41a..8e2fd9dfb 100644
--- a/pw_sync/Android.bp
+++ b/pw_sync/Android.bp
@@ -23,3 +23,14 @@ cc_library_headers {
export_include_dirs: ["public"],
host_supported: true,
}
+
+cc_library_headers {
+ name: "pw_sync_binary_semaphore_thread_notification_backend_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ export_include_dirs: [
+ "public",
+ "public_overrides",
+ ],
+ host_supported: true,
+}
diff --git a/pw_sync/BUILD.bazel b/pw_sync/BUILD.bazel
index 5d48dac9c..76d93c4de 100644
--- a/pw_sync/BUILD.bazel
+++ b/pw_sync/BUILD.bazel
@@ -44,15 +44,19 @@ pw_cc_library(
srcs = [
"binary_semaphore.cc",
],
+ hdrs = [
+ "public/pw_sync/binary_semaphore.h",
+ ],
deps = [
- ":binary_semaphore_facade",
- "@pigweed_config//:pw_sync_binary_semaphore_backend",
+ "//pw_chrono:system_clock",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_sync_binary_semaphore_backend",
],
)
pw_cc_library(
name = "binary_semaphore_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:binary_semaphore"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:binary_semaphore"],
@@ -78,15 +82,19 @@ pw_cc_library(
srcs = [
"counting_semaphore.cc",
],
+ hdrs = [
+ "public/pw_sync/counting_semaphore.h",
+ ],
deps = [
- ":counting_semaphore_facade",
- "@pigweed_config//:pw_sync_counting_semaphore_backend",
+ "//pw_chrono:system_clock",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_sync_counting_semaphore_backend",
],
)
pw_cc_library(
name = "counting_semaphore_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:counting_semaphore"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:counting_semaphore"],
@@ -107,6 +115,14 @@ pw_cc_library(
)
pw_cc_library(
+ name = "lock_traits",
+ hdrs = [
+ "public/pw_sync/lock_traits.h",
+ ],
+ includes = ["public"],
+)
+
+pw_cc_library(
name = "borrow",
hdrs = [
"public/pw_sync/borrow.h",
@@ -114,6 +130,7 @@ pw_cc_library(
includes = ["public"],
deps = [
":lock_annotations",
+ ":lock_traits",
":virtual_basic_lockable",
"//pw_assert",
],
@@ -163,15 +180,20 @@ pw_cc_library(
srcs = [
"mutex.cc",
],
+ hdrs = [
+ "public/pw_sync/mutex.h",
+ ],
deps = [
- ":mutex_facade",
- "@pigweed_config//:pw_sync_mutex_backend",
+ ":lock_annotations",
+ ":virtual_basic_lockable",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_sync_mutex_backend",
],
)
pw_cc_library(
name = "mutex_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:mutex"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:mutex"],
@@ -200,17 +222,22 @@ pw_cc_library(
srcs = [
"timed_mutex.cc",
],
+ hdrs = [
+ "public/pw_sync/timed_mutex.h",
+ ],
deps = [
+ ":lock_annotations",
":mutex",
- ":timed_mutex_facade",
":virtual_basic_lockable",
- "@pigweed_config//:pw_sync_timed_mutex_backend",
+ "//pw_chrono:system_clock",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_sync_timed_mutex_backend",
],
)
pw_cc_library(
name = "timed_mutex_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:timed_mutex"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:timed_mutex"],
@@ -235,13 +262,13 @@ pw_cc_library(
visibility = ["//pw_sync_baremetal:__pkg__"],
deps = [
":recursive_mutex_facade",
- "@pigweed_config//:pw_sync_recursive_mutex_backend",
+ "@pigweed//targets:pw_sync_recursive_mutex_backend",
],
)
pw_cc_library(
name = "recursive_mutex_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"@platforms//os:none": ["//pw_sync_baremetal:recursive_mutex"],
"//conditions:default": ["//pw_sync_stl:recursive_mutex"],
@@ -266,15 +293,20 @@ pw_cc_library(
srcs = [
"interrupt_spin_lock.cc",
],
+ hdrs = [
+ "public/pw_sync/interrupt_spin_lock.h",
+ ],
deps = [
- ":interrupt_spin_lock_facade",
- "@pigweed_config//:pw_sync_interrupt_spin_lock_backend",
+ ":lock_annotations",
+ ":virtual_basic_lockable",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_sync_interrupt_spin_lock_backend",
],
)
pw_cc_library(
name = "interrupt_spin_lock_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:interrupt_spin_lock"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:interrupt_spin_lock"],
@@ -293,15 +325,17 @@ pw_cc_facade(
pw_cc_library(
name = "thread_notification",
+ hdrs = [
+ "public/pw_sync/thread_notification.h",
+ ],
deps = [
- ":thread_notification_facade",
- "@pigweed_config//:pw_sync_thread_notification_backend",
+ "@pigweed//targets:pw_sync_thread_notification_backend",
],
)
pw_cc_library(
name = "thread_notification_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:thread_notification"],
"//conditions:default": [":binary_semaphore_thread_notification_backend"],
@@ -322,16 +356,19 @@ pw_cc_facade(
pw_cc_library(
name = "timed_thread_notification",
+ hdrs = [
+ "public/pw_sync/timed_thread_notification.h",
+ ],
deps = [
":thread_notification",
- ":timed_thread_notification_facade",
- "@pigweed_config//:pw_sync_timed_thread_notification_backend",
+ "//pw_chrono:system_clock",
+ "@pigweed//targets:pw_sync_timed_thread_notification_backend",
],
)
pw_cc_library(
name = "timed_thread_notification_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:timed_thread_notification"],
"//conditions:default": ["//pw_sync:binary_semaphore_timed_thread_notification_backend"],
@@ -339,7 +376,7 @@ pw_cc_library(
)
pw_cc_library(
- name = "binary_semaphore_thread_notification_backend_headers",
+ name = "binary_semaphore_thread_notification_backend",
hdrs = [
"public/pw_sync/backends/binary_semaphore_thread_notification_inline.h",
"public/pw_sync/backends/binary_semaphore_thread_notification_native.h",
@@ -351,21 +388,14 @@ pw_cc_library(
"public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [":binary_semaphore"],
-)
-
-pw_cc_library(
- name = "binary_semaphore_thread_notification_backend",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":binary_semaphore_facade",
- ":binary_semaphore_thread_notification_backend_headers",
+ ":binary_semaphore",
":thread_notification_facade",
],
)
pw_cc_library(
- name = "binary_semaphore_timed_thread_notification_backend_headers",
+ name = "binary_semaphore_timed_thread_notification_backend",
hdrs = [
"public/pw_sync/backends/binary_semaphore_timed_thread_notification_inline.h",
"public_overrides/pw_sync_backend/timed_thread_notification_inline.h",
@@ -376,17 +406,8 @@ pw_cc_library(
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":binary_semaphore_thread_notification_backend_headers",
- "//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "binary_semaphore_timed_thread_notification_backend",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
":binary_semaphore_thread_notification_backend",
- ":binary_semaphore_timed_thread_notification_backend_headers",
+ "//pw_chrono:system_clock",
"//pw_sync:timed_thread_notification_facade",
],
)
@@ -411,7 +432,7 @@ pw_cc_facade(
],
)
-# TODO(b/228998350): This needs to be instantiated for each platform that
+# TODO: b/228998350 - This needs to be instantiated for each platform that
# provides an implementation of $dir_pw_thread:test_threads and
# $dir_pw_sync:condition_variable.
# pw_cc_library(
@@ -423,7 +444,7 @@ pw_cc_facade(
# "//pw_sync:mutex",
# "//pw_sync:timed_thread_notification",
# "//pw_thread:sleep",
-# "//pw_thread:test_threads_header",
+# "//pw_thread:non_portable_test_thread_options",
# "//pw_thread:thread",
# "//pw_unit_test",
# ],
@@ -435,15 +456,42 @@ filegroup(
srcs = ["condition_variable_test.cc"],
)
+pw_cc_library(
+ name = "lock_testing",
+ srcs = ["lock_testing.cc"],
+ hdrs = ["public/pw_sync/lock_testing.h"],
+ includes = ["public"],
+ deps = [
+ ":virtual_basic_lockable",
+ "//pw_assert",
+ ],
+)
+
+pw_cc_library(
+ name = "borrow_lockable_tests",
+ hdrs = ["pw_sync_private/borrow_lockable_tests.h"],
+ deps = [
+ ":borrow",
+ ":lock_traits",
+ ],
+)
+
pw_cc_test(
- name = "borrow_test",
- srcs = [
- "borrow_test.cc",
+ name = "lock_traits_test",
+ srcs = ["lock_traits_test.cc"],
+ deps = [
+ ":lock_testing",
+ ":lock_traits",
],
+)
+
+pw_cc_test(
+ name = "borrow_test",
+ srcs = ["borrow_test.cc"],
deps = [
":borrow",
- ":virtual_basic_lockable",
- "//pw_assert",
+ ":borrow_lockable_tests",
+ ":lock_testing",
"//pw_unit_test",
],
)
@@ -495,6 +543,7 @@ pw_cc_test(
"mutex_facade_test_c.c",
],
deps = [
+ ":borrow_lockable_tests",
":mutex",
"//pw_preprocessor",
"//pw_unit_test",
@@ -508,6 +557,7 @@ pw_cc_test(
"timed_mutex_facade_test_c.c",
],
deps = [
+ ":borrow_lockable_tests",
":timed_mutex",
"//pw_chrono:system_clock",
"//pw_preprocessor",
@@ -535,6 +585,7 @@ pw_cc_test(
"interrupt_spin_lock_facade_test_c.c",
],
deps = [
+ ":borrow_lockable_tests",
":interrupt_spin_lock",
"//pw_preprocessor",
"//pw_unit_test",
diff --git a/pw_sync/BUILD.gn b/pw_sync/BUILD.gn
index 2d7a86742..179004fbb 100644
--- a/pw_sync/BUILD.gn
+++ b/pw_sync/BUILD.gn
@@ -59,11 +59,17 @@ pw_source_set("lock_annotations") {
public_deps = [ "$dir_pw_preprocessor" ]
}
+pw_source_set("lock_traits") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_sync/lock_traits.h" ]
+}
+
pw_source_set("borrow") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_sync/borrow.h" ]
public_deps = [
":lock_annotations",
+ ":lock_traits",
":virtual_basic_lockable",
dir_pw_assert,
]
@@ -210,6 +216,7 @@ pw_facade("condition_variable") {
pw_test_group("tests") {
tests = [
+ ":lock_traits_test",
":borrow_test",
":binary_semaphore_facade_test",
":counting_semaphore_facade_test",
@@ -223,12 +230,36 @@ pw_test_group("tests") {
]
}
+pw_source_set("lock_testing") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_sync/lock_testing.h" ]
+ sources = [ "lock_testing.cc" ]
+ public_deps = [ ":virtual_basic_lockable" ]
+ deps = [ dir_pw_assert ]
+}
+
+pw_source_set("borrow_lockable_tests") {
+ public = [ "pw_sync_private/borrow_lockable_tests.h" ]
+ public_deps = [
+ ":borrow",
+ ":lock_traits",
+ ]
+}
+
+pw_test("lock_traits_test") {
+ sources = [ "lock_traits_test.cc" ]
+ deps = [
+ ":lock_testing",
+ ":lock_traits",
+ ]
+}
+
pw_test("borrow_test") {
sources = [ "borrow_test.cc" ]
deps = [
":borrow",
- ":virtual_basic_lockable",
- dir_pw_assert,
+ ":borrow_lockable_tests",
+ ":lock_testing",
]
}
@@ -275,6 +306,7 @@ pw_test("mutex_facade_test") {
"mutex_facade_test_c.c",
]
deps = [
+ ":borrow_lockable_tests",
":mutex",
"$dir_pw_preprocessor",
pw_sync_MUTEX_BACKEND,
@@ -288,6 +320,7 @@ pw_test("timed_mutex_facade_test") {
"timed_mutex_facade_test_c.c",
]
deps = [
+ ":borrow_lockable_tests",
":timed_mutex",
"$dir_pw_preprocessor",
pw_sync_TIMED_MUTEX_BACKEND,
@@ -314,6 +347,7 @@ pw_test("interrupt_spin_lock_facade_test") {
"interrupt_spin_lock_facade_test_c.c",
]
deps = [
+ ":borrow_lockable_tests",
":interrupt_spin_lock",
"$dir_pw_preprocessor",
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND,
@@ -342,14 +376,15 @@ pw_test("timed_thread_notification_facade_test") {
# an implementation of $dir_pw_thread:test_threads and
# $dir_pw_sync:condition_variable.
pw_source_set("condition_variable_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "condition_variable_test.cc" ]
deps = [
":condition_variable",
"$dir_pw_containers:vector",
"$dir_pw_sync:mutex",
"$dir_pw_sync:timed_thread_notification",
+ "$dir_pw_thread:non_portable_test_thread_options",
"$dir_pw_thread:sleep",
- "$dir_pw_thread:test_threads",
"$dir_pw_thread:thread",
"$dir_pw_unit_test",
]
diff --git a/pw_sync/CMakeLists.txt b/pw_sync/CMakeLists.txt
index 064adb338..86c86d8a6 100644
--- a/pw_sync/CMakeLists.txt
+++ b/pw_sync/CMakeLists.txt
@@ -52,6 +52,13 @@ pw_add_library(pw_sync.lock_annotations INTERFACE
pw_preprocessor
)
+pw_add_library(pw_sync.lock_traits INTERFACE
+ HEADERS
+ public/pw_sync/lock_traits.h
+ PUBLIC_INCLUDES
+ public
+)
+
pw_add_library(pw_sync.borrow INTERFACE
HEADERS
public/pw_sync/borrow.h
@@ -60,6 +67,7 @@ pw_add_library(pw_sync.borrow INTERFACE
PUBLIC_DEPS
pw_assert
pw_sync.lock_annotations
+ pw_sync.lock_traits
pw_sync.virtual_basic_lockable
)
@@ -214,13 +222,42 @@ pw_add_facade(pw_sync.condition_variable INTERFACE
pw_sync.mutex
)
+pw_add_library(pw_sync.lock_testing STATIC
+ HEADERS
+ public/pw_sync/lock_testing.h
+ SOURCES
+ lock_testing.cc
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_sync.virtual_basic_lockable
+ PRIVATE_DEPS
+ pw_assert
+)
+
+pw_add_library(pw_sync.borrow_lockable_tests INTERFACE
+ HEADERS
+ pw_sync_private/borrow_lockable_tests.h
+ PUBLIC_DEPS
+ pw_sync.borrow
+ pw_sync.lock_traits
+)
+
+pw_add_test(pw_sync.lock_traits_test
+ SOURCES
+ lock_traits_test.cc
+ PRIVATE_DEPS
+ pw_sync.lock_testing
+ pw_sync.lock_traits
+)
+
pw_add_test(pw_sync.borrow_test
SOURCES
borrow_test.cc
PRIVATE_DEPS
- pw_assert
pw_sync.borrow
- pw_sync.virtual_basic_lockable
+ pw_sync.borrow_lockable_tests
+ pw_sync.lock_testing
GROUPS
modules
pw_sync
@@ -275,6 +312,7 @@ if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
PRIVATE_DEPS
pw_preprocessor
pw_sync.mutex
+ pw_sync.borrow_lockable_tests
GROUPS
modules
pw_sync
@@ -289,6 +327,7 @@ if(NOT "${pw_sync.timed_mutex_BACKEND}" STREQUAL "")
PRIVATE_DEPS
pw_preprocessor
pw_sync.timed_mutex
+ pw_sync.borrow_lockable_tests
GROUPS
modules
pw_sync
@@ -303,6 +342,7 @@ if(NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL "")
PRIVATE_DEPS
pw_preprocessor
pw_sync.interrupt_spin_lock
+ pw_sync.borrow_lockable_tests
GROUPS
modules
pw_sync
@@ -340,7 +380,7 @@ pw_add_library(pw_sync.condition_variable_test STATIC
pw_sync.mutex
pw_sync.timed_thread_notification
pw_thread.sleep
- pw_thread.test_threads
+ pw_thread.non_portable_test_thread_options
pw_thread.thread
pw_unit_test
SOURCES
diff --git a/pw_sync/binary_semaphore_facade_test.cc b/pw_sync/binary_semaphore_facade_test.cc
index c52d792d9..e4181621e 100644
--- a/pw_sync/binary_semaphore_facade_test.cc
+++ b/pw_sync/binary_semaphore_facade_test.cc
@@ -53,7 +53,7 @@ TEST(BinarySemaphore, EmptyInitialState) {
EXPECT_FALSE(semaphore.try_acquire());
}
-// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
+// TODO: b/235284163 - Add real concurrency tests once we have pw::thread.
TEST(BinarySemaphore, Release) {
BinarySemaphore semaphore;
diff --git a/pw_sync/borrow_test.cc b/pw_sync/borrow_test.cc
index a3d6ba366..4ea0c4cd4 100644
--- a/pw_sync/borrow_test.cc
+++ b/pw_sync/borrow_test.cc
@@ -14,303 +14,16 @@
#include "pw_sync/borrow.h"
-#include <chrono>
-#include <ratio>
-
#include "gtest/gtest.h"
-#include "pw_assert/check.h"
-#include "pw_sync/virtual_basic_lockable.h"
+#include "pw_sync/lock_testing.h"
+#include "pw_sync_private/borrow_lockable_tests.h"
-namespace pw::sync {
+namespace pw::sync::test {
namespace {
-template <typename Lock>
-class BorrowableTest : public ::testing::Test {
- protected:
- static constexpr int kInitialValue = 42;
-
- BorrowableTest()
- : foo_{.value = kInitialValue}, borrowable_foo_(foo_, lock_) {}
-
- void SetUp() override {
- EXPECT_FALSE(lock_.locked()); // Ensure it's not locked on construction.
- }
-
- struct Foo {
- int value;
- };
- Lock lock_;
- Foo foo_;
- Borrowable<Foo, Lock> borrowable_foo_;
-};
-
-class BasicLockable : public VirtualBasicLockable {
- public:
- virtual ~BasicLockable() = default;
-
- bool locked() const { return locked_; }
-
- protected:
- bool locked_ = false;
-
- private:
- void DoLockOperation(Operation operation) override {
- switch (operation) {
- case Operation::kLock:
- PW_CHECK(!locked_, "Recursive lock detected");
- locked_ = true;
- return;
-
- case Operation::kUnlock:
- default:
- PW_CHECK(locked_, "Unlock while unlocked detected");
- locked_ = false;
- return;
- }
- }
-};
-
-using BorrowableBasicLockableTest = BorrowableTest<BasicLockable>;
-
-TEST_F(BorrowableBasicLockableTest, Acquire) {
- {
- BorrowedPointer<Foo, BasicLockable> borrowed_foo =
- borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
- EXPECT_EQ(foo_.value, 13);
-}
-
-TEST_F(BorrowableBasicLockableTest, RepeatedAcquire) {
- {
- BorrowedPointer<Foo, BasicLockable> borrowed_foo =
- borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
- {
- BorrowedPointer<Foo, BasicLockable> borrowed_foo =
- borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, 13);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableBasicLockableTest, Moveable) {
- Borrowable<Foo, BasicLockable> borrowable_foo = std::move(borrowable_foo_);
- {
- BorrowedPointer<Foo, BasicLockable> borrowed_foo = borrowable_foo.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableBasicLockableTest, Copyable) {
- const Borrowable<Foo, BasicLockable>& other = borrowable_foo_;
- Borrowable<Foo, BasicLockable> borrowable_foo(other);
- {
- BorrowedPointer<Foo, BasicLockable> borrowed_foo = borrowable_foo.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-class Lockable : public BasicLockable {
- public:
- bool try_lock() {
- if (locked()) {
- return false;
- }
- locked_ = true;
- return true;
- }
-};
-
-using BorrowableLockableTest = BorrowableTest<Lockable>;
-
-TEST_F(BorrowableLockableTest, Acquire) {
- {
- BorrowedPointer<Foo, Lockable> borrowed_foo = borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
- EXPECT_EQ(foo_.value, 13);
-}
-
-TEST_F(BorrowableLockableTest, RepeatedAcquire) {
- {
- BorrowedPointer<Foo, Lockable> borrowed_foo = borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
- {
- BorrowedPointer<Foo, Lockable> borrowed_foo = borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, 13);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableLockableTest, TryAcquireSuccess) {
- {
- std::optional<BorrowedPointer<Foo, Lockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire();
- ASSERT_TRUE(maybe_borrowed_foo.has_value());
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableLockableTest, TryAcquireFailure) {
- lock_.lock();
- EXPECT_TRUE(lock_.locked());
- {
- std::optional<BorrowedPointer<Foo, Lockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire();
- EXPECT_FALSE(maybe_borrowed_foo.has_value());
- }
- EXPECT_TRUE(lock_.locked());
- lock_.unlock();
-}
-
-struct Clock {
- using rep = int64_t;
- using period = std::micro;
- using duration = std::chrono::duration<rep, period>;
- using time_point = std::chrono::time_point<Clock>;
-};
-
-class TimedLockable : public Lockable {
- public:
- bool try_lock() {
- if (locked()) {
- return false;
- }
- locked_ = true;
- return true;
- }
-
- bool try_lock_for(const Clock::duration&) { return try_lock(); }
- bool try_lock_until(const Clock::time_point&) { return try_lock(); }
-};
-
-using BorrowableTimedLockableTest = BorrowableTest<TimedLockable>;
-
-TEST_F(BorrowableTimedLockableTest, Acquire) {
- {
- BorrowedPointer<Foo, TimedLockable> borrowed_foo =
- borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
- EXPECT_EQ(foo_.value, 13);
-}
-
-TEST_F(BorrowableTimedLockableTest, RepeatedAcquire) {
- {
- BorrowedPointer<Foo, TimedLockable> borrowed_foo =
- borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, kInitialValue);
- borrowed_foo->value = 13;
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
- {
- BorrowedPointer<Foo, TimedLockable> borrowed_foo =
- borrowable_foo_.acquire();
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(borrowed_foo->value, 13);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableTimedLockableTest, TryAcquireSuccess) {
- {
- std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire();
- ASSERT_TRUE(maybe_borrowed_foo.has_value());
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableTimedLockableTest, TryAcquireFailure) {
- lock_.lock();
- EXPECT_TRUE(lock_.locked());
- {
- std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire();
- EXPECT_FALSE(maybe_borrowed_foo.has_value());
- }
- EXPECT_TRUE(lock_.locked());
- lock_.unlock();
-}
-
-TEST_F(BorrowableTimedLockableTest, TryAcquireForSuccess) {
- {
- std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire_for(std::chrono::seconds(0));
- ASSERT_TRUE(maybe_borrowed_foo.has_value());
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableTimedLockableTest, TryAcquireForFailure) {
- lock_.lock();
- EXPECT_TRUE(lock_.locked());
- {
- std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire_for(std::chrono::seconds(0));
- EXPECT_FALSE(maybe_borrowed_foo.has_value());
- }
- EXPECT_TRUE(lock_.locked());
- lock_.unlock();
-}
-
-TEST_F(BorrowableTimedLockableTest, TryAcquireUntilSuccess) {
- {
- std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire_until(
- Clock::time_point(std::chrono::seconds(0)));
- ASSERT_TRUE(maybe_borrowed_foo.has_value());
- EXPECT_TRUE(lock_.locked()); // Ensure the lock is held.
- EXPECT_EQ(maybe_borrowed_foo.value()->value, kInitialValue);
- }
- EXPECT_FALSE(lock_.locked()); // Ensure the lock is released.
-}
-
-TEST_F(BorrowableTimedLockableTest, TryAcquireUntilFailure) {
- lock_.lock();
- EXPECT_TRUE(lock_.locked());
- {
- std::optional<BorrowedPointer<Foo, TimedLockable>> maybe_borrowed_foo =
- borrowable_foo_.try_acquire_until(
- Clock::time_point(std::chrono::seconds(0)));
- EXPECT_FALSE(maybe_borrowed_foo.has_value());
- }
- EXPECT_TRUE(lock_.locked());
- lock_.unlock();
-}
+PW_SYNC_ADD_BORROWABLE_LOCK_TESTS(FakeBasicLockable);
+PW_SYNC_ADD_BORROWABLE_LOCK_TESTS(FakeLockable);
+PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_TESTS(FakeTimedLockable, FakeClock);
} // namespace
-} // namespace pw::sync
+} // namespace pw::sync::test
diff --git a/pw_sync/condition_variable_test.cc b/pw_sync/condition_variable_test.cc
index c0ddc4df0..13845bf78 100644
--- a/pw_sync/condition_variable_test.cc
+++ b/pw_sync/condition_variable_test.cc
@@ -21,8 +21,8 @@
#include "pw_containers/vector.h"
#include "pw_sync/mutex.h"
#include "pw_sync/timed_thread_notification.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread/thread.h"
namespace pw::sync {
@@ -92,6 +92,7 @@ class LambdaThread {
// Starts a new thread which runs `work`, joining the thread on destruction.
explicit LambdaThread(
std::function<void()> work,
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
pw::thread::Options options = pw::thread::test::TestOptionsThread0())
: thread_core_(std::move(work)), thread_(options, thread_core_) {}
~LambdaThread() { thread_.join(); }
diff --git a/pw_sync/counting_semaphore_facade_test.cc b/pw_sync/counting_semaphore_facade_test.cc
index c7224b292..8b1cee2b3 100644
--- a/pw_sync/counting_semaphore_facade_test.cc
+++ b/pw_sync/counting_semaphore_facade_test.cc
@@ -59,7 +59,7 @@ TEST(CountingSemaphore, EmptyInitialState) {
EXPECT_FALSE(semaphore.try_acquire());
}
-// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
+// TODO: b/235284163 - Add real concurrency tests once we have pw::thread.
TEST(CountingSemaphore, SingleRelease) {
CountingSemaphore semaphore;
diff --git a/pw_sync/docs.rst b/pw_sync/docs.rst
index e30c01f51..aba3ea4c7 100644
--- a/pw_sync/docs.rst
+++ b/pw_sync/docs.rst
@@ -731,9 +731,9 @@ internal mutex, however at crash time we may want to switch to a no-op lock. A
virtual lock interface could be used here to minimize the code-size cost that
would occur otherwise if the flash driver were templated.
-VirtualBasicLock
-----------------
-The ``VirtualBasicLock`` interface meets the
+VirtualBasicLockable
+--------------------
+The ``VirtualBasicLockable`` interface meets the
`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_ C++
named requirement. Our critical section lock primitives offer optional virtual
versions, including:
@@ -742,9 +742,24 @@ versions, including:
* :cpp:func:`pw::sync::VirtualTimedMutex`
* :cpp:func:`pw::sync::VirtualInterruptSpinLock`
+.. _module-pw_sync-genericbasiclockable:
+
+GenericBasicLockable
+--------------------
+``GenericBasicLockable`` is a helper construct that can be used to declare
+virtual versions of a critical section lock primitive that meets the
+`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_
+C++ named requirement. For example, given a ``Mutex`` type with ``lock()`` and
+``unlock()`` methods, a ``VirtualMutex`` type that derives from
+``VirtualBasicLockable`` can be declared as follows:
+
+.. code-block:: cpp
+
+ class VirtualMutex : public GenericBasicLockable<Mutex> {};
+
Borrowable
==========
-The Borrowable is a helper construct that enables callers to borrow an object
+``Borrowable`` is a helper construct that enables callers to borrow an object
which is guarded by a lock, enabling a containerized style of external locking.
Users who need access to the guarded object can ask to acquire a
@@ -871,6 +886,12 @@ into the ACK timeout you'd like to use for the transaction. Borrowable can help
you do exactly this if you provide access to the I2c bus through a
``Borrowable``.
+.. note::
+
+ ``Borrowable`` has semantics similar to a pointer and should be passed by
+ value. Furthermore, a ``Borrowable<U>`` can be assigned to a
+ ``Borrowable<T>`` if ``U`` is a subclass of ``T``.
+
C++
---
.. doxygenclass:: pw::sync::BorrowedPointer
@@ -1008,7 +1029,7 @@ Example in C++
pw::sync::InlineBorrowable<ExampleI2c> i2c(std::in_place, kBusId, opts);
pw::Result<ConstByteSpan> ReadI2cData(
- pw::sync::Borrowable<pw::i2c::Initiator>& initiator,
+ pw::sync::Borrowable<pw::i2c::Initiator> initiator,
ByteSpan buffer);
pw::Result<ConstByteSpan> ReadData(ByteSpan buffer) {
diff --git a/pw_sync/interrupt_spin_lock_facade_test.cc b/pw_sync/interrupt_spin_lock_facade_test.cc
index 86a751c03..669551a72 100644
--- a/pw_sync/interrupt_spin_lock_facade_test.cc
+++ b/pw_sync/interrupt_spin_lock_facade_test.cc
@@ -14,6 +14,7 @@
#include "gtest/gtest.h"
#include "pw_sync/interrupt_spin_lock.h"
+#include "pw_sync_private/borrow_lockable_tests.h"
namespace pw::sync {
namespace {
@@ -37,13 +38,13 @@ TEST(InterruptSpinLock, LockUnlock) {
interrupt_spin_lock.unlock();
}
-// TODO(b/235284163): Add real concurrency tests once we have pw::thread on SMP
+// TODO: b/235284163 - Add real concurrency tests once we have pw::thread on SMP
// systems given that uniprocessor systems cannot fail to acquire an ISL.
InterruptSpinLock static_interrupt_spin_lock;
TEST(InterruptSpinLock, LockUnlockStatic) {
static_interrupt_spin_lock.lock();
- // TODO(b/235284163): Ensure other cores fail to lock when its locked.
+ // TODO: b/235284163 - Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(static_interrupt_spin_lock.try_lock());
static_interrupt_spin_lock.unlock();
}
@@ -53,16 +54,19 @@ TEST(InterruptSpinLock, TryLockUnlock) {
const bool locked = interrupt_spin_lock.try_lock();
EXPECT_TRUE(locked);
if (locked) {
- // TODO(b/235284163): Ensure other cores fail to lock when its locked.
+ // TODO: b/235284163 - Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(interrupt_spin_lock.try_lock());
interrupt_spin_lock.unlock();
}
}
+PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(BorrowableInterruptSpinLock,
+ InterruptSpinLock);
+
TEST(VirtualInterruptSpinLock, LockUnlock) {
pw::sync::VirtualInterruptSpinLock interrupt_spin_lock;
interrupt_spin_lock.lock();
- // TODO(b/235284163): Ensure other cores fail to lock when its locked.
+ // TODO: b/235284163 - Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(interrupt_spin_lock.try_lock());
interrupt_spin_lock.unlock();
}
@@ -70,11 +74,14 @@ TEST(VirtualInterruptSpinLock, LockUnlock) {
VirtualInterruptSpinLock static_virtual_interrupt_spin_lock;
TEST(VirtualInterruptSpinLock, LockUnlockStatic) {
static_virtual_interrupt_spin_lock.lock();
- // TODO(b/235284163): Ensure other cores fail to lock when its locked.
+ // TODO: b/235284163 - Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(static_virtual_interrupt_spin_lock.try_lock());
static_virtual_interrupt_spin_lock.unlock();
}
+PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(BorrowableVirtualInterruptSpinLock,
+ VirtualInterruptSpinLock);
+
TEST(InterruptSpinLock, LockUnlockInC) {
pw::sync::InterruptSpinLock interrupt_spin_lock;
pw_sync_InterruptSpinLock_CallLock(&interrupt_spin_lock);
@@ -84,7 +91,7 @@ TEST(InterruptSpinLock, LockUnlockInC) {
TEST(InterruptSpinLock, TryLockUnlockInC) {
pw::sync::InterruptSpinLock interrupt_spin_lock;
ASSERT_TRUE(pw_sync_InterruptSpinLock_CallTryLock(&interrupt_spin_lock));
- // TODO(b/235284163): Ensure other cores fail to lock when its locked.
+ // TODO: b/235284163 - Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(pw_sync_InterruptSpinLock_CallTryLock(&interrupt_spin_lock));
pw_sync_InterruptSpinLock_CallUnlock(&interrupt_spin_lock);
}
diff --git a/pw_sync/lock_testing.cc b/pw_sync/lock_testing.cc
new file mode 100644
index 000000000..e2b75954b
--- /dev/null
+++ b/pw_sync/lock_testing.cc
@@ -0,0 +1,44 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_sync/lock_testing.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::sync::test {
+
+void FakeBasicLockable::DoLockOperation(Operation operation) {
+ switch (operation) {
+ case Operation::kLock:
+ PW_CHECK(!locked_, "Recursive lock detected");
+ locked_ = true;
+ return;
+
+ case Operation::kUnlock:
+ default:
+ PW_CHECK(locked_, "Unlock while unlocked detected");
+ locked_ = false;
+ return;
+ }
+}
+
+bool FakeLockable::try_lock() {
+ if (locked()) {
+ return false;
+ }
+ locked_ = true;
+ return true;
+}
+
+} // namespace pw::sync::test
diff --git a/pw_sync/lock_traits_test.cc b/pw_sync/lock_traits_test.cc
new file mode 100644
index 000000000..65e34a059
--- /dev/null
+++ b/pw_sync/lock_traits_test.cc
@@ -0,0 +1,62 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_sync/lock_traits.h"
+
+#include "gtest/gtest.h"
+#include "pw_sync/lock_testing.h"
+
+namespace pw::sync::test {
+
+struct NotALock {};
+
+TEST(LockTraitsTest, IsBasicLockable) {
+ EXPECT_FALSE(is_basic_lockable_v<NotALock>);
+ EXPECT_TRUE(is_basic_lockable_v<FakeBasicLockable>);
+ EXPECT_TRUE(is_basic_lockable_v<FakeLockable>);
+ EXPECT_TRUE(is_basic_lockable_v<FakeTimedLockable>);
+}
+
+TEST(LockTraitsTest, IsLockable) {
+ EXPECT_FALSE(is_lockable_v<NotALock>);
+ EXPECT_FALSE(is_lockable_v<FakeBasicLockable>);
+ EXPECT_TRUE(is_lockable_v<FakeLockable>);
+ EXPECT_TRUE(is_lockable_v<FakeTimedLockable>);
+}
+
+TEST(LockTraitsTest, IsLockableFor) {
+ EXPECT_FALSE((is_lockable_for_v<NotALock, FakeClock::duration>));
+ EXPECT_FALSE((is_lockable_for_v<FakeBasicLockable, FakeClock::duration>));
+ EXPECT_FALSE((is_lockable_for_v<FakeLockable, FakeClock::duration>));
+ EXPECT_FALSE((is_lockable_for_v<FakeTimedLockable, NotAClock::duration>));
+ EXPECT_TRUE((is_lockable_for_v<FakeTimedLockable, FakeClock::duration>));
+}
+
+TEST(LockTraitsTest, IsLockableUntil) {
+ EXPECT_FALSE((is_lockable_until_v<NotALock, FakeClock::time_point>));
+ EXPECT_FALSE((is_lockable_until_v<FakeBasicLockable, FakeClock::time_point>));
+ EXPECT_FALSE((is_lockable_until_v<FakeLockable, FakeClock::time_point>));
+ EXPECT_FALSE((is_lockable_until_v<FakeTimedLockable, NotAClock::time_point>));
+ EXPECT_TRUE((is_lockable_until_v<FakeTimedLockable, FakeClock::time_point>));
+}
+
+TEST(LockTraitsTest, IsTimedLockable) {
+ EXPECT_FALSE((is_timed_lockable_v<NotALock, FakeClock>));
+ EXPECT_FALSE((is_timed_lockable_v<FakeBasicLockable, FakeClock>));
+ EXPECT_FALSE((is_timed_lockable_v<FakeLockable, FakeClock>));
+ EXPECT_FALSE((is_timed_lockable_v<FakeTimedLockable, NotAClock>));
+ EXPECT_TRUE((is_timed_lockable_v<FakeTimedLockable, FakeClock>));
+}
+
+} // namespace pw::sync::test
diff --git a/pw_sync/mutex_facade_test.cc b/pw_sync/mutex_facade_test.cc
index 4c7cdd03a..71ca94649 100644
--- a/pw_sync/mutex_facade_test.cc
+++ b/pw_sync/mutex_facade_test.cc
@@ -16,6 +16,7 @@
#include "gtest/gtest.h"
#include "pw_sync/mutex.h"
+#include "pw_sync_private/borrow_lockable_tests.h"
namespace pw::sync {
namespace {
@@ -29,12 +30,12 @@ void pw_sync_Mutex_CallUnlock(pw_sync_Mutex* mutex);
} // extern "C"
-// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
+// TODO: b/235284163 - Add real concurrency tests once we have pw::thread.
TEST(Mutex, LockUnlock) {
- pw::sync::Mutex mutex;
+ Mutex mutex;
mutex.lock();
- // TODO(b/235284163): Ensure it fails to lock when already held.
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
@@ -42,26 +43,28 @@ TEST(Mutex, LockUnlock) {
Mutex static_mutex;
TEST(Mutex, LockUnlockStatic) {
static_mutex.lock();
- // TODO(b/235284163): Ensure it fails to lock when already held.
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
// EXPECT_FALSE(static_mutex.try_lock());
static_mutex.unlock();
}
TEST(Mutex, TryLockUnlock) {
- pw::sync::Mutex mutex;
+ Mutex mutex;
const bool locked = mutex.try_lock();
EXPECT_TRUE(locked);
if (locked) {
- // TODO(b/235284163): Ensure it fails to lock when already held.
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
}
+PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(BorrowableMutex, Mutex);
+
TEST(VirtualMutex, LockUnlock) {
- pw::sync::VirtualMutex mutex;
+ VirtualMutex mutex;
mutex.lock();
- // TODO(b/235284163): Ensure it fails to lock when already held.
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
@@ -69,21 +72,32 @@ TEST(VirtualMutex, LockUnlock) {
VirtualMutex static_virtual_mutex;
TEST(VirtualMutex, LockUnlockStatic) {
static_virtual_mutex.lock();
- // TODO(b/235284163): Ensure it fails to lock when already held.
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
// EXPECT_FALSE(static_virtual_mutex.try_lock());
static_virtual_mutex.unlock();
}
+TEST(VirtualMutex, LockUnlockExternal) {
+ VirtualMutex virtual_mutex;
+ auto& mutex = virtual_mutex.mutex();
+ mutex.lock();
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
+ // EXPECT_FALSE(mutex.try_lock());
+ mutex.unlock();
+}
+
+PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(BorrowableVirtualMutex, VirtualMutex);
+
TEST(Mutex, LockUnlockInC) {
- pw::sync::Mutex mutex;
+ Mutex mutex;
pw_sync_Mutex_CallLock(&mutex);
pw_sync_Mutex_CallUnlock(&mutex);
}
TEST(Mutex, TryLockUnlockInC) {
- pw::sync::Mutex mutex;
+ Mutex mutex;
ASSERT_TRUE(pw_sync_Mutex_CallTryLock(&mutex));
- // TODO(b/235284163): Ensure it fails to lock when already held.
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
// EXPECT_FALSE(pw_sync_Mutex_CallTryLock(&mutex));
pw_sync_Mutex_CallUnlock(&mutex);
}
diff --git a/pw_sync/public/pw_sync/borrow.h b/pw_sync/public/pw_sync/borrow.h
index 204b888a9..daa07f9a5 100644
--- a/pw_sync/public/pw_sync/borrow.h
+++ b/pw_sync/public/pw_sync/borrow.h
@@ -19,6 +19,7 @@
#include "pw_assert/assert.h"
#include "pw_sync/lock_annotations.h"
+#include "pw_sync/lock_traits.h"
#include "pw_sync/virtual_basic_lockable.h"
namespace pw::sync {
@@ -107,28 +108,49 @@ class BorrowedPointer {
/// Users who need access to the guarded object can ask to acquire a
/// `BorrowedPointer` which permits access while the lock is held.
///
+/// Thread-safety analysis is not supported for this class, as the
+/// `BorrowedPointer`s it creates conditionally releases the lock. See also
+/// https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-conditionally-held-locks
+///
/// This class is compatible with locks which comply with `BasicLockable`,
/// `Lockable`, and `TimedLockable` C++ named requirements.
+///
+/// `Borrowable<T>` is covariant with respect to `T`, so that `Borrowable<U>`
+/// can be converted to `Borrowable<T>`, if `U` is a subclass of `T`.
+///
+/// `Borrowable` has pointer-like semantics and should be passed by value.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class Borrowable {
public:
+ static_assert(is_basic_lockable_v<Lock>,
+ "lock type must satisfy BasicLockable");
+
constexpr Borrowable(GuardedType& object, Lock& lock) noexcept
: lock_(&lock), object_(&object) {}
+ template <typename U>
+ constexpr Borrowable(const Borrowable<U, Lock>& other)
+ : lock_(other.lock_), object_(other.object_) {}
+
Borrowable(const Borrowable&) = default;
Borrowable& operator=(const Borrowable&) = default;
Borrowable(Borrowable&& other) = default;
Borrowable& operator=(Borrowable&& other) = default;
/// Blocks indefinitely until the object can be borrowed. Failures are fatal.
- BorrowedPointer<GuardedType, Lock> acquire() PW_NO_LOCK_SAFETY_ANALYSIS {
+ BorrowedPointer<GuardedType, Lock> acquire() const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
lock_->lock();
return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
}
/// Tries to borrow the object in a non-blocking manner. Returns a
/// BorrowedPointer on success, otherwise `std::nullopt` (nothing).
- std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire() {
+ template <int&... ExplicitArgumentBarrier,
+ typename T = Lock,
+ typename = std::enable_if_t<is_lockable_v<T>>>
+ std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire() const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
if (!lock_->try_lock()) {
return std::nullopt;
}
@@ -138,9 +160,15 @@ class Borrowable {
/// Tries to borrow the object. Blocks until the specified timeout has elapsed
/// or the object has been borrowed, whichever comes first. Returns a
/// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
- template <class Rep, class Period>
+ template <class Rep,
+ class Period,
+ int&... ExplicitArgumentBarrier,
+ typename T = Lock,
+ typename = std::enable_if_t<
+ is_lockable_for_v<T, std::chrono::duration<Rep, Period>>>>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(
- std::chrono::duration<Rep, Period> timeout) {
+ std::chrono::duration<Rep, Period> timeout) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
if (!lock_->try_lock_for(timeout)) {
return std::nullopt;
}
@@ -150,9 +178,16 @@ class Borrowable {
/// Tries to borrow the object. Blocks until the specified deadline has passed
/// or the object has been borrowed, whichever comes first. Returns a
/// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
- template <class Clock, class Duration>
+ template <
+ class Clock,
+ class Duration,
+ int&... ExplicitArgumentBarrier,
+ typename T = Lock,
+ typename = std::enable_if_t<
+ is_lockable_until_v<T, std::chrono::time_point<Clock, Duration>>>>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(
- std::chrono::time_point<Clock, Duration> deadline) {
+ std::chrono::time_point<Clock, Duration> deadline) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
if (!lock_->try_lock_until(deadline)) {
return std::nullopt;
}
@@ -162,6 +197,10 @@ class Borrowable {
private:
Lock* lock_;
GuardedType* object_;
+
+ // Befriend all template instantiations of this class.
+ template <typename, typename>
+ friend class Borrowable;
};
} // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/interrupt_spin_lock.h b/pw_sync/public/pw_sync/interrupt_spin_lock.h
index 7726c92a8..996ad6de1 100644
--- a/pw_sync/public/pw_sync/interrupt_spin_lock.h
+++ b/pw_sync/public/pw_sync/interrupt_spin_lock.h
@@ -81,32 +81,8 @@ class PW_LOCKABLE("pw::sync::InterruptSpinLock") InterruptSpinLock {
};
class PW_LOCKABLE("pw::sync::VirtualInterruptSpinLock")
- VirtualInterruptSpinLock final : public VirtualBasicLockable {
- public:
- VirtualInterruptSpinLock() = default;
-
- VirtualInterruptSpinLock(const VirtualInterruptSpinLock&) = delete;
- VirtualInterruptSpinLock(VirtualInterruptSpinLock&&) = delete;
- VirtualInterruptSpinLock& operator=(const VirtualInterruptSpinLock&) = delete;
- VirtualInterruptSpinLock& operator=(VirtualInterruptSpinLock&&) = delete;
-
- InterruptSpinLock& interrupt_spin_lock() { return interrupt_spin_lock_; }
-
- private:
- void DoLockOperation(Operation operation) override
- PW_NO_LOCK_SAFETY_ANALYSIS {
- switch (operation) {
- case Operation::kLock:
- return interrupt_spin_lock_.lock();
-
- case Operation::kUnlock:
- default:
- return interrupt_spin_lock_.unlock();
- }
- }
-
- InterruptSpinLock interrupt_spin_lock_;
-};
+ VirtualInterruptSpinLock final
+ : public GenericBasicLockable<InterruptSpinLock> {};
} // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/lock_testing.h b/pw_sync/public/pw_sync/lock_testing.h
new file mode 100644
index 000000000..5a9d0bec1
--- /dev/null
+++ b/pw_sync/public/pw_sync/lock_testing.h
@@ -0,0 +1,72 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+/// This file provides types that meet C++'s lock-related named requirements,
+/// but do no actual locking. They are only intended for use in tests.
+
+#include <chrono>
+#include <ratio>
+
+#include "pw_sync/virtual_basic_lockable.h"
+
+namespace pw::sync::test {
+
+/// Fake lock that meet's C++'s \em BasicLockable named requirement.
+class FakeBasicLockable : public VirtualBasicLockable {
+ public:
+ virtual ~FakeBasicLockable() = default;
+
+ bool locked() const { return locked_; }
+
+ protected:
+ bool locked_ = false;
+
+ private:
+ void DoLockOperation(Operation operation) override;
+};
+
+/// Fake lock that meet's C++'s \em Lockable named requirement.
+class FakeLockable : public FakeBasicLockable {
+ public:
+ bool try_lock();
+};
+
+/// Fake clock that merely provides the expected dependent types.
+struct FakeClock {
+ using rep = int64_t;
+ using period = std::micro;
+ using duration = std::chrono::duration<rep, period>;
+ using time_point = std::chrono::time_point<FakeClock>;
+};
+
+/// Fake clock that provides invalid dependent types.
+///
+/// This clock is guaranteed to fail `is_lockable_until<Lock, NoClock>` for any
+/// `Lock`.
+struct NotAClock {
+ using rep = void;
+ using period = void;
+ using duration = void;
+ using time_point = void;
+};
+
+/// Fake lock that meet's C++'s \em TimedLockable named requirement.
+class FakeTimedLockable : public FakeLockable {
+ public:
+ bool try_lock_for(const FakeClock::duration&) { return try_lock(); }
+ bool try_lock_until(const FakeClock::time_point&) { return try_lock(); }
+};
+
+} // namespace pw::sync::test
diff --git a/pw_sync/public/pw_sync/lock_traits.h b/pw_sync/public/pw_sync/lock_traits.h
new file mode 100644
index 000000000..9dce74a49
--- /dev/null
+++ b/pw_sync/public/pw_sync/lock_traits.h
@@ -0,0 +1,123 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <type_traits>
+#include <utility>
+
+/// This file provide trait types that can be used to check C++ lock-related
+/// named requirements: BasicLockable, Lockable, and TimedLockable.
+
+namespace pw::sync {
+
+/// Checks if a type is a basic lock.
+///
+/// If `Lock` satisfies \em BasicLockable, provides the member constant value
+/// equal to true. Otherwise value is false.
+///
+/// @{
+template <typename Lock, typename = void>
+struct is_basic_lockable : std::false_type {};
+
+template <typename Lock>
+struct is_basic_lockable<Lock,
+ std::void_t<decltype(std::declval<Lock>().lock()),
+ decltype(std::declval<Lock>().unlock())>>
+ : std::true_type {};
+/// @}
+
+/// Helper variable template for `is_basic_lockable<Lock>::value`.
+template <typename Lock>
+inline constexpr bool is_basic_lockable_v = is_basic_lockable<Lock>::value;
+
+/// Checks if a type is a lock.
+///
+/// If `Lock` satisfies C++'s \em Lockable named requirement, provides the
+/// member constant value equal to true. Otherwise value is false.
+///
+/// @{
+template <typename Lock, typename = void>
+struct is_lockable : std::false_type {};
+
+template <typename Lock>
+struct is_lockable<Lock, std::void_t<decltype(std::declval<Lock>().try_lock())>>
+ : is_basic_lockable<Lock> {};
+/// @}
+
+/// Helper variable template for `is_lockable<Lock>::value`.
+template <typename Lock>
+inline constexpr bool is_lockable_v = is_lockable<Lock>::value;
+
+/// Checks if a type is can be locked within a set time.
+///
+/// If `Lock` has a valid `try_lock_for` method, as described by C++'s
+/// \em TimedLockable named requirement, provides the member constant value
+/// equal to true. Otherwise value is false.
+///
+/// @{
+template <typename Lock, typename Duration, typename = void>
+struct is_lockable_for : std::false_type {};
+
+template <typename Lock, typename Duration>
+struct is_lockable_for<Lock,
+ Duration,
+ std::void_t<decltype(std::declval<Lock>().try_lock_for(
+ std::declval<Duration>()))>> : is_lockable<Lock> {};
+/// @}
+
+/// Helper variable template for `is_lockable_for<Lock, Duration>::value`.
+template <typename Lock, typename Duration>
+inline constexpr bool is_lockable_for_v =
+ is_lockable_for<Lock, Duration>::value;
+
+/// Checks if a type is can be locked by a set time.
+///
+/// If `Lock` has a valid `try_lock_until` method, as described by C++'s
+/// \em TimedLockable named requirement, provides the member constant value
+/// equal to true. Otherwise value is false.
+///
+/// @{
+template <typename Lock, typename TimePoint, typename = void>
+struct is_lockable_until : std::false_type {};
+
+template <typename Lock, typename TimePoint>
+struct is_lockable_until<
+ Lock,
+ TimePoint,
+ std::void_t<decltype(std::declval<Lock>().try_lock_until(
+ std::declval<TimePoint>()))>> : is_lockable<Lock> {};
+/// @}
+
+/// Helper variable template for `is_lockable_until<Lock, TimePoint>::value`.
+template <typename Lock, typename TimePoint>
+inline constexpr bool is_lockable_until_v =
+ is_lockable_until<Lock, TimePoint>::value;
+
+/// Checks if a lock type is timed-lockable.
+///
+/// If `Lock` satisfies C++'s \em TimedLockable named requirement, provides the
+/// member constant value equal to true. Otherwise value is false.
+template <typename Lock, typename Clock>
+struct is_timed_lockable
+ : std::integral_constant<
+ bool,
+ is_lockable_for_v<Lock, typename Clock::duration> &&
+ is_lockable_until_v<Lock, typename Clock::time_point>> {};
+
+/// Helper variable template for `is_timed_lockable<Lock, Clock>::value`.
+template <typename Lock, typename Clock>
+inline constexpr bool is_timed_lockable_v =
+ is_timed_lockable<Lock, Clock>::value;
+
+} // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/mutex.h b/pw_sync/public/pw_sync/mutex.h
index 5b4246812..18729f547 100644
--- a/pw_sync/public/pw_sync/mutex.h
+++ b/pw_sync/public/pw_sync/mutex.h
@@ -86,31 +86,9 @@ class PW_LOCKABLE("pw::sync::Mutex") Mutex {
};
class PW_LOCKABLE("pw::sync::VirtualMutex") VirtualMutex final
- : public VirtualBasicLockable {
+ : public GenericBasicLockable<Mutex> {
public:
- VirtualMutex() = default;
-
- VirtualMutex(const VirtualMutex&) = delete;
- VirtualMutex(VirtualMutex&&) = delete;
- VirtualMutex& operator=(const VirtualMutex&) = delete;
- VirtualMutex& operator=(VirtualMutex&&) = delete;
-
- Mutex& mutex() { return mutex_; }
-
- private:
- void DoLockOperation(Operation operation) override
- PW_NO_LOCK_SAFETY_ANALYSIS {
- switch (operation) {
- case Operation::kLock:
- return mutex_.lock();
-
- case Operation::kUnlock:
- default:
- return mutex_.unlock();
- }
- }
-
- Mutex mutex_;
+ Mutex& mutex() { return impl(); }
};
} // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/timed_mutex.h b/pw_sync/public/pw_sync/timed_mutex.h
index abb48c1ee..396f9b68d 100644
--- a/pw_sync/public/pw_sync/timed_mutex.h
+++ b/pw_sync/public/pw_sync/timed_mutex.h
@@ -71,31 +71,9 @@ class TimedMutex : public Mutex {
};
class PW_LOCKABLE("pw::sync::VirtualTimedMutex") VirtualTimedMutex final
- : public VirtualBasicLockable {
+ : public GenericBasicLockable<TimedMutex> {
public:
- VirtualTimedMutex() = default;
-
- VirtualTimedMutex(const VirtualTimedMutex&) = delete;
- VirtualTimedMutex(VirtualTimedMutex&&) = delete;
- VirtualTimedMutex& operator=(const VirtualTimedMutex&) = delete;
- VirtualTimedMutex& operator=(VirtualTimedMutex&&) = delete;
-
- TimedMutex& timed_mutex() { return timed_mutex_; }
-
- private:
- void DoLockOperation(Operation operation) override
- PW_NO_LOCK_SAFETY_ANALYSIS {
- switch (operation) {
- case Operation::kLock:
- return timed_mutex_.lock();
-
- case Operation::kUnlock:
- default:
- return timed_mutex_.unlock();
- }
- }
-
- TimedMutex timed_mutex_;
+ TimedMutex& timed_mutex() { return impl(); }
};
} // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/virtual_basic_lockable.h b/pw_sync/public/pw_sync/virtual_basic_lockable.h
index 501036535..c41f39b5c 100644
--- a/pw_sync/public/pw_sync/virtual_basic_lockable.h
+++ b/pw_sync/public/pw_sync/virtual_basic_lockable.h
@@ -70,4 +70,34 @@ class PW_LOCKABLE("pw::sync::NoOpLock") NoOpLock final
void DoLockOperation(Operation) override {}
};
+/// Templated base class to facilitate making "Virtual{LockType}" from a
+/// "LockType" class that provides `lock()` and `unlock()` methods.
+/// The resulting classes will derive from `VirtualBasicLockable`.
+///
+/// Example:
+/// class VirtualMutex : public GenericBasicLockable<Mutex> {};
+template <typename LockType>
+class GenericBasicLockable : public VirtualBasicLockable {
+ public:
+ virtual ~GenericBasicLockable() = default;
+
+ protected:
+ LockType& impl() { return impl_; }
+
+ private:
+ void DoLockOperation(Operation operation) override
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ switch (operation) {
+ case Operation::kLock:
+ return impl_.lock();
+
+ case Operation::kUnlock:
+ default:
+ return impl_.unlock();
+ }
+ }
+
+ LockType impl_;
+};
+
} // namespace pw::sync
diff --git a/pw_sync/pw_sync_private/borrow_lockable_tests.h b/pw_sync/pw_sync_private/borrow_lockable_tests.h
new file mode 100644
index 000000000..26fe81194
--- /dev/null
+++ b/pw_sync/pw_sync_private/borrow_lockable_tests.h
@@ -0,0 +1,324 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+/// This file contains tests that can be used to verify a lock type can be
+/// used in `pw::sync::Borrowable` to borrow types that use external locking.
+///
+/// Locks must at least meet C++'s \em BasicLockable named requirement. Tests
+/// should be added using the `ADD_BORROWABLE_...` macros from this file.
+///
+/// * If a lock is not \em TimedLockable, use `ADD_BORROWABLE_LOCK_TESTS`, e.g.
+/// `ADD_BORROWABLE_LOCK_TESTS(MyLock);`.
+///
+/// * If a lock is \em TimedLockable, use `ADD_BORROWABLE_TIMED_LOCK_TESTS` and
+/// provide the appropriate clock, e.g.
+/// `ADD_BORROWABLE_TIMED_LOCK_TESTS(MyLock, pw::chrono::SystemClock);`
+///
+/// * If the default test suite name is not suitable, use the `..._NAMED_TESTS`
+/// variants, e.g.
+/// `ADD_BORROWABLE_LOCK_NAMED_TESTS(MyTestSuite, pw::my_module::Lock);`.
+
+#include "gtest/gtest.h"
+#include "pw_sync/borrow.h"
+#include "pw_sync/lock_traits.h"
+
+namespace pw::sync {
+
+// Test fixtures.
+
+/// Simple struct that wraps a value.
+struct Base {
+ static constexpr int kInitialValue = 24;
+ int base_value = kInitialValue;
+};
+
+/// Simple struct that derives from `Base` and wraps a value.
+struct Derived : public Base {
+ static constexpr int kInitialValue = 42;
+ int value = kInitialValue;
+};
+
+/// Checks if a type has a method `locked()`.
+///
+/// Several fake locks are used in testing that simply update a bool instead of
+/// actually locking. The lock state for these types can be accessed using
+/// `locked()`.
+///
+/// @{
+template <typename Lock, typename = void>
+struct has_locked : std::false_type {};
+
+template <typename Lock>
+struct has_locked<Lock, std::void_t<decltype(std::declval<Lock>().locked())>>
+ : std::true_type {};
+/// @}
+
+/// Checks if a lock's state matches the expected state.
+///
+/// This method can check fake locks used for testing as well as locks that
+/// meet C++'s \em Lockable named requirement. This method is a no-op for lock
+/// types that only meet \em BasicLockable.
+///
+/// @param[in] lock The lock to check.
+/// @param[in] expected Indicates if the lock is expected to be locked.
+template <typename Lock>
+void CheckLocked(Lock& lock, bool expected) {
+ if constexpr (has_locked<Lock>::value) {
+ EXPECT_EQ(lock.locked(), expected);
+ } else if constexpr (is_lockable_v<Lock>) {
+ bool locked = !lock.try_lock();
+ EXPECT_EQ(locked, expected);
+ if (!locked) {
+ lock.unlock();
+ }
+ }
+}
+
+template <typename Lock>
+void TestAcquire() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ {
+ BorrowedPointer<Derived, Lock> borrowed = borrowable.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->value, Derived::kInitialValue);
+ borrowed->value = 13;
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+ EXPECT_EQ(derived.value, 13);
+}
+
+template <typename Lock>
+void TestConstAcquire() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ const Borrowable<Derived, Lock> const_borrowable(borrowable);
+ {
+ BorrowedPointer<Derived, Lock> borrowed = const_borrowable.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->value, Derived::kInitialValue);
+ borrowed->value = 13;
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+ EXPECT_EQ(derived.value, 13);
+}
+
+template <typename Lock>
+void TestRepeatedAcquire() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ {
+ BorrowedPointer<Derived, Lock> borrowed = borrowable.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->value, Derived::kInitialValue);
+ borrowed->value = 13;
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+ {
+ BorrowedPointer<Derived, Lock> borrowed = borrowable.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->value, 13);
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+}
+
+template <typename Lock>
+void TestMoveable() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ Borrowable<Derived, Lock> moved = std::move(borrowable);
+ {
+ BorrowedPointer<Derived, Lock> borrowed = moved.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->value, Derived::kInitialValue);
+ borrowed->value = 13;
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+}
+
+template <typename Lock>
+void TestCopyable() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ const Borrowable<Derived, Lock>& intermediate = borrowable;
+ Borrowable<Derived, Lock> copied(intermediate);
+ {
+ BorrowedPointer<Derived, Lock> borrowed = copied.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->value, Derived::kInitialValue);
+ borrowed->value = 13;
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+ EXPECT_EQ(derived.value, 13);
+}
+
+template <typename Lock>
+void TestCopyableCovariant() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ const Borrowable<Derived, Lock>& intermediate = borrowable;
+ Borrowable<Base, Lock> copied_base(intermediate);
+ {
+ BorrowedPointer<Base, Lock> borrowed = copied_base.acquire();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(borrowed->base_value, Base::kInitialValue);
+ borrowed->base_value = 13;
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+ EXPECT_EQ(derived.base_value, 13);
+}
+
+template <typename Lock>
+void TestTryAcquireSuccess() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ if constexpr (is_lockable_v<Lock>) {
+ std::optional<BorrowedPointer<Derived, Lock>> maybe_borrowed =
+ borrowable.try_acquire();
+ ASSERT_TRUE(maybe_borrowed.has_value());
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed.value()->value, Derived::kInitialValue);
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+}
+
+template <typename Lock>
+void TestTryAcquireFailure() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ lock.lock();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ if constexpr (is_lockable_v<Lock>) {
+ std::optional<BorrowedPointer<Derived, Lock>> maybe_borrowed =
+ borrowable.try_acquire();
+ EXPECT_FALSE(maybe_borrowed.has_value());
+ }
+ CheckLocked(lock, true); // Ensure the lock is held.
+ lock.unlock();
+}
+
+template <typename Lock>
+void TestTryAcquireForSuccess() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ if constexpr (is_lockable_for_v<Lock, decltype(std::chrono::seconds(0))>) {
+ std::optional<BorrowedPointer<Derived, Lock>> maybe_borrowed =
+ borrowable.try_acquire_for(std::chrono::seconds(0));
+ ASSERT_TRUE(maybe_borrowed.has_value());
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed.value()->value, Derived::kInitialValue);
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+}
+
+template <typename Lock>
+void TestTryAcquireForFailure() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ lock.lock();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ if constexpr (is_lockable_for_v<Lock, decltype(std::chrono::seconds(0))>) {
+ std::optional<BorrowedPointer<Derived, Lock>> maybe_borrowed =
+ borrowable.try_acquire_for(std::chrono::seconds(0));
+ EXPECT_FALSE(maybe_borrowed.has_value());
+ }
+ CheckLocked(lock, true); // Ensure the lock is held.
+ lock.unlock();
+}
+
+/// Fake clock for use with non-timed locks.
+///
+/// This clock is guaranteed to fail `is_lockable_until<Lock, Clock>` and as
+/// such is suitable to make the `TestTryAcquireUntilSuccess` and
+/// `TestTryAcquireUntilFailure` tests pass trivially for lock types that do not
+/// meet C++'s \em TimedLockable named requirement for any clock.
+struct NoClock {
+ using duration = void;
+ using time_point = void;
+};
+
+template <typename Lock, typename Clock = NoClock>
+void TestTryAcquireUntilSuccess() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ if constexpr (is_lockable_until_v<Lock, Clock>) {
+ std::optional<BorrowedPointer<Derived, Lock>> maybe_borrowed =
+ borrowable.try_acquire_until(Clock::time_point());
+ ASSERT_TRUE(maybe_borrowed.has_value());
+ CheckLocked(lock, true); // Ensure the lock is held.
+ EXPECT_EQ(maybe_borrowed.value()->value, Derived::kInitialValue);
+ }
+ CheckLocked(lock, false); // Ensure the lock is released.
+}
+
+template <typename Lock, typename Clock = NoClock>
+void TestTryAcquireUntilFailure() {
+ Lock lock;
+ Derived derived;
+ Borrowable<Derived, Lock> borrowable(derived, lock);
+ lock.lock();
+ CheckLocked(lock, true); // Ensure the lock is held.
+ if constexpr (is_lockable_until_v<Lock, Clock>) {
+ std::optional<BorrowedPointer<Derived, Lock>> maybe_borrowed =
+ borrowable.try_acquire_until(Clock::time_point());
+ EXPECT_FALSE(maybe_borrowed.has_value());
+ }
+ CheckLocked(lock, true); // Ensure the lock is held.
+ lock.unlock();
+}
+
+/// Register borrowable non-timed lock tests.
+#define PW_SYNC_ADD_BORROWABLE_LOCK_TESTS(lock) \
+ PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_TESTS(lock, NoClock)
+
+/// Register borrowable non-timed lock tests in a named test suite.
+#define PW_SYNC_ADD_BORROWABLE_LOCK_NAMED_TESTS(name, lock) \
+ PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_NAMED_TESTS(name, lock, NoClock)
+
+/// Register all borrowable lock tests.
+#define PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_TESTS(lock, clock) \
+ PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_NAMED_TESTS( \
+ Borrowable##lock##Test, lock, clock)
+
+/// Register all borrowable lock tests in a named test suite.
+#define PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_NAMED_TESTS(name, lock, clock) \
+ TEST(name, Acquire) { TestAcquire<lock>(); } \
+ TEST(name, ConstAcquire) { TestConstAcquire<lock>(); } \
+ TEST(name, RepeatedAcquire) { TestRepeatedAcquire<lock>(); } \
+ TEST(name, Moveable) { TestMoveable<lock>(); } \
+ TEST(name, Copyable) { TestCopyable<lock>(); } \
+ TEST(name, CopyableCovariant) { TestCopyableCovariant<lock>(); } \
+ TEST(name, TryAcquireForSuccess) { TestTryAcquireForSuccess<lock>(); } \
+ TEST(name, TryAcquireForFailure) { TestTryAcquireForFailure<lock>(); } \
+ TEST(name, TryAcquireUntilSuccess) { \
+ TestTryAcquireUntilSuccess<lock, clock>(); \
+ } \
+ TEST(name, TryAcquireUntilFailure) { \
+ TestTryAcquireUntilFailure<lock, clock>(); \
+ } \
+ static_assert(true, "trailing semicolon")
+
+} // namespace pw::sync
diff --git a/pw_sync/recursive_mutex_facade_test.cc b/pw_sync/recursive_mutex_facade_test.cc
index 6dd2bfbc8..55aa4bba1 100644
--- a/pw_sync/recursive_mutex_facade_test.cc
+++ b/pw_sync/recursive_mutex_facade_test.cc
@@ -29,7 +29,7 @@ void pw_sync_RecursiveMutex_CallUnlock(pw_sync_RecursiveMutex* mutex);
} // extern "C"
-// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
+// TODO: b/235284163 - Add real concurrency tests once we have pw::thread.
TEST(RecursiveMutex, LockUnlock) PW_NO_LOCK_SAFETY_ANALYSIS {
pw::sync::RecursiveMutex mutex;
diff --git a/pw_sync/thread_notification_facade_test.cc b/pw_sync/thread_notification_facade_test.cc
index fa10dfa54..c0dfb2141 100644
--- a/pw_sync/thread_notification_facade_test.cc
+++ b/pw_sync/thread_notification_facade_test.cc
@@ -25,7 +25,7 @@ TEST(ThreadNotification, EmptyInitialState) {
EXPECT_FALSE(notification.try_acquire());
}
-// TODO(b/235284163): Add real concurrency tests.
+// TODO: b/235284163 - Add real concurrency tests.
TEST(ThreadNotification, Release) {
ThreadNotification notification;
diff --git a/pw_sync/timed_mutex_facade_test.cc b/pw_sync/timed_mutex_facade_test.cc
index c63233757..a80fc3de0 100644
--- a/pw_sync/timed_mutex_facade_test.cc
+++ b/pw_sync/timed_mutex_facade_test.cc
@@ -17,6 +17,7 @@
#include "gtest/gtest.h"
#include "pw_chrono/system_clock.h"
#include "pw_sync/timed_mutex.h"
+#include "pw_sync_private/borrow_lockable_tests.h"
using pw::chrono::SystemClock;
using namespace std::chrono_literals;
@@ -45,13 +46,13 @@ constexpr SystemClock::duration kRoundedArbitraryDuration =
constexpr pw_chrono_SystemClock_Duration kRoundedArbitraryDurationInC =
PW_SYSTEM_CLOCK_MS(42);
-// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
+// TODO: b/235284163 - Add real concurrency tests once we have pw::thread.
TEST(TimedMutex, LockUnlock) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
mutex.lock();
mutex.unlock();
- // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // TODO: b/235284163 - Ensure it fails to lock when already held by someone
// else.
// EXPECT_FALSE(mutex.try_lock());
}
@@ -60,25 +61,25 @@ TimedMutex static_mutex;
TEST(TimedMutex, LockUnlockStatic) {
static_mutex.lock();
static_mutex.unlock();
- // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // TODO: b/235284163 - Ensure it fails to lock when already held by someone
// else.
// EXPECT_FALSE(static_mutex.try_lock());
}
TEST(TimedMutex, TryLockUnlock) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
const bool locked = mutex.try_lock();
EXPECT_TRUE(locked);
if (locked) {
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
- // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // TODO: b/235284163 - Ensure it fails to lock when already held by someone
// else.
}
TEST(TimedMutex, TryLockUnlockFor) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
SystemClock::time_point before = SystemClock::now();
const bool locked = mutex.try_lock_for(kRoundedArbitraryDuration);
@@ -88,16 +89,16 @@ TEST(TimedMutex, TryLockUnlockFor) {
EXPECT_LT(time_elapsed, kRoundedArbitraryDuration);
mutex.unlock();
}
- // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
+ // TODO: b/235284163 - Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and a zero length duration is used.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and a negative duration is used.
}
TEST(TimedMutex, TryLockUnlockUntil) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
const SystemClock::time_point deadline =
SystemClock::now() + kRoundedArbitraryDuration;
@@ -107,18 +108,22 @@ TEST(TimedMutex, TryLockUnlockUntil) {
EXPECT_LT(SystemClock::now(), deadline);
mutex.unlock();
}
- // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
+ // TODO: b/235284163 - Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and now is used.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and a timestamp in the past is used.
}
+PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_NAMED_TESTS(BorrowableTimedMutex,
+ TimedMutex,
+ chrono::SystemClock);
+
TEST(VirtualTimedMutex, LockUnlock) {
- pw::sync::VirtualTimedMutex mutex;
+ VirtualTimedMutex mutex;
mutex.lock();
- // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // TODO: b/235284163 - Ensure it fails to lock when already held by someone
// else.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
@@ -127,29 +132,42 @@ TEST(VirtualTimedMutex, LockUnlock) {
VirtualTimedMutex static_virtual_mutex;
TEST(VirtualTimedMutex, LockUnlockStatic) {
static_virtual_mutex.lock();
- // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // TODO: b/235284163 - Ensure it fails to lock when already held by someone
// else.
// EXPECT_FALSE(static_virtual_mutex.try_lock());
static_virtual_mutex.unlock();
}
+TEST(VirtualMutex, LockUnlockExternal) {
+ VirtualTimedMutex virtual_timed_mutex;
+ auto& mutex = virtual_timed_mutex.timed_mutex();
+ mutex.lock();
+ // TODO: b/235284163 - Ensure it fails to lock when already held.
+ // EXPECT_FALSE(mutex.try_lock());
+ mutex.unlock();
+}
+
+PW_SYNC_ADD_BORROWABLE_TIMED_LOCK_NAMED_TESTS(BorrowableVirtualTimedMutex,
+ VirtualTimedMutex,
+ chrono::SystemClock);
+
TEST(TimedMutex, LockUnlockInC) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
pw_sync_TimedMutex_CallLock(&mutex);
pw_sync_TimedMutex_CallUnlock(&mutex);
}
TEST(TimedMutex, TryLockUnlockInC) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
ASSERT_TRUE(pw_sync_TimedMutex_CallTryLock(&mutex));
- // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // TODO: b/235284163 - Ensure it fails to lock when already held by someone
// else.
// EXPECT_FALSE(pw_sync_TimedMutex_CallTryLock(&mutex));
pw_sync_TimedMutex_CallUnlock(&mutex);
}
TEST(TimedMutex, TryLockUnlockForInC) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
pw_chrono_SystemClock_TimePoint before = pw_chrono_SystemClock_Now();
ASSERT_TRUE(
@@ -158,16 +176,16 @@ TEST(TimedMutex, TryLockUnlockForInC) {
pw_chrono_SystemClock_TimeElapsed(before, pw_chrono_SystemClock_Now());
EXPECT_LT(time_elapsed.ticks, kRoundedArbitraryDurationInC.ticks);
pw_sync_TimedMutex_CallUnlock(&mutex);
- // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
+ // TODO: b/235284163 - Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and a zero length duration is used.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and a negative duration is used.
}
TEST(TimedMutex, TryLockUnlockUntilInC) {
- pw::sync::TimedMutex mutex;
+ TimedMutex mutex;
pw_chrono_SystemClock_TimePoint deadline;
deadline.duration_since_epoch.ticks =
pw_chrono_SystemClock_Now().duration_since_epoch.ticks +
@@ -176,11 +194,11 @@ TEST(TimedMutex, TryLockUnlockUntilInC) {
EXPECT_LT(pw_chrono_SystemClock_Now().duration_since_epoch.ticks,
deadline.duration_since_epoch.ticks);
pw_sync_TimedMutex_CallUnlock(&mutex);
- // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
+ // TODO: b/235284163 - Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and now is used.
- // TODO(b/235284163): Ensure it does not block and fails to lock when already
+ // TODO: b/235284163 - Ensure it does not block and fails to lock when already
// held by someone else and a timestamp in the past is used.
}
diff --git a/pw_sync/timed_thread_notification_facade_test.cc b/pw_sync/timed_thread_notification_facade_test.cc
index ee1da8983..8342b8b30 100644
--- a/pw_sync/timed_thread_notification_facade_test.cc
+++ b/pw_sync/timed_thread_notification_facade_test.cc
@@ -35,7 +35,7 @@ TEST(TimedThreadNotification, EmptyInitialState) {
EXPECT_FALSE(notification.try_acquire());
}
-// TODO(b/235284163): Add real concurrency tests.
+// TODO: b/235284163 - Add real concurrency tests.
TEST(TimedThreadNotification, Release) {
TimedThreadNotification notification;
diff --git a/pw_sync_baremetal/BUILD.bazel b/pw_sync_baremetal/BUILD.bazel
index d401d99d7..3295b9591 100644
--- a/pw_sync_baremetal/BUILD.bazel
+++ b/pw_sync_baremetal/BUILD.bazel
@@ -22,7 +22,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "interrupt_spin_lock_headers",
+ name = "interrupt_spin_lock",
hdrs = [
"public/pw_sync_baremetal/interrupt_spin_lock_inline.h",
"public/pw_sync_baremetal/interrupt_spin_lock_native.h",
@@ -34,13 +34,7 @@ pw_cc_library(
"public_overrides",
],
target_compatible_with = ["@platforms//os:none"],
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock",
- target_compatible_with = ["@platforms//os:none"],
deps = [
- ":interrupt_spin_lock_headers",
"//pw_assert",
"//pw_sync:interrupt_spin_lock_facade",
"//pw_sync:yield_core",
@@ -48,7 +42,7 @@ pw_cc_library(
)
pw_cc_library(
- name = "mutex_headers",
+ name = "mutex",
hdrs = [
"public/pw_sync_baremetal/mutex_inline.h",
"public/pw_sync_baremetal/mutex_native.h",
@@ -60,13 +54,7 @@ pw_cc_library(
"public_overrides",
],
target_compatible_with = ["@platforms//os:none"],
-)
-
-pw_cc_library(
- name = "mutex",
- target_compatible_with = ["@platforms//os:none"],
deps = [
- ":mutex_headers",
"//pw_assert",
"//pw_sync:mutex_facade",
],
diff --git a/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h b/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h
index 70b286bc2..5cbad3187 100644
--- a/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h
+++ b/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h
@@ -24,7 +24,7 @@ constexpr InterruptSpinLock::InterruptSpinLock() : native_type_() {}
inline void InterruptSpinLock::lock() { PW_ASSERT(try_lock()); }
inline bool InterruptSpinLock::try_lock() {
- // TODO(b/235352722): Use the pw_interrupt API here to disable interrupts.
+ // TODO: b/235352722 - Use the pw_interrupt API here to disable interrupts.
return !native_type_.test_and_set(std::memory_order_acquire);
}
diff --git a/pw_sync_embos/BUILD.bazel b/pw_sync_embos/BUILD.bazel
index 81f2e83e2..be83756b5 100644
--- a/pw_sync_embos/BUILD.bazel
+++ b/pw_sync_embos/BUILD.bazel
@@ -22,7 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "binary_semaphore_headers",
+ name = "binary_semaphore",
+ srcs = [
+ "binary_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_embos/binary_semaphore_inline.h",
"public/pw_sync_embos/binary_semaphore_native.h",
@@ -36,31 +39,20 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
+ # TODO: b/234876414 - This should depend on embOS but our third parties
+ # currently do not have Bazel support.
deps = [
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
- "//pw_chrono_embos:system_clock_headers",
- ],
-)
-
-pw_cc_library(
- name = "binary_semaphore",
- srcs = [
- "binary_semaphore.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:embos",
- ],
- deps = [
- ":binary_semaphore_headers",
"//pw_interrupt:context",
"//pw_sync:binary_semaphore_facade",
],
)
pw_cc_library(
- name = "counting_semaphore_headers",
+ name = "counting_semaphore",
+ srcs = [
+ "counting_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_embos/counting_semaphore_inline.h",
"public/pw_sync_embos/counting_semaphore_native.h",
@@ -74,31 +66,17 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
+ # TODO: b/234876414 - This should depend on embOS but our third parties
+ # currently do not have Bazel support.
deps = [
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
- "//pw_chrono_embos:system_clock_headers",
- ],
-)
-
-pw_cc_library(
- name = "counting_semaphore",
- srcs = [
- "counting_semaphore.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:embos",
- ],
- deps = [
- ":counting_semaphore_headers",
"//pw_interrupt:context",
"//pw_sync:counting_semaphore_facade",
],
)
pw_cc_library(
- name = "mutex_headers",
+ name = "mutex",
hdrs = [
"public/pw_sync_embos/mutex_inline.h",
"public/pw_sync_embos/mutex_native.h",
@@ -112,18 +90,8 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
- deps = [
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
- "//pw_sync:mutex_facade",
- ],
-)
-
-pw_cc_library(
- name = "mutex",
- target_compatible_with = [
- "//pw_build/constraints/rtos:embos",
- ],
+ # TODO: b/234876414 - This should depend on embOS but our third parties
+ # currently do not have Bazel support.
deps = [
":mutex_headers",
"//pw_sync:mutex_facade",
@@ -131,7 +99,10 @@ pw_cc_library(
)
pw_cc_library(
- name = "timed_mutex_headers",
+ name = "timed_mutex",
+ srcs = [
+ "timed_mutex.cc",
+ ],
hdrs = [
"public/pw_sync_embos/timed_mutex_inline.h",
"public_overrides/pw_sync_backend/timed_mutex_inline.h",
@@ -143,32 +114,27 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
+ # TODO: b/234876414 - This should depend on embOS but our third parties
+ # currently do not have Bazel support.
deps = [
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
+ "//pw_interrupt:context",
"//pw_sync:timed_mutex_facade",
],
)
pw_cc_library(
- name = "timed_mutex",
- srcs = [
- "timed_mutex.cc",
- ],
+ name = "interrupt_spin_lock_headers",
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
- deps = [
- ":timed_mutex_headers",
- "//pw_chrono_embos:system_clock_headers",
- "//pw_interrupt:context",
- "//pw_sync:timed_mutex_facade",
- ],
)
pw_cc_library(
- name = "interrupt_spin_lock_headers",
+ name = "interrupt_spin_lock",
+ srcs = [
+ "interrupt_spin_lock.cc",
+ ],
hdrs = [
"public/pw_sync_embos/interrupt_spin_lock_inline.h",
"public/pw_sync_embos/interrupt_spin_lock_native.h",
@@ -182,20 +148,9 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
+ # TODO: b/234876414 - This should depend on embOS but our third parties
# currently do not have Bazel support.
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock",
- srcs = [
- "interrupt_spin_lock.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:embos",
- ],
deps = [
- ":interrupt_spin_lock_headers",
"//pw_sync:interrupt_spin_lock_facade",
],
)
diff --git a/pw_sync_freertos/BUILD.bazel b/pw_sync_freertos/BUILD.bazel
index 1be8b9faa..4bbaa6dc8 100644
--- a/pw_sync_freertos/BUILD.bazel
+++ b/pw_sync_freertos/BUILD.bazel
@@ -22,7 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "binary_semaphore_headers",
+ name = "binary_semaphore",
+ srcs = [
+ "binary_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_freertos/binary_semaphore_inline.h",
"public/pw_sync_freertos/binary_semaphore_native.h",
@@ -39,29 +42,17 @@ pw_cc_library(
deps = [
"//pw_assert",
"//pw_chrono:system_clock",
- "//pw_chrono_freertos:system_clock_headers",
+ "//pw_interrupt:context",
+ "//pw_sync:binary_semaphore_facade",
"@freertos",
],
)
pw_cc_library(
- name = "binary_semaphore",
+ name = "counting_semaphore",
srcs = [
- "binary_semaphore.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":binary_semaphore_headers",
- "//pw_assert",
- "//pw_interrupt:context",
- "//pw_sync:binary_semaphore_facade",
+ "counting_semaphore.cc",
],
-)
-
-pw_cc_library(
- name = "counting_semaphore_headers",
hdrs = [
"public/pw_sync_freertos/counting_semaphore_inline.h",
"public/pw_sync_freertos/counting_semaphore_native.h",
@@ -72,23 +63,6 @@ pw_cc_library(
"public",
"public_overrides",
],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- "//pw_assert",
- "//pw_chrono:system_clock",
- "//pw_chrono_freertos:system_clock_headers",
- "//pw_sync:counting_semaphore_facade",
- "@freertos",
- ],
-)
-
-pw_cc_library(
- name = "counting_semaphore",
- srcs = [
- "counting_semaphore.cc",
- ],
target_compatible_with = select({
# Not compatible with this FreeRTOS config, because it does not enable
# FreeRTOS counting semaphores. We mark it explicitly incompatible to
@@ -101,15 +75,16 @@ pw_cc_library(
],
}),
deps = [
- ":counting_semaphore_headers",
"//pw_assert",
+ "//pw_chrono:system_clock",
"//pw_interrupt:context",
"//pw_sync:counting_semaphore_facade",
+ "@freertos",
],
)
pw_cc_library(
- name = "mutex_headers",
+ name = "mutex",
hdrs = [
"public/pw_sync_freertos/mutex_inline.h",
"public/pw_sync_freertos/mutex_native.h",
@@ -126,23 +101,16 @@ pw_cc_library(
deps = [
"//pw_assert",
"//pw_interrupt:context",
+ "//pw_sync:mutex_facade",
"@freertos",
],
)
pw_cc_library(
- name = "mutex",
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":mutex_headers",
- "//pw_sync:mutex_facade",
+ name = "thread_notification",
+ srcs = [
+ "thread_notification.cc",
],
-)
-
-pw_cc_library(
- name = "thread_notification_headers",
hdrs = [
"public/pw_sync_freertos/config.h",
"public/pw_sync_freertos/thread_notification_inline.h",
@@ -163,28 +131,16 @@ pw_cc_library(
"//pw_polyfill",
"//pw_sync:interrupt_spin_lock",
"//pw_sync:lock_annotations",
+ "//pw_sync:thread_notification_facade",
"@freertos",
],
)
pw_cc_library(
- name = "thread_notification",
+ name = "timed_thread_notification",
srcs = [
- "thread_notification.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":thread_notification_headers",
- "//pw_assert",
- "//pw_interrupt:context",
- "//pw_sync:thread_notification_facade",
+ "timed_thread_notification.cc",
],
-)
-
-pw_cc_library(
- name = "timed_thread_notification_headers",
hdrs = [
"public/pw_sync_freertos/timed_thread_notification_inline.h",
"public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h",
@@ -197,31 +153,19 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
+ "//pw_assert",
"//pw_chrono:system_clock",
+ "//pw_interrupt:context",
"//pw_sync:timed_thread_notification_facade",
"@freertos",
],
)
pw_cc_library(
- name = "timed_thread_notification",
+ name = "timed_mutex",
srcs = [
- "timed_thread_notification.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":timed_thread_notification_headers",
- "//pw_assert",
- "//pw_chrono_freertos:system_clock_headers",
- "//pw_interrupt:context",
- "//pw_sync:timed_thread_notification_facade",
+ "timed_mutex.cc",
],
-)
-
-pw_cc_library(
- name = "timed_mutex_headers",
hdrs = [
"public/pw_sync_freertos/timed_mutex_inline.h",
"public_overrides/pw_sync_backend/timed_mutex_inline.h",
@@ -234,32 +178,20 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
+ "//pw_assert",
"//pw_chrono:system_clock",
+ "//pw_interrupt:context",
+ "//pw_sync:mutex",
"//pw_sync:timed_mutex_facade",
"@freertos",
],
)
pw_cc_library(
- name = "timed_mutex",
+ name = "interrupt_spin_lock",
srcs = [
- "timed_mutex.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":mutex_headers",
- ":timed_mutex_headers",
- "//pw_assert",
- "//pw_chrono_freertos:system_clock_headers",
- "//pw_interrupt:context",
- "//pw_sync:timed_mutex_facade",
+ "interrupt_spin_lock.cc",
],
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock_headers",
hdrs = [
"public/pw_sync_freertos/interrupt_spin_lock_inline.h",
"public/pw_sync_freertos/interrupt_spin_lock_native.h",
@@ -274,30 +206,17 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- "@freertos",
- ],
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock",
- srcs = [
- "interrupt_spin_lock.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":interrupt_spin_lock_headers",
"//pw_assert",
"//pw_interrupt:context",
"//pw_sync:interrupt_spin_lock_facade",
+ "@freertos",
],
)
-# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# TODO: b/228998350 - Figure out how to conditionally enable this test like GN
#
# You can instantiate this with your own implementation of
-# "//pw_thread:test_threads_header", see
+# "//pw_thread:non_portable_test_thread_options", see
# ":thread_notification_test_with_static_threads" below as an example.
# pw_cc_library(
# name = "thread_notification_test",
@@ -307,13 +226,13 @@ pw_cc_library(
# target_compatible_with = [
# "//pw_build/constraints/rtos:freertos",
# ],
-# # TODO(b/234876414): This should depend on FreeRTOS but our third parties
+# # TODO: b/234876414 - This should depend on FreeRTOS but our third parties
# # currently do not have Bazel support.
# deps = [
# "//pw_chrono:system_clock",
# "//pw_sync:thread_notification",
# "//pw_thread:sleep",
-# "//pw_thread:test_threads_header",
+# "//pw_thread:non_portable_test_thread_options",
# "//pw_thread:thread",
# "//pw_unit_test",
# ],
@@ -326,7 +245,7 @@ filegroup(
],
)
-# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# TODO: b/228998350 - Figure out how to conditionally enable this test like GN
# with:
# enable_if = pw_sync_THREAD_NOTIFICATION_BACKEND ==
# "$dir_pw_sync_freertos:thread_notification" &&
@@ -344,23 +263,23 @@ filegroup(
# ],
# )
-# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# TODO: b/228998350 - Figure out how to conditionally enable this test like GN
#
# You can instantiate this with your own implementation of
-# "//pw_thread:test_threads_header", see
+# "//pw_thread:non_portable_test_thread_options", see
# ":timed_thread_notification_test_with_static_threads" below as an example.
#pw_cc_library(
# name = "timed_thread_notification_test",
# srcs = [
# "timed_thread_notification_test.cc",
# ],
-# # TODO(b/234876414): This should depend on FreeRTOS but our third parties
+# # TODO: b/234876414 - This should depend on FreeRTOS but our third parties
# # currently do not have Bazel support.
# deps = [
# "//pw_chrono:system_clock",
# "//pw_sync:timed_thread_notification",
# "//pw_thread:sleep",
-# "//pw_thread:test_threads_header",
+# "//pw_thread:non_portable_test_thread_options",
# "//pw_thread:thread",
# "//pw_unit_test",
# ],
@@ -372,7 +291,7 @@ filegroup(
],
)
-# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# TODO: b/228998350 - Figure out how to conditionally enable this test like GN
# with:
# enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND ==
# "$dir_pw_sync_freertos:timed_thread_notification" &&
diff --git a/pw_sync_freertos/BUILD.gn b/pw_sync_freertos/BUILD.gn
index 89bc0a11a..8ac915df6 100644
--- a/pw_sync_freertos/BUILD.gn
+++ b/pw_sync_freertos/BUILD.gn
@@ -245,7 +245,7 @@ pw_test_group("tests") {
]
}
-# You can instantiate this with your own provided "$dir_pw_thread:test_threads",
+# You can instantiate this with your own provided "$dir_pw_thread:non_portable_test_thread_options",
# see ":thread_notification_test_with_static_threads" below as an example.
pw_source_set("thread_notification_test") {
sources = [ "thread_notification_test.cc" ]
@@ -253,8 +253,8 @@ pw_source_set("thread_notification_test") {
"$dir_pw_chrono:system_clock",
"$dir_pw_sync:thread_notification",
"$dir_pw_third_party/freertos",
+ "$dir_pw_thread:non_portable_test_thread_options",
"$dir_pw_thread:sleep",
- "$dir_pw_thread:test_threads",
"$dir_pw_thread:thread",
"$dir_pw_unit_test",
]
@@ -271,7 +271,7 @@ pw_test("thread_notification_test_with_static_threads") {
]
}
-# You can instantiate this with your own provided "$dir_pw_thread:test_threads",
+# You can instantiate this with your own provided "$dir_pw_thread:non_portable_test_thread_options",
# see ":timed_thread_notification_test_with_static_threads" below as an example.
pw_source_set("timed_thread_notification_test") {
sources = [ "timed_thread_notification_test.cc" ]
@@ -279,8 +279,8 @@ pw_source_set("timed_thread_notification_test") {
"$dir_pw_chrono:system_clock",
"$dir_pw_sync:timed_thread_notification",
"$dir_pw_third_party/freertos",
+ "$dir_pw_thread:non_portable_test_thread_options",
"$dir_pw_thread:sleep",
- "$dir_pw_thread:test_threads",
"$dir_pw_thread:thread",
"$dir_pw_unit_test",
]
diff --git a/pw_sync_freertos/thread_notification_test.cc b/pw_sync_freertos/thread_notification_test.cc
index 0e6322fe1..d8ab27381 100644
--- a/pw_sync_freertos/thread_notification_test.cc
+++ b/pw_sync_freertos/thread_notification_test.cc
@@ -15,12 +15,13 @@
#include "pw_sync/thread_notification.h"
#include <chrono>
+#include <optional>
#include "FreeRTOS.h"
#include "gtest/gtest.h"
#include "pw_chrono/system_clock.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread/thread.h"
#include "pw_thread/thread_core.h"
#include "task.h"
@@ -66,6 +67,7 @@ class NotificationAcquirer : public thread::ThreadCore {
TEST(ThreadNotification, AcquireWithoutSuspend) {
NotificationAcquirer notification_acquirer;
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
Thread thread =
Thread(thread::test::TestOptionsThread0(), notification_acquirer);
diff --git a/pw_sync_freertos/timed_thread_notification.cc b/pw_sync_freertos/timed_thread_notification.cc
index 07625dec6..ee75a5b9c 100644
--- a/pw_sync_freertos/timed_thread_notification.cc
+++ b/pw_sync_freertos/timed_thread_notification.cc
@@ -15,6 +15,7 @@
#include "pw_sync/timed_thread_notification.h"
#include <algorithm>
+#include <optional>
#include "FreeRTOS.h"
#include "pw_assert/check.h"
diff --git a/pw_sync_freertos/timed_thread_notification_test.cc b/pw_sync_freertos/timed_thread_notification_test.cc
index 0ee87c88c..5b9bff628 100644
--- a/pw_sync_freertos/timed_thread_notification_test.cc
+++ b/pw_sync_freertos/timed_thread_notification_test.cc
@@ -15,12 +15,13 @@
#include "pw_sync/timed_thread_notification.h"
#include <chrono>
+#include <optional>
#include "FreeRTOS.h"
#include "gtest/gtest.h"
#include "pw_chrono/system_clock.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread/thread.h"
#include "pw_thread/thread_core.h"
#include "task.h"
@@ -38,7 +39,7 @@ using pw::thread::Thread;
// the FreeRTOS optimized TimedThreadNotification backend.
#if INCLUDE_vTaskSuspend == 1
-class NotificationAcquirer : public thread::ThreadCore {
+class TimedNotificationAcquirer : public thread::ThreadCore {
public:
void WaitUntilRunning() { started_notification_.acquire(); }
void Release() { unblock_notification_.release(); }
@@ -67,7 +68,8 @@ class NotificationAcquirer : public thread::ThreadCore {
};
TEST(TimedThreadNotification, AcquireWithoutSuspend) {
- NotificationAcquirer notification_acquirer;
+ TimedNotificationAcquirer notification_acquirer;
+ // TODO: b/290860904 - Replace TestOptionsThread0 with TestThreadContext.
Thread thread =
Thread(thread::test::TestOptionsThread0(), notification_acquirer);
@@ -89,7 +91,7 @@ TEST(TimedThreadNotification, AcquireWithoutSuspend) {
}
TEST(TimedThreadNotification, AcquireWithSuspend) {
- NotificationAcquirer notification_acquirer;
+ TimedNotificationAcquirer notification_acquirer;
Thread thread =
Thread(thread::test::TestOptionsThread0(), notification_acquirer);
diff --git a/pw_sync_stl/Android.bp b/pw_sync_stl/Android.bp
new file mode 100644
index 000000000..914e5b615
--- /dev/null
+++ b/pw_sync_stl/Android.bp
@@ -0,0 +1,52 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_sync_stl_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ "public_overrides",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+filegroup {
+ name: "pw_sync_stl_src_files",
+ srcs: [
+ "binary_semaphore.cc",
+ "counting_semaphore.cc",
+ "mutex.cc",
+ ],
+}
+
+cc_defaults {
+ name: "pw_sync_stl_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_sync_stl_include_dirs",
+ ],
+
+ export_header_lib_headers: [
+ "pw_sync_stl_include_dirs",
+ ],
+ srcs: [
+ ":pw_sync_stl_src_files"
+ ],
+}
diff --git a/pw_sync_stl/BUILD.bazel b/pw_sync_stl/BUILD.bazel
index d0e62b86c..f8ec59b6b 100644
--- a/pw_sync_stl/BUILD.bazel
+++ b/pw_sync_stl/BUILD.bazel
@@ -26,7 +26,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "binary_semaphore_headers",
+ name = "binary_semaphore",
+ srcs = [
+ "binary_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_stl/binary_semaphore_inline.h",
"public/pw_sync_stl/binary_semaphore_native.h",
@@ -39,18 +42,6 @@ pw_cc_library(
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- "//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "binary_semaphore",
- srcs = [
- "binary_semaphore.cc",
- ],
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
- ":binary_semaphore_headers",
"//pw_assert",
"//pw_chrono:system_clock",
"//pw_sync:binary_semaphore_facade",
@@ -58,7 +49,10 @@ pw_cc_library(
)
pw_cc_library(
- name = "counting_semaphore_headers",
+ name = "counting_semaphore",
+ srcs = [
+ "counting_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_stl/counting_semaphore_inline.h",
"public/pw_sync_stl/counting_semaphore_native.h",
@@ -71,18 +65,6 @@ pw_cc_library(
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- "//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "counting_semaphore",
- srcs = [
- "counting_semaphore.cc",
- ],
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
- ":counting_semaphore_headers",
"//pw_assert",
"//pw_chrono:system_clock",
"//pw_sync:counting_semaphore_facade",
@@ -90,7 +72,8 @@ pw_cc_library(
)
pw_cc_library(
- name = "mutex_headers",
+ name = "mutex",
+ srcs = ["mutex.cc"],
hdrs = [
"public/pw_sync_stl/mutex_inline.h",
"public/pw_sync_stl/mutex_native.h",
@@ -102,22 +85,14 @@ pw_cc_library(
"public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = ["//pw_sync:mutex_facade"],
-)
-
-pw_cc_library(
- name = "mutex",
- srcs = ["mutex.cc"],
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":mutex_headers",
"//pw_assert",
"//pw_sync:mutex_facade",
],
)
pw_cc_library(
- name = "timed_mutex_headers",
+ name = "timed_mutex",
hdrs = [
"public/pw_sync_stl/timed_mutex_inline.h",
"public_overrides/pw_sync_backend/timed_mutex_inline.h",
@@ -129,20 +104,12 @@ pw_cc_library(
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
"//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "timed_mutex",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
- ":timed_mutex_headers",
"//pw_sync:timed_mutex_facade",
],
)
pw_cc_library(
- name = "recursive_mutex_headers",
+ name = "recursive_mutex",
hdrs = [
"public/pw_sync_stl/recursive_mutex_inline.h",
"public/pw_sync_stl/recursive_mutex_native.h",
@@ -154,20 +121,14 @@ pw_cc_library(
"public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = ["//pw_assert"],
-)
-
-pw_cc_library(
- name = "recursive_mutex",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":recursive_mutex_headers",
+ "//pw_assert",
"//pw_sync:recursive_mutex_facade",
],
)
pw_cc_library(
- name = "interrupt_spin_lock_headers",
+ name = "interrupt_spin_lock",
hdrs = [
"public/pw_sync_stl/interrupt_spin_lock_inline.h",
"public/pw_sync_stl/interrupt_spin_lock_native.h",
@@ -180,22 +141,13 @@ pw_cc_library(
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- "//pw_sync:yield_core",
- ],
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
- ":interrupt_spin_lock_headers",
"//pw_sync:interrupt_spin_lock_facade",
"//pw_sync:yield_core",
],
)
pw_cc_library(
- name = "condition_variable_headers",
+ name = "condition_variable",
hdrs = [
"public/pw_sync_stl/condition_variable_inline.h",
"public/pw_sync_stl/condition_variable_native.h",
@@ -207,22 +159,16 @@ pw_cc_library(
"public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
-)
-
-pw_cc_library(
- name = "condition_variable",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":condition_variable_headers",
"//pw_sync:condition_variable_facade",
],
)
-# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# TODO: b/228998350 - Figure out how to conditionally enable this test like GN
# pw_cc_test(
# name = "condition_variable_test",
# deps = [
# "//pw_sync:condition_variable_test",
-# "//pw_thread_stl:test_threads",
+# "//pw_thread_stl:non_portable_test_thread_options",
# ]
# )
diff --git a/pw_sync_stl/BUILD.gn b/pw_sync_stl/BUILD.gn
index e5b2d5d9a..0cad5efa7 100644
--- a/pw_sync_stl/BUILD.gn
+++ b/pw_sync_stl/BUILD.gn
@@ -174,7 +174,7 @@ pw_test("condition_variable_test") {
"$dir_pw_sync_stl:condition_variable"
deps = [
"$dir_pw_sync:condition_variable_test",
- "$dir_pw_thread_stl:test_threads",
+ "$dir_pw_thread_stl:non_portable_test_thread_options",
]
}
diff --git a/pw_sync_threadx/BUILD.bazel b/pw_sync_threadx/BUILD.bazel
index 2b767d8d5..85ae37e6e 100644
--- a/pw_sync_threadx/BUILD.bazel
+++ b/pw_sync_threadx/BUILD.bazel
@@ -22,7 +22,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "binary_semaphore_headers",
+ name = "binary_semaphore",
+ srcs = [
+ "binary_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_threadx/binary_semaphore_inline.h",
"public/pw_sync_threadx/binary_semaphore_native.h",
@@ -36,31 +39,19 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
],
+ # TODO: b/290364219 - Add threadx as a dependency.
deps = [
- # TODO(b/234876414): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "binary_semaphore",
- srcs = [
- "binary_semaphore.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:threadx",
- ],
- deps = [
- ":binary_semaphore_headers",
- "//pw_chrono_threadx:system_clock_headers",
"//pw_interrupt:context",
"//pw_sync:binary_semaphore_facade",
],
)
pw_cc_library(
- name = "counting_semaphore_headers",
+ name = "counting_semaphore",
+ srcs = [
+ "counting_semaphore.cc",
+ ],
hdrs = [
"public/pw_sync_threadx/counting_semaphore_inline.h",
"public/pw_sync_threadx/counting_semaphore_native.h",
@@ -74,32 +65,16 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
],
+ # TODO: b/290364219 - Add threadx as a dependency.
deps = [
- # TODO(b/234876414): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
- # do not have Bazel support.
"//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "counting_semaphore",
- srcs = [
- "counting_semaphore.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:threadx",
- ],
- deps = [
- ":counting_semaphore_headers",
- "//pw_chrono_threadx:system_clock_headers",
"//pw_interrupt:context",
"//pw_sync:counting_semaphore_facade",
],
)
pw_cc_library(
- name = "mutex_headers",
+ name = "mutex",
hdrs = [
"public/pw_sync_threadx/mutex_inline.h",
"public/pw_sync_threadx/mutex_native.h",
@@ -113,26 +88,17 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
],
+ # TODO: b/290364219 - Add threadx as a dependency.
deps = [
- # TODO(b/234876414): This should depend on ThreadX but our third parties currently
- # do not have Bazel support.
"//pw_sync:mutex_facade",
],
)
pw_cc_library(
- name = "mutex",
- target_compatible_with = [
- "//pw_build/constraints/rtos:threadx",
- ],
- deps = [
- ":mutex_headers",
- "//pw_sync:mutex_facade",
+ name = "timed_mutex",
+ srcs = [
+ "timed_mutex.cc",
],
-)
-
-pw_cc_library(
- name = "timed_mutex_headers",
hdrs = [
"public/pw_sync_threadx/timed_mutex_inline.h",
"public_overrides/pw_sync_backend/timed_mutex_inline.h",
@@ -144,32 +110,19 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
],
+ # TODO: b/290364219 - Add threadx as a dependency.
deps = [
- # TODO(b/234876414): This should depend on ThreadX but our third parties currently
- # do not have Bazel support.
"//pw_chrono:system_clock",
+ "//pw_interrupt:context",
"//pw_sync:timed_mutex_facade",
],
)
pw_cc_library(
- name = "timed_mutex",
+ name = "interrupt_spin_lock",
srcs = [
- "timed_mutex.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:threadx",
- ],
- deps = [
- ":timed_mutex_headers",
- "//pw_chrono_threadx:system_clock_headers",
- "//pw_interrupt:context",
- "//pw_sync:timed_mutex_facade",
+ "interrupt_spin_lock.cc",
],
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock_headers",
hdrs = [
"public/pw_sync_threadx/interrupt_spin_lock_inline.h",
"public/pw_sync_threadx/interrupt_spin_lock_native.h",
@@ -180,23 +133,11 @@ pw_cc_library(
"public",
"public_overrides",
],
- # TODO(b/234876414): This should depend on ThreadX but our third parties currently
- # do not have Bazel support.
- target_compatible_with = [
- "//pw_build/constraints/rtos:threadx",
- ],
-)
-
-pw_cc_library(
- name = "interrupt_spin_lock",
- srcs = [
- "interrupt_spin_lock.cc",
- ],
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
],
+ # TODO: b/290364219 - Add threadx as a dependency.
deps = [
- ":interrupt_spin_lock_headers",
"//pw_sync:interrupt_spin_lock_facade",
],
)
diff --git a/pw_sync_zephyr/CMakeLists.txt b/pw_sync_zephyr/CMakeLists.txt
index ce23060ca..c52ffc160 100644
--- a/pw_sync_zephyr/CMakeLists.txt
+++ b/pw_sync_zephyr/CMakeLists.txt
@@ -14,39 +14,93 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(CONFIG_PIGWEED_SYNC_MUTEX)
- pw_add_library(pw_sync_zephyr.mutex_backend INTERFACE
- HEADERS
- public/pw_sync_zephyr/mutex_inline.h
- public/pw_sync_zephyr/mutex_native.h
- public_overrides/pw_sync_backend/mutex_inline.h
- public_overrides/pw_sync_backend/mutex_native.h
- PUBLIC_INCLUDES
- public
- public_overrides
- PUBLIC_DEPS
- pw_sync.mutex.facade
- )
- zephyr_link_libraries(pw_sync_zephyr.mutex_backend)
- zephyr_link_interface(pw_sync_zephyr.mutex_backend)
-endif()
+pw_add_library(pw_sync_zephyr.mutex_backend INTERFACE
+ HEADERS
+ public/pw_sync_zephyr/mutex_inline.h
+ public/pw_sync_zephyr/mutex_native.h
+ public_overrides/pw_sync_backend/mutex_inline.h
+ public_overrides/pw_sync_backend/mutex_native.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_sync.mutex.facade
+)
+pw_zephyrize_libraries_ifdef(
+ CONFIG_PIGWEED_SYNC_MUTEX
+ pw_sync_zephyr.mutex_backend
+)
-if(CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE)
- pw_add_library(pw_sync_zephyr.binary_semaphore_backend STATIC
- HEADERS
- public/pw_sync_zephyr/binary_semaphore_native.h
- public/pw_sync_zephyr/binary_semaphore_inline.h
- public_overrides/pw_sync_backend/binary_semaphore_native.h
- public_overrides/pw_sync_backend/binary_semaphore_inline.h
- PUBLIC_INCLUDES
- public
- public_overrides
- PUBLIC_DEPS
- pw_chrono.system_clock
- pw_sync.binary_semaphore.facade
- SOURCES
- binary_semaphore.cc
- )
- zephyr_link_libraries(pw_sync_zephyr.binary_semaphore_backend)
- zephyr_link_interface(pw_sync_zephyr.binary_semaphore_backend)
-endif()
+pw_add_library(pw_sync_zephyr.binary_semaphore_backend STATIC
+ HEADERS
+ public/pw_sync_zephyr/binary_semaphore_native.h
+ public/pw_sync_zephyr/binary_semaphore_inline.h
+ public_overrides/pw_sync_backend/binary_semaphore_native.h
+ public_overrides/pw_sync_backend/binary_semaphore_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_sync.binary_semaphore.facade
+ SOURCES
+ binary_semaphore.cc
+)
+pw_zephyrize_libraries_ifdef(
+ CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE
+ pw_sync_zephyr.binary_semaphore_backend
+)
+
+pw_add_library(pw_sync_zephyr.interrupt_spin_lock_backend STATIC
+ HEADERS
+ public/pw_sync_zephyr/interrupt_spin_lock_inline.h
+ public/pw_sync_zephyr/interrupt_spin_lock_native.h
+ public_overrides/pw_sync_backend/interrupt_spin_lock_inline.h
+ public_overrides/pw_sync_backend/interrupt_spin_lock_native.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_sync.interrupt_spin_lock.facade
+ SOURCES
+ interrupt_spin_lock.cc
+)
+pw_zephyrize_libraries_ifdef(
+ CONFIG_PIGWEED_SYNC_INTERRUPT_SPIN_LOCK
+ pw_sync_zephyr.interrupt_spin_lock_backend
+)
+
+pw_add_library(pw_sync_zephyr.thread_notification_backend INTERFACE
+ HEADERS
+ public/pw_sync_zephyr/thread_notification_inline.h
+ public/pw_sync_zephyr/thread_notification_native.h
+ public_overrides/thread_notification/pw_sync_backend/thread_notification_inline.h
+ public_overrides/thread_notification/pw_sync_backend/thread_notification_native.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides/thread_notification
+ PUBLIC_DEPS
+ pw_sync.binary_semaphore
+ pw_sync.thread_notification.facade
+)
+pw_zephyrize_libraries_ifdef(
+ CONFIG_PIGWEED_SYNC_THREAD_NOTIFICATION
+ pw_sync_zephyr.thread_notification_backend
+)
+
+pw_add_library(pw_sync_zephyr.timed_thread_notification_backend INTERFACE
+ HEADERS
+ public/pw_sync_zephyr/timed_thread_notification_inline.h
+ public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides/timed_thread_notification
+ PUBLIC_DEPS
+ pw_sync.binary_semaphore
+ pw_sync.timed_thread_notification.facade
+)
+
+pw_zephyrize_libraries_ifdef(
+ CONFIG_PIGWEED_SYNC_TIMED_THREAD_NOTIFICATION
+ pw_sync_zephyr.timed_thread_notification_backend
+)
diff --git a/pw_sync_zephyr/Kconfig b/pw_sync_zephyr/Kconfig
index 15a9ec5ae..10a6e64d3 100644
--- a/pw_sync_zephyr/Kconfig
+++ b/pw_sync_zephyr/Kconfig
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_sync"
+
config PIGWEED_SYNC
bool
select PIGWEED_CHRONO_SYSTEM_CLOCK
@@ -19,10 +21,33 @@ config PIGWEED_SYNC
select PIGWEED_INTERRUPT_CONTEXT
config PIGWEED_SYNC_MUTEX
- bool "Enable Pigweed mutex library (pw_sync.mutex)"
+ bool "Link pw_sync.mutex library"
select PIGWEED_SYNC
select PIGWEED_POLYFILL
+ help
+ See :ref:`module-pw_sync` for module details.
config PIGWEED_SYNC_BINARY_SEMAPHORE
- bool "Enable Pigweed binary semaphore library (pw_sync.binary_semaphore)"
+ bool "Link pw_sync.binary_semaphore library"
select PIGWEED_SYNC
+ help
+ See :ref:`module-pw_sync` for module details.
+
+config PIGWEED_SYNC_INTERRUPT_SPIN_LOCK
+ bool "Link pw_sync.interrupt_spin_lock library"
+ help
+ See :ref:`module-pw_sync` for module details.
+
+config PIGWEED_SYNC_THREAD_NOTIFICATION
+ bool "Link pw_sync.thread_notification library"
+ select PIGWEED_SYNC_BINARY_SEMAPHORE
+ help
+ See :ref:`module-pw_sync` for module details.
+
+config PIGWEED_SYNC_TIMED_THREAD_NOTIFICATION
+ bool "Link pw_sync.timed_thread_notification library"
+ select PIGWEED_SYNC_BINARY_SEMAPHORE
+ help
+ See :ref:`module-pw_sync` for module details.
+
+endmenu
diff --git a/pw_sync_zephyr/binary_semaphore.cc b/pw_sync_zephyr/binary_semaphore.cc
index 082813769..9e3e33efc 100644
--- a/pw_sync_zephyr/binary_semaphore.cc
+++ b/pw_sync_zephyr/binary_semaphore.cc
@@ -14,7 +14,7 @@
#include "pw_sync/binary_semaphore.h"
-#include <kernel.h>
+#include <zephyr/kernel.h>
namespace pw::sync {
diff --git a/pw_sync_zephyr/docs.rst b/pw_sync_zephyr/docs.rst
index 705c144f7..9e96a431b 100644
--- a/pw_sync_zephyr/docs.rst
+++ b/pw_sync_zephyr/docs.rst
@@ -14,3 +14,4 @@ the Kconfig menu.
* ``pw_sync.mutex`` can be enabled via ``CONFIG_PIGWEED_SYNC_MUTEX``.
* ``pw_sync.binary_semaphore`` can be enabled via
``CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE``.
+* ``pw_sync.interrupt_spin_lock`` can be enabled via ``CONFIG_PIGWEED_SYNC_INTERRUPT_SPIN_LOCK``.
diff --git a/pw_sync_zephyr/interrupt_spin_lock.cc b/pw_sync_zephyr/interrupt_spin_lock.cc
new file mode 100644
index 000000000..0d4674433
--- /dev/null
+++ b/pw_sync_zephyr/interrupt_spin_lock.cc
@@ -0,0 +1,35 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_sync/interrupt_spin_lock.h"
+
+#include <zephyr/spinlock.h>
+
+#include "pw_assert/check.h"
+
+namespace pw::sync {
+
+void InterruptSpinLock::lock() {
+ PW_DCHECK(!native_type_.locked,
+ "Recursive InterruptSpinLock::lock() detected");
+ native_type_.key = k_spin_lock(&native_type_.lock);
+ native_type_.locked = true;
+}
+
+void InterruptSpinLock::unlock() {
+ native_type_.locked = false;
+ k_spin_unlock(&native_type_.lock, native_type_.key);
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_inline.h b/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_inline.h
index 9afe7ceb1..7bd05509a 100644
--- a/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_inline.h
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_inline.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
#include "pw_assert/assert.h"
#include "pw_chrono/system_clock.h"
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_native.h b/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_native.h
index 946a93433..fbe82a7c4 100644
--- a/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_native.h
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/binary_semaphore_native.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
namespace pw::sync::backend {
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_inline.h b/pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_inline.h
new file mode 100644
index 000000000..c98f6d9ad
--- /dev/null
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_inline.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_sync/interrupt_spin_lock.h"
+
+namespace pw::sync {
+
+constexpr InterruptSpinLock::InterruptSpinLock()
+ : native_type_{.lock = {}, .locked = false, .key = {0}} {}
+
+inline bool InterruptSpinLock::try_lock() {
+ // This backend will spin the current processor until the deadlock is
+ // available and does not implement a 'try' mechanism. Recursive locking is
+ // already detected by lock().
+ lock();
+ return true;
+}
+
+inline InterruptSpinLock::native_handle_type
+InterruptSpinLock::native_handle() {
+ return native_type_;
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_native.h b/pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_native.h
new file mode 100644
index 000000000..4cd71e037
--- /dev/null
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/interrupt_spin_lock_native.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <zephyr/spinlock.h>
+
+#include "pw_sync/interrupt_spin_lock.h"
+
+namespace pw::sync::backend {
+
+struct ZephyrSpinLock {
+ struct k_spinlock lock;
+ bool locked; // Used to detect recursion.
+ k_spinlock_key_t key;
+};
+
+using NativeInterruptSpinLock = struct ZephyrSpinLock;
+using NativeInterruptSpinLockHandle = NativeInterruptSpinLock&;
+
+} // namespace pw::sync::backend
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_inline.h b/pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_inline.h
new file mode 100644
index 000000000..f14028ce2
--- /dev/null
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_inline.h
@@ -0,0 +1,41 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <zephyr/sys/sem.h>
+
+#include "pw_assert/assert.h"
+#include "pw_interrupt/context.h"
+#include "pw_sync/thread_notification.h"
+
+namespace pw::sync {
+
+inline ThreadNotification::ThreadNotification() : native_type_() {}
+
+inline ThreadNotification::~ThreadNotification() = default;
+
+inline void ThreadNotification::release() { native_type_.release(); }
+
+inline void ThreadNotification::acquire() { native_type_.acquire(); }
+
+inline bool ThreadNotification::try_acquire() {
+ return native_type_.try_acquire();
+}
+
+inline ThreadNotification::native_handle_type
+ThreadNotification::native_handle() {
+ return native_type_;
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_native.h b/pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_native.h
new file mode 100644
index 000000000..5fb0495ef
--- /dev/null
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/thread_notification_native.h
@@ -0,0 +1,23 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <pw_sync/binary_semaphore.h>
+
+namespace pw::sync::backend {
+
+using NativeThreadNotification = pw::sync::BinarySemaphore;
+using NativeThreadNotificationHandle = NativeThreadNotification&;
+
+} // namespace pw::sync::backend
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/timed_thread_notification_inline.h b/pw_sync_zephyr/public/pw_sync_zephyr/timed_thread_notification_inline.h
new file mode 100644
index 000000000..c24388f92
--- /dev/null
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/timed_thread_notification_inline.h
@@ -0,0 +1,33 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <pw_sync/binary_semaphore.h>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_sync/timed_thread_notification.h"
+
+namespace pw::sync {
+
+inline bool TimedThreadNotification::try_acquire_for(
+ chrono::SystemClock::duration timeout) {
+ return native_handle().try_acquire_for(timeout);
+}
+
+inline bool TimedThreadNotification::try_acquire_until(
+ chrono::SystemClock::time_point deadline) {
+ return try_acquire_for(deadline - chrono::SystemClock::now());
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_inline.h b/pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_inline.h
new file mode 100644
index 000000000..af2ba03b3
--- /dev/null
+++ b/pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_sync_zephyr/interrupt_spin_lock_inline.h"
diff --git a/pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_native.h b/pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_native.h
new file mode 100644
index 000000000..02cc000fe
--- /dev/null
+++ b/pw_sync_zephyr/public_overrides/pw_sync_backend/interrupt_spin_lock_native.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_sync_zephyr/interrupt_spin_lock_native.h"
diff --git a/pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_inline.h b/pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_inline.h
new file mode 100644
index 000000000..f18bf3aaf
--- /dev/null
+++ b/pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_sync_zephyr/thread_notification_inline.h"
diff --git a/pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_native.h b/pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_native.h
new file mode 100644
index 000000000..482807ea4
--- /dev/null
+++ b/pw_sync_zephyr/public_overrides/thread_notification/pw_sync_backend/thread_notification_native.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_sync_zephyr/thread_notification_native.h"
diff --git a/pw_sync_zephyr/public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h b/pw_sync_zephyr/public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h
new file mode 100644
index 000000000..08062389d
--- /dev/null
+++ b/pw_sync_zephyr/public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_sync_zephyr/timed_thread_notification_inline.h"
diff --git a/pw_sys_io/BUILD.bazel b/pw_sys_io/BUILD.bazel
index fdba093be..4fa404f4b 100644
--- a/pw_sys_io/BUILD.bazel
+++ b/pw_sys_io/BUILD.bazel
@@ -14,6 +14,7 @@
load(
"//pw_build:pigweed.bzl",
+ "host_backend_alias",
"pw_cc_facade",
"pw_cc_library",
)
@@ -22,6 +23,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_setting(
+ name = "backend_constraint_setting",
+)
+
pw_cc_facade(
name = "facade",
hdrs = ["public/pw_sys_io/sys_io.h"],
@@ -44,18 +49,32 @@ pw_cc_library(
pw_cc_library(
name = "pw_sys_io",
+ hdrs = ["public/pw_sys_io/sys_io.h"],
+ includes = ["public"],
deps = [
- ":facade",
- "@pigweed_config//:pw_sys_io_backend",
+ "//pw_bytes",
+ "//pw_status",
+ "@pigweed//targets:pw_sys_io_backend",
],
)
-pw_cc_library(
+alias(
name = "backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
- deps = select({
- "//pw_build/constraints/chipset:stm32f429": ["@pigweed//pw_sys_io_baremetal_stm32f429"],
- "//pw_build/constraints/chipset:lm3s6965evb": ["@pigweed//pw_sys_io_baremetal_lm3s6965evb"],
- "//conditions:default": ["@pigweed//pw_sys_io_stdio"],
+ actual = select({
+ "//pw_sys_io_arduino:backend": "@pigweed//pw_sys_io_arduino",
+ "//pw_sys_io_baremetal_lm3s6965evb:backend": "@pigweed//pw_sys_io_baremetal_lm3s6965evb",
+ "//pw_sys_io_baremetal_stm32f429:backend": "@pigweed//pw_sys_io_baremetal_stm32f429",
+ "//pw_sys_io_emcraft_sf2:backend": "@pigweed//pw_sys_io_emcraft_sf2",
+ "//pw_sys_io_mcuxpresso:backend": "@pigweed//pw_sys_io_mcuxpresso",
+ "//pw_sys_io_rp2040:backend": "@pigweed//pw_sys_io_rp2040",
+ "//pw_sys_io_stm32cube:backend": "@pigweed//pw_sys_io_stm32cube",
+ "//pw_sys_io_stdio:backend": "@pigweed//pw_sys_io_stdio",
+ "//conditions:default": ":unspecified_backend",
}),
+ visibility = ["@pigweed//targets:__pkg__"],
+)
+
+host_backend_alias(
+ name = "unspecified_backend",
+ backend = "@pigweed//pw_sys_io_stdio",
)
diff --git a/pw_sys_io/docs.rst b/pw_sys_io/docs.rst
index eaf60ba92..4a1257352 100644
--- a/pw_sys_io/docs.rst
+++ b/pw_sys_io/docs.rst
@@ -30,17 +30,27 @@ Setup
=====
This module requires relatively minimal setup:
- 1. Choose a ``pw_sys_io`` backend, or write one yourself.
- 2. If using GN build, Specify the ``pw_sys_io_BACKEND`` GN build arg to point
- the library that provides a ``pw_sys_io`` backend.
+1. Choose a ``pw_sys_io`` backend, or write one yourself.
+2. If using GN build, Specify the ``pw_sys_io_BACKEND`` GN build arg to point
+ the library that provides a ``pw_sys_io`` backend.
Module usage
============
See backend docs for how to interact with the underlying system I/O
implementation.
+API reference
+=============
+.. doxygenfunction:: pw::sys_io::ReadByte(std::byte* dest)
+.. doxygenfunction:: pw::sys_io::TryReadByte(std::byte* dest)
+.. doxygenfunction:: pw::sys_io::WriteByte(std::byte b)
+.. doxygenfunction:: pw::sys_io::WriteLine(const std::string_view& s)
+.. doxygenfunction:: pw::sys_io::ReadBytes(ByteSpan dest)
+.. doxygenfunction:: pw::sys_io::WriteBytes(ConstByteSpan src)
+
Dependencies
============
- * pw_sys_io_backend
- * pw_span
- * pw_status
+- :ref:`module-pw_sys_io`
+- :ref:`module-pw_span`
+- :ref:`module-pw_status`
+
diff --git a/pw_sys_io/public/pw_sys_io/sys_io.h b/pw_sys_io/public/pw_sys_io/sys_io.h
index 2e9f663ff..6f1e78192 100644
--- a/pw_sys_io/public/pw_sys_io/sys_io.h
+++ b/pw_sys_io/public/pw_sys_io/sys_io.h
@@ -45,71 +45,84 @@
namespace pw::sys_io {
-// Read a single byte from the sys io backend.
-// Implemented by: Backend
-//
-// This function will block until it either succeeds or fails to read a byte
-// from the pw_sys_io backend.
-//
-// Returns OkStatus() - A byte was successfully read.
-// Status::ResourceExhausted() - if the underlying source vanished.
+/// Reads a single byte from the `pw_sys_io` backend.
+/// This function blocks until it either succeeds or fails to read a
+/// byte.
+///
+/// @pre This function must be implemented by the `pw_sys_io` backend.
+///
+/// @warning Do not build production projects on top of `pw_sys_io`.
+///
+/// @returns
+/// * @pw_status{OK} - A byte was successfully read and is in `dest`.
+/// * @pw_status{RESOURCE_EXHAUSTED} - The underlying source vanished.
Status ReadByte(std::byte* dest);
-// Read a single byte from the sys io backend, if available.
-// Implemented by: Backend
-//
-// Returns OkStatus() - A byte was successfully read, and is in dest.
-// Status::Unavailable() - No byte is available to read; try later.
-// Status::Unimplemented() - Not supported on this target.
+/// Reads a single byte from the `pw_sys_io` backend, if available.
+///
+/// @pre This function must be implemented by the `pw_sys_io` backend.
+///
+/// @warning Do not build production projects on top of `pw_sys_io`.
+///
+/// @returns
+/// * @pw_status{OK} - A byte was successfully read and is in `dest`.
+/// * @pw_status{UNAVAILABLE} - No byte is available to read; try later.
+/// * @pw_status{UNIMPLEMENTED} - The function is not supported on this target.
Status TryReadByte(std::byte* dest);
-// Write a single byte out the sys io backend.
-// Implemented by: Backend
-//
-// This function will block until it either succeeds or fails to write a byte
-// out the pw_sys_io backend.
-//
-// Returns OkStatus() if a byte was successfully read.
+/// Writes a single byte out the `pw_sys_io` backend. The function blocks until
+/// it either succeeds or fails to write the byte.
+///
+/// @pre This function must be implemented by the `pw_sys_io` backend.
+///
+/// @warning Do not build production projects on top of `pw_sys_io`.
+///
+/// @returns
+/// * @pw_status{OK} - A byte was successfully written.
Status WriteByte(std::byte b);
-// Write a string out the sys io backend.
-// Implemented by: Backend
-//
-// This function takes a null-terminated string and writes it out the sys io
-// backend, adding any platform-specific newline character(s) (these are
-// accounted for in the returned StatusWithSize).
-//
-// Return status is OkStatus() if all the bytes from the source string were
-// successfully written. In all cases, the number of bytes successfully written
-// are returned as part of the StatusWithSize.
+/// Writes a string out the `pw_sys_io` backend.
+///
+/// This function takes a null-terminated string and writes it out the
+/// `pw_sys_io` backend, adding any platform-specific newline character(s)
+/// (these are accounted for in the returned `StatusWithSize`).
+///
+/// @pre This function must be implemented by the `pw_sys_io` backend.
+///
+/// @warning Do not build production projects on top of `pw_sys_io`.
+///
+/// @returns
+/// * @pw_status{OK} if all the bytes from the source string were successfully
+/// written. In all cases, the number of bytes successfully written are
+/// returned as part of the `StatusWithSize`.
StatusWithSize WriteLine(const std::string_view& s);
-// Fill a byte span from the sys io backend using ReadByte().
-// Implemented by: Facade
-//
-// This function is implemented by this facade and simply uses ReadByte() to
-// read enough bytes to fill the destination span. If there's an error reading a
-// byte, the read is aborted and the contents of the destination span are
-// undefined. This function blocks until either an error occurs, or all bytes
-// are successfully read from the backend's ReadByte() implementation.
-//
-// Return status is OkStatus() if the destination span was successfully
-// filled. In all cases, the number of bytes successuflly read to the
-// destination span are returned as part of the StatusWithSize.
+/// Fills a byte span from the `pw_sys_io` backend using `ReadByte()`.
+///
+/// This function is implemented by the facade and simply uses `ReadByte()` to
+/// read enough bytes to fill the destination span. If there's an error reading
+/// a byte, the read is aborted and the contents of the destination span are
+/// undefined. This function blocks until either an error occurs or all bytes
+/// are successfully read from the backend's `ReadByte()` implementation.
+///
+/// @returns
+/// * @pw_status{OK} if the destination span was successfully filled. In all
+/// cases, the number of bytes successuflly read to the destination span are
+/// returned as part of the `StatusWithSize`.
StatusWithSize ReadBytes(ByteSpan dest);
-// Write span of bytes out the sys io backend using WriteByte().
-// Implemented by: Facade
-//
-// This function is implemented by this facade and simply writes the source
-// contents using WriteByte(). If an error writing a byte is encountered, the
-// write is aborted and the error status returned. This function blocks until
-// either an error occurs, or all bytes are successfully read from the backend's
-// WriteByte() implementation.
-//
-// Return status is OkStatus() if all the bytes from the source span were
-// successfully written. In all cases, the number of bytes successfully written
-// are returned as part of the StatusWithSize.
+/// Writes a span of bytes out the `pw_sys_io` backend using `WriteByte()`.
+///
+/// This function is implemented by the facade and simply writes the source
+/// contents using `WriteByte()`. If an error writing a byte is encountered, the
+/// write is aborted and the error status is returned. This function blocks
+/// until either an error occurs, or all bytes are successfully written from the
+/// backend's `WriteByte()` implementation.
+///
+/// @returns
+/// * @pw_status{OK} if all the bytes from the source span were successfully
+/// written. In all cases, the number of bytes successfully written are
+/// returned as part of the `StatusWithSize`.
StatusWithSize WriteBytes(ConstByteSpan src);
} // namespace pw::sys_io
diff --git a/pw_sys_io_ambiq_sdk/BUILD.bazel b/pw_sys_io_ambiq_sdk/BUILD.bazel
new file mode 100644
index 000000000..59fba7284
--- /dev/null
+++ b/pw_sys_io_ambiq_sdk/BUILD.bazel
@@ -0,0 +1,40 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
+pw_cc_library(
+ name = "pw_sys_ambiq_sdk",
+ srcs = ["sys_io.cc"],
+ hdrs = ["public/pw_sys_io_ambiq_sdk/init.h"],
+ tags = ["manual"],
+ deps = [
+ "//pw_boot_cortex_m:armv7m",
+ "//pw_preprocessor",
+ "//pw_sys_io:default_putget_bytes",
+ "//pw_sys_io:facade",
+ ],
+)
diff --git a/pw_sys_io_ambiq_sdk/BUILD.gn b/pw_sys_io_ambiq_sdk/BUILD.gn
new file mode 100644
index 000000000..1f286a76a
--- /dev/null
+++ b/pw_sys_io_ambiq_sdk/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2020 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/ambiq/ambiq.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("default_config") {
+ include_dirs = [ "public" ]
+}
+
+pw_source_set("pw_sys_io_ambiq_sdk") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_sys_io_ambiq_sdk/init.h" ]
+ public_deps = [ "$dir_pw_preprocessor" ]
+
+ deps = [
+ "$dir_pw_sys_io:default_putget_bytes",
+ "$dir_pw_sys_io:facade",
+ "$dir_pw_third_party/ambiq:sdk",
+ ]
+ sources = [ "sys_io.cc" ]
+}
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_sys_io_ambiq_sdk/docs.rst b/pw_sys_io_ambiq_sdk/docs.rst
new file mode 100644
index 000000000..fef2be43d
--- /dev/null
+++ b/pw_sys_io_ambiq_sdk/docs.rst
@@ -0,0 +1,23 @@
+.. _module-pw_sys_io_ambiq_sdk:
+
+===================
+pw_sys_io_ambiq_sdk
+===================
+``pw_sys_io_ambiq_sdk`` implements the ``pw_sys_io`` facade over UART using the
+Ambiq Suite SDK HAL.
+
+The UART baud rate is fixed at 115200 (8N1).
+
+Setup
+=====
+This module requires relatively minimal setup:
+
+1. Write code against the ``pw_sys_io`` facade.
+2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
+ backend.
+3. Call ``pw_sys_io_Init()`` during init so the UART is properly initialized and
+ configured.
+
+The UART peripheral and the GPIO pins are defined in the ``am_bsp.h`` file. Make sure
+that the build argument ``pw_third_party_ambiq_PRODUCT`` is set correctly so that
+the correct bsp header file is included.
diff --git a/pw_sys_io_ambiq_sdk/public/pw_sys_io_ambiq_sdk/init.h b/pw_sys_io_ambiq_sdk/public/pw_sys_io_ambiq_sdk/init.h
new file mode 100644
index 000000000..d4262c56d
--- /dev/null
+++ b/pw_sys_io_ambiq_sdk/public/pw_sys_io_ambiq_sdk/init.h
@@ -0,0 +1,23 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_preprocessor/util.h"
+
+PW_EXTERN_C_START
+
+// The actual implement of PreMainInit() in sys_io_BACKEND.
+void pw_sys_io_Init();
+
+PW_EXTERN_C_END
diff --git a/pw_sys_io_ambiq_sdk/sys_io.cc b/pw_sys_io_ambiq_sdk/sys_io.cc
new file mode 100644
index 000000000..5e19f1fc3
--- /dev/null
+++ b/pw_sys_io_ambiq_sdk/sys_io.cc
@@ -0,0 +1,138 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_sys_io/sys_io.h"
+
+#include <cinttypes>
+
+#include "am_bsp.h"
+#include "am_mcu_apollo.h"
+#include "pw_preprocessor/compiler.h"
+
+namespace {
+void* hal_uart_handle{};
+} // namespace
+
+PW_EXTERN_C void pw_sys_io_Init() {
+ // Use baud rate of 115200 (8N1).
+ static constexpr am_hal_uart_config_t kUartConfig = {
+ .ui32BaudRate = 115200,
+ .eDataBits = AM_HAL_UART_DATA_BITS_8,
+ .eParity = AM_HAL_UART_PARITY_NONE,
+ .eStopBits = AM_HAL_UART_ONE_STOP_BIT,
+ .eFlowControl = AM_HAL_UART_FLOW_CTRL_NONE,
+ .eTXFifoLevel = AM_HAL_UART_FIFO_LEVEL_16,
+ .eRXFifoLevel = AM_HAL_UART_FIFO_LEVEL_16,
+ };
+
+ // Initialize the UART peripheral.
+ am_hal_uart_initialize(AM_BSP_UART_PRINT_INST, &hal_uart_handle);
+
+ // Change the power state of the UART peripheral.
+ am_hal_uart_power_control(hal_uart_handle, AM_HAL_SYSCTRL_WAKE, false);
+
+ // Configure UART (baudrate etc.).
+ am_hal_uart_configure(hal_uart_handle, &kUartConfig);
+
+ // Enable the UART TX and RX GPIO's.
+ am_hal_gpio_pinconfig(AM_BSP_GPIO_COM_UART_TX, g_AM_BSP_GPIO_COM_UART_TX);
+ am_hal_gpio_pinconfig(AM_BSP_GPIO_COM_UART_RX, g_AM_BSP_GPIO_COM_UART_RX);
+}
+
+namespace pw::sys_io {
+
+// Wait for a byte to read on UART0. This blocks until a byte is read. This is
+// extremely inefficient as it requires the target to burn CPU cycles polling to
+// see if a byte is ready yet.
+Status ReadByte(std::byte* dest) {
+ while (true) {
+ if (TryReadByte(dest).ok()) {
+ return OkStatus();
+ }
+ }
+}
+
+Status TryReadByte(std::byte* dest) {
+ am_hal_uart_transfer_t transaction{};
+ uint32_t bytes_read{};
+
+ // Configure UART transaction for the read operation.
+ transaction.eType = AM_HAL_UART_BLOCKING_READ;
+ transaction.pui8Data = reinterpret_cast<uint8_t*>(dest);
+ transaction.ui32NumBytes = 1;
+ transaction.ui32TimeoutMs = AM_HAL_UART_WAIT_FOREVER;
+ transaction.pui32BytesTransferred = &bytes_read;
+
+ // Do read data over UART.
+ if (am_hal_uart_transfer(hal_uart_handle, &transaction) !=
+ AM_HAL_STATUS_SUCCESS) {
+ return Status::ResourceExhausted();
+ }
+
+ if (bytes_read != 1u) {
+ return Status::DataLoss();
+ }
+
+ return OkStatus();
+}
+
+Status WriteByte(std::byte b) {
+ am_hal_uart_transfer_t transaction{};
+ uint32_t chars_written{};
+
+ // Configure UART transaction for the write operation.
+ transaction.eType = AM_HAL_UART_BLOCKING_WRITE;
+ transaction.pui8Data =
+ const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(&b));
+ transaction.ui32NumBytes = 1;
+ transaction.ui32TimeoutMs = AM_HAL_UART_WAIT_FOREVER;
+ transaction.pui32BytesTransferred = &chars_written;
+
+ // Do write data over UART.
+ if (am_hal_uart_transfer(hal_uart_handle, &transaction) !=
+ AM_HAL_STATUS_SUCCESS) {
+ return Status::ResourceExhausted();
+ }
+
+ if (chars_written != 1) {
+ return Status::DataLoss();
+ }
+
+ return OkStatus();
+}
+
+// Writes a string using pw::sys_io, and add newline characters at the end.
+StatusWithSize WriteLine(const std::string_view& s) {
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
+ if (!result.ok()) {
+ return result;
+ }
+
+ size_t chars_written = result.size();
+ if (chars_written != s.size()) {
+ return StatusWithSize::DataLoss(chars_written);
+ }
+
+ // Write trailing newline.
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
+ chars_written += result.size();
+
+ if (result.size() != 2) {
+ return StatusWithSize::DataLoss(chars_written);
+ }
+
+ return StatusWithSize(chars_written);
+}
+
+} // namespace pw::sys_io
diff --git a/pw_sys_io_arduino/BUILD.bazel b/pw_sys_io_arduino/BUILD.bazel
index dafbb9b1b..4b88a368e 100644
--- a/pw_sys_io_arduino/BUILD.bazel
+++ b/pw_sys_io_arduino/BUILD.bazel
@@ -21,15 +21,21 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_arduino",
srcs = ["sys_io_arduino.cc"],
hdrs = ["public/pw_sys_io_arduino/init.h"],
- # TODO(b/259149817): Get this to build
+ # TODO: b/259149817 - Get this to build
tags = ["manual"],
deps = [
"//pw_preprocessor",
- "//pw_sys_io",
+ "//pw_sys_io:default_putget_bytes",
+ "//pw_sys_io:facade",
],
)
diff --git a/pw_sys_io_arduino/docs.rst b/pw_sys_io_arduino/docs.rst
index 79c533dd4..0cc321b84 100644
--- a/pw_sys_io_arduino/docs.rst
+++ b/pw_sys_io_arduino/docs.rst
@@ -12,11 +12,10 @@ rate:
.. code-block:: cpp
- Serial.begin(115200);
+ Serial.begin(115200);
- // Wait for serial port to be available
- while (!Serial) {
- }
+ // Wait for serial port to be available
+ while (!Serial) {}
After ``Serial.begin(115200)`` it will busy wait until a host connects to the
serial port.
diff --git a/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel b/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel
index b3ea1c22d..e2e9984f3 100644
--- a/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel
+++ b/pw_sys_io_baremetal_lm3s6965evb/BUILD.bazel
@@ -21,6 +21,11 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_baremetal_lm3s6965evb",
srcs = ["sys_io_baremetal.cc"],
@@ -32,6 +37,7 @@ pw_cc_library(
deps = [
"//pw_boot_cortex_m:armv7m",
"//pw_preprocessor",
+ "//pw_sys_io:default_putget_bytes",
"//pw_sys_io:facade",
],
)
diff --git a/pw_sys_io_baremetal_lm3s6965evb/docs.rst b/pw_sys_io_baremetal_lm3s6965evb/docs.rst
index a86537373..fc993959b 100644
--- a/pw_sys_io_baremetal_lm3s6965evb/docs.rst
+++ b/pw_sys_io_baremetal_lm3s6965evb/docs.rst
@@ -6,4 +6,4 @@ pw_sys_io_baremetal_lm3s6965evb
.. warning::
- This documentation is under construction.
+ This documentation is under construction.
diff --git a/pw_sys_io_baremetal_stm32f429/BUILD.bazel b/pw_sys_io_baremetal_stm32f429/BUILD.bazel
index 248d987de..8147ce1a5 100644
--- a/pw_sys_io_baremetal_stm32f429/BUILD.bazel
+++ b/pw_sys_io_baremetal_stm32f429/BUILD.bazel
@@ -21,13 +21,17 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_baremetal_stm32f429",
srcs = ["sys_io_baremetal.cc"],
hdrs = ["public/pw_sys_io_baremetal_stm32f429/init.h"],
includes = ["public"],
target_compatible_with = [
- "//pw_build/constraints/chipset:stm32f429",
"@platforms//os:none",
],
deps = [
diff --git a/pw_sys_io_baremetal_stm32f429/docs.rst b/pw_sys_io_baremetal_stm32f429/docs.rst
index 1f1fe08df..41f2633ee 100644
--- a/pw_sys_io_baremetal_stm32f429/docs.rst
+++ b/pw_sys_io_baremetal_stm32f429/docs.rst
@@ -3,7 +3,6 @@
-----------------------------
pw_sys_io_baremetal_stm32f429
-----------------------------
-
``pw_sys_io_baremetal_stm32f429`` implements the ``pw_sys_io`` facade over
UART.
@@ -21,16 +20,16 @@ Setup
=====
This module requires relatively minimal setup:
- 1. Write code against the ``pw_sys_io`` facade.
- 2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
- backend.
- 3. Build an executable with a main() function using a toolchain that
- supports Cortex-M4.
+1. Write code against the ``pw_sys_io`` facade.
+2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
+ backend.
+3. Build an executable with a main() function using a toolchain that
+ supports Cortex-M4.
.. note::
- This module provides early firmware init and a linker script, so it will
- conflict with other modules that do any early device init or provide a linker
- script.
+ This module provides early firmware init and a linker script, so it will
+ conflict with other modules that do any early device init or provide a linker
+ script.
Module usage
============
@@ -47,15 +46,15 @@ Sample connection diagram
.. code-block:: text
- --USB Serial--+ +-----STM32F429 MCU-----
- | |
- TX o--->o PA10/USART1_RX
- | |
- RX o<---o PA9/USART1_TX
- | |
- --------------+ +-----------------------
+ --USB Serial--+ +-----STM32F429 MCU-----
+ | |
+ TX o--->o PA10/USART1_RX
+ | |
+ RX o<---o PA9/USART1_TX
+ | |
+ --------------+ +-----------------------
Dependencies
============
- * ``pw_sys_io`` facade
- * ``pw_preprocessor`` module
+- :ref:`module-pw_sys_io`
+- :ref:`module-pw_preprocessor`
diff --git a/pw_sys_io_emcraft_sf2/BUILD.bazel b/pw_sys_io_emcraft_sf2/BUILD.bazel
index fb10a78c4..026fa6eee 100644
--- a/pw_sys_io_emcraft_sf2/BUILD.bazel
+++ b/pw_sys_io_emcraft_sf2/BUILD.bazel
@@ -21,6 +21,11 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_emcraft_sf2",
srcs = [
@@ -34,6 +39,7 @@ pw_cc_library(
deps = [
"//pw_boot_cortex_m:armv7m",
"//pw_preprocessor",
- "//pw_sys_io",
+ "//pw_sys_io:default_putget_bytes",
+ "//pw_sys_io:facade",
],
)
diff --git a/pw_sys_io_emcraft_sf2/docs.rst b/pw_sys_io_emcraft_sf2/docs.rst
index 66ef6c940..948588d46 100644
--- a/pw_sys_io_emcraft_sf2/docs.rst
+++ b/pw_sys_io_emcraft_sf2/docs.rst
@@ -19,17 +19,17 @@ Setup
=====
This module requires relatively minimal setup:
- 1. Write code against the ``pw_sys_io`` facade.
- 2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
- backend.
- 3. pw_sys_io_Init() provided by this module needs to be called in early boot
- to get pw_sys_io into a working state.
- 4. Build an executable with a main() function using a toolchain that
- supports Cortex-M3.
+1. Write code against the ``pw_sys_io`` facade.
+2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
+ backend.
+3. pw_sys_io_Init() provided by this module needs to be called in early boot
+ to get pw_sys_io into a working state.
+4. Build an executable with a main() function using a toolchain that
+ supports Cortex-M3.
.. note::
- This module provides early firmware init, so it will conflict with other
- modules that do any early device init.
+ This module provides early firmware init, so it will conflict with other
+ modules that do any early device init.
Module usage
============
@@ -40,5 +40,5 @@ connect to the device at a baud rate of 57600 (8N1).
Dependencies
============
- * ``pw_sys_io`` facade
- * ``pw_preprocessor`` module
+- :ref:`module-pw_sys_io`
+- :ref:`module-pw_preprocessor`
diff --git a/pw_sys_io_mcuxpresso/BUILD.bazel b/pw_sys_io_mcuxpresso/BUILD.bazel
index 0e1a323bc..51cb0d829 100644
--- a/pw_sys_io_mcuxpresso/BUILD.bazel
+++ b/pw_sys_io_mcuxpresso/BUILD.bazel
@@ -21,14 +21,23 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_mcuxpresso",
srcs = ["sys_io.cc"],
hdrs = ["public/pw_sys_io_mcuxpresso/init.h"],
- # TODO(b/259150983): Get this to build, requires SDK
- tags = ["manual"],
+ includes = ["public"],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ ],
deps = [
"//pw_preprocessor",
- "//pw_sys_io",
+ "//pw_sys_io:default_putget_bytes",
+ "//pw_sys_io:facade",
+ "@pigweed//targets:mcuxpresso_sdk",
],
)
diff --git a/pw_sys_io_mcuxpresso/docs.rst b/pw_sys_io_mcuxpresso/docs.rst
index a2376112a..fc30fc1f3 100644
--- a/pw_sys_io_mcuxpresso/docs.rst
+++ b/pw_sys_io_mcuxpresso/docs.rst
@@ -12,13 +12,13 @@ Setup
=====
This module requires a little setup:
- 1. Use ``pw_build_mcuxpresso`` to create a ``pw_source_set`` for an
- MCUXpresso SDK.
- 2. Include the debug console component in this SDK definition.
- 3. Specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
- the name of this source set.
- 4. Use a target that calls ``pw_sys_io_mcuxpresso_Init`` in
- ``pw_boot_PreMainInit`` or similar.
+1. Use ``pw_build_mcuxpresso`` to create a ``pw_source_set`` for an
+ MCUXpresso SDK.
+2. Include the debug console component in this SDK definition.
+3. Specify the ``pw_third_party_mcuxpresso_SDK`` GN global variable to specify
+ the name of this source set.
+4. Use a target that calls ``pw_sys_io_mcuxpresso_Init`` in
+ ``pw_boot_PreMainInit`` or similar.
The name of the SDK source set must be set in the
"pw_third_party_mcuxpresso_SDK" GN arg
@@ -31,7 +31,7 @@ of the MCUXpresso source set, see the
.. c:macro:: DEBUG_CONSOLE_TRANSFER_NON_BLOCKING
- Whether the MCUXpresso debug console supports non-blocking transfers. The
- default will depend on your SDK configuration.
+ Whether the MCUXpresso debug console supports non-blocking transfers. The
+ default will depend on your SDK configuration.
- Enabling this adds support for ``pw::sys_io::TryReadByte``.
+ Enabling this adds support for ``pw::sys_io::TryReadByte``.
diff --git a/pw_sys_io_pico/docs.rst b/pw_sys_io_pico/docs.rst
deleted file mode 100644
index 3bcdfc2fc..000000000
--- a/pw_sys_io_pico/docs.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-.. _module-pw_sys_io_pico:
-
---------------
-pw_sys_io_pico
---------------
-
-``pw_sys_io_pico`` implements the ``pw_sys_io`` facade using the Pico's STDIO
-library.
diff --git a/pw_sys_io_pico/BUILD.bazel b/pw_sys_io_rp2040/BUILD.bazel
index 91d4242aa..9017aa7fd 100644
--- a/pw_sys_io_pico/BUILD.bazel
+++ b/pw_sys_io_rp2040/BUILD.bazel
@@ -21,15 +21,21 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
- name = "pw_sys_io_pico",
+ name = "pw_sys_io_rp2040",
srcs = [
"sys_io.cc",
],
- # TODO(b/261603269): Get this to build.
+ # TODO: b/261603269 - Get this to build.
tags = ["manual"],
deps = [
"//pw_status",
- "//pw_sys_io",
+ "//pw_sys_io:default_putget_bytes",
+ "//pw_sys_io:facade",
],
)
diff --git a/pw_sys_io_pico/BUILD.gn b/pw_sys_io_rp2040/BUILD.gn
index 943392af3..f92eb4ac1 100644
--- a/pw_sys_io_pico/BUILD.gn
+++ b/pw_sys_io_rp2040/BUILD.gn
@@ -19,7 +19,7 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
-pw_source_set("pw_sys_io_pico") {
+pw_source_set("pw_sys_io_rp2040") {
sources = [ "sys_io.cc" ]
deps = [
"$PICO_ROOT/src/common/pico_base",
diff --git a/pw_sys_io_pico/OWNERS b/pw_sys_io_rp2040/OWNERS
index 307b1deb5..307b1deb5 100644
--- a/pw_sys_io_pico/OWNERS
+++ b/pw_sys_io_rp2040/OWNERS
diff --git a/pw_sys_io_rp2040/docs.rst b/pw_sys_io_rp2040/docs.rst
new file mode 100644
index 000000000..c135584ab
--- /dev/null
+++ b/pw_sys_io_rp2040/docs.rst
@@ -0,0 +1,8 @@
+.. _module-pw_sys_io_rp2040:
+
+----------------
+pw_sys_io_rp2040
+----------------
+
+``pw_sys_io_rp2040`` implements the ``pw_sys_io`` facade using the Pico's STDIO
+library.
diff --git a/pw_sys_io_pico/sys_io.cc b/pw_sys_io_rp2040/sys_io.cc
index da0ecf652..da0ecf652 100644
--- a/pw_sys_io_pico/sys_io.cc
+++ b/pw_sys_io_rp2040/sys_io.cc
diff --git a/pw_sys_io_stdio/BUILD.bazel b/pw_sys_io_stdio/BUILD.bazel
index 23d510387..381ce9810 100644
--- a/pw_sys_io_stdio/BUILD.bazel
+++ b/pw_sys_io_stdio/BUILD.bazel
@@ -25,6 +25,11 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_stdio",
srcs = ["sys_io.cc"],
diff --git a/pw_sys_io_stdio/docs.rst b/pw_sys_io_stdio/docs.rst
index 728065624..eaa45af41 100644
--- a/pw_sys_io_stdio/docs.rst
+++ b/pw_sys_io_stdio/docs.rst
@@ -8,7 +8,6 @@ stdio.
Why not just use stdio directly?
--------------------------------
-
The nice thing about using ``pw_sys_io`` is that it's rather easy to get a
board up and running with a target-specific backend. This means when drafting
out a quick application you can write it against ``pw_sys_io`` and, with some
@@ -21,8 +20,8 @@ Setup
=====
This module requires relatively minimal setup:
- 1. Write code against the ``pw_sys_io`` facade.
- 2. Direct the ``pw_sys_io_BACKEND`` GN build arg to point to this backend.
+1. Write code against the ``pw_sys_io`` facade.
+2. Direct the ``pw_sys_io_BACKEND`` GN build arg to point to this backend.
Module usage
============
@@ -31,4 +30,4 @@ to an application built directly against stdio.
Dependencies
============
- * ``pw_sys_io`` facade
+- :ref:`module-pw_sys_io`
diff --git a/pw_sys_io_stm32cube/BUILD.bazel b/pw_sys_io_stm32cube/BUILD.bazel
index 39c863ddd..4e97350fc 100644
--- a/pw_sys_io_stm32cube/BUILD.bazel
+++ b/pw_sys_io_stm32cube/BUILD.bazel
@@ -21,6 +21,11 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_value(
+ name = "backend",
+ constraint_setting = "//pw_sys_io:backend_constraint_setting",
+)
+
pw_cc_library(
name = "pw_sys_io_stm32cube",
srcs = [
@@ -28,11 +33,13 @@ pw_cc_library(
"sys_io.cc",
],
hdrs = ["public/pw_sys_io_stm32cube/init.h"],
- # TODO(b/259151566): Get this to build.
- tags = ["manual"],
+ copts = ["-Wno-unused-parameter"],
+ includes = ["public"],
+ target_compatible_with = [":backend"],
deps = [
"//pw_preprocessor",
"//pw_status",
+ "//pw_sys_io:default_putget_bytes",
"//pw_sys_io:facade",
"//third_party/stm32cube",
],
diff --git a/pw_sys_io_stm32cube/docs.rst b/pw_sys_io_stm32cube/docs.rst
index e4155d6a0..a470fbb69 100644
--- a/pw_sys_io_stm32cube/docs.rst
+++ b/pw_sys_io_stm32cube/docs.rst
@@ -13,11 +13,11 @@ Setup
=====
This module requires relatively minimal setup:
- 1. Write code against the ``pw_sys_io`` facade.
- 2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
- backend.
- 3. Call ``pw_sys_io_Init()`` during init so the UART is properly initialized
- and configured.
+1. Write code against the ``pw_sys_io`` facade.
+2. Specify the ``dir_pw_sys_io_backend`` GN global variable to point to this
+ backend.
+3. Call ``pw_sys_io_Init()`` during init so the UART is properly initialized
+ and configured.
For devices other than the STM32F429I-DISC1, this module will need to be
configured to use the appropriate GPIO pins and USART peripheral.
@@ -83,10 +83,10 @@ Sample connection diagram
.. code-block:: text
- --USB Serial--+ +-----STM32F429 MCU-----
- | |
- TX o--->o PA10/USART1_RX
- | |
- RX o<---o PA9/USART1_TX
- | |
- --------------+ +-----------------------
+ --USB Serial--+ +-----STM32F429 MCU-----
+ | |
+ TX o--->o PA10/USART1_RX
+ | |
+ RX o<---o PA9/USART1_TX
+ | |
+ --------------+ +-----------------------
diff --git a/pw_sys_io_zephyr/CMakeLists.txt b/pw_sys_io_zephyr/CMakeLists.txt
index 3a103168d..12e14d1cc 100644
--- a/pw_sys_io_zephyr/CMakeLists.txt
+++ b/pw_sys_io_zephyr/CMakeLists.txt
@@ -21,9 +21,13 @@ endif()
pw_add_library(pw_sys_io_zephyr STATIC
SOURCES
sys_io.cc
- PRIVATE_DEPS
- pw_sys_io.default_putget_bytes
+ PUBLIC_DEPS
pw_sys_io.facade
+ pw_sys_io.default_putget_bytes
zephyr_interface
)
-zephyr_link_libraries(pw_sys_io_zephyr)
+
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_SYS_IO
+ pw_sys_io.default_putget_bytes
+ pw_sys_io_zephyr
+)
diff --git a/pw_sys_io_zephyr/Kconfig b/pw_sys_io_zephyr/Kconfig
index 6fd75b763..5160b748f 100644
--- a/pw_sys_io_zephyr/Kconfig
+++ b/pw_sys_io_zephyr/Kconfig
@@ -12,13 +12,15 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_sys_io"
+
config PIGWEED_SYS_IO
bool "Enable the Zephyr system IO module"
select PIGWEED_SPAN
select PIGWEED_STATUS
help
The system I/O module uses the Zephyr console under the hood to perform
- I/O operations.
+ I/O operations. See :ref:`module-pw_sys_io` for module details.
if PIGWEED_SYS_IO
@@ -31,5 +33,9 @@ config PIGWEED_SYS_IO_INIT_PRIORITY
config PIGWEED_SYS_IO_USB
bool "Use the USB for I/O"
+ help
+ On init of the system I/O, also enable the Zephyr USB subsystem.
endif # PIGWEED_SYS_IO
+
+endmenu
diff --git a/pw_sys_io_zephyr/sys_io.cc b/pw_sys_io_zephyr/sys_io.cc
index 6891341ac..d51328c3b 100644
--- a/pw_sys_io_zephyr/sys_io.cc
+++ b/pw_sys_io_zephyr/sys_io.cc
@@ -19,9 +19,8 @@
#include <zephyr/kernel.h>
#include <zephyr/usb/usb_device.h>
-static int sys_io_init(const struct device* dev) {
+static int sys_io_init(void) {
int err;
- ARG_UNUSED(dev);
if (IS_ENABLED(CONFIG_PIGWEED_SYS_IO_USB)) {
err = usb_enable(nullptr);
diff --git a/pw_system/BUILD.bazel b/pw_system/BUILD.bazel
index d9fb2e97c..5bdd83dbc 100644
--- a/pw_system/BUILD.bazel
+++ b/pw_system/BUILD.bazel
@@ -22,11 +22,25 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+constraint_setting(
+ name = "system_example_tracing_setting",
+)
+
+constraint_value(
+ name = "system_example_tracing",
+ constraint_setting = ":system_example_tracing_setting",
+)
+
pw_cc_library(
name = "config",
hdrs = [
"public/pw_system/config.h",
],
+ defines = select({
+ "//conditions:default": [
+ "PW_SYSTEM_ENABLE_TRACE_SERVICE=0",
+ ],
+ }),
)
pw_cc_library(
@@ -72,43 +86,33 @@ pw_cc_library(
"//pw_sync:lock_annotations",
"//pw_tokenizer",
],
-)
-
-pw_cc_library(
- name = "rpc_server_headers",
- hdrs = [
- "public/pw_system/rpc_server.h",
- ],
- includes = ["public"],
- deps = [
- ":config",
- ],
+ # Log backends, like assert backends, generally need to be alwayslink'ed
+ # because we don't inform Bazel correctly about dependencies on them. We
+ # only add them as deps of binary targets, not intermediate library targets,
+ # to avoid circular dependencies. But this may lead the linker to eagerly
+ # remove some symbols defined here as unused.
+ alwayslink = 1,
)
pw_cc_library(
name = "rpc_server",
- deps = [
- ":config",
- ":hdlc_rpc_server",
- ":rpc_server_headers",
- ],
-)
-
-pw_cc_library(
- name = "hdlc_rpc_server",
srcs = [
"hdlc_rpc_server.cc",
],
+ hdrs = [
+ "public/pw_system/rpc_server.h",
+ ],
includes = ["public"],
deps = [
+ ":config",
":io",
- ":rpc_server_headers",
":target_io",
"//pw_assert",
"//pw_hdlc:pw_rpc",
"//pw_hdlc:rpc_channel_output",
"//pw_sync:mutex",
"//pw_thread:thread_core",
+ "//pw_trace",
],
)
@@ -147,16 +151,26 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ ":file_manager",
":log",
":rpc_server",
":target_hooks",
":thread_snapshot_service",
+ ":transfer_service",
":work_queue",
"//pw_metric:global",
"//pw_metric:metric_service_pwpb",
"//pw_rpc/pwpb:echo_service",
"//pw_thread:thread",
- ],
+ ] + select({
+ ":system_example_tracing": [
+ ":file_service",
+ ":trace_service",
+ "//pw_trace",
+ ],
+ "//conditions:default": [
+ ],
+ }),
)
pw_cc_library(
@@ -203,13 +217,86 @@ pw_cc_library(
)
pw_cc_library(
- name = "target_hooks_headers",
+ name = "transfer_handlers",
+ srcs = [
+ "transfer_handlers.cc",
+ ],
hdrs = [
- "public/pw_system/target_hooks.h",
+ "public/pw_system/transfer_handlers.h",
],
includes = ["public"],
deps = [
- "//pw_thread:thread",
+ "//pw_persistent_ram",
+ "//pw_trace_tokenized:config",
+ "//pw_transfer",
+ ],
+)
+
+pw_cc_library(
+ name = "file_manager",
+ srcs = [
+ "file_manager.cc",
+ ],
+ hdrs = [
+ "public/pw_system/file_manager.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":config",
+ ":transfer_handlers",
+ "//pw_file:flat_file_system",
+ "//pw_persistent_ram:flat_file_system_entry",
+ ] + select({
+ ":system_example_tracing": [
+ ":trace_service",
+ ],
+ "//conditions:default": [
+ ],
+ }),
+)
+
+pw_cc_library(
+ name = "transfer_service",
+ srcs = [
+ "transfer_service.cc",
+ ],
+ hdrs = [
+ "public/pw_system/transfer_service.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":file_manager",
+ "//pw_transfer",
+ ],
+)
+
+pw_cc_library(
+ name = "file_service",
+ srcs = [
+ "file_service.cc",
+ ],
+ hdrs = [
+ "public/pw_system/file_service.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":file_manager",
+ ],
+)
+
+pw_cc_library(
+ name = "trace_service",
+ srcs = [
+ "trace_service.cc",
+ ],
+ hdrs = [
+ "public/pw_system/trace_service.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":transfer_handlers",
+ "//pw_persistent_ram",
+ "//pw_trace_tokenized:trace_service_pwpb",
],
)
@@ -221,7 +308,7 @@ pw_cc_library(
includes = ["public"],
deps = [
"//pw_thread:thread",
- "@pigweed_config//:pw_system_target_hooks_backend",
+ "@pigweed//targets:pw_system_target_hooks_backend",
],
)
@@ -230,7 +317,7 @@ pw_cc_library(
# projects. For now, assume the pre-baked OS-specific hooks are good enough.
pw_cc_library(
name = "target_hooks_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:freertos": [":freertos_target_hooks"],
"//conditions:default": [":stl_target_hooks"],
@@ -242,7 +329,9 @@ pw_cc_library(
srcs = [
"stl_target_hooks.cc",
],
+ includes = ["public"],
deps = [
+ ":config",
"//pw_thread:thread",
"//pw_thread_stl:thread",
],
@@ -253,11 +342,15 @@ pw_cc_library(
srcs = [
"freertos_target_hooks.cc",
],
+ hdrs = [
+ "public/pw_system/target_hooks.h",
+ ],
+ includes = ["public"],
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
deps = [
- ":target_hooks_headers",
+ ":config",
"//pw_thread:thread",
"//pw_thread_freertos:thread",
],
@@ -274,7 +367,11 @@ pw_cc_binary(
"//pw_stream:sys_io_stream",
"//pw_unit_test:rpc_service",
] + select({
- "//pw_build/constraints/rtos:freertos": [],
+ "//pw_build/constraints/rtos:freertos": [
+ "//pw_tokenizer:linker_script",
+ "//targets/stm32f429i_disc1_stm32cube:linker_script",
+ "//targets/stm32f429i_disc1_stm32cube:pre_init",
+ ],
"//conditions:default": ["//targets/host_device_simulator:boot"],
}),
)
diff --git a/pw_system/BUILD.gn b/pw_system/BUILD.gn
index a0e84d52d..993b748ce 100644
--- a/pw_system/BUILD.gn
+++ b/pw_system/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pigweed/third_party/ambiq/ambiq.gni")
import("$dir_pigweed/third_party/freertos/freertos.gni")
import("$dir_pigweed/third_party/nanopb/nanopb.gni")
import("$dir_pigweed/third_party/pico_sdk/pi_pico.gni")
@@ -38,6 +39,33 @@ config("public_include_path") {
include_dirs = [ "public" ]
}
+# This config moves RPC logging to a separate RPC channel and HDLC
+# address. This does two things:
+# * The separate RPC channel allows logging traffic to be treated as
+# if it is being sent to a different client via a separate RPC
+# channel. This illustrates the ability for an RPC server to
+# communicate to multiple clients over multiple physical links.
+# * The separate HDLC address completely isolates typical RPC traffic
+# from logging traffic by communicating to a different HDLC endpoint
+# address. This effectively creates two virtual data pipes over the
+# same physical link.
+#
+# This is mostly to illustrate pw_rpc's capability to route and multiplex
+# traffic.
+config("multi_endpoint_rpc_overrides") {
+ defines = [
+ "PW_SYSTEM_LOGGING_CHANNEL_ID=10000",
+ "PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS=10000",
+ ]
+}
+
+# The Pigweed config pattern requires a pw_source_set to provide the
+# configuration defines. This provides the flags in
+# multi_endpoint_rpc_overrides.
+pw_source_set("multi_endpoint_rpc_config") {
+ public_configs = [ ":multi_endpoint_rpc_overrides" ]
+}
+
pw_source_set("config") {
sources = [ "public/pw_system/config.h" ]
public_configs = [ ":public_include_path" ]
@@ -114,15 +142,20 @@ pw_source_set("init") {
public = [ "public/pw_system/init.h" ]
sources = [ "init.cc" ]
deps = [
+ ":file_manager",
+ ":file_service",
":log",
":rpc_server",
":target_hooks.facade",
":thread_snapshot_service",
+ ":trace_service",
+ ":transfer_service",
":work_queue",
"$dir_pw_metric:global",
"$dir_pw_metric:metric_service_pwpb",
"$dir_pw_rpc/pwpb:echo_service",
"$dir_pw_thread:thread",
+ "$dir_pw_trace",
]
}
@@ -137,6 +170,7 @@ pw_source_set("hdlc_rpc_server") {
"$dir_pw_hdlc:rpc_channel_output",
"$dir_pw_log",
"$dir_pw_sync:mutex",
+ "$dir_pw_trace",
]
}
@@ -168,6 +202,58 @@ pw_source_set("socket_target_io") {
]
}
+pw_source_set("transfer_handlers") {
+ public = [ "public/pw_system/transfer_handlers.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_persistent_ram",
+ "$dir_pw_trace_tokenized:config",
+ "$dir_pw_transfer",
+ ]
+ sources = [ "transfer_handlers.cc" ]
+ deps = []
+}
+
+pw_source_set("file_manager") {
+ public = [ "public/pw_system/file_manager.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":config",
+ ":transfer_handlers",
+ "$dir_pw_file:flat_file_system",
+ "$dir_pw_persistent_ram:flat_file_system_entry",
+ ]
+ sources = [ "file_manager.cc" ]
+ deps = [ ":trace_service" ]
+}
+
+pw_source_set("transfer_service") {
+ public = [ "public/pw_system/transfer_service.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ "$dir_pw_transfer" ]
+ sources = [ "transfer_service.cc" ]
+ deps = [ ":file_manager" ]
+}
+
+pw_source_set("file_service") {
+ public = [ "public/pw_system/file_service.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = []
+ sources = [ "file_service.cc" ]
+ deps = [ ":file_manager" ]
+}
+
+pw_source_set("trace_service") {
+ public = [ "public/pw_system/trace_service.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ ":transfer_handlers" ]
+ sources = [ "trace_service.cc" ]
+ deps = [
+ "$dir_pw_persistent_ram",
+ "$dir_pw_trace_tokenized:trace_service_pwpb",
+ ]
+}
+
pw_source_set("thread_snapshot_service") {
public = [ "public/pw_system/thread_snapshot_service.h" ]
public_configs = [ ":public_include_path" ]
@@ -191,6 +277,7 @@ if (pw_system_TARGET_HOOKS_BACKEND == "") {
get_label_info(":stl_target_hooks", "label_no_toolchain")) {
pw_source_set("stl_target_hooks") {
deps = [
+ ":config",
"$dir_pw_thread:thread",
"$dir_pw_thread_stl:thread",
]
@@ -201,6 +288,7 @@ if (pw_system_TARGET_HOOKS_BACKEND == "") {
get_label_info(":freertos_target_hooks", "label_no_toolchain")) {
pw_source_set("freertos_target_hooks") {
deps = [
+ ":config",
":init",
"$dir_pw_third_party/freertos",
"$dir_pw_thread:thread",
@@ -222,11 +310,15 @@ group("pw_system") {
}
pw_executable("system_example") {
+ # TODO: b/303282642 - Remove this testonly
+ testonly = pw_unit_test_TESTONLY
+
sources = [ "example_user_app_init.cc" ]
deps = [
":pw_system",
"$dir_pw_log",
"$dir_pw_thread:sleep",
+ "$dir_pw_trace",
"$dir_pw_unit_test:rpc_service",
# Adds a test that the test server can run.
@@ -236,10 +328,14 @@ pw_executable("system_example") {
}
group("system_examples") {
+ # TODO: b/303282642 - Remove this testonly
+ testonly = pw_unit_test_TESTONLY
+
deps = [ ":system_example($dir_pigweed/targets/host_device_simulator:host_device_simulator.speed_optimized)" ]
if (dir_pw_third_party_stm32cube_f4 != "" &&
dir_pw_third_party_freertos != "") {
deps += [ ":system_example($dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube.size_optimized)" ]
+ deps += [ ":system_example($dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube_clang.size_optimized)" ]
}
if (dir_pw_third_party_smartfusion_mss != "" &&
dir_pw_third_party_freertos != "") {
@@ -255,10 +351,19 @@ group("system_examples") {
":system_example($dir_pigweed/targets/rp2040_pw_system:rp2040_pw_system.size_optimized)",
]
}
+ if (dir_pw_third_party_ambiq_SDK != "" && dir_pw_third_party_freertos != "") {
+ deps += [
+ ":system_example($dir_pigweed/targets/apollo4_pw_system:apollo4_pw_system.debug)",
+ ":system_example($dir_pigweed/targets/apollo4_pw_system:apollo4_pw_system.size_optimized)",
+ ]
+ }
}
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "cli.rst",
+ "docs.rst",
+ ]
}
pw_test_group("tests") {
diff --git a/pw_system/CMakeLists.txt b/pw_system/CMakeLists.txt
index f926726ef..7e1afc975 100644
--- a/pw_system/CMakeLists.txt
+++ b/pw_system/CMakeLists.txt
@@ -28,15 +28,13 @@ pw_add_library(pw_system.config INTERFACE
pw_add_library(pw_system.log STATIC
PUBLIC_DEPS
- # TODO(b/246101669): Add CMake support.
- # pw_log_rpc.log_service
- # pw_log_rpc.rpc_log_drain_thread
+ pw_log_rpc.log_service
+ pw_log_rpc.rpc_log_drain_thread
pw_multisink
PRIVATE_DEPS
pw_system.config
pw_system.rpc_server
- # TODO(b/246101669): Add CMake support.
- # pw_log_rpc.rpc_log_drain
+ pw_log_rpc.rpc_log_drain
pw_sync.lock_annotations
pw_sync.mutex
HEADERS
@@ -92,6 +90,84 @@ pw_add_library(pw_system.hdlc_rpc_server STATIC
hdlc_rpc_server.cc
)
+pw_add_library(pw_system.thread_snapshot_service STATIC
+ HEADERS
+ public/pw_system/thread_snapshot_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_thread.thread_snapshot_service
+ SOURCES
+ thread_snapshot_service.cc
+)
+
+pw_add_library(pw_system.transfer_handlers STATIC
+ HEADERS
+ public/pw_system/transfer_handlers.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_persistent_ram
+ pw_trace_tokenized.config
+ pw_transfer
+ pw_transfer.proto.pwpb
+ SOURCES
+ transfer_handlers.cc
+)
+
+pw_add_library(pw_system.file_manager STATIC
+ HEADERS
+ public/pw_system/file_manager.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_system.config
+ pw_system.transfer_handlers
+ pw_persistent_ram.flat_file_system_entry
+ PRIVATE_DEPS
+ pw_system.trace_service
+ SOURCES
+ file_manager.cc
+)
+
+pw_add_library(pw_system.transfer_service STATIC
+ HEADERS
+ public/pw_system/transfer_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_transfer
+ PRIVATE_DEPS
+ pw_system.file_manager
+ SOURCES
+ transfer_service.cc
+)
+
+pw_add_library(pw_system.file_service STATIC
+ HEADERS
+ public/pw_system/file_service.h
+ PUBLIC_INCLUDES
+ public
+ PRIVATE_DEPS
+ pw_system.file_manager
+ SOURCES
+ file_service.cc
+)
+
+pw_add_library(pw_system.trace_service STATIC
+ HEADERS
+ public/pw_system/trace_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_system.transfer_handlers
+ PRIVATE_DEPS
+ pw_persistent_ram
+ pw_trace_tokenized.trace_service_pwpb
+ SOURCES
+ trace_service.cc
+)
+
pw_add_library(pw_system.io INTERFACE
HEADERS
public/pw_system/io.h
@@ -109,10 +185,19 @@ pw_add_library(pw_system.init STATIC
SOURCES
init.cc
PRIVATE_DEPS
- pw_system.log
- pw_system.rpc_server
- pw_rpc.pwpb.echo_service
- pw_thread.thread
+ pw_system.file_service
+ pw_system.log
+ pw_system.rpc_server
+ pw_system.target_hooks
+ pw_system.thread_snapshot_service
+ pw_system.trace_service
+ pw_system.transfer_service
+ pw_system.file_manager
+ pw_system.work_queue
+ pw_rpc.pwpb.echo_service
+ pw_metric.metric_service_pwpb
+ pw_thread.thread
+ pw_trace
)
pw_add_library(pw_system.work_queue STATIC
@@ -122,8 +207,10 @@ pw_add_library(pw_system.work_queue STATIC
public
SOURCES
work_queue.cc
- PRIVATE_DEPS
+ PUBLIC_DEPS
pw_work_queue
+ PRIVATE_DEPS
+ pw_system.config
)
pw_add_library(pw_system.target_io STATIC
@@ -135,7 +222,9 @@ pw_add_library(pw_system.target_io STATIC
target_io.cc
)
-pw_add_library(pw_system.target_hooks INTERFACE
+pw_add_facade(pw_system.target_hooks INTERFACE
+ BACKEND
+ pw_system.target_hooks_BACKEND
HEADERS
public/pw_system/target_hooks.h
PUBLIC_INCLUDES
@@ -146,10 +235,10 @@ pw_add_library(pw_system.target_hooks INTERFACE
pw_add_library(pw_system.stl_target_hooks STATIC
PRIVATE_DEPS
+ pw_system.config
pw_thread.sleep
pw_thread.thread
pw_thread_stl.thread
-
SOURCES
stl_target_hooks.cc
)
@@ -158,12 +247,22 @@ pw_add_library(pw_system.freertos_target_hooks STATIC
SOURCES
freertos_target_hooks.cc
PRIVATE_DEPS
+ pw_system.config
pw_thread.thread
pw_thread_freertos.thread
- # TODO(b/234876414): This should depend on FreeRTOS but our third parties
+ # TODO: b/234876414 - This should depend on FreeRTOS but our third parties
# currently do not have CMake support.
)
+pw_add_library(pw_system.zephyr_target_hooks STATIC
+ SOURCES
+ zephyr_target_hooks.cc
+ PRIVATE_DEPS
+ pw_system.target_hooks.facade
+ pw_thread.thread
+ pw_thread_zephyr.thread
+)
+
pw_add_library(pw_system.system_example STATIC
PRIVATE_DEPS
pw_system.init
diff --git a/pw_system/Kconfig b/pw_system/Kconfig
new file mode 100644
index 000000000..61f6420bd
--- /dev/null
+++ b/pw_system/Kconfig
@@ -0,0 +1,52 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+menu "pw_system"
+
+config PIGWEED_SYSTEM_LOG_BACKEND
+ bool "Use pw_system.log_backend as the logging backend"
+ help
+ See :ref:`module-pw_log` for logging module details.
+
+config PIGWEED_SYSTEM_TARGET_HOOKS
+ bool "Use the backend for the pw_system.target_hooks library"
+ help
+ Configure the system's:
+ - log thread
+ - RPC thread
+ - Work queue thread
+
+config PIGWEED_SYSTEM_HDLC_RPC_SERVER
+ bool "Link pw_system.hdlc_rpc_server library and set backend"
+ help
+ Automatically include an HDLC (:ref:`module-pw_hdlc`) RPC server
+ (:ref:`module-pw_rpc`) in the system.
+
+if PIGWEED_SYSTEM_TARGET_HOOKS
+
+config PIGWEED_SYSTEM_TARGET_HOOKS_LOG_STACK_SIZE
+ int "Log thread stack size"
+ default 4096
+
+config PIGWEED_SYSTEM_TARGET_HOOKS_RPC_STACK_SIZE
+ int "RPC thread stack size"
+ default 4096
+
+config PIGWEED_SYSTEM_TARGET_HOOKS_WORK_QUEUE_STACK_SIZE
+ int "Work Queue thread stack size"
+ default 4096
+
+endif # PIGWEED_SYSTEM_TARGET_HOOKS
+
+endmenu
diff --git a/pw_system/backend.cmake b/pw_system/backend.cmake
index 940de5f26..297eaff6e 100644
--- a/pw_system/backend.cmake
+++ b/pw_system/backend.cmake
@@ -17,3 +17,6 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
# The pw_system backend that provides the system RPC server.
pw_add_backend_variable(pw_system.rpc_server_BACKEND)
+
+# The pw_system backend that provides the system target hooks.
+pw_add_backend_variable(pw_system.target_hooks_BACKEND)
diff --git a/pw_system/cli.rst b/pw_system/cli.rst
new file mode 100644
index 000000000..b7e976cfe
--- /dev/null
+++ b/pw_system/cli.rst
@@ -0,0 +1,12 @@
+.. _module-pw_system-cli:
+
+=======================
+pw_system CLI reference
+=======================
+.. argparse::
+ :module: pw_system.console
+ :func: get_parser
+ :prog: pw system console
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
diff --git a/pw_system/docs.rst b/pw_system/docs.rst
index 33fa91823..604a05718 100644
--- a/pw_system/docs.rst
+++ b/pw_system/docs.rst
@@ -15,8 +15,9 @@ of starting a new project using Pigweed by drastically reducing the required
configuration space required to go from first signs of on-device life to a more
sophisticated production-ready system.
+--------------------
Trying out pw_system
-====================
+--------------------
If you'd like to give pw_system a spin and have a STM32F429I Discovery board,
refer to the board's
:ref:`target documentation<target-stm32f429i-disc1-stm32cube>` for instructions
@@ -26,8 +27,9 @@ If you don't have a discovery board, there's a simulated device variation that
you can run on your local machine with no additional hardware. Check out the
steps for trying this out :ref:`here<target-host-device-simulator>`.
+--------------
Target Bringup
-==============
+--------------
Bringing up a new device is as easy as 1-2-3! (Kidding, this is a work in
progress)
@@ -77,81 +79,111 @@ being foundational infrastructure.
.. code-block::
- # Declare a toolchain with suggested, compiler, compiler flags, and default
- # backends.
- pw_system_target("stm32f429i_disc1_stm32cube_size_optimized") {
- # These options drive the logic for automatic configuration by this
- # template.
- cpu = PW_SYSTEM_CPU.CORTEX_M4F
- scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
-
- # Optionally, override pw_system's defaults to build with clang.
- system_toolchain = pw_toolchain_arm_clang
-
- # The pre_init source set provides things like the interrupt vector table,
- # pre-main init, and provision of FreeRTOS hooks.
- link_deps = [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]
-
- # These are hardware-specific options that set up this particular board.
- # These are declared in ``declare_args()`` blocks throughout Pigweed. Any
- # build arguments set by the user will be overridden by these settings.
- build_args = {
- pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_freertos_config"
- pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
- pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
- dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
- pw_third_party_stm32cube_PRODUCT = "STM32F429xx"
- pw_third_party_stm32cube_CONFIG =
- "//targets/stm32f429i_disc1_stm32cube:stm32f4xx_hal_config"
- pw_third_party_stm32cube_CORE_INIT = ""
- pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
- "PW_BOOT_FLASH_BEGIN=0x08000200",
- "PW_BOOT_FLASH_SIZE=2048K",
- "PW_BOOT_HEAP_SIZE=7K",
- "PW_BOOT_MIN_STACK_SIZE=1K",
- "PW_BOOT_RAM_BEGIN=0x20000000",
- "PW_BOOT_RAM_SIZE=192K",
- "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
- ]
- }
- }
-
- # Example for the Emcraft SmartFusion2 system-on-module
- pw_system_target("emcraft_sf2_som_size_optimized") {
- cpu = PW_SYSTEM_CPU.CORTEX_M3
- scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
-
- link_deps = [ "$dir_pigweed/targets/emcraft_sf2_som:pre_init" ]
- build_args = {
- pw_log_BACKEND = dir_pw_log_basic #dir_pw_log_tokenized
- pw_log_tokenized_HANDLER_BACKEND = "//pw_system:log"
- pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/emcraft_sf2_som:sf2_freertos_config"
- pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm3"
- pw_sys_io_BACKEND = dir_pw_sys_io_emcraft_sf2
- dir_pw_third_party_smartfusion_mss = dir_pw_third_party_smartfusion_mss_exported
- pw_third_party_stm32cube_CONFIG =
- "//targets/emcraft_sf2_som:sf2_mss_hal_config"
- pw_third_party_stm32cube_CORE_INIT = ""
- pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
- "PW_BOOT_FLASH_BEGIN=0x00000200",
- "PW_BOOT_FLASH_SIZE=200K",
-
- # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
- # least 6K bytes in heap when using pw_malloc_freelist. The heap size
- # required for tests should be investigated.
- "PW_BOOT_HEAP_SIZE=7K",
- "PW_BOOT_MIN_STACK_SIZE=1K",
- "PW_BOOT_RAM_BEGIN=0x20000000",
- "PW_BOOT_RAM_SIZE=64K",
- "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
- ]
- }
- }
-
-
+ # Declare a toolchain with suggested, compiler, compiler flags, and default
+ # backends.
+ pw_system_target("stm32f429i_disc1_stm32cube_size_optimized") {
+ # These options drive the logic for automatic configuration by this
+ # template.
+ cpu = PW_SYSTEM_CPU.CORTEX_M4F
+ scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
+
+ # Optionally, override pw_system's defaults to build with clang.
+ system_toolchain = pw_toolchain_arm_clang
+
+ # The pre_init source set provides things like the interrupt vector table,
+ # pre-main init, and provision of FreeRTOS hooks.
+ link_deps = [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]
+
+ # These are hardware-specific options that set up this particular board.
+ # These are declared in ``declare_args()`` blocks throughout Pigweed. Any
+ # build arguments set by the user will be overridden by these settings.
+ build_args = {
+ pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_freertos_config"
+ pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
+ pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
+ dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
+ pw_third_party_stm32cube_PRODUCT = "STM32F429xx"
+ pw_third_party_stm32cube_CONFIG =
+ "//targets/stm32f429i_disc1_stm32cube:stm32f4xx_hal_config"
+ pw_third_party_stm32cube_CORE_INIT = ""
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+ "PW_BOOT_FLASH_BEGIN=0x08000200",
+ "PW_BOOT_FLASH_SIZE=2048K",
+ "PW_BOOT_HEAP_SIZE=7K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20000000",
+ "PW_BOOT_RAM_SIZE=192K",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ]
+ }
+ }
+
+ # Example for the Emcraft SmartFusion2 system-on-module
+ pw_system_target("emcraft_sf2_som_size_optimized") {
+ cpu = PW_SYSTEM_CPU.CORTEX_M3
+ scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
+
+ link_deps = [ "$dir_pigweed/targets/emcraft_sf2_som:pre_init" ]
+ build_args = {
+ pw_log_BACKEND = dir_pw_log_basic #dir_pw_log_tokenized
+ pw_log_tokenized_HANDLER_BACKEND = "//pw_system:log"
+ pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/emcraft_sf2_som:sf2_freertos_config"
+ pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm3"
+ pw_sys_io_BACKEND = dir_pw_sys_io_emcraft_sf2
+ dir_pw_third_party_smartfusion_mss = dir_pw_third_party_smartfusion_mss_exported
+ pw_third_party_stm32cube_CONFIG =
+ "//targets/emcraft_sf2_som:sf2_mss_hal_config"
+ pw_third_party_stm32cube_CORE_INIT = ""
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+ "PW_BOOT_FLASH_BEGIN=0x00000200",
+ "PW_BOOT_FLASH_SIZE=200K",
+
+ # TODO: b/235348465 - Currently "pw_tokenizer/detokenize_test" requires at
+ # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+ # required for tests should be investigated.
+ "PW_BOOT_HEAP_SIZE=7K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20000000",
+ "PW_BOOT_RAM_SIZE=64K",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ]
+ }
+ }
+
+-------
Metrics
-=======
+-------
The log backend is tracking metrics to illustrate how to use pw_metric and
retrieve them using `Device.get_and_log_metrics()`.
+
+-------
+Console
+-------
+The ``pw-system-console`` can be used to interact with the targets.
+See :ref:`module-pw_system-cli` for detailed CLI usage information.
+
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+
+ cli
+
+-------------------
+Multi-endpoint mode
+-------------------
+
+The default configuration serves all its traffic with the same
+channel ID and RPC address. There is an alternative mode that assigns a separate
+channel ID and address for logging. This can be useful if you want to separate logging and primary RPC to
+``pw_system`` among multiple clients.
+
+To use this mode, add the following to ``gn args out``:
+
+.. code-block::
+
+ pw_system_USE_MULTI_ENDPOINT_CONFIG = true
+
+The settings for the channel ID and address can be found in the target
+``//pw_system:multi_endpoint_rpc_overrides``.
diff --git a/pw_system/example_user_app_init.cc b/pw_system/example_user_app_init.cc
index 025dc7b8f..e8951d91a 100644
--- a/pw_system/example_user_app_init.cc
+++ b/pw_system/example_user_app_init.cc
@@ -15,7 +15,7 @@
#include "pw_log/log.h"
#include "pw_system/rpc_server.h"
-#include "pw_thread/sleep.h"
+#include "pw_trace/trace.h"
#include "pw_unit_test/unit_test_service.h"
namespace pw::system {
@@ -25,6 +25,8 @@ pw::unit_test::UnitTestService unit_test_service;
// This will run once after pw::system::Init() completes. This callback must
// return or it will block the work queue.
void UserAppInit() {
+ PW_TRACE_FUNCTION();
+
PW_LOG_INFO("Pigweed is fun!");
GetRpcServer().RegisterService(unit_test_service);
}
diff --git a/pw_system/file_manager.cc b/pw_system/file_manager.cc
new file mode 100644
index 000000000..97fb4987b
--- /dev/null
+++ b/pw_system/file_manager.cc
@@ -0,0 +1,47 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_system/file_manager.h"
+
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+#include "pw_system/trace_service.h"
+#endif
+
+namespace pw::system {
+
+namespace {
+FileManager file_manager;
+}
+
+FileManager& GetFileManager() { return file_manager; }
+
+FileManager::FileManager()
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+ : trace_data_handler_(kTraceTransferHandlerId, GetTraceData()),
+ trace_data_filesystem_entry_(
+ "/trace/0.bin",
+ kTraceTransferHandlerId,
+ file::FlatFileSystemService::Entry::FilePermissions::READ,
+ GetTraceData())
+#endif
+{
+ // Every handler & filesystem element must be added to the collections, using
+ // the associated handler ID as the index.
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+ transfer_handlers_[kTraceTransferHandlerId] = &trace_data_handler_;
+ file_system_entries_[kTraceTransferHandlerId] = &trace_data_filesystem_entry_;
+#endif
+}
+
+} // namespace pw::system
diff --git a/pw_system/file_service.cc b/pw_system/file_service.cc
new file mode 100644
index 000000000..4e3f3519a
--- /dev/null
+++ b/pw_system/file_service.cc
@@ -0,0 +1,32 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_system/file_service.h"
+
+#include "public/pw_system/file_manager.h"
+
+namespace pw::system {
+namespace {
+
+constexpr size_t kMaxFileNameLength = 48;
+file::FlatFileSystemServiceWithBuffer<kMaxFileNameLength, 1> file_service(
+ GetFileManager().GetFileSystemEntries());
+
+} // namespace
+
+void RegisterFileService(rpc::Server& rpc_server) {
+ rpc_server.RegisterService(file_service);
+}
+
+} // namespace pw::system
diff --git a/pw_system/freertos_backends.gni b/pw_system/freertos_backends.gni
index 8829bc2bb..2c5b85a5d 100644
--- a/pw_system/freertos_backends.gni
+++ b/pw_system/freertos_backends.gni
@@ -33,6 +33,7 @@ PW_SYSTEM_FREERTOS_BACKENDS = {
"$dir_pw_thread_freertos:thread_iteration"
pw_thread_YIELD_BACKEND = "$dir_pw_thread_freertos:yield"
pw_system_TARGET_HOOKS_BACKEND = "$dir_pw_system:freertos_target_hooks"
+ pw_trace_tokenizer_time = "$dir_pw_trace_tokenized:fake_trace_time"
# Enable pw_third_party_freertos_DISABLE_TASKS_STATICS so thread iteration
# works out-of-the-box.
diff --git a/pw_system/freertos_target_hooks.cc b/pw_system/freertos_target_hooks.cc
index d533308a6..c617f41d4 100644
--- a/pw_system/freertos_target_hooks.cc
+++ b/pw_system/freertos_target_hooks.cc
@@ -13,6 +13,7 @@
// the License.
#include "FreeRTOS.h"
+#include "pw_system/config.h"
#include "pw_thread/detached_thread.h"
#include "pw_thread/thread.h"
#include "pw_thread_freertos/context.h"
@@ -27,6 +28,9 @@ enum class ThreadPriority : UBaseType_t {
// there's synchronization issues when they are.
kLog = kWorkQueue,
kRpc = kWorkQueue,
+#if PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+ kTransfer = kWorkQueue,
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
kNumPriorities,
};
@@ -57,6 +61,20 @@ const thread::Options& RpcThreadOptions() {
return options;
}
+#if PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+static constexpr size_t kTransferThreadStackWords = 512;
+static thread::freertos::StaticContextWithStack<kTransferThreadStackWords>
+ transfer_thread_context;
+const thread::Options& TransferThreadOptions() {
+ static constexpr auto options =
+ pw::thread::freertos::Options()
+ .set_name("TransferThread")
+ .set_static_context(transfer_thread_context)
+ .set_priority(static_cast<UBaseType_t>(ThreadPriority::kTransfer));
+ return options;
+}
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+
static constexpr size_t kWorkQueueThreadStackWords = 512;
static thread::freertos::StaticContextWithStack<kWorkQueueThreadStackWords>
work_queue_thread_context;
diff --git a/pw_system/hdlc_rpc_server.cc b/pw_system/hdlc_rpc_server.cc
index 5bbcd83ab..72c6c925b 100644
--- a/pw_system/hdlc_rpc_server.cc
+++ b/pw_system/hdlc_rpc_server.cc
@@ -16,16 +16,25 @@
#include <cstddef>
#include <cstdint>
#include <cstdio>
+#include <mutex>
#include "pw_assert/check.h"
#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/rpc_channel.h"
#include "pw_hdlc/rpc_packets.h"
#include "pw_log/log.h"
+#include "pw_rpc/channel.h"
#include "pw_sync/mutex.h"
#include "pw_system/config.h"
#include "pw_system/io.h"
#include "pw_system/rpc_server.h"
+#include "pw_trace/trace.h"
+
+#if PW_SYSTEM_DEFAULT_CHANNEL_ID != PW_SYSTEM_LOGGING_CHANNEL_ID && \
+ PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS == PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS
+#error \
+ "Default and logging addresses must be different to support multiple channels."
+#endif
namespace pw::system {
namespace {
@@ -35,10 +44,50 @@ constexpr size_t kMaxTransmissionUnit = PW_SYSTEM_MAX_TRANSMISSION_UNIT;
static_assert(kMaxTransmissionUnit ==
hdlc::MaxEncodedFrameSize(rpc::cfg::kEncodingBufferSizeBytes));
+#if PW_SYSTEM_DEFAULT_CHANNEL_ID == PW_SYSTEM_LOGGING_CHANNEL_ID
hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
GetWriter(), PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS, "HDLC channel");
rpc::Channel channels[] = {
rpc::Channel::Create<kDefaultRpcChannelId>(&hdlc_channel_output)};
+#else
+class SynchronizedChannelOutput : public rpc::ChannelOutput {
+ public:
+ SynchronizedChannelOutput(stream::Writer& writer,
+ uint64_t address,
+ const char* channel_name)
+ : rpc::ChannelOutput(channel_name),
+ inner_(writer, address, channel_name) {}
+
+ Status Send(span<const std::byte> buffer) override {
+ std::lock_guard guard(mtx_);
+ auto s = inner_.Send(buffer);
+ return s;
+ }
+
+ size_t MaximumTransmissionUnit() override {
+ std::lock_guard guard(mtx_);
+ auto s = inner_.MaximumTransmissionUnit();
+ return s;
+ }
+
+ private:
+ sync::Mutex mtx_;
+ hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> inner_ PW_GUARDED_BY(mtx_);
+};
+
+SynchronizedChannelOutput hdlc_channel_output[] = {
+ SynchronizedChannelOutput(GetWriter(),
+ PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS,
+ "HDLC default channel"),
+ SynchronizedChannelOutput(GetWriter(),
+ PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS,
+ "HDLC logging channel"),
+};
+rpc::Channel channels[] = {
+ rpc::Channel::Create<kDefaultRpcChannelId>(&hdlc_channel_output[0]),
+ rpc::Channel::Create<kLoggingRpcChannelId>(&hdlc_channel_output[1]),
+};
+#endif
rpc::Server server(channels);
constexpr size_t kDecoderBufferSize =
@@ -69,7 +118,9 @@ class RpcDispatchThread final : public thread::ThreadCore {
for (std::byte byte : ret_val.value()) {
if (auto result = decoder.Process(byte); result.ok()) {
hdlc::Frame& frame = result.value();
- if (frame.address() == PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS) {
+ PW_TRACE_SCOPE("RPC process frame");
+ if (frame.address() == PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS ||
+ frame.address() == PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS) {
server.ProcessPacket(frame.data());
}
}
diff --git a/pw_system/init.cc b/pw_system/init.cc
index 13c4e1bf6..96d5dad39 100644
--- a/pw_system/init.cc
+++ b/pw_system/init.cc
@@ -26,6 +26,18 @@
#include "pw_system_private/log.h"
#include "pw_thread/detached_thread.h"
+#if PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+#include "pw_system/transfer_service.h"
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+#include "pw_system/file_service.h"
+#include "pw_system/trace_service.h"
+#include "pw_trace/trace.h"
+#endif // PW_SYSTEM_ENABLE_TRACE_SERVICE
+
+#include "pw_system/file_manager.h"
+
#if PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
#include "pw_system/thread_snapshot_service.h"
#endif // PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
@@ -38,11 +50,17 @@ metric::MetricService metric_service(metric::global_metrics,
rpc::EchoService echo_service;
void InitImpl() {
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+ // tracing is off by default, requring a user to enable it through
+ // the trace service
+ PW_TRACE_SET_ENABLED(false);
+#endif
+
PW_LOG_INFO("System init");
// Setup logging.
const Status status = GetLogThread().OpenUnrequestedLogStream(
- kDefaultRpcChannelId, GetRpcServer(), GetLogService());
+ kLoggingRpcChannelId, GetRpcServer(), GetLogService());
if (!status.ok()) {
PW_LOG_ERROR("Error opening unrequested log streams %d",
static_cast<int>(status.code()));
@@ -52,6 +70,16 @@ void InitImpl() {
GetRpcServer().RegisterService(echo_service);
GetRpcServer().RegisterService(GetLogService());
GetRpcServer().RegisterService(metric_service);
+
+#if PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+ RegisterTransferService(GetRpcServer());
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+ RegisterFileService(GetRpcServer());
+ RegisterTraceService(GetRpcServer(), FileManager::kTraceTransferHandlerId);
+#endif // PW_SYSTEM_ENABLE_TRACE_SERVICE
+
#if PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
RegisterThreadSnapshotService(GetRpcServer());
#endif // PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
@@ -61,6 +89,11 @@ void InitImpl() {
thread::DetachedThread(system::LogThreadOptions(), GetLogThread());
thread::DetachedThread(system::RpcThreadOptions(), GetRpcDispatchThread());
+#if PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+ thread::DetachedThread(system::TransferThreadOptions(), GetTransferThread());
+ InitTransferService();
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+
GetWorkQueue().CheckPushWork(UserAppInit);
}
diff --git a/pw_system/log.cc b/pw_system/log.cc
index 652f3e2b2..3a5f0fd8e 100644
--- a/pw_system/log.cc
+++ b/pw_system/log.cc
@@ -46,7 +46,7 @@ std::array<std::byte, PW_SYSTEM_MAX_LOG_ENTRY_SIZE> log_decode_buffer
PW_GUARDED_BY(drains_mutex);
std::array<RpcLogDrain, 1> drains{{
- RpcLogDrain(kDefaultRpcChannelId,
+ RpcLogDrain(kLoggingRpcChannelId,
log_decode_buffer,
drains_mutex,
RpcLogDrain::LogDrainErrorHandling::kIgnoreWriterErrors),
diff --git a/pw_system/public/pw_system/config.h b/pw_system/public/pw_system/config.h
index 1dc3c3cbd..b4024ca89 100644
--- a/pw_system/public/pw_system/config.h
+++ b/pw_system/public/pw_system/config.h
@@ -44,6 +44,16 @@
#define PW_SYSTEM_DEFAULT_CHANNEL_ID 1
#endif // PW_SYSTEM_DEFAULT_CHANNEL_ID
+// PW_SYSTEM_LOGGING_CHANNEL_ID logging RPC channel ID to host. If this is
+// different from PW_SYSTEM_DEFAULT_CHANNEL_ID, then
+// PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS must also be different from
+// PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS.
+//
+// Defaults to PW_SYSTEM_DEFAULT_CHANNEL_ID.
+#ifndef PW_SYSTEM_LOGGING_CHANNEL_ID
+#define PW_SYSTEM_LOGGING_CHANNEL_ID PW_SYSTEM_DEFAULT_CHANNEL_ID
+#endif // PW_SYSTEM_LOGGING_CHANNEL_ID
+
// PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS RPC HDLC default address.
//
// Defaults to 82.
@@ -51,6 +61,28 @@
#define PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS 82
#endif // PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS
+// PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS RPC HDLC logging address.
+//
+// Defaults to PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS.
+#ifndef PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS
+#define PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS
+#endif // PW_SYSTEM_LOGGING_RPC_HDLC_ADDRESS
+
+// PW_SYSTEM_ENABLE_TRACE_SERVICE specifies if the trace RPC service is enabled.
+//
+// Defaults to 1.
+#ifndef PW_SYSTEM_ENABLE_TRACE_SERVICE
+#define PW_SYSTEM_ENABLE_TRACE_SERVICE 1
+#endif // PW_SYSTEM_ENABLE_TRACE_SERVICE
+
+// PW_SYSTEM_ENABLE_TRANSFER_SERVICE specifies if the transfer RPC service is
+// enabled.
+//
+// Defaults to 1.
+#ifndef PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+#define PW_SYSTEM_ENABLE_TRANSFER_SERVICE 1
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+
// PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE specifies if the thread snapshot
// RPC service is enabled.
//
diff --git a/pw_system/public/pw_system/file_manager.h b/pw_system/public/pw_system/file_manager.h
new file mode 100644
index 000000000..a061cd5a3
--- /dev/null
+++ b/pw_system/public/pw_system/file_manager.h
@@ -0,0 +1,61 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_persistent_ram/flat_file_system_entry.h"
+#include "pw_system/config.h"
+#include "pw_system/transfer_handlers.h"
+
+namespace pw::system {
+
+class FileManager {
+ public:
+ // Each transfer handler ID corresponds 1:1 with a transfer handler and
+ // filesystem element pair. The ID must be unique and increment from 0 to
+ // ensure no gaps in the FileManager handler & filesystem arrays.
+ // NOTE: the enumerators should never have values defined, to ensure they
+ // increment from zero and kNumFileSystemEntries is correct
+ enum TransferHandlerId : uint32_t {
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+ kTraceTransferHandlerId,
+#endif
+ kNumFileSystemEntries
+ };
+
+ FileManager();
+
+ std::array<transfer::Handler*, kNumFileSystemEntries>& GetTransferHandlers() {
+ return transfer_handlers_;
+ }
+ std::array<file::FlatFileSystemService::Entry*, kNumFileSystemEntries>&
+ GetFileSystemEntries() {
+ return file_system_entries_;
+ }
+
+ private:
+#if PW_SYSTEM_ENABLE_TRACE_SERVICE
+ TracePersistentBufferTransfer trace_data_handler_;
+ persistent_ram::FlatFileSystemPersistentBufferEntry<
+ PW_TRACE_BUFFER_SIZE_BYTES>
+ trace_data_filesystem_entry_;
+#endif
+
+ std::array<transfer::Handler*, kNumFileSystemEntries> transfer_handlers_;
+ std::array<file::FlatFileSystemService::Entry*, kNumFileSystemEntries>
+ file_system_entries_;
+};
+
+FileManager& GetFileManager();
+
+} // namespace pw::system
diff --git a/pw_system/public/pw_system/file_service.h b/pw_system/public/pw_system/file_service.h
new file mode 100644
index 000000000..1610bff01
--- /dev/null
+++ b/pw_system/public/pw_system/file_service.h
@@ -0,0 +1,22 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_rpc/server.h"
+
+namespace pw::system {
+
+void RegisterFileService(rpc::Server& rpc_server);
+
+} // namespace pw::system
diff --git a/pw_system/public/pw_system/rpc_server.h b/pw_system/public/pw_system/rpc_server.h
index bf05fccdb..033757064 100644
--- a/pw_system/public/pw_system/rpc_server.h
+++ b/pw_system/public/pw_system/rpc_server.h
@@ -23,10 +23,13 @@
namespace pw::system {
// This is the default channel used by the pw_system RPC server. Some other
-// parts of pw_system (e.g. logging) use this channel ID as the default
-// destination for unrequested data streams.
+// parts of pw_system use this channel ID as the default destination for
+// unrequested data streams.
inline constexpr uint32_t kDefaultRpcChannelId = PW_SYSTEM_DEFAULT_CHANNEL_ID;
+// This is the channel ID used for logging.
+inline constexpr uint32_t kLoggingRpcChannelId = PW_SYSTEM_LOGGING_CHANNEL_ID;
+
rpc::Server& GetRpcServer();
thread::ThreadCore& GetRpcDispatchThread();
diff --git a/pw_system/public/pw_system/target_hooks.h b/pw_system/public/pw_system/target_hooks.h
index 58460ca8b..19628c2ad 100644
--- a/pw_system/public/pw_system/target_hooks.h
+++ b/pw_system/public/pw_system/target_hooks.h
@@ -21,6 +21,8 @@ const thread::Options& LogThreadOptions();
const thread::Options& RpcThreadOptions();
+const thread::Options& TransferThreadOptions();
+
const thread::Options& WorkQueueThreadOptions();
// This will run once after pw::system::Init() completes. This callback must
diff --git a/pw_system/public/pw_system/trace_service.h b/pw_system/public/pw_system/trace_service.h
new file mode 100644
index 000000000..7c16c0e38
--- /dev/null
+++ b/pw_system/public/pw_system/trace_service.h
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_rpc/server.h"
+#include "pw_system/transfer_handlers.h"
+
+namespace pw::system {
+
+void RegisterTraceService(rpc::Server& rpc_server, uint32_t transfer_id);
+
+TracePersistentBuffer& GetTraceData();
+
+} // namespace pw::system
diff --git a/pw_system/public/pw_system/transfer_handlers.h b/pw_system/public/pw_system/transfer_handlers.h
new file mode 100644
index 000000000..f82308559
--- /dev/null
+++ b/pw_system/public/pw_system/transfer_handlers.h
@@ -0,0 +1,37 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_persistent_ram/persistent_buffer.h"
+#include "pw_trace_tokenized/config.h"
+#include "pw_transfer/transfer.h"
+
+namespace pw::system {
+
+using TracePersistentBuffer =
+ persistent_ram::PersistentBuffer<PW_TRACE_BUFFER_SIZE_BYTES>;
+
+class TracePersistentBufferTransfer : public transfer::ReadOnlyHandler {
+ public:
+ TracePersistentBufferTransfer(uint32_t id,
+ TracePersistentBuffer& persistent_buffer);
+
+ Status PrepareRead() final;
+
+ private:
+ TracePersistentBuffer& persistent_buffer_;
+ stream::MemoryReader reader_;
+};
+
+} // namespace pw::system
diff --git a/pw_system/public/pw_system/transfer_service.h b/pw_system/public/pw_system/transfer_service.h
new file mode 100644
index 000000000..64a60b99c
--- /dev/null
+++ b/pw_system/public/pw_system/transfer_service.h
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_rpc/server.h"
+#include "pw_transfer/transfer_thread.h"
+
+namespace pw::system {
+
+void RegisterTransferService(rpc::Server& rpc_server);
+
+void InitTransferService();
+
+transfer::TransferThread& GetTransferThread();
+
+} // namespace pw::system
diff --git a/pw_system/py/BUILD.bazel b/pw_system/py/BUILD.bazel
index 0feba3139..61c72038d 100644
--- a/pw_system/py/BUILD.bazel
+++ b/pw_system/py/BUILD.bazel
@@ -16,7 +16,7 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library")
package(default_visibility = ["//visibility:public"])
-# TODO(b/241456982): The following deps are required to build :pw_system_lib
+# TODO: b/241456982 - The following deps are required to build :pw_system_lib
# deps = [
# "//pw_thread/py:pw_thread",
# "//pw_log:log_proto_py_pb2",
@@ -33,6 +33,7 @@ py_library(
"pw_system/__init__.py",
"pw_system/console.py",
"pw_system/device.py",
+ "pw_system/device_tracing.py",
],
imports = ["."],
tags = ["manual"],
@@ -40,9 +41,10 @@ py_library(
"//pw_cli/py:pw_cli",
"//pw_console/py:pw_console",
"//pw_hdlc/py:pw_hdlc",
+ "//pw_log/py:pw_log",
+ "//pw_log_rpc/py:pw_log_rpc",
"//pw_metric/py:pw_metric",
"//pw_rpc/py:pw_rpc",
- "//pw_status/py:pw_status",
"//pw_tokenizer/py:pw_tokenizer",
],
)
diff --git a/pw_system/py/BUILD.gn b/pw_system/py/BUILD.gn
index f8af592f9..7799436a3 100644
--- a/pw_system/py/BUILD.gn
+++ b/pw_system/py/BUILD.gn
@@ -20,18 +20,22 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_system/__init__.py",
"pw_system/console.py",
"pw_system/device.py",
+ "pw_system/device_tracing.py",
+ "pw_system/trace_client.py",
]
python_deps = [
"$dir_pw_cli/py",
"$dir_pw_console/py",
+ "$dir_pw_file/py",
"$dir_pw_hdlc/py",
"$dir_pw_log:protos.python",
+ "$dir_pw_log/py",
+ "$dir_pw_log_rpc/py",
"$dir_pw_metric:metric_service_proto.python",
"$dir_pw_metric/py",
"$dir_pw_protobuf_compiler/py",
@@ -39,6 +43,8 @@ pw_python_package("py") {
"$dir_pw_thread:protos.python",
"$dir_pw_thread/py",
"$dir_pw_tokenizer/py",
+ "$dir_pw_trace_tokenized:protos.python",
+ "$dir_pw_transfer:proto.python",
"$dir_pw_unit_test:unit_test_proto.python",
"$dir_pw_unit_test/py",
]
diff --git a/pw_system/py/pw_system/console.py b/pw_system/py/pw_system/console.py
index 49950884a..6f6405cac 100644
--- a/pw_system/py/pw_system/console.py
+++ b/pw_system/py/pw_system/console.py
@@ -38,6 +38,7 @@ from inspect import cleandoc
import logging
from pathlib import Path
import sys
+import time
from types import ModuleType
from typing import (
Any,
@@ -49,7 +50,6 @@ from typing import (
Optional,
Union,
)
-import socket
import serial
import IPython # type: ignore
@@ -60,8 +60,11 @@ from pw_console.log_store import LogStore
from pw_console.plugins.bandwidth_toolbar import BandwidthToolbar
from pw_console.pyserial_wrapper import SerialWithLogging
from pw_console.python_logging import create_temp_log_file, JsonLogFormatter
+from pw_console.socket_client import SocketClient, SocketClientWithLogging
+from pw_hdlc import rpc
from pw_rpc.console_tools.console import flattened_rpc_completions
from pw_system.device import Device
+from pw_system.device_tracing import DeviceWithTracing
from pw_tokenizer.detokenize import AutoUpdatingDetokenizer
# Default proto imports:
@@ -69,20 +72,21 @@ from pw_log.proto import log_pb2
from pw_metric_proto import metric_service_pb2
from pw_thread_protos import thread_snapshot_service_pb2
from pw_unit_test_proto import unit_test_pb2
+from pw_file import file_pb2
+from pw_trace_protos import trace_service_pb2
+from pw_transfer import transfer_pb2
_LOG = logging.getLogger('tools')
_DEVICE_LOG = logging.getLogger('rpc_device')
_SERIAL_DEBUG = logging.getLogger('pw_console.serial_debug_logger')
_ROOT_LOG = logging.getLogger()
-PW_RPC_MAX_PACKET_SIZE = 256
-SOCKET_SERVER = 'localhost'
-SOCKET_PORT = 33000
MKFIFO_MODE = 0o666
-def _parse_args():
- """Parses and returns the command line arguments."""
+def get_parser() -> argparse.ArgumentParser:
+ """Gets argument parser with console arguments."""
+
parser = argparse.ArgumentParser(
prog="python -m pw_system.console", description=__doc__
)
@@ -157,8 +161,13 @@ def _parse_args():
'-s',
'--socket-addr',
type=str,
- help='use socket to connect to server, type default for\
- localhost:33000, or manually input the server address:port',
+ help=(
+ 'Socket address used to connect to server. Type "default" to use '
+ 'localhost:33000, pass the server address and port as '
+ 'address:port, or prefix the path to a forwarded socket with '
+ f'"{SocketClient.FILE_SOCKET_SERVER}:" as '
+ f'{SocketClient.FILE_SOCKET_SERVER}:path_to_file.'
+ ),
)
parser.add_argument(
"--token-databases",
@@ -179,6 +188,13 @@ def _parse_args():
help='glob pattern for .proto files.',
)
parser.add_argument(
+ '-f',
+ '--ticks_per_second',
+ type=int,
+ dest='ticks_per_second',
+ help=('The clock rate of the trace events.'),
+ )
+ parser.add_argument(
'-v',
'--verbose',
action='store_true',
@@ -191,7 +207,7 @@ def _parse_args():
help='Use IPython instead of pw_console.',
)
- # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is
+ # TODO: b/248257406 - Use argparse.BooleanOptionalAction when Python 3.8 is
# no longer supported.
parser.add_argument(
'--rpc-logging',
@@ -207,6 +223,38 @@ def _parse_args():
help="Don't use pw_rpc based logging.",
)
+ # TODO: b/248257406 - Use argparse.BooleanOptionalAction when Python 3.8 is
+ # no longer supported.
+ parser.add_argument(
+ '--hdlc-encoding',
+ action='store_true',
+ default=True,
+ help='Use HDLC encoding on transfer interfaces.',
+ )
+
+ parser.add_argument(
+ '--no-hdlc-encoding',
+ action='store_false',
+ dest='hdlc_encoding',
+ help="Don't use HDLC encoding on transfer interface.",
+ )
+
+ parser.add_argument(
+ '--channel-id',
+ type=int,
+ default=rpc.DEFAULT_CHANNEL_ID,
+ help="Channel ID used in RPC communications.",
+ )
+
+ return parser
+
+
+def _parse_args(args: Optional[argparse.Namespace] = None):
+ """Parses and returns the command line arguments."""
+ if args is not None:
+ return args
+
+ parser = get_parser()
return parser.parse_args()
@@ -269,8 +317,10 @@ def _start_python_terminal( # pylint: disable=too-many-arguments
if use_ipython:
print(welcome_message)
- IPython.terminal.embed.InteractiveShellEmbed().mainloop(
- local_ns=local_variables, module=argparse.Namespace()
+ IPython.start_ipython(
+ argv=[],
+ display_banner=False,
+ user_ns=local_variables,
)
return
@@ -307,32 +357,12 @@ def _start_python_terminal( # pylint: disable=too-many-arguments
interactive_console.embed()
-class SocketClientImpl:
- def __init__(self, config: str):
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_server = ''
- socket_port = 0
-
- if config == 'default':
- socket_server = SOCKET_SERVER
- socket_port = SOCKET_PORT
- else:
- socket_server, socket_port_str = config.split(':')
- socket_port = int(socket_port_str)
- self.socket.connect((socket_server, socket_port))
-
- def write(self, data: bytes):
- self.socket.sendall(data)
-
- def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE):
- return self.socket.recv(num_bytes)
-
-
# pylint: disable=too-many-arguments,too-many-locals
def console(
device: str,
baudrate: int,
proto_globs: Collection[str],
+ ticks_per_second: Optional[int],
token_databases: Collection[Path],
socket_addr: str,
logfile: str,
@@ -347,6 +377,8 @@ def console(
merge_device_and_host_logs: bool = False,
rpc_logging: bool = True,
use_ipython: bool = False,
+ channel_id: int = rpc.DEFAULT_CHANNEL_ID,
+ hdlc_encoding: bool = True,
) -> int:
"""Starts an interactive RPC console for HDLC."""
# argparse.FileType doesn't correctly handle '-' for binary files.
@@ -418,7 +450,11 @@ def console(
detokenizer = None
if token_databases:
- detokenizer = AutoUpdatingDetokenizer(*token_databases)
+ token_databases_with_domains = [] * len(token_databases)
+ for token_database in token_databases:
+ token_databases_with_domains.append(str(token_database) + "#trace")
+
+ detokenizer = AutoUpdatingDetokenizer(*token_databases_with_domains)
detokenizer.show_errors = True
protos: List[Union[ModuleType, Path]] = list(_expand_globs(proto_globs))
@@ -435,6 +471,9 @@ def console(
protos.extend(compiled_protos)
protos.append(metric_service_pb2)
protos.append(thread_snapshot_service_pb2)
+ protos.append(file_pb2)
+ protos.append(trace_service_pb2)
+ protos.append(transfer_pb2)
if not protos:
_LOG.critical(
@@ -449,12 +488,9 @@ def console(
', '.join(proto_globs),
)
- serial_impl = serial.Serial
- if serial_debug:
- serial_impl = SerialWithLogging
-
timestamp_decoder = None
if socket_addr is None:
+ serial_impl = SerialWithLogging if serial_debug else serial.Serial
serial_device = serial_impl(
device,
baudrate,
@@ -464,7 +500,7 @@ def console(
# https://pythonhosted.org/pyserial/pyserial_api.html#serial.Serial
timeout=0.1,
)
- read = lambda: serial_device.read(8192)
+ reader = rpc.SerialReader(serial_device, 8192)
write = serial_device.write
# Overwrite decoder for serial device.
@@ -474,47 +510,68 @@ def console(
timestamp_decoder = milliseconds_to_string
else:
+ socket_impl = SocketClientWithLogging if serial_debug else SocketClient
+
+ def disconnect_handler(socket_device: SocketClient) -> None:
+ """Attempts to reconnect on disconnected socket."""
+ _LOG.error('Socket disconnected. Will retry to connect.')
+ while True:
+ try:
+ socket_device.connect()
+ break
+ except: # pylint: disable=bare-except
+ # Ignore errors and retry to reconnect.
+ time.sleep(1)
+ _LOG.info('Successfully reconnected')
+
try:
- socket_device = SocketClientImpl(socket_addr)
- read = socket_device.read
+ socket_device = socket_impl(
+ socket_addr, on_disconnect=disconnect_handler
+ )
+ reader = rpc.SelectableReader(socket_device)
write = socket_device.write
except ValueError:
_LOG.exception('Failed to initialize socket at %s', socket_addr)
return 1
- device_client = Device(
- 1,
- read,
- write,
- protos,
- detokenizer,
- timestamp_decoder=timestamp_decoder,
- rpc_timeout_s=5,
- use_rpc_logging=rpc_logging,
- )
-
- _start_python_terminal(
- device=device_client,
- device_log_store=device_log_store,
- root_log_store=root_log_store,
- serial_debug_log_store=serial_debug_log_store,
- log_file=logfile,
- host_logfile=host_logfile,
- device_logfile=device_logfile,
- json_logfile=json_logfile,
- serial_debug=serial_debug,
- config_file_path=config_file,
- use_ipython=use_ipython,
- )
+ with reader:
+ device_client = DeviceWithTracing(
+ ticks_per_second,
+ channel_id,
+ reader,
+ write,
+ protos,
+ detokenizer=detokenizer,
+ timestamp_decoder=timestamp_decoder,
+ rpc_timeout_s=5,
+ use_rpc_logging=rpc_logging,
+ use_hdlc_encoding=hdlc_encoding,
+ )
+ with device_client:
+ _start_python_terminal(
+ device=device_client,
+ device_log_store=device_log_store,
+ root_log_store=root_log_store,
+ serial_debug_log_store=serial_debug_log_store,
+ log_file=logfile,
+ host_logfile=host_logfile,
+ device_logfile=device_logfile,
+ json_logfile=json_logfile,
+ serial_debug=serial_debug,
+ config_file_path=config_file,
+ use_ipython=use_ipython,
+ )
return 0
-def main() -> int:
- return console(**vars(_parse_args()))
+def main(args: Optional[argparse.Namespace] = None) -> int:
+ return console(**vars(_parse_args(args)))
-def main_with_compiled_protos(compiled_protos):
- return console(**vars(_parse_args()), compiled_protos=compiled_protos)
+def main_with_compiled_protos(
+ compiled_protos, args: Optional[argparse.Namespace] = None
+):
+ return console(**vars(_parse_args(args)), compiled_protos=compiled_protos)
if __name__ == '__main__':
diff --git a/pw_system/py/pw_system/device.py b/pw_system/py/pw_system/device.py
index 3ec387f99..470ddc236 100644
--- a/pw_system/py/pw_system/device.py
+++ b/pw_system/py/pw_system/device.py
@@ -13,29 +13,39 @@
# the License.
"""Device classes to interact with targets via RPC."""
-import datetime
import logging
from pathlib import Path
from types import ModuleType
from typing import Any, Callable, List, Union, Optional
-from pw_hdlc.rpc import HdlcRpcClient, default_channels
-from pw_log_tokenized import FormatStringWithMetadata
-from pw_log.proto import log_pb2
+from pw_thread_protos import thread_pb2
+from pw_hdlc.rpc import (
+ HdlcRpcClient,
+ channel_output,
+ NoEncodingSingleChannelRpcClient,
+ RpcClient,
+ CancellableReader,
+)
+from pw_log.log_decoder import (
+ Log,
+ LogStreamDecoder,
+ log_decoded_log,
+ timestamp_parser_ns_since_boot,
+)
+from pw_log_rpc.rpc_log_stream import LogStreamHandler
from pw_metric import metric_parser
-from pw_rpc import callback_client, console_tools
-from pw_status import Status
+from pw_rpc import callback_client, Channel, console_tools
from pw_thread.thread_analyzer import ThreadSnapshotAnalyzer
-from pw_thread_protos import thread_pb2
from pw_tokenizer import detokenize
from pw_tokenizer.proto import decode_optionally_tokenized
-from pw_unit_test.rpc import run_tests as pw_unit_test_run_tests
+from pw_unit_test.rpc import run_tests as pw_unit_test_run_tests, TestRecord
# Internal log for troubleshooting this tool (the console).
_LOG = logging.getLogger('tools')
DEFAULT_DEVICE_LOGGER = logging.getLogger('rpc_device')
+# pylint: disable=too-many-arguments
class Device:
"""Represents an RPC Client for a device running a Pigweed target.
@@ -43,26 +53,30 @@ class Device:
Note: use this class as a base for specialized device representations.
"""
+ # pylint: disable=too-many-instance-attributes
def __init__(
+ # pylint: disable=too-many-arguments
self,
channel_id: int,
- read,
+ reader: CancellableReader,
write,
proto_library: List[Union[ModuleType, Path]],
- detokenizer: Optional[detokenize.Detokenizer],
- timestamp_decoder: Optional[Callable[[int], str]],
+ detokenizer: Optional[detokenize.Detokenizer] = None,
+ timestamp_decoder: Optional[Callable[[int], str]] = None,
rpc_timeout_s: float = 5,
+ time_offset: int = 0,
use_rpc_logging: bool = True,
+ use_hdlc_encoding: bool = True,
+ logger: logging.Logger = DEFAULT_DEVICE_LOGGER,
):
self.channel_id = channel_id
self.protos = proto_library
self.detokenizer = detokenizer
self.rpc_timeout_s = rpc_timeout_s
+ self.time_offset = time_offset
- self.logger = DEFAULT_DEVICE_LOGGER
+ self.logger = logger
self.logger.setLevel(logging.DEBUG) # Allow all device logs through.
- self.timestamp_decoder = timestamp_decoder
- self._expected_log_sequence_id = 0
callback_client_impl = callback_client.Impl(
default_unary_timeout_s=self.rpc_timeout_s,
@@ -82,17 +96,56 @@ class Device:
for line in log_messages.splitlines():
self.logger.info(line)
- self.client = HdlcRpcClient(
- read,
- self.protos,
- default_channels(write),
- detokenize_and_log_output,
- client_impl=callback_client_impl,
- )
+ self.client: RpcClient
+ if use_hdlc_encoding:
+ channels = [Channel(self.channel_id, channel_output(write))]
+ self.client = HdlcRpcClient(
+ reader,
+ self.protos,
+ channels,
+ detokenize_and_log_output,
+ client_impl=callback_client_impl,
+ )
+ else:
+ channel = Channel(self.channel_id, write)
+ self.client = NoEncodingSingleChannelRpcClient(
+ reader,
+ self.protos,
+ channel,
+ client_impl=callback_client_impl,
+ )
if use_rpc_logging:
+ # Create the log decoder used by the LogStreamHandler.
+
+ def decoded_log_handler(log: Log) -> None:
+ log_decoded_log(log, self.logger)
+
+ self._log_decoder = LogStreamDecoder(
+ decoded_log_handler=decoded_log_handler,
+ detokenizer=self.detokenizer,
+ source_name='RpcDevice',
+ timestamp_parser=(
+ timestamp_decoder
+ if timestamp_decoder
+ else timestamp_parser_ns_since_boot
+ ),
+ )
+
# Start listening to logs as soon as possible.
- self.listen_to_log_stream()
+ self.log_stream_handler = LogStreamHandler(
+ self.rpcs, self._log_decoder
+ )
+ self.log_stream_handler.listen_to_logs()
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *exc_info):
+ self.close()
+
+ def close(self) -> None:
+ self.client.close()
def info(self) -> console_tools.ClientInfo:
return console_tools.ClientInfo('device', self.rpcs, self.client.client)
@@ -102,124 +155,10 @@ class Device:
"""Returns an object for accessing services on the specified channel."""
return next(iter(self.client.client.channels())).rpcs
- def run_tests(self, timeout_s: Optional[float] = 5) -> bool:
+ def run_tests(self, timeout_s: Optional[float] = 5) -> TestRecord:
"""Runs the unit tests on this device."""
return pw_unit_test_run_tests(self.rpcs, timeout_s=timeout_s)
- def listen_to_log_stream(self):
- """Opens a log RPC for the device's unrequested log stream.
-
- The RPCs remain open until the server cancels or closes them, either
- with a response or error packet.
- """
- self.rpcs.pw.log.Logs.Listen.open(
- on_next=lambda _, log_entries_proto: self._log_entries_proto_parser(
- log_entries_proto
- ),
- on_completed=lambda _, status: _LOG.info(
- 'Log stream completed with status: %s', status
- ),
- on_error=lambda _, error: self._handle_log_stream_error(error),
- )
-
- def _handle_log_stream_error(self, error: Status):
- """Resets the log stream RPC on error to avoid losing logs."""
- _LOG.error('Log stream error: %s', error)
-
- # Only re-request logs if the RPC was not cancelled by the client.
- if error != Status.CANCELLED:
- self.listen_to_log_stream()
-
- def _handle_log_drop_count(self, drop_count: int, reason: str):
- log_text = 'log' if drop_count == 1 else 'logs'
- message = f'Dropped {drop_count} {log_text} due to {reason}'
- self._emit_device_log(logging.WARNING, '', '', message)
-
- def _check_for_dropped_logs(self, log_entries_proto: log_pb2.LogEntries):
- # Count log messages received that don't use the dropped field.
- messages_received = sum(
- 1 if not log_proto.dropped else 0
- for log_proto in log_entries_proto.entries
- )
- dropped_log_count = (
- log_entries_proto.first_entry_sequence_id
- - self._expected_log_sequence_id
- )
- self._expected_log_sequence_id = (
- log_entries_proto.first_entry_sequence_id + messages_received
- )
- if dropped_log_count > 0:
- self._handle_log_drop_count(dropped_log_count, 'loss at transport')
- elif dropped_log_count < 0:
- _LOG.error('Log sequence ID is smaller than expected')
-
- def _log_entries_proto_parser(self, log_entries_proto: log_pb2.LogEntries):
- self._check_for_dropped_logs(log_entries_proto)
- for log_proto in log_entries_proto.entries:
- decoded_timestamp = self.decode_timestamp(log_proto.timestamp)
- # Parse level and convert to logging module level number.
- level = (log_proto.line_level & 0x7) * 10
- if self.detokenizer:
- message = str(
- decode_optionally_tokenized(
- self.detokenizer, log_proto.message
- )
- )
- else:
- message = log_proto.message.decode('utf-8')
- log = FormatStringWithMetadata(message)
-
- # Handle dropped count.
- if log_proto.dropped:
- drop_reason = (
- log_proto.message.decode('utf-8').lower()
- if log_proto.message
- else 'enqueue failure on device'
- )
- self._handle_log_drop_count(log_proto.dropped, drop_reason)
- continue
- self._emit_device_log(
- level,
- decoded_timestamp,
- log.module,
- log.message,
- **dict(log.fields),
- )
-
- def _emit_device_log(
- self,
- level: int,
- timestamp: str,
- module_name: str,
- message: str,
- **metadata_fields,
- ):
- # Fields used for console table view
- fields = metadata_fields
- fields['timestamp'] = timestamp
- fields['msg'] = message
- fields['module'] = module_name
-
- # Format used for file or stdout logging.
- self.logger.log(
- level,
- '%s %s%s',
- timestamp,
- f'{module_name} '.lstrip(),
- message,
- extra=dict(extra_metadata_fields=fields),
- )
-
- def decode_timestamp(self, timestamp: int) -> str:
- """Decodes timestamp to a human-readable value.
-
- Defaults to interpreting the input timestamp as nanoseconds since boot.
- Devices can override this to match their timestamp units.
- """
- if self.timestamp_decoder:
- return self.timestamp_decoder(timestamp)
- return str(datetime.timedelta(seconds=timestamp / 1e9))[:-3]
-
def get_and_log_metrics(self) -> dict:
"""Retrieves the parsed metrics and logs them to the console."""
metrics = metric_parser.parse_metrics(
@@ -238,9 +177,8 @@ class Device:
return metrics
def snapshot_peak_stack_usage(self, thread_name: Optional[str] = None):
- _, rsp = self.rpcs.pw.thread.ThreadSnapshotService.GetPeakStackUsage(
- name=thread_name
- )
+ snapshot_service = self.rpcs.pw.thread.proto.ThreadSnapshotService
+ _, rsp = snapshot_service.GetPeakStackUsage(name=thread_name)
thread_info = thread_pb2.SnapshotThreadInfo()
for thread_info_block in rsp:
diff --git a/pw_system/py/pw_system/device_tracing.py b/pw_system/py/pw_system/device_tracing.py
new file mode 100644
index 000000000..c97dabe08
--- /dev/null
+++ b/pw_system/py/pw_system/device_tracing.py
@@ -0,0 +1,162 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Device tracing classes to interact with targets via RPC."""
+
+import os
+import logging
+import tempfile
+
+# from pathlib import Path
+# from types import ModuleType
+# from typing import Callable, List, Optional, Union
+from typing import List, Optional
+
+import pw_transfer
+from pw_file import file_pb2
+from pw_rpc.callback_client.errors import RpcError
+from pw_system.device import Device
+from pw_trace import trace
+from pw_trace_tokenized import trace_tokenized
+
+_LOG = logging.getLogger('tracing')
+DEFAULT_TICKS_PER_SECOND = 1000
+
+
+class DeviceWithTracing(Device):
+ """Represents an RPC Client for a device running a Pigweed target with
+ tracing.
+
+ The target must have and RPC support for the following services:
+ - logging
+ - file
+ - transfer
+ - tracing
+ Note: use this class as a base for specialized device representations.
+ """
+
+ def __init__(self, ticks_per_second: Optional[int], *argv, **kargv):
+ super().__init__(*argv, **kargv)
+
+ # Create the transfer manager
+ self.transfer_service = self.rpcs.pw.transfer.Transfer
+ self.transfer_manager = pw_transfer.Manager(
+ self.transfer_service,
+ default_response_timeout_s=self.rpc_timeout_s,
+ initial_response_timeout_s=self.rpc_timeout_s,
+ default_protocol_version=pw_transfer.ProtocolVersion.LATEST,
+ )
+
+ if ticks_per_second:
+ self.ticks_per_second = ticks_per_second
+ else:
+ self.ticks_per_second = self.get_ticks_per_second()
+ _LOG.info('ticks_per_second set to %i', self.ticks_per_second)
+
+ def get_ticks_per_second(self) -> int:
+ trace_service = self.rpcs.pw.trace.proto.TraceService
+ try:
+ resp = trace_service.GetClockParameters()
+ if not resp.status.ok():
+ _LOG.error(
+ 'Failed to get clock parameters: %s. Using default \
+ value',
+ resp.status,
+ )
+ return DEFAULT_TICKS_PER_SECOND
+ except RpcError as rpc_err:
+ _LOG.exception('%s. Using default value', rpc_err)
+ return DEFAULT_TICKS_PER_SECOND
+
+ return resp.response.clock_parameters.tick_period_seconds_denominator
+
+ def list_files(self) -> List:
+ """Lists all files on this device."""
+ fs_service = self.rpcs.pw.file.FileSystem
+ stream_response = fs_service.List()
+
+ if not stream_response.status.ok():
+ _LOG.error('Failed to list files %s', stream_response.status)
+ return []
+
+ return stream_response.responses
+
+ def delete_file(self, path: str) -> bool:
+ """Delete a file on this device."""
+ fs_service = self.rpcs.pw.file.FileSystem
+ req = file_pb2.DeleteRequest(path=path)
+ stream_response = fs_service.Delete(req)
+ if not stream_response.status.ok():
+ _LOG.error(
+ 'Failed to delete file %s file: %s',
+ path,
+ stream_response.status,
+ )
+ return False
+
+ return True
+
+ def transfer_file(self, file_id: int, dest_path: str) -> bool:
+ """Transfer a file on this device to the host."""
+ try:
+ data = self.transfer_manager.read(file_id)
+ with open(dest_path, "wb") as bin_file:
+ bin_file.write(data)
+ except pw_transfer.Error:
+ _LOG.exception('Failed to transfer file_id %i', file_id)
+ return False
+
+ return True
+
+ def start_tracing(self) -> None:
+ """Turns on tracing on this device."""
+ trace_service = self.rpcs.pw.trace.proto.TraceService
+ trace_service.Start()
+
+ def stop_tracing(self, trace_output_path: str = "trace.json") -> None:
+ """Turns off tracing on this device and downloads the trace file."""
+ trace_service = self.rpcs.pw.trace.proto.TraceService
+ resp = trace_service.Stop()
+
+ # If there's no tokenizer, there's no need to transfer the trace
+ # file from the device after stopping tracing, as there's not much
+ # that can be done with it.
+ if not self.detokenizer:
+ _LOG.error('No tokenizer specified. Not transfering trace')
+ return
+
+ trace_bin_path = tempfile.NamedTemporaryFile(delete=False)
+ trace_bin_path.close()
+ try:
+ if not self.transfer_file(
+ resp.response.file_id, trace_bin_path.name
+ ):
+ return
+
+ with open(trace_bin_path.name, 'rb') as bin_file:
+ trace_data = bin_file.read()
+ events = trace_tokenized.get_trace_events(
+ [self.detokenizer.database],
+ trace_data,
+ self.ticks_per_second,
+ self.time_offset,
+ )
+ json_lines = trace.generate_trace_json(events)
+ trace_tokenized.save_trace_file(json_lines, trace_output_path)
+
+ _LOG.info(
+ 'Wrote trace file %s',
+ trace_output_path,
+ )
+ finally:
+ os.remove(trace_bin_path.name)
diff --git a/pw_system/py/pw_system/py.typed b/pw_system/py/pw_system/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_system/py/pw_system/py.typed
diff --git a/pw_system/py/pw_system/trace_client.py b/pw_system/py/pw_system/trace_client.py
new file mode 100644
index 000000000..575659b26
--- /dev/null
+++ b/pw_system/py/pw_system/trace_client.py
@@ -0,0 +1,211 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""
+Generates json trace files viewable using chrome://tracing using RPCs from a
+connected trace service.
+
+Example usage:
+python pw_console/py/pw_console/trace_client.py
+ -o trace.json
+ -t out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
+"""
+
+import argparse
+import logging
+import sys
+from pathlib import Path
+from types import ModuleType
+from typing import List, Union
+
+
+from pw_transfer import transfer_pb2
+from pw_log.proto import log_pb2
+from pw_trace_protos import trace_service_pb2
+from pw_trace import trace
+from pw_trace_tokenized import trace_tokenized
+import pw_transfer
+from pw_file import file_pb2
+from pw_hdlc import rpc
+from pw_system.device_tracing import DeviceWithTracing
+from pw_tokenizer.detokenize import AutoUpdatingDetokenizer
+from pw_console.socket_client import SocketClient
+
+
+_LOG = logging.getLogger('pw_console_trace_client')
+_LOG.level = logging.DEBUG
+_LOG.addHandler(logging.StreamHandler(sys.stdout))
+
+
+def start_tracing_on_device(client):
+ """Start tracing on the device"""
+ service = client.rpcs.pw.trace.proto.TraceService
+ service.Start()
+
+
+def stop_tracing_on_device(client):
+ """Stop tracing on the device"""
+ service = client.rpcs.pw.trace.proto.TraceService
+ return service.Stop()
+
+
+def list_files_on_device(client):
+ """List files on the device"""
+ service = client.rpcs.pw.file.FileSystem
+ return service.List()
+
+
+def delete_file_on_device(client, path):
+ """Delete a file on the device"""
+ service = client.rpcs.pw.file.FileSystem
+ req = file_pb2.DeleteRequest(path=path)
+ return service.Delete(req)
+
+
+def _parse_args():
+ """Parse and return command line arguments."""
+
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ group = parser.add_mutually_exclusive_group(required=False)
+ group.add_argument('-d', '--device', help='the serial port to use')
+ parser.add_argument(
+ '-b',
+ '--baudrate',
+ type=int,
+ default=115200,
+ help='the baud rate to use',
+ )
+ group.add_argument(
+ '-s',
+ '--socket-addr',
+ type=str,
+ default="default",
+ help='use socket to connect to server, type default for\
+ localhost:33000, or manually input the server address:port',
+ )
+ parser.add_argument(
+ '-o',
+ '--trace_output',
+ dest='trace_output_file',
+ help=('The json file to which to write the output.'),
+ )
+ parser.add_argument(
+ '-t',
+ '--trace_token_database',
+ help='Databases (ELF, binary, or CSV) to use to lookup trace tokens.',
+ )
+ parser.add_argument(
+ '-f',
+ '--ticks_per_second',
+ type=int,
+ dest='ticks_per_second',
+ help=('The clock rate of the trace events.'),
+ )
+ parser.add_argument(
+ '--time_offset',
+ type=int,
+ dest='time_offset',
+ default=0,
+ help=('Time offset (us) of the trace events (Default 0).'),
+ )
+ parser.add_argument(
+ '--channel-id',
+ type=int,
+ dest='channel_id',
+ default=rpc.DEFAULT_CHANNEL_ID,
+ help="Channel ID used in RPC communications.",
+ )
+ return parser.parse_args()
+
+
+def _main(args) -> int:
+ detokenizer = AutoUpdatingDetokenizer(args.trace_token_database + "#trace")
+ detokenizer.show_errors = True
+
+ socket_impl = SocketClient
+ try:
+ socket_device = socket_impl(args.socket_addr)
+ reader = rpc.SelectableReader(socket_device)
+ write = socket_device.write
+ except ValueError:
+ _LOG.exception('Failed to initialize socket at %s', args.socket_addr)
+ return 1
+
+ protos: List[Union[ModuleType, Path]] = [
+ log_pb2,
+ file_pb2,
+ transfer_pb2,
+ trace_service_pb2,
+ ]
+
+ with reader:
+ device_client = DeviceWithTracing(
+ args.ticks_per_second,
+ args.channel_id,
+ reader,
+ write,
+ protos,
+ detokenizer=detokenizer,
+ timestamp_decoder=None,
+ rpc_timeout_s=5,
+ use_rpc_logging=True,
+ use_hdlc_encoding=True,
+ )
+
+ with device_client:
+ _LOG.info("Starting tracing")
+ start_tracing_on_device(device_client)
+
+ _LOG.info("Stopping tracing")
+ file_id = stop_tracing_on_device(device_client)
+ _LOG.info("Trace file id = %d", file_id.response.file_id)
+
+ _LOG.info("Listing Files")
+ stream_response = list_files_on_device(device_client)
+
+ if not stream_response.status.ok():
+ _LOG.error('Failed to list files %s', stream_response.status)
+ return 1
+
+ for list_response in stream_response.responses:
+ for file in list_response.paths:
+ _LOG.info("Transfering File: %s", file.path)
+ try:
+ data = device_client.transfer_manager.read(file.file_id)
+ events = trace_tokenized.get_trace_events(
+ [detokenizer.database],
+ data,
+ device_client.ticks_per_second,
+ args.time_offset,
+ )
+ json_lines = trace.generate_trace_json(events)
+ trace_tokenized.save_trace_file(
+ json_lines, args.trace_output_file
+ )
+ except pw_transfer.Error as err:
+ print('Failed to read:', err.status)
+
+ _LOG.info("Deleting File: %s", file.path)
+ delete_file_on_device(device_client, file.path)
+
+ _LOG.info("All trace transfers completed successfully")
+
+ return 0
+
+
+if __name__ == '__main__':
+ _main(_parse_args())
diff --git a/pw_system/py/setup.py b/pw_system/py/setup.py
deleted file mode 100644
index 7308b8c47..000000000
--- a/pw_system/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_console"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_system/stl_backends.gni b/pw_system/stl_backends.gni
index 06d901ebd..f8d60a89e 100644
--- a/pw_system/stl_backends.gni
+++ b/pw_system/stl_backends.gni
@@ -36,4 +36,5 @@ PW_SYSTEM_STL_BACKENDS = {
pw_thread_THREAD_ITERATION_BACKEND = "$dir_pw_thread_stl:thread_iteration"
pw_thread_YIELD_BACKEND = "$dir_pw_thread_stl:yield"
pw_system_TARGET_HOOKS_BACKEND = "$dir_pw_system:stl_target_hooks"
+ pw_trace_tokenizer_time = "$dir_pw_trace_tokenized:host_trace_time"
}
diff --git a/pw_system/stl_target_hooks.cc b/pw_system/stl_target_hooks.cc
index 048153d4c..e470a97af 100644
--- a/pw_system/stl_target_hooks.cc
+++ b/pw_system/stl_target_hooks.cc
@@ -12,6 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include "pw_system/config.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
@@ -27,6 +28,13 @@ const thread::Options& RpcThreadOptions() {
return rpc_thread_options;
}
+#if PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+const thread::Options& TransferThreadOptions() {
+ static thread::stl::Options transfer_thread_options;
+ return transfer_thread_options;
+}
+#endif // PW_SYSTEM_ENABLE_TRANSFER_SERVICE
+
const thread::Options& WorkQueueThreadOptions() {
static thread::stl::Options work_queue_thread_options;
return work_queue_thread_options;
diff --git a/pw_system/system_target.gni b/pw_system/system_target.gni
index 6000f1c8c..55dbcf1b7 100644
--- a/pw_system/system_target.gni
+++ b/pw_system/system_target.gni
@@ -30,6 +30,8 @@ import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
import("$dir_pw_toolchain/host_gcc/toolchains.gni")
+import("$dir_pw_trace/backend.gni")
+import("$dir_pw_trace_tokenized/config.gni")
import("$dir_pw_unit_test/test.gni")
import("backend.gni")
import("freertos_backends.gni")
@@ -41,6 +43,7 @@ PW_SYSTEM_CPU = {
CORTEX_M4F = "cortex-m4f"
CORTEX_M3 = "cortex-m3"
CORTEX_M7F = "cortex-m7f"
+ CORTEX_M33 = "cortex-m33"
# Native builds for the host CPU.
NATIVE = "native"
@@ -56,12 +59,25 @@ PW_SYSTEM_SCHEDULER = {
NATIVE = "native"
}
+declare_args() {
+ # This argument is intended to be user-facing and should NOT be set by a
+ # toolchain. This switches ALL pw_system_target toolchains to use the
+ # multi_endpoint_rpc_config config to illustrate a multi-endpoint mode that
+ # isolates logging and RPC traffic via HDLC multiplexing.
+ #
+ # If you would like to use this in production, it is strongly recommended that
+ # you instead just add the appropriate defines to your target's toolchain
+ # definition.
+ pw_system_USE_MULTI_ENDPOINT_CONFIG = false
+}
+
# Defines a target toolchain, automatically setting many required build
# arguments to simplify instantiation of a target.
#
# Args:
# cpu: (required) The architecture to target.
-# Supported choices: PW_SYSTEM_CPU.CORTEX_M7F, PW_SYSTEM_CPU.CORTEX_M4F, PW_SYSTEM_CPU.CORTEX_M3, PW_SYSTEM_CPU.NATIVE
+# Supported choices: PW_SYSTEM_CPU.CORTEX_M7F, PW_SYSTEM_CPU.CORTEX_M4F, PW_SYSTEM_CPU.CORTEX_M3,
+# PW_SYSTEM_CPU.CORTEX_M33, PW_SYSTEM_CPU.NATIVE
# scheduler: (required) The scheduler implementation and API to use for this
# target.
# Supported choices: PW_SYSTEM_SCHEDULER.FREERTOS, PW_SYSTEM_SCHEDULER.NATIVE
@@ -111,13 +127,51 @@ template("pw_system_target") {
# TODO(amontanez): This should be set to a "$dir_pw_unit_test:rpc_main"
# when RPC is working.
pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+
+ pw_trace_BACKEND = "$dir_pw_trace_tokenized"
+
+ if (pw_system_USE_MULTI_ENDPOINT_CONFIG) {
+ pw_system_CONFIG = "$dir_pw_system:multi_endpoint_rpc_config"
+ }
}
# Populate architecture-specific build args.
assert(
defined(invoker.cpu),
- "Please select a `cpu` for $target_name. Options: PW_SYSTEM_CPU.CORTEX_M7, PW_SYSTEM_CPU.CORTEX_M4F, PW_SYSTEM_CPU.CORTEX_M3, PW_SYSTEM_CPU.NATIVE")
- if (invoker.cpu == PW_SYSTEM_CPU.CORTEX_M7F) {
+ "Please select a `cpu` for $target_name. Options: PW_SYSTEM_CPU.CORTEX_M7, PW_SYSTEM_CPU.CORTEX_M4F, PW_SYSTEM_CPU.CORTEX_M3, PW_SYSTEM_CPU.CORTEX_M33, PW_SYSTEM_CPU.NATIVE")
+ if (invoker.cpu == PW_SYSTEM_CPU.CORTEX_M33) {
+ _current_cpu = "arm"
+ _default_configs += [ "$dir_pw_toolchain/arm_gcc:enable_float_printf" ]
+ _arch_build_args = {
+ pw_bloat_BLOATY_CONFIG = "$dir_pw_boot_cortex_m/bloaty_config.bloaty"
+ pw_boot_BACKEND = "$dir_pw_boot_cortex_m:pw_boot_cortex_m"
+ pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context"
+ }
+ _link_deps += [ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support" ]
+
+ if (defined(invoker.system_toolchain)) {
+ _system_toolchain = invoker.system_toolchain
+ } else {
+ _system_toolchain = pw_toolchain_arm_gcc
+ }
+
+ _final_binary_extension = ".elf"
+
+ _toolchains = [
+ {
+ toolchain_base = _system_toolchain.cortex_m33_debug
+ level_name = _OPTIMIZATION_LEVELS.DEBUG
+ },
+ {
+ toolchain_base = _system_toolchain.cortex_m33_size_optimized
+ level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
+ },
+ {
+ toolchain_base = _system_toolchain.cortex_m33_speed_optimized
+ level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
+ },
+ ]
+ } else if (invoker.cpu == PW_SYSTEM_CPU.CORTEX_M7F) {
_current_cpu = "arm"
_default_configs += [ "$dir_pw_toolchain/arm_gcc:enable_float_printf" ]
_arch_build_args = {
@@ -125,6 +179,7 @@ template("pw_system_target") {
pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv7m"
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ _link_deps += [ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support" ]
if (defined(invoker.system_toolchain)) {
_system_toolchain = invoker.system_toolchain
@@ -156,6 +211,7 @@ template("pw_system_target") {
pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv7m"
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ _link_deps += [ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support" ]
if (defined(invoker.system_toolchain)) {
_system_toolchain = invoker.system_toolchain
@@ -186,6 +242,7 @@ template("pw_system_target") {
pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv7m"
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ _link_deps += [ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support" ]
if (defined(invoker.system_toolchain)) {
_system_toolchain = invoker.system_toolchain
@@ -216,6 +273,7 @@ template("pw_system_target") {
pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv7m"
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ _link_deps += [ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support" ]
if (defined(invoker.system_toolchain)) {
_system_toolchain = invoker.system_toolchain
diff --git a/pw_system/trace_service.cc b/pw_system/trace_service.cc
new file mode 100644
index 000000000..c0a92dcba
--- /dev/null
+++ b/pw_system/trace_service.cc
@@ -0,0 +1,40 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_system/trace_service.h"
+
+#include "pw_trace_tokenized/trace_service_pwpb.h"
+#include "pw_trace_tokenized/trace_tokenized.h"
+
+namespace pw::system {
+namespace {
+
+// TODO: b/305795949 - place trace_data in a persistent region of memory
+TracePersistentBuffer trace_data;
+persistent_ram::PersistentBufferWriter trace_data_writer(
+ trace_data.GetWriter());
+
+trace::TraceService trace_service(trace::GetTokenizedTracer(),
+ trace_data_writer);
+
+} // namespace
+
+TracePersistentBuffer& GetTraceData() { return trace_data; }
+
+void RegisterTraceService(rpc::Server& rpc_server, uint32_t transfer_id) {
+ rpc_server.RegisterService(trace_service);
+ trace_service.SetTransferId(transfer_id);
+}
+
+} // namespace pw::system
diff --git a/pw_system/transfer_handlers.cc b/pw_system/transfer_handlers.cc
new file mode 100644
index 000000000..2737f8e8f
--- /dev/null
+++ b/pw_system/transfer_handlers.cc
@@ -0,0 +1,39 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_system/transfer_handlers.h"
+
+namespace pw::system {
+
+TracePersistentBufferTransfer::TracePersistentBufferTransfer(
+ uint32_t id, TracePersistentBuffer& persistent_buffer)
+ : transfer::ReadOnlyHandler(id),
+ persistent_buffer_(persistent_buffer),
+ reader_({}) {
+ set_reader(reader_);
+}
+
+Status TracePersistentBufferTransfer::PrepareRead() {
+ if (!persistent_buffer_.has_value()) {
+ return Status::Unavailable();
+ }
+
+ // As seeking is not yet supported, reinitialize the reader from the start
+ // of the snapshot buffer.
+ new (&reader_) stream::MemoryReader(
+ pw::ConstByteSpan{persistent_buffer_.data(), persistent_buffer_.size()});
+
+ return OkStatus();
+}
+
+} // namespace pw::system
diff --git a/pw_system/transfer_service.cc b/pw_system/transfer_service.cc
new file mode 100644
index 000000000..7a68376cc
--- /dev/null
+++ b/pw_system/transfer_service.cc
@@ -0,0 +1,68 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_system/transfer_service.h"
+
+#include "pw_system/file_manager.h"
+
+namespace pw::system {
+namespace {
+// The maximum number of concurrent transfers the thread should support as
+// either a client or a server. These can be set to 0 (if only using one or
+// the other).
+constexpr size_t kMaxConcurrentClientTransfers = 5;
+constexpr size_t kMaxConcurrentServerTransfers = 3;
+
+// The maximum payload size that can be transmitted by the system's
+// transport stack. This would typically be defined within some transport
+// header.
+constexpr size_t kMaxTransmissionUnit = 512;
+
+// The maximum amount of data that should be sent within a single transfer
+// packet. By necessity, this should be less than the max transmission unit.
+//
+// pw_transfer requires some additional per-packet overhead, so the actual
+// amount of data it sends may be lower than this.
+constexpr size_t kMaxTransferChunkSizeBytes = 480;
+
+// In a write transfer, the maximum number of bytes to receive at one time
+// (potentially across multiple chunks), unless specified otherwise by the
+// transfer handler's stream::Writer.
+constexpr size_t kDefaultMaxBytesToReceive = 1024;
+
+// Buffers for storing and encoding chunks (see documentation above).
+std::array<std::byte, kMaxTransferChunkSizeBytes> chunk_buffer;
+std::array<std::byte, kMaxTransmissionUnit> encode_buffer;
+
+transfer::Thread<kMaxConcurrentClientTransfers, kMaxConcurrentServerTransfers>
+ transfer_thread(chunk_buffer, encode_buffer);
+
+transfer::TransferService transfer_service(transfer_thread,
+ kDefaultMaxBytesToReceive);
+} // namespace
+
+void RegisterTransferService(rpc::Server& rpc_server) {
+ rpc_server.RegisterService(transfer_service);
+}
+
+void InitTransferService() {
+ // the handlers need to be registered after the transfer thread has started
+ for (auto handler : GetFileManager().GetTransferHandlers()) {
+ transfer_service.RegisterHandler(*handler);
+ }
+}
+
+transfer::TransferThread& GetTransferThread() { return transfer_thread; }
+
+} // namespace pw::system
diff --git a/pw_system/work_queue.cc b/pw_system/work_queue.cc
index 4c680e285..e754dbb00 100644
--- a/pw_system/work_queue.cc
+++ b/pw_system/work_queue.cc
@@ -19,7 +19,7 @@
namespace pw::system {
-// TODO(b/234876895): Consider switching this to a "NoDestroy" wrapped type to
+// TODO: b/234876895 - Consider switching this to a "NoDestroy" wrapped type to
// allow the static destructor to be optimized out.
work_queue::WorkQueue& GetWorkQueue() {
static constexpr size_t kMaxWorkQueueEntries =
diff --git a/pw_system/zephyr_target_hooks.cc b/pw_system/zephyr_target_hooks.cc
new file mode 100644
index 000000000..3566d2713
--- /dev/null
+++ b/pw_system/zephyr_target_hooks.cc
@@ -0,0 +1,67 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_thread/thread.h"
+#include "pw_thread_zephyr/config.h"
+#include "pw_thread_zephyr/options.h"
+
+namespace pw::system {
+
+using namespace pw::thread::zephyr::config;
+
+// Low to high priorities.
+enum class ThreadPriority : int {
+ kWorkQueue = kDefaultPriority,
+ // TODO(amontanez): These should ideally be at different priority levels, but
+ // there's synchronization issues when they are.
+ kLog = kWorkQueue,
+ kRpc = kWorkQueue,
+ kNumPriorities,
+};
+
+static constexpr size_t kLogThreadStackWords =
+ CONFIG_PIGWEED_SYSTEM_TARGET_HOOKS_LOG_STACK_SIZE;
+static thread::zephyr::StaticContextWithStack<kLogThreadStackWords>
+ log_thread_context;
+const thread::Options& LogThreadOptions() {
+ static constexpr auto options =
+ pw::thread::zephyr::Options(log_thread_context)
+ .set_priority(static_cast<int>(ThreadPriority::kLog));
+ return options;
+}
+
+static constexpr size_t kRpcThreadStackWords =
+ CONFIG_PIGWEED_SYSTEM_TARGET_HOOKS_RPC_STACK_SIZE;
+static thread::zephyr::StaticContextWithStack<kRpcThreadStackWords>
+ rpc_thread_context;
+const thread::Options& RpcThreadOptions() {
+ static constexpr auto options =
+ pw::thread::zephyr::Options(rpc_thread_context)
+ .set_priority(static_cast<int>(ThreadPriority::kRpc));
+ return options;
+}
+
+static constexpr size_t kWorkQueueThreadStackWords =
+ CONFIG_PIGWEED_SYSTEM_TARGET_HOOKS_WORK_QUEUE_STACK_SIZE;
+static thread::zephyr::StaticContextWithStack<kWorkQueueThreadStackWords>
+ work_queue_thread_context;
+
+const thread::Options& WorkQueueThreadOptions() {
+ static constexpr auto options =
+ pw::thread::zephyr::Options(work_queue_thread_context)
+ .set_priority(static_cast<int>(ThreadPriority::kWorkQueue));
+ return options;
+}
+
+} // namespace pw::system \ No newline at end of file
diff --git a/pw_target_runner/docs.rst b/pw_target_runner/docs.rst
index 25b210dac..17efcc80d 100644
--- a/pw_target_runner/docs.rst
+++ b/pw_target_runner/docs.rst
@@ -51,7 +51,7 @@ STM32F429I Discovery board with a specified serial number.
**server_config.txt**
-.. code:: text
+.. code-block:: text
runner {
command: "stm32f429i_disc1_unit_test_runner"
@@ -75,7 +75,7 @@ Running the server
To start the standalone server, run the ``pw_target_runner_server`` program and
point it to your config file.
-.. code:: text
+.. code-block:: text
$ pw_target_runner_server -config server_config.txt -port 8080
@@ -85,7 +85,7 @@ Sending requests
To request the server to run an executable, run the ``pw_target_runner_client``,
specifying the path to the executable through a ``-binary`` option.
-.. code:: text
+.. code-block:: text
$ pw_target_runner_client -host localhost -port 8080 -binary /path/to/my/test.elf
diff --git a/pw_thread/Android.bp b/pw_thread/Android.bp
new file mode 100644
index 000000000..d4cb01ffb
--- /dev/null
+++ b/pw_thread/Android.bp
@@ -0,0 +1,55 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_thread_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+filegroup {
+ name: "pw_thread_src_files",
+ srcs: [
+ "sleep.cc",
+ "thread.cc",
+ "yield.cc",
+ ],
+}
+
+cc_defaults {
+ name: "pw_thread_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_thread_include_dirs",
+ "pw_function_headers",
+ "pw_preprocessor_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_thread_include_dirs",
+ ],
+ static_libs: [
+ "pw_status",
+ ],
+ srcs: [
+ ":pw_thread_src_files"
+ ],
+}
diff --git a/pw_thread/BUILD.bazel b/pw_thread/BUILD.bazel
index c54848a8f..4b4052bca 100644
--- a/pw_thread/BUILD.bazel
+++ b/pw_thread/BUILD.bazel
@@ -19,7 +19,7 @@ load(
"pw_cc_test",
)
load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -35,15 +35,18 @@ pw_cc_facade(
pw_cc_library(
name = "id",
+ hdrs = [
+ "public/pw_thread/id.h",
+ ],
+ includes = ["public"],
deps = [
- ":id_facade",
- "@pigweed_config//:pw_thread_id_backend",
+ "@pigweed//targets:pw_thread_id_backend",
],
)
pw_cc_library(
name = "id_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_thread_embos:id"],
"//pw_build/constraints/rtos:freertos": ["//pw_thread_freertos:id"],
@@ -80,15 +83,21 @@ pw_cc_facade(
pw_cc_library(
name = "thread_iteration",
+ hdrs = [
+ "public/pw_thread/thread_iteration.h",
+ ],
+ includes = ["public"],
deps = [
- ":thread_iteration_facade",
- "@pigweed_config//:pw_thread_iteration_backend",
+ ":thread_info",
+ "//pw_function",
+ "//pw_status",
+ "@pigweed//targets:pw_thread_iteration_backend",
],
)
pw_cc_library(
name = "iteration_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_thread_embos:thread_iteration"],
"//pw_build/constraints/rtos:freertos": ["//pw_thread_freertos:thread_iteration"],
@@ -114,16 +123,21 @@ pw_cc_library(
srcs = [
"sleep.cc",
],
+ hdrs = [
+ "public/pw_thread/sleep.h",
+ ],
+ includes = ["public"],
deps = [
":id",
- ":sleep_facade",
- "@pigweed_config//:pw_thread_sleep_backend",
+ "//pw_chrono:system_clock",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_thread_sleep_backend",
],
)
pw_cc_library(
name = "sleep_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_thread_embos:sleep"],
"//pw_build/constraints/rtos:freertos": ["//pw_thread_freertos:sleep"],
@@ -150,19 +164,22 @@ pw_cc_library(
srcs = [
"thread.cc",
],
- hdrs = ["public/pw_thread/config.h"],
+ hdrs = [
+ "public/pw_thread/config.h",
+ "public/pw_thread/detached_thread.h",
+ "public/pw_thread/thread.h",
+ ],
includes = ["public"],
deps = [
":id",
":thread_core",
- ":thread_facade",
- "@pigweed_config//:pw_thread_thread_backend",
+ "@pigweed//targets:pw_thread_thread_backend",
],
)
pw_cc_library(
name = "thread_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_thread_embos:thread"],
"//pw_build/constraints/rtos:freertos": ["//pw_thread_freertos:thread"],
@@ -207,6 +224,33 @@ pw_cc_library(
],
)
+pw_cc_library(
+ name = "test_thread_context_backend_multiplexer",
+ visibility = ["@pigweed//targets:__pkg__"],
+ deps = select({
+ "//conditions:default": ["//pw_thread_stl:test_thread_context"],
+ }),
+)
+
+pw_cc_facade(
+ name = "test_thread_context_facade",
+ hdrs = [
+ "public/pw_thread/test_thread_context.h",
+ ],
+ includes = ["public"],
+)
+
+pw_cc_library(
+ name = "test_thread_context",
+ hdrs = [
+ "public/pw_thread/test_thread_context.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "@pigweed//targets:pw_thread_test_thread_context_backend",
+ ],
+)
+
pw_cc_facade(
name = "yield_facade",
hdrs = [
@@ -223,16 +267,20 @@ pw_cc_library(
srcs = [
"yield.cc",
],
+ hdrs = [
+ "public/pw_thread/yield.h",
+ ],
+ includes = ["public"],
deps = [
":id",
- ":yield_facade",
- "@pigweed_config//:pw_thread_yield_backend",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_thread_yield_backend",
],
)
pw_cc_library(
name = "yield_backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = select({
"//pw_build/constraints/rtos:embos": ["//pw_thread_embos:yield"],
"//pw_build/constraints/rtos:freertos": ["//pw_thread_freertos:yield"],
@@ -261,9 +309,9 @@ pw_cc_library(
)
pw_cc_library(
- name = "test_threads_header",
+ name = "non_portable_test_thread_options",
hdrs = [
- "public/pw_thread/test_threads.h",
+ "public/pw_thread/non_portable_test_thread_options.h",
],
deps = [
":thread",
@@ -271,8 +319,8 @@ pw_cc_library(
)
# To instantiate this as a pw_cc_test, depend on this pw_cc_library and the
-# pw_cc_library which implements the backend for test_threads_header. See
-# //pw_thread:thread_backend_test as an example.
+# pw_cc_library which implements the backend for non_portable_test_thread_options. See
+# //pw_thread_stl:thread_backend_test as an example.
pw_cc_library(
name = "thread_facade_test",
srcs = [
@@ -280,7 +328,7 @@ pw_cc_library(
],
deps = [
":id",
- ":test_threads_header",
+ ":non_portable_test_thread_options",
":thread",
"//pw_chrono:system_clock",
"//pw_sync:binary_semaphore",
@@ -289,6 +337,19 @@ pw_cc_library(
)
pw_cc_test(
+ name = "test_thread_context_facade_test",
+ srcs = [
+ "test_thread_context_facade_test.cc",
+ ],
+ deps = [
+ ":test_thread_context",
+ ":thread",
+ "//pw_sync:binary_semaphore",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "id_facade_test",
srcs = [
"id_facade_test.cc",
@@ -364,7 +425,7 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "thread_proto_py_pb2",
tags = ["manual"],
@@ -385,7 +446,7 @@ pw_proto_library(
deps = [":thread_snapshot_service_proto"],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "thread_snapshot_service_py_pb2",
tags = ["manual"],
diff --git a/pw_thread/BUILD.gn b/pw_thread/BUILD.gn
index 011e465fd..5eb542505 100644
--- a/pw_thread/BUILD.gn
+++ b/pw_thread/BUILD.gn
@@ -72,6 +72,12 @@ pw_facade("thread") {
sources = [ "thread.cc" ]
}
+pw_facade("test_thread_context") {
+ backend = pw_thread_TEST_THREAD_CONTEXT_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_thread/test_thread_context.h" ]
+}
+
pw_source_set("thread_core") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_thread/thread_core.h" ]
@@ -151,10 +157,21 @@ pw_test_group("tests") {
":sleep_facade_test",
":thread_info_test",
":yield_facade_test",
+ ":test_thread_context_facade_test",
":thread_snapshot_service_test",
]
}
+pw_test("test_thread_context_facade_test") {
+ enable_if = pw_thread_TEST_THREAD_CONTEXT_BACKEND != ""
+ sources = [ "test_thread_context_facade_test.cc" ]
+ deps = [
+ ":test_thread_context",
+ ":thread",
+ "$dir_pw_sync:binary_semaphore",
+ ]
+}
+
pw_test("id_facade_test") {
enable_if = pw_thread_ID_BACKEND != ""
sources = [ "id_facade_test.cc" ]
@@ -191,9 +208,9 @@ pw_test("sleep_facade_test") {
]
}
-pw_source_set("test_threads") {
+pw_source_set("non_portable_test_thread_options") {
public_configs = [ ":public_include_path" ]
- public = [ "public/pw_thread/test_threads.h" ]
+ public = [ "public/pw_thread/non_portable_test_thread_options.h" ]
public_deps = [ ":thread" ]
}
@@ -202,11 +219,12 @@ pw_source_set("test_threads") {
# pw_source_set and a pw_source_set which provides the implementation of
# test_threads. See "$dir_pw_thread_stl:thread_backend_test" as an example.
pw_source_set("thread_facade_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "thread_facade_test.cc" ]
deps = [
":id",
+ ":non_portable_test_thread_options",
":sleep",
- ":test_threads",
":thread",
"$dir_pw_sync:binary_semaphore",
dir_pw_unit_test,
@@ -242,5 +260,7 @@ pw_proto_library("protos") {
}
pw_doc_group("docs") {
+ inputs =
+ [ "$dir_pw_thread_stl/public/pw_thread_stl/test_thread_context_native.h" ]
sources = [ "docs.rst" ]
}
diff --git a/pw_thread/CMakeLists.txt b/pw_thread/CMakeLists.txt
index dc16136bd..db1328c58 100644
--- a/pw_thread/CMakeLists.txt
+++ b/pw_thread/CMakeLists.txt
@@ -71,6 +71,65 @@ pw_add_library(pw_thread.thread_core INTERFACE
public
)
+pw_add_library(pw_thread.thread_info INTERFACE
+ HEADERS
+ public/pw_thread/thread_info.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_span
+)
+
+pw_add_facade(pw_thread.thread_iteration INTERFACE
+ BACKEND
+ pw_thread.thread_iteration_BACKEND
+ HEADERS
+ public/pw_thread/thread_iteration.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_thread.thread_info
+ pw_function
+ pw_status
+)
+
+pw_proto_library(pw_thread.thread_snapshot_service_cc
+ SOURCES
+ pw_thread_protos/thread_snapshot_service.proto
+ DEPS
+ pw_thread.protos
+)
+
+pw_add_library(pw_thread.thread_snapshot_service STATIC
+ HEADERS
+ public/pw_thread/thread_snapshot_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_protobuf
+ pw_rpc.raw.server_api
+ pw_span
+ pw_status
+ pw_thread.config
+ pw_thread.protos.pwpb
+ pw_thread.thread_info
+ pw_thread.thread_iteration
+ pw_thread.thread_snapshot_service_cc.pwpb
+ pw_thread.thread_snapshot_service_cc.raw_rpc
+ SOURCES
+ pw_thread_private/thread_snapshot_service.h
+ thread_snapshot_service.cc
+)
+
+pw_add_facade(pw_thread.test_thread_context INTERFACE
+ BACKEND
+ pw_thread.test_thread_context_BACKEND
+ HEADERS
+ public/pw_thread/test_thread_context.h
+ PUBLIC_INCLUDES
+ public
+)
+
pw_add_facade(pw_thread.yield STATIC
BACKEND
pw_thread.yield_BACKEND
@@ -137,9 +196,9 @@ if((NOT "${pw_thread.id_BACKEND}" STREQUAL "") AND
)
endif()
-pw_add_library(pw_thread.test_threads INTERFACE
+pw_add_library(pw_thread.non_portable_test_thread_options INTERFACE
HEADERS
- public/pw_thread/test_threads.h
+ public/pw_thread/non_portable_test_thread_options.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
@@ -156,7 +215,7 @@ pw_add_library(pw_thread.thread_facade_test STATIC
PRIVATE_DEPS
pw_thread.id
pw_thread.sleep
- pw_thread.test_threads
+ pw_thread.non_portable_test_thread_options
pw_thread.thread
pw_sync.binary_semaphore
pw_unit_test
@@ -176,3 +235,14 @@ if((NOT "${pw_thread.id_BACKEND}" STREQUAL "") AND
pw_thread
)
endif()
+
+if(NOT "${pw_thread.test_thread_context_BACKEND}" STREQUAL "")
+ pw_add_test(pw_thread.test_thread_context_facade_test
+ SOURCES
+ test_thread_context_facade_test.cc
+ PRIVATE_DEPS
+ pw_thread.test_thread_context
+ pw_thread.thread
+ pw_sync.binary_semaphore
+ )
+endif()
diff --git a/pw_thread/backend.cmake b/pw_thread/backend.cmake
index 8928f052a..c93b9c8ce 100644
--- a/pw_thread/backend.cmake
+++ b/pw_thread/backend.cmake
@@ -24,5 +24,11 @@ pw_add_backend_variable(pw_thread.sleep_BACKEND)
# Backend for the pw_thread module's pw::thread::Thread to create threads.
pw_add_backend_variable(pw_thread.thread_BACKEND)
+# Backend for the pw_thread module's pw::thread::thread_iteration.
+pw_add_backend_variable(pw_thread.thread_iteration_BACKEND)
+
# Backend for the pw_thread module's pw::thread::yield.
pw_add_backend_variable(pw_thread.yield_BACKEND)
+
+# Backend for the pw_thread module's pw::thread::test_thread_context.
+pw_add_backend_variable(pw_thread.test_thread_context_BACKEND)
diff --git a/pw_thread/backend.gni b/pw_thread/backend.gni
index c104808b7..a534b3b48 100644
--- a/pw_thread/backend.gni
+++ b/pw_thread/backend.gni
@@ -25,6 +25,9 @@ declare_args() {
# Backend for the pw_thread module's pw::thread::yield.
pw_thread_YIELD_BACKEND = ""
+ # Backend for the pw_thread module's pw::thread::test_thread_context.
+ pw_thread_TEST_THREAD_CONTEXT_BACKEND = ""
+
# Whether the GN asserts should be silenced in ensuring that a compatible
# backend for pw_chrono_SYSTEM_CLOCK_BACKEND is chosen.
# Set to true to disable the asserts.
diff --git a/pw_thread/docs.rst b/pw_thread/docs.rst
index f68c8136b..dcc9f0336 100644
--- a/pw_thread/docs.rst
+++ b/pw_thread/docs.rst
@@ -162,7 +162,7 @@ thread's context. Unlike ``std::thread``, the API requires
``pw::thread::ThreadCore`` objects and functions which match the
``pw::thread::Thread::ThreadRoutine`` signature.
-We recognize that the C++11's STL ``std::thread``` API has some drawbacks where
+We recognize that the C++11's STL ``std::thread`` API has some drawbacks where
it is easy to forget to join or detach the thread handle. Because of this, we
offer helper wrappers like the ``pw::thread::DetachedThread``. Soon we will
extend this by also adding a ``pw::thread::JoiningThread`` helper wrapper which
@@ -329,7 +329,6 @@ something like:
} // namespace my_app
-
Detaching & Joining
===================
The ``Thread::detach()`` API is always available, to let you separate the
@@ -374,7 +373,7 @@ match the Thread constuctor arguments for creating a thread of execution.
ThreadRoutine & ThreadCore
==========================
Threads must either be invoked through a
-``pw::thread::Thread::ThreadRoutine``` style function or implement the
+``pw::thread::Thread::ThreadRoutine`` style function or implement the
``pw::thread::ThreadCore`` interface.
.. code-block:: cpp
@@ -452,6 +451,19 @@ function without arguments. For example:
implements the ThreadCore MUST meet or exceed the lifetime of its thread of
execution!
+-------------------------
+Unit testing with threads
+-------------------------
+.. doxygenclass:: pw::thread::test::TestThreadContext
+ :members:
+
+As an example, the STL :cpp:class:`TestThreadContext` backend implementation in
+``test_thread_context_native.h`` is shown below.
+
+.. literalinclude:: ../pw_thread_stl/public/pw_thread_stl/test_thread_context_native.h
+ :language: cpp
+ :lines: 18-36
+
----------------
Thread Iteration
----------------
@@ -498,7 +510,7 @@ To expose a ``ThreadSnapshotService`` in your application, do the following:
For example:
-.. code::
+.. code-block::
#include "pw_rpc/server.h"
#include "pw_thread/thread_snapshot_service.h"
diff --git a/pw_thread/public/pw_thread/config.h b/pw_thread/public/pw_thread/config.h
index 7b83a7eca..e8dbde11f 100644
--- a/pw_thread/public/pw_thread/config.h
+++ b/pw_thread/public/pw_thread/config.h
@@ -26,4 +26,4 @@
// The max number of threads to bundle by default for thread snapshot service.
#ifndef PW_THREAD_NUM_BUNDLED_THREADS
#define PW_THREAD_NUM_BUNDLED_THREADS 3
-#endif // PW_THREAD_MAXIMUM_THREADS \ No newline at end of file
+#endif // PW_THREAD_MAXIMUM_THREADS
diff --git a/pw_thread/public/pw_thread/non_portable_test_thread_options.h b/pw_thread/public/pw_thread/non_portable_test_thread_options.h
new file mode 100644
index 000000000..48277b543
--- /dev/null
+++ b/pw_thread/public/pw_thread/non_portable_test_thread_options.h
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// IMPORTANT: DO NOT USE THIS HEADER FOR UNIT TESTS!!!
+//
+// Instead, use pw::thread::test::TestThreadContext in
+// pw_thread/test_thread_context.h. See
+// https://pigweed.dev/pw_thread#unit-testing-with-threads.
+//
+// pw_thread/non_portable_test_thread_options.h is not a facade. Code written
+// against it is not portable. It was created for testing of pw_thread itself,
+// so threads with different configurations can be instantiated in tests.
+#pragma once
+
+#include "pw_thread/thread.h"
+
+namespace pw::thread::test {
+
+// Two test threads are used to verify the thread facade.
+//
+// These functions are NOT part of a facade! They are used to allocate thread
+// options for testing pw_thread backends only. Multiple variations of these
+// functions may be instantiated within a single toolchain for testing purposes.
+// Do NOT use unless absolutely necessary. Instead, use the TestThreadContext
+// facade for unit tests.
+const Options& TestOptionsThread0();
+const Options& TestOptionsThread1();
+
+// The proper way to ensure a thread is finished and cleaned up is to call
+// join(). However, the pw_thread facade tests must test detached thread
+// functionality. This backend-specific cleanup API blocks until all the test
+// threads above have finished executing.
+//
+// Threads may be backed by static contexts or dynamic context heap allocations.
+// After this function returns, the threads' static contexts are ready for reuse
+// and/or their dynamically allocated memory has been freed.
+//
+// Precondition: The threads must have started to execute before calling this
+// if cleanup is expected.
+void WaitUntilDetachedThreadsCleanedUp();
+
+} // namespace pw::thread::test
diff --git a/pw_thread/public/pw_thread/snapshot.h b/pw_thread/public/pw_thread/snapshot.h
index c2502ae2b..4ae30e054 100644
--- a/pw_thread/public/pw_thread/snapshot.h
+++ b/pw_thread/public/pw_thread/snapshot.h
@@ -28,7 +28,7 @@ namespace pw::thread {
// field. This should encode either raw_backtrace or raw_stack to the provided
// Thread stream encoder.
using ProcessThreadStackCallback =
- Function<Status(proto::Thread::StreamEncoder&, ConstByteSpan)>;
+ Function<Status(proto::pwpb::Thread::StreamEncoder&, ConstByteSpan)>;
struct StackContext {
std::string_view thread_name;
@@ -48,7 +48,7 @@ struct StackContext {
// stack_end_pointer
// stack_pointer
Status SnapshotStack(const StackContext& stack,
- proto::Thread::StreamEncoder& encoder,
+ proto::pwpb::Thread::StreamEncoder& encoder,
const ProcessThreadStackCallback& thread_stack_callback);
} // namespace pw::thread
diff --git a/pw_thread/public/pw_thread/test_thread_context.h b/pw_thread/public/pw_thread/test_thread_context.h
new file mode 100644
index 000000000..f4b8039f1
--- /dev/null
+++ b/pw_thread/public/pw_thread/test_thread_context.h
@@ -0,0 +1,68 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_backend/test_thread_context_native.h"
+
+namespace pw::thread::test {
+
+/// `TestThreadContext` is a facade class for creating threads for unit tests in
+/// a platform independent way. To use it, set
+/// `pw_thread_TEST_THREAD_CONTEXT_BACKEND` to a backend that implements the
+/// `pw::thread::test::backend::TestThreadContextNative` class.
+///
+/// To create a thread for unit testing, instantiate a `TestThreadContext`, then
+/// call `options()` to obtain a `pw::thread::Options`. Use that `Options` to
+/// start a `Thread`. Users must ensure the context's lifespan outlives the
+/// thread it creates. Recycling or destroying the context is only allowed if
+/// `join()` is called on the thread first.
+///
+/// @code{.cpp}
+/// pw::thread::test::TestThreadContext context;
+/// pw::thread::Thread test_thread(context.options(),
+/// ExampleThreadFunction);
+/// @endcode
+///
+/// Threads created with `TestThreadContext` cannot be configured in any way.
+/// Backends should create threads with sufficient resources to execute typical
+/// unit tests. Tests for complex scenarios or interactions where e.g. priority
+/// matters are not portable, and `TestThreadContext` may not work for them.
+/// Non-portable tests may include backend-specific headers and instantiate
+/// thread options for their platforms as required.
+///
+/// @note Developers should structure their logic so it can be tested without
+/// spawning a thread. Unit tests should avoid spawning threads unless
+/// absolutely necessary.
+///
+/// @warning Threads using the `TestThreadContext` may only be detached if the
+/// context has a static lifetime, meaning the context is both never re-used and
+/// not destroyed before the end of the lifetime of the application.
+class TestThreadContext {
+ public:
+ TestThreadContext() = default;
+
+ TestThreadContext(const TestThreadContext&) = delete;
+ TestThreadContext& operator=(const TestThreadContext&) = delete;
+
+ /// `pw::thread::test::TestThreadContext` returns a `pw::thread::Options`
+ /// associated with the this object, which can be used to contruct a thread.
+ ///
+ /// @return The default options for testing thread.
+ const Options& options() const { return context_.options(); }
+
+ private:
+ backend::TestThreadContextNative context_;
+};
+
+} // namespace pw::thread::test
diff --git a/pw_thread/public/pw_thread/test_threads.h b/pw_thread/public/pw_thread/test_threads.h
deleted file mode 100644
index 4d7a45627..000000000
--- a/pw_thread/public/pw_thread/test_threads.h
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#pragma once
-
-#include "pw_thread/thread.h"
-
-namespace pw::thread::test {
-
-// Two test threads are used to verify the thread facade.
-const Options& TestOptionsThread0();
-const Options& TestOptionsThread1();
-
-// Unfortunately the thread facade test's job is also to test detached threads
-// which may be backed by static contexts or dynamic context heap allocations.
-// In literally every other case you would use join for this, however that is
-// not an option here as detached thread functionality is being tested.
-// For this reason a backend specific cleanup API is provided which shall block
-// until all the test threads above have finished executions and are ready for
-// potential re-use and/or freed any dynamic allocations.
-//
-// Precondition: The threads must have started to execute before calling this
-// if cleanup is expected.
-void WaitUntilDetachedThreadsCleanedUp();
-
-} // namespace pw::thread::test
diff --git a/pw_thread/py/BUILD.bazel b/pw_thread/py/BUILD.bazel
index 873ea5ad0..094035421 100644
--- a/pw_thread/py/BUILD.bazel
+++ b/pw_thread/py/BUILD.bazel
@@ -14,7 +14,7 @@
package(default_visibility = ["//visibility:public"])
-# TODO(b/241456982): Not expected to build. We need a dependency on a
+# TODO: b/241456982 - Not expected to build. We need a dependency on a
# py_proto_library built from thread_proto, but that in turn depends on
# creating a py_proto_library for tokenizer_proto.
py_library(
@@ -31,7 +31,7 @@ py_library(
],
)
-# TODO(b/241456982): Not expected to build. We need a dependency on a
+# TODO: b/241456982 - Not expected to build. We need a dependency on a
# py_proto_library built from thread_proto, but that in turn depends on
# creating a py_proto_library for tokenizer_proto.
py_test(
diff --git a/pw_thread/py/thread_analyzer_test.py b/pw_thread/py/thread_analyzer_test.py
index 4f380f300..b2104c16c 100644
--- a/pw_thread/py/thread_analyzer_test.py
+++ b/pw_thread/py/thread_analyzer_test.py
@@ -17,6 +17,8 @@
import unittest
from pw_thread.thread_analyzer import ThreadInfo, ThreadSnapshotAnalyzer
from pw_thread_protos import thread_pb2
+import pw_tokenizer
+from pw_tokenizer import tokens
class ThreadInfoTest(unittest.TestCase):
@@ -292,6 +294,91 @@ class ThreadSnapshotAnalyzerTest(unittest.TestCase):
self.assertEqual(analyzer.active_thread(), temp_thread)
self.assertEqual(str(ThreadSnapshotAnalyzer(snapshot)), expected)
+ def test_tokenized_thread_name(self):
+ """Ensures a tokenized thread name is detokenized."""
+ snapshot = thread_pb2.SnapshotThreadInfo()
+ detokenizer = pw_tokenizer.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 0x46BE7497, 'The thread for Kuzco'
+ ),
+ ]
+ )
+ )
+
+ temp_thread = thread_pb2.Thread()
+ temp_thread.name = b'\x97\x74\xBE\x46'
+ snapshot.threads.append(temp_thread)
+ temp_thread.name = b'\x5D\xA8\x66\xAE'
+ snapshot.threads.append(temp_thread)
+
+ # pylint: disable=line-too-long
+ expected = '\n'.join(
+ (
+ 'Thread State',
+ ' 2 threads running.',
+ '',
+ 'Thread (UNKNOWN): The thread for Kuzco',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ '',
+ 'Thread (UNKNOWN): $Xahmrg==',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ '',
+ )
+ )
+ # pylint: enable=line-too-long
+ analyzer = ThreadSnapshotAnalyzer(snapshot, tokenizer_db=detokenizer)
+
+ # Ensure text dump matches expected contents.
+ self.assertEqual(str(analyzer), expected)
+
+ def test_no_db_tokenized_thread_name(self):
+ """Ensures a tokenized thread name is detokenized."""
+ snapshot = thread_pb2.SnapshotThreadInfo()
+
+ temp_thread = thread_pb2.Thread()
+ temp_thread.name = b'\x97\x74\xBE\x46'
+ snapshot.threads.append(temp_thread)
+ temp_thread.name = b'\x5D\xA8\x66\xAE'
+ snapshot.threads.append(temp_thread)
+
+ # pylint: disable=line-too-long
+ expected = '\n'.join(
+ (
+ 'Thread State',
+ ' 2 threads running.',
+ '',
+ 'Thread (UNKNOWN): $l3S+Rg==',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ '',
+ 'Thread (UNKNOWN): $Xahmrg==',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ '',
+ )
+ )
+ # pylint: enable=line-too-long
+ analyzer = ThreadSnapshotAnalyzer(snapshot)
+
+ # Ensure text dump matches expected contents.
+ self.assertEqual(str(analyzer), expected)
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_thread/snapshot.cc b/pw_thread/snapshot.cc
index 5b15239ec..72debb592 100644
--- a/pw_thread/snapshot.cc
+++ b/pw_thread/snapshot.cc
@@ -30,9 +30,9 @@
namespace pw::thread {
Status SnapshotStack(const StackContext& stack,
- proto::Thread::StreamEncoder& encoder,
+ proto::pwpb::Thread::StreamEncoder& encoder,
const ProcessThreadStackCallback& thread_stack_callback) {
- // TODO(b/234890430): Add support for ascending stacks.
+ // TODO: b/234890430 - Add support for ascending stacks.
encoder.WriteStackStartPointer(stack.stack_high_addr).IgnoreError();
encoder.WriteStackEndPointer(stack.stack_low_addr).IgnoreError();
encoder.WriteStackPointer(stack.stack_pointer).IgnoreError();
diff --git a/pw_thread/test_thread_context_facade_test.cc b/pw_thread/test_thread_context_facade_test.cc
new file mode 100644
index 000000000..81dd7f938
--- /dev/null
+++ b/pw_thread/test_thread_context_facade_test.cc
@@ -0,0 +1,62 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "gtest/gtest.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/test_thread_context.h"
+#include "pw_thread/thread.h"
+
+using pw::thread::test::TestThreadContext;
+
+namespace pw::thread {
+namespace {
+
+static void ReleaseBinarySemaphore(void* arg) {
+ static_cast<sync::BinarySemaphore*>(arg)->release();
+}
+
+TEST(Thread, TestThreadContext) {
+ TestThreadContext context_0;
+ TestThreadContext context_1;
+ Thread thread_0;
+ Thread thread_1;
+ sync::BinarySemaphore thread_ran_sem_0;
+ sync::BinarySemaphore thread_ran_sem_1;
+
+ thread_0 =
+ Thread(context_0.options(), ReleaseBinarySemaphore, &thread_ran_sem_0);
+ thread_1 =
+ Thread(context_1.options(), ReleaseBinarySemaphore, &thread_ran_sem_1);
+
+ EXPECT_NE(thread_0.get_id(), Id());
+ EXPECT_TRUE(thread_0.joinable());
+
+ EXPECT_NE(thread_1.get_id(), Id());
+ EXPECT_TRUE(thread_1.joinable());
+
+ thread_0.detach();
+ thread_1.detach();
+
+ EXPECT_EQ(thread_0.get_id(), Id());
+ EXPECT_FALSE(thread_0.joinable());
+
+ EXPECT_EQ(thread_1.get_id(), Id());
+ EXPECT_FALSE(thread_1.joinable());
+
+ thread_ran_sem_0.acquire();
+ thread_ran_sem_1.acquire();
+}
+
+} // namespace
+} // namespace pw::thread
diff --git a/pw_thread/thread_facade_test.cc b/pw_thread/thread_facade_test.cc
index 008e2cacd..ff2b4ce59 100644
--- a/pw_thread/thread_facade_test.cc
+++ b/pw_thread/thread_facade_test.cc
@@ -15,7 +15,7 @@
#include "gtest/gtest.h"
#include "pw_sync/binary_semaphore.h"
#include "pw_thread/id.h"
-#include "pw_thread/test_threads.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/thread.h"
using pw::thread::test::TestOptionsThread0;
diff --git a/pw_thread/thread_snapshot_service_test.cc b/pw_thread/thread_snapshot_service_test.cc
index 9b37d9c79..086653f98 100644
--- a/pw_thread/thread_snapshot_service_test.cc
+++ b/pw_thread/thread_snapshot_service_test.cc
@@ -33,7 +33,8 @@ bool EncodedThreadExists(ConstByteSpan serialized_thread_buffer,
protobuf::Decoder decoder(serialized_thread_buffer);
while (decoder.Next().ok()) {
switch (decoder.FieldNumber()) {
- case static_cast<uint32_t>(proto::SnapshotThreadInfo::Fields::kThreads): {
+ case static_cast<uint32_t>(
+ proto::pwpb::SnapshotThreadInfo::Fields::kThreads): {
ConstByteSpan thread_buffer;
EXPECT_EQ(OkStatus(), decoder.ReadBytes(&thread_buffer));
ConstByteSpan encoded_name;
@@ -78,7 +79,7 @@ ThreadInfo CreateThreadInfoObject(std::optional<ConstByteSpan> name,
TEST(ThreadSnapshotService, DecodeSingleThreadInfoObject) {
std::array<std::byte, RequiredServiceBufferSize(1)> encode_buffer;
- proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+ proto::pwpb::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
ConstByteSpan name = bytes::String("MyThread\0");
ThreadInfo thread_info = CreateThreadInfoObject(
@@ -99,7 +100,7 @@ TEST(ThreadSnapshotService, DecodeSingleThreadInfoObject) {
TEST(ThreadSnapshotService, DecodeMultipleThreadInfoObjects) {
std::array<std::byte, RequiredServiceBufferSize(3)> encode_buffer;
- proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+ proto::pwpb::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
ConstByteSpan name = bytes::String("MyThread1\0");
ThreadInfo thread_info_1 =
@@ -139,7 +140,7 @@ TEST(ThreadSnapshotService, DecodeMultipleThreadInfoObjects) {
TEST(ThreadSnapshotService, DefaultBufferSize) {
static std::array<std::byte, RequiredServiceBufferSize()> encode_buffer;
- proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+ proto::pwpb::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
ConstByteSpan name = bytes::String("MyThread\0");
std::optional<uintptr_t> example_addr =
@@ -160,7 +161,7 @@ TEST(ThreadSnapshotService, DefaultBufferSize) {
TEST(ThreadSnapshotService, FailedPrecondition) {
static std::array<std::byte, RequiredServiceBufferSize(1)> encode_buffer;
- proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+ proto::pwpb::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
ThreadInfo thread_info_no_name = CreateThreadInfoObject(
std::nullopt,
@@ -186,7 +187,7 @@ TEST(ThreadSnapshotService, FailedPrecondition) {
TEST(ThreadSnapshotService, Unimplemented) {
static std::array<std::byte, RequiredServiceBufferSize(1)> encode_buffer;
- proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+ proto::pwpb::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
ConstByteSpan name = bytes::String("MyThread\0");
ThreadInfo thread_info_no_peak_addr =
diff --git a/pw_thread_embos/BUILD.bazel b/pw_thread_embos/BUILD.bazel
index e4329bd5a..83e080408 100644
--- a/pw_thread_embos/BUILD.bazel
+++ b/pw_thread_embos/BUILD.bazel
@@ -22,7 +22,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "id_headers",
+ name = "id",
hdrs = [
"id_public_overrides/pw_thread_backend/id_inline.h",
"id_public_overrides/pw_thread_backend/id_native.h",
@@ -36,20 +36,18 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
-)
-
-pw_cc_library(
- name = "id",
deps = [
- ":id_headers",
"//pw_thread:id_facade",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
+ # TODO: b/234876414 - This should depend on embOS but our third parties
# currently do not have Bazel support.
)
pw_cc_library(
- name = "sleep_headers",
+ name = "sleep",
+ srcs = [
+ "sleep.cc",
+ ],
hdrs = [
"public/pw_thread_embos/sleep_inline.h",
"sleep_public_overrides/pw_thread_backend/sleep_inline.h",
@@ -58,30 +56,23 @@ pw_cc_library(
"public",
"sleep_public_overrides",
],
- deps = [
- "//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "sleep",
- srcs = [
- "sleep.cc",
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:embos",
],
deps = [
- ":sleep_headers",
"//pw_assert",
"//pw_chrono:system_clock",
- "//pw_chrono_embos:system_clock_headers",
"//pw_thread:sleep_facade",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
+ # TODO: b/234876414 - This should depend on embOS but our third parties
# currently do not have Bazel support.
)
-# This target provides the embOS specific headers needs for thread creation.
pw_cc_library(
- name = "thread_headers",
+ name = "thread",
+ srcs = [
+ "thread.cc",
+ ],
hdrs = [
"public/pw_thread_embos/config.h",
"public/pw_thread_embos/context.h",
@@ -101,42 +92,28 @@ pw_cc_library(
"//pw_string",
"//pw_thread:thread_facade",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
+ # TODO: b/234876414 - This should depend on embOS but our third parties
# currently do not have Bazel support.
)
pw_cc_library(
- name = "thread",
- srcs = [
- "thread.cc",
- ],
- deps = [
- ":id",
- ":thread_headers",
- "//pw_assert",
- ],
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
-)
-
-pw_cc_library(
- name = "test_threads",
+ name = "non_portable_test_thread_options",
srcs = [
"test_threads.cc",
],
includes = ["public"],
- # TODO(b/260637734): This target doesn't build
+ # TODO: b/260637734 - This target doesn't build
tags = ["manual"],
deps = [
"//pw_chrono:system_clock",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:sleep",
- "//pw_thread:test_threads_header",
"//pw_thread:thread_facade",
],
)
pw_cc_library(
- name = "yield_headers",
+ name = "yield",
hdrs = [
"public/pw_thread_embos/yield_inline.h",
"yield_public_overrides/pw_thread_backend/yield_inline.h",
@@ -145,16 +122,11 @@ pw_cc_library(
"public",
"yield_public_overrides",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
- # currently do not have Bazel support.
-)
-
-pw_cc_library(
- name = "yield",
deps = [
- ":yield_headers",
"//pw_thread:yield_facade",
],
+ # TODO: b/234876414 - This should depend on embOS but our third parties
+ # currently do not have Bazel support.
)
pw_cc_library(
@@ -166,13 +138,13 @@ pw_cc_library(
"public/pw_thread_embos/util.h",
],
includes = ["public"],
- # TODO(b/260637734): This target doesn't build
+ # TODO: b/260637734 - This target doesn't build
tags = ["manual"],
deps = [
"//pw_function",
"//pw_status",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
+ # TODO: b/234876414 - This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -185,7 +157,7 @@ pw_cc_library(
"public/pw_thread_embos/snapshot.h",
],
includes = ["public"],
- # TODO(b/260637734): This target doesn't build
+ # TODO: b/260637734 - This target doesn't build
tags = ["manual"],
deps = [
":util",
@@ -196,6 +168,6 @@ pw_cc_library(
"//pw_status",
"//pw_thread:snapshot",
],
- # TODO(b/234876414): This should depend on embOS but our third parties
+ # TODO: b/234876414 - This should depend on embOS but our third parties
# currently do not have Bazel support.
)
diff --git a/pw_thread_embos/BUILD.gn b/pw_thread_embos/BUILD.gn
index 023fdcda2..a7e26a4fa 100644
--- a/pw_thread_embos/BUILD.gn
+++ b/pw_thread_embos/BUILD.gn
@@ -195,8 +195,8 @@ pw_test_group("tests") {
tests = [ ":thread_backend_test" ]
}
-pw_source_set("test_threads") {
- public_deps = [ "$dir_pw_thread:test_threads" ]
+pw_source_set("non_portable_test_thread_options") {
+ public_deps = [ "$dir_pw_thread:non_portable_test_thread_options" ]
sources = [ "test_threads.cc" ]
deps = [
"$dir_pw_chrono:system_clock",
@@ -210,7 +210,7 @@ pw_source_set("test_threads") {
pw_test("thread_backend_test") {
enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_embos:thread"
deps = [
- ":test_threads",
+ ":non_portable_test_thread_options",
"$dir_pw_thread:thread_facade_test",
]
}
diff --git a/pw_thread_embos/docs.rst b/pw_thread_embos/docs.rst
index b1fb32ba9..8d079bf55 100644
--- a/pw_thread_embos/docs.rst
+++ b/pw_thread_embos/docs.rst
@@ -193,7 +193,7 @@ the most up-to-date information is captured, the stack pointer for the currently
running thread must be provided for cases where the running thread is being
captured. For ARM Cortex-M CPUs, you can do something like this:
-.. Code:: cpp
+.. code-block:: cpp
// Capture PSP.
void* stack_ptr = 0;
diff --git a/pw_thread_embos/test_threads.cc b/pw_thread_embos/test_threads.cc
index db95071b6..df1963105 100644
--- a/pw_thread_embos/test_threads.cc
+++ b/pw_thread_embos/test_threads.cc
@@ -12,14 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_thread/test_threads.h"
-
#include <chrono>
#include "RTOS.h"
#include "pw_assert/check.h"
#include "pw_chrono/system_clock.h"
#include "pw_log/log.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
#include "pw_thread_embos/context.h"
#include "pw_thread_embos/options.h"
diff --git a/pw_thread_freertos/BUILD.bazel b/pw_thread_freertos/BUILD.bazel
index c401c2a85..bf1b58c2d 100644
--- a/pw_thread_freertos/BUILD.bazel
+++ b/pw_thread_freertos/BUILD.bazel
@@ -25,7 +25,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "id_headers",
+ name = "id",
hdrs = [
"id_public_overrides/pw_thread_backend/id_inline.h",
"id_public_overrides/pw_thread_backend/id_native.h",
@@ -36,25 +36,21 @@ pw_cc_library(
"id_public_overrides",
"public",
],
- deps = [
- "//pw_interrupt:context",
- "@freertos",
- ],
-)
-
-pw_cc_library(
- name = "id",
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
deps = [
- ":id_headers",
+ "//pw_interrupt:context",
"//pw_thread:id_facade",
+ "@freertos",
],
)
pw_cc_library(
- name = "sleep_headers",
+ name = "sleep",
+ srcs = [
+ "sleep.cc",
+ ],
hdrs = [
"public/pw_thread_freertos/sleep_inline.h",
"sleep_public_overrides/pw_thread_backend/sleep_inline.h",
@@ -63,32 +59,23 @@ pw_cc_library(
"public",
"sleep_public_overrides",
],
- deps = [
- "//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "sleep",
- srcs = [
- "sleep.cc",
- ],
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
deps = [
- ":sleep_headers",
"//pw_assert",
"//pw_chrono:system_clock",
- "//pw_chrono_freertos:system_clock_headers",
"//pw_thread:id",
"//pw_thread:sleep_facade",
+ "@freertos",
],
)
-# This target provides the FreeRTOS specific headers needs for thread creation.
pw_cc_library(
- name = "thread_headers",
+ name = "thread",
+ srcs = [
+ "thread.cc",
+ ],
hdrs = [
"public/pw_thread_freertos/config.h",
"public/pw_thread_freertos/context.h",
@@ -115,21 +102,6 @@ pw_cc_library(
)
pw_cc_library(
- name = "thread",
- srcs = [
- "thread.cc",
- ],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- ":id",
- ":thread_headers",
- "//pw_assert",
- ],
-)
-
-pw_cc_library(
name = "dynamic_test_threads",
srcs = [
"dynamic_test_threads.cc",
@@ -139,15 +111,15 @@ pw_cc_library(
],
deps = [
"//pw_chrono:system_clock",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:sleep",
- "//pw_thread:test_threads_header",
"//pw_thread:thread_facade",
],
)
pw_cc_test(
name = "dynamic_thread_backend_test",
- # TODO(b/271465588): Get this test to build.
+ # TODO: b/271465588 - Get this test to build.
tags = ["manual"],
deps = [
":dynamic_test_threads",
@@ -165,15 +137,15 @@ pw_cc_library(
],
deps = [
"//pw_chrono:system_clock",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:sleep",
- "//pw_thread:test_threads_header",
"//pw_thread:thread_facade",
],
)
pw_cc_test(
name = "static_thread_backend_test",
- # TODO(b/271465588): Get this test to build.
+ # TODO: b/271465588 - Get this test to build.
tags = ["manual"],
deps = [
":static_test_threads",
@@ -182,7 +154,7 @@ pw_cc_test(
)
pw_cc_library(
- name = "yield_headers",
+ name = "yield",
hdrs = [
"public/pw_thread_freertos/yield_inline.h",
"yield_public_overrides/pw_thread_backend/yield_inline.h",
@@ -191,19 +163,9 @@ pw_cc_library(
"public",
"yield_public_overrides",
],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
- deps = [
- "@freertos",
- ],
-)
-
-pw_cc_library(
- name = "yield",
deps = [
- ":yield_headers",
"//pw_thread:yield_facade",
+ "@freertos",
],
)
@@ -233,7 +195,7 @@ pw_cc_test(
"pw_thread_freertos_private/thread_iteration.h",
"thread_iteration_test.cc",
],
- # TODO(b/271465588): Get this test to build.
+ # TODO: b/271465588 - Get this test to build.
tags = ["manual"],
deps = [
":freertos_tasktcb",
@@ -244,7 +206,7 @@ pw_cc_test(
"//pw_string:builder",
"//pw_string:util",
"//pw_sync:thread_notification",
- "//pw_thread:test_threads_header",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:thread",
"//pw_thread:thread_info",
"//pw_thread:thread_iteration",
@@ -266,6 +228,7 @@ pw_cc_library(
deps = [
"//pw_function",
"//pw_log",
+ "//pw_span",
"//pw_status",
"@freertos",
],
@@ -279,7 +242,7 @@ pw_cc_library(
hdrs = [
"public/pw_thread_freertos/snapshot.h",
],
- # TODO(b/269204725): Put this in the toolchain configuration instead. I
+ # TODO: b/269204725 - Put this in the toolchain configuration instead. I
# would like to say `copts = ["-Wno-c++20-designator"]`, but arm-gcc tells
# me that's an "unrecognized command line option"; I think it may be a
# clang-only flag.
@@ -313,11 +276,12 @@ pw_cc_facade(
pw_cc_library(
name = "freertos_tasktcb",
hdrs = [
+ "public/pw_thread_freertos/freertos_tsktcb.h",
":generate_freertos_tsktcb",
],
- includes = ["thread_public_overrides"],
- deps = [
- ":freertos_tasktcb_facade",
+ includes = [
+ "public",
+ "thread_public_overrides",
],
)
diff --git a/pw_thread_freertos/BUILD.gn b/pw_thread_freertos/BUILD.gn
index 532a79dba..042bde146 100644
--- a/pw_thread_freertos/BUILD.gn
+++ b/pw_thread_freertos/BUILD.gn
@@ -271,7 +271,8 @@ pw_test_group("tests") {
if (pw_thread_freertos_FREERTOS_TSKTCB_BACKEND != "") {
pw_test("thread_iteration_test") {
- enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_freertos:thread"
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_freertos:thread" &&
+ pw_thread_THREAD_ITERATION_BACKEND != ""
sources = [
"pw_thread_freertos_private/thread_iteration.h",
"thread_iteration_test.cc",
@@ -286,7 +287,7 @@ if (pw_thread_freertos_FREERTOS_TSKTCB_BACKEND != "") {
"$dir_pw_string:util",
"$dir_pw_sync:thread_notification",
"$dir_pw_third_party/freertos",
- "$dir_pw_thread:test_threads",
+ "$dir_pw_thread:non_portable_test_thread_options",
"$dir_pw_thread:thread",
"$dir_pw_thread:thread_info",
"$dir_pw_thread:thread_iteration",
@@ -295,7 +296,7 @@ if (pw_thread_freertos_FREERTOS_TSKTCB_BACKEND != "") {
}
pw_source_set("dynamic_test_threads") {
- public_deps = [ "$dir_pw_thread:test_threads" ]
+ public_deps = [ "$dir_pw_thread:non_portable_test_thread_options" ]
sources = [ "dynamic_test_threads.cc" ]
deps = [
"$dir_pw_chrono:system_clock",
@@ -313,7 +314,7 @@ pw_test("dynamic_thread_backend_test") {
}
pw_source_set("static_test_threads") {
- public_deps = [ "$dir_pw_thread:test_threads" ]
+ public_deps = [ "$dir_pw_thread:non_portable_test_thread_options" ]
sources = [ "static_test_threads.cc" ]
deps = [
"$dir_pw_chrono:system_clock",
diff --git a/pw_thread_freertos/docs.rst b/pw_thread_freertos/docs.rst
index c0cd11d0b..e38ecf7ad 100644
--- a/pw_thread_freertos/docs.rst
+++ b/pw_thread_freertos/docs.rst
@@ -252,7 +252,7 @@ the most up-to-date information is captured, the stack pointer for the currently
running thread must be provided for cases where the running thread is being
captured. For ARM Cortex-M CPUs, you can do something like this:
-.. Code:: cpp
+.. code-block:: cpp
// Capture PSP.
void* stack_ptr = 0;
diff --git a/pw_thread_freertos/dynamic_test_threads.cc b/pw_thread_freertos/dynamic_test_threads.cc
index f596baba1..5d0cf5d75 100644
--- a/pw_thread_freertos/dynamic_test_threads.cc
+++ b/pw_thread_freertos/dynamic_test_threads.cc
@@ -15,8 +15,8 @@
#include <chrono>
#include "pw_chrono/system_clock.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread_freertos/context.h"
#include "pw_thread_freertos/options.h"
diff --git a/pw_thread_freertos/public/pw_thread_freertos/snapshot.h b/pw_thread_freertos/public/pw_thread_freertos/snapshot.h
index d05b928de..b8c6229a5 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/snapshot.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/snapshot.h
@@ -37,7 +37,7 @@ namespace pw::thread::freertos {
// void* stack_ptr = 0;
// asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
// pw::thread::ProcessThreadStackCallback cb =
-// [](pw::thread::proto::Thread::StreamEncoder& encoder,
+// [](pw::thread::proto::pwbp::Thread::StreamEncoder& encoder,
// pw::ConstByteSpan stack) -> pw::Status {
// return encoder.WriteRawStack(stack);
// };
@@ -47,7 +47,7 @@ namespace pw::thread::freertos {
// Warning: This is only safe to use when the scheduler and interrupts are
// disabled.
Status SnapshotThreads(void* running_thread_stack_pointer,
- proto::SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::pwpb::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
// Captures only the provided thread handle as a pw::thread::Thread proto
@@ -75,7 +75,7 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
Status SnapshotThread(TaskHandle_t thread,
eTaskState thread_state,
void* running_thread_stack_pointer,
- proto::Thread::StreamEncoder& encoder,
+ proto::pwpb::Thread::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
} // namespace pw::thread::freertos
diff --git a/pw_thread_freertos/py/BUILD.gn b/pw_thread_freertos/py/BUILD.gn
index 33621663b..ee6834f9a 100644
--- a/pw_thread_freertos/py/BUILD.gn
+++ b/pw_thread_freertos/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_thread_freertos/__init__.py",
diff --git a/pw_thread_freertos/py/setup.py b/pw_thread_freertos/py/setup.py
deleted file mode 100644
index 3ed783f64..000000000
--- a/pw_thread_freertos/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2022 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_thread_freertos"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_thread_freertos/snapshot.cc b/pw_thread_freertos/snapshot.cc
index 730623598..98d8da139 100644
--- a/pw_thread_freertos/snapshot.cc
+++ b/pw_thread_freertos/snapshot.cc
@@ -44,37 +44,39 @@ extern "C" uint16_t prvTaskCheckFreeStackSpace(const uint8_t* pucStackByte);
// (INCLUDE_uxTaskGetStackHighWaterMark == 1))
void CaptureThreadState(eTaskState thread_state,
- proto::Thread::StreamEncoder& encoder) {
+ proto::pwpb::Thread::StreamEncoder& encoder) {
switch (thread_state) {
case eRunning:
PW_LOG_DEBUG("Thread state: RUNNING");
- encoder.WriteState(proto::ThreadState::Enum::RUNNING).IgnoreError();
+ encoder.WriteState(proto::pwpb::ThreadState::Enum::RUNNING).IgnoreError();
return;
case eReady:
PW_LOG_DEBUG("Thread state: READY");
- encoder.WriteState(proto::ThreadState::Enum::READY).IgnoreError();
+ encoder.WriteState(proto::pwpb::ThreadState::Enum::READY).IgnoreError();
return;
case eBlocked:
PW_LOG_DEBUG("Thread state: BLOCKED");
- encoder.WriteState(proto::ThreadState::Enum::BLOCKED).IgnoreError();
+ encoder.WriteState(proto::pwpb::ThreadState::Enum::BLOCKED).IgnoreError();
return;
case eSuspended:
PW_LOG_DEBUG("Thread state: SUSPENDED");
- encoder.WriteState(proto::ThreadState::Enum::SUSPENDED).IgnoreError();
+ encoder.WriteState(proto::pwpb::ThreadState::Enum::SUSPENDED)
+ .IgnoreError();
return;
case eDeleted:
PW_LOG_DEBUG("Thread state: INACTIVE");
- encoder.WriteState(proto::ThreadState::Enum::INACTIVE).IgnoreError();
+ encoder.WriteState(proto::pwpb::ThreadState::Enum::INACTIVE)
+ .IgnoreError();
return;
case eInvalid:
default:
PW_LOG_DEBUG("Thread state: UNKNOWN");
- encoder.WriteState(proto::ThreadState::Enum::UNKNOWN).IgnoreError();
+ encoder.WriteState(proto::pwpb::ThreadState::Enum::UNKNOWN).IgnoreError();
return;
}
}
@@ -82,11 +84,11 @@ void CaptureThreadState(eTaskState thread_state,
} // namespace
Status SnapshotThreads(void* running_thread_stack_pointer,
- proto::SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::pwpb::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& stack_dumper) {
struct {
void* running_thread_stack_pointer;
- proto::SnapshotThreadInfo::StreamEncoder* encoder;
+ proto::pwpb::SnapshotThreadInfo::StreamEncoder* encoder;
ProcessThreadStackCallback* stack_dumper;
Status thread_capture_status;
} ctx;
@@ -97,7 +99,7 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
ThreadCallback thread_capture_cb(
[&ctx](TaskHandle_t thread, eTaskState thread_state) -> bool {
- proto::Thread::StreamEncoder thread_encoder =
+ proto::pwpb::Thread::StreamEncoder thread_encoder =
ctx.encoder->GetThreadsEncoder();
ctx.thread_capture_status.Update(
SnapshotThread(thread,
@@ -119,7 +121,7 @@ Status SnapshotThread(
TaskHandle_t thread,
eTaskState thread_state,
void* running_thread_stack_pointer,
- proto::Thread::StreamEncoder& encoder,
+ proto::pwpb::Thread::StreamEncoder& encoder,
[[maybe_unused]] ProcessThreadStackCallback& thread_stack_callback) {
const tskTCB& tcb = *reinterpret_cast<tskTCB*>(thread);
@@ -128,7 +130,7 @@ Status SnapshotThread(
CaptureThreadState(thread_state, encoder);
- // TODO(b/234890430): Update this once we add support for ascending stacks.
+ // TODO: b/234890430 - Update this once we add support for ascending stacks.
static_assert(portSTACK_GROWTH < 0, "Ascending stacks are not yet supported");
// If the thread is active, the stack pointer in the TCB is stale.
@@ -141,27 +143,27 @@ Status SnapshotThread(
const uintptr_t stack_high_addr =
reinterpret_cast<uintptr_t>(tcb.pxEndOfStack);
const StackContext thread_ctx = {
- .thread_name = tcb.pcTaskName,
- .stack_low_addr = stack_low_addr,
- .stack_high_addr = stack_high_addr,
- .stack_pointer = stack_pointer,
+ .thread_name = tcb.pcTaskName,
+ .stack_low_addr = stack_low_addr,
+ .stack_high_addr = stack_high_addr,
+ .stack_pointer = stack_pointer,
#if ((configUSE_TRACE_FACILITY == 1) || \
(INCLUDE_uxTaskGetStackHighWaterMark == 1))
#if (portSTACK_GROWTH > 0)
- .stack_pointer_est_peak =
- stack_high_addr -
- (sizeof(StackType_t) *
- prvTaskCheckFreeStackSpace(
- reinterpret_cast<const uint8_t*>(stack_high_addr))),
+ .stack_pointer_est_peak =
+ stack_high_addr -
+ (sizeof(StackType_t) *
+ prvTaskCheckFreeStackSpace(
+ reinterpret_cast<const uint8_t*>(stack_high_addr))),
#else
- .stack_pointer_est_peak =
- stack_low_addr +
- (sizeof(StackType_t) *
- prvTaskCheckFreeStackSpace(
- reinterpret_cast<const uint8_t*>(stack_low_addr))),
+ .stack_pointer_est_peak =
+ stack_low_addr +
+ (sizeof(StackType_t) *
+ prvTaskCheckFreeStackSpace(
+ reinterpret_cast<const uint8_t*>(stack_low_addr))),
#endif // (portSTACK_GROWTH > 0)
#else
- .stack_pointer_est_peak = std::nullopt,
+ .stack_pointer_est_peak = std::nullopt,
#endif // ((configUSE_TRACE_FACILITY == 1) ||
// (INCLUDE_uxTaskGetStackHighWaterMark == 1))
};
diff --git a/pw_thread_freertos/static_test_threads.cc b/pw_thread_freertos/static_test_threads.cc
index fc1884dc4..810cfdfcd 100644
--- a/pw_thread_freertos/static_test_threads.cc
+++ b/pw_thread_freertos/static_test_threads.cc
@@ -15,8 +15,8 @@
#include <chrono>
#include "pw_chrono/system_clock.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
-#include "pw_thread/test_threads.h"
#include "pw_thread_freertos/context.h"
#include "pw_thread_freertos/options.h"
diff --git a/pw_thread_freertos/thread.cc b/pw_thread_freertos/thread.cc
index a7e36814d..de9f0041d 100644
--- a/pw_thread_freertos/thread.cc
+++ b/pw_thread_freertos/thread.cc
@@ -193,22 +193,20 @@ Thread::Thread(const thread::Options& facade_options,
void Thread::detach() {
PW_CHECK(joinable());
- if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
-#if (INCLUDE_vTaskSuspend == 1)
- // No need to suspend extra tasks.
- vTaskSuspend(native_type_->task_handle());
-#else
+ // xTaskResumeAll() can only be used after the scheduler has been started.
+ const bool scheduler_initialized =
+ xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED;
+
+ if (scheduler_initialized) {
+ // We don't want to individually suspend and resume this task using
+ // vTaskResume() as that can cause tasks to prematurely wake up and return
+ // from blocking APIs (b/303885539).
vTaskSuspendAll();
-#endif // INCLUDE_vTaskSuspend == 1
}
native_type_->set_detached();
const bool thread_done = native_type_->thread_done();
- if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
-#if (INCLUDE_vTaskSuspend == 1)
- vTaskResume(native_type_->task_handle());
-#else
+ if (scheduler_initialized) {
xTaskResumeAll();
-#endif // INCLUDE_vTaskSuspend == 1
}
if (thread_done) {
diff --git a/pw_thread_freertos/thread_iteration_test.cc b/pw_thread_freertos/thread_iteration_test.cc
index 05f4e3625..9e0e5c14e 100644
--- a/pw_thread_freertos/thread_iteration_test.cc
+++ b/pw_thread_freertos/thread_iteration_test.cc
@@ -24,7 +24,7 @@
#include "pw_string/string_builder.h"
#include "pw_string/util.h"
#include "pw_sync/thread_notification.h"
-#include "pw_thread/test_threads.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/thread.h"
#include "pw_thread/thread_info.h"
#include "pw_thread_freertos/freertos_tsktcb.h"
diff --git a/pw_thread_stl/Android.bp b/pw_thread_stl/Android.bp
new file mode 100644
index 000000000..7efca867f
--- /dev/null
+++ b/pw_thread_stl/Android.bp
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_thread_stl_include_dirs",
+ cpp_std: "c++20",
+ export_include_dirs: [
+ "public",
+ "id_public_overrides",
+ "sleep_public_overrides",
+ "thread_public_overrides",
+ "yield_public_overrides",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+cc_defaults {
+ name: "pw_thread_stl_defaults",
+ cpp_std: "c++20",
+ header_libs: [
+ "pw_thread_stl_include_dirs",
+ ],
+
+ export_header_lib_headers: [
+ "pw_thread_stl_include_dirs",
+ ],
+}
diff --git a/pw_thread_stl/BUILD.bazel b/pw_thread_stl/BUILD.bazel
index 3923c7c66..980b6c20c 100644
--- a/pw_thread_stl/BUILD.bazel
+++ b/pw_thread_stl/BUILD.bazel
@@ -27,7 +27,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "id_headers",
+ name = "id",
hdrs = [
"id_public_overrides/pw_thread_backend/id_inline.h",
"id_public_overrides/pw_thread_backend/id_native.h",
@@ -39,19 +39,13 @@ pw_cc_library(
"public",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
-)
-
-pw_cc_library(
- name = "id",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":id_headers",
"//pw_thread:id_facade",
],
)
pw_cc_library(
- name = "sleep_headers",
+ name = "sleep",
hdrs = [
"public/pw_thread_stl/sleep_inline.h",
"sleep_public_overrides/pw_thread_backend/sleep_inline.h",
@@ -63,21 +57,12 @@ pw_cc_library(
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
"//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "sleep",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
- deps = [
- ":sleep_headers",
- "//pw_chrono:system_clock",
"//pw_thread:sleep_facade",
],
)
pw_cc_library(
- name = "thread_headers",
+ name = "thread",
hdrs = [
"public/pw_thread_stl/options.h",
"public/pw_thread_stl/thread_inline.h",
@@ -90,13 +75,7 @@ pw_cc_library(
"thread_public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
-)
-
-pw_cc_library(
- name = "thread",
- target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":thread_headers",
"//pw_thread:thread_facade",
],
)
@@ -114,13 +93,13 @@ pw_cc_library(
)
pw_cc_library(
- name = "test_threads",
+ name = "non_portable_test_thread_options",
srcs = [
"test_threads.cc",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- "//pw_thread:test_threads_header",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:thread_facade",
],
)
@@ -129,13 +108,13 @@ pw_cc_test(
name = "thread_backend_test",
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":test_threads",
+ ":non_portable_test_thread_options",
"//pw_thread:thread_facade_test",
],
)
pw_cc_library(
- name = "yield_headers",
+ name = "yield",
hdrs = [
"public/pw_thread_stl/yield_inline.h",
"yield_public_overrides/pw_thread_backend/yield_inline.h",
@@ -145,13 +124,24 @@ pw_cc_library(
"yield_public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ "//pw_thread:yield_facade",
+ ],
)
pw_cc_library(
- name = "yield",
+ name = "test_thread_context",
+ hdrs = [
+ "public/pw_thread_stl/test_thread_context_native.h",
+ "test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h",
+ ],
+ includes = [
+ "public",
+ "test_thread_context_public_overrides",
+ ],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- ":yield_headers",
- "//pw_thread:yield_facade",
+ ":thread",
+ "//pw_thread:test_thread_context_facade",
],
)
diff --git a/pw_thread_stl/BUILD.gn b/pw_thread_stl/BUILD.gn
index da4f2991a..0f23bf268 100644
--- a/pw_thread_stl/BUILD.gn
+++ b/pw_thread_stl/BUILD.gn
@@ -135,8 +135,26 @@ pw_test_group("tests") {
tests = [ ":thread_backend_test" ]
}
-pw_source_set("test_threads") {
- public_deps = [ "$dir_pw_thread:test_threads" ]
+config("test_thread_context_public_overrides") {
+ include_dirs = [ "test_thread_context_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("test_thread_context") {
+ public_deps = [ ":thread" ]
+ public_configs = [
+ ":public_include_path",
+ ":test_thread_context_public_overrides",
+ ]
+ public = [
+ "public/pw_thread_stl/test_thread_context_native.h",
+ "test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h",
+ ]
+ deps = [ "$dir_pw_thread:test_thread_context.facade" ]
+}
+
+pw_source_set("non_portable_test_thread_options") {
+ public_deps = [ "$dir_pw_thread:non_portable_test_thread_options" ]
sources = [ "test_threads.cc" ]
deps = [ "$dir_pw_thread:thread" ]
}
@@ -145,7 +163,7 @@ pw_test("thread_backend_test") {
enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
pw_thread_SLEEP_BACKEND != ""
deps = [
- ":test_threads",
+ ":non_portable_test_thread_options",
"$dir_pw_thread:thread_facade_test",
]
}
diff --git a/pw_thread_stl/CMakeLists.txt b/pw_thread_stl/CMakeLists.txt
index 7f290e5b3..e865a2ba1 100644
--- a/pw_thread_stl/CMakeLists.txt
+++ b/pw_thread_stl/CMakeLists.txt
@@ -70,9 +70,22 @@ pw_add_library(pw_thread_stl.yield INTERFACE
pw_thread.yield.facade
)
+# This target provides the backend for pw::thread::test::TestThreadContext.
+pw_add_library(pw_thread_stl.test_thread_context INTERFACE
+ HEADERS
+ public/pw_thread_stl/test_thread_context_native.h
+ test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h
+ PUBLIC_INCLUDES
+ public
+ test_thread_context_public_overrides
+ PUBLIC_DEPS
+ pw_thread_stl.thread
+ pw_thread.test_thread_context.facade
+)
+
pw_add_library(pw_thread_stl.test_threads STATIC
PUBLIC_DEPS
- pw_thread.test_threads
+ pw_thread.non_portable_test_thread_options
SOURCES
test_threads.cc
PRIVATE_DEPS
diff --git a/pw_thread_stl/public/pw_thread_stl/test_thread_context_native.h b/pw_thread_stl/public/pw_thread_stl/test_thread_context_native.h
new file mode 100644
index 000000000..0b85fdadb
--- /dev/null
+++ b/pw_thread_stl/public/pw_thread_stl/test_thread_context_native.h
@@ -0,0 +1,36 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_stl/options.h"
+
+namespace pw::thread::test::backend {
+
+// Native Test Thread Options backend class for the C++ standard library.
+class TestThreadContextNative {
+ public:
+ constexpr TestThreadContextNative() = default;
+
+ TestThreadContextNative(const TestThreadContextNative&) = delete;
+ TestThreadContextNative& operator=(const TestThreadContextNative&) = delete;
+
+ ~TestThreadContextNative() = default;
+
+ const Options& options() const { return options_; }
+
+ private:
+ stl::Options options_;
+};
+
+} // namespace pw::thread::test::backend
diff --git a/pw_thread_stl/test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h b/pw_thread_stl/test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h
new file mode 100644
index 000000000..0821f88c5
--- /dev/null
+++ b/pw_thread_stl/test_thread_context_public_overrides/pw_thread_backend/test_thread_context_native.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_stl/test_thread_context_native.h"
diff --git a/pw_thread_stl/test_threads.cc b/pw_thread_stl/test_threads.cc
index 64ab33e40..30fddeefa 100644
--- a/pw_thread_stl/test_threads.cc
+++ b/pw_thread_stl/test_threads.cc
@@ -12,8 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_thread/test_threads.h"
-
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread_stl/options.h"
namespace pw::thread::test {
diff --git a/pw_thread_threadx/BUILD.bazel b/pw_thread_threadx/BUILD.bazel
index 78f7f36b7..57536677c 100644
--- a/pw_thread_threadx/BUILD.bazel
+++ b/pw_thread_threadx/BUILD.bazel
@@ -23,7 +23,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "id_headers",
+ name = "id",
hdrs = [
"id_public_overrides/pw_thread_backend/id_inline.h",
"id_public_overrides/pw_thread_backend/id_native.h",
@@ -34,21 +34,17 @@ pw_cc_library(
"id_public_overrides",
"public",
],
-)
-
-pw_cc_library(
- name = "id",
+ # TODO: b/257321712 - Add ThreadX dependency.
deps = [
- ":id_headers",
"//pw_thread:id_facade",
],
- # TODO(b/234876414): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
)
-# This target provides the ThreadX specific headers needs for thread creation.
pw_cc_library(
- name = "thread_headers",
+ name = "thread",
+ srcs = [
+ "thread.cc",
+ ],
hdrs = [
"public/pw_thread_threadx/config.h",
"public/pw_thread_threadx/context.h",
@@ -62,7 +58,7 @@ pw_cc_library(
"public",
"thread_public_overrides",
],
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - Add ThreadX dependency.
tags = ["manual"],
deps = [
":id",
@@ -70,51 +66,38 @@ pw_cc_library(
"//pw_string",
"//pw_thread:thread_facade",
],
- # TODO(b/234876414): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
-)
-
-pw_cc_library(
- name = "thread",
- srcs = [
- "thread.cc",
- ],
- # TODO(b/257321712): This target doesn't build.
- tags = ["manual"],
- deps = [
- ":id",
- ":thread_headers",
- "//pw_assert",
- ],
)
pw_cc_library(
- name = "test_threads",
+ name = "non_portable_test_thread_options",
srcs = [
"test_threads.cc",
],
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - This target doesn't build.
tags = ["manual"],
deps = [
"//pw_chrono:system_clock",
+ "//pw_thread:non_portable_test_thread_options",
"//pw_thread:sleep",
- "//pw_thread:test_threads_header",
"//pw_thread:thread_facade",
],
)
pw_cc_test(
name = "thread_backend_test",
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - This target doesn't build.
tags = ["manual"],
deps = [
- ":test_threads",
+ ":non_portable_test_thread_options",
"//pw_thread:thread_facade_test",
],
)
pw_cc_library(
- name = "sleep_headers",
+ name = "sleep",
+ srcs = [
+ "sleep.cc",
+ ],
hdrs = [
"public/pw_thread_threadx/sleep_inline.h",
"sleep_public_overrides/pw_thread_backend/sleep_inline.h",
@@ -123,17 +106,7 @@ pw_cc_library(
"public",
"sleep_public_overrides",
],
- deps = [
- "//pw_chrono:system_clock",
- ],
-)
-
-pw_cc_library(
- name = "sleep",
- srcs = [
- "sleep.cc",
- ],
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - This target doesn't build.
tags = ["manual"],
deps = [
":sleep_headers",
@@ -145,7 +118,7 @@ pw_cc_library(
)
pw_cc_library(
- name = "yield_headers",
+ name = "yield",
hdrs = [
"public/pw_thread_threadx/yield_inline.h",
"yield_public_overrides/pw_thread_backend/yield_inline.h",
@@ -154,14 +127,9 @@ pw_cc_library(
"public",
"yield_public_overrides",
],
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - This target doesn't build.
tags = ["manual"],
-)
-
-pw_cc_library(
- name = "yield",
deps = [
- ":yield_headers",
"//pw_thread:yield_facade",
],
)
@@ -175,7 +143,7 @@ pw_cc_library(
"public/pw_thread_threadx/util.h",
],
includes = ["public"],
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - This target doesn't build.
tags = ["manual"],
deps = [
"//pw_function",
@@ -191,7 +159,7 @@ pw_cc_library(
hdrs = [
"public/pw_thread_threadx/snapshot.h",
],
- # TODO(b/257321712): This target doesn't build.
+ # TODO: b/257321712 - This target doesn't build.
tags = ["manual"],
deps = [
":util",
diff --git a/pw_thread_threadx/BUILD.gn b/pw_thread_threadx/BUILD.gn
index fb314380c..874f88d5c 100644
--- a/pw_thread_threadx/BUILD.gn
+++ b/pw_thread_threadx/BUILD.gn
@@ -201,8 +201,8 @@ pw_test_group("tests") {
tests = [ ":thread_backend_test" ]
}
-pw_source_set("test_threads") {
- public_deps = [ "$dir_pw_thread:test_threads" ]
+pw_source_set("non_portable_test_thread_options") {
+ public_deps = [ "$dir_pw_thread:non_portable_test_thread_options" ]
sources = [ "test_threads.cc" ]
deps = [
"$dir_pw_chrono:system_clock",
@@ -216,7 +216,7 @@ pw_source_set("test_threads") {
pw_test("thread_backend_test") {
enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_threadx:thread"
deps = [
- ":test_threads",
+ ":non_portable_test_thread_options",
"$dir_pw_thread:thread_facade_test",
]
}
diff --git a/pw_thread_threadx/docs.rst b/pw_thread_threadx/docs.rst
index 026ed2bff..d49c133ca 100644
--- a/pw_thread_threadx/docs.rst
+++ b/pw_thread_threadx/docs.rst
@@ -6,7 +6,7 @@ pw_thread_threadx
This is a set of backends for pw_thread based on ThreadX.
.. Warning::
- This module is still under construction, the API is not yet stable.
+ This module is still under construction, the API is not yet stable.
-----------------------
Thread Creation Backend
@@ -21,48 +21,48 @@ be created as follows:
.. code-block:: cpp
- #include "pw_thread/detached_thread.h"
- #include "pw_thread_threadx/config.h"
- #include "pw_thread_threadx/context.h"
- #include "pw_thread_threadx/options.h"
- #include "tx_api.h"
-
- constexpr UINT kFooPriority =
- pw::thread::threadx::config::kDefaultPriority;
- constexpr ULONG kFooTimeSliceInterval =
- pw::thread::threadx::config::kDefaultTimeSliceInterval;
- constexpr size_t kFooStackSizeWords =
- pw::thread::threadx::config::kDefaultStackSizeWords;
-
- pw::thread::threadx::ContextWithStack<kFooStackSizeWords>
- example_thread_context;
- void StartExampleThread() {
- pw::thread::DetachedThread(
- pw::thread::threadx::Options()
- .set_name("example_thread")
- .set_priority(kFooPriority)
- .set_time_slice_interval(kFooTimeSliceInterval)
- .set_context(example_thread_context),
- example_thread_function);
- }
+ #include "pw_thread/detached_thread.h"
+ #include "pw_thread_threadx/config.h"
+ #include "pw_thread_threadx/context.h"
+ #include "pw_thread_threadx/options.h"
+ #include "tx_api.h"
+
+ constexpr UINT kFooPriority =
+ pw::thread::threadx::config::kDefaultPriority;
+ constexpr ULONG kFooTimeSliceInterval =
+ pw::thread::threadx::config::kDefaultTimeSliceInterval;
+ constexpr size_t kFooStackSizeWords =
+ pw::thread::threadx::config::kDefaultStackSizeWords;
+
+ pw::thread::threadx::ContextWithStack<kFooStackSizeWords>
+ example_thread_context;
+ void StartExampleThread() {
+ pw::thread::DetachedThread(
+ pw::thread::threadx::Options()
+ .set_name("example_thread")
+ .set_priority(kFooPriority)
+ .set_time_slice_interval(kFooTimeSliceInterval)
+ .set_context(example_thread_context),
+ example_thread_function);
+ }
.. list-table::
- * - :ref:`module-pw_thread` Facade
- - Backend Target
- - Description
- * - ``pw_thread:id``
- - ``pw_thread_threadx:id``
- - Thread identification.
- * - ``pw_thread:yield``
- - ``pw_thread_threadx:yield``
- - Thread scheduler yielding.
- * - ``pw_thread:sleep``
- - ``pw_thread_threadx:sleep``
- - Thread scheduler sleeping.
- * - ``pw_thread:thread``
- - ``pw_thread_threadx:thread``
- - Thread creation.
+ * - :ref:`module-pw_thread` Facade
+ - Backend Target
+ - Description
+ * - ``pw_thread:id``
+ - ``pw_thread_threadx:id``
+ - Thread identification.
+ * - ``pw_thread:yield``
+ - ``pw_thread_threadx:yield``
+ - Thread scheduler yielding.
+ * - ``pw_thread:sleep``
+ - ``pw_thread_threadx:sleep``
+ - Thread scheduler sleeping.
+ * - ``pw_thread:thread``
+ - ``pw_thread_threadx:thread``
+ - Thread creation.
Module Configuration Options
============================
@@ -73,111 +73,111 @@ more details.
.. c:macro:: PW_THREAD_THREADX_CONFIG_JOINING_ENABLED
- Whether thread joining is enabled. By default this is disabled.
+ Whether thread joining is enabled. By default this is disabled.
- We suggest only enabling this when thread joining is required to minimize
- the RAM and ROM cost of threads.
+ We suggest only enabling this when thread joining is required to minimize
+ the RAM and ROM cost of threads.
- Enabling this grows the RAM footprint of every pw::thread::Thread as it adds
- a TX_EVENT_FLAGS_GROUP to every thread's pw::thread::threadx::Context. In
- addition, there is a minute ROM cost to construct and destroy this added
- object.
+ Enabling this grows the RAM footprint of every pw::thread::Thread as it adds
+ a TX_EVENT_FLAGS_GROUP to every thread's pw::thread::threadx::Context. In
+ addition, there is a minute ROM cost to construct and destroy this added
+ object.
- PW_THREAD_JOINING_ENABLED gets set to this value.
+ PW_THREAD_JOINING_ENABLED gets set to this value.
.. c:macro:: PW_THREAD_THREADX_CONFIG_DEFAULT_STACK_SIZE_WORDS
- The default stack size in words. By default this uses the minimal ThreadX
- stack size.
+ The default stack size in words. By default this uses the minimal ThreadX
+ stack size.
.. c:macro:: PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN
- The maximum length of a thread's name, not including null termination. By
- default this is arbitrarily set to 15. This results in an array of characters
- which is this length + 1 bytes in every pw::thread::Thread's context.
+ The maximum length of a thread's name, not including null termination. By
+ default this is arbitrarily set to 15. This results in an array of characters
+ which is this length + 1 bytes in every pw::thread::Thread's context.
.. c:macro:: PW_THREAD_THREADX_CONFIG_DEFAULT_TIME_SLICE_INTERVAL
- The round robin time slice tick interval for threads at the same priority.
- By default this is disabled as not all ports support this, using a value of 0
- ticks.
+ The round robin time slice tick interval for threads at the same priority.
+ By default this is disabled as not all ports support this, using a value of 0
+ ticks.
.. c:macro:: PW_THREAD_THREADX_CONFIG_MIN_PRIORITY
- The minimum priority level, this is normally based on the number of priority
- levels.
+ The minimum priority level, this is normally based on the number of priority
+ levels.
.. c:macro:: PW_THREAD_THREADX_CONFIG_DEFAULT_PRIORITY
- The default priority level. By default this uses the minimal ThreadX
- priority level, given that 0 is the highest priority.
+ The default priority level. By default this uses the minimal ThreadX
+ priority level, given that 0 is the highest priority.
.. c:macro:: PW_THREAD_THREADX_CONFIG_LOG_LEVEL
- The log level to use for this module. Logs below this level are omitted.
+ The log level to use for this module. Logs below this level are omitted.
ThreadX Thread Options
======================
.. cpp:class:: pw::thread::threadx::Options
- .. cpp:function:: set_name(const char* name)
+ .. cpp:function:: set_name(const char* name)
- Sets the name for the ThreadX thread, note that this will be deep copied
- into the context and may be truncated based on
- ``PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN``.
+ Sets the name for the ThreadX thread, note that this will be deep copied
+ into the context and may be truncated based on
+ ``PW_THREAD_THREADX_CONFIG_MAX_THREAD_NAME_LEN``.
- .. cpp:function:: set_priority(UINT priority)
+ .. cpp:function:: set_priority(UINT priority)
- Sets the priority for the ThreadX thread from 0 through 31, where a value
- of 0 represents the highest priority, see ThreadX tx_thread_create for
- more detail.
+ Sets the priority for the ThreadX thread from 0 through 31, where a value
+ of 0 represents the highest priority, see ThreadX tx_thread_create for
+ more detail.
- **Precondition**: priority <= ``PW_THREAD_THREADX_CONFIG_MIN_PRIORITY``.
+ **Precondition**: priority <= ``PW_THREAD_THREADX_CONFIG_MIN_PRIORITY``.
- .. cpp:function:: set_preemption_threshold(UINT preemption_threshold)
+ .. cpp:function:: set_preemption_threshold(UINT preemption_threshold)
- Optionally sets the preemption threshold for the ThreadX thread from 0
- through 31.
+ Optionally sets the preemption threshold for the ThreadX thread from 0
+ through 31.
- Only priorities higher than this level (i.e. lower number) are allowed to
- preempt this thread. In other words this allows the thread to specify the
- priority ceiling for disabling preemption. Threads that have a higher
- priority than the ceiling are still allowed to preempt while those with
- less than the ceiling are not allowed to preempt.
+ Only priorities higher than this level (i.e. lower number) are allowed to
+ preempt this thread. In other words this allows the thread to specify the
+ priority ceiling for disabling preemption. Threads that have a higher
+ priority than the ceiling are still allowed to preempt while those with
+ less than the ceiling are not allowed to preempt.
- Not setting the preemption threshold or explicitly specifying a value
- equal to the priority disables preemption threshold.
+ Not setting the preemption threshold or explicitly specifying a value
+ equal to the priority disables preemption threshold.
- Time slicing is disabled while the preemption threshold is enabled, i.e.
- not equal to the priority, even if a time slice interval was specified.
+ Time slicing is disabled while the preemption threshold is enabled, i.e.
+ not equal to the priority, even if a time slice interval was specified.
- The preemption threshold can be adjusted at run time, this only sets the
- initial threshold.
+ The preemption threshold can be adjusted at run time, this only sets the
+ initial threshold.
- **Precondition**: preemption_threshold <= priority
+ **Precondition**: preemption_threshold <= priority
- .. cpp:function:: set_time_slice_interval(UINT time_slice_interval)
+ .. cpp:function:: set_time_slice_interval(UINT time_slice_interval)
- Sets the number of ticks this thread is allowed to run before other ready
- threads of the same priority are given a chance to run.
+ Sets the number of ticks this thread is allowed to run before other ready
+ threads of the same priority are given a chance to run.
- Time slicing is disabled while the preemption threshold is enabled, i.e.
- not equal to the priority, even if a time slice interval was specified.
+ Time slicing is disabled while the preemption threshold is enabled, i.e.
+ not equal to the priority, even if a time slice interval was specified.
- A value of ``TX_NO_TIME_SLICE`` (a value of 0) disables time-slicing of
- this thread.
+ A value of ``TX_NO_TIME_SLICE`` (a value of 0) disables time-slicing of
+ this thread.
- Using time slicing results in a slight amount of system overhead, threads
- with a unique priority should consider ``TX_NO_TIME_SLICE``.
+ Using time slicing results in a slight amount of system overhead, threads
+ with a unique priority should consider ``TX_NO_TIME_SLICE``.
- .. cpp:function:: set_context(pw::thread::embos::Context& context)
+ .. cpp:function:: set_context(pw::thread::embos::Context& context)
- Set the pre-allocated context (all memory needed to run a thread). Note
- that this is required for this thread creation backend! The Context can
- either be constructed with an externally provided ``pw::span<ULONG>``
- stack or the templated form of ``ContextWihtStack<kStackSizeWords`` can be
- used.
+ Set the pre-allocated context (all memory needed to run a thread). Note
+ that this is required for this thread creation backend! The Context can
+ either be constructed with an externally provided ``pw::span<ULONG>``
+ stack or the templated form of ``ContextWihtStack<kStackSizeWords`` can be
+ used.
-----------------------------
Thread Identification Backend
@@ -234,18 +234,18 @@ the most up-to-date information is captured, the stack pointer for the currently
running thread must be provided for cases where the running thread is being
captured. For ARM Cortex-M CPUs, you can do something like this:
-.. Code:: cpp
-
- // Capture PSP.
- void* stack_ptr = 0;
- asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
- pw::thread::ProcessThreadStackCallback cb =
- [](pw::thread::proto::Thread::StreamEncoder& encoder,
- pw::ConstByteSpan stack) -> pw::Status {
- return encoder.WriteRawStack(stack);
- };
- pw::thread::threadx::SnapshotThread(my_thread, stack_ptr,
- snapshot_encoder, cb);
+.. code-block:: cpp
+
+ // Capture PSP.
+ void* stack_ptr = 0;
+ asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
+ pw::thread::ProcessThreadStackCallback cb =
+ [](pw::thread::proto::Thread::StreamEncoder& encoder,
+ pw::ConstByteSpan stack) -> pw::Status {
+ return encoder.WriteRawStack(stack);
+ };
+ pw::thread::threadx::SnapshotThread(my_thread, stack_ptr,
+ snapshot_encoder, cb);
``SnapshotThreads()`` wraps the singular thread capture to instead captures
all created threads to a ``pw::thread::proto::SnapshotThreadInfo`` message.
diff --git a/pw_thread_threadx/test_threads.cc b/pw_thread_threadx/test_threads.cc
index 0cf222b56..24a18f8af 100644
--- a/pw_thread_threadx/test_threads.cc
+++ b/pw_thread_threadx/test_threads.cc
@@ -12,13 +12,12 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_thread/test_threads.h"
-
#include <chrono>
#include "pw_assert/check.h"
#include "pw_chrono/system_clock.h"
#include "pw_log/log.h"
+#include "pw_thread/non_portable_test_thread_options.h"
#include "pw_thread/sleep.h"
#include "pw_thread_threadx/context.h"
#include "pw_thread_threadx/options.h"
diff --git a/pw_thread_zephyr/CMakeLists.txt b/pw_thread_zephyr/CMakeLists.txt
index 41e357392..8a8176206 100644
--- a/pw_thread_zephyr/CMakeLists.txt
+++ b/pw_thread_zephyr/CMakeLists.txt
@@ -11,6 +11,34 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_module_config(pw_thread_zephyr_CONFIG)
+
+pw_add_library(pw_thread_zephyr.config INTERFACE
+ HEADERS
+ public/pw_thread_zephyr/config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ "${pw_thread_zephyr_CONFIG}"
+)
+
+# This target provides the backend for pw::thread::Id & pw::this_thread::get_id.
+pw_add_library(pw_thread_zephyr.id INTERFACE
+ HEADERS
+ public/pw_thread_zephyr/id_inline.h
+ public/pw_thread_zephyr/id_native.h
+ id_public_overrides/pw_thread_backend/id_inline.h
+ id_public_overrides/pw_thread_backend/id_native.h
+ PUBLIC_INCLUDES
+ public
+ id_public_overrides
+ PUBLIC_DEPS
+ pw_assert
+ pw_interrupt.context
+ pw_thread.id.facade
+)
pw_add_library(pw_thread_zephyr.sleep STATIC
HEADERS
@@ -28,3 +56,39 @@ pw_add_library(pw_thread_zephyr.sleep STATIC
pw_chrono_zephyr.system_clock
pw_assert.check
)
+
+# This target provides the backend for pw::thread::Thread and the headers needed
+# for thread creation.
+pw_add_library(pw_thread_zephyr.thread STATIC
+ HEADERS
+ public/pw_thread_zephyr/context.h
+ public/pw_thread_zephyr/options.h
+ public/pw_thread_zephyr/thread_inline.h
+ public/pw_thread_zephyr/thread_native.h
+ thread_public_overrides/pw_thread_backend/thread_inline.h
+ thread_public_overrides/pw_thread_backend/thread_native.h
+ PUBLIC_INCLUDES
+ public
+ thread_public_overrides
+ PUBLIC_DEPS
+ pw_assert
+ pw_span
+ pw_string
+ pw_thread.id
+ pw_thread.thread.facade
+ pw_thread_zephyr.config
+ SOURCES
+ thread.cc
+)
+
+pw_add_library(pw_thread_zephyr.thread_iteration STATIC
+ PUBLIC_INCLUDES
+ public
+ thread_public_overrides
+ PUBLIC_DEPS
+ pw_thread.thread_info
+ pw_thread.thread_iteration.facade
+ SOURCES
+ pw_thread_zephyr_private/thread_iteration.h
+ thread_iteration.cc
+)
diff --git a/pw_thread_zephyr/Kconfig b/pw_thread_zephyr/Kconfig
index 65b9c3ab8..89ce62cee 100644
--- a/pw_thread_zephyr/Kconfig
+++ b/pw_thread_zephyr/Kconfig
@@ -12,7 +12,44 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_thread"
+
config PIGWEED_THREAD_SLEEP
- bool "Enabled the Zephyr pw_thread.sleep backend"
+ bool "Link and set pw_thread.sleep library backend"
select PIGWEED_CHRONO_SYSTEM_CLOCK
select PIGWEED_ASSERT
+ help
+ See :ref:`module-pw_thread` for module details.
+
+config PIGWEED_THREAD
+ bool "Link and set pw_thread.thread and pw_thread.id backends"
+ select PIGWEED_ASSERT
+ help
+ See :ref:`module-pw_thread` for module details.
+
+config PIGWEED_THREAD_ITERATION
+ bool "Link and set pw_thread.thread_iteration backend"
+ select PIGWEED_ASSERT
+ select PIGWEED_THREAD
+ help
+ See :ref:`module-pw_thread` for module details.
+
+if PIGWEED_THREAD
+
+config PIGWEED_THREAD_DEFAULT_PRIORITY
+ int "Default thread priority"
+ default MAIN_THREAD_PRIORITY
+
+config PIGWEED_THREAD_NUM_COOP_PRIORITIES
+ int "Number of cooperative thread priorities"
+ default NUM_COOP_PRIORITIES
+ range 0 NUM_COOP_PRIORITIES
+
+config PIGWEED_THREAD_NUM_PREEMPT_PRIORITIES
+ int "Number of preemptible thread priorities"
+ default NUM_PREEMPT_PRIORITIES
+ range 0 NUM_PREEMPT_PRIORITIES
+
+endif # PIGWEED_THREAD
+
+endmenu
diff --git a/pw_thread_zephyr/docs.rst b/pw_thread_zephyr/docs.rst
index 9608eb149..535c1ed27 100644
--- a/pw_thread_zephyr/docs.rst
+++ b/pw_thread_zephyr/docs.rst
@@ -1,8 +1,47 @@
.. _module-pw_thread_zephyr:
-----------------
+================
pw_thread_zephyr
-----------------
-This is a set of backends for pw_thread based on the Zephyr RTOS. Currently,
-only the pw_thread.sleep facade is implemented which is enabled via
-``CONFIG_PIGWEED_THREAD_SLEEP=y``.
+================
+This is a set of backends for pw_thread based on the Zephyr RTOS.
+
+.. Warning::
+ This module is still under construction, the API is not yet stable and
+ documentation is incomplete.
+
+-----------------------
+Thread Creation Backend
+-----------------------
+A backend for ``pw::thread::Thread`` is offered using ``k_thread_create()``.
+This backend only supports threads with static contexts (which are passed via
+Options).
+All created threads support joining and detaching.
+To enable this backend, add ``CONFIG_PIGWEED_THREAD=y`` to the Zephyr
+project`s configuration.
+
+.. code-block:: cpp
+
+ #include "pw_thread/detached_thread.h"
+ #include "pw_thread_zephyr/config.h"
+ #include "pw_thread_zephyr/context.h"
+ #include "pw_thread_zephyr/options.h"
+
+ constexpr int kFooPriority =
+ pw::thread::zephyr::config::kDefaultPriority;
+ constexpr size_t kFooStackSizeBytes = 512;
+
+ pw::thread::zephyr::StaticContextWithStack<kFooStackSizeBytes>
+ example_thread_context;
+ void StartExampleThread() {
+ pw::thread::DetachedThread(
+ pw::thread::zephyr::Options(example_thread_context)
+ .set_priority(kFooPriority),
+ example_thread_function, example_arg);
+ }
+
+--------------------
+Thread Sleep Backend
+--------------------
+A backend for ``pw::thread::sleep_for()`` and ``pw::thread::sleep_until()``.
+To enable this backend, add ``CONFIG_PIGWEED_THREAD_SLEEP=y``
+to the Zephyr project`s configuration.
diff --git a/pw_crypto/public_overrides/boringssl/pw_crypto/sha256_backend.h b/pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_inline.h
index 5ba880efb..7243202ca 100644
--- a/pw_crypto/public_overrides/boringssl/pw_crypto/sha256_backend.h
+++ b/pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_inline.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -11,7 +11,6 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-
#pragma once
-#include "pw_crypto/sha256_boringssl.h"
+#include "pw_thread_zephyr/id_inline.h"
diff --git a/pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_native.h b/pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_native.h
new file mode 100644
index 000000000..d31b500e2
--- /dev/null
+++ b/pw_thread_zephyr/id_public_overrides/pw_thread_backend/id_native.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_zephyr/id_native.h"
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/config.h b/pw_thread_zephyr/public/pw_thread_zephyr/config.h
new file mode 100644
index 000000000..3919c85d7
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/config.h
@@ -0,0 +1,30 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+// Configuration macros for the tokenizer module.
+#pragma once
+
+#include <zephyr/kernel.h>
+
+namespace pw::thread::zephyr::config {
+
+inline constexpr int kDefaultPriority = CONFIG_PIGWEED_THREAD_DEFAULT_PRIORITY;
+inline constexpr int kHighestSchedulerPriority =
+ -CONFIG_PIGWEED_THREAD_NUM_COOP_PRIORITIES;
+inline constexpr int kLowestSchedulerPriority =
+ CONFIG_PIGWEED_THREAD_NUM_PREEMPT_PRIORITIES - 1;
+
+static_assert(kDefaultPriority <= kLowestSchedulerPriority);
+static_assert(kDefaultPriority >= kHighestSchedulerPriority);
+
+} // namespace pw::thread::zephyr::config
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/context.h b/pw_thread_zephyr/public/pw_thread_zephyr/context.h
new file mode 100644
index 000000000..6ac04e520
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/context.h
@@ -0,0 +1,120 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <zephyr/kernel.h>
+#include <zephyr/kernel/thread_stack.h>
+
+#include <cstdint>
+
+#include "pw_span/span.h"
+#include "pw_thread_zephyr/config.h"
+
+namespace pw::thread {
+
+class Thread; // Forward declare Thread which depends on Context.
+
+} // namespace pw::thread
+
+namespace pw::thread::zephyr {
+
+// At the moment, Zephyr RTOS doesn't support dynamic thread stack allocation
+// (due to various alignment and size requirements on different architectures).
+// Still, we separate the context in two parts:
+//
+// 1) Context which just contains the Thread Control Block (k_thread) and
+// additional context pw::thread::Thread requires.
+//
+// 2) StaticContextWithStack which contains the stack.
+//
+// Only StaticContextWithStack can be instantiated directly.
+class Context {
+ public:
+ Context(const Context&) = delete;
+ Context& operator=(const Context&) = delete;
+
+ protected:
+ // We can't use `= default` here, because it allows to create an Context
+ // instance in C++17 with `pw::thread::zephyr::Context{}` syntax.
+ Context(){};
+
+ private:
+ friend Thread;
+
+ k_tid_t task_handle() const { return task_handle_; }
+ void set_task_handle(const k_tid_t task_handle) {
+ task_handle_ = task_handle;
+ }
+
+ k_thread& thread_info() { return thread_info_; }
+
+ using ThreadRoutine = void (*)(void* arg);
+ void set_thread_routine(ThreadRoutine entry, void* arg) {
+ user_thread_entry_function_ = entry;
+ user_thread_entry_arg_ = arg;
+ }
+
+ bool detached() const { return detached_; }
+ void set_detached(bool value = true) { detached_ = value; }
+
+ bool thread_done() const { return thread_done_; }
+ void set_thread_done(bool value = true) { thread_done_ = value; }
+
+ static void ThreadEntryPoint(void* void_context_ptr, void*, void*);
+
+ k_tid_t task_handle_ = nullptr;
+ k_thread thread_info_;
+ ThreadRoutine user_thread_entry_function_ = nullptr;
+ void* user_thread_entry_arg_ = nullptr;
+ bool detached_ = false;
+ bool thread_done_ = false;
+};
+
+// Intermediate class to type-erase kStackSizeBytes parameter of
+// StaticContextWithStack.
+class StaticContext : public Context {
+ protected:
+ explicit StaticContext(z_thread_stack_element* stack,
+ size_t available_stack_size)
+ : stack_(stack), available_stack_size_(available_stack_size) {}
+
+ private:
+ friend Thread;
+
+ z_thread_stack_element* stack() { return stack_; }
+ size_t available_stack_size() { return available_stack_size_; }
+
+ // Zephyr RTOS doesn't specify how Zephyr-owned thread information is
+ // stored in the stack, how much spaces it takes, etc.
+ // All we know is that K_THREAD_STACK(stack, size) macro will allocate
+ // enough memory to hold size bytes of user-owned stack and that
+ // we must pass that stack pointer to k_thread_create.
+ z_thread_stack_element* stack_;
+ size_t available_stack_size_;
+};
+
+// Static thread context allocation including the stack along with the Context.
+//
+// See docs.rst for an usage example.
+template <size_t kStackSizeBytes>
+class StaticContextWithStack final : public StaticContext {
+ public:
+ constexpr StaticContextWithStack()
+ : StaticContext(stack_storage_, kStackSizeBytes) {}
+
+ private:
+ K_THREAD_STACK_MEMBER(stack_storage_, kStackSizeBytes);
+};
+
+} // namespace pw::thread::zephyr
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/id_inline.h b/pw_thread_zephyr/public/pw_thread_zephyr/id_inline.h
new file mode 100644
index 000000000..f7c005805
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/id_inline.h
@@ -0,0 +1,30 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <zephyr/kernel.h>
+
+#include "pw_assert/assert.h"
+#include "pw_interrupt/context.h"
+#include "pw_thread/id.h"
+
+namespace pw::this_thread {
+
+inline thread::Id get_id() noexcept {
+ // Ensure this is not being called by an interrupt.
+ PW_DASSERT(!interrupt::InInterruptContext());
+ return thread::Id(k_current_get());
+}
+
+} // namespace pw::this_thread
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/id_native.h b/pw_thread_zephyr/public/pw_thread_zephyr/id_native.h
new file mode 100644
index 000000000..e65fd3b76
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/id_native.h
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <zephyr/kernel.h>
+
+namespace pw::thread::backend {
+
+// Trivial wrapper around Zephyr RTOS-specific k_tid_t type
+// (note that k_tid_t is just a pointer to the k_thread aka TCB).
+class NativeId {
+ public:
+ constexpr NativeId(const k_tid_t thread_id = nullptr)
+ : thread_id_(thread_id) {}
+
+ constexpr bool operator==(NativeId other) const {
+ return thread_id_ == other.thread_id_;
+ }
+ constexpr bool operator!=(NativeId other) const {
+ return thread_id_ != other.thread_id_;
+ }
+ constexpr bool operator<(NativeId other) const {
+ return thread_id_ < other.thread_id_;
+ }
+ constexpr bool operator<=(NativeId other) const {
+ return thread_id_ <= other.thread_id_;
+ }
+ constexpr bool operator>(NativeId other) const {
+ return thread_id_ > other.thread_id_;
+ }
+ constexpr bool operator>=(NativeId other) const {
+ return thread_id_ >= other.thread_id_;
+ }
+
+ private:
+ const k_tid_t thread_id_;
+};
+
+} // namespace pw::thread::backend
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/options.h b/pw_thread_zephyr/public/pw_thread_zephyr/options.h
new file mode 100644
index 000000000..5fb88b3d0
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/options.h
@@ -0,0 +1,70 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_assert/assert.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_zephyr/config.h"
+#include "pw_thread_zephyr/context.h"
+
+namespace pw::thread::zephyr {
+
+// pw::thread::Options for Zephyr RTOS.
+//
+// Example usage:
+//
+// pw::thread::Thread example_thread(
+// pw::thread::zephyr::Options(static_example_thread_context)
+// .set_priority(kFooPriority),
+// example_thread_function, example_arg);
+//
+// TODO(aeremin): Add support for time slice configuration
+// (k_thread_time_slice_set when CONFIG_TIMESLICE_PER_THREAD=y).
+// TODO(aeremin): Add support for thread name
+// (k_thread_name_set when CONFIG_THREAD_MONITOR is enabled).
+class Options : public thread::Options {
+ public:
+ constexpr Options(StaticContext& context) : context_(&context) {}
+ constexpr Options(const Options&) = default;
+ constexpr Options(Options&&) = default;
+
+ // Sets the priority for the Zephyr RTOS thread.
+ // Lower priority values have a higher scheduling priority.
+ constexpr Options& set_priority(int priority) {
+ PW_DASSERT(priority <= config::kLowestSchedulerPriority);
+ PW_DASSERT(priority >= config::kHighestSchedulerPriority);
+ priority_ = priority;
+ return *this;
+ }
+
+ // Sets the Zephyr RTOS native options
+ // (https://docs.zephyrproject.org/latest/kernel/services/threads/index.html#thread-options)
+ constexpr Options& set_native_options(uint32_t native_options) {
+ native_options_ = native_options;
+ return *this;
+ }
+
+ private:
+ friend thread::Thread;
+
+ int priority() const { return priority_; }
+ uint32_t native_options() const { return native_options_; }
+ StaticContext* static_context() const { return context_; }
+
+ int priority_ = config::kDefaultPriority;
+ uint32_t native_options_ = 0;
+ StaticContext* context_ = nullptr;
+};
+
+} // namespace pw::thread::zephyr
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/thread_inline.h b/pw_thread_zephyr/public/pw_thread_zephyr/thread_inline.h
new file mode 100644
index 000000000..672067e56
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/thread_inline.h
@@ -0,0 +1,49 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <algorithm>
+
+#include "pw_assert/assert.h"
+#include "pw_thread/id.h"
+#include "pw_thread/thread.h"
+
+namespace pw::thread {
+
+inline Thread::Thread() {}
+
+inline Thread& Thread::operator=(Thread&& other) {
+ native_type_ = other.native_type_;
+ other.native_type_ = nullptr;
+ return *this;
+}
+
+inline Thread::~Thread() { PW_DASSERT(native_type_ == nullptr); }
+
+inline Id Thread::get_id() const {
+ if (native_type_ == nullptr) {
+ return Id(nullptr);
+ }
+ return Id(native_type_->task_handle());
+}
+
+inline void Thread::swap(Thread& other) {
+ std::swap(native_type_, other.native_type_);
+}
+
+inline Thread::native_handle_type Thread::native_handle() {
+ return native_type_;
+}
+
+} // namespace pw::thread
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/thread_native.h b/pw_thread_zephyr/public/pw_thread_zephyr/thread_native.h
new file mode 100644
index 000000000..269e9ed29
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/thread_native.h
@@ -0,0 +1,30 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_zephyr/context.h"
+
+// Zephyr RTOS threads are always joinable, there is no configuration setting
+// disabling it.
+#define PW_THREAD_JOINING_ENABLED 1
+
+namespace pw::thread::backend {
+
+// The native thread is a pointer to a thread's context.
+using NativeThread = pw::thread::zephyr::Context*;
+
+// The native thread handle is the same as the NativeThread.
+using NativeThreadHandle = pw::thread::zephyr::Context*;
+
+} // namespace pw::thread::backend
diff --git a/pw_thread_zephyr/pw_thread_zephyr_private/thread_iteration.h b/pw_thread_zephyr/pw_thread_zephyr_private/thread_iteration.h
new file mode 100644
index 000000000..c80f1e56d
--- /dev/null
+++ b/pw_thread_zephyr/pw_thread_zephyr_private/thread_iteration.h
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+#include <zephyr/kernel.h>
+
+#include "pw_thread/thread_iteration.h"
+
+namespace pw::thread::zephyr {
+
+// TBD
+
+} // namespace pw::thread::zephyr
diff --git a/pw_thread_zephyr/thread.cc b/pw_thread_zephyr/thread.cc
new file mode 100644
index 000000000..244109ee9
--- /dev/null
+++ b/pw_thread_zephyr/thread.cc
@@ -0,0 +1,118 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_thread/thread.h"
+
+#include <zephyr/kernel.h>
+#include <zephyr/spinlock.h>
+
+#include "pw_assert/check.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_thread/id.h"
+#include "pw_thread_zephyr/config.h"
+#include "pw_thread_zephyr/context.h"
+#include "pw_thread_zephyr/options.h"
+
+using pw::thread::zephyr::Context;
+
+namespace pw::thread {
+namespace {
+
+k_spinlock global_thread_done_lock;
+
+} // namespace
+
+void Context::ThreadEntryPoint(void* void_context_ptr, void*, void*) {
+ Context& context = *static_cast<Context*>(void_context_ptr);
+
+ // Invoke the user's thread function. This may never return.
+ context.user_thread_entry_function_(context.user_thread_entry_arg_);
+
+ k_spinlock_key_t key = k_spin_lock(&global_thread_done_lock);
+ if (context.detached()) {
+ context.set_task_handle(nullptr);
+ } else {
+ // Defer cleanup to Thread's join() or detach().
+ context.set_thread_done();
+ }
+ k_spin_unlock(&global_thread_done_lock, key);
+}
+
+Thread::Thread(const thread::Options& facade_options,
+ ThreadRoutine entry,
+ void* arg)
+ : native_type_(nullptr) {
+ // Cast the generic facade options to the backend specific option of which
+ // only one type can exist at compile time.
+ auto options = static_cast<const zephyr::Options&>(facade_options);
+ PW_CHECK(options.static_context() != nullptr);
+
+ // Use the statically allocated context.
+ native_type_ = options.static_context();
+ // Can't use a context more than once.
+ PW_DCHECK_PTR_EQ(native_type_->task_handle(), nullptr);
+ // Reset the state of the static context in case it was re-used.
+ native_type_->set_detached(false);
+ native_type_->set_thread_done(false);
+
+ native_type_->set_thread_routine(entry, arg);
+ const k_tid_t task_handle =
+ k_thread_create(&native_type_->thread_info(),
+ options.static_context()->stack(),
+ options.static_context()->available_stack_size(),
+ Context::ThreadEntryPoint,
+ options.static_context(),
+ nullptr,
+ nullptr,
+ options.priority(),
+ options.native_options(),
+ K_NO_WAIT);
+ PW_CHECK_NOTNULL(task_handle); // Ensure it succeeded.
+ native_type_->set_task_handle(task_handle);
+}
+
+void Thread::detach() {
+ PW_CHECK(joinable());
+
+ k_spinlock_key_t key = k_spin_lock(&global_thread_done_lock);
+ native_type_->set_detached();
+ const bool thread_done = native_type_->thread_done();
+
+ if (thread_done) {
+ // The task finished (hit end of Context::ThreadEntryPoint) before we
+ // invoked detach, clean up the task handle to allow the Context reuse.
+ native_type_->set_task_handle(nullptr);
+ } else {
+ // We're detaching before the task finished, defer cleanup to the task at
+ // the end of Context::ThreadEntryPoint.
+ }
+
+ k_spin_unlock(&global_thread_done_lock, key);
+
+ // Update to no longer represent a thread of execution.
+ native_type_ = nullptr;
+}
+
+void Thread::join() {
+ PW_CHECK(joinable());
+ PW_CHECK(this_thread::get_id() != get_id());
+
+ PW_CHECK_INT_EQ(0, k_thread_join(native_type_->task_handle_, K_FOREVER));
+
+ native_type_->set_task_handle(nullptr);
+
+ // Update to no longer represent a thread of execution.
+ native_type_ = nullptr;
+}
+
+} // namespace pw::thread
diff --git a/pw_thread_zephyr/thread_iteration.cc b/pw_thread_zephyr/thread_iteration.cc
new file mode 100644
index 000000000..bc120deb5
--- /dev/null
+++ b/pw_thread_zephyr/thread_iteration.cc
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_thread/thread_iteration.h"
+
+#include <zephyr/kernel.h>
+
+namespace pw::thread {
+
+namespace zephyr {
+void zephyr_adapter(const struct k_thread* thread, void* user_data) {
+ const pw::thread::ThreadCallback& cb =
+ *reinterpret_cast<const pw::thread::ThreadCallback*>(user_data);
+
+ ThreadInfo thread_info;
+
+ span<const std::byte> current_name =
+ as_bytes(span(std::string_view(k_thread_name_get((k_tid_t)thread))));
+ thread_info.set_thread_name(current_name);
+
+#ifdef CONFIG_THREAD_STACK_INFO
+ thread_info.set_stack_low_addr(thread->stack_info.start);
+ thread_info.set_stack_pointer(thread->stack_info.start +
+ thread->stack_info.size);
+#endif
+
+ cb(thread_info);
+}
+} // namespace zephyr
+
+Status ForEachThread(const pw::thread::ThreadCallback& cb) {
+ k_thread_user_cb_t adapter_cb = zephyr::zephyr_adapter;
+
+ k_thread_foreach(adapter_cb, (void*)&cb);
+
+ return OkStatus();
+}
+
+} // namespace pw::thread
diff --git a/pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_inline.h
new file mode 100644
index 000000000..c3353433e
--- /dev/null
+++ b/pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_zephyr/thread_inline.h"
diff --git a/pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_native.h b/pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_native.h
new file mode 100644
index 000000000..4954d7daf
--- /dev/null
+++ b/pw_thread_zephyr/thread_public_overrides/pw_thread_backend/thread_native.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_thread_zephyr/thread_native.h"
diff --git a/pw_tls_client/BUILD.bazel b/pw_tls_client/BUILD.bazel
index d9d388db9..6b256af42 100644
--- a/pw_tls_client/BUILD.bazel
+++ b/pw_tls_client/BUILD.bazel
@@ -37,12 +37,25 @@ pw_cc_facade(
"//pw_result",
"//pw_status",
"//pw_stream",
+ "//pw_string",
],
)
pw_cc_library(
name = "pw_tls_client",
- deps = [":pw_tls_client_facade"],
+ hdrs = [
+ "public/pw_tls_client/options.h",
+ "public/pw_tls_client/session.h",
+ "public/pw_tls_client/status.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_result",
+ "//pw_status",
+ "//pw_stream",
+ ],
)
# TODO(zyecheng): Add a "backend_multiplexer" target once BoringSSL/MbedTLS is
@@ -63,9 +76,14 @@ pw_cc_facade(
pw_cc_library(
name = "fake_entropy",
srcs = ["fake_entropy.cc"],
+ hdrs = [
+ "public/pw_tls_client/entropy.h",
+ ],
+ includes = ["public"],
deps = [
- ":entropy_facade",
+ "//pw_bytes",
"//pw_log",
+ "//pw_status",
],
)
@@ -78,7 +96,7 @@ pw_cc_library(
srcs = [
"build_time.cc",
],
- # TODO(b/257527057): Get this to build.
+ # TODO: b/257527057 - Get this to build.
tags = ["manual"],
)
@@ -96,7 +114,7 @@ pw_cc_library(
srcs = ["test_server.cc"],
hdrs = ["public/pw_tls_client/test/test_server.h"],
includes = ["public"],
- # TODO(b/257527057): Get this to build.
+ # TODO: b/257527057 - Get this to build.
tags = ["manual"],
deps = [
"//pw_bytes",
@@ -110,7 +128,7 @@ pw_cc_library(
pw_cc_test(
name = "test_server_test",
srcs = ["test_server_test.cc"],
- # TODO(b/257527057): Get this to build.
+ # TODO: b/257527057 - Get this to build.
tags = ["manual"],
deps = [":test_server"],
)
diff --git a/pw_tls_client/BUILD.gn b/pw_tls_client/BUILD.gn
index c944fda57..d2541af23 100644
--- a/pw_tls_client/BUILD.gn
+++ b/pw_tls_client/BUILD.gn
@@ -80,6 +80,7 @@ pw_facade("time") {
":*",
"$dir_pw_third_party/boringssl",
"$dir_pw_third_party/mbedtls",
+ "$dir_pw_third_party/mbedtls:mbedtls_v3",
]
}
@@ -103,7 +104,7 @@ pw_source_set("build_time") {
]
}
-# TODO(b/235290724): Add a python target to generate source file from the
+# TODO: b/235290724 - Add a python target to generate source file from the
# specified CRLSet file in `pw_tls_client_CRLSET_FILE`
pw_source_set("crlset") {
@@ -111,7 +112,7 @@ pw_source_set("crlset") {
public = [ "public/pw_tls_client/crlset.h" ]
public_deps = [ dir_pw_bytes ]
- # TODO(b/235290724): Add sources generated from a CRLSet file to build.
+ # TODO: b/235290724 - Add sources generated from a CRLSet file to build.
}
pw_source_set("test_server") {
diff --git a/pw_tls_client/docs.rst b/pw_tls_client/docs.rst
index 78c9cf0f2..774abdcd5 100644
--- a/pw_tls_client/docs.rst
+++ b/pw_tls_client/docs.rst
@@ -16,18 +16,18 @@ Write() and Close() methods for TLS communication. An instance is created by
connection options. The list of supported configurations currently include:
1. Host name of the target server. This will be used as the Server Name
-Indication(SNI) extension during TLS handshake.
+ Indication(SNI) extension during TLS handshake.
2. User-implemented transport. The underlying transport for the TLS
-communication. It is an object that implements the interface of
-``pw::stream::ReaderWriter``.
+ communication. It is an object that implements the interface of
+ ``pw::stream::ReaderWriter``.
The module will also provide mechanisms/APIs for users to specify sources of
trust anchors, time and entropy. These are under construction.
.. warning::
- This module is under construction, not ready for use, and the documentation
- is incomplete.
+ This module is under construction, not ready for use, and the documentation
+ is incomplete.
Prerequisites
=============
@@ -50,7 +50,7 @@ support in pw_package for downloading compatible and tested version:
.. code-block:: sh
- pw package install chromium_verifier
+ pw package install chromium_verifier
Then follow instruction for setting ``dir_pw_third_party_chromium_verifier`` to
the path of the downloaded repo.
@@ -90,7 +90,7 @@ run the following command to download the latest version:
.. code-block:: sh
- pw package install crlset --force
+ pw package install crlset --force
The `--force` option forces CRLSet to be always re-downloaded so that it is
up-to-date. Project that are concerned about up-to-date CRLSet should always
@@ -103,14 +103,14 @@ Setup
=====
This module requires the following setup:
- 1. Choose a ``pw_tls_client`` backend, or write one yourself.
- 2. If using GN build, Specify the ``pw_tls_client_BACKEND`` GN build arg to
- point the library that provides a ``pw_tls_client`` backend. To use the
- MbedTLS backend, set variable ``pw_tls_client_BACKEND`` to
- ``//pw_tls_client_mbedtls``. To use the BoringSSL backend, set it to
- ``//pw_tls_client_boringssl``.
- 3. Provide a `pw_tls_client:entropy` backend. If using GN build, specify the
- backend with variable ``pw_tls_client_ENTROPY_BACKEND``.
+1. Choose a ``pw_tls_client`` backend, or write one yourself.
+2. If using GN build, Specify the ``pw_tls_client_BACKEND`` GN build arg to
+ point the library that provides a ``pw_tls_client`` backend. To use the
+ MbedTLS backend, set variable ``pw_tls_client_BACKEND`` to
+ ``//pw_tls_client_mbedtls``. To use the BoringSSL backend, set it to
+ ``//pw_tls_client_boringssl``.
+3. Provide a `pw_tls_client:entropy` backend. If using GN build, specify the
+ backend with variable ``pw_tls_client_ENTROPY_BACKEND``.
Module usage
============
@@ -122,111 +122,109 @@ connection to www.google.com:
.. code-block:: cpp
- // Host domain name
- constexpr char kHost[] = "www.google.com";
-
- constexpr int kPort = 443;
-
- // Server Name Indication.
- constexpr const char* kServerNameIndication = kHost;
-
- // An example message to send.
- constexpr char kHTTPRequest[] = "GET / HTTP/1.1\r\n\r\n";
-
- // pw::stream::SocketStream doesn't accept host domain name as input. Thus we
- // introduce this helper function for getting the IP address
- pw::Status GetIPAddrFromHostName(std::string_view host, pw::span<char> ip) {
- char null_terminated_host_name[256] = {0};
- auto host_copy_status = pw::string::Copy(host, null_terminated_host_name);
- if (!host_copy_status.ok()) {
- return host_copy_status.status();
- }
-
- struct hostent* ent = gethostbyname(null_terminated_host_name);
- if (ent == NULL) {
- return PW_STATUS_INTERNAL;
- }
-
- in_addr** addr_list = reinterpret_cast<in_addr**>(ent->h_addr_list);
- if (addr_list[0] == nullptr) {
- return PW_STATUS_INTERNAL;
- }
-
- auto ip_copy_status = pw::string::Copy(inet_ntoa(*addr_list[0]), ip);
- if (!ip_copy_status.ok()) {
- return ip_copy_status.status();
- }
-
- return pw::OkStatus();
- }
-
- int main() {
- // Get the IP address of the target host.
- char ip_address[64] = {0};
- auto get_ip_status = GetIPAddrFromHostName(kHost, ip_address);
- if (!get_ip_status.ok()) {
- return 1;
- }
-
- // Use a socket stream as the transport.
- pw::stream::SocketStream socket_stream;
-
- // Connect the socket to the remote host.
- auto socket_connect_status = socket_stream.Connect(ip_address, kPort);
- if (!socket_connect_status.ok()) {
- return 1;
- }
-
- // Create a TLS session. Register the transport.
- auto options = pw::tls_client::SessionOptions()
- .set_server_name(kServerNameIndication)
- .set_transport(socket_stream);
- auto tls_conn = pw::tls_client::Session::Create(options);
- if (!tls_conn.ok()) {
- // Handle errors.
- return 1;
- }
-
- auto open_status = tls_conn.value()->Open();
- if (!open_status.ok()) {
- // Inspect/handle error with open_status.code() and
- // tls_conn.value()->GetLastTLSStatus().
- return 1;
- }
-
- auto write_status = tls_conn.value()->Write(pw::as_bytes(pw::span{kHTTPRequest}));
- if (!write_status.ok()) {
- // Inspect/handle error with write_status.code() and
- // tls_conn.value()->GetLastTLSStatus().
- return 0;
- }
-
- // Listen for incoming data.
- std::array<std::byte, 4096> buffer;
- while (true) {
- auto res = tls_conn.value()->Read(buffer);
- if (!res.ok()) {
- // Inspect/handle error with res.status().code() and
- // tls_conn.value()->GetLastTLSStatus().
- return 1;
- }
-
- // Process data in |buffer|. res.value() gives the span of read bytes.
- // The following simply print to console.
- if (res.value().size()) {
- auto print_status = pw::sys_io::WriteBytes(res.value());
- if (!print_status.ok()) {
- return 1;
- }
- }
-
- }
- }
+ // Host domain name
+ constexpr char kHost[] = "www.google.com";
+
+ constexpr int kPort = 443;
+
+ // Server Name Indication.
+ constexpr const char* kServerNameIndication = kHost;
+
+ // An example message to send.
+ constexpr char kHTTPRequest[] = "GET / HTTP/1.1\r\n\r\n";
+
+ // pw::stream::SocketStream doesn't accept host domain name as input. Thus we
+ // introduce this helper function for getting the IP address
+ pw::Status GetIPAddrFromHostName(std::string_view host, pw::span<char> ip) {
+ char null_terminated_host_name[256] = {0};
+ auto host_copy_status = pw::string::Copy(host, null_terminated_host_name);
+ if (!host_copy_status.ok()) {
+ return host_copy_status.status();
+ }
+
+ struct hostent* ent = gethostbyname(null_terminated_host_name);
+ if (ent == NULL) {
+ return PW_STATUS_INTERNAL;
+ }
+
+ in_addr** addr_list = reinterpret_cast<in_addr**>(ent->h_addr_list);
+ if (addr_list[0] == nullptr) {
+ return PW_STATUS_INTERNAL;
+ }
+
+ auto ip_copy_status = pw::string::Copy(inet_ntoa(*addr_list[0]), ip);
+ if (!ip_copy_status.ok()) {
+ return ip_copy_status.status();
+ }
+
+ return pw::OkStatus();
+ }
+
+ int main() {
+ // Get the IP address of the target host.
+ char ip_address[64] = {0};
+ auto get_ip_status = GetIPAddrFromHostName(kHost, ip_address);
+ if (!get_ip_status.ok()) {
+ return 1;
+ }
+
+ // Use a socket stream as the transport.
+ pw::stream::SocketStream socket_stream;
+
+ // Connect the socket to the remote host.
+ auto socket_connect_status = socket_stream.Connect(ip_address, kPort);
+ if (!socket_connect_status.ok()) {
+ return 1;
+ }
+
+ // Create a TLS session. Register the transport.
+ auto options = pw::tls_client::SessionOptions()
+ .set_server_name(kServerNameIndication)
+ .set_transport(socket_stream);
+ auto tls_conn = pw::tls_client::Session::Create(options);
+ if (!tls_conn.ok()) {
+ // Handle errors.
+ return 1;
+ }
+
+ auto open_status = tls_conn.value()->Open();
+ if (!open_status.ok()) {
+ // Inspect/handle error with open_status.code() and
+ // tls_conn.value()->GetLastTLSStatus().
+ return 1;
+ }
+
+ auto write_status = tls_conn.value()->Write(pw::as_bytes(pw::span{kHTTPRequest}));
+ if (!write_status.ok()) {
+ // Inspect/handle error with write_status.code() and
+ // tls_conn.value()->GetLastTLSStatus().
+ return 0;
+ }
+
+ // Listen for incoming data.
+ std::array<std::byte, 4096> buffer;
+ while (true) {
+ auto res = tls_conn.value()->Read(buffer);
+ if (!res.ok()) {
+ // Inspect/handle error with res.status().code() and
+ // tls_conn.value()->GetLastTLSStatus().
+ return 1;
+ }
+
+ // Process data in |buffer|. res.value() gives the span of read bytes.
+ // The following simply print to console.
+ if (res.value().size()) {
+ auto print_status = pw::sys_io::WriteBytes(res.value());
+ if (!print_status.ok()) {
+ return 1;
+ }
+ }
+
+ }
+ }
A list of other demos will be provided in ``//pw_tls_client/examples/``
-Warning
-============
-
-Open()/Read() APIs are synchronous for now. Support for
-non-blocking/asynchronous usage will be added in the future.
+.. warning::
+ Open()/Read() APIs are synchronous for now. Support for
+ non-blocking/asynchronous usage will be added in the future.
diff --git a/pw_tls_client/py/BUILD.gn b/pw_tls_client/py/BUILD.gn
index fc7bbb8b1..b8c28d32d 100644
--- a/pw_tls_client/py/BUILD.gn
+++ b/pw_tls_client/py/BUILD.gn
@@ -19,7 +19,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_tls_client/__init__.py",
diff --git a/pw_tls_client/py/setup.py b/pw_tls_client/py/setup.py
deleted file mode 100644
index 15092a157..000000000
--- a/pw_tls_client/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Python scripts for pw_tls_client"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_tls_client/test_server_test.cc b/pw_tls_client/test_server_test.cc
index 3e76aef85..5a15e92f5 100644
--- a/pw_tls_client/test_server_test.cc
+++ b/pw_tls_client/test_server_test.cc
@@ -90,7 +90,8 @@ void CreateSSLClient(bssl::UniquePtr<SSL_CTX>* ctx,
// Load trust anchors to client
auto store = SSL_CTX_get_cert_store(ctx->get());
- X509_VERIFY_PARAM_clear_flags(store->param, X509_V_FLAG_USE_CHECK_TIME);
+ X509_VERIFY_PARAM_clear_flags(X509_STORE_get0_param(store),
+ X509_V_FLAG_USE_CHECK_TIME);
const pw::ConstByteSpan kTrustAnchors[] = {kRootACert, kRootBCert};
for (auto cert : kTrustAnchors) {
auto res = ParseDerCertificate(cert);
diff --git a/pw_tls_client_boringssl/BUILD.bazel b/pw_tls_client_boringssl/BUILD.bazel
index 38e25d338..084bda7b5 100644
--- a/pw_tls_client_boringssl/BUILD.bazel
+++ b/pw_tls_client_boringssl/BUILD.bazel
@@ -43,7 +43,7 @@ pw_cc_test(
srcs = [
"tls_client_boringssl_test.cc",
],
- #TODO(b/257529537): Get this to build.
+ #TODO: b/257529537 - Get this to build.
tags = ["manual"],
deps = [
":pw_tls_client_boringssl",
diff --git a/pw_tls_client_boringssl/tls_client_boringssl.cc b/pw_tls_client_boringssl/tls_client_boringssl.cc
index aeb4c75ae..6f871fd83 100644
--- a/pw_tls_client_boringssl/tls_client_boringssl.cc
+++ b/pw_tls_client_boringssl/tls_client_boringssl.cc
@@ -19,7 +19,7 @@ namespace pw::tls_client {
namespace backend {
SessionImplementation::SessionImplementation(SessionOptions) {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
}
SessionImplementation::~SessionImplementation() = default;
@@ -27,33 +27,33 @@ SessionImplementation::~SessionImplementation() = default;
} // namespace backend
Session::Session(const SessionOptions& options) : session_impl_(options) {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
}
Session::~Session() = default;
Result<Session*> Session::Create(const SessionOptions&) {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
return PW_STATUS_UNIMPLEMENTED;
}
Status Session::Open() {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
return PW_STATUS_UNIMPLEMENTED;
}
Status Session::Close() {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
return PW_STATUS_UNIMPLEMENTED;
}
StatusWithSize Session::DoRead(ByteSpan) {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
return StatusWithSize(PW_STATUS_UNIMPLEMENTED, 0);
}
Status Session::DoWrite(ConstByteSpan) {
- // TODO(b/235291139): To implement
+ // TODO: b/235291139 - To implement
return PW_STATUS_UNIMPLEMENTED;
}
diff --git a/pw_tls_client_boringssl/tls_client_boringssl_test.cc b/pw_tls_client_boringssl/tls_client_boringssl_test.cc
index 0157d53fa..abb4dff2c 100644
--- a/pw_tls_client_boringssl/tls_client_boringssl_test.cc
+++ b/pw_tls_client_boringssl/tls_client_boringssl_test.cc
@@ -28,4 +28,4 @@ TEST(TLSClientBoringSSL, SessionCreationSucceeds) {
ASSERT_EQ(res.status(), PW_STATUS_UNIMPLEMENTED);
}
-} // namespace pw::tls_client \ No newline at end of file
+} // namespace pw::tls_client
diff --git a/pw_tls_client_mbedtls/BUILD.bazel b/pw_tls_client_mbedtls/BUILD.bazel
index 5833994d7..9b0a34040 100644
--- a/pw_tls_client_mbedtls/BUILD.bazel
+++ b/pw_tls_client_mbedtls/BUILD.bazel
@@ -33,7 +33,7 @@ pw_cc_library(
"public",
"public_overrides",
],
- # TODO(b/258068735): Get this target to build. Requires adding mbedtls
+ # TODO: b/258068735 - Get this target to build. Requires adding mbedtls
# build targets.
tags = ["manual"],
deps = [
@@ -47,7 +47,7 @@ pw_cc_test(
srcs = [
"tls_client_mbedtls_test.cc",
],
- # TODO(b/258068735): Get this target to build. Requires adding mbedtls
+ # TODO: b/258068735 - Get this target to build. Requires adding mbedtls
# build targets.
tags = ["manual"],
deps = [
diff --git a/pw_tls_client_mbedtls/BUILD.gn b/pw_tls_client_mbedtls/BUILD.gn
index 261b85a0b..5475f5cd7 100644
--- a/pw_tls_client_mbedtls/BUILD.gn
+++ b/pw_tls_client_mbedtls/BUILD.gn
@@ -42,14 +42,13 @@ pw_source_set("pw_tls_client_mbedtls") {
"$dir_pw_assert",
"$dir_pw_log",
"$dir_pw_stream",
- "$dir_pw_third_party/mbedtls",
+ "$dir_pw_third_party/mbedtls:mbedtls",
"$dir_pw_tls_client:tls_entropy",
]
}
pw_test("tls_client_mbedtls_test") {
- enable_if =
- pw_tls_client_ENTROPY_BACKEND != "" && dir_pw_third_party_mbedtls != ""
+ enable_if = pw_tls_client_BACKEND != "" && dir_pw_third_party_mbedtls != ""
deps = [ ":pw_tls_client_mbedtls" ]
sources = [ "tls_client_mbedtls_test.cc" ]
}
diff --git a/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h b/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h
index e7cb83ef7..ef8b22687 100644
--- a/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h
+++ b/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h
@@ -18,7 +18,6 @@
PW_MODIFY_DIAGNOSTICS_PUSH();
PW_MODIFY_DIAGNOSTIC(ignored, "-Wswitch-enum");
-#include "mbedtls/certs.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include "mbedtls/error.h"
diff --git a/pw_tls_client_mbedtls/tls_client_mbedtls.cc b/pw_tls_client_mbedtls/tls_client_mbedtls.cc
index c28990138..47de9fccc 100644
--- a/pw_tls_client_mbedtls/tls_client_mbedtls.cc
+++ b/pw_tls_client_mbedtls/tls_client_mbedtls.cc
@@ -144,7 +144,7 @@ Status SessionImplementation::Setup() {
// The API does not fail.
mbedtls_ssl_conf_authmode(&ssl_config_, MBEDTLS_SSL_VERIFY_REQUIRED);
- // TODO(b/235289501): Add logic for loading trust anchors.
+ // TODO: b/235289501 - Add logic for loading trust anchors.
// Load configuration to SSL.
ret = mbedtls_ssl_setup(&ssl_ctx_, &ssl_config_);
@@ -186,7 +186,7 @@ Result<Session*> Session::Create(const SessionOptions& options) {
auto setup_status = sess->session_impl_.Setup();
if (!setup_status.ok()) {
PW_LOG_DEBUG("Failed to setup");
- // TODO(b/235289501): `tls_status_` may be set, but the session object will
+ // TODO: b/235289501 - `tls_status_` may be set, but the session object will
// be released. Map `tls_stauts_` to string and print out here so that
// the information can be catched.
delete sess;
@@ -197,22 +197,22 @@ Result<Session*> Session::Create(const SessionOptions& options) {
}
Status Session::Open() {
- // TODO(b/235289501): To implement
+ // TODO: b/235289501 - To implement
return Status::Unimplemented();
}
Status Session::Close() {
- // TODO(b/235289501): To implement
+ // TODO: b/235289501 - To implement
return Status::Unimplemented();
}
StatusWithSize Session::DoRead(ByteSpan) {
- // TODO(b/235289501): To implement
+ // TODO: b/235289501 - To implement
return StatusWithSize(Status::Unimplemented(), 0);
}
Status Session::DoWrite(ConstByteSpan) {
- // TODO(b/235289501): To implement
+ // TODO: b/235289501 - To implement
return Status::Unimplemented();
}
diff --git a/pw_tls_client_mbedtls/tls_client_mbedtls_test.cc b/pw_tls_client_mbedtls/tls_client_mbedtls_test.cc
index 66242e8ad..1d8afedf2 100644
--- a/pw_tls_client_mbedtls/tls_client_mbedtls_test.cc
+++ b/pw_tls_client_mbedtls/tls_client_mbedtls_test.cc
@@ -38,4 +38,4 @@ TEST(TLSClientMbedTLS, EntropySourceFail) {
ASSERT_NE(res.status(), OkStatus());
}
-} // namespace pw::tls_client \ No newline at end of file
+} // namespace pw::tls_client
diff --git a/pw_tokenizer/Android.bp b/pw_tokenizer/Android.bp
index d62f1cb04..d3b1614e3 100644
--- a/pw_tokenizer/Android.bp
+++ b/pw_tokenizer/Android.bp
@@ -20,18 +20,173 @@ cc_library_static {
name: "pw_detokenizer",
cpp_std: "c++20",
vendor_available: true,
+ host_supported: true,
export_include_dirs: ["public"],
+ header_libs: [
+ "fuchsia_sdk_lib_stdcompat",
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_log_tokenized_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
+ export_header_lib_headers: [
+ "fuchsia_sdk_lib_stdcompat",
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
srcs: [
+ "base64.cc",
"detokenize.cc",
"decode.cc",
],
+ static_libs: [
+ "pw_base64",
+ "pw_bytes",
+ "pw_containers",
+ "pw_varint"
+ ],
+ export_static_lib_headers: [
+ "pw_base64",
+ "pw_bytes",
+ "pw_containers",
+ "pw_varint",
+ ],
+}
+
+cc_library_static {
+ name: "pw_tokenizer",
+ cpp_std: "c++20",
+ host_supported: true,
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ srcs: [
+ "encode_args.cc",
+ "hash.cc",
+ "tokenize.cc",
+ ],
header_libs: [
"pw_polyfill_headers",
"pw_preprocessor_headers",
"pw_span_headers",
],
+ export_header_lib_headers: [
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
static_libs: [
- "pw_bytes",
- "pw_varint"
+ "pw_containers",
+ "pw_varint",
+ ],
+ export_static_lib_headers: [
+ "pw_containers",
+ "pw_varint",
+ ],
+}
+
+cc_library_static {
+ name: "pw_tokenizer_base64",
+ cpp_std: "c++20",
+ host_supported: true,
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ srcs: [
+ "base64.cc",
+ ],
+ header_libs: [
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
+ export_header_lib_headers: [
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
+ static_libs: [
+ "pw_base64",
+ "pw_containers",
+ "pw_string",
+ "pw_tokenizer",
+ ],
+ export_static_lib_headers: [
+ "pw_base64",
+ "pw_containers",
+ "pw_string",
+ "pw_tokenizer",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_tokenizer_pwpb_headers",
+ cpp_std: "c++20",
+ vendor_available: true,
+ host_supported: true,
+ generated_headers: [
+ "google_protobuf_descriptor_pwpb_h",
+ "pw_tokenizer_proto_options_pwpb_h",
+ ],
+ export_generated_headers: [
+ "google_protobuf_descriptor_pwpb_h",
+ "pw_tokenizer_proto_options_pwpb_h",
+ ],
+}
+
+// Copies the proto files to a prefix directory to add the prefix to the
+// compiled proto. The prefix is taken from the directory name of the first
+// item listen in out.
+genrule_defaults {
+ name: "pw_tokenizer_add_prefix_to_proto",
+ cmd: "out_files=($(out)); prefix=$$(dirname $${out_files[0]}); " +
+ "mkdir -p $${prefix}; cp -t $${prefix} $(in);"
+}
+
+// The proto subdirectory is added to support the desired options.proto
+// filepath.
+genrule {
+ name: "pw_tokenizer_options_proto_with_prefix",
+ defaults: ["pw_tokenizer_add_prefix_to_proto"],
+ srcs: [
+ "options.proto",
+ ],
+ out: [
+ "pw_tokenizer/proto/options.proto",
+ ],
+}
+
+// Expose the generated pw_tokenizer/proto/options.proto.
+filegroup {
+ name: "pw_tokenizer_proto_options_proto",
+ srcs: [":pw_tokenizer_options_proto_with_prefix"],
+}
+
+genrule {
+ name: "pw_tokenizer_proto_options_pwpb_h",
+ srcs: [
+ ":libprotobuf-internal-protos",
+ ":pw_tokenizer_options_proto_with_prefix",
+ ],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--proto-path=external/pigweed/pw_tokenizer/ " +
+ "--proto-path=external/protobuf/src/ " +
+ "--out-dir=$$(dirname $(location pw_tokenizer/proto/options.pwpb.h)) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$$(dirname $(location :pw_tokenizer_options_proto_with_prefix)) " +
+ "--sources $(location :pw_tokenizer_options_proto_with_prefix) " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) ",
+ out: [
+ "pw_tokenizer/proto/options.pwpb.h",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
],
}
diff --git a/pw_tokenizer/BUILD.bazel b/pw_tokenizer/BUILD.bazel
index 26ce5dcbb..812a7df0b 100644
--- a/pw_tokenizer/BUILD.bazel
+++ b/pw_tokenizer/BUILD.bazel
@@ -17,9 +17,10 @@ load(
"pw_cc_binary",
"pw_cc_library",
"pw_cc_test",
+ "pw_linker_script",
)
-load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
package(default_visibility = ["//visibility:public"])
@@ -44,6 +45,7 @@ pw_cc_library(
hdrs = [
"public/pw_tokenizer/encode_args.h",
"public/pw_tokenizer/hash.h",
+ "public/pw_tokenizer/nested_tokenization.h",
"public/pw_tokenizer/tokenize.h",
],
includes = ["public"],
@@ -57,17 +59,14 @@ pw_cc_library(
],
)
-pw_cc_library(
- name = "test_backend",
- visibility = ["@pigweed_config//:__pkg__"],
+pw_linker_script(
+ name = "linker_script",
+ linker_script = "pw_tokenizer_linker_sections.ld",
)
-# This header is only provided for backwards compatibility and is not used in
-# the Bazel build.
pw_cc_library(
- name = "global_handler_with_payload",
- hdrs = ["public/pw_tokenizer/tokenize_to_global_handler_with_payload.h"],
- visibility = ["//pw_log_tokenized:__pkg__"],
+ name = "test_backend",
+ visibility = ["@pigweed//targets:__pkg__"],
)
pw_cc_library(
@@ -102,6 +101,7 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ ":base64",
"//pw_bytes",
"//pw_span",
"//pw_varint",
@@ -120,7 +120,7 @@ proto_library(
],
)
-# TODO(b/241456982): Not expected to build yet.
+# TODO: b/241456982 - Not expected to build yet.
py_proto_library(
name = "tokenizer_proto_py_pb2",
tags = ["manual"],
@@ -208,7 +208,6 @@ pw_cc_fuzz_test(
deps = [
":decoder",
":pw_tokenizer",
- "//pw_fuzzer",
],
)
@@ -271,6 +270,26 @@ pw_cc_test(
],
)
+pw_cc_test(
+ name = "tokenize_c99_test",
+ srcs = ["tokenize_c99_test_entry_point.cc"],
+ deps = [
+ ":tokenize_c99_test_c",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_library(
+ name = "tokenize_c99_test_c",
+ srcs = ["tokenize_c99_test.c"],
+ copts = ["-std=c99"],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":pw_tokenizer",
+ "//pw_containers:variable_length_entry_queue",
+ ],
+)
+
# Create a shared library for the tokenizer JNI wrapper. The include paths for
# the JNI headers must be available in the system or provided with the
# pw_java_native_interface_include_dirs variable.
diff --git a/pw_tokenizer/BUILD.gn b/pw_tokenizer/BUILD.gn
index fc276f456..ab488af54 100644
--- a/pw_tokenizer/BUILD.gn
+++ b/pw_tokenizer/BUILD.gn
@@ -22,7 +22,6 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_fuzzer/fuzzer.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_unit_test/test.gni")
-import("backend.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -37,10 +36,7 @@ config("public_include_path") {
}
config("linker_script") {
- inputs = [
- "pw_tokenizer_linker_sections.ld",
- "pw_tokenizer_linker_rules.ld",
- ]
+ inputs = [ "pw_tokenizer_linker_sections.ld" ]
lib_dirs = [ "." ]
# Automatically add the tokenizer linker sections when cross-compiling or
@@ -81,11 +77,12 @@ pw_source_set("pw_tokenizer") {
dir_pw_polyfill,
dir_pw_preprocessor,
dir_pw_span,
+ dir_pw_varint,
]
- deps = [ dir_pw_varint ]
public = [
"public/pw_tokenizer/encode_args.h",
"public/pw_tokenizer/hash.h",
+ "public/pw_tokenizer/nested_tokenization.h",
"public/pw_tokenizer/tokenize.h",
]
sources = [
@@ -116,19 +113,16 @@ pw_source_set("base64") {
]
}
-# TODO(hepler): Remove this backwards compatibility header after projects have
-# migrated.
-pw_source_set("global_handler_with_payload") {
- public_configs = [ ":public_include_path" ]
- public = [ "public/pw_tokenizer/tokenize_to_global_handler_with_payload.h" ]
- public_deps = [ dir_pw_preprocessor ]
-}
-
pw_source_set("decoder") {
public_configs = [ ":public_include_path" ]
- public_deps = [ dir_pw_span ]
+ public_deps = [
+ dir_pw_preprocessor,
+ dir_pw_span,
+ ]
deps = [
+ ":base64",
"$dir_pw_bytes:bit",
+ dir_pw_base64,
dir_pw_bytes,
dir_pw_varint,
]
@@ -172,20 +166,22 @@ pw_test_group("tests") {
":argument_types_test",
":base64_test",
":decode_test",
- ":detokenize_fuzzer_test",
":detokenize_test",
":encode_args_test",
":hash_test",
":simple_tokenize_test",
- ":token_database_fuzzer_test",
":token_database_test",
":tokenize_test",
+ ":tokenize_c99_test",
+ ]
+ group_deps = [
+ ":fuzzers",
+ "$dir_pw_preprocessor:tests",
]
- group_deps = [ "$dir_pw_preprocessor:tests" ]
}
-group("fuzzers") {
- deps = [
+pw_fuzzer_group("fuzzers") {
+ fuzzers = [
":detokenize_fuzzer",
":token_database_fuzzer",
]
@@ -272,23 +268,32 @@ pw_test("tokenize_test") {
]
}
+pw_test("tokenize_c99_test") {
+ cflags_c = [ "-std=c99" ]
+ sources = [
+ "tokenize_c99_test.c",
+ "tokenize_c99_test_entry_point.cc",
+ ]
+ deps = [
+ ":pw_tokenizer",
+ "$dir_pw_containers:variable_length_entry_queue",
+ ]
+}
+
pw_fuzzer("token_database_fuzzer") {
sources = [ "token_database_fuzzer.cc" ]
deps = [
":decoder",
- "$dir_pw_fuzzer",
- "$dir_pw_preprocessor",
+ dir_pw_preprocessor,
dir_pw_span,
]
- enable_test_if = false
}
pw_fuzzer("detokenize_fuzzer") {
sources = [ "detokenize_fuzzer.cc" ]
deps = [
":decoder",
- "$dir_pw_fuzzer",
- "$dir_pw_preprocessor",
+ dir_pw_preprocessor,
]
}
@@ -329,8 +334,12 @@ pw_shared_library("detokenizer_jni") {
pw_doc_group("docs") {
sources = [
+ "api.rst",
+ "detokenization.rst",
"docs.rst",
- "proto.rst",
+ "get_started.rst",
+ "token_databases.rst",
+ "tokenization.rst",
]
inputs = [ "py/pw_tokenizer/encode.py" ]
report_deps = [ ":tokenizer_size_report" ]
diff --git a/pw_tokenizer/CMakeLists.txt b/pw_tokenizer/CMakeLists.txt
index cd4a419b8..e2d5e3bdd 100644
--- a/pw_tokenizer/CMakeLists.txt
+++ b/pw_tokenizer/CMakeLists.txt
@@ -30,6 +30,7 @@ pw_add_library(pw_tokenizer STATIC
HEADERS
public/pw_tokenizer/encode_args.h
public/pw_tokenizer/hash.h
+ public/pw_tokenizer/nested_tokenization.h
public/pw_tokenizer/tokenize.h
PUBLIC_INCLUDES
public
@@ -38,6 +39,7 @@ pw_add_library(pw_tokenizer STATIC
pw_span
pw_preprocessor
pw_tokenizer.config
+ pw_varint
SOURCES
encode_args.cc
hash.cc
@@ -50,12 +52,10 @@ pw_add_library(pw_tokenizer STATIC
public/pw_tokenizer/internal/pw_tokenizer_65599_fixed_length_96_hash_macro.h
public/pw_tokenizer/internal/tokenize_string.h
tokenize.cc
- PRIVATE_DEPS
- pw_varint
)
-if(Zephyr_FOUND)
- zephyr_linker_sources(SECTIONS "${CMAKE_CURRENT_SOURCE_DIR}/pw_tokenizer_linker_rules.ld")
+if(Zephyr_FOUND AND CONFIG_PIGWEED_TOKENIZER)
+ zephyr_linker_sources(SECTIONS "${CMAKE_CURRENT_SOURCE_DIR}/pw_tokenizer_zephyr.ld")
elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "")
target_link_options(pw_tokenizer
PUBLIC
@@ -94,6 +94,7 @@ pw_add_library(pw_tokenizer.decoder STATIC
PUBLIC_DEPS
pw_span
pw_tokenizer
+ pw_tokenizer.base64
SOURCES
decode.cc
detokenize.cc
diff --git a/pw_tokenizer/Kconfig b/pw_tokenizer/Kconfig
index 99d128068..167587770 100644
--- a/pw_tokenizer/Kconfig
+++ b/pw_tokenizer/Kconfig
@@ -12,22 +12,32 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_tokenizer"
+
config PIGWEED_TOKENIZER
- bool "Enable the pw_tokenizer library"
- select PIGWEED_CONTAINERS
- select PIGWEED_PREPROCESSOR
- select PIGWEED_SPAN
- select PIGWEED_VARINT
+ bool "Link pw_tokenizer library"
+ select PIGWEED_CONTAINERS
+ select PIGWEED_PREPROCESSOR
+ select PIGWEED_SPAN
+ select PIGWEED_VARINT
+ help
+ See :ref:`module-pw_tokenizer` for module details.
config PIGWEED_TOKENIZER_BASE64
- bool "Enable the pw_tokenizer.base64 library"
- select PIGWEED_TOKENIZER
- select PIGWEED_BASE64
- select PIGWEED_SPAN
+ bool "Link pw_tokenizer.base64 library"
+ select PIGWEED_TOKENIZER
+ select PIGWEED_BASE64
+ select PIGWEED_SPAN
+ help
+ See :ref:`module-pw_tokenizer` for module details.
config PIGWEED_DETOKENIZER
- bool "Enable the pw_tokenizer.decoder library"
- select PIGWEED_TOKENIZER
- select PIGWEED_SPAN
- select PIGWEED_BYTES
- select PIGWEED_VARINT \ No newline at end of file
+ bool "Link pw_tokenizer.decoder library"
+ select PIGWEED_TOKENIZER
+ select PIGWEED_SPAN
+ select PIGWEED_BYTES
+ select PIGWEED_VARINT
+ help
+ See :ref:`module-pw_tokenizer` for module details.
+
+endmenu
diff --git a/pw_tokenizer/OWNERS b/pw_tokenizer/OWNERS
index d96cbc68d..6b1127e08 100644
--- a/pw_tokenizer/OWNERS
+++ b/pw_tokenizer/OWNERS
@@ -1 +1,2 @@
+cachinchilla@google.com
hepler@google.com
diff --git a/pw_tokenizer/api.rst b/pw_tokenizer/api.rst
new file mode 100644
index 000000000..7539c1cc9
--- /dev/null
+++ b/pw_tokenizer/api.rst
@@ -0,0 +1,94 @@
+:tocdepth: 2
+
+.. _module-pw_tokenizer-api:
+
+==========================
+pw_tokenizer API reference
+==========================
+.. pigweed-module-subpage::
+ :name: pw_tokenizer
+ :tagline: Compress strings to shrink logs by +75%
+
+.. _module-pw_tokenizer-api-configuration:
+
+-------------
+Configuration
+-------------
+.. tab-set::
+
+ .. tab-item:: C++ / C
+ :sync: cpp
+
+ .. doxygenfile:: pw_tokenizer/config.h
+ :sections: define
+
+------------
+Tokenization
+------------
+.. tab-set::
+
+ .. tab-item:: C++ / C
+ :sync: cpp
+
+ .. doxygenfunction:: pw::tokenizer::EncodeArgs
+ .. doxygenclass:: pw::tokenizer::EncodedMessage
+ :members:
+ .. doxygenfunction:: pw::tokenizer::MinEncodingBufferSizeBytes
+ .. doxygendefine:: PW_TOKEN_FMT
+ .. doxygendefine:: PW_TOKENIZE_FORMAT_STRING
+ .. doxygendefine:: PW_TOKENIZE_FORMAT_STRING_ANY_ARG_COUNT
+ .. doxygendefine:: PW_TOKENIZE_STRING
+ .. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN
+ .. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN_EXPR
+ .. doxygendefine:: PW_TOKENIZE_STRING_EXPR
+ .. doxygendefine:: PW_TOKENIZE_STRING_MASK
+ .. doxygendefine:: PW_TOKENIZE_STRING_MASK_EXPR
+ .. doxygendefine:: PW_TOKENIZE_TO_BUFFER
+ .. doxygendefine:: PW_TOKENIZE_TO_BUFFER_DOMAIN
+ .. doxygendefine:: PW_TOKENIZE_TO_BUFFER_MASK
+ .. doxygendefine:: PW_TOKENIZER_REPLACE_FORMAT_STRING
+ .. doxygendefine:: PW_TOKENIZER_ARG_TYPES
+ .. doxygenfunction:: pw_tokenizer_EncodeArgs
+ .. doxygenfunction:: pw_tokenizer_EncodeInt
+ .. doxygenfunction:: pw_tokenizer_EncodeInt64
+ .. doxygentypedef:: pw_tokenizer_Token
+
+ .. tab-item:: Python
+ :sync: py
+
+ .. autofunction:: pw_tokenizer.encode.encode_token_and_args
+ .. autofunction:: pw_tokenizer.tokens.pw_tokenizer_65599_hash
+
+ .. tab-item:: Rust
+ :sync: rs
+
+ See `Crate pw_tokenizer </rustdoc/pw_tokenizer/>`_.
+
+.. _module-pw_tokenizer-api-token-databases:
+
+---------------
+Token databases
+---------------
+.. tab-set::
+
+ .. tab-item:: C++ / C
+ :sync: cpp
+
+ .. doxygenclass:: pw::tokenizer::TokenDatabase
+ :members:
+
+.. _module-pw_tokenizer-api-detokenization:
+
+--------------
+Detokenization
+--------------
+.. tab-set::
+
+ .. tab-item:: Python
+ :sync: py
+
+ .. automodule:: pw_tokenizer.detokenize
+ :members:
+
+ .. automodule:: pw_tokenizer.proto
+ :members:
diff --git a/pw_tokenizer/base64.cc b/pw_tokenizer/base64.cc
index 658ee5930..7872dabcd 100644
--- a/pw_tokenizer/base64.cc
+++ b/pw_tokenizer/base64.cc
@@ -32,7 +32,7 @@ extern "C" size_t pw_tokenizer_PrefixedBase64Encode(
return 0;
}
- output[0] = kBase64Prefix;
+ output[0] = PW_TOKENIZER_NESTED_PREFIX;
base64::Encode(
span(static_cast<const std::byte*>(binary_message), binary_size_bytes),
&output[1]);
@@ -42,7 +42,7 @@ extern "C" size_t pw_tokenizer_PrefixedBase64Encode(
void PrefixedBase64Encode(span<const std::byte> binary_message,
InlineString<>& output) {
- output.push_back(kBase64Prefix);
+ output.push_back(PW_TOKENIZER_NESTED_PREFIX);
base64::Encode(binary_message, output);
}
@@ -52,7 +52,7 @@ extern "C" size_t pw_tokenizer_PrefixedBase64Decode(const void* base64_message,
size_t output_buffer_size) {
const char* base64 = static_cast<const char*>(base64_message);
- if (base64_size_bytes == 0 || base64[0] != kBase64Prefix) {
+ if (base64_size_bytes == 0 || base64[0] != PW_TOKENIZER_NESTED_PREFIX) {
return 0;
}
diff --git a/pw_tokenizer/base64_test.cc b/pw_tokenizer/base64_test.cc
index fee5a673d..babc87564 100644
--- a/pw_tokenizer/base64_test.cc
+++ b/pw_tokenizer/base64_test.cc
@@ -97,7 +97,7 @@ TEST_F(PrefixedBase64, Encode_NoRoomForNullAfterMessage_OnlyNullTerminates) {
TEST_F(PrefixedBase64, Encode_InlineString) {
for (auto& [binary, base64] : kTestData) {
- EXPECT_EQ(base64, PrefixedBase64Encode(binary));
+ EXPECT_EQ(base64, PrefixedBase64Encode<64>(binary));
}
}
diff --git a/pw_tokenizer/database.cmake b/pw_tokenizer/database.cmake
new file mode 100644
index 000000000..3d529471f
--- /dev/null
+++ b/pw_tokenizer/database.cmake
@@ -0,0 +1,124 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+include_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# This function creates a library under the specified ${NAME} which provides a
+# generated token database for a given ELF file using pw_tokenizer/database.py.
+#
+# Produces the ${NAME} token database.
+#
+# Args:
+#
+# NAME - name of the library to create
+# COMMIT - Deletes temporary tokens in memory and on disk when a CSV exists
+# within a commit.
+# CREATE - Create a database. Must be set to one of the supported database
+# types: "csv" or "binary".
+# DATABASE - If updating a database, path to an existing database in the
+# source tree; optional if creating a database, but may provide an output
+# directory path to override the default of
+# "$target_gen_dir/$target_name.[csv/binary]"
+# DEPS - CMake targets to build prior to generating the database; artifacts
+# from these targets are NOT implicitly used for database generation.
+# DOMAIN - If provided, extract strings from tokenization domains matching
+# this regular expression.
+# TARGET - CMake target (executable or library) from which to add tokens;
+# this target is also added to deps.
+function(pw_tokenizer_database NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ COMMIT
+ CREATE
+ DATABASE
+ DEPS
+ DOMAIN
+ TARGET
+ REQUIRED_ARGS
+ TARGET
+ )
+
+ if(NOT DEFINED arg_CREATE AND NOT DEFINED arg_DATABASE)
+ message(FATAL_ERROR "pw_tokenizer_database requires a `database` "
+ "variable, unless 'CREATE' is specified")
+ endif()
+
+ set(_create "")
+ if(DEFINED arg_CREATE)
+ if (NOT (${arg_CREATE} STREQUAL "csv" OR ${arg_CREATE} STREQUAL "binary"))
+ message(FATAL_ERROR "'CREATE' must be \"csv\" or \"binary\".")
+ endif()
+ set(_create ${arg_CREATE})
+ set(_create_new_database TRUE)
+ endif()
+
+ set(_database "")
+ if(DEFINED arg_DATABASE)
+ set(_database ${arg_DATABASE})
+ else()
+ # Default to appending the create type as the extension.
+ set(_database ${NAME}.${_create})
+ endif()
+
+ set(_domain "")
+ if(DEFINED arg_DOMAIN)
+ set(_domain "#${arg_DOMAIN}")
+ endif()
+
+ add_library(${NAME} INTERFACE)
+ add_dependencies(${NAME} INTERFACE ${NAME}_generated_token_db)
+
+ if (DEFINED _create_new_database)
+ add_custom_command(
+ COMMENT "Generating the ${_database} token database"
+ COMMAND
+ ${Python3_EXECUTABLE}
+ "$ENV{PW_ROOT}/pw_tokenizer/py/pw_tokenizer/database.py" create
+ --database ${_database}
+ --type ${_create}
+ "$<TARGET_FILE:${arg_TARGET}>${_domain}"
+ --force
+ DEPENDS
+ ${arg_DEPS}
+ ${arg_TARGET}
+ OUTPUT ${_database} POST_BUILD
+ )
+ else()
+ set(_discard_temporary "")
+ if(DEFINED arg_COMMIT)
+ set(_discard_temporary "--discard-temporary ${arg_COMMIT}")
+ endif()
+
+ add_custom_command(
+ COMMENT "Updating the ${_database} token database"
+ COMMAND
+ ${Python3_EXECUTABLE}
+ "$ENV{PW_ROOT}/pw_tokenizer/py/pw_tokenizer/database.py" add
+ --database ${_database}
+ ${_discard_temporary}
+ "$<TARGET_FILE:${arg_TARGET}>${_domain}"
+ DEPENDS
+ ${arg_DEPS}
+ ${arg_TARGET}
+ OUTPUT ${_database} POST_BUILD
+ )
+ endif()
+
+ add_custom_target(${NAME}_generated_token_db
+ DEPENDS ${_database}
+ )
+endfunction()
diff --git a/pw_tokenizer/detokenization.rst b/pw_tokenizer/detokenization.rst
new file mode 100644
index 000000000..7fbefec88
--- /dev/null
+++ b/pw_tokenizer/detokenization.rst
@@ -0,0 +1,583 @@
+:tocdepth: 3
+
+.. _module-pw_tokenizer-detokenization:
+
+==============
+Detokenization
+==============
+.. pigweed-module-subpage::
+ :name: pw_tokenizer
+ :tagline: Compress strings to shrink logs by +75%
+
+Detokenization is the process of expanding a token to the string it represents
+and decoding its arguments. ``pw_tokenizer`` provides Python, C++ and
+TypeScript detokenization libraries.
+
+--------------------------------
+Example: decoding tokenized logs
+--------------------------------
+A project might tokenize its log messages with the
+:ref:`module-pw_tokenizer-base64-format`. Consider the following log file, which
+has four tokenized logs and one plain text log:
+
+.. code-block:: text
+
+ 20200229 14:38:58 INF $HL2VHA==
+ 20200229 14:39:00 DBG $5IhTKg==
+ 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
+ 20200229 14:39:21 INF $EgFj8lVVAUI=
+ 20200229 14:39:23 ERR $DFRDNwlOT1RfUkVBRFk=
+
+The project's log strings are stored in a database like the following:
+
+.. code-block::
+
+ 1c95bd1c, ,"Initiating retrieval process for recovery object"
+ 2a5388e4, ,"Determining optimal approach and coordinating vectors"
+ 3743540c, ,"Recovery object retrieval failed with status %s"
+ f2630112, ,"Calculated acceptable probability of success (%.2f%%)"
+
+Using the detokenizing tools with the database, the logs can be decoded:
+
+.. code-block:: text
+
+ 20200229 14:38:58 INF Initiating retrieval process for recovery object
+ 20200229 14:39:00 DBG Determining optimal algorithm and coordinating approach vectors
+ 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
+ 20200229 14:39:21 INF Calculated acceptable probability of success (32.33%)
+ 20200229 14:39:23 ERR Recovery object retrieval failed with status NOT_READY
+
+.. note::
+
+ This example uses the :ref:`module-pw_tokenizer-base64-format`, which
+ occupies about 4/3 (133%) as much space as the default binary format when
+ encoded. For projects that wish to interleave tokenized with plain text,
+ using Base64 is a worthwhile tradeoff.
+
+------------------------
+Detokenization in Python
+------------------------
+To detokenize in Python, import ``Detokenizer`` from the ``pw_tokenizer``
+package, and instantiate it with paths to token databases or ELF files.
+
+.. code-block:: python
+
+ import pw_tokenizer
+
+ detokenizer = pw_tokenizer.Detokenizer('path/to/database.csv', 'other/path.elf')
+
+ def process_log_message(log_message):
+ result = detokenizer.detokenize(log_message.payload)
+ self._log(str(result))
+
+The ``pw_tokenizer`` package also provides the ``AutoUpdatingDetokenizer``
+class, which can be used in place of the standard ``Detokenizer``. This class
+monitors database files for changes and automatically reloads them when they
+change. This is helpful for long-running tools that use detokenization. The
+class also supports token domains for the given database files in the
+``<path>#<domain>`` format.
+
+For messages that are optionally tokenized and may be encoded as binary,
+Base64, or plaintext UTF-8, use
+:func:`pw_tokenizer.proto.decode_optionally_tokenized`. This will attempt to
+determine the correct method to detokenize and always provide a printable
+string.
+
+.. _module-pw_tokenizer-base64-decoding:
+
+Decoding Base64
+===============
+The Python ``Detokenizer`` class supports decoding and detokenizing prefixed
+Base64 messages with ``detokenize_base64`` and related methods.
+
+.. tip::
+ The Python detokenization tools support recursive detokenization for prefixed
+ Base64 text. Tokenized strings found in detokenized text are detokenized, so
+ prefixed Base64 messages can be passed as ``%s`` arguments.
+
+ For example, the tokenized string for "Wow!" is ``$RhYjmQ==``. This could be
+ passed as an argument to the printf-style string ``Nested message: %s``, which
+ encodes to ``$pEVTYQkkUmhZam1RPT0=``. The detokenizer would decode the message
+ as follows:
+
+ ::
+
+ "$pEVTYQkkUmhZam1RPT0=" → "Nested message: $RhYjmQ==" → "Nested message: Wow!"
+
+Base64 decoding is supported in C++ or C with the
+``pw::tokenizer::PrefixedBase64Decode`` or ``pw_tokenizer_PrefixedBase64Decode``
+functions.
+
+Investigating undecoded Base64 messages
+---------------------------------------
+Tokenized messages cannot be decoded if the token is not recognized. The Python
+package includes the ``parse_message`` tool, which parses tokenized Base64
+messages without looking up the token in a database. This tool attempts to guess
+the types of the arguments and displays potential ways to decode them.
+
+This tool can be used to extract argument information from an otherwise unusable
+message. It could help identify which statement in the code produced the
+message. This tool is not particularly helpful for tokenized messages without
+arguments, since all it can do is show the value of the unknown token.
+
+The tool is executed by passing Base64 tokenized messages, with or without the
+``$`` prefix, to ``pw_tokenizer.parse_message``. Pass ``-h`` or ``--help`` to
+see full usage information.
+
+Example
+^^^^^^^
+.. code-block::
+
+ $ python -m pw_tokenizer.parse_message '$329JMwA=' koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw== --specs %s %d
+
+ INF Decoding arguments for '$329JMwA='
+ INF Binary: b'\xdfoI3\x00' [df 6f 49 33 00] (5 bytes)
+ INF Token: 0x33496fdf
+ INF Args: b'\x00' [00] (1 bytes)
+ INF Decoding with up to 8 %s or %d arguments
+ INF Attempt 1: [%s]
+ INF Attempt 2: [%d] 0
+
+ INF Decoding arguments for '$koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw=='
+ INF Binary: b'\x92\x84\xa5\xe7n\x13FAILED_PRECONDITION\x02OK' [92 84 a5 e7 6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (28 bytes)
+ INF Token: 0xe7a58492
+ INF Args: b'n\x13FAILED_PRECONDITION\x02OK' [6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (24 bytes)
+ INF Decoding with up to 8 %s or %d arguments
+ INF Attempt 1: [%d %s %d %d %d] 55 FAILED_PRECONDITION 1 -40 -38
+ INF Attempt 2: [%d %s %s] 55 FAILED_PRECONDITION OK
+
+
+.. _module-pw_tokenizer-protobuf-tokenization-python:
+
+Detokenizing protobufs
+======================
+The :py:mod:`pw_tokenizer.proto` Python module defines functions that may be
+used to detokenize protobuf objects in Python. The function
+:py:func:`pw_tokenizer.proto.detokenize_fields` detokenizes all fields
+annotated as tokenized, replacing them with their detokenized version. For
+example:
+
+.. code-block:: python
+
+ my_detokenizer = pw_tokenizer.Detokenizer(some_database)
+
+ my_message = SomeMessage(tokenized_field=b'$YS1EMQ==')
+ pw_tokenizer.proto.detokenize_fields(my_detokenizer, my_message)
+
+ assert my_message.tokenized_field == b'The detokenized string! Cool!'
+
+Decoding optionally tokenized strings
+-------------------------------------
+The encoding used for an optionally tokenized field is not recorded in the
+protobuf. Despite this, the text can reliably be decoded. This is accomplished
+by attempting to decode the field as binary or Base64 tokenized data before
+treating it like plain text.
+
+The following diagram describes the decoding process for optionally tokenized
+fields in detail.
+
+.. mermaid::
+
+ flowchart TD
+ start([Received bytes]) --> binary
+
+ binary[Decode as<br>binary tokenized] --> binary_ok
+ binary_ok{Detokenizes<br>successfully?} -->|no| utf8
+ binary_ok -->|yes| done_binary([Display decoded binary])
+
+ utf8[Decode as UTF-8] --> utf8_ok
+ utf8_ok{Valid UTF-8?} -->|no| base64_encode
+ utf8_ok -->|yes| base64
+
+ base64_encode[Encode as<br>tokenized Base64] --> display
+ display([Display encoded Base64])
+
+ base64[Decode as<br>Base64 tokenized] --> base64_ok
+
+ base64_ok{Fully<br>or partially<br>detokenized?} -->|no| is_plain_text
+ base64_ok -->|yes| base64_results
+
+ is_plain_text{Text is<br>printable?} -->|no| base64_encode
+ is_plain_text-->|yes| plain_text
+
+ base64_results([Display decoded Base64])
+ plain_text([Display text])
+
+Potential decoding problems
+---------------------------
+The decoding process for optionally tokenized fields will yield correct results
+in almost every situation. In rare circumstances, it is possible for it to fail,
+but these can be avoided with a low-overhead mitigation if desired.
+
+There are two ways in which the decoding process may fail.
+
+Accidentally interpreting plain text as tokenized binary
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+If a plain-text string happens to decode as a binary tokenized message, the
+incorrect message could be displayed. This is very unlikely to occur. While many
+tokens will incidentally end up being valid UTF-8 strings, it is highly unlikely
+that a device will happen to log one of these strings as plain text. The
+overwhelming majority of these strings will be nonsense.
+
+If an implementation wishes to guard against this extremely improbable
+situation, it is possible to prevent it. This situation is prevented by
+appending 0xFF (or another byte never valid in UTF-8) to binary tokenized data
+that happens to be valid UTF-8 (or all binary tokenized messages, if desired).
+When decoding, if there is an extra 0xFF byte, it is discarded.
+
+Displaying undecoded binary as plain text instead of Base64
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+If a message fails to decode as binary tokenized and it is not valid UTF-8, it
+is displayed as tokenized Base64. This makes it easily recognizable as a
+tokenized message and makes it simple to decode later from the text output (for
+example, with an updated token database).
+
+A binary message for which the token is not known may coincidentally be valid
+UTF-8 or ASCII. 6.25% of 4-byte sequences are composed only of ASCII characters
+When decoding with an out-of-date token database, it is possible that some
+binary tokenized messages will be displayed as plain text rather than tokenized
+Base64.
+
+This situation is likely to occur, but should be infrequent. Even if it does
+happen, it is not a serious issue. A very small number of strings will be
+displayed incorrectly, but these strings cannot be decoded anyway. One nonsense
+string (e.g. ``a-D1``) would be displayed instead of another (``$YS1EMQ==``).
+Updating the token database would resolve the issue, though the non-Base64 logs
+would be difficult decode later from a log file.
+
+This situation can be avoided with the same approach described in
+`Accidentally interpreting plain text as tokenized binary`_. Appending
+an invalid UTF-8 character prevents the undecoded binary message from being
+interpreted as plain text.
+
+---------------------
+Detokenization in C++
+---------------------
+The C++ detokenization libraries can be used in C++ or any language that can
+call into C++ with a C-linkage wrapper, such as Java or Rust. A reference
+Java Native Interface (JNI) implementation is provided.
+
+The C++ detokenization library uses binary-format token databases (created with
+``database.py create --type binary``). Read a binary format database from a
+file or include it in the source code. Pass the database array to
+``TokenDatabase::Create``, and construct a detokenizer.
+
+.. code-block:: cpp
+
+ Detokenizer detokenizer(TokenDatabase::Create(token_database_array));
+
+ std::string ProcessLog(span<uint8_t> log_data) {
+ return detokenizer.Detokenize(log_data).BestString();
+ }
+
+The ``TokenDatabase`` class verifies that its data is valid before using it. If
+it is invalid, the ``TokenDatabase::Create`` returns an empty database for which
+``ok()`` returns false. If the token database is included in the source code,
+this check can be done at compile time.
+
+.. code-block:: cpp
+
+ // This line fails to compile with a static_assert if the database is invalid.
+ constexpr TokenDatabase kDefaultDatabase = TokenDatabase::Create<kData>();
+
+ Detokenizer OpenDatabase(std::string_view path) {
+ std::vector<uint8_t> data = ReadWholeFile(path);
+
+ TokenDatabase database = TokenDatabase::Create(data);
+
+ // This checks if the file contained a valid database. It is safe to use a
+ // TokenDatabase that failed to load (it will be empty), but it may be
+ // desirable to provide a default database or otherwise handle the error.
+ if (database.ok()) {
+ return Detokenizer(database);
+ }
+ return Detokenizer(kDefaultDatabase);
+ }
+
+----------------------------
+Detokenization in TypeScript
+----------------------------
+To detokenize in TypeScript, import ``Detokenizer`` from the ``pigweedjs``
+package, and instantiate it with a CSV token database.
+
+.. code-block:: typescript
+
+ import { pw_tokenizer, pw_hdlc } from 'pigweedjs';
+ const { Detokenizer } = pw_tokenizer;
+ const { Frame } = pw_hdlc;
+
+ const detokenizer = new Detokenizer(String(tokenCsv));
+
+ function processLog(frame: Frame){
+ const result = detokenizer.detokenize(frame);
+ console.log(result);
+ }
+
+For messages that are encoded in Base64, use ``Detokenizer::detokenizeBase64``.
+`detokenizeBase64` will also attempt to detokenize nested Base64 tokens. There
+is also `detokenizeUint8Array` that works just like `detokenize` but expects
+`Uint8Array` instead of a `Frame` argument.
+
+
+
+.. _module-pw_tokenizer-cli-detokenizing:
+
+---------------------
+Detokenizing CLI tool
+---------------------
+``pw_tokenizer`` provides two standalone command line utilities for detokenizing
+Base64-encoded tokenized strings.
+
+* ``detokenize.py`` -- Detokenizes Base64-encoded strings in files or from
+ stdin.
+* ``serial_detokenizer.py`` -- Detokenizes Base64-encoded strings from a
+ connected serial device.
+
+If the ``pw_tokenizer`` Python package is installed, these tools may be executed
+as runnable modules. For example:
+
+.. code-block::
+
+ # Detokenize Base64-encoded strings in a file
+ python -m pw_tokenizer.detokenize -i input_file.txt
+
+ # Detokenize Base64-encoded strings in output from a serial device
+ python -m pw_tokenizer.serial_detokenizer --device /dev/ttyACM0
+
+See the ``--help`` options for these tools for full usage information.
+
+--------
+Appendix
+--------
+
+.. _module-pw_tokenizer-python-detokenization-c99-printf-notes:
+
+Python detokenization: C99 ``printf`` compatibility notes
+=========================================================
+This implementation is designed to align with the
+`C99 specification, section 7.19.6
+<https://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf>`_.
+Notably, this specification is slightly different than what is implemented
+in most compilers due to each compiler choosing to interpret undefined
+behavior in slightly different ways. Treat the following description as the
+source of truth.
+
+This implementation supports:
+
+- Overall Format: ``%[flags][width][.precision][length][specifier]``
+- Flags (Zero or More)
+ - ``-``: Left-justify within the given field width; Right justification is
+ the default (see Width modifier).
+ - ``+``: Forces to preceed the result with a plus or minus sign (``+`` or
+ ``-``) even for positive numbers. By default, only negative numbers are
+ preceded with a ``-`` sign.
+ - (space): If no sign is going to be written, a blank space is inserted
+ before the value.
+ - ``#``: Specifies an alternative print syntax should be used.
+ - Used with ``o``, ``x`` or ``X`` specifiers the value is preceeded with
+ ``0``, ``0x`` or ``0X``, respectively, for values different than zero.
+ - Used with ``a``, ``A``, ``e``, ``E``, ``f``, ``F``, ``g``, or ``G`` it
+ forces the written output to contain a decimal point even if no more
+ digits follow. By default, if no digits follow, no decimal point is
+ written.
+ - ``0``: Left-pads the number with zeroes (``0``) instead of spaces when
+ padding is specified (see width sub-specifier).
+- Width (Optional)
+ - ``(number)``: Minimum number of characters to be printed. If the value to
+ be printed is shorter than this number, the result is padded with blank
+ spaces or ``0`` if the ``0`` flag is present. The value is not truncated
+ even if the result is larger. If the value is negative and the ``0`` flag
+ is present, the ``0``\s are padded after the ``-`` symbol.
+ - ``*``: The width is not specified in the format string, but as an
+ additional integer value argument preceding the argument that has to be
+ formatted.
+- Precision (Optional)
+ - ``.(number)``
+ - For ``d``, ``i``, ``o``, ``u``, ``x``, ``X``, specifies the minimum
+ number of digits to be written. If the value to be written is shorter
+ than this number, the result is padded with leading zeros. The value is
+ not truncated even if the result is longer.
+
+ - A precision of ``0`` means that no character is written for the value
+ ``0``.
+
+ - For ``a``, ``A``, ``e``, ``E``, ``f``, and ``F``, specifies the number
+ of digits to be printed after the decimal point. By default, this is
+ ``6``.
+
+ - For ``g`` and ``G``, specifies the maximum number of significant digits
+ to be printed.
+
+ - For ``s``, specifies the maximum number of characters to be printed. By
+ default all characters are printed until the ending null character is
+ encountered.
+
+ - If the period is specified without an explicit value for precision,
+ ``0`` is assumed.
+ - ``.*``: The precision is not specified in the format string, but as an
+ additional integer value argument preceding the argument that has to be
+ formatted.
+- Length (Optional)
+ - ``hh``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed char`` or ``unsigned char``.
+ However, this is largely ignored in the implementation due to it not being
+ necessary for Python or argument decoding (since the argument is always
+ encoded at least as a 32-bit integer).
+ - ``h``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed short int`` or
+ ``unsigned short int``. However, this is largely ignored in the
+ implementation due to it not being necessary for Python or argument
+ decoding (since the argument is always encoded at least as a 32-bit
+ integer).
+ - ``l``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed long int`` or
+ ``unsigned long int``. Also is usable with ``c`` and ``s`` to specify that
+ the arguments will be encoded with ``wchar_t`` values (which isn't
+ different from normal ``char`` values). However, this is largely ignored in
+ the implementation due to it not being necessary for Python or argument
+ decoding (since the argument is always encoded at least as a 32-bit
+ integer).
+ - ``ll``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed long long int`` or
+ ``unsigned long long int``. This is required to properly decode the
+ argument as a 64-bit integer.
+ - ``L``: Usable with ``a``, ``A``, ``e``, ``E``, ``f``, ``F``, ``g``, or
+ ``G`` conversion specifiers applies to a long double argument. However,
+ this is ignored in the implementation due to floating point value encoded
+ that is unaffected by bit width.
+ - ``j``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``intmax_t`` or ``uintmax_t``.
+ - ``z``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``size_t``. This will force the argument
+ to be decoded as an unsigned integer.
+ - ``t``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``ptrdiff_t``.
+ - If a length modifier is provided for an incorrect specifier, it is ignored.
+- Specifier (Required)
+ - ``d`` / ``i``: Used for signed decimal integers.
+
+ - ``u``: Used for unsigned decimal integers.
+
+ - ``o``: Used for unsigned decimal integers and specifies formatting should
+ be as an octal number.
+
+ - ``x``: Used for unsigned decimal integers and specifies formatting should
+ be as a hexadecimal number using all lowercase letters.
+
+ - ``X``: Used for unsigned decimal integers and specifies formatting should
+ be as a hexadecimal number using all uppercase letters.
+
+ - ``f``: Used for floating-point values and specifies to use lowercase,
+ decimal floating point formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``F``: Used for floating-point values and specifies to use uppercase,
+ decimal floating point formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``e``: Used for floating-point values and specifies to use lowercase,
+ exponential (scientific) formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``E``: Used for floating-point values and specifies to use uppercase,
+ exponential (scientific) formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``g``: Used for floating-point values and specified to use ``f`` or ``e``
+ formatting depending on which would be the shortest representation.
+
+ - Precision specifies the number of significant digits, not just digits
+ after the decimal place.
+
+ - If the precision is specified as ``0``, it is interpreted to mean ``1``.
+
+ - ``e`` formatting is used if the the exponent would be less than ``-4`` or
+ is greater than or equal to the precision.
+
+ - Trailing zeros are removed unless the ``#`` flag is set.
+
+ - A decimal point only appears if it is followed by a digit.
+
+ - ``NaN`` or infinities always follow ``f`` formatting.
+
+ - ``G``: Used for floating-point values and specified to use ``f`` or ``e``
+ formatting depending on which would be the shortest representation.
+
+ - Precision specifies the number of significant digits, not just digits
+ after the decimal place.
+
+ - If the precision is specified as ``0``, it is interpreted to mean ``1``.
+
+ - ``E`` formatting is used if the the exponent would be less than ``-4`` or
+ is greater than or equal to the precision.
+
+ - Trailing zeros are removed unless the ``#`` flag is set.
+
+ - A decimal point only appears if it is followed by a digit.
+
+ - ``NaN`` or infinities always follow ``F`` formatting.
+
+ - ``c``: Used for formatting a ``char`` value.
+
+ - ``s``: Used for formatting a string of ``char`` values.
+
+ - If width is specified, the null terminator character is included as a
+ character for width count.
+
+ - If precision is specified, no more ``char``\s than that value will be
+ written from the string (padding is used to fill additional width).
+
+ - ``p``: Used for formatting a pointer address.
+
+ - ``%``: Prints a single ``%``. Only valid as ``%%`` (supports no flags,
+ width, precision, or length modifiers).
+
+Underspecified details:
+
+- If both ``+`` and (space) flags appear, the (space) is ignored.
+- The ``+`` and (space) flags will error if used with ``c`` or ``s``.
+- The ``#`` flag will error if used with ``d``, ``i``, ``u``, ``c``, ``s``, or
+ ``p``.
+- The ``0`` flag will error if used with ``c``, ``s``, or ``p``.
+- Both ``+`` and (space) can work with the unsigned integer specifiers ``u``,
+ ``o``, ``x``, and ``X``.
+- If a length modifier is provided for an incorrect specifier, it is ignored.
+- The ``z`` length modifier will decode arugments as signed as long as ``d`` or
+ ``i`` is used.
+- ``p`` is implementation defined.
+
+ - For this implementation, it will print with a ``0x`` prefix and then the
+ pointer value was printed using ``%08X``.
+
+ - ``p`` supports the ``+``, ``-``, and (space) flags, but not the ``#`` or
+ ``0`` flags.
+
+ - None of the length modifiers are usable with ``p``.
+
+ - This implementation will try to adhere to user-specified width (assuming the
+ width provided is larger than the guaranteed minimum of ``10``).
+
+ - Specifying precision for ``p`` is considered an error.
+- Only ``%%`` is allowed with no other modifiers. Things like ``%+%`` will fail
+ to decode. Some C stdlib implementations support any modifiers being
+ present between ``%``, but ignore any for the output.
+- If a width is specified with the ``0`` flag for a negative value, the padded
+ ``0``\s will appear after the ``-`` symbol.
+- A precision of ``0`` for ``d``, ``i``, ``u``, ``o``, ``x``, or ``X`` means
+ that no character is written for the value ``0``.
+- Precision cannot be specified for ``c``.
+- Using ``*`` or fixed precision with the ``s`` specifier still requires the
+ string argument to be null-terminated. This is due to argument encoding
+ happening on the C/C++-side while the precision value is not read or
+ otherwise used until decoding happens in this Python code.
+
+Non-conformant details:
+
+- ``n`` specifier: We do not support the ``n`` specifier since it is impossible
+ for us to retroactively tell the original program how many characters have
+ been printed since this decoding happens a great deal of time after the
+ device sent it, usually on a separate processing device entirely.
diff --git a/pw_tokenizer/detokenize.cc b/pw_tokenizer/detokenize.cc
index 5e3262f86..b0557985e 100644
--- a/pw_tokenizer/detokenize.cc
+++ b/pw_tokenizer/detokenize.cc
@@ -19,11 +19,76 @@
#include "pw_bytes/bit.h"
#include "pw_bytes/endian.h"
+#include "pw_tokenizer/base64.h"
#include "pw_tokenizer/internal/decode.h"
+#include "pw_tokenizer/nested_tokenization.h"
namespace pw::tokenizer {
namespace {
+class NestedMessageDetokenizer {
+ public:
+ NestedMessageDetokenizer(const Detokenizer& detokenizer)
+ : detokenizer_(detokenizer) {}
+
+ void Detokenize(std::string_view chunk) {
+ for (char next_char : chunk) {
+ Detokenize(next_char);
+ }
+ }
+
+ void Detokenize(char next_char) {
+ switch (state_) {
+ case kNonMessage:
+ if (next_char == PW_TOKENIZER_NESTED_PREFIX) {
+ message_buffer_.push_back(next_char);
+ state_ = kMessage;
+ } else {
+ output_.push_back(next_char);
+ }
+ break;
+ case kMessage:
+ if (base64::IsValidChar(next_char)) {
+ message_buffer_.push_back(next_char);
+ } else {
+ HandleEndOfMessage();
+ if (next_char == PW_TOKENIZER_NESTED_PREFIX) {
+ message_buffer_.push_back(next_char);
+ } else {
+ output_.push_back(next_char);
+ state_ = kNonMessage;
+ }
+ }
+ break;
+ }
+ }
+
+ std::string Flush() {
+ if (state_ == kMessage) {
+ HandleEndOfMessage();
+ state_ = kNonMessage;
+ }
+ return std::move(output_);
+ }
+
+ private:
+ void HandleEndOfMessage() {
+ if (auto result = detokenizer_.DetokenizeBase64Message(message_buffer_);
+ result.ok()) {
+ output_ += result.BestString();
+ } else {
+ output_ += message_buffer_; // Keep the original if it doesn't decode.
+ }
+ message_buffer_.clear();
+ }
+
+ const Detokenizer& detokenizer_;
+ std::string output_;
+ std::string message_buffer_;
+
+ enum { kNonMessage, kMessage } state_ = kNonMessage;
+};
+
std::string UnknownTokenMessage(uint32_t value) {
std::string output(PW_TOKENIZER_ARG_DECODING_ERROR_PREFIX "unknown token ");
@@ -126,4 +191,17 @@ DetokenizedString Detokenizer::Detokenize(
: encoded.subspan(sizeof(token)));
}
+DetokenizedString Detokenizer::DetokenizeBase64Message(
+ std::string_view text) const {
+ std::string buffer(text);
+ buffer.resize(PrefixedBase64DecodeInPlace(buffer));
+ return Detokenize(buffer);
+}
+
+std::string Detokenizer::DetokenizeBase64(std::string_view text) const {
+ NestedMessageDetokenizer nested_detokenizer(*this);
+ nested_detokenizer.Detokenize(text);
+ return nested_detokenizer.Flush();
+}
+
} // namespace pw::tokenizer
diff --git a/pw_tokenizer/detokenize_fuzzer.cc b/pw_tokenizer/detokenize_fuzzer.cc
index d7d36c15e..e96438d31 100644
--- a/pw_tokenizer/detokenize_fuzzer.cc
+++ b/pw_tokenizer/detokenize_fuzzer.cc
@@ -44,7 +44,7 @@ enum DetokenizeBufferArgumentType : uint8_t {
// database, we construct a minimal database with 4 entries out of a string
// literal array that matches the token database format (see token_database.h
// for detailed info on the database entry format)
-alignas(TokenDatabase::RawEntry) constexpr char kBasicData[] =
+constexpr char kBasicData[] =
"TOKENS\0\0"
"\x04\x00\x00\x00"
"\0\0\0\0"
diff --git a/pw_tokenizer/detokenize_test.cc b/pw_tokenizer/detokenize_test.cc
index 07dc1853a..456f89ef3 100644
--- a/pw_tokenizer/detokenize_test.cc
+++ b/pw_tokenizer/detokenize_test.cc
@@ -26,9 +26,21 @@ using namespace std::literals::string_view_literals;
// Use a shorter name for the error string macro.
#define ERR PW_TOKENIZER_ARG_DECODING_ERROR
-// Use alignas to ensure that the data is properly aligned to be read from a
-// token database entry struct. This avoids unaligned memory reads.
-alignas(TokenDatabase::RawEntry) constexpr char kBasicData[] =
+using Case = std::pair<std::string_view, std::string_view>;
+
+template <typename... Args>
+auto TestCases(Args... args) {
+ return std::array<Case, sizeof...(Args)>{args...};
+}
+
+// Database with the following entries:
+// {
+// 0x00000001: "One",
+// 0x00000005: "TWO",
+// 0x000000ff: "333",
+// 0xDDEEEEFF: "One",
+// }
+constexpr char kBasicData[] =
"TOKENS\0\0"
"\x04\x00\x00\x00"
"\0\0\0\0"
@@ -102,7 +114,33 @@ TEST_F(Detokenize, BestStringWithErrors_UnknownToken_ErrorMessage) {
ERR("unknown token fedcba98"));
}
-alignas(TokenDatabase::RawEntry) constexpr char kDataWithArguments[] =
+// Base64 versions of the four tokens
+#define ONE "$AQAAAA=="
+#define TWO "$BQAAAA=="
+#define THREE "$/wAAAA=="
+#define FOUR "$/+7u3Q=="
+
+TEST_F(Detokenize, Base64_NoArguments) {
+ for (auto [data, expected] : TestCases(
+ Case{ONE, "One"},
+ Case{TWO, "TWO"},
+ Case{THREE, "333"},
+ Case{FOUR, "FOUR"},
+ Case{FOUR ONE ONE, "FOUROneOne"},
+ Case{ONE TWO THREE FOUR, "OneTWO333FOUR"},
+ Case{ONE "\r\n" TWO "\r\n" THREE "\r\n" FOUR "\r\n",
+ "One\r\nTWO\r\n333\r\nFOUR\r\n"},
+ Case{"123" FOUR, "123FOUR"},
+ Case{"123" FOUR ", 56", "123FOUR, 56"},
+ Case{"12" THREE FOUR ", 56", "12333FOUR, 56"},
+ Case{"$0" ONE, "$0One"},
+ Case{"$/+7u3Q=", "$/+7u3Q="}, // incomplete message (missing "=")
+ Case{"$123456==" FOUR, "$123456==FOUR"})) {
+ EXPECT_EQ(detok_.DetokenizeBase64(data), expected);
+ }
+}
+
+constexpr char kDataWithArguments[] =
"TOKENS\0\0"
"\x09\x00\x00\x00"
"\0\0\0\0"
@@ -126,14 +164,6 @@ alignas(TokenDatabase::RawEntry) constexpr char kDataWithArguments[] =
"%llu!"; // FF
constexpr TokenDatabase kWithArgs = TokenDatabase::Create<kDataWithArguments>();
-
-using Case = std::pair<std::string_view, std::string_view>;
-
-template <typename... Args>
-auto TestCases(Args... args) {
- return std::array<Case, sizeof...(Args)>{args...};
-}
-
class DetokenizeWithArgs : public ::testing::Test {
protected:
DetokenizeWithArgs() : detok_(kWithArgs) {}
@@ -191,7 +221,7 @@ TEST_F(DetokenizeWithArgs, DecodingError) {
"Now there are " ERR("%d ERROR") " of " ERR("%s SKIPPED") "!");
}
-alignas(TokenDatabase::RawEntry) constexpr char kDataWithCollisions[] =
+constexpr char kDataWithCollisions[] =
"TOKENS\0\0"
"\x0F\x00\x00\x00"
"\0\0\0\0"
diff --git a/pw_tokenizer/docs.rst b/pw_tokenizer/docs.rst
index 944f4cb32..01d619a62 100644
--- a/pw_tokenizer/docs.rst
+++ b/pw_tokenizer/docs.rst
@@ -3,1529 +3,122 @@
============
pw_tokenizer
============
-:bdg-primary:`host`
-:bdg-primary:`device`
-:bdg-secondary:`Python`
-:bdg-secondary:`C++`
-:bdg-secondary:`TypeScript`
-:bdg-success:`stable`
+.. pigweed-module::
+ :name: pw_tokenizer
+ :tagline: Compress strings to shrink logs by +75%
+ :status: stable
+ :languages: C++, C11, Python, Rust, TypeScript, Java
+ :code-size-impact: 50% reduction in log size
Logging is critical, but developers are often forced to choose between
additional logging or saving crucial flash space. The ``pw_tokenizer`` module
-helps address this by replacing printf-style strings with binary tokens during
-compilation. This enables extensive logging with substantially less memory
-usage.
+enables **extensive logging with substantially less memory usage** by replacing
+printf-style strings with binary tokens during compilation. It is designed to
+integrate easily into existing logging systems.
-.. note::
- This usage of the term "tokenizer" is not related to parsing! The
- module is called tokenizer because it replaces a whole string literal with an
- integer token. It does not parse strings into separate tokens.
+Although the most common application of ``pw_tokenizer`` is binary logging,
+**the tokenizer is general purpose and can be used to tokenize any strings**,
+with or without printf-style arguments.
-The most common application of ``pw_tokenizer`` is binary logging, and it is
-designed to integrate easily into existing logging systems. However, the
-tokenizer is general purpose and can be used to tokenize any strings, with or
-without printf-style arguments.
+Why tokenize strings?
-**Why tokenize strings?**
-
-* Dramatically reduce binary size by removing string literals from binaries.
-* Reduce I/O traffic, RAM, and flash usage by sending and storing compact tokens
+* **Dramatically reduce binary size** by removing string literals from binaries.
+* **Reduce I/O traffic, RAM, and flash usage** by sending and storing compact tokens
instead of strings. We've seen over 50% reduction in encoded log contents.
-* Reduce CPU usage by replacing snprintf calls with simple tokenization code.
-* Remove potentially sensitive log, assert, and other strings from binaries.
-
---------------
-Basic overview
---------------
-There are two sides to ``pw_tokenizer``, which we call tokenization and
-detokenization.
-
-* **Tokenization** converts string literals in the source code to binary tokens
- at compile time. If the string has printf-style arguments, these are encoded
- to compact binary form at runtime.
-* **Detokenization** converts tokenized strings back to the original
- human-readable strings.
-
-Here's an overview of what happens when ``pw_tokenizer`` is used:
-
-1. During compilation, the ``pw_tokenizer`` module hashes string literals to
- generate stable 32-bit tokens.
-2. The tokenization macro removes these strings by declaring them in an ELF
- section that is excluded from the final binary.
-3. After compilation, strings are extracted from the ELF to build a database of
- tokenized strings for use by the detokenizer. The ELF file may also be used
- directly.
-4. During operation, the device encodes the string token and its arguments, if
- any.
-5. The encoded tokenized strings are sent off-device or stored.
-6. Off-device, the detokenizer tools use the token database to decode the
- strings to human-readable form.
-
-Example: tokenized logging
-==========================
-This example demonstrates using ``pw_tokenizer`` for logging. In this example,
-tokenized logging saves ~90% in binary size (41 → 4 bytes) and 70% in encoded
-size (49 → 15 bytes).
-
-**Before**: plain text logging
-
-+------------------+-------------------------------------------+---------------+
-| Location | Logging Content | Size in bytes |
-+==================+===========================================+===============+
-| Source contains | ``LOG("Battery state: %s; battery | |
-| | voltage: %d mV", state, voltage);`` | |
-+------------------+-------------------------------------------+---------------+
-| Binary contains | ``"Battery state: %s; battery | 41 |
-| | voltage: %d mV"`` | |
-+------------------+-------------------------------------------+---------------+
-| | (log statement is called with | |
-| | ``"CHARGING"`` and ``3989`` as arguments) | |
-+------------------+-------------------------------------------+---------------+
-| Device transmits | ``"Battery state: CHARGING; battery | 49 |
-| | voltage: 3989 mV"`` | |
-+------------------+-------------------------------------------+---------------+
-| When viewed | ``"Battery state: CHARGING; battery | |
-| | voltage: 3989 mV"`` | |
-+------------------+-------------------------------------------+---------------+
-
-**After**: tokenized logging
-
-+------------------+-----------------------------------------------------------+---------+
-| Location | Logging Content | Size in |
-| | | bytes |
-+==================+===========================================================+=========+
-| Source contains | ``LOG("Battery state: %s; battery | |
-| | voltage: %d mV", state, voltage);`` | |
-+------------------+-----------------------------------------------------------+---------+
-| Binary contains | ``d9 28 47 8e`` (0x8e4728d9) | 4 |
-+------------------+-----------------------------------------------------------+---------+
-| | (log statement is called with | |
-| | ``"CHARGING"`` and ``3989`` as arguments) | |
-+------------------+-----------------------------------------------------------+---------+
-| Device transmits | =============== ============================== ========== | 15 |
-| | ``d9 28 47 8e`` ``08 43 48 41 52 47 49 4E 47`` ``aa 3e`` | |
-| | --------------- ------------------------------ ---------- | |
-| | Token ``"CHARGING"`` argument ``3989``, | |
-| | as | |
-| | varint | |
-| | =============== ============================== ========== | |
-+------------------+-----------------------------------------------------------+---------+
-| When viewed | ``"Battery state: CHARGING; battery voltage: 3989 mV"`` | |
-+------------------+-----------------------------------------------------------+---------+
-
----------------
-Getting started
----------------
-Integrating ``pw_tokenizer`` requires a few steps beyond building the code. This
-section describes one way ``pw_tokenizer`` might be integrated with a project.
-These steps can be adapted as needed.
-
-1. Add ``pw_tokenizer`` to your build. Build files for GN, CMake, and Bazel are
- provided. For Make or other build systems, add the files specified in the
- BUILD.gn's ``pw_tokenizer`` target to the build.
-2. Use the tokenization macros in your code. See `Tokenization`_.
-3. Add the contents of ``pw_tokenizer_linker_sections.ld`` to your project's
- linker script. In GN and CMake, this step is done automatically.
-4. Compile your code to produce an ELF file.
-5. Run ``database.py create`` on the ELF file to generate a CSV token
- database. See `Managing token databases`_.
-6. Commit the token database to your repository. See notes in `Database
- management`_.
-7. Integrate a ``database.py add`` command to your build to automatically update
- the committed token database. In GN, use the ``pw_tokenizer_database``
- template to do this. See `Update a database`_.
-8. Integrate ``detokenize.py`` or the C++ detokenization library with your tools
- to decode tokenized logs. See `Detokenization`_.
-
-Using with Zephyr
-=================
-When building ``pw_tokenizer`` with Zephyr, 3 Kconfigs can be used currently:
-
-* ``CONFIG_PIGWEED_TOKENIZER`` will automatically link ``pw_tokenizer`` as well
- as any dependencies.
-* ``CONFIG_PIGWEED_TOKENIZER_BASE64`` will automatically link
- ``pw_tokenizer.base64`` as well as any dependencies.
-* ``CONFIG_PIGWEED_DETOKENIZER`` will automatically link
- ``pw_tokenizer.decoder`` as well as any dependencies.
-
-Once enabled, the tokenizer headers can be included like any Zephyr headers:
-
-.. code-block:: cpp
-
- #include <pw_tokenizer/tokenize.h>
-
-.. note::
- Zephyr handles the additional linker sections via
- ``pw_tokenizer_linker_rules.ld`` which is added to the end of the linker file
- via a call to ``zephyr_linker_sources(SECTIONS ...)``.
-
-------------
-Tokenization
-------------
-Tokenization converts a string literal to a token. If it's a printf-style
-string, its arguments are encoded along with it. The results of tokenization can
-be sent off device or stored in place of a full string.
-
-.. doxygentypedef:: pw_tokenizer_Token
-
-Tokenization macros
-===================
-Adding tokenization to a project is simple. To tokenize a string, include
-``pw_tokenizer/tokenize.h`` and invoke one of the ``PW_TOKENIZE_`` macros.
-
-Tokenize a string literal
--------------------------
-``pw_tokenizer`` provides macros for tokenizing string literals with no
-arguments.
-
-.. doxygendefine:: PW_TOKENIZE_STRING
-.. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN
-.. doxygendefine:: PW_TOKENIZE_STRING_MASK
-
-The tokenization macros above cannot be used inside other expressions.
-
-.. admonition:: **Yes**: Assign :c:macro:`PW_TOKENIZE_STRING` to a ``constexpr`` variable.
- :class: checkmark
-
- .. code:: cpp
-
- constexpr uint32_t kGlobalToken = PW_TOKENIZE_STRING("Wowee Zowee!");
-
- void Function() {
- constexpr uint32_t local_token = PW_TOKENIZE_STRING("Wowee Zowee?");
- }
-
-.. admonition:: **No**: Use :c:macro:`PW_TOKENIZE_STRING` in another expression.
- :class: error
-
- .. code:: cpp
-
- void BadExample() {
- ProcessToken(PW_TOKENIZE_STRING("This won't compile!"));
- }
-
- Use :c:macro:`PW_TOKENIZE_STRING_EXPR` instead.
-
-An alternate set of macros are provided for use inside expressions. These make
-use of lambda functions, so while they can be used inside expressions, they
-require C++ and cannot be assigned to constexpr variables or be used with
-special function variables like ``__func__``.
-
-.. doxygendefine:: PW_TOKENIZE_STRING_EXPR
-.. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN_EXPR
-.. doxygendefine:: PW_TOKENIZE_STRING_MASK_EXPR
-
-.. admonition:: When to use these macros
-
- Use :c:macro:`PW_TOKENIZE_STRING` and related macros to tokenize string
- literals that do not need %-style arguments encoded.
-
-.. admonition:: **Yes**: Use :c:macro:`PW_TOKENIZE_STRING_EXPR` within other expressions.
- :class: checkmark
-
- .. code:: cpp
-
- void GoodExample() {
- ProcessToken(PW_TOKENIZE_STRING_EXPR("This will compile!"));
- }
-
-.. admonition:: **No**: Assign :c:macro:`PW_TOKENIZE_STRING_EXPR` to a ``constexpr`` variable.
- :class: error
-
- .. code:: cpp
-
- constexpr uint32_t wont_work = PW_TOKENIZE_STRING_EXPR("This won't compile!"));
-
- Instead, use :c:macro:`PW_TOKENIZE_STRING` to assign to a ``constexpr`` variable.
-
-.. admonition:: **No**: Tokenize ``__func__`` in :c:macro:`PW_TOKENIZE_STRING_EXPR`.
- :class: error
-
- .. code:: cpp
-
- void BadExample() {
- // This compiles, but __func__ will not be the outer function's name, and
- // there may be compiler warnings.
- constexpr uint32_t wont_work = PW_TOKENIZE_STRING_EXPR(__func__);
- }
-
- Instead, use :c:macro:`PW_TOKENIZE_STRING` to tokenize ``__func__`` or similar macros.
-
-.. _module-pw_tokenizer-custom-macro:
-
-Tokenize a message with arguments in a custom macro
----------------------------------------------------
-Projects can leverage the tokenization machinery in whichever way best suits
-their needs. The most efficient way to use ``pw_tokenizer`` is to pass tokenized
-data to a global handler function. A project's custom tokenization macro can
-handle tokenized data in a function of their choosing.
-
-``pw_tokenizer`` provides two low-level macros for projects to use
-to create custom tokenization macros.
-
-.. doxygendefine:: PW_TOKENIZE_FORMAT_STRING
-.. doxygendefine:: PW_TOKENIZER_ARG_TYPES
-
-The outputs of these macros are typically passed to an encoding function. That
-function encodes the token, argument types, and argument data to a buffer using
-helpers provided by ``pw_tokenizer/encode_args.h``.
-
-.. doxygenfunction:: pw::tokenizer::EncodeArgs
-.. doxygenclass:: pw::tokenizer::EncodedMessage
- :members:
-.. doxygenfunction:: pw_tokenizer_EncodeArgs
-
-Example
-^^^^^^^
-The following example implements a custom tokenization macro similar to
-:ref:`module-pw_log_tokenized`.
-
-.. code-block:: cpp
-
- #include "pw_tokenizer/tokenize.h"
-
- #ifndef __cplusplus
- extern "C" {
- #endif
-
- void EncodeTokenizedMessage(uint32_t metadata,
- pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...);
-
- #ifndef __cplusplus
- } // extern "C"
- #endif
-
- #define PW_LOG_TOKENIZED_ENCODE_MESSAGE(metadata, format, ...) \
- do { \
- PW_TOKENIZE_FORMAT_STRING( \
- PW_TOKENIZER_DEFAULT_DOMAIN, UINT32_MAX, format, __VA_ARGS__); \
- EncodeTokenizedMessage(payload, \
- _pw_tokenizer_token, \
- PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
- PW_COMMA_ARGS(__VA_ARGS__)); \
- } while (0)
-
-In this example, the ``EncodeTokenizedMessage`` function would handle encoding
-and processing the message. Encoding is done by the
-:cpp:class:`pw::tokenizer::EncodedMessage` class or
-:cpp:func:`pw::tokenizer::EncodeArgs` function from
-``pw_tokenizer/encode_args.h``. The encoded message can then be transmitted or
-stored as needed.
-
-.. code-block:: cpp
-
- #include "pw_log_tokenized/log_tokenized.h"
- #include "pw_tokenizer/encode_args.h"
-
- void HandleTokenizedMessage(pw::log_tokenized::Metadata metadata,
- pw::span<std::byte> message);
-
- extern "C" void EncodeTokenizedMessage(const uint32_t metadata,
- const pw_tokenizer_Token token,
- const pw_tokenizer_ArgTypes types,
- ...) {
- va_list args;
- va_start(args, types);
- pw::tokenizer::EncodedMessage<> encoded_message(token, types, args);
- va_end(args);
-
- HandleTokenizedMessage(metadata, encoded_message);
- }
-
-.. admonition:: Why use a custom macro
-
- - Optimal code size. Invoking a free function with the tokenized data results
- in the smallest possible call site.
- - Pass additional arguments, such as metadata, with the tokenized message.
- - Integrate ``pw_tokenizer`` with other systems.
-
-Tokenize a message with arguments to a buffer
----------------------------------------------
-.. doxygendefine:: PW_TOKENIZE_TO_BUFFER
-.. doxygendefine:: PW_TOKENIZE_TO_BUFFER_DOMAIN
-.. doxygendefine:: PW_TOKENIZE_TO_BUFFER_MASK
-
-.. admonition:: Why use this macro
-
- - Encode a tokenized message for consumption within a function.
- - Encode a tokenized message into an existing buffer.
-
- Avoid using ``PW_TOKENIZE_TO_BUFFER`` in widely expanded macros, such as a
- logging macro, because it will result in larger code size than passing the
- tokenized data to a function.
-
-Binary logging with pw_tokenizer
-================================
-String tokenization can be used to convert plain text logs to a compact,
-efficient binary format. See :ref:`module-pw_log_tokenized`.
-
-Tokenizing function names
-=========================
-The string literal tokenization functions support tokenizing string literals or
-constexpr character arrays (``constexpr const char[]``). In GCC and Clang, the
-special ``__func__`` variable and ``__PRETTY_FUNCTION__`` extension are declared
-as ``static constexpr char[]`` in C++ instead of the standard ``static const
-char[]``. This means that ``__func__`` and ``__PRETTY_FUNCTION__`` can be
-tokenized while compiling C++ with GCC or Clang.
-
-.. code-block:: cpp
-
- // Tokenize the special function name variables.
- constexpr uint32_t function = PW_TOKENIZE_STRING(__func__);
- constexpr uint32_t pretty_function = PW_TOKENIZE_STRING(__PRETTY_FUNCTION__);
-
-Note that ``__func__`` and ``__PRETTY_FUNCTION__`` are not string literals.
-They are defined as static character arrays, so they cannot be implicitly
-concatentated with string literals. For example, ``printf(__func__ ": %d",
-123);`` will not compile.
-
-Tokenization in Python
-======================
-The Python ``pw_tokenizer.encode`` module has limited support for encoding
-tokenized messages with the ``encode_token_and_args`` function.
-
-.. autofunction:: pw_tokenizer.encode.encode_token_and_args
-
-This function requires a string's token is already calculated. Typically these
-tokens are provided by a database, but they can be manually created using the
-tokenizer hash.
-
-.. autofunction:: pw_tokenizer.tokens.pw_tokenizer_65599_hash
-
-This is particularly useful for offline token database generation in cases where
-tokenized strings in a binary cannot be embedded as parsable pw_tokenizer
-entries.
-
-.. note::
- In C, the hash length of a string has a fixed limit controlled by
- ``PW_TOKENIZER_CFG_C_HASH_LENGTH``. To match tokens produced by C (as opposed
- to C++) code, ``pw_tokenizer_65599_hash()`` should be called with a matching
- hash length limit. When creating an offline database, it's a good idea to
- generate tokens for both, and merge the databases.
-
-Encoding
-========
-The token is a 32-bit hash calculated during compilation. The string is encoded
-little-endian with the token followed by arguments, if any. For example, the
-31-byte string ``You can go about your business.`` hashes to 0xdac9a244.
-This is encoded as 4 bytes: ``44 a2 c9 da``.
-
-Arguments are encoded as follows:
-
-* **Integers** (1--10 bytes) --
- `ZagZag and varint encoded <https://developers.google.com/protocol-buffers/docs/encoding#signed-integers>`_,
- similarly to Protocol Buffers. Smaller values take fewer bytes.
-* **Floating point numbers** (4 bytes) -- Single precision floating point.
-* **Strings** (1--128 bytes) -- Length byte followed by the string contents.
- The top bit of the length whether the string was truncated or not. The
- remaining 7 bits encode the string length, with a maximum of 127 bytes.
-
-.. TODO(hepler): insert diagram here!
-
-.. tip::
- ``%s`` arguments can quickly fill a tokenization buffer. Keep ``%s``
- arguments short or avoid encoding them as strings (e.g. encode an enum as an
- integer instead of a string). See also `Tokenized strings as %s arguments`_.
-
-Buffer sizing helper
---------------------
-.. doxygenfunction:: pw::tokenizer::MinEncodingBufferSizeBytes
-
-Encoding command line utility
------------------------------
-The ``pw_tokenizer.encode`` command line tool can be used to encode tokenized
-strings.
-
-.. code-block:: bash
-
- python -m pw_tokenizer.encode [-h] FORMAT_STRING [ARG ...]
-
-Example:
-
-.. code-block:: text
-
- $ python -m pw_tokenizer.encode "There's... %d many of %s!" 2 them
- Raw input: "There's... %d many of %s!" % (2, 'them')
- Formatted input: There's... 2 many of them!
- Token: 0xb6ef8b2d
- Encoded: b'-\x8b\xef\xb6\x04\x04them' (2d 8b ef b6 04 04 74 68 65 6d) [10 bytes]
- Prefixed Base64: $LYvvtgQEdGhlbQ==
-
-See ``--help`` for full usage details.
-
-Token generation: fixed length hashing at compile time
-======================================================
-String tokens are generated using a modified version of the x65599 hash used by
-the SDBM project. All hashing is done at compile time.
-
-In C code, strings are hashed with a preprocessor macro. For compatibility with
-macros, the hash must be limited to a fixed maximum number of characters. This
-value is set by ``PW_TOKENIZER_CFG_C_HASH_LENGTH``. Increasing
-``PW_TOKENIZER_CFG_C_HASH_LENGTH`` increases the compilation time for C due to
-the complexity of the hashing macros.
-
-C++ macros use a constexpr function instead of a macro. This function works with
-any length of string and has lower compilation time impact than the C macros.
-For consistency, C++ tokenization uses the same hash algorithm, but the
-calculated values will differ between C and C++ for strings longer than
-``PW_TOKENIZER_CFG_C_HASH_LENGTH`` characters.
-
-.. _module-pw_tokenizer-domains:
-
-Tokenization domains
-====================
-``pw_tokenizer`` supports having multiple tokenization domains. Domains are a
-string label associated with each tokenized string. This allows projects to keep
-tokens from different sources separate. Potential use cases include the
-following:
-
-* Keep large sets of tokenized strings separate to avoid collisions.
-* Create a separate database for a small number of strings that use truncated
- tokens, for example only 10 or 16 bits instead of the full 32 bits.
-
-If no domain is specified, the domain is empty (``""``). For many projects, this
-default domain is sufficient, so no additional configuration is required.
-
-.. code-block:: cpp
-
- // Tokenizes this string to the default ("") domain.
- PW_TOKENIZE_STRING("Hello, world!");
-
- // Tokenizes this string to the "my_custom_domain" domain.
- PW_TOKENIZE_STRING_DOMAIN("my_custom_domain", "Hello, world!");
-
-The database and detokenization command line tools default to reading from the
-default domain. The domain may be specified for ELF files by appending
-``#DOMAIN_NAME`` to the file path. Use ``#.*`` to read from all domains. For
-example, the following reads strings in ``some_domain`` from ``my_image.elf``.
-
-.. code-block:: sh
-
- ./database.py create --database my_db.csv path/to/my_image.elf#some_domain
-
-See `Managing token databases`_ for information about the ``database.py``
-command line tool.
-
-.. _module-pw_tokenizer-masks:
-
-Smaller tokens with masking
-===========================
-``pw_tokenizer`` uses 32-bit tokens. On 32-bit or 64-bit architectures, using
-fewer than 32 bits does not improve runtime or code size efficiency. However,
-when tokens are packed into data structures or stored in arrays, the size of the
-token directly affects memory usage. In those cases, every bit counts, and it
-may be desireable to use fewer bits for the token.
-
-``pw_tokenizer`` allows users to provide a mask to apply to the token. This
-masked token is used in both the token database and the code. The masked token
-is not a masked version of the full 32-bit token, the masked token is the token.
-This makes it trivial to decode tokens that use fewer than 32 bits.
-
-Masking functionality is provided through the ``*_MASK`` versions of the macros.
-For example, the following generates 16-bit tokens and packs them into an
-existing value.
-
-.. code-block:: cpp
-
- constexpr uint32_t token = PW_TOKENIZE_STRING_MASK("domain", 0xFFFF, "Pigweed!");
- uint32_t packed_word = (other_bits << 16) | token;
-
-Tokens are hashes, so tokens of any size have a collision risk. The fewer bits
-used for tokens, the more likely two strings are to hash to the same token. See
-`token collisions`_.
-
-Masked tokens without arguments may be encoded in fewer bytes. For example, the
-16-bit token ``0x1234`` may be encoded as two little-endian bytes (``34 12``)
-rather than four (``34 12 00 00``). The detokenizer tools zero-pad data smaller
-than four bytes. Tokens with arguments must always be encoded as four bytes.
-
-Token collisions
-================
-Tokens are calculated with a hash function. It is possible for different
-strings to hash to the same token. When this happens, multiple strings will have
-the same token in the database, and it may not be possible to unambiguously
-decode a token.
-
-The detokenization tools attempt to resolve collisions automatically. Collisions
-are resolved based on two things:
-
-- whether the tokenized data matches the strings arguments' (if any), and
-- if / when the string was marked as having been removed from the database.
-
-Working with collisions
------------------------
-Collisions may occur occasionally. Run the command
-``python -m pw_tokenizer.database report <database>`` to see information about a
-token database, including any collisions.
-
-If there are collisions, take the following steps to resolve them.
-
-- Change one of the colliding strings slightly to give it a new token.
-- In C (not C++), artificial collisions may occur if strings longer than
- ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` are hashed. If this is happening, consider
- setting ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` to a larger value. See
- ``pw_tokenizer/public/pw_tokenizer/config.h``.
-- Run the ``mark_removed`` command with the latest version of the build
- artifacts to mark missing strings as removed. This deprioritizes them in
- collision resolution.
-
- .. code-block:: sh
-
- python -m pw_tokenizer.database mark_removed --database <database> <ELF files>
-
- The ``purge`` command may be used to delete these tokens from the database.
-
-Probability of collisions
--------------------------
-Hashes of any size have a collision risk. The probability of one at least
-one collision occurring for a given number of strings is unintuitively high
-(this is known as the `birthday problem
-<https://en.wikipedia.org/wiki/Birthday_problem>`_). If fewer than 32 bits are
-used for tokens, the probability of collisions increases substantially.
-
-This table shows the approximate number of strings that can be hashed to have a
-1% or 50% probability of at least one collision (assuming a uniform, random
-hash).
-
-+-------+---------------------------------------+
-| Token | Collision probability by string count |
-| bits +--------------------+------------------+
-| | 50% | 1% |
-+=======+====================+==================+
-| 32 | 77000 | 9300 |
-+-------+--------------------+------------------+
-| 31 | 54000 | 6600 |
-+-------+--------------------+------------------+
-| 24 | 4800 | 580 |
-+-------+--------------------+------------------+
-| 16 | 300 | 36 |
-+-------+--------------------+------------------+
-| 8 | 19 | 3 |
-+-------+--------------------+------------------+
-
-Keep this table in mind when masking tokens (see `Smaller tokens with
-masking`_). 16 bits might be acceptable when tokenizing a small set of strings,
-such as module names, but won't be suitable for large sets of strings, like log
-messages.
-
----------------
-Token databases
----------------
-Token databases store a mapping of tokens to the strings they represent. An ELF
-file can be used as a token database, but it only contains the strings for its
-exact build. A token database file aggregates tokens from multiple ELF files, so
-that a single database can decode tokenized strings from any known ELF.
-
-Token databases contain the token, removal date (if any), and string for each
-tokenized string.
-
-Token database formats
-======================
-Three token database formats are supported: CSV, binary, and directory. Tokens
-may also be read from ELF files or ``.a`` archives, but cannot be written to
-these formats.
-
-CSV database format
--------------------
-The CSV database format has three columns: the token in hexadecimal, the removal
-date (if any) in year-month-day format, and the string literal, surrounded by
-quotes. Quote characters within the string are represented as two quote
-characters.
-
-This example database contains six strings, three of which have removal dates.
-
-.. code-block::
-
- 141c35d5, ,"The answer: ""%s"""
- 2e668cd6,2019-12-25,"Jello, world!"
- 7b940e2a, ,"Hello %s! %hd %e"
- 851beeb6, ,"%u %d"
- 881436a0,2020-01-01,"The answer is: %s"
- e13b0f94,2020-04-01,"%llu"
-
-Binary database format
-----------------------
-The binary database format is comprised of a 16-byte header followed by a series
-of 8-byte entries. Each entry stores the token and the removal date, which is
-0xFFFFFFFF if there is none. The string literals are stored next in the same
-order as the entries. Strings are stored with null terminators. See
-`token_database.h <https://pigweed.googlesource.com/pigweed/pigweed/+/HEAD/pw_tokenizer/public/pw_tokenizer/token_database.h>`_
-for full details.
-
-The binary form of the CSV database is shown below. It contains the same
-information, but in a more compact and easily processed form. It takes 141 B
-compared with the CSV database's 211 B.
-
-.. code-block:: text
-
- [header]
- 0x00: 454b4f54 0000534e TOKENS..
- 0x08: 00000006 00000000 ........
-
- [entries]
- 0x10: 141c35d5 ffffffff .5......
- 0x18: 2e668cd6 07e30c19 ..f.....
- 0x20: 7b940e2a ffffffff *..{....
- 0x28: 851beeb6 ffffffff ........
- 0x30: 881436a0 07e40101 .6......
- 0x38: e13b0f94 07e40401 ..;.....
-
- [string table]
- 0x40: 54 68 65 20 61 6e 73 77 65 72 3a 20 22 25 73 22 The answer: "%s"
- 0x50: 00 4a 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 00 48 .Jello, world!.H
- 0x60: 65 6c 6c 6f 20 25 73 21 20 25 68 64 20 25 65 00 ello %s! %hd %e.
- 0x70: 25 75 20 25 64 00 54 68 65 20 61 6e 73 77 65 72 %u %d.The answer
- 0x80: 20 69 73 3a 20 25 73 00 25 6c 6c 75 00 is: %s.%llu.
-
-Directory database format
--------------------------
-pw_tokenizer can consume directories of CSV databases. A directory database
-will be searched recursively for files with a `.pw_tokenizer.csv` suffix, all
-of which will be used for subsequent detokenization lookups.
-
-An example directory database might look something like this:
-
-.. code-block:: text
-
- token_database
- ├── chuck_e_cheese.pw_tokenizer.csv
- ├── fungi_ble.pw_tokenizer.csv
- └── some_more
- └── arcade.pw_tokenizer.csv
-
-This format is optimized for storage in a Git repository alongside source code.
-The token database commands randomly generate unique file names for the CSVs in
-the database to prevent merge conflicts. Running ``mark_removed`` or ``purge``
-commands in the database CLI consolidates the files to a single CSV.
-
-The database command line tool supports a ``--discard-temporary
-<upstream_commit>`` option for ``add``. In this mode, the tool attempts to
-discard temporary tokens. It identifies the latest CSV not present in the
-provided ``<upstream_commit>``, and tokens present that CSV that are not in the
-newly added tokens are discarded. This helps keep temporary tokens (e.g from
-debug logs) out of the database.
-
-JSON support
-============
-While pw_tokenizer doesn't specify a JSON database format, a token database can
-be created from a JSON formatted array of strings. This is useful for side-band
-token database generation for strings that are not embedded as parsable tokens
-in compiled binaries. See :ref:`module-pw_tokenizer-database-creation` for
-instructions on generating a token database from a JSON file.
-
-Managing token databases
-========================
-Token databases are managed with the ``database.py`` script. This script can be
-used to extract tokens from compilation artifacts and manage database files.
-Invoke ``database.py`` with ``-h`` for full usage information.
-
-An example ELF file with tokenized logs is provided at
-``pw_tokenizer/py/example_binary_with_tokenized_strings.elf``. You can use that
-file to experiment with the ``database.py`` commands.
-
-.. _module-pw_tokenizer-database-creation:
-
-Create a database
------------------
-The ``create`` command makes a new token database from ELF files (.elf, .o, .so,
-etc.), archives (.a), existing token databases (CSV or binary), or a JSON file
-containing an array of strings.
-
-.. code-block:: sh
-
- ./database.py create --database DATABASE_NAME ELF_OR_DATABASE_FILE...
-
-Two database output formats are supported: CSV and binary. Provide
-``--type binary`` to ``create`` to generate a binary database instead of the
-default CSV. CSV databases are great for checking into a source control or for
-human review. Binary databases are more compact and simpler to parse. The C++
-detokenizer library only supports binary databases currently.
-
-Update a database
------------------
-As new tokenized strings are added, update the database with the ``add``
-command.
-
-.. code-block:: sh
-
- ./database.py add --database DATABASE_NAME ELF_OR_DATABASE_FILE...
-
-This command adds new tokens from ELF files or other databases to the database.
-Adding tokens already present in the database updates the date removed, if any,
-to the latest.
-
-A CSV token database can be checked into a source repository and updated as code
-changes are made. The build system can invoke ``database.py`` to update the
-database after each build.
+* **Reduce CPU usage** by replacing snprintf calls with simple tokenization code.
+* **Remove potentially sensitive log, assert, and other strings** from binaries.
-GN integration
---------------
-Token databases may be updated or created as part of a GN build. The
-``pw_tokenizer_database`` template provided by
-``$dir_pw_tokenizer/database.gni`` automatically updates an in-source tokenized
-strings database or creates a new database with artifacts from one or more GN
-targets or other database files.
+.. grid:: 1
-To create a new database, set the ``create`` variable to the desired database
-type (``"csv"`` or ``"binary"``). The database will be created in the output
-directory. To update an existing database, provide the path to the database with
-the ``database`` variable.
+ .. grid-item-card:: :octicon:`rocket` Get started
+ :link: module-pw_tokenizer-get-started
+ :link-type: ref
+ :class-item: sales-pitch-cta-primary
-.. code-block::
+ Integrate pw_tokenizer into your project.
- import("//build_overrides/pigweed.gni")
+.. grid:: 2
- import("$dir_pw_tokenizer/database.gni")
+ .. grid-item-card:: :octicon:`code-square` Tokenization
+ :link: module-pw_tokenizer-tokenization
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
- pw_tokenizer_database("my_database") {
- database = "database_in_the_source_tree.csv"
- targets = [ "//firmware/image:foo(//targets/my_board:some_toolchain)" ]
- input_databases = [ "other_database.csv" ]
- }
+ Convert strings and arguments to tokens.
-Instead of specifying GN targets, paths or globs to output files may be provided
-with the ``paths`` option.
+ .. grid-item-card:: :octicon:`code-square` Token databases
+ :link: module-pw_tokenizer-token-databases
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-.. code-block::
+ Store a mapping of tokens to the strings and arguments they represent.
- pw_tokenizer_database("my_database") {
- database = "database_in_the_source_tree.csv"
- deps = [ ":apps" ]
- optional_paths = [ "$root_build_dir/**/*.elf" ]
- }
+.. grid:: 2
-.. note::
+ .. grid-item-card:: :octicon:`code-square` Detokenization
+ :link: module-pw_tokenizer-detokenization
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
- The ``paths`` and ``optional_targets`` arguments do not add anything to
- ``deps``, so there is no guarantee that the referenced artifacts will exist
- when the database is updated. Provide ``targets`` or ``deps`` or build other
- GN targets first if this is a concern.
+ Expand tokens back to the strings and arguments they represent.
---------------
-Detokenization
---------------
-Detokenization is the process of expanding a token to the string it represents
-and decoding its arguments. This module provides Python, C++ and TypeScript
-detokenization libraries.
+ .. grid-item-card:: :octicon:`info` API reference
+ :link: module-pw_tokenizer-api
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
-**Example: decoding tokenized logs**
+ Detailed reference information about the pw_tokenizer API.
-A project might tokenize its log messages with the `Base64 format`_. Consider
-the following log file, which has four tokenized logs and one plain text log:
-.. code-block:: text
-
- 20200229 14:38:58 INF $HL2VHA==
- 20200229 14:39:00 DBG $5IhTKg==
- 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
- 20200229 14:39:21 INF $EgFj8lVVAUI=
- 20200229 14:39:23 ERR $DFRDNwlOT1RfUkVBRFk=
-
-The project's log strings are stored in a database like the following:
-
-.. code-block::
-
- 1c95bd1c, ,"Initiating retrieval process for recovery object"
- 2a5388e4, ,"Determining optimal approach and coordinating vectors"
- 3743540c, ,"Recovery object retrieval failed with status %s"
- f2630112, ,"Calculated acceptable probability of success (%.2f%%)"
-
-Using the detokenizing tools with the database, the logs can be decoded:
-
-.. code-block:: text
-
- 20200229 14:38:58 INF Initiating retrieval process for recovery object
- 20200229 14:39:00 DBG Determining optimal algorithm and coordinating approach vectors
- 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
- 20200229 14:39:21 INF Calculated acceptable probability of success (32.33%)
- 20200229 14:39:23 ERR Recovery object retrieval failed with status NOT_READY
-
-.. note::
-
- This example uses the `Base64 format`_, which occupies about 4/3 (133%) as
- much space as the default binary format when encoded. For projects that wish
- to interleave tokenized with plain text, using Base64 is a worthwhile
- tradeoff.
-
-Python
-======
-To detokenize in Python, import ``Detokenizer`` from the ``pw_tokenizer``
-package, and instantiate it with paths to token databases or ELF files.
-
-.. code-block:: python
-
- import pw_tokenizer
-
- detokenizer = pw_tokenizer.Detokenizer('path/to/database.csv', 'other/path.elf')
-
- def process_log_message(log_message):
- result = detokenizer.detokenize(log_message.payload)
- self._log(str(result))
-
-The ``pw_tokenizer`` package also provides the ``AutoUpdatingDetokenizer``
-class, which can be used in place of the standard ``Detokenizer``. This class
-monitors database files for changes and automatically reloads them when they
-change. This is helpful for long-running tools that use detokenization. The
-class also supports token domains for the given database files in the
-``<path>#<domain>`` format.
-
-For messages that are optionally tokenized and may be encoded as binary,
-Base64, or plaintext UTF-8, use
-:func:`pw_tokenizer.proto.decode_optionally_tokenized`. This will attempt to
-determine the correct method to detokenize and always provide a printable
-string. For more information on this feature, see
-:ref:`module-pw_tokenizer-proto`.
-
-C99 ``printf`` Compatibility Notes
-----------------------------------
-This implementation is designed to align with the
-`C99 specification, section 7.19.6
-<https://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf>`_.
-Notably, this specification is slightly different than what is implemented
-in most compilers due to each compiler choosing to interpret undefined
-behavior in slightly different ways. Treat the following description as the
-source of truth.
-
-This implementation supports:
-
-- Overall Format: ``%[flags][width][.precision][length][specifier]``
-- Flags (Zero or More)
- - ``-``: Left-justify within the given field width; Right justification is
- the default (see Width modifier).
- - ``+``: Forces to preceed the result with a plus or minus sign (``+`` or
- ``-``) even for positive numbers. By default, only negative numbers are
- preceded with a ``-`` sign.
- - (space): If no sign is going to be written, a blank space is inserted
- before the value.
- - ``#``: Specifies an alternative print syntax should be used.
- - Used with ``o``, ``x`` or ``X`` specifiers the value is preceeded with
- ``0``, ``0x`` or ``0X``, respectively, for values different than zero.
- - Used with ``a``, ``A``, ``e``, ``E``, ``f``, ``F``, ``g``, or ``G`` it
- forces the written output to contain a decimal point even if no more
- digits follow. By default, if no digits follow, no decimal point is
- written.
- - ``0``: Left-pads the number with zeroes (``0``) instead of spaces when
- padding is specified (see width sub-specifier).
-- Width (Optional)
- - ``(number)``: Minimum number of characters to be printed. If the value to
- be printed is shorter than this number, the result is padded with blank
- spaces or ``0`` if the ``0`` flag is present. The value is not truncated
- even if the result is larger. If the value is negative and the ``0`` flag
- is present, the ``0``\s are padded after the ``-`` symbol.
- - ``*``: The width is not specified in the format string, but as an
- additional integer value argument preceding the argument that has to be
- formatted.
-- Precision (Optional)
- - ``.(number)``
- - For ``d``, ``i``, ``o``, ``u``, ``x``, ``X``, specifies the minimum
- number of digits to be written. If the value to be written is shorter
- than this number, the result is padded with leading zeros. The value is
- not truncated even if the result is longer.
-
- - A precision of ``0`` means that no character is written for the value
- ``0``.
-
- - For ``a``, ``A``, ``e``, ``E``, ``f``, and ``F``, specifies the number
- of digits to be printed after the decimal point. By default, this is
- ``6``.
-
- - For ``g`` and ``G``, specifies the maximum number of significant digits
- to be printed.
-
- - For ``s``, specifies the maximum number of characters to be printed. By
- default all characters are printed until the ending null character is
- encountered.
-
- - If the period is specified without an explicit value for precision,
- ``0`` is assumed.
- - ``.*``: The precision is not specified in the format string, but as an
- additional integer value argument preceding the argument that has to be
- formatted.
-- Length (Optional)
- - ``hh``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``signed char`` or ``unsigned char``.
- However, this is largely ignored in the implementation due to it not being
- necessary for Python or argument decoding (since the argument is always
- encoded at least as a 32-bit integer).
- - ``h``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``signed short int`` or
- ``unsigned short int``. However, this is largely ignored in the
- implementation due to it not being necessary for Python or argument
- decoding (since the argument is always encoded at least as a 32-bit
- integer).
- - ``l``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``signed long int`` or
- ``unsigned long int``. Also is usable with ``c`` and ``s`` to specify that
- the arguments will be encoded with ``wchar_t`` values (which isn't
- different from normal ``char`` values). However, this is largely ignored in
- the implementation due to it not being necessary for Python or argument
- decoding (since the argument is always encoded at least as a 32-bit
- integer).
- - ``ll``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``signed long long int`` or
- ``unsigned long long int``. This is required to properly decode the
- argument as a 64-bit integer.
- - ``L``: Usable with ``a``, ``A``, ``e``, ``E``, ``f``, ``F``, ``g``, or
- ``G`` conversion specifiers applies to a long double argument. However,
- this is ignored in the implementation due to floating point value encoded
- that is unaffected by bit width.
- - ``j``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``intmax_t`` or ``uintmax_t``.
- - ``z``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``size_t``. This will force the argument
- to be decoded as an unsigned integer.
- - ``t``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
- to convey the argument will be a ``ptrdiff_t``.
- - If a length modifier is provided for an incorrect specifier, it is ignored.
-- Specifier (Required)
- - ``d`` / ``i``: Used for signed decimal integers.
-
- - ``u``: Used for unsigned decimal integers.
-
- - ``o``: Used for unsigned decimal integers and specifies formatting should
- be as an octal number.
-
- - ``x``: Used for unsigned decimal integers and specifies formatting should
- be as a hexadecimal number using all lowercase letters.
-
- - ``X``: Used for unsigned decimal integers and specifies formatting should
- be as a hexadecimal number using all uppercase letters.
-
- - ``f``: Used for floating-point values and specifies to use lowercase,
- decimal floating point formatting.
-
- - Default precision is ``6`` decimal places unless explicitly specified.
-
- - ``F``: Used for floating-point values and specifies to use uppercase,
- decimal floating point formatting.
-
- - Default precision is ``6`` decimal places unless explicitly specified.
-
- - ``e``: Used for floating-point values and specifies to use lowercase,
- exponential (scientific) formatting.
-
- - Default precision is ``6`` decimal places unless explicitly specified.
-
- - ``E``: Used for floating-point values and specifies to use uppercase,
- exponential (scientific) formatting.
-
- - Default precision is ``6`` decimal places unless explicitly specified.
-
- - ``g``: Used for floating-point values and specified to use ``f`` or ``e``
- formatting depending on which would be the shortest representation.
-
- - Precision specifies the number of significant digits, not just digits
- after the decimal place.
-
- - If the precision is specified as ``0``, it is interpreted to mean ``1``.
-
- - ``e`` formatting is used if the the exponent would be less than ``-4`` or
- is greater than or equal to the precision.
-
- - Trailing zeros are removed unless the ``#`` flag is set.
-
- - A decimal point only appears if it is followed by a digit.
-
- - ``NaN`` or infinities always follow ``f`` formatting.
-
- - ``G``: Used for floating-point values and specified to use ``f`` or ``e``
- formatting depending on which would be the shortest representation.
-
- - Precision specifies the number of significant digits, not just digits
- after the decimal place.
-
- - If the precision is specified as ``0``, it is interpreted to mean ``1``.
-
- - ``E`` formatting is used if the the exponent would be less than ``-4`` or
- is greater than or equal to the precision.
-
- - Trailing zeros are removed unless the ``#`` flag is set.
-
- - A decimal point only appears if it is followed by a digit.
-
- - ``NaN`` or infinities always follow ``F`` formatting.
-
- - ``c``: Used for formatting a ``char`` value.
-
- - ``s``: Used for formatting a string of ``char`` values.
-
- - If width is specified, the null terminator character is included as a
- character for width count.
-
- - If precision is specified, no more ``char``\s than that value will be
- written from the string (padding is used to fill additional width).
-
- - ``p``: Used for formatting a pointer address.
-
- - ``%``: Prints a single ``%``. Only valid as ``%%`` (supports no flags,
- width, precision, or length modifiers).
-
-Underspecified details:
-
-- If both ``+`` and (space) flags appear, the (space) is ignored.
-- The ``+`` and (space) flags will error if used with ``c`` or ``s``.
-- The ``#`` flag will error if used with ``d``, ``i``, ``u``, ``c``, ``s``, or
- ``p``.
-- The ``0`` flag will error if used with ``c``, ``s``, or ``p``.
-- Both ``+`` and (space) can work with the unsigned integer specifiers ``u``,
- ``o``, ``x``, and ``X``.
-- If a length modifier is provided for an incorrect specifier, it is ignored.
-- The ``z`` length modifier will decode arugments as signed as long as ``d`` or
- ``i`` is used.
-- ``p`` is implementation defined.
-
- - For this implementation, it will print with a ``0x`` prefix and then the
- pointer value was printed using ``%08X``.
-
- - ``p`` supports the ``+``, ``-``, and (space) flags, but not the ``#`` or
- ``0`` flags.
-
- - None of the length modifiers are usable with ``p``.
-
- - This implementation will try to adhere to user-specified width (assuming the
- width provided is larger than the guaranteed minimum of ``10``).
-
- - Specifying precision for ``p`` is considered an error.
-- Only ``%%`` is allowed with no other modifiers. Things like ``%+%`` will fail
- to decode. Some C stdlib implementations support any modifiers being
- present between ``%``, but ignore any for the output.
-- If a width is specified with the ``0`` flag for a negative value, the padded
- ``0``\s will appear after the ``-`` symbol.
-- A precision of ``0`` for ``d``, ``i``, ``u``, ``o``, ``x``, or ``X`` means
- that no character is written for the value ``0``.
-- Precision cannot be specified for ``c``.
-- Using ``*`` or fixed precision with the ``s`` specifier still requires the
- string argument to be null-terminated. This is due to argument encoding
- happening on the C/C++-side while the precision value is not read or
- otherwise used until decoding happens in this Python code.
-
-Non-conformant details:
-
-- ``n`` specifier: We do not support the ``n`` specifier since it is impossible
- for us to retroactively tell the original program how many characters have
- been printed since this decoding happens a great deal of time after the
- device sent it, usually on a separate processing device entirely.
-
-C++
-===
-The C++ detokenization libraries can be used in C++ or any language that can
-call into C++ with a C-linkage wrapper, such as Java or Rust. A reference
-Java Native Interface (JNI) implementation is provided.
-
-The C++ detokenization library uses binary-format token databases (created with
-``database.py create --type binary``). Read a binary format database from a
-file or include it in the source code. Pass the database array to
-``TokenDatabase::Create``, and construct a detokenizer.
-
-.. code-block:: cpp
-
- Detokenizer detokenizer(TokenDatabase::Create(token_database_array));
-
- std::string ProcessLog(span<uint8_t> log_data) {
- return detokenizer.Detokenize(log_data).BestString();
- }
-
-The ``TokenDatabase`` class verifies that its data is valid before using it. If
-it is invalid, the ``TokenDatabase::Create`` returns an empty database for which
-``ok()`` returns false. If the token database is included in the source code,
-this check can be done at compile time.
-
-.. code-block:: cpp
-
- // This line fails to compile with a static_assert if the database is invalid.
- constexpr TokenDatabase kDefaultDatabase = TokenDatabase::Create<kData>();
-
- Detokenizer OpenDatabase(std::string_view path) {
- std::vector<uint8_t> data = ReadWholeFile(path);
-
- TokenDatabase database = TokenDatabase::Create(data);
-
- // This checks if the file contained a valid database. It is safe to use a
- // TokenDatabase that failed to load (it will be empty), but it may be
- // desirable to provide a default database or otherwise handle the error.
- if (database.ok()) {
- return Detokenizer(database);
- }
- return Detokenizer(kDefaultDatabase);
- }
-
-
-TypeScript
-==========
-To detokenize in TypeScript, import ``Detokenizer`` from the ``pigweedjs``
-package, and instantiate it with a CSV token database.
-
-.. code-block:: typescript
-
- import { pw_tokenizer, pw_hdlc } from 'pigweedjs';
- const { Detokenizer } = pw_tokenizer;
- const { Frame } = pw_hdlc;
-
- const detokenizer = new Detokenizer(String(tokenCsv));
-
- function processLog(frame: Frame){
- const result = detokenizer.detokenize(frame);
- console.log(result);
- }
-
-For messages that are encoded in Base64, use ``Detokenizer::detokenizeBase64``.
-`detokenizeBase64` will also attempt to detokenize nested Base64 tokens. There
-is also `detokenizeUint8Array` that works just like `detokenize` but expects
-`Uint8Array` instead of a `Frame` argument.
-
-Protocol buffers
-================
-``pw_tokenizer`` provides utilities for handling tokenized fields in protobufs.
-See :ref:`module-pw_tokenizer-proto` for details.
-
-.. toctree::
- :hidden:
-
- proto.rst
-
--------------
-Base64 format
--------------
-The tokenizer encodes messages to a compact binary representation. Applications
-may desire a textual representation of tokenized strings. This makes it easy to
-use tokenized messages alongside plain text messages, but comes at a small
-efficiency cost: encoded Base64 messages occupy about 4/3 (133%) as much memory
-as binary messages.
-
-The Base64 format is comprised of a ``$`` character followed by the
-Base64-encoded contents of the tokenized message. For example, consider
-tokenizing the string ``This is an example: %d!`` with the argument -1. The
-string's token is 0x4b016e66.
-
-.. code-block:: text
-
- Source code: PW_LOG("This is an example: %d!", -1);
-
- Plain text: This is an example: -1! [23 bytes]
-
- Binary: 66 6e 01 4b 01 [ 5 bytes]
-
- Base64: $Zm4BSwE= [ 9 bytes]
-
-Encoding
-========
-To encode with the Base64 format, add a call to
-``pw::tokenizer::PrefixedBase64Encode`` or ``pw_tokenizer_PrefixedBase64Encode``
-in the tokenizer handler function. For example,
-
-.. code-block:: cpp
-
- void TokenizedMessageHandler(const uint8_t encoded_message[],
- size_t size_bytes) {
- pw::InlineBasicString base64 = pw::tokenizer::PrefixedBase64Encode(
- pw::span(encoded_message, size_bytes));
-
- TransmitLogMessage(base64.data(), base64.size());
- }
-
-Decoding
-========
-The Python ``Detokenizer`` class supprts decoding and detokenizing prefixed
-Base64 messages with ``detokenize_base64`` and related methods.
-
-.. tip::
- The Python detokenization tools support recursive detokenization for prefixed
- Base64 text. Tokenized strings found in detokenized text are detokenized, so
- prefixed Base64 messages can be passed as ``%s`` arguments.
-
- For example, the tokenized string for "Wow!" is ``$RhYjmQ==``. This could be
- passed as an argument to the printf-style string ``Nested message: %s``, which
- encodes to ``$pEVTYQkkUmhZam1RPT0=``. The detokenizer would decode the message
- as follows:
-
- ::
-
- "$pEVTYQkkUmhZam1RPT0=" → "Nested message: $RhYjmQ==" → "Nested message: Wow!"
-
-Base64 decoding is supported in C++ or C with the
-``pw::tokenizer::PrefixedBase64Decode`` or ``pw_tokenizer_PrefixedBase64Decode``
-functions.
-
-Investigating undecoded messages
-================================
-Tokenized messages cannot be decoded if the token is not recognized. The Python
-package includes the ``parse_message`` tool, which parses tokenized Base64
-messages without looking up the token in a database. This tool attempts to guess
-the types of the arguments and displays potential ways to decode them.
-
-This tool can be used to extract argument information from an otherwise unusable
-message. It could help identify which statement in the code produced the
-message. This tool is not particularly helpful for tokenized messages without
-arguments, since all it can do is show the value of the unknown token.
-
-The tool is executed by passing Base64 tokenized messages, with or without the
-``$`` prefix, to ``pw_tokenizer.parse_message``. Pass ``-h`` or ``--help`` to
-see full usage information.
-
-Example
--------
-.. code-block::
-
- $ python -m pw_tokenizer.parse_message '$329JMwA=' koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw== --specs %s %d
-
- INF Decoding arguments for '$329JMwA='
- INF Binary: b'\xdfoI3\x00' [df 6f 49 33 00] (5 bytes)
- INF Token: 0x33496fdf
- INF Args: b'\x00' [00] (1 bytes)
- INF Decoding with up to 8 %s or %d arguments
- INF Attempt 1: [%s]
- INF Attempt 2: [%d] 0
-
- INF Decoding arguments for '$koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw=='
- INF Binary: b'\x92\x84\xa5\xe7n\x13FAILED_PRECONDITION\x02OK' [92 84 a5 e7 6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (28 bytes)
- INF Token: 0xe7a58492
- INF Args: b'n\x13FAILED_PRECONDITION\x02OK' [6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (24 bytes)
- INF Decoding with up to 8 %s or %d arguments
- INF Attempt 1: [%d %s %d %d %d] 55 FAILED_PRECONDITION 1 -40 -38
- INF Attempt 2: [%d %s %s] 55 FAILED_PRECONDITION OK
-
-Command line utilities
-----------------------
-``pw_tokenizer`` provides two standalone command line utilities for detokenizing
-Base64-encoded tokenized strings.
-
-* ``detokenize.py`` -- Detokenizes Base64-encoded strings in files or from
- stdin.
-* ``serial_detokenizer.py`` -- Detokenizes Base64-encoded strings from a
- connected serial device.
-
-If the ``pw_tokenizer`` Python package is installed, these tools may be executed
-as runnable modules. For example:
-
-.. code-block::
-
- # Detokenize Base64-encoded strings in a file
- python -m pw_tokenizer.detokenize -i input_file.txt
-
- # Detokenize Base64-encoded strings in output from a serial device
- python -m pw_tokenizer.serial_detokenizer --device /dev/ttyACM0
-
-See the ``--help`` options for these tools for full usage information.
-
---------------------
-Deployment war story
---------------------
-The tokenizer module was developed to bring tokenized logging to an
-in-development product. The product already had an established text-based
-logging system. Deploying tokenization was straightforward and had substantial
-benefits.
-
-Results
-=======
-* Log contents shrunk by over 50%, even with Base64 encoding.
-
- * Significant size savings for encoded logs, even using the less-efficient
- Base64 encoding required for compatibility with the existing log system.
- * Freed valuable communication bandwidth.
- * Allowed storing many more logs in crash dumps.
-
-* Substantial flash savings.
-
- * Reduced the size firmware images by up to 18%.
-
-* Simpler logging code.
-
- * Removed CPU-heavy ``snprintf`` calls.
- * Removed complex code for forwarding log arguments to a low-priority task.
-
-This section describes the tokenizer deployment process and highlights key
-insights.
-
-Firmware deployment
-===================
-* In the project's logging macro, calls to the underlying logging function were
- replaced with a tokenized log macro invocation.
-* The log level was passed as the payload argument to facilitate runtime log
- level control.
-* For this project, it was necessary to encode the log messages as text. In
- the handler function the log messages were encoded in the $-prefixed `Base64
- format`_, then dispatched as normal log messages.
-* Asserts were tokenized a callback-based API that has been removed (a
- :ref:`custom macro <module-pw_tokenizer-custom-macro>` is a better
- alternative).
-
-.. attention::
- Do not encode line numbers in tokenized strings. This results in a huge
- number of lines being added to the database, since every time code moves,
- new strings are tokenized. If :ref:`module-pw_log_tokenized` is used, line
- numbers are encoded in the log metadata. Line numbers may also be included by
- by adding ``"%d"`` to the format string and passing ``__LINE__``.
-
-Database management
-===================
-* The token database was stored as a CSV file in the project's Git repo.
-* The token database was automatically updated as part of the build, and
- developers were expected to check in the database changes alongside their code
- changes.
-* A presubmit check verified that all strings added by a change were added to
- the token database.
-* The token database included logs and asserts for all firmware images in the
- project.
-* No strings were purged from the token database.
-
-.. tip::
- Merge conflicts may be a frequent occurrence with an in-source CSV database.
- Use the `Directory database format`_ instead.
-
-Decoding tooling deployment
-===========================
-* The Python detokenizer in ``pw_tokenizer`` was deployed to two places:
-
- * Product-specific Python command line tools, using
- ``pw_tokenizer.Detokenizer``.
- * Standalone script for decoding prefixed Base64 tokens in files or
- live output (e.g. from ``adb``), using ``detokenize.py``'s command line
- interface.
-
-* The C++ detokenizer library was deployed to two Android apps with a Java
- Native Interface (JNI) layer.
-
- * The binary token database was included as a raw resource in the APK.
- * In one app, the built-in token database could be overridden by copying a
- file to the phone.
-
-.. tip::
- Make the tokenized logging tools simple to use for your project.
-
- * Provide simple wrapper shell scripts that fill in arguments for the
- project. For example, point ``detokenize.py`` to the project's token
- databases.
- * Use ``pw_tokenizer.AutoUpdatingDetokenizer`` to decode in
- continuously-running tools, so that users don't have to restart the tool
- when the token database updates.
- * Integrate detokenization everywhere it is needed. Integrating the tools
- takes just a few lines of code, and token databases can be embedded in APKs
- or binaries.
+.. _module-pw_tokenizer-tokenized-logging-example:
---------------------------
-Limitations and future work
+Tokenized logging in action
---------------------------
+Here's an example of how ``pw_tokenizer`` enables you to store
+and send the same logging information using significantly less
+resources:
+
+.. mermaid::
+
+ flowchart TD
+
+ subgraph after["After: Tokenized Logs (37 bytes saved!)"]
+ after_log["LOG(#quot;Battery Voltage: %d mV#quot;, voltage)"] -- 4 bytes stored on-device as... -->
+ after_encoding["d9 28 47 8e"] -- 6 bytes sent over the wire as... -->
+ after_transmission["d9 28 47 8e aa 3e"] -- Displayed in logs as... -->
+ after_display["#quot;Battery Voltage: 3989 mV#quot;"]
+ end
+
+ subgraph before["Before: No Tokenization"]
+ before_log["LOG(#quot;Battery Voltage: %d mV#quot;, voltage)"] -- 41 bytes stored on-device as... -->
+ before_encoding["#quot;Battery Voltage: %d mV#quot;"] -- 43 bytes sent over the wire as... -->
+ before_transmission["#quot;Battery Voltage: 3989 mV#quot;"] -- Displayed in logs as... -->
+ before_display["#quot;Battery Voltage: 3989 mV#quot;"]
+ end
+
+ style after stroke:#00c852,stroke-width:3px
+ style before stroke:#ff5252,stroke-width:3px
+
+A quick overview of how the tokenized version works:
+
+* You tokenize ``"Battery Voltage: %d mV"`` with a macro like
+ :c:macro:`PW_TOKENIZE_STRING`. You can use :ref:`module-pw_log_tokenized`
+ to handle the tokenization automatically.
+* After tokenization, ``"Battery Voltage: %d mV"`` becomes ``d9 28 47 8e``.
+* The first 4 bytes sent over the wire is the tokenized version of
+ ``"Battery Voltage: %d mV"``. The last 2 bytes are the value of ``voltage``
+ converted to a varint using :ref:`module-pw_varint`.
+* The logs are converted back to the original, human-readable message
+ via the :ref:`Detokenization API <module-pw_tokenizer-detokenization>`
+ and a :ref:`token database <module-pw_tokenizer-token-databases>`.
-GCC bug: tokenization in template functions
-===========================================
-GCC incorrectly ignores the section attribute for template `functions
-<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70435>`_ and `variables
-<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88061>`_. For example, the
-following won't work when compiling with GCC and tokenized logging:
-
-.. code-block:: cpp
-
- template <...>
- void DoThings() {
- int value = GetValue();
- // This log won't work with tokenized logs due to the templated context.
- PW_LOG_INFO("Got value: %d", value);
- ...
- }
-
-The bug causes tokenized strings in template functions to be emitted into
-``.rodata`` instead of the special tokenized string section. This causes two
-problems:
-
-1. Tokenized strings will not be discovered by the token database tools.
-2. Tokenized strings may not be removed from the final binary.
-
-There are two workarounds.
-
-#. **Use Clang.** Clang puts the string data in the requested section, as
- expected. No extra steps are required.
-
-#. **Move tokenization calls to a non-templated context.** Creating a separate
- non-templated function and invoking it from the template resolves the issue.
- This enables tokenizing in most cases encountered in practice with
- templates.
-
- .. code-block:: cpp
-
- // In .h file:
- void LogThings(value);
-
- template <...>
- void DoThings() {
- int value = GetValue();
- // This log will work: calls non-templated helper.
- LogThings(value);
- ...
- }
-
- // In .cc file:
- void LogThings(int value) {
- // Tokenized logging works as expected in this non-templated context.
- PW_LOG_INFO("Got value %d", value);
- }
-
-There is a third option, which isn't implemented yet, which is to compile the
-binary twice: once to extract the tokens, and once for the production binary
-(without tokens). If this is interesting to you please get in touch.
-
-64-bit tokenization
-===================
-The Python and C++ detokenizing libraries currently assume that strings were
-tokenized on a system with 32-bit ``long``, ``size_t``, ``intptr_t``, and
-``ptrdiff_t``. Decoding may not work correctly for these types if a 64-bit
-device performed the tokenization.
-
-Supporting detokenization of strings tokenized on 64-bit targets would be
-simple. This could be done by adding an option to switch the 32-bit types to
-64-bit. The tokenizer stores the sizes of these types in the
-``.pw_tokenizer.info`` ELF section, so the sizes of these types can be verified
-by checking the ELF file, if necessary.
-
-Tokenization in headers
-=======================
-Tokenizing code in header files (inline functions or templates) may trigger
-warnings such as ``-Wlto-type-mismatch`` under certain conditions. That
-is because tokenization requires declaring a character array for each tokenized
-string. If the tokenized string includes macros that change value, the size of
-this character array changes, which means the same static variable is defined
-with different sizes. It should be safe to suppress these warnings, but, when
-possible, code that tokenizes strings with macros that can change value should
-be moved to source files rather than headers.
-
-Tokenized strings as ``%s`` arguments
-=====================================
-Encoding ``%s`` string arguments is inefficient, since ``%s`` strings are
-encoded 1:1, with no tokenization. It would be better to send a tokenized string
-literal as an integer instead of a string argument, but this is not yet
-supported.
-
-A string token could be sent by marking an integer % argument in a way
-recognized by the detokenization tools. The detokenizer would expand the
-argument to the string represented by the integer.
-
-.. code-block:: cpp
-
- #define PW_TOKEN_ARG PRIx32 "<PW_TOKEN]"
-
- constexpr uint32_t answer_token = PW_TOKENIZE_STRING("Uh, who is there");
-
- PW_TOKENIZE_STRING("Knock knock: %" PW_TOKEN_ARG "?", answer_token);
-
-Strings with arguments could be encoded to a buffer, but since printf strings
-are null-terminated, a binary encoding would not work. These strings can be
-prefixed Base64-encoded and sent as ``%s`` instead. See `Base64 format`_.
-
-Another possibility: encode strings with arguments to a ``uint64_t`` and send
-them as an integer. This would be efficient and simple, but only support a small
-number of arguments.
-
--------------
-Compatibility
--------------
-* C11
-* C++14
-* Python 3
+.. toctree::
+ :hidden:
+ :maxdepth: 1
-------------
-Dependencies
-------------
-* ``pw_varint`` module
-* ``pw_preprocessor`` module
-* ``pw_span`` module
+ Get started <get_started>
+ tokenization
+ token_databases
+ detokenization
+ API reference <api>
diff --git a/pw_tokenizer/encode_args.cc b/pw_tokenizer/encode_args.cc
index 444afb73a..9939c1c2d 100644
--- a/pw_tokenizer/encode_args.cc
+++ b/pw_tokenizer/encode_args.cc
@@ -20,6 +20,10 @@
#include "pw_preprocessor/compiler.h"
#include "pw_varint/varint.h"
+static_assert((PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES == 4) ||
+ (PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES == 8),
+ "PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES must be 4 or 8");
+
namespace pw {
namespace tokenizer {
namespace {
@@ -33,11 +37,12 @@ enum class ArgType : uint8_t {
};
size_t EncodeInt(int value, const span<std::byte>& output) {
- return varint::Encode(value, as_writable_bytes(output));
+ // Use the 64-bit function to avoid instantiating both 32-bit and 64-bit.
+ return pw_tokenizer_EncodeInt64(value, output.data(), output.size());
}
size_t EncodeInt64(int64_t value, const span<std::byte>& output) {
- return varint::Encode(value, as_writable_bytes(output));
+ return pw_tokenizer_EncodeInt64(value, output.data(), output.size());
}
size_t EncodeFloat(float value, const span<std::byte>& output) {
diff --git a/pw_tokenizer/encode_args_test.cc b/pw_tokenizer/encode_args_test.cc
index 131f27b9a..3947ac35e 100644
--- a/pw_tokenizer/encode_args_test.cc
+++ b/pw_tokenizer/encode_args_test.cc
@@ -38,5 +38,17 @@ static_assert(
MinEncodingBufferSizeBytes<const char*, long long, int, short>() ==
4 + 1 + 10 + 5 + 3);
+TEST(TokenizerCEncodingFunctions, EncodeInt) {
+ uint8_t buffer[5] = {};
+ EXPECT_EQ(pw_tokenizer_EncodeInt(-1, buffer, sizeof(buffer)), 1u);
+ EXPECT_EQ(buffer[0], 1); // -1 encodes to 1 with ZigZag
+}
+
+TEST(TokenizerCEncodingFunctions, EncodeInt64) {
+ uint8_t buffer[5] = {};
+ EXPECT_EQ(pw_tokenizer_EncodeInt(1, buffer, sizeof(buffer)), 1u);
+ EXPECT_EQ(buffer[0], 2); // 1 encodes to 2 with ZigZag
+}
+
} // namespace tokenizer
} // namespace pw
diff --git a/pw_tokenizer/get_started.rst b/pw_tokenizer/get_started.rst
new file mode 100644
index 000000000..f424ed9bb
--- /dev/null
+++ b/pw_tokenizer/get_started.rst
@@ -0,0 +1,95 @@
+.. _module-pw_tokenizer-get-started:
+
+=============================
+Get started with pw_tokenizer
+=============================
+.. pigweed-module-subpage::
+ :name: pw_tokenizer
+ :tagline: Compress strings to shrink logs by +75%
+
+.. _module-pw_tokenizer-get-started-overview:
+
+--------
+Overview
+--------
+There are two sides to ``pw_tokenizer``, which we call tokenization and
+detokenization.
+
+* **Tokenization** converts string literals in the source code to binary tokens
+ at compile time. If the string has printf-style arguments, these are encoded
+ to compact binary form at runtime.
+* **Detokenization** converts tokenized strings back to the original
+ human-readable strings.
+
+Here's an overview of what happens when ``pw_tokenizer`` is used:
+
+1. During compilation, the ``pw_tokenizer`` module hashes string literals to
+ generate stable 32-bit tokens.
+2. The tokenization macro removes these strings by declaring them in an ELF
+ section that is excluded from the final binary.
+3. After compilation, strings are extracted from the ELF to build a database of
+ tokenized strings for use by the detokenizer. The ELF file may also be used
+ directly.
+4. During operation, the device encodes the string token and its arguments, if
+ any.
+5. The encoded tokenized strings are sent off-device or stored.
+6. Off-device, the detokenizer tools use the token database to decode the
+ strings to human-readable form.
+
+.. _module-pw_tokenizer-get-started-integration:
+
+Integrating with Bazel / GN / CMake projects
+============================================
+Integrating ``pw_tokenizer`` requires a few steps beyond building the code. This
+section describes one way ``pw_tokenizer`` might be integrated with a project.
+These steps can be adapted as needed.
+
+#. Add ``pw_tokenizer`` to your build. Build files for GN, CMake, and Bazel are
+ provided. For Make or other build systems, add the files specified in the
+ BUILD.gn's ``pw_tokenizer`` target to the build.
+#. Use the tokenization macros in your code. See
+ :ref:`module-pw_tokenizer-tokenization`.
+#. Ensure the ``.pw_tokenizer.*`` sections are included in your output ELF file:
+
+ * In GN and CMake, this is done automatically.
+ * In Bazel, include ``"@pigweed//pw_tokenizer:linker_script"`` in the
+ ``deps`` of your main binary rule (assuming you're already overriding the
+ default linker script).
+ * If your binary does not use a custom linker script, you can pass
+ ``add_tokenizer_sections_to_default_script.ld`` to the linker which will
+ augment the default linker script (rather than override it).
+ * Alternatively, add the contents of ``pw_tokenizer_linker_sections.ld`` to
+ your project's linker script.
+
+#. Compile your code to produce an ELF file.
+#. Run ``database.py create`` on the ELF file to generate a CSV token
+ database. See :ref:`module-pw_tokenizer-managing-token-databases`.
+#. Commit the token database to your repository. See notes in
+ :ref:`module-pw_tokenizer-database-management`.
+#. Integrate a ``database.py add`` command to your build to automatically update
+ the committed token database. In GN, use the ``pw_tokenizer_database``
+ template to do this. See :ref:`module-pw_tokenizer-update-token-database`.
+#. Integrate ``detokenize.py`` or the C++ detokenization library with your tools
+ to decode tokenized logs. See :ref:`module-pw_tokenizer-detokenization`.
+
+Using with Zephyr
+=================
+When building ``pw_tokenizer`` with Zephyr, 3 Kconfigs can be used currently:
+
+* ``CONFIG_PIGWEED_TOKENIZER`` will automatically link ``pw_tokenizer`` as well
+ as any dependencies.
+* ``CONFIG_PIGWEED_TOKENIZER_BASE64`` will automatically link
+ ``pw_tokenizer.base64`` as well as any dependencies.
+* ``CONFIG_PIGWEED_DETOKENIZER`` will automatically link
+ ``pw_tokenizer.decoder`` as well as any dependencies.
+
+Once enabled, the tokenizer headers can be included like any Zephyr headers:
+
+.. code-block:: cpp
+
+ #include <pw_tokenizer/tokenize.h>
+
+.. note::
+ Zephyr handles the additional linker sections via
+ ``pw_tokenizer_zephyr.ld`` which is added to the end of the linker file
+ via a call to ``zephyr_linker_sources(SECTIONS ...)``.
diff --git a/pw_tokenizer/options.proto b/pw_tokenizer/options.proto
index 288a2dac6..1ffbfece0 100644
--- a/pw_tokenizer/options.proto
+++ b/pw_tokenizer/options.proto
@@ -29,7 +29,7 @@ enum Tokenization {
extend google.protobuf.FieldOptions {
// The field number was randomly selected from the reserved, internal use
// field numbers (50000-99999).
- // TODO(b/234886465): Register with the Protobuf Global Extension Registry:
+ // TODO: b/234886465 - Register with the Protobuf Global Extension Registry:
// https://github.com/protocolbuffers/protobuf/blob/HEAD/docs/options.md
Tokenization format = 78576;
}
diff --git a/pw_tokenizer/proto.rst b/pw_tokenizer/proto.rst
deleted file mode 100644
index 2479dd47f..000000000
--- a/pw_tokenizer/proto.rst
+++ /dev/null
@@ -1,138 +0,0 @@
-.. _module-pw_tokenizer-proto:
-
-------------------------------------
-Tokenized fields in protocol buffers
-------------------------------------
-Text may be represented in a few different ways:
-
-- Plain ASCII or UTF-8 text (``This is plain text``)
-- Base64-encoded tokenized message (``$ibafcA==``)
-- Binary-encoded tokenized message (``89 b6 9f 70``)
-- Little-endian 32-bit integer token (``0x709fb689``)
-
-``pw_tokenizer`` provides tools for working with protobuf fields that may
-contain tokenized text.
-
-Tokenized field protobuf option
-===============================
-``pw_tokenizer`` provides the ``pw.tokenizer.format`` protobuf field option.
-This option may be applied to a protobuf field to indicate that it may contain a
-tokenized string. A string that is optionally tokenized is represented with a
-single ``bytes`` field annotated with ``(pw.tokenizer.format) =
-TOKENIZATION_OPTIONAL``.
-
-For example, the following protobuf has one field that may contain a tokenized
-string.
-
-.. code-block:: protobuf
-
- message MessageWithOptionallyTokenizedField {
- bytes just_bytes = 1;
- bytes maybe_tokenized = 2 [(pw.tokenizer.format) = TOKENIZATION_OPTIONAL];
- string just_text = 3;
- }
-
-Decoding optionally tokenized strings
-=====================================
-The encoding used for an optionally tokenized field is not recorded in the
-protobuf. Despite this, the text can reliably be decoded. This is accomplished
-by attempting to decode the field as binary or Base64 tokenized data before
-treating it like plain text.
-
-The following diagram describes the decoding process for optionally tokenized
-fields in detail.
-
-.. mermaid::
-
- flowchart TD
- start([Received bytes]) --> binary
-
- binary[Decode as<br>binary tokenized] --> binary_ok
- binary_ok{Detokenizes<br>successfully?} -->|no| utf8
- binary_ok -->|yes| done_binary([Display decoded binary])
-
- utf8[Decode as UTF-8] --> utf8_ok
- utf8_ok{Valid UTF-8?} -->|no| base64_encode
- utf8_ok -->|yes| base64
-
- base64_encode[Encode as<br>tokenized Base64] --> display
- display([Display encoded Base64])
-
- base64[Decode as<br>Base64 tokenized] --> base64_ok
-
- base64_ok{Fully<br>or partially<br>detokenized?} -->|no| is_plain_text
- base64_ok -->|yes| base64_results
-
- is_plain_text{Text is<br>printable?} -->|no| base64_encode
- is_plain_text-->|yes| plain_text
-
- base64_results([Display decoded Base64])
- plain_text([Display text])
-
-Potential decoding problems
----------------------------
-The decoding process for optionally tokenized fields will yield correct results
-in almost every situation. In rare circumstances, it is possible for it to fail,
-but these can be avoided with a low-overhead mitigation if desired.
-
-There are two ways in which the decoding process may fail.
-
-Accidentally interpreting plain text as tokenized binary
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-If a plain-text string happens to decode as a binary tokenized message, the
-incorrect message could be displayed. This is very unlikely to occur. While many
-tokens will incidentally end up being valid UTF-8 strings, it is highly unlikely
-that a device will happen to log one of these strings as plain text. The
-overwhelming majority of these strings will be nonsense.
-
-If an implementation wishes to guard against this extremely improbable
-situation, it is possible to prevent it. This situation is prevented by
-appending 0xFF (or another byte never valid in UTF-8) to binary tokenized data
-that happens to be valid UTF-8 (or all binary tokenized messages, if desired).
-When decoding, if there is an extra 0xFF byte, it is discarded.
-
-Displaying undecoded binary as plain text instead of Base64
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-If a message fails to decode as binary tokenized and it is not valid UTF-8, it
-is displayed as tokenized Base64. This makes it easily recognizable as a
-tokenized message and makes it simple to decode later from the text output (for
-example, with an updated token database).
-
-A binary message for which the token is not known may coincidentally be valid
-UTF-8 or ASCII. 6.25% of 4-byte sequences are composed only of ASCII characters.
-When decoding with an out-of-date token database, it is possible that some
-binary tokenized messages will be displayed as plain text rather than tokenized
-Base64.
-
-This situation is likely to occur, but should be infrequent. Even if it does
-happen, it is not a serious issue. A very small number of strings will be
-displayed incorrectly, but these strings cannot be decoded anyway. One nonsense
-string (e.g. ``a-D1``) would be displayed instead of another (``$YS1EMQ==``).
-Updating the token database would resolve the issue, though the non-Base64 logs
-would be difficult decode later from a log file.
-
-This situation can be avoided with the same approach described in
-`Accidentally interpreting plain text as tokenized binary`_. Appending
-an invalid UTF-8 character prevents the undecoded binary message from being
-interpreted as plain text.
-
-Python library
-==============
-The ``pw_tokenizer.proto`` module defines functions that may be used to
-detokenize protobuf objects in Python. The function
-:func:`pw_tokenizer.proto.detokenize_fields` detokenizes all fields annotated as
-tokenized, replacing them with their detokenized version. For example:
-
-.. code-block:: python
-
- my_detokenizer = pw_tokenizer.Detokenizer(some_database)
-
- my_message = SomeMessage(tokenized_field=b'$YS1EMQ==')
- pw_tokenizer.proto.detokenize_fields(my_detokenizer, my_message)
-
- assert my_message.tokenized_field == b'The detokenized string! Cool!'
-
-pw_tokenizer.proto
-------------------
-.. automodule:: pw_tokenizer.proto
- :members:
diff --git a/pw_tokenizer/public/pw_tokenizer/base64.h b/pw_tokenizer/public/pw_tokenizer/base64.h
index 36acaf20d..2729ef61b 100644
--- a/pw_tokenizer/public/pw_tokenizer/base64.h
+++ b/pw_tokenizer/public/pw_tokenizer/base64.h
@@ -31,11 +31,7 @@
#include "pw_preprocessor/util.h"
#include "pw_tokenizer/config.h"
-
-// This character is used to mark the start of a Base64-encoded tokenized
-// message. For consistency, it is recommended to always use $ if possible.
-// If required, a different non-Base64 character may be used as a prefix.
-#define PW_TOKENIZER_BASE64_PREFIX '$'
+#include "pw_tokenizer/nested_tokenization.h"
PW_EXTERN_C_START
@@ -74,15 +70,11 @@ PW_EXTERN_C_END
namespace pw::tokenizer {
-inline constexpr char kBase64Prefix = PW_TOKENIZER_BASE64_PREFIX;
-
-#undef PW_TOKENIZER_BASE64_PREFIX // In C++, use the variable, not the macro.
-
// Returns the size of a tokenized message (token + arguments) when encoded as
// prefixed Base64. Includes room for the prefix character ($) and encoded
// message. This value is the capacity needed to encode to a pw::InlineString.
constexpr size_t Base64EncodedStringSize(size_t message_size) {
- return sizeof(kBase64Prefix) + base64::EncodedSize(message_size);
+ return sizeof(PW_TOKENIZER_NESTED_PREFIX) + base64::EncodedSize(message_size);
}
// Same as Base64EncodedStringSize(), but for sizing char buffers. Includes room
@@ -91,11 +83,6 @@ constexpr size_t Base64EncodedBufferSize(size_t message_size) {
return Base64EncodedStringSize(message_size) + sizeof('\0');
}
-// The minimum buffer size that can hold a tokenized message that is
-// PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES long encoded as prefixed Base64.
-inline constexpr size_t kDefaultBase64EncodedBufferSize =
- Base64EncodedBufferSize(PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES);
-
// Encodes a binary tokenized message as prefixed Base64 with a null terminator.
// Returns the encoded string length (excluding the null terminator). Returns 0
// if the buffer is too small. Always null terminates if the output buffer is
@@ -128,18 +115,16 @@ inline void PrefixedBase64Encode(span<const uint8_t> binary_message,
// Encodes a binary tokenized message as prefixed Base64 to a pw::InlineString.
// The pw::InlineString is sized to fit messages up to
// kMaxBinaryMessageSizeBytes long. Asserts if the message is larger.
-template <size_t kMaxBinaryMessageSizeBytes =
- PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+template <size_t kMaxBinaryMessageSizeBytes>
auto PrefixedBase64Encode(span<const std::byte> binary_message) {
static_assert(kMaxBinaryMessageSizeBytes >= 1, "Messages cannot be empty");
InlineString<Base64EncodedStringSize(kMaxBinaryMessageSizeBytes)> string(
- 1, kBase64Prefix);
+ 1, PW_TOKENIZER_NESTED_PREFIX);
base64::Encode(binary_message, string);
return string;
}
-template <size_t kMaxBinaryMessageSizeBytes =
- PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+template <size_t kMaxBinaryMessageSizeBytes>
auto PrefixedBase64Encode(span<const uint8_t> binary_message) {
return PrefixedBase64Encode<kMaxBinaryMessageSizeBytes>(
as_bytes(binary_message));
@@ -163,6 +148,10 @@ inline size_t PrefixedBase64DecodeInPlace(span<std::byte> buffer) {
buffer.data(), buffer.size(), buffer.data(), buffer.size());
}
+inline size_t PrefixedBase64DecodeInPlace(span<char> buffer) {
+ return PrefixedBase64DecodeInPlace(as_writable_bytes(buffer));
+}
+
// Decodes a prefixed Base64 tokenized message to binary in place. Resizes the
// string to fit the decoded binary data.
template <typename CharT>
diff --git a/pw_tokenizer/public/pw_tokenizer/config.h b/pw_tokenizer/public/pw_tokenizer/config.h
index d48ce53e3..6557907c0 100644
--- a/pw_tokenizer/public/pw_tokenizer/config.h
+++ b/pw_tokenizer/public/pw_tokenizer/config.h
@@ -15,54 +15,56 @@
// Configuration macros for the tokenizer module.
#pragma once
-#include <assert.h>
-
-// For a tokenized string that has arguments, the types of the arguments are
-// encoded in either a 4-byte (uint32_t) or a 8-byte (uint64_t) value. The 4 or
-// 6 least-significant bits, respectively, store the number of arguments, while
-// the remaining bits encode the argument types. Argument types are encoded
-// two-bits per argument, in little-endian order. Up to 14 arguments in 4 bytes
-// or 29 arguments in 8 bytes are supported.
+/// For a tokenized string with arguments, the types of the arguments are
+/// encoded in either 4 bytes (`uint32_t`) or 8 bytes (`uint64_t`). 4 bytes
+/// supports up to 14 tokenized string arguments; 8 bytes supports up to 29
+/// arguments. Using 8 bytes increases code size for 32-bit machines.
+///
+/// Argument types are encoded two bits per argument, in little-endian order.
+/// The 4 or 6 least-significant bits, respectively, store the number of
+/// arguments, while the remaining bits encode the argument types.
#ifndef PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES
#define PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES 4
#endif // PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES
-static_assert(PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES == 4 ||
- PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES == 8,
- "PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES must be 4 or 8");
-
-// Maximum number of characters to hash in C. In C code, strings shorter than
-// this length are treated as if they were zero-padded up to the length. Strings
-// that are the same length and share a common prefix longer than this value
-// hash to the same value. Increasing PW_TOKENIZER_CFG_C_HASH_LENGTH increases
-// the compilation time for C due to the complexity of the hashing macros.
-//
-// PW_TOKENIZER_CFG_C_HASH_LENGTH has no effect on C++ code. In C++, hashing is
-// done with a constexpr function instead of a macro. There are no string length
-// limitations and compilation times are unaffected by this macro.
-//
-// Only hash lengths for which there is a corresponding macro header
-// (pw_tokenizer/internal/mash_macro_#.h) are supported. Additional macros may
-// be generated with the generate_hash_macro.py function. New macro headers must
-// then be added to pw_tokenizer/internal/hash.h.
-//
-// This MUST match the value of DEFAULT_C_HASH_LENGTH in
-// pw_tokenizer/py/pw_tokenizer/tokens.py.
+/// Maximum number of characters to hash in C. In C code, strings shorter than
+/// this length are treated as if they were zero-padded up to the length.
+/// Strings that are the same length and share a common prefix longer than this
+/// value hash to the same value. Increasing `PW_TOKENIZER_CFG_C_HASH_LENGTH`
+/// increases the compilation time for C due to the complexity of the hashing
+/// macros.
+///
+/// `PW_TOKENIZER_CFG_C_HASH_LENGTH` has no effect on C++ code. In C++, hashing
+/// is done with a `constexpr` function instead of a macro. There are no string
+/// length limitations and compilation times are unaffected by this macro.
+///
+/// Only hash lengths for which there is a corresponding macro header
+/// (`pw_tokenizer/internal/pw_tokenizer_65599_fixed_length_#_hash_macro.`) are
+/// supported. Additional macros may be generated with the
+/// `generate_hash_macro.py` function. New macro headers must then be added to
+/// `pw_tokenizer/internal/tokenize_string.h`.
+///
+/// This MUST match the value of `DEFAULT_C_HASH_LENGTH` in
+/// `pw_tokenizer/py/pw_tokenizer/tokens.py`.
#ifndef PW_TOKENIZER_CFG_C_HASH_LENGTH
#define PW_TOKENIZER_CFG_C_HASH_LENGTH 128
#endif // PW_TOKENIZER_CFG_C_HASH_LENGTH
-// The size of the stack-allocated argument encoding buffer to use by default.
-// This only affects tokenization macros that use the
-// pw::tokenizer::EncodedMessage class. A buffer of this size is allocated and
-// used for the 4-byte token and for encoding all arguments. It must be at least
-// large enough for the token (4 bytes).
-//
-// This buffer does not need to be large to accommodate a good number of
-// tokenized string arguments. Integer arguments are usually encoded smaller
-// than their native size (e.g. 1 or 2 bytes for smaller numbers). All floating
-// point types are encoded as four bytes. Null-terminated strings are encoded
-// 1:1 in size.
+/// `PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES` is deprecated. It is used as
+/// the default value for pw_log_tokenized's
+/// @c_macro{PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES}. This value should not
+/// be configured; set @c_macro{PW_LOG_TOKENIZED_ENCODING_BUFFER_SIZE_BYTES}
+/// instead.
#ifndef PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES
#define PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES 52
#endif // PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES
+
+// This character is used to mark the start of all tokenized messages. For
+// consistency, it is recommended to always use $ if possible.
+// If required, a different non-Base64 character may be used as a prefix.
+//
+// A string version of the character is required for format-string-literal
+// concatenation.
+#ifndef PW_TOKENIZER_NESTED_PREFIX_STR
+#define PW_TOKENIZER_NESTED_PREFIX_STR "$"
+#endif // PW_TOKENIZER_NESTED_PREFIX_STR
diff --git a/pw_tokenizer/public/pw_tokenizer/detokenize.h b/pw_tokenizer/public/pw_tokenizer/detokenize.h
index 9a3e58847..d26bf2e81 100644
--- a/pw_tokenizer/public/pw_tokenizer/detokenize.h
+++ b/pw_tokenizer/public/pw_tokenizer/detokenize.h
@@ -84,7 +84,16 @@ class Detokenizer {
// that stores all possible detokenized string results.
DetokenizedString Detokenize(const span<const uint8_t>& encoded) const;
- DetokenizedString Detokenize(const std::string_view& encoded) const {
+ // Decodes and detokenizes a Base64 encoded message. Returns a
+ // DetokenizedString that stores all possible detokenized string results.
+ DetokenizedString DetokenizeBase64Message(std::string_view text) const;
+
+ // Decodes and detokenizes nested Base64 messages in a string. Returns the
+ // original string with Base64 tokenized messages decoded in context. Messages
+ // that fail to decode are left as is.
+ std::string DetokenizeBase64(std::string_view text) const;
+
+ DetokenizedString Detokenize(std::string_view encoded) const {
return Detokenize(encoded.data(), encoded.size());
}
diff --git a/pw_tokenizer/public/pw_tokenizer/encode_args.h b/pw_tokenizer/public/pw_tokenizer/encode_args.h
index 07e6e3192..e5b585cc3 100644
--- a/pw_tokenizer/public/pw_tokenizer/encode_args.h
+++ b/pw_tokenizer/public/pw_tokenizer/encode_args.h
@@ -20,6 +20,7 @@
#include "pw_polyfill/standard.h"
#include "pw_preprocessor/util.h"
#include "pw_tokenizer/internal/argument_types.h"
+#include "pw_varint/varint.h"
#if PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -82,10 +83,8 @@ size_t EncodeArgs(pw_tokenizer_ArgTypes types,
va_list args,
span<std::byte> output);
-/// Encodes a tokenized message to a fixed size buffer. By default, the buffer
-/// size is set by the @c_macro{PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES}
-/// config macro. This class is used to encode tokenized messages passed in from
-/// tokenization macros.
+/// Encodes a tokenized message to a fixed size buffer. This class is used to
+/// encode tokenized messages passed in from tokenization macros.
///
/// To use `pw::tokenizer::EncodedMessage`, construct it with the token,
/// argument types, and `va_list` from the variadic arguments:
@@ -104,7 +103,7 @@ size_t EncodeArgs(pw_tokenizer_ArgTypes types,
/// SendLogMessage(encoded_message); // EncodedMessage converts to span
/// }
/// @endcode
-template <size_t kMaxSizeBytes = PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+template <size_t kMaxSizeBytes>
class EncodedMessage {
public:
// Encodes a tokenized message to an internal buffer.
@@ -149,4 +148,23 @@ size_t pw_tokenizer_EncodeArgs(pw_tokenizer_ArgTypes types,
va_list args,
void* output_buffer,
size_t output_buffer_size);
+
+/// Encodes an `int` with the standard integer encoding: zig-zag + LEB128.
+/// This function is only necessary when manually encoding tokenized messages.
+static inline size_t pw_tokenizer_EncodeInt(int value,
+ void* output,
+ size_t output_size_bytes) {
+ return pw_varint_Encode32(
+ pw_varint_ZigZagEncode32(value), output, output_size_bytes);
+}
+
+/// Encodes an `int64_t` with the standard integer encoding: zig-zag + LEB128.
+/// This function is only necessary when manually encoding tokenized messages.
+static inline size_t pw_tokenizer_EncodeInt64(int64_t value,
+ void* output,
+ size_t output_size_bytes) {
+ return pw_varint_Encode64(
+ pw_varint_ZigZagEncode64(value), output, output_size_bytes);
+}
+
PW_EXTERN_C_END
diff --git a/pw_tokenizer/public/pw_tokenizer/hash.h b/pw_tokenizer/public/pw_tokenizer/hash.h
index 35fb28457..e738abc4c 100644
--- a/pw_tokenizer/public/pw_tokenizer/hash.h
+++ b/pw_tokenizer/public/pw_tokenizer/hash.h
@@ -57,14 +57,14 @@ inline constexpr uint32_t k65599HashConstant = 65599u;
constexpr uint32_t Hash(std::string_view string)
PW_NO_SANITIZE("unsigned-integer-overflow") {
// The length is hashed as if it were the first character.
- uint32_t hash = string.size();
+ uint32_t hash = static_cast<uint32_t>(string.size());
uint32_t coefficient = k65599HashConstant;
// Hash all of the characters in the string as unsigned ints.
// The coefficient calculation is done modulo 0x100000000, so the unsigned
// integer overflows are intentional.
- for (uint8_t ch : string) {
- hash += coefficient * ch;
+ for (char ch : string) {
+ hash += coefficient * static_cast<uint8_t>(ch);
coefficient *= k65599HashConstant;
}
@@ -85,11 +85,11 @@ constexpr uint32_t PwTokenizer65599FixedLengthHash(
std::string_view string,
size_t hash_length = PW_TOKENIZER_CFG_C_HASH_LENGTH)
PW_NO_SANITIZE("unsigned-integer-overflow") {
- uint32_t hash = string.size();
+ uint32_t hash = static_cast<uint32_t>(string.size());
uint32_t coefficient = k65599HashConstant;
- for (uint8_t ch : string.substr(0, hash_length)) {
- hash += coefficient * ch;
+ for (char ch : string.substr(0, hash_length)) {
+ hash += coefficient * static_cast<uint8_t>(ch);
coefficient *= k65599HashConstant;
}
diff --git a/pw_tokenizer/public/pw_tokenizer/internal/decode.h b/pw_tokenizer/public/pw_tokenizer/internal/decode.h
index 8cdf69081..66709eab8 100644
--- a/pw_tokenizer/public/pw_tokenizer/internal/decode.h
+++ b/pw_tokenizer/public/pw_tokenizer/internal/decode.h
@@ -25,6 +25,7 @@
#include <utility>
#include <vector>
+#include "pw_preprocessor/compiler.h"
#include "pw_span/span.h"
// Decoding errors are marked with prefix and suffix so that they stand out from
@@ -240,6 +241,8 @@ class FormatString {
std::vector<StringSegment> segments_;
};
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wformat-nonliteral");
// Implementation of DecodedArg::FromValue template function.
template <typename ArgumentType>
DecodedArg DecodedArg::FromValue(const char* format,
@@ -263,5 +266,6 @@ DecodedArg DecodedArg::FromValue(const char* format,
return arg;
}
+PW_MODIFY_DIAGNOSTICS_POP();
} // namespace pw::tokenizer
diff --git a/pw_tokenizer/public/pw_tokenizer/nested_tokenization.h b/pw_tokenizer/public/pw_tokenizer/nested_tokenization.h
new file mode 100644
index 000000000..6c6fe25e5
--- /dev/null
+++ b/pw_tokenizer/public/pw_tokenizer/nested_tokenization.h
@@ -0,0 +1,25 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This file provides common utilities across all token types.
+#pragma once
+
+#include <inttypes.h>
+
+#include "pw_tokenizer/config.h"
+
+#define PW_TOKENIZER_NESTED_PREFIX PW_TOKENIZER_NESTED_PREFIX_STR[0]
+
+/// Format specifier for a token argument.
+#define PW_TOKEN_FMT() PW_TOKENIZER_NESTED_PREFIX_STR "#%08" PRIx32
diff --git a/pw_tokenizer/public/pw_tokenizer/token_database.h b/pw_tokenizer/public/pw_tokenizer/token_database.h
index 8be56d044..e885339c4 100644
--- a/pw_tokenizer/public/pw_tokenizer/token_database.h
+++ b/pw_tokenizer/public/pw_tokenizer/token_database.h
@@ -20,39 +20,60 @@
namespace pw::tokenizer {
-// Reads entries from a binary token string database. This class does not copy
-// or modify the contents of the database.
-//
-// A binary token database is comprised of a 16-byte header followed by an array
-// of 8-byte entries and a table of null-terminated strings. The header
-// specifies the number of entries. Each entry contains information about a
-// tokenized string: the token and removal date, if any. All fields are
-// little-endian.
-//
-// Header
-// ======
-// Offset Size Field
-// -----------------------------------
-// 0 6 Magic number (TOKENS)
-// 6 2 Version (00 00)
-// 8 4 Entry count
-// 12 4 Reserved
-//
-// Entry
-// =====
-// Offset Size Field
-// -----------------------------------
-// 0 4 Token
-// 4 4 Removal date (d m yy)
-//
-// Entries are sorted by token. A string table with a null-terminated string for
-// each entry in order follows the entries.
-//
-// Entries are accessed by iterating over the database. A O(n) Find function is
-// also provided. In typical use, a TokenDatabase is preprocessed by a
-// Detokenizer into a std::unordered_map.
+/// Reads entries from a v0 binary token string database. This class does not
+/// copy or modify the contents of the database.
+///
+/// The v0 token database has two significant shortcomings:
+///
+/// - Strings cannot contain null terminators (`\0`). If a string contains a
+/// `\0`, the database will not work correctly.
+/// - The domain is not included in entries. All tokens belong to a single
+/// domain, which must be known independently.
+///
+/// A v0 binary token database is comprised of a 16-byte header followed by an
+/// array of 8-byte entries and a table of null-terminated strings. The header
+/// specifies the number of entries. Each entry contains information about a
+/// tokenized string: the token and removal date, if any. All fields are little-
+/// endian.
+///
+/// The token removal date is stored within an unsigned 32-bit integer. It is
+/// stored as `<day> <month> <year>`, where `<day>` and `<month>` are 1 byte
+/// each and `<year>` is two bytes. The fields are set to their maximum value
+/// (`0xFF` or `0xFFFF`) if they are unset. With this format, dates may be
+/// compared naturally as unsigned integers.
+///
+/// @rst
+/// ====== ==== =========================
+/// Header (16 bytes)
+/// ---------------------------------------
+/// Offset Size Field
+/// ====== ==== =========================
+/// 0 6 Magic number (``TOKENS``)
+/// 6 2 Version (``00 00``)
+/// 8 4 Entry count
+/// 12 4 Reserved
+/// ====== ==== =========================
+///
+/// ====== ==== ==================================
+/// Entry (8 bytes)
+/// ------------------------------------------------
+/// Offset Size Field
+/// ====== ==== ==================================
+/// 0 4 Token
+/// 4 1 Removal day (1-31, 255 if unset)
+/// 5 1 Removal month (1-12, 255 if unset)
+/// 6 2 Removal year (65535 if unset)
+/// ====== ==== ==================================
+/// @endrst
+///
+/// Entries are sorted by token. A string table with a null-terminated string
+/// for each entry in order follows the entries.
+///
+/// Entries are accessed by iterating over the database. A O(n) `Find` function
+/// is also provided. In typical use, a `TokenDatabase` is preprocessed by a
+/// `pw::tokenizer::Detokenizer` into a `std::unordered_map`.
class TokenDatabase {
- public:
+ private:
// Internal struct that describes how the underlying binary token database
// stores entries. RawEntries generally should not be used directly. Instead,
// use an Entry, which contains a pointer to the entry's string.
@@ -63,111 +84,142 @@ class TokenDatabase {
static_assert(sizeof(RawEntry) == 8u);
- // An entry in the token database. This struct adds the string to a RawEntry.
+ public:
+ /// An entry in the token database.
struct Entry {
- // The token calculated for this string.
+ /// The token that represents this string.
uint32_t token;
- // The date the token and string was removed from the database, or
- // 0xFFFFFFFF if it was never removed. Dates are encoded such that natural
- // integer sorting sorts from oldest to newest dates. The day is stored an
- // an 8-bit day, 8-bit month, and 16-bit year, packed into a little-endian
- // uint32_t.
+ /// The date the token and string was removed from the database, or
+ /// `0xFFFFFFFF` if it was never removed. Dates are encoded such that
+ /// natural integer sorting sorts from oldest to newest dates. The day is
+ /// stored an an 8-bit day, 8-bit month, and 16-bit year, packed into a
+ /// little-endian `uint32_t`.
uint32_t date_removed;
- // The null-terminated string represented by this token.
+ /// The null-terminated string represented by this token.
const char* string;
};
- // Iterator for TokenDatabase values. Note that this is not a normal STL-style
- // iterator, since * returns a value instead of a reference.
- class Iterator {
+ /// Iterator for `TokenDatabase` values.
+ class iterator {
public:
- constexpr Iterator(const RawEntry* raw_entry, const char* string)
- : raw_(raw_entry), string_(string) {}
+ using difference_type = std::ptrdiff_t;
+ using value_type = Entry;
+ using pointer = const Entry*;
+ using reference = const Entry&;
+ using iterator_category = std::forward_iterator_tag;
- // Constructs a TokenDatabase::Entry for the entry this iterator refers to.
- constexpr Entry entry() const {
- return {raw_->token, raw_->date_removed, string_};
- }
+ constexpr iterator() : entry_{}, raw_(nullptr) {}
- constexpr Iterator& operator++() {
- raw_ += 1;
+ constexpr iterator(const iterator& other) = default;
+ constexpr iterator& operator=(const iterator& other) = default;
+
+ constexpr iterator& operator++() {
+ raw_ += sizeof(RawEntry);
+ ReadRawEntry();
// Move string_ to the character beyond the next null terminator.
- while (*string_++ != '\0') {
+ while (*entry_.string++ != '\0') {
}
return *this;
}
- constexpr Iterator operator++(int) {
- Iterator previous(raw_, string_);
+ constexpr iterator operator++(int) {
+ iterator previous(*this);
operator++();
return previous;
}
- constexpr bool operator==(const Iterator& rhs) const {
+ constexpr bool operator==(const iterator& rhs) const {
return raw_ == rhs.raw_;
}
- constexpr bool operator!=(const Iterator& rhs) const {
+ constexpr bool operator!=(const iterator& rhs) const {
return raw_ != rhs.raw_;
}
- // Derefencing a TokenDatabase::Iterator returns an Entry, not an Entry&.
- constexpr Entry operator*() const { return entry(); }
+ constexpr const Entry& operator*() const { return entry_; }
- // Point directly into the underlying RawEntry. Strings are not accessible
- // via this operator.
- constexpr const RawEntry* operator->() const { return raw_; }
+ constexpr const Entry* operator->() const { return &entry_; }
- constexpr ptrdiff_t operator-(const Iterator& rhs) const {
- return raw_ - rhs.raw_;
+ constexpr difference_type operator-(const iterator& rhs) const {
+ return (raw_ - rhs.raw_) / sizeof(RawEntry);
}
private:
- const RawEntry* raw_;
- const char* string_;
+ friend class TokenDatabase;
+
+ // Constructs a new iterator to a valid entry.
+ constexpr iterator(const char* raw_entry, const char* string)
+ : entry_{0, 0, string}, raw_{raw_entry} {
+ if (raw_entry != string) { // raw_entry == string if the DB is empty
+ ReadRawEntry();
+ }
+ }
+
+ explicit constexpr iterator(const char* end) : entry_{}, raw_(end) {}
+
+ constexpr void ReadRawEntry() {
+ entry_.token = ReadUint32(raw_);
+ entry_.date_removed = ReadUint32(raw_ + sizeof(entry_.token));
+ }
+
+ Entry entry_;
+ const char* raw_;
};
- // A list of token entries returned from a Find operation. This object can be
- // iterated over or indexed as an array.
+ using value_type = Entry;
+ using size_type = std::size_t;
+ using difference_type = std::ptrdiff_t;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using pointer = const value_type*;
+ using const_pointer = const value_type*;
+ using const_iterator = iterator;
+ using reverse_iterator = std::reverse_iterator<iterator>;
+ using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+ /// A list of token entries returned from a `Find` operation. This object can
+ /// be iterated over or indexed as an array.
class Entries {
public:
- constexpr Entries(const Iterator& begin, const Iterator& end)
+ constexpr Entries(const iterator& begin, const iterator& end)
: begin_(begin), end_(end) {}
// The number of entries in this list.
- constexpr size_t size() const { return end_ - begin_; }
+ constexpr size_type size() const { return end_ - begin_; }
// True of the list is empty.
constexpr bool empty() const { return begin_ == end_; }
- // Accesses the specified entry in this set. Returns an Entry object, which
- // is constructed from the underlying raw entry. The index must be less than
+ // Accesses the specified entry in this set. The index must be less than
// size(). This operation is O(n) in size().
- Entry operator[](size_t index) const;
+ Entry operator[](size_type index) const;
- constexpr const Iterator& begin() const { return begin_; }
- constexpr const Iterator& end() const { return end_; }
+ constexpr const iterator& begin() const { return begin_; }
+ constexpr const iterator& end() const { return end_; }
private:
- Iterator begin_;
- Iterator end_;
+ iterator begin_;
+ iterator end_;
};
- // Returns true if the provided data is a valid token database. This checks
- // the magic number ("TOKENS"), version (which must be 0), and that there is
- // is one string for each entry in the database. A database with extra strings
- // or other trailing data is considered valid.
+ /// Returns true if the provided data is a valid token database. This checks
+ /// the magic number (`TOKENS`), version (which must be `0`), and that there
+ /// is is one string for each entry in the database. A database with extra
+ /// strings or other trailing data is considered valid.
template <typename ByteArray>
static constexpr bool IsValid(const ByteArray& bytes) {
return HasValidHeader(bytes) && EachEntryHasAString(bytes);
}
- // Creates a TokenDatabase and checks if the provided data is valid at compile
- // time. Accepts references to constexpr containers (array, span, string_view,
- // etc.) with static storage duration. For example:
- //
- // constexpr char kMyData[] = ...;
- // constexpr TokenDatabase db = TokenDatabase::Create<kMyData>();
- //
+ /// Creates a `TokenDatabase` and checks if the provided data is valid at
+ /// compile time. Accepts references to constexpr containers (`array`, `span`,
+ /// `string_view`, etc.) with static storage duration. For example:
+ ///
+ /// @code{.cpp}
+ ///
+ /// constexpr char kMyData[] = ...;
+ /// constexpr TokenDatabase db = TokenDatabase::Create<kMyData>();
+ ///
+ /// @endcode
template <const auto& kDatabaseBytes>
static constexpr TokenDatabase Create() {
static_assert(
@@ -180,35 +232,39 @@ class TokenDatabase {
return TokenDatabase(std::data(kDatabaseBytes));
}
- // Creates a TokenDatabase from the provided byte array. The array may be a
- // span, array, or other container type. If the data is not valid, returns a
- // default-constructed database for which ok() is false.
- //
- // Prefer the Create overload that takes the data as a template parameter
- // whenever possible, since that function checks the integrity of the data at
- // compile time.
+ /// Creates a `TokenDatabase` from the provided byte array. The array may be a
+ /// span, array, or other container type. If the data is not valid, returns a
+ /// default-constructed database for which ok() is false.
+ ///
+ /// Prefer the `Create` overload that takes the data as a template parameter
+ /// when possible, since that overload verifies data integrity at compile
+ /// time.
template <typename ByteArray>
static constexpr TokenDatabase Create(const ByteArray& database_bytes) {
return IsValid<ByteArray>(database_bytes)
? TokenDatabase(std::data(database_bytes))
: TokenDatabase(); // Invalid database.
}
- // Creates a database with no data. ok() returns false.
+ /// Creates a database with no data. `ok()` returns false.
constexpr TokenDatabase() : begin_{.data = nullptr}, end_{.data = nullptr} {}
- // Returns all entries associated with this token. This is a O(n) operation.
+ /// Returns all entries associated with this token. This is `O(n)`.
Entries Find(uint32_t token) const;
- // Returns the total number of entries (unique token-string pairs).
- constexpr size_t size() const {
+ /// Returns the total number of entries (unique token-string pairs).
+ constexpr size_type size() const {
return (end_.data - begin_.data) / sizeof(RawEntry);
}
- // True if this database was constructed with valid data.
+ /// True if this database was constructed with valid data. The database might
+ /// be empty, but it has an intact header and a string for each entry.
constexpr bool ok() const { return begin_.data != nullptr; }
- Iterator begin() const { return Iterator(begin_.entry, end_.data); }
- Iterator end() const { return Iterator(end_.entry, nullptr); }
+ /// Returns an iterator for the first token entry.
+ constexpr iterator begin() const { return iterator(begin_.data, end_.data); }
+
+ /// Returns an iterator for one past the last token entry.
+ constexpr iterator end() const { return iterator(end_.data); }
private:
struct Header {
@@ -229,7 +285,7 @@ class TokenDatabase {
}
// Check the magic number and version.
- for (size_t i = 0; i < kMagicAndVersion.size(); ++i) {
+ for (size_type i = 0; i < kMagicAndVersion.size(); ++i) {
if (bytes[i] != kMagicAndVersion[i]) {
return false;
}
@@ -240,7 +296,7 @@ class TokenDatabase {
template <typename ByteArray>
static constexpr bool EachEntryHasAString(const ByteArray& bytes) {
- const size_t entries = ReadEntryCount(std::data(bytes));
+ const size_type entries = ReadEntryCount(std::data(bytes));
// Check that the data is large enough to have a string table.
if (std::size(bytes) < StringTable(entries)) {
@@ -248,7 +304,7 @@ class TokenDatabase {
}
// Count the strings in the string table.
- size_t string_count = 0;
+ size_type string_count = 0;
for (auto i = std::begin(bytes) + StringTable(entries); i < std::end(bytes);
++i) {
string_count += (*i == '\0') ? 1 : 0;
@@ -263,6 +319,11 @@ class TokenDatabase {
template <typename T>
static constexpr uint32_t ReadEntryCount(const T* header_bytes) {
const T* bytes = header_bytes + offsetof(Header, entry_count);
+ return ReadUint32(bytes);
+ }
+
+ template <typename T>
+ static constexpr uint32_t ReadUint32(const T* bytes) {
return static_cast<uint8_t>(bytes[0]) |
static_cast<uint8_t>(bytes[1]) << 8 |
static_cast<uint8_t>(bytes[2]) << 16 |
@@ -270,7 +331,7 @@ class TokenDatabase {
}
// Calculates the offset of the string table.
- static constexpr size_t StringTable(size_t entries) {
+ static constexpr size_type StringTable(size_type entries) {
return sizeof(Header) + entries * sizeof(RawEntry);
}
@@ -302,7 +363,6 @@ class TokenDatabase {
// Store the beginning and end pointers as a union to avoid breaking constexpr
// rules for reinterpret_cast.
union {
- const RawEntry* entry;
const char* data;
const unsigned char* unsigned_data;
const signed char* signed_data;
diff --git a/pw_tokenizer/public/pw_tokenizer/tokenize.h b/pw_tokenizer/public/pw_tokenizer/tokenize.h
index 6b8e62c57..aa8efd888 100644
--- a/pw_tokenizer/public/pw_tokenizer/tokenize.h
+++ b/pw_tokenizer/public/pw_tokenizer/tokenize.h
@@ -75,7 +75,7 @@ typedef uint32_t pw_tokenizer_Token;
}()
/// Tokenizes a string literal in a standalone statement using the specified
-/// @rstref{domain <module-pw_tokenizer-domains>}. C and C++ compatible.
+/// @rstref{domain<module-pw_tokenizer-domains>}. C and C++ compatible.
#define PW_TOKENIZE_STRING_DOMAIN(domain, string_literal) \
PW_TOKENIZE_STRING_MASK(domain, UINT32_MAX, string_literal)
@@ -111,7 +111,7 @@ typedef uint32_t pw_tokenizer_Token;
}()
#define _PW_TOKENIZER_MASK_TOKEN(mask, string_literal) \
- ((pw_tokenizer_Token)(mask)&PW_TOKENIZER_STRING_TOKEN(string_literal))
+ ((pw_tokenizer_Token)(mask) & PW_TOKENIZER_STRING_TOKEN(string_literal))
/// Encodes a tokenized string and arguments to the provided buffer. The size of
/// the buffer is passed via a pointer to a `size_t`. After encoding is
@@ -159,23 +159,56 @@ typedef uint32_t pw_tokenizer_Token;
/// Same as @c_macro{PW_TOKENIZE_TO_BUFFER_DOMAIN}, but applies a
/// @rstref{bit mask <module-pw_tokenizer-masks>} to the token.
-#define PW_TOKENIZE_TO_BUFFER_MASK( \
- domain, mask, buffer, buffer_size_pointer, format, ...) \
- do { \
- PW_TOKENIZE_FORMAT_STRING(domain, mask, format, __VA_ARGS__); \
- _pw_tokenizer_ToBuffer(buffer, \
- buffer_size_pointer, \
- _pw_tokenizer_token, \
- PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
- PW_COMMA_ARGS(__VA_ARGS__)); \
+#define PW_TOKENIZE_TO_BUFFER_MASK( \
+ domain, mask, buffer, buffer_size_pointer, format, ...) \
+ do { \
+ PW_TOKENIZE_FORMAT_STRING(domain, mask, format, __VA_ARGS__); \
+ _pw_tokenizer_ToBuffer(buffer, \
+ buffer_size_pointer, \
+ PW_TOKENIZER_REPLACE_FORMAT_STRING(__VA_ARGS__)); \
} while (0)
+/// @brief Low-level macro for calling functions that handle tokenized strings.
+///
+/// Functions that work with tokenized format strings must take the following
+/// arguments:
+///
+/// - The 32-bit token (@cpp_type{pw_tokenizer_Token})
+/// - The 32- or 64-bit argument types (@cpp_type{pw_tokenizer_ArgTypes})
+/// - Variadic arguments, if any
+///
+/// This macro expands to those arguments. Custom tokenization macros should use
+/// this macro to pass these arguments to a function or other macro.
+///
+/** @code{cpp}
+ * EncodeMyTokenizedString(uint32_t token,
+ * pw_tokenier_ArgTypes arg_types,
+ * ...);
+ *
+ * #define CUSTOM_TOKENIZATION_MACRO(format, ...) \
+ * PW_TOKENIZE_FORMAT_STRING(domain, mask, format, __VA_ARGS__); \
+ * EncodeMyTokenizedString(PW_TOKENIZER_REPLACE_FORMAT_STRING(__VA_ARGS__))
+ * @endcode
+ */
+#define PW_TOKENIZER_REPLACE_FORMAT_STRING(...) \
+ _PW_TOKENIZER_REPLACE_FORMAT_STRING(PW_EMPTY_ARGS(__VA_ARGS__), __VA_ARGS__)
+
+#define _PW_TOKENIZER_REPLACE_FORMAT_STRING(empty_args, ...) \
+ _PW_CONCAT_2(_PW_TOKENIZER_REPLACE_FORMAT_STRING_, empty_args)(__VA_ARGS__)
+
+#define _PW_TOKENIZER_REPLACE_FORMAT_STRING_1() _pw_tokenizer_token, 0u
+#define _PW_TOKENIZER_REPLACE_FORMAT_STRING_0(...) \
+ _pw_tokenizer_token, PW_TOKENIZER_ARG_TYPES(__VA_ARGS__), __VA_ARGS__
+
/// Converts a series of arguments to a compact format that replaces the format
/// string literal. Evaluates to a `pw_tokenizer_ArgTypes` value.
///
/// Depending on the size of `pw_tokenizer_ArgTypes`, the bottom 4 or 6 bits
/// store the number of arguments and the remaining bits store the types, two
/// bits per type. The arguments are not evaluated; only their types are used.
+///
+/// In general, @c_macro{PW_TOKENIZER_ARG_TYPES} should not be used directly.
+/// Instead, use @c_macro{PW_TOKENIZER_REPLACE_FORMAT_STRING}.
#define PW_TOKENIZER_ARG_TYPES(...) \
PW_DELEGATE_BY_ARG_COUNT(_PW_TOKENIZER_TYPES_, __VA_ARGS__)
@@ -204,25 +237,40 @@ PW_EXTERN_C_END
/// since the same variable is used in every invocation.
///
/// The tokenized string uses the specified @rstref{tokenization domain
-/// <module-pw_tokenizer-domains>}. Use `PW_TOKENIZER_DEFAULT_DOMAIN` for the
+/// <module-pw_tokenizer-domains>}. Use `PW_TOKENIZER_DEFAULT_DOMAIN` for the
/// default. The token also may be masked; use `UINT32_MAX` to keep all bits.
///
-/// This macro checks that the printf-style format string matches the arguments,
-/// stores the format string in a special section, and calculates the string's
-/// token at compile time.
+/// This macro checks that the printf-style format string matches the arguments
+/// and that no more than @c_macro{PW_TOKENIZER_MAX_SUPPORTED_ARGS} are
+/// provided. It then stores the format string in a special section, and
+/// calculates the string's token at compile time.
// clang-format off
-#define PW_TOKENIZE_FORMAT_STRING(domain, mask, format, ...) \
- if (0) { /* Do not execute to prevent double evaluation of the arguments. */ \
- pw_tokenizer_CheckFormatString(format PW_COMMA_ARGS(__VA_ARGS__)); \
- } \
- \
- /* Check that the macro is invoked with a supported number of arguments. */ \
+#define PW_TOKENIZE_FORMAT_STRING(domain, mask, format, ...) \
static_assert( \
PW_FUNCTION_ARG_COUNT(__VA_ARGS__) <= PW_TOKENIZER_MAX_SUPPORTED_ARGS, \
"Tokenized strings cannot have more than " \
PW_STRINGIFY(PW_TOKENIZER_MAX_SUPPORTED_ARGS) " arguments; " \
PW_STRINGIFY(PW_FUNCTION_ARG_COUNT(__VA_ARGS__)) \
" arguments were used for " #format " (" #__VA_ARGS__ ")"); \
+ PW_TOKENIZE_FORMAT_STRING_ANY_ARG_COUNT(domain, mask, format, __VA_ARGS__)
+// clang-format on
+
+/// Equivalent to `PW_TOKENIZE_FORMAT_STRING`, but supports any number of
+/// arguments.
+///
+/// This is a low-level macro that should rarely be used directly. It is
+/// intended for situations when @cpp_type{pw_tokenizer_ArgTypes} is not used.
+/// There are two situations where @cpp_type{pw_tokenizer_ArgTypes} is
+/// unnecessary:
+///
+/// - The exact format string argument types and count are fixed.
+/// - The format string supports a variable number of arguments of only one
+/// type. In this case, @c_macro{PW_FUNCTION_ARG_COUNT} may be used to pass
+/// the argument count to the function.
+#define PW_TOKENIZE_FORMAT_STRING_ANY_ARG_COUNT(domain, mask, format, ...) \
+ if (0) { /* Do not execute to prevent double evaluation of the arguments. */ \
+ pw_tokenizer_CheckFormatString(format PW_COMMA_ARGS(__VA_ARGS__)); \
+ } \
\
/* Tokenize the string to a pw_tokenizer_Token at compile time. */ \
static _PW_TOKENIZER_CONST pw_tokenizer_Token _pw_tokenizer_token = \
@@ -230,8 +278,6 @@ PW_EXTERN_C_END
\
_PW_TOKENIZER_RECORD_ORIGINAL_STRING(_pw_tokenizer_token, domain, format)
-// clang-format on
-
// Creates unique names to use for tokenized string entries and linker sections.
#define _PW_TOKENIZER_UNIQUE(prefix) PW_CONCAT(prefix, __LINE__, _, __COUNTER__)
@@ -255,9 +301,10 @@ using Token = ::pw_tokenizer_Token;
#else
#define _PW_TOKENIZER_CONST const
+#define _PW_ALIGNAS(alignment) __attribute__((aligned(alignment)))
#define _PW_TOKENIZER_RECORD_ORIGINAL_STRING(token, domain, string) \
- _Alignas(1) static const _PW_TOKENIZER_STRING_ENTRY(token, domain, string)
+ _PW_ALIGNAS(1) static const _PW_TOKENIZER_STRING_ENTRY(token, domain, string)
#endif // __cplusplus
diff --git a/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h b/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h
deleted file mode 100644
index 175f71e5c..000000000
--- a/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-#pragma once
-
-#include <stddef.h>
-#include <stdint.h>
-
-#include "pw_preprocessor/util.h"
-
-PW_EXTERN_C_START
-
-// This typedef is deprecated. Use uint32_t instead.
-typedef uint32_t pw_tokenizer_Payload;
-
-// This function is deprecated. For use with pw_log_tokenized, call
-// pw_log_tokenized_HandleLog. For other uses, implement a pw_tokenizer macro
-// that calls a custom handler.
-void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload payload,
- const uint8_t encoded_message[],
- size_t size_bytes);
-
-PW_EXTERN_C_END
diff --git a/pw_tokenizer/pw_tokenizer_linker_rules.ld b/pw_tokenizer/pw_tokenizer_linker_rules.ld
deleted file mode 100644
index d1f0aa796..000000000
--- a/pw_tokenizer/pw_tokenizer_linker_rules.ld
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2020 The Pigweed Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-/*
- * This file is separate from pw_tokenizer_linker_sections.ld because Zephyr
- * already defines the top level SECTIONS label and requires new linker
- * scripts to only add the individual sections.
- */
-
-/*
- * This section stores metadata that may be used during tokenized string
- * decoding. This metadata describes properties that may affect how the
- * tokenized string is encoded or decoded -- the maximum length of the hash
- * function and the sizes of certain integer types.
- *
- * Metadata is declared as key-value pairs. See the metadata variable in
- * tokenize.cc for further details.
- */
-.pw_tokenizer.info 0x0 (INFO) :
-{
- KEEP(*(.pw_tokenizer.info))
-}
-
-/*
- * Tokenized string entries are stored in this section. Each entry contains
- * the original string literal and the calculated token that represents it. In
- * the compiled code, the token and a compact argument list encoded in a
- * uint32_t are used in place of the format string. The compiled code
- * contains no references to the tokenized string entries in this section.
- *
- * The tokenized string entry format is specified by the
- * pw::tokenizer::internal::Entry class in
- * pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
- *
- * The section contents are declared with KEEP so that they are not removed
- * from the ELF. These are never emitted in the final binary or loaded into
- * memory.
- */
-.pw_tokenizer.entries 0x0 (INFO) :
-{
- KEEP(*(.pw_tokenizer.entries.*))
-}
diff --git a/pw_tokenizer/pw_tokenizer_linker_sections.ld b/pw_tokenizer/pw_tokenizer_linker_sections.ld
index a48a804cc..ae17f47cd 100644
--- a/pw_tokenizer/pw_tokenizer_linker_sections.ld
+++ b/pw_tokenizer/pw_tokenizer_linker_sections.ld
@@ -34,5 +34,37 @@
SECTIONS
{
- INCLUDE pw_tokenizer_linker_rules.ld
+ /*
+ * This section stores metadata that may be used during tokenized string
+ * decoding. This metadata describes properties that may affect how the
+ * tokenized string is encoded or decoded -- the maximum length of the hash
+ * function and the sizes of certain integer types.
+ *
+ * Metadata is declared as key-value pairs. See the metadata variable in
+ * tokenize.cc for further details.
+ */
+ .pw_tokenizer.info 0x0 (INFO) :
+ {
+ KEEP(*(.pw_tokenizer.info))
+ }
+
+ /*
+ * Tokenized string entries are stored in this section. Each entry contains
+ * the original string literal and the calculated token that represents it. In
+ * the compiled code, the token and a compact argument list encoded in a
+ * uint32_t are used in place of the format string. The compiled code
+ * contains no references to the tokenized string entries in this section.
+ *
+ * The tokenized string entry format is specified by the
+ * pw::tokenizer::internal::Entry class in
+ * pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
+ *
+ * The section contents are declared with KEEP so that they are not removed
+ * from the ELF. These are never emitted in the final binary or loaded into
+ * memory.
+ */
+ .pw_tokenizer.entries 0x0 (INFO) :
+ {
+ KEEP(*(.pw_tokenizer.entries.*))
+ }
}
diff --git a/pw_tokenizer/pw_tokenizer_private/generated_hash_test_cases.h b/pw_tokenizer/pw_tokenizer_private/generated_hash_test_cases.h
index 4f7f779da..21b7a530e 100644
--- a/pw_tokenizer/pw_tokenizer_private/generated_hash_test_cases.h
+++ b/pw_tokenizer/pw_tokenizer_private/generated_hash_test_cases.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -37,865 +37,865 @@ inline constexpr struct {
} kHashTests[] = {
{
- std::string_view(),
+ std::string_view("", 0u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(0), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH(""), // macro-calculated hash
},
{
- std::string_view(),
+ std::string_view("", 0u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(0), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH(""), // macro-calculated hash
},
{
- std::string_view(),
+ std::string_view("", 0u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(0), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH(""), // macro-calculated hash
},
{
- std::string_view("\xa1", 1u),
+ std::string_view("\xa1", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(10561440), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xa1"), // macro-calculated hash
},
{
- std::string_view("\xa1", 1u),
+ std::string_view("\xa1", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(10561440), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xa1"), // macro-calculated hash
},
{
- std::string_view("\xa1", 1u),
+ std::string_view("\xa1", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(10561440), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xa1"), // macro-calculated hash
},
{
- std::string_view("\xff", 1u),
+ std::string_view("\xff", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(16727746), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xff"), // macro-calculated hash
},
{
- std::string_view("\xff", 1u),
+ std::string_view("\xff", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(16727746), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xff"), // macro-calculated hash
},
{
- std::string_view("\xff", 1u),
+ std::string_view("\xff", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(16727746), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xff"), // macro-calculated hash
},
{
- std::string_view("\x00", 1u),
+ std::string_view("\x00", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x00"), // macro-calculated hash
},
{
- std::string_view("\x00", 1u),
+ std::string_view("\x00", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x00"), // macro-calculated hash
},
{
- std::string_view("\x00", 1u),
+ std::string_view("\x00", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x00"), // macro-calculated hash
},
{
- std::string_view("\x00\x00", 2u),
+ std::string_view("\x00\x00", 2u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x00\x00"), // macro-calculated hash
},
{
- std::string_view("\x00\x00", 2u),
+ std::string_view("\x00\x00", 2u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x00\x00"), // macro-calculated hash
},
{
- std::string_view("\x00\x00", 2u),
+ std::string_view("\x00\x00", 2u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x00\x00"), // macro-calculated hash
},
{
- std::string_view("a", 1u),
+ std::string_view("a", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(6363104), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("a"), // macro-calculated hash
},
{
- std::string_view("a", 1u),
+ std::string_view("a", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(6363104), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("a"), // macro-calculated hash
},
{
- std::string_view("a", 1u),
+ std::string_view("a", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(6363104), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("a"), // macro-calculated hash
},
{
- std::string_view("A", 1u),
+ std::string_view("A", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4263936), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("A"), // macro-calculated hash
},
{
- std::string_view("A", 1u),
+ std::string_view("A", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4263936), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("A"), // macro-calculated hash
},
{
- std::string_view("A", 1u),
+ std::string_view("A", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(4263936), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("A"), // macro-calculated hash
},
{
- std::string_view("hello, \"world\"", 14u),
+ std::string_view("hello, \"world\"", 14u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(3537412730), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("hello, \"world\""), // macro-calculated hash
},
{
- std::string_view("hello, \"world\"", 14u),
+ std::string_view("hello, \"world\"", 14u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(3537412730), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("hello, \"world\""), // macro-calculated hash
},
{
- std::string_view("hello, \"world\"", 14u),
+ std::string_view("hello, \"world\"", 14u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(3537412730), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("hello, \"world\""), // macro-calculated hash
},
{
- std::string_view("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO", 200u),
+ std::string_view("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO", 200u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2035157304), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO"), // macro-calculated hash
},
{
- std::string_view("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO", 200u),
+ std::string_view("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO", 200u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4222077672), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO"), // macro-calculated hash
},
{
- std::string_view("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO", 200u),
+ std::string_view("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO", 200u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(255790664), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO"), // macro-calculated hash
},
{
- std::string_view("4", 1u),
+ std::string_view("4", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(3411149), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("4"), // macro-calculated hash
},
{
- std::string_view("4", 1u),
+ std::string_view("4", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(3411149), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("4"), // macro-calculated hash
},
{
- std::string_view("4", 1u),
+ std::string_view("4", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(3411149), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("4"), // macro-calculated hash
},
{
- std::string_view("\xe0", 1u),
+ std::string_view("\xe0", 1u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(14694177), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xe0"), // macro-calculated hash
},
{
- std::string_view("\xe0", 1u),
+ std::string_view("\xe0", 1u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(14694177), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xe0"), // macro-calculated hash
},
{
- std::string_view("\xe0", 1u),
+ std::string_view("\xe0", 1u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(14694177), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xe0"), // macro-calculated hash
},
{
- std::string_view("\x90\xb9", 2u),
+ std::string_view("\x90\xb9", 2u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1537824683), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x90\xb9"), // macro-calculated hash
},
{
- std::string_view("\x90\xb9", 2u),
+ std::string_view("\x90\xb9", 2u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1537824683), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x90\xb9"), // macro-calculated hash
},
{
- std::string_view("\x90\xb9", 2u),
+ std::string_view("\x90\xb9", 2u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1537824683), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x90\xb9"), // macro-calculated hash
},
{
- std::string_view("\x6a\xe7", 2u),
+ std::string_view("\x6a\xe7", 2u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1915361151), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x6a\xe7"), // macro-calculated hash
},
{
- std::string_view("\x6a\xe7", 2u),
+ std::string_view("\x6a\xe7", 2u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1915361151), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x6a\xe7"), // macro-calculated hash
},
{
- std::string_view("\x6a\xe7", 2u),
+ std::string_view("\x6a\xe7", 2u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1915361151), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x6a\xe7"), // macro-calculated hash
},
{
- std::string_view("dy0", 3u),
+ std::string_view("dy0", 3u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4114649192), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("dy0"), // macro-calculated hash
},
{
- std::string_view("dy0", 3u),
+ std::string_view("dy0", 3u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4114649192), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("dy0"), // macro-calculated hash
},
{
- std::string_view("dy0", 3u),
+ std::string_view("dy0", 3u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(4114649192), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("dy0"), // macro-calculated hash
},
{
- std::string_view("\xc4\x18\x32", 3u),
+ std::string_view("\xc4\x18\x32", 3u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(585787813), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xc4\x18\x32"), // macro-calculated hash
},
{
- std::string_view("\xc4\x18\x32", 3u),
+ std::string_view("\xc4\x18\x32", 3u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(585787813), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xc4\x18\x32"), // macro-calculated hash
},
{
- std::string_view("\xc4\x18\x32", 3u),
+ std::string_view("\xc4\x18\x32", 3u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(585787813), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xc4\x18\x32"), // macro-calculated hash
},
{
- std::string_view("\x1c\xfc\x28\x2b", 4u),
+ std::string_view("\x1c\xfc\x28\x2b", 4u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(704109799), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x1c\xfc\x28\x2b"), // macro-calculated hash
},
{
- std::string_view("\x1c\xfc\x28\x2b", 4u),
+ std::string_view("\x1c\xfc\x28\x2b", 4u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(704109799), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x1c\xfc\x28\x2b"), // macro-calculated hash
},
{
- std::string_view("\x1c\xfc\x28\x2b", 4u),
+ std::string_view("\x1c\xfc\x28\x2b", 4u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(704109799), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x1c\xfc\x28\x2b"), // macro-calculated hash
},
{
- std::string_view("\xab\x96\x56\x70", 4u),
+ std::string_view("\xab\x96\x56\x70", 4u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2738614345), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xab\x96\x56\x70"), // macro-calculated hash
},
{
- std::string_view("\xab\x96\x56\x70", 4u),
+ std::string_view("\xab\x96\x56\x70", 4u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2738614345), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xab\x96\x56\x70"), // macro-calculated hash
},
{
- std::string_view("\xab\x96\x56\x70", 4u),
+ std::string_view("\xab\x96\x56\x70", 4u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2738614345), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xab\x96\x56\x70"), // macro-calculated hash
},
{
- std::string_view("\x18\x1e\x6e\x6a\x73", 5u),
+ std::string_view("\x18\x1e\x6e\x6a\x73", 5u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(580554452), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x18\x1e\x6e\x6a\x73"), // macro-calculated hash
},
{
- std::string_view("\x18\x1e\x6e\x6a\x73", 5u),
+ std::string_view("\x18\x1e\x6e\x6a\x73", 5u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(580554452), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x18\x1e\x6e\x6a\x73"), // macro-calculated hash
},
{
- std::string_view("\x18\x1e\x6e\x6a\x73", 5u),
+ std::string_view("\x18\x1e\x6e\x6a\x73", 5u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(580554452), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x18\x1e\x6e\x6a\x73"), // macro-calculated hash
},
{
- std::string_view("\xde\xe5\xdf\x22\x00", 5u),
+ std::string_view("\xde\xe5\xdf\x22\x00", 5u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4269181327), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xde\xe5\xdf\x22\x00"), // macro-calculated hash
},
{
- std::string_view("\xde\xe5\xdf\x22\x00", 5u),
+ std::string_view("\xde\xe5\xdf\x22\x00", 5u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4269181327), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xde\xe5\xdf\x22\x00"), // macro-calculated hash
},
{
- std::string_view("\xde\xe5\xdf\x22\x00", 5u),
+ std::string_view("\xde\xe5\xdf\x22\x00", 5u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(4269181327), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xde\xe5\xdf\x22\x00"), // macro-calculated hash
},
{
- std::string_view("\x59\xac\x64\x3b\xc7\x36", 6u),
+ std::string_view("\x59\xac\x64\x3b\xc7\x36", 6u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2461849503), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x59\xac\x64\x3b\xc7\x36"), // macro-calculated hash
},
{
- std::string_view("\x59\xac\x64\x3b\xc7\x36", 6u),
+ std::string_view("\x59\xac\x64\x3b\xc7\x36", 6u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2461849503), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x59\xac\x64\x3b\xc7\x36"), // macro-calculated hash
},
{
- std::string_view("\x59\xac\x64\x3b\xc7\x36", 6u),
+ std::string_view("\x59\xac\x64\x3b\xc7\x36", 6u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2461849503), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x59\xac\x64\x3b\xc7\x36"), // macro-calculated hash
},
{
- std::string_view("\xe1\xef\x87\x8d\xbc\xd7", 6u),
+ std::string_view("\xe1\xef\x87\x8d\xbc\xd7", 6u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2407518645), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xe1\xef\x87\x8d\xbc\xd7"), // macro-calculated hash
},
{
- std::string_view("\xe1\xef\x87\x8d\xbc\xd7", 6u),
+ std::string_view("\xe1\xef\x87\x8d\xbc\xd7", 6u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2407518645), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xe1\xef\x87\x8d\xbc\xd7"), // macro-calculated hash
},
{
- std::string_view("\xe1\xef\x87\x8d\xbc\xd7", 6u),
+ std::string_view("\xe1\xef\x87\x8d\xbc\xd7", 6u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2407518645), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xe1\xef\x87\x8d\xbc\xd7"), // macro-calculated hash
},
{
- std::string_view("\x34\xd8\x3a\xbb\xf1\x0e\x07", 7u),
+ std::string_view("\x34\xd8\x3a\xbb\xf1\x0e\x07", 7u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2657240642), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x34\xd8\x3a\xbb\xf1\x0e\x07"), // macro-calculated hash
},
{
- std::string_view("\x34\xd8\x3a\xbb\xf1\x0e\x07", 7u),
+ std::string_view("\x34\xd8\x3a\xbb\xf1\x0e\x07", 7u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2657240642), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x34\xd8\x3a\xbb\xf1\x0e\x07"), // macro-calculated hash
},
{
- std::string_view("\x34\xd8\x3a\xbb\xf1\x0e\x07", 7u),
+ std::string_view("\x34\xd8\x3a\xbb\xf1\x0e\x07", 7u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2657240642), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x34\xd8\x3a\xbb\xf1\x0e\x07"), // macro-calculated hash
},
{
- std::string_view("\xa2\x8e\xb6\x56\x83\xd2\x89", 7u),
+ std::string_view("\xa2\x8e\xb6\x56\x83\xd2\x89", 7u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2016713689), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xa2\x8e\xb6\x56\x83\xd2\x89"), // macro-calculated hash
},
{
- std::string_view("\xa2\x8e\xb6\x56\x83\xd2\x89", 7u),
+ std::string_view("\xa2\x8e\xb6\x56\x83\xd2\x89", 7u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2016713689), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xa2\x8e\xb6\x56\x83\xd2\x89"), // macro-calculated hash
},
{
- std::string_view("\xa2\x8e\xb6\x56\x83\xd2\x89", 7u),
+ std::string_view("\xa2\x8e\xb6\x56\x83\xd2\x89", 7u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2016713689), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xa2\x8e\xb6\x56\x83\xd2\x89"), // macro-calculated hash
},
{
- std::string_view("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f", 8u),
+ std::string_view("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f", 8u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(727179216), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f"), // macro-calculated hash
},
{
- std::string_view("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f", 8u),
+ std::string_view("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f", 8u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(727179216), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f"), // macro-calculated hash
},
{
- std::string_view("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f", 8u),
+ std::string_view("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f", 8u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(727179216), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x20\x3b\x66\x3f\x80\x8b\xd6\x9f"), // macro-calculated hash
},
{
- std::string_view("\xe5\x15\xbf\x96\x52\xd8\x22\x72", 8u),
+ std::string_view("\xe5\x15\xbf\x96\x52\xd8\x22\x72", 8u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(110264805), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xe5\x15\xbf\x96\x52\xd8\x22\x72"), // macro-calculated hash
},
{
- std::string_view("\xe5\x15\xbf\x96\x52\xd8\x22\x72", 8u),
+ std::string_view("\xe5\x15\xbf\x96\x52\xd8\x22\x72", 8u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(110264805), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xe5\x15\xbf\x96\x52\xd8\x22\x72"), // macro-calculated hash
},
{
- std::string_view("\xe5\x15\xbf\x96\x52\xd8\x22\x72", 8u),
+ std::string_view("\xe5\x15\xbf\x96\x52\xd8\x22\x72", 8u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(110264805), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xe5\x15\xbf\x96\x52\xd8\x22\x72"), // macro-calculated hash
},
{
- std::string_view("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82", 9u),
+ std::string_view("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82", 9u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(261914122), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82"), // macro-calculated hash
},
{
- std::string_view("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82", 9u),
+ std::string_view("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82", 9u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(261914122), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82"), // macro-calculated hash
},
{
- std::string_view("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82", 9u),
+ std::string_view("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82", 9u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(261914122), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82"), // macro-calculated hash
},
{
- std::string_view("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40", 9u),
+ std::string_view("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40", 9u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1833718768), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40"), // macro-calculated hash
},
{
- std::string_view("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40", 9u),
+ std::string_view("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40", 9u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1833718768), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40"), // macro-calculated hash
},
{
- std::string_view("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40", 9u),
+ std::string_view("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40", 9u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1833718768), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40"), // macro-calculated hash
},
{
- std::string_view("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d", 10u),
+ std::string_view("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d", 10u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2326646568), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d"), // macro-calculated hash
},
{
- std::string_view("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d", 10u),
+ std::string_view("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d", 10u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2326646568), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d"), // macro-calculated hash
},
{
- std::string_view("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d", 10u),
+ std::string_view("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d", 10u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2326646568), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d"), // macro-calculated hash
},
{
- std::string_view("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5", 10u),
+ std::string_view("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5", 10u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2712532084), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5"), // macro-calculated hash
},
{
- std::string_view("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5", 10u),
+ std::string_view("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5", 10u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2712532084), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5"), // macro-calculated hash
},
{
- std::string_view("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5", 10u),
+ std::string_view("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5", 10u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2712532084), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5"), // macro-calculated hash
},
{
- std::string_view("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24", 11u),
+ std::string_view("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24", 11u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(544632964), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24"), // macro-calculated hash
},
{
- std::string_view("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24", 11u),
+ std::string_view("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24", 11u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(544632964), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24"), // macro-calculated hash
},
{
- std::string_view("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24", 11u),
+ std::string_view("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24", 11u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(544632964), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24"), // macro-calculated hash
},
{
- std::string_view("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00", 11u),
+ std::string_view("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00", 11u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(3878380686), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00"), // macro-calculated hash
},
{
- std::string_view("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00", 11u),
+ std::string_view("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00", 11u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(3878380686), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00"), // macro-calculated hash
},
{
- std::string_view("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00", 11u),
+ std::string_view("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00", 11u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(3878380686), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00"), // macro-calculated hash
},
{
- std::string_view("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97", 12u),
+ std::string_view("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97", 12u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4053891765), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97"), // macro-calculated hash
},
{
- std::string_view("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97", 12u),
+ std::string_view("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97", 12u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4053891765), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97"), // macro-calculated hash
},
{
- std::string_view("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97", 12u),
+ std::string_view("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97", 12u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(4053891765), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97"), // macro-calculated hash
},
{
- std::string_view("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59", 12u),
+ std::string_view("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59", 12u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2009683698), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59"), // macro-calculated hash
},
{
- std::string_view("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59", 12u),
+ std::string_view("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59", 12u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2009683698), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59"), // macro-calculated hash
},
{
- std::string_view("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59", 12u),
+ std::string_view("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59", 12u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2009683698), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59"), // macro-calculated hash
},
{
- std::string_view("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26", 13u),
+ std::string_view("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26", 13u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(3862326851), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26"), // macro-calculated hash
},
{
- std::string_view("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26", 13u),
+ std::string_view("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26", 13u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(3862326851), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26"), // macro-calculated hash
},
{
- std::string_view("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26", 13u),
+ std::string_view("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26", 13u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(3862326851), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26"), // macro-calculated hash
},
{
- std::string_view("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e", 13u),
+ std::string_view("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e", 13u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2358079886), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e"), // macro-calculated hash
},
{
- std::string_view("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e", 13u),
+ std::string_view("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e", 13u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2358079886), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e"), // macro-calculated hash
},
{
- std::string_view("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e", 13u),
+ std::string_view("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e", 13u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2358079886), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e"), // macro-calculated hash
},
{
- std::string_view("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a", 14u),
+ std::string_view("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a", 14u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4215296608), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a"), // macro-calculated hash
},
{
- std::string_view("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a", 14u),
+ std::string_view("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a", 14u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4215296608), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a"), // macro-calculated hash
},
{
- std::string_view("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a", 14u),
+ std::string_view("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a", 14u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(4215296608), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a"), // macro-calculated hash
},
{
- std::string_view("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8", 14u),
+ std::string_view("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8", 14u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1051337960), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8"), // macro-calculated hash
},
{
- std::string_view("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8", 14u),
+ std::string_view("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8", 14u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1051337960), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8"), // macro-calculated hash
},
{
- std::string_view("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8", 14u),
+ std::string_view("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8", 14u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1051337960), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8"), // macro-calculated hash
},
{
- std::string_view("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a", 15u),
+ std::string_view("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a", 15u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(3916582129), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a"), // macro-calculated hash
},
{
- std::string_view("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a", 15u),
+ std::string_view("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a", 15u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(3916582129), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a"), // macro-calculated hash
},
{
- std::string_view("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a", 15u),
+ std::string_view("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a", 15u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(3916582129), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a"), // macro-calculated hash
},
{
- std::string_view("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76", 15u),
+ std::string_view("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76", 15u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2665036172), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76"), // macro-calculated hash
},
{
- std::string_view("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76", 15u),
+ std::string_view("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76", 15u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2665036172), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76"), // macro-calculated hash
},
{
- std::string_view("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76", 15u),
+ std::string_view("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76", 15u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2665036172), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76"), // macro-calculated hash
},
{
- std::string_view("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb", 79u),
+ std::string_view("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb", 79u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2352453932), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb"), // macro-calculated hash
},
{
- std::string_view("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb", 79u),
+ std::string_view("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb", 79u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(2352453932), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb"), // macro-calculated hash
},
{
- std::string_view("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb", 79u),
+ std::string_view("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb", 79u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2352453932), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb"), // macro-calculated hash
},
{
- std::string_view("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13", 80u),
+ std::string_view("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13", 80u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4169625832), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13"), // macro-calculated hash
},
{
- std::string_view("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13", 80u),
+ std::string_view("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13", 80u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(4169625832), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13"), // macro-calculated hash
},
{
- std::string_view("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13", 80u),
+ std::string_view("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13", 80u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(4169625832), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13"), // macro-calculated hash
},
{
- std::string_view("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e", 81u),
+ std::string_view("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e", 81u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(2417296923), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e"), // macro-calculated hash
},
{
- std::string_view("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e", 81u),
+ std::string_view("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e", 81u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(987115853), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e"), // macro-calculated hash
},
{
- std::string_view("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e", 81u),
+ std::string_view("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e", 81u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(987115853), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e"), // macro-calculated hash
},
{
- std::string_view("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17", 95u),
+ std::string_view("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17", 95u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1750895817), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17"), // macro-calculated hash
},
{
- std::string_view("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17", 95u),
+ std::string_view("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17", 95u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(720276802), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17"), // macro-calculated hash
},
{
- std::string_view("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17", 95u),
+ std::string_view("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17", 95u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(720276802), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17"), // macro-calculated hash
},
{
- std::string_view("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b", 96u),
+ std::string_view("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b", 96u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(760136888), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b"), // macro-calculated hash
},
{
- std::string_view("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b", 96u),
+ std::string_view("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b", 96u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1408671026), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b"), // macro-calculated hash
},
{
- std::string_view("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b", 96u),
+ std::string_view("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b", 96u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1408671026), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b"), // macro-calculated hash
},
{
- std::string_view("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7", 97u),
+ std::string_view("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7", 97u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(4113347769), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7"), // macro-calculated hash
},
{
- std::string_view("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7", 97u),
+ std::string_view("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7", 97u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1367119804), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7"), // macro-calculated hash
},
{
- std::string_view("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7", 97u),
+ std::string_view("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7", 97u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(687960245), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7"), // macro-calculated hash
},
{
- std::string_view("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98", 127u),
+ std::string_view("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98", 127u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1288060573), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98"), // macro-calculated hash
},
{
- std::string_view("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98", 127u),
+ std::string_view("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98", 127u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1810369278), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98"), // macro-calculated hash
},
{
- std::string_view("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98", 127u),
+ std::string_view("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98", 127u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(2429195322), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98"), // macro-calculated hash
},
{
- std::string_view("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65", 128u),
+ std::string_view("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65", 128u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(1125261758), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65"), // macro-calculated hash
},
{
- std::string_view("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65", 128u),
+ std::string_view("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65", 128u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(1477867120), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65"), // macro-calculated hash
},
{
- std::string_view("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65", 128u),
+ std::string_view("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65", 128u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(3694995364), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65"), // macro-calculated hash
},
{
- std::string_view("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1", 129u),
+ std::string_view("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1", 129u), // NOLINT(bugprone-string-constructor)
80u, // fixed hash length
UINT32_C(6281856), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_80_HASH("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1"), // macro-calculated hash
},
{
- std::string_view("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1", 129u),
+ std::string_view("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1", 129u), // NOLINT(bugprone-string-constructor)
96u, // fixed hash length
UINT32_C(598421397), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_96_HASH("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1"), // macro-calculated hash
},
{
- std::string_view("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1", 129u),
+ std::string_view("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1", 129u), // NOLINT(bugprone-string-constructor)
128u, // fixed hash length
UINT32_C(1313299978), // Python-calculated hash
PW_TOKENIZER_65599_FIXED_LENGTH_128_HASH("\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1"), // macro-calculated hash
diff --git a/pw_tokenizer/pw_tokenizer_private/tokenize_test.h b/pw_tokenizer/pw_tokenizer_private/tokenize_test.h
index e65ceb610..391c5069a 100644
--- a/pw_tokenizer/pw_tokenizer_private/tokenize_test.h
+++ b/pw_tokenizer/pw_tokenizer_private/tokenize_test.h
@@ -13,7 +13,7 @@
// the License.
// Functions to test that the tokenization macro works correctly in C code.
-// These are defined in tokenize_test.c and global_handlers_test.c.
+// These are defined in tokenize_test.c.
#pragma once
#include <stddef.h>
diff --git a/pw_tokenizer/pw_tokenizer_zephyr.ld b/pw_tokenizer/pw_tokenizer_zephyr.ld
new file mode 100644
index 000000000..7a34ff832
--- /dev/null
+++ b/pw_tokenizer/pw_tokenizer_zephyr.ld
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 The Pigweed Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/*
+ * This file is separate from pw_tokenizer_linker_sections.ld because Zephyr
+ * already defines the top level SECTIONS label and requires new linker
+ * scripts to only add the individual sections. Additionally, the 0x0 address
+ * is incompatible with Zephyr's linker scripts.
+ *
+ * See detailed section info at pw_tokenizer_linker_sections.ld
+ */
+
+#ifdef CONFIG_ARCH_POSIX
+#define ADDR
+#else
+#define ADDR 0x0
+#endif
+
+.pw_tokenizer.info ADDR (INFO) :
+{
+ KEEP(*(.pw_tokenizer.info))
+}
+
+.pw_tokenizer.entries ADDR (INFO) :
+{
+ KEEP(*(.pw_tokenizer.entries.*))
+}
diff --git a/pw_tokenizer/py/BUILD.bazel b/pw_tokenizer/py/BUILD.bazel
index 540495a66..299d29ce4 100644
--- a/pw_tokenizer/py/BUILD.bazel
+++ b/pw_tokenizer/py/BUILD.bazel
@@ -30,6 +30,7 @@ py_library(
"pw_tokenizer/serial_detokenizer.py",
"pw_tokenizer/tokens.py",
],
+ imports = ["."],
deps = [
"//pw_cli/py:pw_cli",
],
@@ -46,7 +47,7 @@ py_binary(
# This test attempts to directly access files in the source tree, which is
# incompatible with sandboxing.
-# TODO(b/241307309): Fix this test.
+# TODO: b/241307309 - Fix this test.
filegroup(
name = "database_test",
srcs = ["database_test.py"],
@@ -83,7 +84,7 @@ proto_library(
],
)
-# TODO(b/241456982): This target can't be built due to limitations of
+# TODO: b/241456982 - This target can't be built due to limitations of
# py_proto_library.
# py_proto_library(
# name = "detokenize_proto_test_pb2",
@@ -101,7 +102,6 @@ py_test(
],
deps = [
":pw_tokenizer",
- "@rules_python//python/runfiles",
],
)
@@ -113,7 +113,6 @@ py_test(
],
deps = [
":pw_tokenizer",
- "@rules_python//python/runfiles",
],
)
diff --git a/pw_tokenizer/py/detokenize_test.py b/pw_tokenizer/py/detokenize_test.py
index df710c7e9..36bb1fa6e 100755
--- a/pw_tokenizer/py/detokenize_test.py
+++ b/pw_tokenizer/py/detokenize_test.py
@@ -15,12 +15,15 @@
"""Tests for detokenize."""
import base64
+import concurrent
import datetime as dt
+import functools
import io
import os
from pathlib import Path
import struct
import tempfile
+from typing import Any, Callable, NamedTuple, Tuple
import unittest
from unittest import mock
@@ -451,6 +454,35 @@ class DetokenizeWithCollisions(unittest.TestCase):
self.assertIn('#0 -1', repr(unambiguous))
+class ManualPoolExecutor(concurrent.futures.Executor):
+ """A stubbed pool executor that captures the most recent work request
+ and holds it until the public process method is manually called."""
+
+ def __init__(self):
+ super().__init__()
+ self._func = None
+
+ # pylint: disable=arguments-differ
+ def submit(self, func, *args, **kwargs):
+ """Submits work to the pool, stashing the partial for later use."""
+ self._func = functools.partial(func, *args, **kwargs)
+
+ def process(self):
+ """Processes the latest func submitted to the pool."""
+ if self._func is not None:
+ self._func()
+ self._func = None
+
+
+class InlinePoolExecutor(concurrent.futures.Executor):
+ """A stubbed pool executor that runs work immediately, inline."""
+
+ # pylint: disable=arguments-differ
+ def submit(self, func, *args, **kwargs):
+ """Submits work to the pool, stashing the partial for later use."""
+ func(*args, **kwargs)
+
+
@mock.patch('os.path.getmtime')
class AutoUpdatingDetokenizerTest(unittest.TestCase):
"""Tests the AutoUpdatingDetokenizer class."""
@@ -478,18 +510,79 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
try:
file.close()
+ pool = ManualPoolExecutor()
detok = detokenize.AutoUpdatingDetokenizer(
- file.name, min_poll_period_s=0
+ file.name, min_poll_period_s=0, pool=pool
)
self.assertFalse(detok.detokenize(JELLO_WORLD_TOKEN).ok())
with open(file.name, 'wb') as fd:
tokens.write_binary(db, fd)
+ # After the change but before the pool runs in another thread,
+ # the token should not exist.
+ self.assertFalse(detok.detokenize(JELLO_WORLD_TOKEN).ok())
+
+ # After the pool is allowed to process, it should.
+ pool.process()
self.assertTrue(detok.detokenize(JELLO_WORLD_TOKEN).ok())
finally:
os.unlink(file.name)
+ def test_update_with_directory(self, mock_getmtime):
+ """Tests the update command with a directory format database."""
+ db = database.load_token_database(
+ io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS)
+ )
+ self.assertEqual(len(db), TOKENS_IN_ELF)
+
+ the_time = [100]
+
+ def move_back_time_if_file_exists(path):
+ if os.path.exists(path):
+ the_time[0] -= 1
+ return the_time[0]
+
+ raise FileNotFoundError
+
+ mock_getmtime.side_effect = move_back_time_if_file_exists
+
+ with tempfile.TemporaryDirectory() as dbdir:
+ with tempfile.NamedTemporaryFile(
+ 'wb', delete=False, suffix='.pw_tokenizer.csv', dir=dbdir
+ ) as matching_suffix_file, tempfile.NamedTemporaryFile(
+ 'wb', delete=False, suffix='.not.right', dir=dbdir
+ ) as mismatched_suffix_file:
+ try:
+ matching_suffix_file.close()
+ mismatched_suffix_file.close()
+
+ pool = ManualPoolExecutor()
+ detok = detokenize.AutoUpdatingDetokenizer(
+ dbdir, min_poll_period_s=0, pool=pool
+ )
+ self.assertFalse(detok.detokenize(JELLO_WORLD_TOKEN).ok())
+
+ with open(mismatched_suffix_file.name, 'wb') as fd:
+ tokens.write_csv(db, fd)
+ pool.process()
+ self.assertFalse(detok.detokenize(JELLO_WORLD_TOKEN).ok())
+
+ with open(matching_suffix_file.name, 'wb') as fd:
+ tokens.write_csv(db, fd)
+
+ # After the change but before the pool runs in another
+ # thread, the token should not exist.
+ self.assertFalse(detok.detokenize(JELLO_WORLD_TOKEN).ok())
+ pool.process()
+
+ # After the pool is allowed to process, it should.
+ self.assertTrue(detok.detokenize(JELLO_WORLD_TOKEN).ok())
+ finally:
+ os.unlink(mismatched_suffix_file.name)
+ os.unlink(matching_suffix_file.name)
+ os.rmdir(dbdir)
+
# The database stays around if the file is deleted.
self.assertTrue(detok.detokenize(JELLO_WORLD_TOKEN).ok())
@@ -507,7 +600,7 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
file.close()
detok = detokenize.AutoUpdatingDetokenizer(
- file.name, min_poll_period_s=0
+ file.name, min_poll_period_s=0, pool=InlinePoolExecutor()
)
self.assertTrue(detok.detokenize(JELLO_WORLD_TOKEN).ok())
@@ -527,7 +620,9 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
def test_token_domain_in_str(self, _) -> None:
"""Tests a str containing a domain"""
detok = detokenize.AutoUpdatingDetokenizer(
- f'{ELF_WITH_TOKENIZER_SECTIONS_PATH}#.*', min_poll_period_s=0
+ f'{ELF_WITH_TOKENIZER_SECTIONS_PATH}#.*',
+ min_poll_period_s=0,
+ pool=InlinePoolExecutor(),
)
self.assertEqual(
len(detok.database), TOKENS_IN_ELF_WITH_TOKENIZER_SECTIONS
@@ -536,7 +631,9 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
def test_token_domain_in_path(self, _) -> None:
"""Tests a Path() containing a domain"""
detok = detokenize.AutoUpdatingDetokenizer(
- Path(f'{ELF_WITH_TOKENIZER_SECTIONS_PATH}#.*'), min_poll_period_s=0
+ Path(f'{ELF_WITH_TOKENIZER_SECTIONS_PATH}#.*'),
+ min_poll_period_s=0,
+ pool=InlinePoolExecutor(),
)
self.assertEqual(
len(detok.database), TOKENS_IN_ELF_WITH_TOKENIZER_SECTIONS
@@ -545,14 +642,18 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
def test_token_no_domain_in_str(self, _) -> None:
"""Tests a str without a domain"""
detok = detokenize.AutoUpdatingDetokenizer(
- str(ELF_WITH_TOKENIZER_SECTIONS_PATH), min_poll_period_s=0
+ str(ELF_WITH_TOKENIZER_SECTIONS_PATH),
+ min_poll_period_s=0,
+ pool=InlinePoolExecutor(),
)
self.assertEqual(len(detok.database), TOKENS_IN_ELF)
def test_token_no_domain_in_path(self, _) -> None:
"""Tests a Path() without a domain"""
detok = detokenize.AutoUpdatingDetokenizer(
- ELF_WITH_TOKENIZER_SECTIONS_PATH, min_poll_period_s=0
+ ELF_WITH_TOKENIZER_SECTIONS_PATH,
+ min_poll_period_s=0,
+ pool=InlinePoolExecutor(),
)
self.assertEqual(len(detok.database), TOKENS_IN_ELF)
@@ -561,39 +662,173 @@ def _next_char(message: bytes) -> bytes:
return bytes(b + 1 for b in message)
-class PrefixedMessageDecoderTest(unittest.TestCase):
- def setUp(self):
- super().setUp()
- self.decode = detokenize.PrefixedMessageDecoder('$', 'abcdefg')
+class NestedMessageParserTest(unittest.TestCase):
+ """Tests parsing prefixed messages."""
+
+ class _Case(NamedTuple):
+ data: bytes
+ expected: bytes
+ title: str
+ transform: Callable[[bytes], bytes] = _next_char
+
+ TRANSFORM_TEST_CASES = (
+ _Case(b'$abcd', b'%bcde', 'single message'),
+ _Case(
+ b'$$WHAT?$abc$WHY? is this $ok $',
+ b'%%WHAT?%bcd%WHY? is this %ok %',
+ 'message and non-message',
+ ),
+ _Case(b'$1$', b'%1%', 'empty message'),
+ _Case(b'$abc$defgh', b'%bcd%efghh', 'sequential message'),
+ _Case(
+ b'w$abcx$defygh$$abz',
+ b'w$ABCx$DEFygh$$ABz',
+ 'interspersed start/end non-message',
+ bytes.upper,
+ ),
+ _Case(
+ b'$abcx$defygh$$ab',
+ b'$ABCx$DEFygh$$AB',
+ 'interspersed start/end message ',
+ bytes.upper,
+ ),
+ )
+
+ def setUp(self) -> None:
+ self.decoder = detokenize.NestedMessageParser('$', 'abcdefg')
+
+ def test_transform_io(self) -> None:
+ for data, expected, title, transform in self.TRANSFORM_TEST_CASES:
+ self.assertEqual(
+ expected,
+ b''.join(
+ self.decoder.transform_io(io.BytesIO(data), transform)
+ ),
+ f'{title}: {data!r}',
+ )
+
+ def test_transform_bytes_with_flush(self) -> None:
+ for data, expected, title, transform in self.TRANSFORM_TEST_CASES:
+ self.assertEqual(
+ expected,
+ self.decoder.transform(data, transform, flush=True),
+ f'{title}: {data!r}',
+ )
+
+ def test_transform_bytes_sequential(self) -> None:
+ transform = lambda message: message.upper().replace(b'$', b'*')
- def test_transform_single_message(self):
+ self.assertEqual(self.decoder.transform(b'abc$abcd', transform), b'abc')
+ self.assertEqual(self.decoder.transform(b'$', transform), b'*ABCD')
+ self.assertEqual(self.decoder.transform(b'$b', transform), b'*')
+ self.assertEqual(self.decoder.transform(b'', transform), b'')
+ self.assertEqual(self.decoder.transform(b' ', transform), b'*B ')
+ self.assertEqual(self.decoder.transform(b'hello', transform), b'hello')
+ self.assertEqual(self.decoder.transform(b'?? $ab', transform), b'?? ')
self.assertEqual(
- b'%bcde',
- b''.join(self.decode.transform(io.BytesIO(b'$abcd'), _next_char)),
+ self.decoder.transform(b'123$ab4$56$a', transform), b'*AB123*AB4*56'
)
+ self.assertEqual(
+ self.decoder.transform(b'bc', transform, flush=True), b'*ABC'
+ )
+
+ MESSAGES_TEST: Any = (
+ (b'123$abc456$a', (False, b'123'), (True, b'$abc'), (False, b'456')),
+ (b'7$abcd', (True, b'$a'), (False, b'7')),
+ (b'e',),
+ (b'',),
+ (b'$', (True, b'$abcde')),
+ (b'$', (True, b'$')),
+ (b'$a$b$c', (True, b'$'), (True, b'$a'), (True, b'$b')),
+ (b'1', (True, b'$c'), (False, b'1')),
+ (b'',),
+ (b'?', (False, b'?')),
+ (b'!@', (False, b'!@')),
+ (b'%^&', (False, b'%^&')),
+ )
- def test_transform_message_amidst_other_only_affects_message(self):
+ def test_read_messages(self) -> None:
+ for step in self.MESSAGES_TEST:
+ data: bytes = step[0]
+ pieces: Tuple[Tuple[bool, bytes], ...] = step[1:]
+ self.assertEqual(tuple(self.decoder.read_messages(data)), pieces)
+
+ def test_read_messages_flush(self) -> None:
self.assertEqual(
- b'%%WHAT?%bcd%WHY? is this %ok %',
- b''.join(
- self.decode.transform(
- io.BytesIO(b'$$WHAT?$abc$WHY? is this $ok $'), _next_char
- )
- ),
+ list(self.decoder.read_messages(b'123$a')), [(False, b'123')]
)
+ self.assertEqual(list(self.decoder.read_messages(b'b')), [])
+ self.assertEqual(
+ list(self.decoder.read_messages(b'', flush=True)), [(True, b'$ab')]
+ )
+
+ def test_read_messages_io(self) -> None:
+ # Rework the read_messages test data for stream input.
+ data = io.BytesIO(b''.join(step[0] for step in self.MESSAGES_TEST))
+ expected_pieces = sum((step[1:] for step in self.MESSAGES_TEST), ())
+
+ result = self.decoder.read_messages_io(data)
+ for expected_is_message, expected_data in expected_pieces:
+ if expected_is_message:
+ is_message, piece = next(result)
+ self.assertTrue(is_message)
+ self.assertEqual(expected_data, piece)
+ else: # the IO version yields non-messages byte by byte
+ for byte in expected_data:
+ is_message, piece = next(result)
+ self.assertFalse(is_message)
+ self.assertEqual(bytes([byte]), piece)
- def test_transform_empty_message(self):
+
+class DetokenizeNested(unittest.TestCase):
+ """Tests detokenizing nested tokens"""
+
+ def test_nested_hashed_arg(self):
+ detok = detokenize.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(0xA, 'tokenized argument'),
+ tokens.TokenizedStringEntry(
+ 2,
+ 'This is a ' + '$#%08x',
+ ),
+ ]
+ )
+ )
self.assertEqual(
- b'%1%',
- b''.join(self.decode.transform(io.BytesIO(b'$1$'), _next_char)),
+ str(detok.detokenize(b'\x02\0\0\0\x14')),
+ 'This is a tokenized argument',
)
- def test_transform_sequential_messages(self):
+ def test_nested_base64_arg(self):
+ detok = detokenize.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(1, 'base64 argument'),
+ tokens.TokenizedStringEntry(2, 'This is a %s'),
+ ]
+ )
+ )
self.assertEqual(
- b'%bcd%efghh',
- b''.join(
- self.decode.transform(io.BytesIO(b'$abc$defgh'), _next_char)
- ),
+ str(detok.detokenize(b'\x02\0\0\0\x09$AQAAAA==')), # token for 1
+ 'This is a base64 argument',
+ )
+
+ def test_deeply_nested_arg(self):
+ detok = detokenize.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(1, '$10#0000000005'),
+ tokens.TokenizedStringEntry(2, 'This is a $#%08x'),
+ tokens.TokenizedStringEntry(3, 'deeply nested argument'),
+ tokens.TokenizedStringEntry(4, '$AQAAAA=='),
+ tokens.TokenizedStringEntry(5, '$AwAAAA=='),
+ ]
+ )
+ )
+ self.assertEqual(
+ str(detok.detokenize(b'\x02\0\0\0\x08')), # token for 4
+ 'This is a deeply nested argument',
)
@@ -627,6 +862,10 @@ class DetokenizeBase64(unittest.TestCase):
(JELLO + b'$a' + JELLO + b'bcd', b'Jello, world!$aJello, world!bcd'),
(b'$3141', b'$3141'),
(JELLO + b'$3141', b'Jello, world!$3141'),
+ (
+ JELLO + b'$a' + JELLO + b'b' + JELLO + b'c',
+ b'Jello, world!$aJello, world!bJello, world!c',
+ ),
(RECURSION, b'The secret message is "Jello, world!"'),
(
RECURSION_2,
@@ -650,7 +889,7 @@ class DetokenizeBase64(unittest.TestCase):
output = io.BytesIO()
self.detok.detokenize_base64_live(io.BytesIO(data), output, '$')
- self.assertEqual(expected, output.getvalue())
+ self.assertEqual(expected, output.getvalue(), f'Input: {data!r}')
def test_detokenize_base64_to_file(self):
for data, expected in self.TEST_CASES:
@@ -670,6 +909,52 @@ class DetokenizeBase64(unittest.TestCase):
)
+class DetokenizeInfiniteRecursion(unittest.TestCase):
+ """Tests that infinite Base64 token recursion resolves."""
+
+ def setUp(self):
+ super().setUp()
+ self.detok = detokenize.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(0, '$AAAAAA=='), # token for 0
+ tokens.TokenizedStringEntry(1, '$AgAAAA=='), # token for 2
+ tokens.TokenizedStringEntry(2, '$#00000003'), # token for 3
+ tokens.TokenizedStringEntry(3, '$AgAAAA=='), # token for 2
+ ]
+ )
+ )
+
+ def test_detokenize_self_recursion(self):
+ for depth in range(5):
+ self.assertEqual(
+ self.detok.detokenize_text(
+ b'This one is deep: $AAAAAA==', recursion=depth
+ ),
+ b'This one is deep: $AAAAAA==',
+ )
+
+ def test_detokenize_self_recursion_default(self):
+ self.assertEqual(
+ self.detok.detokenize_text(
+ b'This one is deep: $AAAAAA==',
+ ),
+ b'This one is deep: $AAAAAA==',
+ )
+
+ def test_detokenize_cyclic_recursion_even(self):
+ self.assertEqual(
+ self.detok.detokenize_text(b'I said "$AQAAAA=="', recursion=6),
+ b'I said "$AgAAAA=="',
+ )
+
+ def test_detokenize_cyclic_recursion_odd(self):
+ self.assertEqual(
+ self.detok.detokenize_text(b'I said "$AQAAAA=="', recursion=7),
+ b'I said "$#00000003"',
+ )
+
+
class DetokenizeBase64InfiniteRecursion(unittest.TestCase):
"""Tests that infinite Bas64 token recursion resolves."""
@@ -697,7 +982,7 @@ class DetokenizeBase64InfiniteRecursion(unittest.TestCase):
def test_detokenize_self_recursion_default(self):
self.assertEqual(
- self.detok.detokenize_base64(b'This one is deep: $AAAAAA=='),
+ self.detok.detokenize_base64(b'This one is deep: $64#AAAAAA=='),
b'This one is deep: $AAAAAA==',
)
diff --git a/pw_tokenizer/py/generate_hash_test_data.py b/pw_tokenizer/py/generate_hash_test_data.py
index b875f188f..b658b1fd2 100755
--- a/pw_tokenizer/py/generate_hash_test_data.py
+++ b/pw_tokenizer/py/generate_hash_test_data.py
@@ -23,7 +23,7 @@ from pw_tokenizer import tokens
HASH_LENGTHS = 80, 96, 128
HASH_MACRO = 'PW_TOKENIZER_65599_FIXED_LENGTH_{}_HASH'
-FILE_HEADER = """\
+SHARED_HEADER = """\
// Copyright {year} The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
@@ -42,6 +42,9 @@ FILE_HEADER = """\
//
// This file was generated by {script}.
// To make changes, update the script and run it to generate new files.
+"""
+
+CPP_HEADER = """\
#pragma once
#include <cstddef>
@@ -62,7 +65,7 @@ inline constexpr struct {{
"""
-FILE_FOOTER = """
+CPP_FOOTER = """
}; // kHashTests
// clang-format on
@@ -70,14 +73,31 @@ FILE_FOOTER = """
} // namespace pw::tokenizer
"""
-_TEST_CASE = """{{
- std::string_view("{str}", {string_length}u),
+_CPP_TEST_CASE = """{{
+ std::string_view("{str}", {string_length}u), // NOLINT(bugprone-string-constructor)
{hash_length}u, // fixed hash length
UINT32_C({hash}), // Python-calculated hash
{macro}("{str}"), // macro-calculated hash
}},
"""
+RUST_HEADER = """
+fn test_cases() -> Vec<TestCase> {{
+ vec![
+"""
+
+RUST_FOOTER = """
+ ]
+}
+"""
+
+_RUST_TEST_CASE = """ TestCase{{
+ string: b"{str}",
+ hash_length: {hash_length},
+ hash: {hash},
+ }},
+"""
+
def _include_paths(lengths):
return '\n'.join(
@@ -89,7 +109,7 @@ def _include_paths(lengths):
)
-def _test_case_at_length(data, hash_length):
+def _test_case_at_length(test_case_template, data, hash_length):
"""Generates a test case for a particular hash length."""
if isinstance(data, str):
@@ -100,7 +120,7 @@ def _test_case_at_length(data, hash_length):
else:
escaped_str = ''.join(r'\x{:02x}'.format(b) for b in data)
- return _TEST_CASE.format(
+ return test_case_template.format(
str=escaped_str,
string_length=len(data),
hash_length=hash_length,
@@ -109,22 +129,23 @@ def _test_case_at_length(data, hash_length):
)
-def test_case(data):
+def test_case(test_case_template, data):
return ''.join(
- _test_case_at_length(data, length) for length in (80, 96, 128)
+ _test_case_at_length(test_case_template, data, length)
+ for length in (80, 96, 128)
)
-def generate_test_cases():
- yield test_case('')
- yield test_case(b'\xa1')
- yield test_case(b'\xff')
- yield test_case('\0')
- yield test_case('\0\0')
- yield test_case('a')
- yield test_case('A')
- yield test_case('hello, "world"')
- yield test_case('YO' * 100)
+def generate_test_cases(test_case_template):
+ yield test_case(test_case_template, '')
+ yield test_case(test_case_template, b'\xa1')
+ yield test_case(test_case_template, b'\xff')
+ yield test_case(test_case_template, '\0')
+ yield test_case(test_case_template, '\0\0')
+ yield test_case(test_case_template, 'a')
+ yield test_case(test_case_template, 'A')
+ yield test_case(test_case_template, 'hello, "world"')
+ yield test_case(test_case_template, 'YO' * 100)
random.seed(600613)
@@ -133,37 +154,60 @@ def generate_test_cases():
)
for i in range(1, 16):
- yield test_case(random_string(i))
- yield test_case(random_string(i))
+ yield test_case(test_case_template, random_string(i))
+ yield test_case(test_case_template, random_string(i))
for length in HASH_LENGTHS:
- yield test_case(random_string(length - 1))
- yield test_case(random_string(length))
- yield test_case(random_string(length + 1))
+ yield test_case(test_case_template, random_string(length - 1))
+ yield test_case(test_case_template, random_string(length))
+ yield test_case(test_case_template, random_string(length + 1))
-if __name__ == '__main__':
+def generate_file(
+ path_array, header_template, footer_template, test_case_template
+):
path = os.path.realpath(
- os.path.join(
- os.path.dirname(__file__),
- '..',
- 'pw_tokenizer_private',
- 'generated_hash_test_cases.h',
- )
+ os.path.join(os.path.dirname(__file__), *path_array)
)
with open(path, 'w') as output:
output.write(
- FILE_HEADER.format(
+ SHARED_HEADER.format(
year=datetime.date.today().year,
script=os.path.basename(__file__),
+ )
+ )
+ output.write(
+ header_template.format(
includes=_include_paths(HASH_LENGTHS),
)
)
- for case in generate_test_cases():
+ for case in generate_test_cases(test_case_template):
output.write(case)
- output.write(FILE_FOOTER)
+ output.write(footer_template)
+ print('Wrote test data to', path)
+
- print('Wrote test data to', path)
+if __name__ == '__main__':
+ generate_file(
+ [
+ '..',
+ 'pw_tokenizer_private',
+ 'generated_hash_test_cases.h',
+ ],
+ CPP_HEADER,
+ CPP_FOOTER,
+ _CPP_TEST_CASE,
+ )
+ generate_file(
+ [
+ '..',
+ 'rust',
+ 'pw_tokenizer_core_test_cases.rs',
+ ],
+ RUST_HEADER,
+ RUST_FOOTER,
+ _RUST_TEST_CASE,
+ )
diff --git a/pw_tokenizer/py/pw_tokenizer/database.py b/pw_tokenizer/py/pw_tokenizer/database.py
index 26a32a7fa..54d142eff 100755
--- a/pw_tokenizer/py/pw_tokenizer/database.py
+++ b/pw_tokenizer/py/pw_tokenizer/database.py
@@ -297,6 +297,9 @@ def _handle_create(
f'The file {database} already exists! Use --force to overwrite.'
)
+ if not database.parent.exists():
+ database.parent.mkdir(parents=True)
+
if output_type == 'directory':
if str(database) == '-':
raise ValueError(
diff --git a/pw_tokenizer/py/pw_tokenizer/detokenize.py b/pw_tokenizer/py/pw_tokenizer/detokenize.py
index 3aa7a3a8b..c777252da 100755
--- a/pw_tokenizer/py/pw_tokenizer/detokenize.py
+++ b/pw_tokenizer/py/pw_tokenizer/detokenize.py
@@ -20,11 +20,11 @@ or a file object for an ELF file or CSV. Then, call the detokenize method with
encoded messages, one at a time. The detokenize method returns a
DetokenizedString object with the result.
-For example,
+For example::
from pw_tokenizer import detokenize
- detok = detokenize.Detokenizer('path/to/my/image.elf')
+ detok = detokenize.Detokenizer('path/to/firmware/image.elf')
print(detok.detokenize(b'\x12\x34\x56\x78\x03hi!'))
This module also provides a command line interface for decoding and detokenizing
@@ -34,6 +34,8 @@ messages from a file or stdin.
import argparse
import base64
import binascii
+from concurrent.futures import Executor, ThreadPoolExecutor
+import enum
import io
import logging
import os
@@ -42,6 +44,7 @@ import re
import string
import struct
import sys
+import threading
import time
from typing import (
AnyStr,
@@ -70,10 +73,48 @@ except ImportError:
_LOG = logging.getLogger('pw_tokenizer')
ENCODED_TOKEN = struct.Struct('<I')
-BASE64_PREFIX = encode.BASE64_PREFIX.encode()
+_BASE64_CHARS = string.ascii_letters + string.digits + '+/-_='
DEFAULT_RECURSION = 9
+NESTED_TOKEN_PREFIX = encode.NESTED_TOKEN_PREFIX.encode()
+NESTED_TOKEN_BASE_PREFIX = encode.NESTED_TOKEN_BASE_PREFIX.encode()
+
+_BASE8_TOKEN_REGEX = rb'(?P<base8>[0-7]{11})'
+_BASE10_TOKEN_REGEX = rb'(?P<base10>[0-9]{10})'
+_BASE16_TOKEN_REGEX = rb'(?P<base16>[A-Fa-f0-9]{8})'
+_BASE64_TOKEN_REGEX = (
+ rb'(?P<base64>'
+ # Tokenized Base64 contains 0 or more blocks of four Base64 chars.
+ rb'(?:[A-Za-z0-9+/\-_]{4})*'
+ # The last block of 4 chars may have one or two padding chars (=).
+ rb'(?:[A-Za-z0-9+/\-_]{3}=|[A-Za-z0-9+/\-_]{2}==)?'
+ rb')'
+)
+_NESTED_TOKEN_FORMATS = (
+ _BASE8_TOKEN_REGEX,
+ _BASE10_TOKEN_REGEX,
+ _BASE16_TOKEN_REGEX,
+ _BASE64_TOKEN_REGEX,
+)
+
+_RawIo = Union[io.RawIOBase, BinaryIO]
+_RawIoOrBytes = Union[_RawIo, bytes]
+
-_RawIO = Union[io.RawIOBase, BinaryIO]
+def _token_regex(prefix: bytes) -> Pattern[bytes]:
+ """Returns a regular expression for prefixed tokenized strings."""
+ return re.compile(
+ # Tokenized strings start with the prefix character ($).
+ re.escape(prefix)
+ # Optional; no base specifier defaults to BASE64.
+ # Hash (#) with no number specified defaults to Base-16.
+ + rb'(?P<basespec>(?P<base>[0-9]*)?'
+ + NESTED_TOKEN_BASE_PREFIX
+ + rb')?'
+ # Match one of the following token formats.
+ + rb'('
+ + rb'|'.join(_NESTED_TOKEN_FORMATS)
+ + rb')'
+ )
class DetokenizedString:
@@ -85,6 +126,7 @@ class DetokenizedString:
format_string_entries: Iterable[tuple],
encoded_message: bytes,
show_errors: bool = False,
+ recursive_detokenize: Optional[Callable[[str], str]] = None,
):
self.token = token
self.encoded_message = encoded_message
@@ -99,6 +141,12 @@ class DetokenizedString:
result = fmt.format(
encoded_message[ENCODED_TOKEN.size :], show_errors
)
+ if recursive_detokenize:
+ result = decode.FormattedString(
+ recursive_detokenize(result.value),
+ result.args,
+ result.remaining,
+ )
decode_attempts.append((result.score(entry.date_removed), result))
# Sort the attempts by the score so the most likely results are first.
@@ -186,28 +234,39 @@ class Detokenizer:
"""
self.show_errors = show_errors
+ self._database_lock = threading.Lock()
+
# Cache FormatStrings for faster lookup & formatting.
self._cache: Dict[int, List[_TokenizedFormatString]] = {}
self._initialize_database(token_database_or_elf)
def _initialize_database(self, token_sources: Iterable) -> None:
- self.database = database.load_token_database(*token_sources)
- self._cache.clear()
+ with self._database_lock:
+ self.database = database.load_token_database(*token_sources)
+ self._cache.clear()
def lookup(self, token: int) -> List[_TokenizedFormatString]:
"""Returns (TokenizedStringEntry, FormatString) list for matches."""
- try:
- return self._cache[token]
- except KeyError:
- format_strings = [
- _TokenizedFormatString(entry, decode.FormatString(str(entry)))
- for entry in self.database.token_to_entries[token]
- ]
- self._cache[token] = format_strings
- return format_strings
-
- def detokenize(self, encoded_message: bytes) -> DetokenizedString:
+ with self._database_lock:
+ try:
+ return self._cache[token]
+ except KeyError:
+ format_strings = [
+ _TokenizedFormatString(
+ entry, decode.FormatString(str(entry))
+ )
+ for entry in self.database.token_to_entries[token]
+ ]
+ self._cache[token] = format_strings
+ return format_strings
+
+ def detokenize(
+ self,
+ encoded_message: bytes,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> DetokenizedString:
"""Decodes and detokenizes a message as a DetokenizedString."""
if not encoded_message:
return DetokenizedString(
@@ -222,14 +281,25 @@ class Detokenizer:
encoded_message += b'\0' * missing_token_bytes
(token,) = ENCODED_TOKEN.unpack_from(encoded_message)
+
+ recursive_detokenize = None
+ if recursion > 0:
+ recursive_detokenize = self._detokenize_nested_callback(
+ prefix, recursion
+ )
+
return DetokenizedString(
- token, self.lookup(token), encoded_message, self.show_errors
+ token,
+ self.lookup(token),
+ encoded_message,
+ self.show_errors,
+ recursive_detokenize,
)
- def detokenize_base64(
+ def detokenize_text(
self,
data: AnyStr,
- prefix: Union[str, bytes] = BASE64_PREFIX,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
recursion: int = DEFAULT_RECURSION,
) -> AnyStr:
"""Decodes and replaces prefixed Base64 messages in the provided data.
@@ -242,88 +312,174 @@ class Detokenizer:
Returns:
copy of the data with all recognized tokens decoded
"""
- output = io.BytesIO()
- self.detokenize_base64_to_file(data, output, prefix, recursion)
- result = output.getvalue()
- return result.decode() if isinstance(data, str) else result
+ return self._detokenize_nested_callback(prefix, recursion)(data)
- def detokenize_base64_to_file(
+ # TODO(gschen): remove unnecessary function
+ def detokenize_base64(
+ self,
+ data: AnyStr,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> AnyStr:
+ """Alias of detokenize_text for backwards compatibility."""
+ return self.detokenize_text(data, prefix, recursion)
+
+ def detokenize_text_to_file(
self,
- data: Union[str, bytes],
+ data: AnyStr,
output: BinaryIO,
- prefix: Union[str, bytes] = BASE64_PREFIX,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
recursion: int = DEFAULT_RECURSION,
) -> None:
"""Decodes prefixed Base64 messages in data; decodes to output file."""
- data = data.encode() if isinstance(data, str) else data
- prefix = prefix.encode() if isinstance(prefix, str) else prefix
+ output.write(self._detokenize_nested(data, prefix, recursion))
- output.write(
- _base64_message_regex(prefix).sub(
- self._detokenize_prefixed_base64(prefix, recursion), data
- )
- )
+ # TODO(gschen): remove unnecessary function
+ def detokenize_base64_to_file(
+ self,
+ data: AnyStr,
+ output: BinaryIO,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> None:
+ """Alias of detokenize_text_to_file for backwards compatibility."""
+ self.detokenize_text_to_file(data, output, prefix, recursion)
- def detokenize_base64_live(
+ def detokenize_text_live(
self,
- input_file: _RawIO,
+ input_file: _RawIo,
output: BinaryIO,
- prefix: Union[str, bytes] = BASE64_PREFIX,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
recursion: int = DEFAULT_RECURSION,
) -> None:
"""Reads chars one-at-a-time, decoding messages; SLOW for big files."""
- prefix_bytes = prefix.encode() if isinstance(prefix, str) else prefix
-
- base64_message = _base64_message_regex(prefix_bytes)
def transform(data: bytes) -> bytes:
- return base64_message.sub(
- self._detokenize_prefixed_base64(prefix_bytes, recursion), data
- )
+ return self._detokenize_nested(data.decode(), prefix, recursion)
- for message in PrefixedMessageDecoder(
- prefix, string.ascii_letters + string.digits + '+/-_='
- ).transform(input_file, transform):
+ for message in NestedMessageParser(prefix, _BASE64_CHARS).transform_io(
+ input_file, transform
+ ):
output.write(message)
# Flush each line to prevent delays when piping between processes.
if b'\n' in message:
output.flush()
- def _detokenize_prefixed_base64(
- self, prefix: bytes, recursion: int
- ) -> Callable[[Match[bytes]], bytes]:
- """Returns a function that decodes prefixed Base64."""
+ # TODO(gschen): remove unnecessary function
+ def detokenize_base64_live(
+ self,
+ input_file: _RawIo,
+ output: BinaryIO,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> None:
+ """Alias of detokenize_text_live for backwards compatibility."""
+ self.detokenize_text_live(input_file, output, prefix, recursion)
- def decode_and_detokenize(match: Match[bytes]) -> bytes:
- """Decodes prefixed base64 with this detokenizer."""
- original = match.group(0)
+ def _detokenize_nested_callback(
+ self,
+ prefix: Union[str, bytes],
+ recursion: int,
+ ) -> Callable[[AnyStr], AnyStr]:
+ """Returns a function that replaces all tokens for a given string."""
- try:
- detokenized_string = self.detokenize(
- base64.b64decode(original[1:], validate=True)
- )
- if detokenized_string.matches():
- result = str(detokenized_string).encode()
+ def detokenize(message: AnyStr) -> AnyStr:
+ result = self._detokenize_nested(message, prefix, recursion)
+ return result.decode() if isinstance(message, str) else result
+
+ return detokenize
+
+ def _detokenize_nested(
+ self,
+ message: Union[str, bytes],
+ prefix: Union[str, bytes],
+ recursion: int,
+ ) -> bytes:
+ """Returns the message with recognized tokens replaced.
+
+ Message data is internally handled as bytes regardless of input message
+ type and returns the result as bytes.
+ """
+ # A unified format across the token types is required for regex
+ # consistency.
+ message = message.encode() if isinstance(message, str) else message
+ prefix = prefix.encode() if isinstance(prefix, str) else prefix
+
+ if not self.database:
+ return message
+
+ result = message
+ for _ in range(recursion - 1):
+ result = _token_regex(prefix).sub(self._detokenize_scan, result)
+
+ if result == message:
+ return result
+ return result
- if recursion > 0 and original != result:
- result = self.detokenize_base64(
- result, prefix, recursion - 1
- )
+ def _detokenize_scan(self, match: Match[bytes]) -> bytes:
+ """Decodes prefixed tokens for one of multiple formats."""
+ basespec = match.group('basespec')
+ base = match.group('base')
- return result
- except binascii.Error:
- pass
+ if not basespec or (base == b'64'):
+ return self._detokenize_once_base64(match)
+ if not base:
+ base = b'16'
+
+ return self._detokenize_once(match, base)
+
+ def _detokenize_once(
+ self,
+ match: Match[bytes],
+ base: bytes,
+ ) -> bytes:
+ """Performs lookup on a plain token"""
+ original = match.group(0)
+ token = match.group('base' + base.decode())
+ if not token:
return original
- return decode_and_detokenize
+ token = int(token, int(base))
+ entries = self.database.token_to_entries[token]
+
+ if len(entries) == 1:
+ return str(entries[0]).encode()
+
+ # TODO(gschen): improve token collision reporting
+
+ return original
+
+ def _detokenize_once_base64(
+ self,
+ match: Match[bytes],
+ ) -> bytes:
+ """Performs lookup on a Base64 token"""
+ original = match.group(0)
+
+ try:
+ encoded_token = match.group('base64')
+ if not encoded_token:
+ return original
+
+ detokenized_string = self.detokenize(
+ base64.b64decode(encoded_token, validate=True), recursion=0
+ )
+
+ if detokenized_string.matches():
+ return str(detokenized_string).encode()
+
+ except binascii.Error:
+ pass
+
+ return original
_PathOrStr = Union[Path, str]
-# TODO(b/265334753): Reuse this function in database.py:LoadTokenDatabases
+# TODO: b/265334753 - Reuse this function in database.py:LoadTokenDatabases
def _parse_domain(path: _PathOrStr) -> Tuple[Path, Optional[Pattern[str]]]:
"""Extracts an optional domain regex pattern suffix from a path"""
@@ -364,6 +520,12 @@ class AutoUpdatingDetokenizer(Detokenizer):
return True
def _last_modified_time(self) -> Optional[float]:
+ if self.path.is_dir():
+ mtime = -1.0
+ for child in self.path.glob(tokens.DIR_DB_GLOB):
+ mtime = max(mtime, os.path.getmtime(child))
+ return mtime if mtime >= 0 else None
+
try:
return os.path.getmtime(self.path)
except FileNotFoundError:
@@ -380,119 +542,181 @@ class AutoUpdatingDetokenizer(Detokenizer):
return database.load_token_database()
def __init__(
- self, *paths_or_files: _PathOrStr, min_poll_period_s: float = 1.0
+ self,
+ *paths_or_files: _PathOrStr,
+ min_poll_period_s: float = 1.0,
+ pool: Executor = ThreadPoolExecutor(max_workers=1),
) -> None:
self.paths = tuple(self._DatabasePath(path) for path in paths_or_files)
self.min_poll_period_s = min_poll_period_s
self._last_checked_time: float = time.time()
+ # Thread pool to use for loading the databases. Limit to a single
+ # worker since this is low volume and not time critical.
+ self._pool = pool
super().__init__(*(path.load() for path in self.paths))
+ def __del__(self) -> None:
+ self._pool.shutdown(wait=False)
+
+ def _reload_paths(self) -> None:
+ self._initialize_database([path.load() for path in self.paths])
+
def _reload_if_changed(self) -> None:
if time.time() - self._last_checked_time >= self.min_poll_period_s:
self._last_checked_time = time.time()
if any(path.updated() for path in self.paths):
_LOG.info('Changes detected; reloading token database')
- self._initialize_database(path.load() for path in self.paths)
+ self._pool.submit(self._reload_paths)
def lookup(self, token: int) -> List[_TokenizedFormatString]:
self._reload_if_changed()
return super().lookup(token)
-class PrefixedMessageDecoder:
- """Parses messages that start with a prefix character from a byte stream."""
+class NestedMessageParser:
+ """Parses nested tokenized messages from a byte stream or string."""
- def __init__(self, prefix: Union[str, bytes], chars: Union[str, bytes]):
- """Parses prefixed messages.
+ class _State(enum.Enum):
+ MESSAGE = 1
+ NON_MESSAGE = 2
+
+ def __init__(
+ self,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
+ chars: Union[str, bytes] = _BASE64_CHARS,
+ ) -> None:
+ """Initializes a parser.
Args:
- prefix: one character that signifies the start of a message
- chars: characters allowed in a message
+ prefix: one character that signifies the start of a message (``$``).
+ chars: characters allowed in a message
"""
- self._prefix = prefix.encode() if isinstance(prefix, str) else prefix
+ self._prefix = ord(prefix)
if isinstance(chars, str):
chars = chars.encode()
- # Store the valid message bytes as a set of binary strings.
- self._message_bytes = frozenset(
- chars[i : i + 1] for i in range(len(chars))
- )
+ # Store the valid message bytes as a set of byte values.
+ self._message_bytes = frozenset(chars)
- if len(self._prefix) != 1 or self._prefix in self._message_bytes:
+ if len(prefix) != 1 or self._prefix in self._message_bytes:
raise ValueError(
- 'Invalid prefix {!r}: the prefix must be a single '
- 'character that is not a valid message character.'.format(
- prefix
- )
+ f'Invalid prefix {prefix!r}: the prefix must be a single '
+ 'character that is not a valid message character.'
)
- self.data = bytearray()
+ self._buffer = bytearray()
+ self._state: NestedMessageParser._State = self._State.NON_MESSAGE
- def _read_next(self, fd: _RawIO) -> Tuple[bytes, int]:
- """Returns the next character and its index."""
- char = fd.read(1) or b''
- index = len(self.data)
- self.data += char
- return char, index
+ def read_messages_io(
+ self, binary_io: _RawIo
+ ) -> Iterator[Tuple[bool, bytes]]:
+ """Reads prefixed messages from a byte stream (BinaryIO object).
- def read_messages(self, binary_fd: _RawIO) -> Iterator[Tuple[bool, bytes]]:
- """Parses prefixed messages; yields (is_message, contents) chunks."""
- message_start = None
+ Reads until EOF. If the stream is nonblocking (``read(1)`` returns
+ ``None``), then this function returns and may be called again with the
+ same IO object to continue parsing. Partial messages are preserved
+ between calls.
- while True:
- # This reads the file character-by-character. Non-message characters
- # are yielded right away; message characters are grouped.
- char, index = self._read_next(binary_fd)
+ Yields:
+ ``(is_message, contents)`` chunks.
+ """
+ # The read may block indefinitely, depending on the IO object.
+ while (read_byte := binary_io.read(1)) != b'':
+ # Handle non-blocking IO by returning when no bytes are available.
+ if read_byte is None:
+ return
- # If in a message, keep reading until the message completes.
- if message_start is not None:
- if char in self._message_bytes:
- continue
+ for byte in read_byte:
+ yield from self._handle_byte(byte)
- yield True, self.data[message_start:index]
- message_start = None
+ if self._state is self._State.NON_MESSAGE: # yield non-message byte
+ yield from self._flush()
- # Handle a non-message character.
- if not char:
- return
+ yield from self._flush() # Always flush after EOF
+ self._state = self._State.NON_MESSAGE
- if char == self._prefix:
- message_start = index
- else:
- yield False, char
+ def read_messages(
+ self, chunk: bytes, *, flush: bool = False
+ ) -> Iterator[Tuple[bool, bytes]]:
+ """Reads prefixed messages from a byte string.
- def transform(
- self, binary_fd: _RawIO, transform: Callable[[bytes], bytes]
+ This function may be called repeatedly with chunks of a stream. Partial
+ messages are preserved between calls, unless ``flush=True``.
+
+ Args:
+ chunk: byte string that may contain nested messagses
+ flush: whether to flush any incomplete messages after processing
+ this chunk
+
+ Yields:
+ ``(is_message, contents)`` chunks.
+ """
+ for byte in chunk:
+ yield from self._handle_byte(byte)
+
+ if flush or self._state is self._State.NON_MESSAGE:
+ yield from self._flush()
+
+ def _handle_byte(self, byte: int) -> Iterator[Tuple[bool, bytes]]:
+ if self._state is self._State.MESSAGE:
+ if byte not in self._message_bytes:
+ yield from self._flush()
+ if byte != self._prefix:
+ self._state = self._State.NON_MESSAGE
+ elif self._state is self._State.NON_MESSAGE:
+ if byte == self._prefix:
+ yield from self._flush()
+ self._state = self._State.MESSAGE
+ else:
+ raise NotImplementedError(f'Unsupported state: {self._state}')
+
+ self._buffer.append(byte)
+
+ def _flush(self) -> Iterator[Tuple[bool, bytes]]:
+ data = bytes(self._buffer)
+ self._buffer.clear()
+ if data:
+ yield self._state is self._State.MESSAGE, data
+
+ def transform_io(
+ self,
+ binary_io: _RawIo,
+ transform: Callable[[bytes], bytes],
) -> Iterator[bytes]:
"""Yields the file with a transformation applied to the messages."""
- for is_message, chunk in self.read_messages(binary_fd):
+ for is_message, chunk in self.read_messages_io(binary_io):
yield transform(chunk) if is_message else chunk
-
-def _base64_message_regex(prefix: bytes) -> Pattern[bytes]:
- """Returns a regular expression for prefixed base64 tokenized strings."""
- return re.compile(
- # Base64 tokenized strings start with the prefix character ($)
- re.escape(prefix)
- + (
- # Tokenized strings contain 0 or more blocks of four Base64 chars.
- br'(?:[A-Za-z0-9+/\-_]{4})*'
- # The last block of 4 chars may have one or two padding chars (=).
- br'(?:[A-Za-z0-9+/\-_]{3}=|[A-Za-z0-9+/\-_]{2}==)?'
+ def transform(
+ self,
+ chunk: bytes,
+ transform: Callable[[bytes], bytes],
+ *,
+ flush: bool = False,
+ ) -> bytes:
+ """Yields the chunk with a transformation applied to the messages.
+
+ Partial messages are preserved between calls unless ``flush=True``.
+ """
+ return b''.join(
+ transform(data) if is_message else data
+ for is_message, data in self.read_messages(chunk, flush=flush)
)
- )
# TODO(hepler): Remove this unnecessary function.
def detokenize_base64(
detokenizer: Detokenizer,
data: bytes,
- prefix: Union[str, bytes] = BASE64_PREFIX,
+ prefix: Union[str, bytes] = NESTED_TOKEN_PREFIX,
recursion: int = DEFAULT_RECURSION,
) -> bytes:
- """Alias for detokenizer.detokenize_base64 for backwards compatibility."""
+ """Alias for detokenizer.detokenize_base64 for backwards compatibility.
+
+ This function is deprecated; do not call it.
+ """
return detokenizer.detokenize_base64(data, prefix, recursion)
@@ -596,10 +820,10 @@ def _parse_args() -> argparse.Namespace:
subparser.add_argument(
'-p',
'--prefix',
- default=BASE64_PREFIX,
+ default=NESTED_TOKEN_PREFIX,
help=(
'The one-character prefix that signals the start of a '
- 'Base64-encoded message. (default: $)'
+ 'nested tokenized message. (default: $)'
),
)
subparser.add_argument(
diff --git a/pw_tokenizer/py/pw_tokenizer/encode.py b/pw_tokenizer/py/pw_tokenizer/encode.py
index 5b8583256..f47e0ec27 100644
--- a/pw_tokenizer/py/pw_tokenizer/encode.py
+++ b/pw_tokenizer/py/pw_tokenizer/encode.py
@@ -23,7 +23,8 @@ from pw_tokenizer import tokens
_INT32_MAX = 2**31 - 1
_UINT32_MAX = 2**32 - 1
-BASE64_PREFIX = '$'
+NESTED_TOKEN_PREFIX = '$'
+NESTED_TOKEN_BASE_PREFIX = '#'
def _zig_zag_encode(value: int) -> int:
diff --git a/pw_tokenizer/py/pw_tokenizer/proto/__init__.py b/pw_tokenizer/py/pw_tokenizer/proto/__init__.py
index da11d5e5e..7e54835d2 100644
--- a/pw_tokenizer/py/pw_tokenizer/proto/__init__.py
+++ b/pw_tokenizer/py/pw_tokenizer/proto/__init__.py
@@ -36,7 +36,7 @@ def _tokenized_fields(proto: Message) -> Iterator[FieldDescriptor]:
def decode_optionally_tokenized(
detokenizer: detokenize.Detokenizer,
data: bytes,
- prefix: str = encode.BASE64_PREFIX,
+ prefix: str = encode.NESTED_TOKEN_PREFIX,
) -> str:
"""Decodes data that may be plain text or binary / Base64 tokenized text."""
# Try detokenizing as binary.
@@ -70,7 +70,7 @@ def decode_optionally_tokenized(
def detokenize_fields(
detokenizer: detokenize.Detokenizer,
proto: Message,
- prefix: str = encode.BASE64_PREFIX,
+ prefix: str = encode.NESTED_TOKEN_PREFIX,
) -> None:
"""Detokenizes fields annotated as tokenized in the given proto.
diff --git a/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py b/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
index ab673a55e..79dda3a5e 100644
--- a/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
+++ b/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
@@ -59,7 +59,7 @@ def _parse_args():
parser.add_argument(
'-p',
'--prefix',
- default=detokenize.BASE64_PREFIX,
+ default=detokenize.NESTED_TOKEN_PREFIX,
help=(
'The one-character prefix that signals the start of a '
'Base64-encoded message. (default: $)'
diff --git a/pw_tokenizer/py/pw_tokenizer/tokens.py b/pw_tokenizer/py/pw_tokenizer/tokens.py
index b7ebac87c..fa9339cdf 100644
--- a/pw_tokenizer/py/pw_tokenizer/tokens.py
+++ b/pw_tokenizer/py/pw_tokenizer/tokens.py
@@ -575,7 +575,7 @@ class _BinaryDatabase(DatabaseFile):
def add_and_discard_temporary(
self, entries: Iterable[TokenizedStringEntry], commit: str
) -> None:
- # TODO(b/241471465): Implement adding new tokens and removing
+ # TODO: b/241471465 - Implement adding new tokens and removing
# temporary entries for binary databases.
raise NotImplementedError(
'--discard-temporary is currently only '
@@ -597,7 +597,7 @@ class _CSVDatabase(DatabaseFile):
def add_and_discard_temporary(
self, entries: Iterable[TokenizedStringEntry], commit: str
) -> None:
- # TODO(b/241471465): Implement adding new tokens and removing
+ # TODO: b/241471465 - Implement adding new tokens and removing
# temporary entries for CSV databases.
raise NotImplementedError(
'--discard-temporary is currently only '
@@ -607,12 +607,12 @@ class _CSVDatabase(DatabaseFile):
# The suffix used for CSV files in a directory database.
DIR_DB_SUFFIX = '.pw_tokenizer.csv'
-_DIR_DB_GLOB = '*' + DIR_DB_SUFFIX
+DIR_DB_GLOB = '*' + DIR_DB_SUFFIX
def _parse_directory(directory: Path) -> Iterable[TokenizedStringEntry]:
"""Parses TokenizedStringEntries tokenizer CSV files in the directory."""
- for path in directory.glob(_DIR_DB_GLOB):
+ for path in directory.glob(DIR_DB_GLOB):
yield from _CSVDatabase(path).entries()
@@ -633,7 +633,7 @@ class _DirectoryDatabase(DatabaseFile):
write_csv(self, fd)
# Delete all CSV files except for the new CSV with everything.
- for csv_file in self.path.glob(_DIR_DB_GLOB):
+ for csv_file in self.path.glob(DIR_DB_GLOB):
if csv_file != new_file:
csv_file.unlink()
else:
@@ -648,7 +648,7 @@ class _DirectoryDatabase(DatabaseFile):
"""Returns a list of files from a Git command, filtered to matc."""
try:
output = subprocess.run(
- ['git', *commands, _DIR_DB_GLOB],
+ ['git', *commands, DIR_DB_GLOB],
capture_output=True,
check=True,
cwd=self.path,
diff --git a/pw_tokenizer/rust/BUILD.bazel b/pw_tokenizer/rust/BUILD.bazel
new file mode 100644
index 000000000..862f01149
--- /dev/null
+++ b/pw_tokenizer/rust/BUILD.bazel
@@ -0,0 +1,90 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_doc_test", "rust_library", "rust_proc_macro", "rust_test")
+
+rust_proc_macro(
+ name = "pw_tokenizer_macro",
+ srcs = [
+ "pw_tokenizer_macro.rs",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":pw_tokenizer_core",
+ "//pw_format/rust:pw_format",
+ "//pw_status/rust:pw_status",
+ "@rust_crates//:proc-macro2",
+ "@rust_crates//:quote",
+ "@rust_crates//:syn",
+ ],
+)
+
+rust_test(
+ name = "pw_tokenizer_macro_test",
+ crate = ":pw_tokenizer_macro",
+)
+
+rust_library(
+ name = "pw_tokenizer_core",
+ srcs = [
+ "pw_tokenizer_core.rs",
+ "pw_tokenizer_core_test_cases.rs",
+ ],
+ visibility = ["//visibility:public"],
+)
+
+rust_test(
+ name = "pw_tokenizer_core_test",
+ crate = ":pw_tokenizer_core",
+)
+
+rust_doc_test(
+ name = "pw_tokenizer_core_doc_test",
+ crate = ":pw_tokenizer_core",
+)
+
+rust_doc(
+ name = "pw_tokenizer_core_doc",
+ crate = ":pw_tokenizer_core",
+)
+
+rust_library(
+ name = "pw_tokenizer",
+ srcs = [
+ "pw_tokenizer/internal.rs",
+ "pw_tokenizer/lib.rs",
+ ],
+ proc_macro_deps = [":pw_tokenizer_macro"],
+ visibility = ["//visibility:public"],
+ deps = [
+ ":pw_tokenizer_core", # Added for rustdoc linking support.
+ "//pw_status/rust:pw_status",
+ "//pw_stream/rust:pw_stream",
+ ],
+)
+
+rust_test(
+ name = "pw_tokenizer_test",
+ crate = ":pw_tokenizer",
+)
+
+rust_doc_test(
+ name = "pw_tokenizer_doc_test",
+ crate = ":pw_tokenizer",
+)
+
+rust_doc(
+ name = "pw_tokenizer_doc",
+ crate = ":pw_tokenizer",
+)
diff --git a/pw_tokenizer/rust/pw_tokenizer/internal.rs b/pw_tokenizer/rust/pw_tokenizer/internal.rs
new file mode 100644
index 000000000..4b798b973
--- /dev/null
+++ b/pw_tokenizer/rust/pw_tokenizer/internal.rs
@@ -0,0 +1,79 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+use core::cmp::min;
+
+use pw_status::Result;
+use pw_stream::{Cursor, Write};
+
+pub fn encode_string(cursor: &mut Cursor<&mut [u8]>, value: &str) -> Result<()> {
+ const MAX_STRING_LENGTH: usize = 0x7f;
+
+ let string_bytes = value.as_bytes();
+
+ // Limit the encoding to the lesser of 127 or the available space in the buffer.
+ let max_len = min(MAX_STRING_LENGTH, cursor.remaining() - 1);
+ let overflow = max_len < string_bytes.len();
+ let len = min(max_len, string_bytes.len());
+
+ // First byte of an encoded string is it's length.
+ let mut header = len as u8;
+
+ // The high bit of the first byte is used to indicate if the string was
+ // truncated.
+ if overflow {
+ header |= 0x80;
+ }
+ cursor.write_all(&[header as u8])?;
+
+ cursor.write_all(&string_bytes[..len])
+}
+
+#[cfg(test)]
+mod test {
+ use pw_stream::{Cursor, Seek};
+
+ use super::encode_string;
+
+ fn do_string_encode_test<const BUFFER_LEN: usize>(value: &str, expected: &[u8]) {
+ let mut buffer = [0u8; BUFFER_LEN];
+ let mut cursor = Cursor::new(&mut buffer[..]);
+ encode_string(&mut cursor, value).unwrap();
+
+ let len = cursor.stream_position().unwrap() as usize;
+ let buffer = cursor.into_inner();
+
+ assert_eq!(len, expected.len());
+ assert_eq!(&buffer[..len], expected);
+ }
+
+ #[test]
+ fn test_string_encode() {
+ do_string_encode_test::<64>("test", b"\x04test");
+ do_string_encode_test::<4>("test", b"\x83tes");
+ do_string_encode_test::<1>("test", b"\x80");
+
+ // Truncates when the string does not fit.
+ do_string_encode_test::<64>(
+ "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest",
+ b"\xbftesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttes",
+ );
+
+ // Truncates when string is over 127 bytes.
+ do_string_encode_test::<1024>(
+ "testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttest",
+ b"\xfftesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttesttes",
+ );
+ }
+}
diff --git a/pw_tokenizer/rust/pw_tokenizer/lib.rs b/pw_tokenizer/rust/pw_tokenizer/lib.rs
new file mode 100644
index 000000000..7909f124b
--- /dev/null
+++ b/pw_tokenizer/rust/pw_tokenizer/lib.rs
@@ -0,0 +1,277 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! `pw_tokenizer` - Efficient string handling and printf style encoding.
+//!
+//! Logging is critical, but developers are often forced to choose between
+//! additional logging or saving crucial flash space. The `pw_tokenizer` crate
+//! helps address this by replacing printf-style strings with binary tokens
+//! during compilation. This enables extensive logging with substantially less
+//! memory usage.
+//!
+//! For a more in depth explanation of the systems design and motivations,
+//! see [Pigweed's pw_tokenizer module documentation](https://pigweed.dev/pw_tokenizer/).
+//!
+//! # Example
+//!
+//! ```
+//! use pw_tokenizer::tokenize_to_buffer;
+//!
+//! # fn doctest() -> pw_status::Result<()> {
+//! let mut buffer = [0u8; 1024];
+//! let len = tokenize_to_buffer!(&mut buffer, "The answer is %d", 42)?;
+//!
+//! // 4 bytes used to encode the token and one to encode the value 42. This
+//! // is a **3.5x** reduction in size compared to the raw string!
+//! assert_eq!(len, 5);
+//! # Ok(())
+//! # }
+//! # doctest().unwrap();
+//! ```
+
+#![no_std]
+#![deny(missing_docs)]
+
+#[doc(hidden)]
+pub mod internal;
+
+#[doc(hidden)]
+// Creating a __private namespace allows us a way to get to the modules
+// we need from macros by doing:
+// use $crate::__private as __pw_tokenizer_crate;
+//
+// This is how proc macro generated code can reliably reference back to
+// `pw_tokenizer` while still allowing a user to import it under a different
+// name.
+pub mod __private {
+ pub use crate::*;
+ pub use pw_status::Result;
+ pub use pw_stream::{Cursor, Seek, WriteInteger, WriteVarint};
+ pub use pw_tokenizer_macro::{_token, _tokenize_to_buffer};
+}
+
+/// Return the [`u32`] token for the specified string and add it to the token
+/// database.
+///
+/// This is where the magic happens in `pw_tokenizer`! ... and by magic
+/// we mean hiding information in a special linker section that ends up in the
+/// final elf binary but does not get flashed to the device.
+///
+/// Two things are accomplished here:
+/// 1) The string is hashed into its stable `u32` token. This is the value that
+/// is returned from the macro.
+/// 2) A [token database entry](https://pigweed.dev/pw_tokenizer/design.html#binary-database-format)
+/// is generated, assigned to a unique static symbol, placed in a linker
+/// section named `pw_tokenizer.entries.<TOKEN_HASH>`. A
+/// [linker script](https://pigweed.googlesource.com/pigweed/pigweed/+/refs/heads/main/pw_tokenizer/pw_tokenizer_linker_sections.ld)
+/// is responsible for picking these symbols up and aggregating them into a
+/// single `.pw_tokenizer.entries` section in the final binary.
+///
+/// # Example
+/// ```
+/// use pw_tokenizer::token;
+///
+/// let token = token!("hello, \"world\"");
+/// assert_eq!(token, 3537412730);
+/// ```
+///
+/// Currently there is no support for encoding tokens to specific domains
+/// or with "fixed lengths" per [`pw_tokenizer_core::hash_bytes_fixed`].
+#[macro_export]
+macro_rules! token {
+ ($string:literal) => {{
+ $crate::__private::_token!($string)
+ }};
+}
+
+/// Tokenize a format string and arguments to an [`AsMut<u8>`] buffer and add
+/// the format string's token to the token database.
+///
+/// See [`token`] for an explanation on how strings are tokenized and entries
+/// are added to the token database.
+///
+/// Returns a [`pw_status::Result<usize>`] the number of bytes written to the buffer.
+///
+/// # Errors
+/// - [`pw_status::Error::OutOfRange`] - Buffer is not large enough to fit
+/// tokenized data.
+/// - [`pw_status::Error::InvalidArgument`] - Invalid buffer was provided.
+///
+/// # Example
+///
+/// ```
+/// use pw_tokenizer::tokenize_to_buffer;
+///
+/// # fn doctest() -> pw_status::Result<()> {
+/// let mut buffer = [0u8; 1024];
+/// let len = tokenize_to_buffer!(&mut buffer, "The answer is %d", 42)?;
+///
+/// // 4 bytes used to encode the token and one to encode the value 42.
+/// assert_eq!(len, 5);
+/// # Ok(())
+/// # }
+/// # doctest().unwrap();
+/// ```
+#[macro_export]
+macro_rules! tokenize_to_buffer {
+ ($buffer:expr, $format_string:literal) => {{
+ use $crate::__private as __pw_tokenizer_crate;
+ __pw_tokenizer_crate::_tokenize_to_buffer!($buffer, $format_string)
+ }};
+
+ ($buffer:expr, $format_string:literal, $($args:expr),*) => {{
+ use $crate::__private as __pw_tokenizer_crate;
+ __pw_tokenizer_crate::_tokenize_to_buffer!($buffer, $format_string, $($args),*)
+ }};
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ extern crate self as pw_tokenizer;
+
+ // This is not meant to be an exhaustive test of tokenization which is
+ // covered by `pw_tokenizer_core`'s unit tests. Rather, this is testing
+ // that the `tokenize!` macro connects to that correctly.
+ #[test]
+ fn test_token() {}
+
+ macro_rules! tokenize_to_buffer_test {
+ ($expected_data:expr, $buffer_len:expr, $fmt:expr) => {
+ {
+ let mut orig_buffer = [0u8; $buffer_len];
+ let buffer =
+ tokenize_to_buffer!(&mut orig_buffer, $fmt).unwrap();
+ let len = buffer.len();
+ assert_eq!(
+ &orig_buffer[..(($buffer_len) - len)],
+ $expected_data,
+ );
+ }
+ };
+
+ ($expected_data:expr, $buffer_len:expr, $fmt:expr, $($args:expr),*) => {
+ {
+ let mut buffer = [0u8; $buffer_len];
+ let len = tokenize_to_buffer!(&mut buffer, $fmt, $($args),*).unwrap();
+ assert_eq!(
+ &buffer[..len],
+ $expected_data,
+ );
+ }
+ };
+ }
+
+ #[test]
+ fn test_decimal_format() {
+ tokenize_to_buffer_test!(
+ &[0x52, 0x1c, 0xb0, 0x4c, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %d!",
+ 1
+ );
+
+ tokenize_to_buffer_test!(
+ &[0x36, 0xd0, 0xfb, 0x69, 0x1], // expected buffer
+ 64, // buffer size
+ "No! The answer is %d!",
+ -1
+ );
+
+ tokenize_to_buffer_test!(
+ &[0xa4, 0xad, 0x50, 0x54, 0x0], // expected buffer
+ 64, // buffer size
+ "I think you'll find that the answer is %d!",
+ 0
+ );
+ }
+
+ #[test]
+ fn test_misc_integer_format() {
+ // %d, %i, %o, %u, %x, %X all encode integers the same.
+ tokenize_to_buffer_test!(
+ &[0x52, 0x1c, 0xb0, 0x4c, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %d!",
+ 1
+ );
+
+ // Because %i is an alias for %d, it gets converted to a %d by the
+ // `pw_format` macro infrastructure.
+ tokenize_to_buffer_test!(
+ &[0x52, 0x1c, 0xb0, 0x4c, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %i!",
+ 1
+ );
+
+ tokenize_to_buffer_test!(
+ &[0x5d, 0x70, 0x12, 0xb4, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %o!",
+ 1u32
+ );
+
+ tokenize_to_buffer_test!(
+ &[0x63, 0x58, 0x5f, 0x8f, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %u!",
+ 1u32
+ );
+
+ tokenize_to_buffer_test!(
+ &[0x66, 0xcc, 0x05, 0x7d, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %x!",
+ 1u32
+ );
+
+ tokenize_to_buffer_test!(
+ &[0x46, 0x4c, 0x16, 0x96, 0x2], // expected buffer
+ 64, // buffer size
+ "The answer is %X!",
+ 1u32
+ );
+ }
+
+ #[test]
+ fn test_string_format() {
+ tokenize_to_buffer_test!(
+ b"\x25\xf6\x2e\x66\x07Pigweed", // expected buffer
+ 64, // buffer size
+ "Hello: %s!",
+ "Pigweed"
+ );
+ }
+
+ #[test]
+ fn test_string_format_overflow() {
+ tokenize_to_buffer_test!(
+ b"\x25\xf6\x2e\x66\x83Pig", // expected buffer
+ 8, // buffer size
+ "Hello: %s!",
+ "Pigweed"
+ );
+ }
+
+ #[test]
+ fn test_char_format() {
+ tokenize_to_buffer_test!(
+ &[0x2e, 0x52, 0xac, 0xe4, 0x50], // expected buffer
+ 64, // buffer size
+ "Hello: %cigweed",
+ "P".as_bytes()[0]
+ );
+ }
+}
diff --git a/pw_tokenizer/rust/pw_tokenizer_core.rs b/pw_tokenizer/rust/pw_tokenizer_core.rs
new file mode 100644
index 000000000..c8e550413
--- /dev/null
+++ b/pw_tokenizer/rust/pw_tokenizer_core.rs
@@ -0,0 +1,107 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! # pw_tokenizer_core
+//!
+//! This crate provides the core functionality of calculating a token hash
+//! for a string or byte sequence. This is intended to provide a minimal core
+//! for both the main `pw_tokenizer` and `pw_tokenizer_macro` crates. Users
+//! should prefer depending `pw_tokenizer` instead of this crate.
+
+use core::num::Wrapping;
+
+pub const HASH_CONSTANT: Wrapping<u32> = Wrapping(65599u32);
+
+/// Calculate the hash for a sequence of bytes.
+///
+/// ```
+/// use pw_tokenizer_core::hash_bytes;
+///
+/// let hash = hash_bytes(&[0x34, 0xd8, 0x3a, 0xbb, 0xf1, 0x0e, 0x07]);
+/// assert_eq!(hash, 0x9e624642);
+/// ```
+pub fn hash_bytes(bytes: &[u8]) -> u32 {
+ hash_bytes_fixed(bytes, bytes.len())
+}
+
+/// Calculate the hash for a sequence of bytes, examining at most `len` bytes.
+///
+/// ```
+/// use pw_tokenizer_core::hash_bytes_fixed;
+///
+/// let hash = hash_bytes_fixed(&[0x34, 0xd8, 0x3a, 0xbb, 0xf1, 0x0e, 0x07], 4);
+/// assert_eq!(hash, 0x92c5d2ac);
+/// ```
+pub fn hash_bytes_fixed(bytes: &[u8], len: usize) -> u32 {
+ // The length is hashed as if it were the first byte.
+ let mut hash = Wrapping(bytes.len() as u32);
+ let mut coefficient = HASH_CONSTANT;
+
+ // Fixed sized hashes are seeded with the total length of the slice
+ // but only hash over at most `len` bytes.
+ let bytes = if bytes.len() > len {
+ &bytes[0..len]
+ } else {
+ bytes
+ };
+
+ // Hash all of the bytes in the slice as u32s. Wrapping arithmetic
+ // is used intentionally as part of the hashing algorithm.
+ for b in bytes {
+ hash += coefficient * Wrapping(*b as u32);
+ coefficient *= HASH_CONSTANT;
+ }
+
+ hash.0
+}
+
+/// Calculate the hash for a string.
+///
+/// ```
+/// use pw_tokenizer_core::hash_string;
+///
+/// let hash = hash_string("I 💖 Pigweed");
+/// assert_eq!(hash, 0xe318d1b3);
+/// ```
+pub fn hash_string(s: &str) -> u32 {
+ hash_bytes(s.as_bytes())
+}
+
+pub const TOKENIZER_ENTRY_MAGIC: u32 = 0xBAA98DEE;
+
+#[cfg(test)]
+mod tests {
+ use super::hash_bytes_fixed;
+
+ struct TestCase {
+ string: &'static [u8],
+ hash_length: usize,
+ hash: u32,
+ }
+
+ // Generated file defines `test_cases()`.
+ include!("pw_tokenizer_core_test_cases.rs");
+
+ #[test]
+ fn hash() {
+ for test in test_cases() {
+ let hash = hash_bytes_fixed(test.string, test.hash_length);
+ assert_eq!(
+ hash, test.hash,
+ "{:08x} != {:08x} string: {:x?} hash_size: {}",
+ hash, test.hash, test.string, test.hash_length
+ );
+ }
+ }
+}
diff --git a/pw_tokenizer/rust/pw_tokenizer_core_test_cases.rs b/pw_tokenizer/rust/pw_tokenizer_core_test_cases.rs
new file mode 100644
index 000000000..215842506
--- /dev/null
+++ b/pw_tokenizer/rust/pw_tokenizer_core_test_cases.rs
@@ -0,0 +1,744 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// AUTOGENERATED - DO NOT EDIT
+//
+// This file was generated by generate_hash_test_data.py.
+// To make changes, update the script and run it to generate new files.
+
+fn test_cases() -> Vec<TestCase> {
+ vec![
+ TestCase{
+ string: b"",
+ hash_length: 80,
+ hash: 0,
+ },
+ TestCase{
+ string: b"",
+ hash_length: 96,
+ hash: 0,
+ },
+ TestCase{
+ string: b"",
+ hash_length: 128,
+ hash: 0,
+ },
+ TestCase{
+ string: b"\xa1",
+ hash_length: 80,
+ hash: 10561440,
+ },
+ TestCase{
+ string: b"\xa1",
+ hash_length: 96,
+ hash: 10561440,
+ },
+ TestCase{
+ string: b"\xa1",
+ hash_length: 128,
+ hash: 10561440,
+ },
+ TestCase{
+ string: b"\xff",
+ hash_length: 80,
+ hash: 16727746,
+ },
+ TestCase{
+ string: b"\xff",
+ hash_length: 96,
+ hash: 16727746,
+ },
+ TestCase{
+ string: b"\xff",
+ hash_length: 128,
+ hash: 16727746,
+ },
+ TestCase{
+ string: b"\x00",
+ hash_length: 80,
+ hash: 1,
+ },
+ TestCase{
+ string: b"\x00",
+ hash_length: 96,
+ hash: 1,
+ },
+ TestCase{
+ string: b"\x00",
+ hash_length: 128,
+ hash: 1,
+ },
+ TestCase{
+ string: b"\x00\x00",
+ hash_length: 80,
+ hash: 2,
+ },
+ TestCase{
+ string: b"\x00\x00",
+ hash_length: 96,
+ hash: 2,
+ },
+ TestCase{
+ string: b"\x00\x00",
+ hash_length: 128,
+ hash: 2,
+ },
+ TestCase{
+ string: b"a",
+ hash_length: 80,
+ hash: 6363104,
+ },
+ TestCase{
+ string: b"a",
+ hash_length: 96,
+ hash: 6363104,
+ },
+ TestCase{
+ string: b"a",
+ hash_length: 128,
+ hash: 6363104,
+ },
+ TestCase{
+ string: b"A",
+ hash_length: 80,
+ hash: 4263936,
+ },
+ TestCase{
+ string: b"A",
+ hash_length: 96,
+ hash: 4263936,
+ },
+ TestCase{
+ string: b"A",
+ hash_length: 128,
+ hash: 4263936,
+ },
+ TestCase{
+ string: b"hello, \"world\"",
+ hash_length: 80,
+ hash: 3537412730,
+ },
+ TestCase{
+ string: b"hello, \"world\"",
+ hash_length: 96,
+ hash: 3537412730,
+ },
+ TestCase{
+ string: b"hello, \"world\"",
+ hash_length: 128,
+ hash: 3537412730,
+ },
+ TestCase{
+ string: b"YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO",
+ hash_length: 80,
+ hash: 2035157304,
+ },
+ TestCase{
+ string: b"YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO",
+ hash_length: 96,
+ hash: 4222077672,
+ },
+ TestCase{
+ string: b"YOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYOYO",
+ hash_length: 128,
+ hash: 255790664,
+ },
+ TestCase{
+ string: b"4",
+ hash_length: 80,
+ hash: 3411149,
+ },
+ TestCase{
+ string: b"4",
+ hash_length: 96,
+ hash: 3411149,
+ },
+ TestCase{
+ string: b"4",
+ hash_length: 128,
+ hash: 3411149,
+ },
+ TestCase{
+ string: b"\xe0",
+ hash_length: 80,
+ hash: 14694177,
+ },
+ TestCase{
+ string: b"\xe0",
+ hash_length: 96,
+ hash: 14694177,
+ },
+ TestCase{
+ string: b"\xe0",
+ hash_length: 128,
+ hash: 14694177,
+ },
+ TestCase{
+ string: b"\x90\xb9",
+ hash_length: 80,
+ hash: 1537824683,
+ },
+ TestCase{
+ string: b"\x90\xb9",
+ hash_length: 96,
+ hash: 1537824683,
+ },
+ TestCase{
+ string: b"\x90\xb9",
+ hash_length: 128,
+ hash: 1537824683,
+ },
+ TestCase{
+ string: b"\x6a\xe7",
+ hash_length: 80,
+ hash: 1915361151,
+ },
+ TestCase{
+ string: b"\x6a\xe7",
+ hash_length: 96,
+ hash: 1915361151,
+ },
+ TestCase{
+ string: b"\x6a\xe7",
+ hash_length: 128,
+ hash: 1915361151,
+ },
+ TestCase{
+ string: b"dy0",
+ hash_length: 80,
+ hash: 4114649192,
+ },
+ TestCase{
+ string: b"dy0",
+ hash_length: 96,
+ hash: 4114649192,
+ },
+ TestCase{
+ string: b"dy0",
+ hash_length: 128,
+ hash: 4114649192,
+ },
+ TestCase{
+ string: b"\xc4\x18\x32",
+ hash_length: 80,
+ hash: 585787813,
+ },
+ TestCase{
+ string: b"\xc4\x18\x32",
+ hash_length: 96,
+ hash: 585787813,
+ },
+ TestCase{
+ string: b"\xc4\x18\x32",
+ hash_length: 128,
+ hash: 585787813,
+ },
+ TestCase{
+ string: b"\x1c\xfc\x28\x2b",
+ hash_length: 80,
+ hash: 704109799,
+ },
+ TestCase{
+ string: b"\x1c\xfc\x28\x2b",
+ hash_length: 96,
+ hash: 704109799,
+ },
+ TestCase{
+ string: b"\x1c\xfc\x28\x2b",
+ hash_length: 128,
+ hash: 704109799,
+ },
+ TestCase{
+ string: b"\xab\x96\x56\x70",
+ hash_length: 80,
+ hash: 2738614345,
+ },
+ TestCase{
+ string: b"\xab\x96\x56\x70",
+ hash_length: 96,
+ hash: 2738614345,
+ },
+ TestCase{
+ string: b"\xab\x96\x56\x70",
+ hash_length: 128,
+ hash: 2738614345,
+ },
+ TestCase{
+ string: b"\x18\x1e\x6e\x6a\x73",
+ hash_length: 80,
+ hash: 580554452,
+ },
+ TestCase{
+ string: b"\x18\x1e\x6e\x6a\x73",
+ hash_length: 96,
+ hash: 580554452,
+ },
+ TestCase{
+ string: b"\x18\x1e\x6e\x6a\x73",
+ hash_length: 128,
+ hash: 580554452,
+ },
+ TestCase{
+ string: b"\xde\xe5\xdf\x22\x00",
+ hash_length: 80,
+ hash: 4269181327,
+ },
+ TestCase{
+ string: b"\xde\xe5\xdf\x22\x00",
+ hash_length: 96,
+ hash: 4269181327,
+ },
+ TestCase{
+ string: b"\xde\xe5\xdf\x22\x00",
+ hash_length: 128,
+ hash: 4269181327,
+ },
+ TestCase{
+ string: b"\x59\xac\x64\x3b\xc7\x36",
+ hash_length: 80,
+ hash: 2461849503,
+ },
+ TestCase{
+ string: b"\x59\xac\x64\x3b\xc7\x36",
+ hash_length: 96,
+ hash: 2461849503,
+ },
+ TestCase{
+ string: b"\x59\xac\x64\x3b\xc7\x36",
+ hash_length: 128,
+ hash: 2461849503,
+ },
+ TestCase{
+ string: b"\xe1\xef\x87\x8d\xbc\xd7",
+ hash_length: 80,
+ hash: 2407518645,
+ },
+ TestCase{
+ string: b"\xe1\xef\x87\x8d\xbc\xd7",
+ hash_length: 96,
+ hash: 2407518645,
+ },
+ TestCase{
+ string: b"\xe1\xef\x87\x8d\xbc\xd7",
+ hash_length: 128,
+ hash: 2407518645,
+ },
+ TestCase{
+ string: b"\x34\xd8\x3a\xbb\xf1\x0e\x07",
+ hash_length: 80,
+ hash: 2657240642,
+ },
+ TestCase{
+ string: b"\x34\xd8\x3a\xbb\xf1\x0e\x07",
+ hash_length: 96,
+ hash: 2657240642,
+ },
+ TestCase{
+ string: b"\x34\xd8\x3a\xbb\xf1\x0e\x07",
+ hash_length: 128,
+ hash: 2657240642,
+ },
+ TestCase{
+ string: b"\xa2\x8e\xb6\x56\x83\xd2\x89",
+ hash_length: 80,
+ hash: 2016713689,
+ },
+ TestCase{
+ string: b"\xa2\x8e\xb6\x56\x83\xd2\x89",
+ hash_length: 96,
+ hash: 2016713689,
+ },
+ TestCase{
+ string: b"\xa2\x8e\xb6\x56\x83\xd2\x89",
+ hash_length: 128,
+ hash: 2016713689,
+ },
+ TestCase{
+ string: b"\x20\x3b\x66\x3f\x80\x8b\xd6\x9f",
+ hash_length: 80,
+ hash: 727179216,
+ },
+ TestCase{
+ string: b"\x20\x3b\x66\x3f\x80\x8b\xd6\x9f",
+ hash_length: 96,
+ hash: 727179216,
+ },
+ TestCase{
+ string: b"\x20\x3b\x66\x3f\x80\x8b\xd6\x9f",
+ hash_length: 128,
+ hash: 727179216,
+ },
+ TestCase{
+ string: b"\xe5\x15\xbf\x96\x52\xd8\x22\x72",
+ hash_length: 80,
+ hash: 110264805,
+ },
+ TestCase{
+ string: b"\xe5\x15\xbf\x96\x52\xd8\x22\x72",
+ hash_length: 96,
+ hash: 110264805,
+ },
+ TestCase{
+ string: b"\xe5\x15\xbf\x96\x52\xd8\x22\x72",
+ hash_length: 128,
+ hash: 110264805,
+ },
+ TestCase{
+ string: b"\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82",
+ hash_length: 80,
+ hash: 261914122,
+ },
+ TestCase{
+ string: b"\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82",
+ hash_length: 96,
+ hash: 261914122,
+ },
+ TestCase{
+ string: b"\x21\x5a\x75\x73\xf1\x70\xc1\x0e\x82",
+ hash_length: 128,
+ hash: 261914122,
+ },
+ TestCase{
+ string: b"\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40",
+ hash_length: 80,
+ hash: 1833718768,
+ },
+ TestCase{
+ string: b"\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40",
+ hash_length: 96,
+ hash: 1833718768,
+ },
+ TestCase{
+ string: b"\x37\x1b\xf3\x87\x5c\xd9\x94\xc6\x40",
+ hash_length: 128,
+ hash: 1833718768,
+ },
+ TestCase{
+ string: b"\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d",
+ hash_length: 80,
+ hash: 2326646568,
+ },
+ TestCase{
+ string: b"\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d",
+ hash_length: 96,
+ hash: 2326646568,
+ },
+ TestCase{
+ string: b"\x71\x48\x39\xc6\x53\x98\xfa\xc6\x54\x3d",
+ hash_length: 128,
+ hash: 2326646568,
+ },
+ TestCase{
+ string: b"\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5",
+ hash_length: 80,
+ hash: 2712532084,
+ },
+ TestCase{
+ string: b"\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5",
+ hash_length: 96,
+ hash: 2712532084,
+ },
+ TestCase{
+ string: b"\x82\x26\x3a\x43\x83\xcf\x86\x3d\x3b\xf5",
+ hash_length: 128,
+ hash: 2712532084,
+ },
+ TestCase{
+ string: b"\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24",
+ hash_length: 80,
+ hash: 544632964,
+ },
+ TestCase{
+ string: b"\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24",
+ hash_length: 96,
+ hash: 544632964,
+ },
+ TestCase{
+ string: b"\xde\x35\x78\x6e\x3f\x98\x61\x43\x53\x28\x24",
+ hash_length: 128,
+ hash: 544632964,
+ },
+ TestCase{
+ string: b"\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00",
+ hash_length: 80,
+ hash: 3878380686,
+ },
+ TestCase{
+ string: b"\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00",
+ hash_length: 96,
+ hash: 3878380686,
+ },
+ TestCase{
+ string: b"\x28\x5a\xef\x49\x5c\xfb\x43\x91\xdd\x27\x00",
+ hash_length: 128,
+ hash: 3878380686,
+ },
+ TestCase{
+ string: b"\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97",
+ hash_length: 80,
+ hash: 4053891765,
+ },
+ TestCase{
+ string: b"\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97",
+ hash_length: 96,
+ hash: 4053891765,
+ },
+ TestCase{
+ string: b"\xca\x45\x01\x88\x5d\xf2\x24\xa9\x78\xbf\x91\x97",
+ hash_length: 128,
+ hash: 4053891765,
+ },
+ TestCase{
+ string: b"\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59",
+ hash_length: 80,
+ hash: 2009683698,
+ },
+ TestCase{
+ string: b"\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59",
+ hash_length: 96,
+ hash: 2009683698,
+ },
+ TestCase{
+ string: b"\x43\xa1\xfb\x5c\x60\x89\xaf\x2b\xdb\xa9\xe5\x59",
+ hash_length: 128,
+ hash: 2009683698,
+ },
+ TestCase{
+ string: b"\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26",
+ hash_length: 80,
+ hash: 3862326851,
+ },
+ TestCase{
+ string: b"\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26",
+ hash_length: 96,
+ hash: 3862326851,
+ },
+ TestCase{
+ string: b"\xde\x1a\x80\x72\x19\x63\x71\x85\x6c\x53\x51\x7a\x26",
+ hash_length: 128,
+ hash: 3862326851,
+ },
+ TestCase{
+ string: b"\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e",
+ hash_length: 80,
+ hash: 2358079886,
+ },
+ TestCase{
+ string: b"\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e",
+ hash_length: 96,
+ hash: 2358079886,
+ },
+ TestCase{
+ string: b"\x59\x74\xd1\xa5\x70\x0b\xef\x7d\x45\xa9\xcc\xef\x1e",
+ hash_length: 128,
+ hash: 2358079886,
+ },
+ TestCase{
+ string: b"\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a",
+ hash_length: 80,
+ hash: 4215296608,
+ },
+ TestCase{
+ string: b"\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a",
+ hash_length: 96,
+ hash: 4215296608,
+ },
+ TestCase{
+ string: b"\xed\xf2\x0a\x96\x1e\xec\x9e\xda\x71\xba\x60\x4d\x49\x8a",
+ hash_length: 128,
+ hash: 4215296608,
+ },
+ TestCase{
+ string: b"\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8",
+ hash_length: 80,
+ hash: 1051337960,
+ },
+ TestCase{
+ string: b"\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8",
+ hash_length: 96,
+ hash: 1051337960,
+ },
+ TestCase{
+ string: b"\x89\x5f\xe4\x0a\xfb\x75\xff\x6a\x24\x1c\x06\xec\xad\xc8",
+ hash_length: 128,
+ hash: 1051337960,
+ },
+ TestCase{
+ string: b"\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a",
+ hash_length: 80,
+ hash: 3916582129,
+ },
+ TestCase{
+ string: b"\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a",
+ hash_length: 96,
+ hash: 3916582129,
+ },
+ TestCase{
+ string: b"\x63\xe4\xd0\xdd\xf5\x83\xdb\xa4\x6b\x25\xc2\x2f\x8e\xfe\x1a",
+ hash_length: 128,
+ hash: 3916582129,
+ },
+ TestCase{
+ string: b"\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76",
+ hash_length: 80,
+ hash: 2665036172,
+ },
+ TestCase{
+ string: b"\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76",
+ hash_length: 96,
+ hash: 2665036172,
+ },
+ TestCase{
+ string: b"\x12\xbf\x0e\x44\x14\x1d\x31\x3c\x77\x5e\xf9\xa4\x98\x42\x76",
+ hash_length: 128,
+ hash: 2665036172,
+ },
+ TestCase{
+ string: b"\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb",
+ hash_length: 80,
+ hash: 2352453932,
+ },
+ TestCase{
+ string: b"\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb",
+ hash_length: 96,
+ hash: 2352453932,
+ },
+ TestCase{
+ string: b"\x97\xec\x22\xd5\x2d\xdb\xd7\x6a\xd7\x80\xae\xd1\x68\x3a\xca\xbe\x6b\x3c\xbb\x1b\x68\xca\xb4\xde\xa0\xb3\x7f\x80\x44\xd2\xa3\xe9\x80\x56\x06\xb9\xe4\xeb\xde\xe2\x9f\xc5\xcd\xc0\x21\x19\x21\x77\xdc\x38\xf9\x6c\xdb\x00\x64\x46\x40\xfa\x29\xd4\x9c\x87\x3e\x80\xd6\xbe\x4e\xed\x12\x54\xa9\x38\xe3\xff\x6f\x79\x30\xd7\xeb",
+ hash_length: 128,
+ hash: 2352453932,
+ },
+ TestCase{
+ string: b"\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13",
+ hash_length: 80,
+ hash: 4169625832,
+ },
+ TestCase{
+ string: b"\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13",
+ hash_length: 96,
+ hash: 4169625832,
+ },
+ TestCase{
+ string: b"\x4a\x94\x21\xd0\xe7\xa0\xd4\x23\x23\x38\xc1\x04\xce\xbb\xc9\xe6\xcc\x08\x90\x33\x7b\x0a\x28\x31\xf9\xe3\xe6\xc9\x78\x47\x10\x2c\xda\xb3\xf8\xcd\x7a\x20\xb7\xd1\xd6\x4c\xba\x18\x31\x44\x57\x08\x97\x89\xfd\x43\xce\xf2\x06\x67\xa1\x6d\x15\x47\xa1\xe1\x52\xf6\x4a\x9e\x79\x31\xae\x12\xae\xdd\x4b\x3c\x20\xba\xce\x50\x1e\x13",
+ hash_length: 128,
+ hash: 4169625832,
+ },
+ TestCase{
+ string: b"\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e",
+ hash_length: 80,
+ hash: 2417296923,
+ },
+ TestCase{
+ string: b"\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e",
+ hash_length: 96,
+ hash: 987115853,
+ },
+ TestCase{
+ string: b"\x79\x1d\xba\x71\x02\x36\xfd\xaf\xbe\x49\x5e\x0b\x77\x7b\x57\xf7\x8b\xad\x6a\xe3\xc5\x57\x5a\x34\xa6\x12\xb2\xb2\x8a\x4e\x11\x13\xa5\x97\x2f\xf6\xbc\x62\xdb\x63\x0b\xa4\xc3\x3d\x66\x92\x3e\x8b\x53\x47\x12\x3a\x36\x9a\xe2\x31\xf9\x0d\x62\x71\x79\x3f\xa7\x04\x09\x8c\x40\xa7\x8a\x17\x3a\xb3\x6a\xea\x51\xdf\x91\x5a\x1d\x42\x4e",
+ hash_length: 128,
+ hash: 987115853,
+ },
+ TestCase{
+ string: b"\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17",
+ hash_length: 80,
+ hash: 1750895817,
+ },
+ TestCase{
+ string: b"\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17",
+ hash_length: 96,
+ hash: 720276802,
+ },
+ TestCase{
+ string: b"\x08\xd5\x5f\x9b\x1a\xd5\x15\x4b\x80\x3f\x01\x35\x6f\xda\xf3\x9a\x2d\x8d\xb6\xb2\x36\x8b\xc4\x69\x46\xfe\xe1\x3f\x83\xbc\x45\xc8\x53\x75\xf5\x89\x22\x8b\x14\xfa\xd0\xce\xc9\x85\xe8\x98\x6b\x47\xc4\xa5\xf9\x06\x4c\x39\xdc\x8c\xe2\xf1\xa4\x59\x1c\xc1\xd4\x16\xb1\xb4\x2a\x61\x2c\x48\x2c\x7f\xd2\x1f\x77\xd2\x92\xf9\xfa\x84\x8c\x74\xc7\xa1\x3d\x72\x46\x97\x63\xc1\x97\x9a\x4b\xb2\x17",
+ hash_length: 128,
+ hash: 720276802,
+ },
+ TestCase{
+ string: b"\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b",
+ hash_length: 80,
+ hash: 760136888,
+ },
+ TestCase{
+ string: b"\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b",
+ hash_length: 96,
+ hash: 1408671026,
+ },
+ TestCase{
+ string: b"\x9b\xf2\x2d\xc5\x5f\xe7\xa6\xf3\xf6\xd8\x2c\x7f\x89\x72\x1d\xba\x88\x1a\x84\xb1\x7b\xad\x24\x96\x31\x80\x10\x2b\x1f\x32\x06\xc8\xef\x00\x5a\xe2\x9c\xfc\x3a\x6f\x5d\x70\xc0\x06\xe0\x8b\xcd\xd5\xec\xf4\x25\x91\xd9\xe4\x86\x4f\x3a\xdb\x36\x42\xde\x57\x8d\x5b\xeb\xd3\x67\x47\x99\x0b\x1b\x26\xd1\x06\x93\x5e\xa2\xf9\xc3\x28\x2e\x51\xed\x99\x12\x84\xd8\x79\x85\x12\x16\xde\x1d\xdc\x47\x4b",
+ hash_length: 128,
+ hash: 1408671026,
+ },
+ TestCase{
+ string: b"\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7",
+ hash_length: 80,
+ hash: 4113347769,
+ },
+ TestCase{
+ string: b"\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7",
+ hash_length: 96,
+ hash: 1367119804,
+ },
+ TestCase{
+ string: b"\xa7\x97\xb4\x6c\x4b\x3e\xa9\x40\x2d\x1c\x46\xd6\x42\xf7\xee\xd3\xc4\xa7\xa8\xbd\xd3\xe0\x1a\x56\x31\x4e\x9b\xbd\x28\x16\x56\x1f\x38\x57\x56\x8b\x7d\xa4\xc9\xe4\xb2\xce\x3c\xf8\x0f\x13\x83\x35\x66\x86\xdf\x33\xfa\x6e\x09\xf7\x3c\x05\xd9\x05\xb3\xb6\x62\xc2\xd9\x75\x00\x7f\x00\xd9\x2c\x67\x78\x8c\x4c\x45\x3b\x9b\xc7\xaf\x6e\xdf\x23\x79\x09\xa4\xbb\x29\x29\x64\xd4\xc7\x2c\x50\x83\x24\xc7",
+ hash_length: 128,
+ hash: 687960245,
+ },
+ TestCase{
+ string: b"\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98",
+ hash_length: 80,
+ hash: 1288060573,
+ },
+ TestCase{
+ string: b"\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98",
+ hash_length: 96,
+ hash: 1810369278,
+ },
+ TestCase{
+ string: b"\xf1\xdf\xb7\x7c\xcb\xf7\xc8\xe8\x94\xd3\x04\x33\x3e\x1a\x9c\x7d\x4a\xa4\xc3\xaf\x87\x97\xdb\xcc\xd6\x23\xe5\x0d\xf2\xa9\x9e\x63\xf6\xc4\xa8\x4d\x56\x86\x2b\x9c\x5f\xca\x4c\x17\x88\xa4\x5c\x73\x80\x6b\x2c\x03\x0e\xcd\x9f\xb8\x99\x44\x40\x2a\x33\x73\x94\xd1\x3f\x64\x56\x76\xf1\x9f\xfc\xb4\x1b\x1c\xa7\xc8\x28\x90\x03\x21\xe1\xcf\xb4\x57\x14\x2b\x5f\xbb\x84\x99\x4f\x16\xd4\x25\xe4\x54\x6f\xcd\x2c\x0f\x70\x98\xdb\xf3\xb0\xfe\xe0\x1a\x2e\x3d\x8b\x26\xdf\x28\x41\x16\x21\xc4\x86\x43\x9a\x29\x2b\xa3\x18\x74\x98",
+ hash_length: 128,
+ hash: 2429195322,
+ },
+ TestCase{
+ string: b"\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65",
+ hash_length: 80,
+ hash: 1125261758,
+ },
+ TestCase{
+ string: b"\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65",
+ hash_length: 96,
+ hash: 1477867120,
+ },
+ TestCase{
+ string: b"\x43\xd7\x5e\xff\x9b\x29\x34\x97\x50\x92\xb3\xcd\x38\xee\x3c\xdf\xc7\x7a\x76\x4e\x72\xaf\xb2\xdd\x37\x1c\x8e\x1c\xec\x08\x15\x72\x4f\xac\x2d\x67\x19\xc1\xc5\x6f\x52\x7d\x79\xe9\xa8\x3d\xcb\x3b\xdb\x4d\x29\x81\x89\xa8\x1f\xcb\xbf\xc6\x2f\x5a\xab\x9e\x05\xc6\xec\x42\x24\xf0\x9e\x2c\xb4\x7e\xc1\x85\x10\x3c\xc0\xd5\x99\x9a\x62\x52\x04\xde\xe1\xd1\x44\x5e\x4b\x4b\xc1\x10\xf1\xbe\x06\xc8\xf7\x07\xd2\x92\xc8\x92\xb3\xb6\xa5\x79\x6e\x93\x6d\xfa\xd1\x68\x6e\xec\x25\x10\xcf\x4f\x8e\xd4\xfb\x3c\x56\x04\xbc\x6f\xbc\x65",
+ hash_length: 128,
+ hash: 3694995364,
+ },
+ TestCase{
+ string: b"\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1",
+ hash_length: 80,
+ hash: 6281856,
+ },
+ TestCase{
+ string: b"\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1",
+ hash_length: 96,
+ hash: 598421397,
+ },
+ TestCase{
+ string: b"\x23\x66\x8c\x45\x63\x79\xfb\x31\x74\x7c\xa5\xcc\x58\x09\xc6\x46\x13\x9e\xf1\x96\x66\xb5\x03\x3a\xaa\x03\x78\x6e\x93\xdb\x9e\x70\x74\x05\xaa\x30\xca\x00\xf3\xc3\xef\xd0\x2e\xb5\xc3\x6c\xbd\x7f\xbc\x41\xd2\x1a\x83\xec\x72\xb0\x8c\x35\x9e\xcf\x6f\x16\x90\x6b\xdb\xa5\x88\xc6\xdc\x05\x8c\xdf\x51\x99\xa7\xca\xa9\xe3\x59\x68\x20\xb2\xdd\x63\xab\xd9\x27\xec\xcb\x1a\x2c\xac\xed\x5d\x8c\x97\x04\xbe\x22\x76\xbb\x14\x3b\x5f\xff\xc3\x78\xe2\xed\x79\xea\xdf\xc4\x5a\x28\xf2\x7a\xcb\x20\x68\x7b\xdc\xbf\xf2\x77\x61\x56\x5a\xe1",
+ hash_length: 128,
+ hash: 1313299978,
+ },
+
+ ]
+}
diff --git a/pw_tokenizer/rust/pw_tokenizer_macro.rs b/pw_tokenizer/rust/pw_tokenizer_macro.rs
new file mode 100644
index 000000000..b271ec433
--- /dev/null
+++ b/pw_tokenizer/rust/pw_tokenizer_macro.rs
@@ -0,0 +1,214 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This proc macro crate is a private API for the `pw_tokenizer` crate.
+#![doc(hidden)]
+
+use std::ffi::CString;
+
+use proc_macro::TokenStream;
+use proc_macro2::Ident;
+use quote::{format_ident, quote};
+use syn::{
+ parse::{Parse, ParseStream},
+ parse_macro_input, Expr, LitStr, Token,
+};
+
+use pw_format::macros::{generate_printf, FormatAndArgs, PrintfFormatMacroGenerator, Result};
+use pw_tokenizer_core::{hash_string, TOKENIZER_ENTRY_MAGIC};
+
+type TokenStream2 = proc_macro2::TokenStream;
+
+// Handles tokenizing (hashing) `string` and adding it to the token database
+// with the specified `domain`. A detailed description of what's happening is
+// found in the docs for [`pw_tokenizer::token`] macro.
+fn token_backend(domain: &str, string: &str) -> TokenStream2 {
+ let hash = hash_string(string);
+
+ // Line number is omitted as getting that info requires an experimental API:
+ // https://doc.rust-lang.org/proc_macro/struct.Span.html#method.start
+ let ident = format_ident!("_PW_TOKENIZER_STRING_ENTRY_{:08X}", hash);
+
+ // pw_tokenizer is intended for use with ELF files only. Mach-O files (macOS
+ // executables) do not support section names longer than 16 characters, so a
+ // short, unused section name is used on macOS.
+ let section = if cfg!(target_os = "macos") {
+ ",pw,".to_string()
+ } else {
+ format!(".pw_tokenizer.entries.{:08X}", hash)
+ };
+
+ let string = CString::new(string).unwrap();
+ let string_bytes = string.as_bytes_with_nul();
+ let string_bytes_len = string_bytes.len();
+
+ let domain = CString::new(domain).unwrap();
+ let domain_bytes = domain.as_bytes_with_nul();
+ let domain_bytes_len = domain_bytes.len();
+
+ quote! {
+ // Use an inner scope to avoid identifier collision. Name mangling
+ // will disambiguate these in the symbol table.
+ {
+ #[repr(C, packed(1))]
+ struct TokenEntry {
+ magic: u32,
+ token: u32,
+ domain_size: u32,
+ string_length: u32,
+ domain: [u8; #domain_bytes_len],
+ string: [u8; #string_bytes_len],
+ };
+ // This is currently manually verified to be correct.
+ // TODO: b/287132907 - Add integration tests for token database.
+ #[link_section = #section ]
+ static #ident: TokenEntry = TokenEntry {
+ magic: #TOKENIZER_ENTRY_MAGIC,
+ token: #hash,
+ domain_size: #domain_bytes_len as u32,
+ string_length: #string_bytes_len as u32,
+ domain: [ #(#domain_bytes),* ],
+ string: [ #(#string_bytes),* ],
+ };
+
+ #hash
+ }
+ }
+}
+
+// Documented in `pw_tokenizer::token`.
+#[proc_macro]
+pub fn _token(tokens: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(tokens as LitStr);
+ token_backend("", &input.value()).into()
+}
+
+// Args to tokenize to buffer that are parsed according to the pattern:
+// ($buffer:expr, $format_string:literal, $($args:expr),*)
+#[derive(Debug)]
+struct TokenizeToBufferArgs {
+ buffer: Expr,
+ format_and_args: FormatAndArgs,
+}
+
+impl Parse for TokenizeToBufferArgs {
+ fn parse(input: ParseStream) -> syn::parse::Result<Self> {
+ let buffer: Expr = input.parse()?;
+ input.parse::<Token![,]>()?;
+ let format_and_args: FormatAndArgs = input.parse()?;
+
+ Ok(TokenizeToBufferArgs {
+ buffer,
+ format_and_args,
+ })
+ }
+}
+
+struct TokenizeToBufferGenerator<'a> {
+ domain: &'a str,
+ buffer: &'a Expr,
+ encoding_fragments: Vec<TokenStream2>,
+}
+
+impl<'a> TokenizeToBufferGenerator<'a> {
+ fn new(domain: &'a str, buffer: &'a Expr) -> Self {
+ Self {
+ domain,
+ buffer,
+ encoding_fragments: Vec::new(),
+ }
+ }
+}
+
+impl<'a> PrintfFormatMacroGenerator for TokenizeToBufferGenerator<'a> {
+ fn finalize(self, format_string: String) -> Result<TokenStream2> {
+ // Locally scoped aliases so we can refer to them in `quote!()`
+ let buffer = self.buffer;
+ let encoding_fragments = self.encoding_fragments;
+
+ // `token_backend` returns a `TokenStream2` which both inserts the
+ // string into the token database and returns the hash value.
+ let token = token_backend(self.domain, &format_string);
+
+ Ok(quote! {
+ {
+ // Wrapping code in an internal function to allow `?` to work in
+ // functions that don't return Results.
+ fn _pw_tokenizer_internal_encode(
+ buffer: &mut [u8],
+ token: u32
+ ) -> __pw_tokenizer_crate::Result<usize> {
+ // use pw_tokenizer's private re-export of these pw_stream bits to
+ // allow referencing with needing `pw_stream` in scope.
+ use __pw_tokenizer_crate::{Cursor, Seek, WriteInteger, WriteVarint};
+ let mut cursor = Cursor::new(buffer);
+ cursor.write_u32_le(&token)?;
+ #(#encoding_fragments);*;
+ Ok(cursor.stream_position()? as usize)
+ }
+ _pw_tokenizer_internal_encode(#buffer, #token)
+ }
+ })
+ }
+
+ fn string_fragment(&mut self, _string: &str) -> Result<()> {
+ // String fragments are encoded directly into the format string.
+ Ok(())
+ }
+
+ fn integer_conversion(&mut self, ty: Ident, expression: Expr) -> Result<Option<String>> {
+ self.encoding_fragments.push(quote! {
+ // pw_tokenizer always uses signed packing for all integers.
+ cursor.write_signed_varint(#ty::from(#expression) as i64)?;
+ });
+
+ Ok(None)
+ }
+
+ fn string_conversion(&mut self, expression: Expr) -> Result<Option<String>> {
+ self.encoding_fragments.push(quote! {
+ __pw_tokenizer_crate::internal::encode_string(&mut cursor, #expression)?;
+ });
+ Ok(None)
+ }
+
+ fn char_conversion(&mut self, expression: Expr) -> Result<Option<String>> {
+ self.encoding_fragments.push(quote! {
+ cursor.write_u8_le(&u8::from(#expression))?;
+ });
+ Ok(None)
+ }
+}
+
+// Generates code to marshal a tokenized string and arguments into a buffer.
+// See [`pw_tokenizer::tokenize_to_buffer`] for details on behavior.
+//
+// Internally the [`AsMut<u8>`] is wrapped in a [`pw_stream::Cursor`] to
+// fill the buffer incrementally.
+#[proc_macro]
+pub fn _tokenize_to_buffer(tokens: TokenStream) -> TokenStream {
+ let input = parse_macro_input!(tokens as TokenizeToBufferArgs);
+
+ // Hard codes domain to "".
+ let generator = TokenizeToBufferGenerator::new("", &input.buffer);
+
+ match generate_printf(generator, input.format_and_args) {
+ Ok(token_stream) => token_stream.into(),
+ Err(e) => e.to_compile_error().into(),
+ }
+}
+
+// Macros tested in `pw_tokenizer` crate.
+#[cfg(test)]
+mod tests {}
diff --git a/pw_tokenizer/token_database.cc b/pw_tokenizer/token_database.cc
index 42c145b67..7ab5d6b28 100644
--- a/pw_tokenizer/token_database.cc
+++ b/pw_tokenizer/token_database.cc
@@ -17,20 +17,20 @@
namespace pw::tokenizer {
TokenDatabase::Entry TokenDatabase::Entries::operator[](size_t index) const {
- Iterator it = begin();
+ iterator it = begin();
for (size_t i = 0; i < index; ++i) {
++it;
}
- return it.entry();
+ return *it;
}
TokenDatabase::Entries TokenDatabase::Find(const uint32_t token) const {
- Iterator first = begin();
+ iterator first = begin();
while (first != end() && token > first->token) {
++first;
}
- Iterator last = first;
+ iterator last = first;
while (last != end() && token == last->token) {
++last;
}
diff --git a/pw_tokenizer/token_database_fuzzer.cc b/pw_tokenizer/token_database_fuzzer.cc
index f73340a66..8385e40ca 100644
--- a/pw_tokenizer/token_database_fuzzer.cc
+++ b/pw_tokenizer/token_database_fuzzer.cc
@@ -66,7 +66,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
constexpr char kDefaultHeader[] = "TOKENS\0\0\0\0\0\0\0\0\0";
static uint8_t buffer[kBufferSizeMax];
- if (size > kFuzzDataSizeMax) {
+ if (!data || size == 0 || size > kFuzzDataSizeMax) {
return 0;
}
diff --git a/pw_tokenizer/token_database_test.cc b/pw_tokenizer/token_database_test.cc
index e06780fd0..3d343e9f9 100644
--- a/pw_tokenizer/token_database_test.cc
+++ b/pw_tokenizer/token_database_test.cc
@@ -27,7 +27,7 @@ using namespace std::literals::string_view_literals;
// Use alignas to ensure that the data is properly aligned for database entries.
// This avoids unaligned memory reads.
-alignas(TokenDatabase::RawEntry) constexpr char kBasicData[] =
+constexpr char kBasicData[] =
"TOKENS\0\0\x03\x00\x00\x00\0\0\0\0"
"\x01\0\0\0\0\0\0\0"
"\x02\0\0\0\0\0\0\0"
@@ -36,23 +36,21 @@ alignas(TokenDatabase::RawEntry) constexpr char kBasicData[] =
"goodbye\0"
":)";
-alignas(TokenDatabase::RawEntry) constexpr char kEmptyData[] =
+constexpr char kEmptyData[] =
"TOKENS\0\0\x00\x00\x00\x00\0\0\0"; // Last byte is null terminator.
-alignas(TokenDatabase::RawEntry) constexpr char kBadMagic[] =
+constexpr char kBadMagic[] =
"TOKENs\0\0\x03\x00\x00\x00\0\0\0\0"
"\x01\0\0\0\0\0\0\0"
"hi!\0";
-alignas(TokenDatabase::RawEntry) constexpr char kBadVersion[] =
- "TOKENS\0\1\x00\0\0\0\0\0\0\0";
+constexpr char kBadVersion[] = "TOKENS\0\1\x00\0\0\0\0\0\0\0";
-alignas(TokenDatabase::RawEntry) constexpr char kBadEntryCount[] =
- "TOKENS\0\0\xff\x00\x00\x00\0\0\0\0";
+constexpr char kBadEntryCount[] = "TOKENS\0\0\xff\x00\x00\x00\0\0\0\0";
// Use signed data and a size with the top bit set to test that the entry count
// is read correctly, without per-byte sign extension.
-alignas(TokenDatabase::RawEntry) constexpr signed char kSignedWithTopBit[] =
+constexpr signed char kSignedWithTopBit[] =
"TOKENS\0\0\x80\x00\x00\x00\0\0\0\0"
// Entries
"TOKNdateTOKNdateTOKNdateTOKNdateTOKNdateTOKNdateTOKNdateTOKNdate"
@@ -132,16 +130,16 @@ TEST(TokenDatabase, ValidCheck) {
TEST(TokenDatabase, Iterator) {
auto it = kBasicDatabase.begin();
EXPECT_EQ(it->token, 1u);
- EXPECT_STREQ(it.entry().string, "hi!");
+ EXPECT_STREQ(it->string, "hi!");
++it;
EXPECT_EQ(it->token, 2u);
- EXPECT_STREQ(it.entry().string, "goodbye");
+ EXPECT_STREQ(it->string, "goodbye");
EXPECT_EQ(it - kBasicDatabase.begin(), 1);
++it;
- EXPECT_EQ(it->token, 0xFFu);
- EXPECT_STREQ(it.entry().string, ":)");
+ EXPECT_EQ((*it).token, 0xFFu);
+ EXPECT_STREQ((*it).string, ":)");
EXPECT_EQ(it - kBasicDatabase.begin(), 2);
++it;
@@ -150,10 +148,33 @@ TEST(TokenDatabase, Iterator) {
kBasicDatabase.size());
}
+static_assert(
+ [] {
+ auto it1 = kBasicDatabase.begin();
+ auto it2 = it1;
+ ++it2;
+ return it1->token == 1u && it2->token == 2u;
+ }(),
+ "Iterators work in constant expression");
+
+static_assert(
+ [] {
+ constexpr uint32_t expected[3] = {1, 2, 0xff};
+
+ int i = 0;
+ for (const auto& entry : kBasicDatabase) {
+ if (entry.token != expected[i++]) {
+ return false;
+ }
+ }
+ return i == 3;
+ }(),
+ "Range based for loop iteration");
+
TEST(TokenDatabase, Iterator_PreIncrement) {
auto it = kBasicDatabase.begin();
EXPECT_EQ((++it)->token, 2u);
- EXPECT_STREQ(it.entry().string, "goodbye");
+ EXPECT_STREQ((*it).string, "goodbye");
}
TEST(TokenDatabase, Iterator_PostIncrement) {
@@ -161,7 +182,7 @@ TEST(TokenDatabase, Iterator_PostIncrement) {
EXPECT_EQ((it++)->token, 1u);
EXPECT_EQ(it->token, 2u);
- EXPECT_STREQ(it.entry().string, "goodbye");
+ EXPECT_STREQ((*it).string, "goodbye");
}
TEST(TokenDatabase, SingleEntryLookup_FirstEntry) {
@@ -210,7 +231,7 @@ TEST(TokenDatabase, SingleEntryLookup_NoMatches) {
}
}
-alignas(TokenDatabase::RawEntry) constexpr char kCollisionsData[] =
+constexpr char kCollisionsData[] =
"TOKENS\0\0\x05\0\0\0\0\0\0\0"
"\x01\0\0\0date"
"\x01\0\0\0date"
@@ -241,7 +262,8 @@ TEST(TokenDatabase, MultipleEntriesWithSameToken) {
TEST(TokenDatabase, Empty) {
constexpr TokenDatabase empty_db = TokenDatabase::Create<kEmptyData>();
static_assert(empty_db.size() == 0u);
- static_assert(empty_db.ok());
+ static_assert(empty_db.ok(), "Database has no entries, but is valid");
+ static_assert(empty_db.end() == empty_db.begin());
EXPECT_TRUE(empty_db.Find(0).empty());
EXPECT_TRUE(empty_db.Find(123).empty());
@@ -252,8 +274,8 @@ TEST(TokenDatabase, Empty) {
}
}
-TEST(TokenDatabase, NullDatabase) {
- constexpr TokenDatabase empty_db;
+TEST(TokenDatabase, DefaultConstructedDatabase) {
+ constexpr TokenDatabase empty_db; // No underlying data
static_assert(empty_db.size() == 0u);
static_assert(!empty_db.ok());
diff --git a/pw_tokenizer/token_databases.rst b/pw_tokenizer/token_databases.rst
new file mode 100644
index 000000000..dbd96511a
--- /dev/null
+++ b/pw_tokenizer/token_databases.rst
@@ -0,0 +1,310 @@
+.. _module-pw_tokenizer-token-databases:
+
+===============
+Token databases
+===============
+.. pigweed-module-subpage::
+ :name: pw_tokenizer
+ :tagline: Compress strings to shrink logs by +75%
+
+Token databases store a mapping of tokens to the strings they represent. An ELF
+file can be used as a token database, but it only contains the strings for its
+exact build. A token database file aggregates tokens from multiple ELF files, so
+that a single database can decode tokenized strings from any known ELF.
+
+Token databases contain the token, removal date (if any), and string for each
+tokenized string.
+
+----------------------
+Token database formats
+----------------------
+Three token database formats are supported: CSV, binary, and directory. Tokens
+may also be read from ELF files or ``.a`` archives, but cannot be written to
+these formats.
+
+CSV database format
+===================
+The CSV database format has three columns: the token in hexadecimal, the removal
+date (if any) in year-month-day format, and the string literal, surrounded by
+quotes. Quote characters within the string are represented as two quote
+characters.
+
+This example database contains six strings, three of which have removal dates.
+
+.. code-block::
+
+ 141c35d5, ,"The answer: ""%s"""
+ 2e668cd6,2019-12-25,"Jello, world!"
+ 7b940e2a, ,"Hello %s! %hd %e"
+ 851beeb6, ,"%u %d"
+ 881436a0,2020-01-01,"The answer is: %s"
+ e13b0f94,2020-04-01,"%llu"
+
+Binary database format
+======================
+The binary database format is comprised of a 16-byte header followed by a series
+of 8-byte entries. Each entry stores the token and the removal date, which is
+0xFFFFFFFF if there is none. The string literals are stored next in the same
+order as the entries. Strings are stored with null terminators. See
+`token_database.h <https://pigweed.googlesource.com/pigweed/pigweed/+/HEAD/pw_tokenizer/public/pw_tokenizer/token_database.h>`_
+for full details.
+
+The binary form of the CSV database is shown below. It contains the same
+information, but in a more compact and easily processed form. It takes 141 B
+compared with the CSV database's 211 B.
+
+.. code-block:: text
+
+ [header]
+ 0x00: 454b4f54 0000534e TOKENS..
+ 0x08: 00000006 00000000 ........
+
+ [entries]
+ 0x10: 141c35d5 ffffffff .5......
+ 0x18: 2e668cd6 07e30c19 ..f.....
+ 0x20: 7b940e2a ffffffff *..{....
+ 0x28: 851beeb6 ffffffff ........
+ 0x30: 881436a0 07e40101 .6......
+ 0x38: e13b0f94 07e40401 ..;.....
+
+ [string table]
+ 0x40: 54 68 65 20 61 6e 73 77 65 72 3a 20 22 25 73 22 The answer: "%s"
+ 0x50: 00 4a 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 00 48 .Jello, world!.H
+ 0x60: 65 6c 6c 6f 20 25 73 21 20 25 68 64 20 25 65 00 ello %s! %hd %e.
+ 0x70: 25 75 20 25 64 00 54 68 65 20 61 6e 73 77 65 72 %u %d.The answer
+ 0x80: 20 69 73 3a 20 25 73 00 25 6c 6c 75 00 is: %s.%llu.
+
+.. _module-pw_tokenizer-directory-database-format:
+
+Directory database format
+=========================
+pw_tokenizer can consume directories of CSV databases. A directory database
+will be searched recursively for files with a `.pw_tokenizer.csv` suffix, all
+of which will be used for subsequent detokenization lookups.
+
+An example directory database might look something like this:
+
+.. code-block:: text
+
+ token_database
+ ├── chuck_e_cheese.pw_tokenizer.csv
+ ├── fungi_ble.pw_tokenizer.csv
+ └── some_more
+ └── arcade.pw_tokenizer.csv
+
+This format is optimized for storage in a Git repository alongside source code.
+The token database commands randomly generate unique file names for the CSVs in
+the database to prevent merge conflicts. Running ``mark_removed`` or ``purge``
+commands in the database CLI consolidates the files to a single CSV.
+
+The database command line tool supports a ``--discard-temporary
+<upstream_commit>`` option for ``add``. In this mode, the tool attempts to
+discard temporary tokens. It identifies the latest CSV not present in the
+provided ``<upstream_commit>``, and tokens present that CSV that are not in the
+newly added tokens are discarded. This helps keep temporary tokens (e.g from
+debug logs) out of the database.
+
+JSON support
+============
+While pw_tokenizer doesn't specify a JSON database format, a token database can
+be created from a JSON formatted array of strings. This is useful for side-band
+token database generation for strings that are not embedded as parsable tokens
+in compiled binaries. See :ref:`module-pw_tokenizer-database-creation` for
+instructions on generating a token database from a JSON file.
+
+.. _module-pw_tokenizer-managing-token-databases:
+
+------------------------
+Managing token databases
+------------------------
+Token databases are managed with the ``database.py`` script. This script can be
+used to extract tokens from compilation artifacts and manage database files.
+Invoke ``database.py`` with ``-h`` for full usage information.
+
+An example ELF file with tokenized logs is provided at
+``pw_tokenizer/py/example_binary_with_tokenized_strings.elf``. You can use that
+file to experiment with the ``database.py`` commands.
+
+.. _module-pw_tokenizer-database-creation:
+
+Create a database
+=================
+The ``create`` command makes a new token database from ELF files (.elf, .o, .so,
+etc.), archives (.a), existing token databases (CSV or binary), or a JSON file
+containing an array of strings.
+
+.. code-block:: sh
+
+ ./database.py create --database DATABASE_NAME ELF_OR_DATABASE_FILE...
+
+Two database output formats are supported: CSV and binary. Provide
+``--type binary`` to ``create`` to generate a binary database instead of the
+default CSV. CSV databases are great for checking into a source control or for
+human review. Binary databases are more compact and simpler to parse. The C++
+detokenizer library only supports binary databases currently.
+
+.. _module-pw_tokenizer-update-token-database:
+
+Update a database
+=================
+As new tokenized strings are added, update the database with the ``add``
+command.
+
+.. code-block:: sh
+
+ ./database.py add --database DATABASE_NAME ELF_OR_DATABASE_FILE...
+
+This command adds new tokens from ELF files or other databases to the database.
+Adding tokens already present in the database updates the date removed, if any,
+to the latest.
+
+A CSV token database can be checked into a source repository and updated as code
+changes are made. The build system can invoke ``database.py`` to update the
+database after each build.
+
+GN integration
+==============
+Token databases may be updated or created as part of a GN build. The
+``pw_tokenizer_database`` template provided by
+``$dir_pw_tokenizer/database.gni`` automatically updates an in-source tokenized
+strings database or creates a new database with artifacts from one or more GN
+targets or other database files.
+
+To create a new database, set the ``create`` variable to the desired database
+type (``"csv"`` or ``"binary"``). The database will be created in the output
+directory. To update an existing database, provide the path to the database with
+the ``database`` variable.
+
+.. code-block::
+
+ import("//build_overrides/pigweed.gni")
+
+ import("$dir_pw_tokenizer/database.gni")
+
+ pw_tokenizer_database("my_database") {
+ database = "database_in_the_source_tree.csv"
+ targets = [ "//firmware/image:foo(//targets/my_board:some_toolchain)" ]
+ input_databases = [ "other_database.csv" ]
+ }
+
+Instead of specifying GN targets, paths or globs to output files may be provided
+with the ``paths`` option.
+
+.. code-block::
+
+ pw_tokenizer_database("my_database") {
+ database = "database_in_the_source_tree.csv"
+ deps = [ ":apps" ]
+ optional_paths = [ "$root_build_dir/**/*.elf" ]
+ }
+
+.. note::
+
+ The ``paths`` and ``optional_targets`` arguments do not add anything to
+ ``deps``, so there is no guarantee that the referenced artifacts will exist
+ when the database is updated. Provide ``targets`` or ``deps`` or build other
+ GN targets first if this is a concern.
+
+CMake integration
+=================
+Token databases may be updated or created as part of a CMake build. The
+``pw_tokenizer_database`` template provided by
+``$dir_pw_tokenizer/database.cmake`` automatically updates an in-source tokenized
+strings database or creates a new database with artifacts from a CMake target.
+
+To create a new database, set the ``CREATE`` variable to the desired database
+type (``"csv"`` or ``"binary"``). The database will be created in the output
+directory.
+
+.. code-block::
+
+ include("$dir_pw_tokenizer/database.cmake")
+
+ pw_tokenizer_database("my_database") {
+ CREATE binary
+ TARGET my_target.ext
+ DEPS ${deps_list}
+ }
+
+To update an existing database, provide the path to the database with
+the ``database`` variable.
+
+.. code-block::
+
+ pw_tokenizer_database("my_database") {
+ DATABASE database_in_the_source_tree.csv
+ TARGET my_target.ext
+ DEPS ${deps_list}
+ }
+
+.. _module-pw_tokenizer-collisions:
+
+----------------
+Token collisions
+----------------
+Tokens are calculated with a hash function. It is possible for different
+strings to hash to the same token. When this happens, multiple strings will have
+the same token in the database, and it may not be possible to unambiguously
+decode a token.
+
+The detokenization tools attempt to resolve collisions automatically. Collisions
+are resolved based on two things:
+
+- whether the tokenized data matches the strings arguments' (if any), and
+- if / when the string was marked as having been removed from the database.
+
+Resolving collisions
+====================
+Collisions may occur occasionally. Run the command
+``python -m pw_tokenizer.database report <database>`` to see information about a
+token database, including any collisions.
+
+If there are collisions, take the following steps to resolve them.
+
+- Change one of the colliding strings slightly to give it a new token.
+- In C (not C++), artificial collisions may occur if strings longer than
+ ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` are hashed. If this is happening, consider
+ setting ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` to a larger value. See
+ ``pw_tokenizer/public/pw_tokenizer/config.h``.
+- Run the ``mark_removed`` command with the latest version of the build
+ artifacts to mark missing strings as removed. This deprioritizes them in
+ collision resolution.
+
+ .. code-block:: sh
+
+ python -m pw_tokenizer.database mark_removed --database <database> <ELF files>
+
+ The ``purge`` command may be used to delete these tokens from the database.
+
+Probability of collisions
+=========================
+Hashes of any size have a collision risk. The probability of one at least
+one collision occurring for a given number of strings is unintuitively high
+(this is known as the `birthday problem
+<https://en.wikipedia.org/wiki/Birthday_problem>`_). If fewer than 32 bits are
+used for tokens, the probability of collisions increases substantially.
+
+This table shows the approximate number of strings that can be hashed to have a
+1% or 50% probability of at least one collision (assuming a uniform, random
+hash).
+
++-------+---------------------------------------+
+| Token | Collision probability by string count |
+| bits +--------------------+------------------+
+| | 50% | 1% |
++=======+====================+==================+
+| 32 | 77000 | 9300 |
++-------+--------------------+------------------+
+| 31 | 54000 | 6600 |
++-------+--------------------+------------------+
+| 24 | 4800 | 580 |
++-------+--------------------+------------------+
+| 16 | 300 | 36 |
++-------+--------------------+------------------+
+| 8 | 19 | 3 |
++-------+--------------------+------------------+
+
+Keep this table in mind when masking tokens (see
+:ref:`module-pw_tokenizer-masks`). 16 bits might be acceptable when
+tokenizing a small set of strings, such as module names, but won't be suitable
+for large sets of strings, like log messages.
diff --git a/pw_tokenizer/tokenization.rst b/pw_tokenizer/tokenization.rst
new file mode 100644
index 000000000..c96349d8c
--- /dev/null
+++ b/pw_tokenizer/tokenization.rst
@@ -0,0 +1,741 @@
+:tocdepth: 3
+
+.. _module-pw_tokenizer-tokenization:
+
+============
+Tokenization
+============
+.. pigweed-module-subpage::
+ :name: pw_tokenizer
+ :tagline: Compress strings to shrink logs by +75%
+
+Tokenization converts a string literal to a token. If it's a printf-style
+string, its arguments are encoded along with it. The results of tokenization can
+be sent off device or stored in place of a full string.
+
+--------
+Concepts
+--------
+See :ref:`module-pw_tokenizer-get-started-overview` for a high-level
+explanation of how ``pw_tokenizer`` works.
+
+Token generation: fixed length hashing at compile time
+======================================================
+String tokens are generated using a modified version of the x65599 hash used by
+the SDBM project. All hashing is done at compile time.
+
+In C code, strings are hashed with a preprocessor macro. For compatibility with
+macros, the hash must be limited to a fixed maximum number of characters. This
+value is set by ``PW_TOKENIZER_CFG_C_HASH_LENGTH``. Increasing
+``PW_TOKENIZER_CFG_C_HASH_LENGTH`` increases the compilation time for C due to
+the complexity of the hashing macros.
+
+C++ macros use a constexpr function instead of a macro. This function works with
+any length of string and has lower compilation time impact than the C macros.
+For consistency, C++ tokenization uses the same hash algorithm, but the
+calculated values will differ between C and C++ for strings longer than
+``PW_TOKENIZER_CFG_C_HASH_LENGTH`` characters.
+
+Token encoding
+==============
+The token is a 32-bit hash calculated during compilation. The string is encoded
+little-endian with the token followed by arguments, if any. For example, the
+31-byte string ``You can go about your business.`` hashes to 0xdac9a244.
+This is encoded as 4 bytes: ``44 a2 c9 da``.
+
+Arguments are encoded as follows:
+
+* **Integers** (1--10 bytes) --
+ `ZagZag and varint encoded <https://developers.google.com/protocol-buffers/docs/encoding#signed-integers>`_,
+ similarly to Protocol Buffers. Smaller values take fewer bytes.
+* **Floating point numbers** (4 bytes) -- Single precision floating point.
+* **Strings** (1--128 bytes) -- Length byte followed by the string contents.
+ The top bit of the length whether the string was truncated or not. The
+ remaining 7 bits encode the string length, with a maximum of 127 bytes.
+
+.. TODO(hepler): insert diagram here!
+
+.. tip::
+ ``%s`` arguments can quickly fill a tokenization buffer. Keep ``%s``
+ arguments short or avoid encoding them as strings (e.g. encode an enum as an
+ integer instead of a string). See also
+ :ref:`module-pw_tokenizer-nested-arguments`.
+
+.. _module-pw_tokenizer-proto:
+
+Tokenized fields in protocol buffers
+====================================
+Text may be represented in a few different ways:
+
+- Plain ASCII or UTF-8 text (``This is plain text``)
+- Base64-encoded tokenized message (``$ibafcA==``)
+- Binary-encoded tokenized message (``89 b6 9f 70``)
+- Little-endian 32-bit integer token (``0x709fb689``)
+
+``pw_tokenizer`` provides the ``pw.tokenizer.format`` protobuf field option.
+This option may be applied to a protobuf field to indicate that it may contain a
+tokenized string. A string that is optionally tokenized is represented with a
+single ``bytes`` field annotated with ``(pw.tokenizer.format) =
+TOKENIZATION_OPTIONAL``.
+
+For example, the following protobuf has one field that may contain a tokenized
+string.
+
+.. code-block:: protobuf
+
+ message MessageWithOptionallyTokenizedField {
+ bytes just_bytes = 1;
+ bytes maybe_tokenized = 2 [(pw.tokenizer.format) = TOKENIZATION_OPTIONAL];
+ string just_text = 3;
+ }
+
+-----------------------
+Tokenization in C++ / C
+-----------------------
+To tokenize a string, include ``pw_tokenizer/tokenize.h`` and invoke one of the
+``PW_TOKENIZE_*`` macros.
+
+Tokenize string literals outside of expressions
+===============================================
+``pw_tokenizer`` provides macros for tokenizing string literals with no
+arguments:
+
+* :c:macro:`PW_TOKENIZE_STRING`
+* :c:macro:`PW_TOKENIZE_STRING_DOMAIN`
+* :c:macro:`PW_TOKENIZE_STRING_MASK`
+
+The tokenization macros above cannot be used inside other expressions.
+
+.. admonition:: **Yes**: Assign :c:macro:`PW_TOKENIZE_STRING` to a ``constexpr`` variable.
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ constexpr uint32_t kGlobalToken = PW_TOKENIZE_STRING("Wowee Zowee!");
+
+ void Function() {
+ constexpr uint32_t local_token = PW_TOKENIZE_STRING("Wowee Zowee?");
+ }
+
+.. admonition:: **No**: Use :c:macro:`PW_TOKENIZE_STRING` in another expression.
+ :class: error
+
+ .. code-block:: cpp
+
+ void BadExample() {
+ ProcessToken(PW_TOKENIZE_STRING("This won't compile!"));
+ }
+
+ Use :c:macro:`PW_TOKENIZE_STRING_EXPR` instead.
+
+Tokenize inside expressions
+===========================
+An alternate set of macros are provided for use inside expressions. These make
+use of lambda functions, so while they can be used inside expressions, they
+require C++ and cannot be assigned to constexpr variables or be used with
+special function variables like ``__func__``.
+
+* :c:macro:`PW_TOKENIZE_STRING_EXPR`
+* :c:macro:`PW_TOKENIZE_STRING_DOMAIN_EXPR`
+* :c:macro:`PW_TOKENIZE_STRING_MASK_EXPR`
+
+.. admonition:: When to use these macros
+
+ Use :c:macro:`PW_TOKENIZE_STRING` and related macros to tokenize string
+ literals that do not need %-style arguments encoded.
+
+.. admonition:: **Yes**: Use :c:macro:`PW_TOKENIZE_STRING_EXPR` within other expressions.
+ :class: checkmark
+
+ .. code-block:: cpp
+
+ void GoodExample() {
+ ProcessToken(PW_TOKENIZE_STRING_EXPR("This will compile!"));
+ }
+
+.. admonition:: **No**: Assign :c:macro:`PW_TOKENIZE_STRING_EXPR` to a ``constexpr`` variable.
+ :class: error
+
+ .. code-block:: cpp
+
+ constexpr uint32_t wont_work = PW_TOKENIZE_STRING_EXPR("This won't compile!"));
+
+ Instead, use :c:macro:`PW_TOKENIZE_STRING` to assign to a ``constexpr`` variable.
+
+.. admonition:: **No**: Tokenize ``__func__`` in :c:macro:`PW_TOKENIZE_STRING_EXPR`.
+ :class: error
+
+ .. code-block:: cpp
+
+ void BadExample() {
+ // This compiles, but __func__ will not be the outer function's name, and
+ // there may be compiler warnings.
+ constexpr uint32_t wont_work = PW_TOKENIZE_STRING_EXPR(__func__);
+ }
+
+ Instead, use :c:macro:`PW_TOKENIZE_STRING` to tokenize ``__func__`` or similar macros.
+
+Tokenize a message with arguments to a buffer
+=============================================
+* :c:macro:`PW_TOKENIZE_TO_BUFFER`
+* :c:macro:`PW_TOKENIZE_TO_BUFFER_DOMAIN`
+* :c:macro:`PW_TOKENIZE_TO_BUFFER_MASK`
+
+.. admonition:: Why use this macro
+
+ - Encode a tokenized message for consumption within a function.
+ - Encode a tokenized message into an existing buffer.
+
+ Avoid using ``PW_TOKENIZE_TO_BUFFER`` in widely expanded macros, such as a
+ logging macro, because it will result in larger code size than passing the
+ tokenized data to a function.
+
+.. _module-pw_tokenizer-nested-arguments:
+
+Tokenize nested arguments
+=========================
+Encoding ``%s`` string arguments is inefficient, since ``%s`` strings are
+encoded 1:1, with no tokenization. Tokens can therefore be used to replace
+string arguments to tokenized format strings.
+
+* :c:macro:`PW_TOKEN_FMT`
+
+.. admonition:: Logging nested tokens
+
+ Users will typically interact with nested token arguments during logging.
+ In this case there is a slightly different interface described by
+ :ref:`module-pw_log-tokenized-args` that does not generally invoke
+ ``PW_TOKEN_FMT`` directly.
+
+The format specifier for a token is given by PRI-style macro ``PW_TOKEN_FMT()``,
+which is concatenated to the rest of the format string by the C preprocessor.
+
+.. code-block:: cpp
+
+ PW_TOKENIZE_FORMAT_STRING("margarine_domain",
+ UINT32_MAX,
+ "I can't believe it's not " PW_TOKEN_FMT() "!",
+ PW_TOKENIZE_STRING_EXPR("butter"));
+
+This feature is currently only supported by the Python detokenizer.
+
+Nested token format
+-------------------
+Nested tokens have the following format within strings:
+
+.. code-block::
+
+ $[BASE#]TOKEN
+
+The ``$`` is a common prefix required for all nested tokens. It is possible to
+configure a different common prefix if necessary, but using the default ``$``
+character is strongly recommended.
+
+The optional ``BASE`` defines the numeric base encoding of the token. Accepted
+values are 8, 10, 16, and 64. If the hash symbol ``#`` is used without
+specifying a number, the base is assumed to be 16. If the base option is
+omitted entirely, the base defaults to 64 for backward compatibility. All
+encodings except Base64 are not case sensitive. This may be expanded to support
+other bases in the future.
+
+Non-Base64 tokens are encoded strictly as 32-bit integers with padding.
+Base64 data may additionally encode string arguments for the detokenized token,
+and therefore does not have a maximum width.
+
+The meaning of ``TOKEN`` depends on the current phase of transformation for the
+current tokenized format string. Within the format string's entry in the token
+database, when the actual value of the token argument is not known, ``TOKEN`` is
+a printf argument specifier (e.g. ``%08x`` for a base-16 token with correct
+padding). The actual tokens that will be used as arguments have separate
+entries in the token database.
+
+After the top-level format string has been detokenized and formatted, ``TOKEN``
+should be the value of the token argument in the specified base, with any
+necessary padding. This is the final format of a nested token if it cannot be
+tokenized.
+
+.. list-table:: Example tokens
+ :widths: 10 25 25
+
+ * - Base
+ - | Token database
+ | (within format string entry)
+ - Partially detokenized
+ * - 10
+ - ``$10#%010d``
+ - ``$10#0086025943``
+ * - 16
+ - ``$#%08x``
+ - ``$#0000001A``
+ * - 64
+ - ``%s``
+ - ``$QA19pfEQ``
+
+.. _module-pw_tokenizer-custom-macro:
+
+Tokenize a message with arguments in a custom macro
+===================================================
+Projects can leverage the tokenization machinery in whichever way best suits
+their needs. The most efficient way to use ``pw_tokenizer`` is to pass tokenized
+data to a global handler function. A project's custom tokenization macro can
+handle tokenized data in a function of their choosing. The function may accept
+any arguments, but its final arguments must be:
+
+* The 32-bit token (:cpp:type:`pw_tokenizer_Token`)
+* The argument types (:cpp:type:`pw_tokenizer_ArgTypes`)
+* Variadic arguments, if any
+
+``pw_tokenizer`` provides two low-level macros to help projects create custom
+tokenization macros:
+
+* :c:macro:`PW_TOKENIZE_FORMAT_STRING`
+* :c:macro:`PW_TOKENIZER_REPLACE_FORMAT_STRING`
+
+.. caution::
+
+ Note the spelling difference! The first macro begins with ``PW_TOKENIZE_``
+ (no ``R``) whereas the second begins with ``PW_TOKENIZER_``.
+
+Use these macros to invoke an encoding function with the token, argument types,
+and variadic arguments. The function can then encode the tokenized message to a
+buffer using helpers in ``pw_tokenizer/encode_args.h``:
+
+.. Note: pw_tokenizer_EncodeArgs is a C function so you would expect to
+.. reference it as :c:func:`pw_tokenizer_EncodeArgs`. That doesn't work because
+.. it's defined in a header file that mixes C and C++.
+
+* :cpp:func:`pw::tokenizer::EncodeArgs`
+* :cpp:class:`pw::tokenizer::EncodedMessage`
+* :cpp:func:`pw_tokenizer_EncodeArgs`
+
+Example
+-------
+The following example implements a custom tokenization macro similar to
+:ref:`module-pw_log_tokenized`.
+
+.. code-block:: cpp
+
+ #include "pw_tokenizer/tokenize.h"
+
+ #ifndef __cplusplus
+ extern "C" {
+ #endif
+
+ void EncodeTokenizedMessage(uint32_t metadata,
+ pw_tokenizer_Token token,
+ pw_tokenizer_ArgTypes types,
+ ...);
+
+ #ifndef __cplusplus
+ } // extern "C"
+ #endif
+
+ #define PW_LOG_TOKENIZED_ENCODE_MESSAGE(metadata, format, ...) \
+ do { \
+ PW_TOKENIZE_FORMAT_STRING("logs", UINT32_MAX, format, __VA_ARGS__); \
+ EncodeTokenizedMessage( \
+ metadata, PW_TOKENIZER_REPLACE_FORMAT_STRING(__VA_ARGS__)); \
+ } while (0)
+
+In this example, the ``EncodeTokenizedMessage`` function would handle encoding
+and processing the message. Encoding is done by the
+:cpp:class:`pw::tokenizer::EncodedMessage` class or
+:cpp:func:`pw::tokenizer::EncodeArgs` function from
+``pw_tokenizer/encode_args.h``. The encoded message can then be transmitted or
+stored as needed.
+
+.. code-block:: cpp
+
+ #include "pw_log_tokenized/log_tokenized.h"
+ #include "pw_tokenizer/encode_args.h"
+
+ void HandleTokenizedMessage(pw::log_tokenized::Metadata metadata,
+ pw::span<std::byte> message);
+
+ extern "C" void EncodeTokenizedMessage(const uint32_t metadata,
+ const pw_tokenizer_Token token,
+ const pw_tokenizer_ArgTypes types,
+ ...) {
+ va_list args;
+ va_start(args, types);
+ pw::tokenizer::EncodedMessage<kLogBufferSize> encoded_message(token, types, args);
+ va_end(args);
+
+ HandleTokenizedMessage(metadata, encoded_message);
+ }
+
+.. admonition:: Why use a custom macro
+
+ - Optimal code size. Invoking a free function with the tokenized data results
+ in the smallest possible call site.
+ - Pass additional arguments, such as metadata, with the tokenized message.
+ - Integrate ``pw_tokenizer`` with other systems.
+
+Tokenizing function names
+=========================
+The string literal tokenization functions support tokenizing string literals or
+constexpr character arrays (``constexpr const char[]``). In GCC and Clang, the
+special ``__func__`` variable and ``__PRETTY_FUNCTION__`` extension are declared
+as ``static constexpr char[]`` in C++ instead of the standard ``static const
+char[]``. This means that ``__func__`` and ``__PRETTY_FUNCTION__`` can be
+tokenized while compiling C++ with GCC or Clang.
+
+.. code-block:: cpp
+
+ // Tokenize the special function name variables.
+ constexpr uint32_t function = PW_TOKENIZE_STRING(__func__);
+ constexpr uint32_t pretty_function = PW_TOKENIZE_STRING(__PRETTY_FUNCTION__);
+
+Note that ``__func__`` and ``__PRETTY_FUNCTION__`` are not string literals.
+They are defined as static character arrays, so they cannot be implicitly
+concatentated with string literals. For example, ``printf(__func__ ": %d",
+123);`` will not compile.
+
+Calculate minimum required buffer size
+======================================
+See :cpp:func:`pw::tokenizer::MinEncodingBufferSizeBytes`.
+
+.. _module-pw_tokenizer-base64-format:
+
+Encoding Base64
+===============
+The tokenizer encodes messages to a compact binary representation. Applications
+may desire a textual representation of tokenized strings. This makes it easy to
+use tokenized messages alongside plain text messages, but comes at a small
+efficiency cost: encoded Base64 messages occupy about 4/3 (133%) as much memory
+as binary messages.
+
+The Base64 format is comprised of a ``$`` character followed by the
+Base64-encoded contents of the tokenized message. For example, consider
+tokenizing the string ``This is an example: %d!`` with the argument -1. The
+string's token is 0x4b016e66.
+
+.. code-block:: text
+
+ Source code: PW_LOG("This is an example: %d!", -1);
+
+ Plain text: This is an example: -1! [23 bytes]
+
+ Binary: 66 6e 01 4b 01 [ 5 bytes]
+
+ Base64: $Zm4BSwE= [ 9 bytes]
+
+To encode with the Base64 format, add a call to
+``pw::tokenizer::PrefixedBase64Encode`` or ``pw_tokenizer_PrefixedBase64Encode``
+in the tokenizer handler function. For example,
+
+.. code-block:: cpp
+
+ void TokenizedMessageHandler(const uint8_t encoded_message[],
+ size_t size_bytes) {
+ pw::InlineBasicString base64 = pw::tokenizer::PrefixedBase64Encode(
+ pw::span(encoded_message, size_bytes));
+
+ TransmitLogMessage(base64.data(), base64.size());
+ }
+
+.. _module-pw_tokenizer-masks:
+
+Reduce token size with masking
+==============================
+``pw_tokenizer`` uses 32-bit tokens. On 32-bit or 64-bit architectures, using
+fewer than 32 bits does not improve runtime or code size efficiency. However,
+when tokens are packed into data structures or stored in arrays, the size of the
+token directly affects memory usage. In those cases, every bit counts, and it
+may be desireable to use fewer bits for the token.
+
+``pw_tokenizer`` allows users to provide a mask to apply to the token. This
+masked token is used in both the token database and the code. The masked token
+is not a masked version of the full 32-bit token, the masked token is the token.
+This makes it trivial to decode tokens that use fewer than 32 bits.
+
+Masking functionality is provided through the ``*_MASK`` versions of the macros:
+
+* :c:macro:`PW_TOKENIZE_STRING_MASK`
+* :c:macro:`PW_TOKENIZE_STRING_MASK_EXPR`
+* :c:macro:`PW_TOKENIZE_TO_BUFFER_MASK`
+
+For example, the following generates 16-bit tokens and packs them into an
+existing value.
+
+.. code-block:: cpp
+
+ constexpr uint32_t token = PW_TOKENIZE_STRING_MASK("domain", 0xFFFF, "Pigweed!");
+ uint32_t packed_word = (other_bits << 16) | token;
+
+Tokens are hashes, so tokens of any size have a collision risk. The fewer bits
+used for tokens, the more likely two strings are to hash to the same token. See
+:ref:`module-pw_tokenizer-collisions`.
+
+Masked tokens without arguments may be encoded in fewer bytes. For example, the
+16-bit token ``0x1234`` may be encoded as two little-endian bytes (``34 12``)
+rather than four (``34 12 00 00``). The detokenizer tools zero-pad data smaller
+than four bytes. Tokens with arguments must always be encoded as four bytes.
+
+.. _module-pw_tokenizer-domains:
+
+Keep tokens from different sources separate with domains
+========================================================
+``pw_tokenizer`` supports having multiple tokenization domains. Domains are a
+string label associated with each tokenized string. This allows projects to keep
+tokens from different sources separate. Potential use cases include the
+following:
+
+* Keep large sets of tokenized strings separate to avoid collisions.
+* Create a separate database for a small number of strings that use truncated
+ tokens, for example only 10 or 16 bits instead of the full 32 bits.
+
+If no domain is specified, the domain is empty (``""``). For many projects, this
+default domain is sufficient, so no additional configuration is required.
+
+.. code-block:: cpp
+
+ // Tokenizes this string to the default ("") domain.
+ PW_TOKENIZE_STRING("Hello, world!");
+
+ // Tokenizes this string to the "my_custom_domain" domain.
+ PW_TOKENIZE_STRING_DOMAIN("my_custom_domain", "Hello, world!");
+
+The database and detokenization command line tools default to reading from the
+default domain. The domain may be specified for ELF files by appending
+``#DOMAIN_NAME`` to the file path. Use ``#.*`` to read from all domains. For
+example, the following reads strings in ``some_domain`` from ``my_image.elf``.
+
+.. code-block:: sh
+
+ ./database.py create --database my_db.csv path/to/my_image.elf#some_domain
+
+See :ref:`module-pw_tokenizer-managing-token-databases` for information about
+the ``database.py`` command line tool.
+
+Limitations, bugs, and future work
+==================================
+
+GCC bug: tokenization in template functions
+-------------------------------------------
+GCC incorrectly ignores the section attribute for template `functions
+<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70435>`_ and `variables
+<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88061>`_. For example, the
+following won't work when compiling with GCC and tokenized logging:
+
+.. code-block:: cpp
+
+ template <...>
+ void DoThings() {
+ int value = GetValue();
+ // This log won't work with tokenized logs due to the templated context.
+ PW_LOG_INFO("Got value: %d", value);
+ ...
+ }
+
+The bug causes tokenized strings in template functions to be emitted into
+``.rodata`` instead of the special tokenized string section. This causes two
+problems:
+
+1. Tokenized strings will not be discovered by the token database tools.
+2. Tokenized strings may not be removed from the final binary.
+
+There are two workarounds.
+
+#. **Use Clang.** Clang puts the string data in the requested section, as
+ expected. No extra steps are required.
+
+#. **Move tokenization calls to a non-templated context.** Creating a separate
+ non-templated function and invoking it from the template resolves the issue.
+ This enables tokenizing in most cases encountered in practice with
+ templates.
+
+ .. code-block:: cpp
+
+ // In .h file:
+ void LogThings(value);
+
+ template <...>
+ void DoThings() {
+ int value = GetValue();
+ // This log will work: calls non-templated helper.
+ LogThings(value);
+ ...
+ }
+
+ // In .cc file:
+ void LogThings(int value) {
+ // Tokenized logging works as expected in this non-templated context.
+ PW_LOG_INFO("Got value %d", value);
+ }
+
+There is a third option, which isn't implemented yet, which is to compile the
+binary twice: once to extract the tokens, and once for the production binary
+(without tokens). If this is interesting to you please get in touch.
+
+64-bit tokenization
+-------------------
+The Python and C++ detokenizing libraries currently assume that strings were
+tokenized on a system with 32-bit ``long``, ``size_t``, ``intptr_t``, and
+``ptrdiff_t``. Decoding may not work correctly for these types if a 64-bit
+device performed the tokenization.
+
+Supporting detokenization of strings tokenized on 64-bit targets would be
+simple. This could be done by adding an option to switch the 32-bit types to
+64-bit. The tokenizer stores the sizes of these types in the
+``.pw_tokenizer.info`` ELF section, so the sizes of these types can be verified
+by checking the ELF file, if necessary.
+
+Tokenization in headers
+-----------------------
+Tokenizing code in header files (inline functions or templates) may trigger
+warnings such as ``-Wlto-type-mismatch`` under certain conditions. That
+is because tokenization requires declaring a character array for each tokenized
+string. If the tokenized string includes macros that change value, the size of
+this character array changes, which means the same static variable is defined
+with different sizes. It should be safe to suppress these warnings, but, when
+possible, code that tokenizes strings with macros that can change value should
+be moved to source files rather than headers.
+
+----------------------
+Tokenization in Python
+----------------------
+The Python ``pw_tokenizer.encode`` module has limited support for encoding
+tokenized messages with the :func:`pw_tokenizer.encode.encode_token_and_args`
+function. This function requires a string's token is already calculated.
+Typically these tokens are provided by a database, but they can be manually
+created using the tokenizer hash.
+
+:func:`pw_tokenizer.tokens.pw_tokenizer_65599_hash` is particularly useful
+for offline token database generation in cases where tokenized strings in a
+binary cannot be embedded as parsable pw_tokenizer entries.
+
+.. note::
+ In C, the hash length of a string has a fixed limit controlled by
+ ``PW_TOKENIZER_CFG_C_HASH_LENGTH``. To match tokens produced by C (as opposed
+ to C++) code, ``pw_tokenizer_65599_hash()`` should be called with a matching
+ hash length limit. When creating an offline database, it's a good idea to
+ generate tokens for both, and merge the databases.
+
+.. _module-pw_tokenizer-cli-encoding:
+
+-----------------
+Encoding CLI tool
+-----------------
+The ``pw_tokenizer.encode`` command line tool can be used to encode
+format strings and optional arguments.
+
+.. code-block:: bash
+
+ python -m pw_tokenizer.encode [-h] FORMAT_STRING [ARG ...]
+
+Example:
+
+.. code-block:: text
+
+ $ python -m pw_tokenizer.encode "There's... %d many of %s!" 2 them
+ Raw input: "There's... %d many of %s!" % (2, 'them')
+ Formatted input: There's... 2 many of them!
+ Token: 0xb6ef8b2d
+ Encoded: b'-\x8b\xef\xb6\x04\x04them' (2d 8b ef b6 04 04 74 68 65 6d) [10 bytes]
+ Prefixed Base64: $LYvvtgQEdGhlbQ==
+
+See ``--help`` for full usage details.
+
+--------
+Appendix
+--------
+
+Case study
+==========
+.. note:: This section discusses the implementation, results, and lessons
+ learned from a real-world deployment of ``pw_tokenizer``.
+
+The tokenizer module was developed to bring tokenized logging to an
+in-development product. The product already had an established text-based
+logging system. Deploying tokenization was straightforward and had substantial
+benefits.
+
+Results
+-------
+* Log contents shrunk by over 50%, even with Base64 encoding.
+
+ * Significant size savings for encoded logs, even using the less-efficient
+ Base64 encoding required for compatibility with the existing log system.
+ * Freed valuable communication bandwidth.
+ * Allowed storing many more logs in crash dumps.
+
+* Substantial flash savings.
+
+ * Reduced the size firmware images by up to 18%.
+
+* Simpler logging code.
+
+ * Removed CPU-heavy ``snprintf`` calls.
+ * Removed complex code for forwarding log arguments to a low-priority task.
+
+This section describes the tokenizer deployment process and highlights key
+insights.
+
+Firmware deployment
+-------------------
+* In the project's logging macro, calls to the underlying logging function were
+ replaced with a tokenized log macro invocation.
+* The log level was passed as the payload argument to facilitate runtime log
+ level control.
+* For this project, it was necessary to encode the log messages as text. In
+ the handler function the log messages were encoded in the $-prefixed
+ :ref:`module-pw_tokenizer-base64-format`, then dispatched as normal log messages.
+* Asserts were tokenized a callback-based API that has been removed (a
+ :ref:`custom macro <module-pw_tokenizer-custom-macro>` is a better
+ alternative).
+
+.. attention::
+ Do not encode line numbers in tokenized strings. This results in a huge
+ number of lines being added to the database, since every time code moves,
+ new strings are tokenized. If :ref:`module-pw_log_tokenized` is used, line
+ numbers are encoded in the log metadata. Line numbers may also be included by
+ by adding ``"%d"`` to the format string and passing ``__LINE__``.
+
+.. _module-pw_tokenizer-database-management:
+
+Database management
+-------------------
+* The token database was stored as a CSV file in the project's Git repo.
+* The token database was automatically updated as part of the build, and
+ developers were expected to check in the database changes alongside their code
+ changes.
+* A presubmit check verified that all strings added by a change were added to
+ the token database.
+* The token database included logs and asserts for all firmware images in the
+ project.
+* No strings were purged from the token database.
+
+.. tip::
+ Merge conflicts may be a frequent occurrence with an in-source CSV database.
+ Use the :ref:`module-pw_tokenizer-directory-database-format` instead.
+
+Decoding tooling deployment
+---------------------------
+* The Python detokenizer in ``pw_tokenizer`` was deployed to two places:
+
+ * Product-specific Python command line tools, using
+ ``pw_tokenizer.Detokenizer``.
+ * Standalone script for decoding prefixed Base64 tokens in files or
+ live output (e.g. from ``adb``), using ``detokenize.py``'s command line
+ interface.
+
+* The C++ detokenizer library was deployed to two Android apps with a Java
+ Native Interface (JNI) layer.
+
+ * The binary token database was included as a raw resource in the APK.
+ * In one app, the built-in token database could be overridden by copying a
+ file to the phone.
+
+.. tip::
+ Make the tokenized logging tools simple to use for your project.
+
+ * Provide simple wrapper shell scripts that fill in arguments for the
+ project. For example, point ``detokenize.py`` to the project's token
+ databases.
+ * Use ``pw_tokenizer.AutoUpdatingDetokenizer`` to decode in
+ continuously-running tools, so that users don't have to restart the tool
+ when the token database updates.
+ * Integrate detokenization everywhere it is needed. Integrating the tools
+ takes just a few lines of code, and token databases can be embedded in APKs
+ or binaries.
diff --git a/pw_tokenizer/tokenize.cc b/pw_tokenizer/tokenize.cc
index dbe3fd696..c4dcf4798 100644
--- a/pw_tokenizer/tokenize.cc
+++ b/pw_tokenizer/tokenize.cc
@@ -27,6 +27,9 @@ namespace pw {
namespace tokenizer {
namespace {
+static_assert(sizeof(PW_TOKENIZER_NESTED_PREFIX_STR) == 2,
+ "The nested prefix must be a single character string");
+
// Store metadata about this compilation's string tokenization in the ELF.
//
// The tokenizer metadata will not go into the on-device executable binary code.
diff --git a/pw_tokenizer/tokenize_c99_test.c b/pw_tokenizer/tokenize_c99_test.c
new file mode 100644
index 000000000..62bd7c49a
--- /dev/null
+++ b/pw_tokenizer/tokenize_c99_test.c
@@ -0,0 +1,134 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// This is a test of using pw_tokenizer in C99. It can be compiled outside of
+// the GN build by adding a main() function that calls RunTestAndReturnPassed()
+// and invoking the compiler as follows:
+/*
+ gcc -std=c99 -Wall -Wextra \
+ -Ipw_assert/public \
+ -Ipw_assert/print_and_abort_check_public_overrides \
+ -Ipw_containers/public \
+ -Ipw_polyfill/public \
+ -Ipw_preprocessor/public \
+ -Ipw_tokenizer/public \
+ -Ipw_varint/public \
+ pw_tokenizer/tokenize_c99_test.c \
+ pw_varint/varint_c.c \
+ pw_containers/variable_length_entry_queue.c
+ */
+
+#include <inttypes.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "pw_containers/variable_length_entry_queue.h"
+#include "pw_tokenizer/encode_args.h"
+#include "pw_tokenizer/tokenize.h"
+#include "pw_varint/varint.h"
+
+#if __STDC_VERSION__ != 199901L
+#error "This test should be compiled with -std=c99."
+#endif // __STDC_VERSION__ != 199901L
+
+PW_VARIABLE_LENGTH_ENTRY_QUEUE_DECLARE(buffer, 256);
+
+// Encodes a tokenized message with any number of int arguments.
+static void TokenizeIntegersOnly(uint32_t token, int arg_count, ...) {
+ va_list args;
+ va_start(args, arg_count);
+
+ // Encode the tokenized log to a temporary buffer.
+ uint8_t encoded[32];
+ memcpy(encoded, &token, sizeof(token));
+
+ size_t index = sizeof(token);
+
+ for (int i = 0; i < arg_count; ++i) {
+ // Encode each int argument.
+ int argument = va_arg(args, uint32_t);
+
+ // Encode the argument to the buffer
+ index += pw_tokenizer_EncodeInt(
+ argument, &encoded[index], sizeof(encoded) - index);
+ }
+
+ // Write the encoded log to the ring buffer
+ pw_VariableLengthEntryQueue_PushOverwrite(buffer, encoded, index);
+
+ va_end(args);
+}
+
+// Tokenization macro that only handles int arguments.
+#define TOKENIZE_INTS(format, ...) \
+ do { \
+ PW_TOKENIZE_FORMAT_STRING_ANY_ARG_COUNT( \
+ "tokenize_c99_test", UINT32_MAX, format, __VA_ARGS__); \
+ TokenizeIntegersOnly(_pw_tokenizer_token, \
+ PW_FUNCTION_ARG_COUNT(__VA_ARGS__) \
+ PW_COMMA_ARGS(__VA_ARGS__)); \
+ } while (0)
+
+// C version of the ASSERT_EQ macro that returns a string version of the failing
+// line.
+#define ASSERT_EQ(lhs, rhs) \
+ if ((lhs) != (rhs)) { \
+ return FILE_LINE ": ASSERT_EQ(" #lhs ", " #rhs ") failed!"; \
+ }
+
+#define FILE_LINE __FILE__ ":" STRINGIFY(__LINE__)
+#define STRINGIFY(x) _STRINGIFY(x)
+#define _STRINGIFY(x) #x
+
+// This test tokenize a few strings with arguments and checks the contents.
+// It is called from tokenize_c99_test_entry_point.cc.
+const char* RunTestAndReturnPassed(void) {
+ TOKENIZE_INTS("Tokenize this with no arguments!");
+ TOKENIZE_INTS("One arg, one byte: %x", -1);
+ TOKENIZE_INTS("One arg, 5 bytes: %ld", (long)INT32_MAX);
+ TOKENIZE_INTS("Three args, 4 bytes: %d %d %d", 1, 63, 128);
+
+ ASSERT_EQ(pw_VariableLengthEntryQueue_Size(buffer), 4u);
+
+ pw_VariableLengthEntryQueue_Iterator it =
+ pw_VariableLengthEntryQueue_Begin(buffer);
+ pw_VariableLengthEntryQueue_Entry entry =
+ pw_VariableLengthEntryQueue_GetEntry(&it);
+
+ ASSERT_EQ(entry.size_1, sizeof(uint32_t) + 0);
+ ASSERT_EQ(entry.size_2, 0u);
+
+ pw_VariableLengthEntryQueue_Iterator_Advance(&it);
+ entry = pw_VariableLengthEntryQueue_GetEntry(&it);
+ ASSERT_EQ(entry.size_1, sizeof(uint32_t) + 1);
+ ASSERT_EQ(entry.size_2, 0u);
+
+ pw_VariableLengthEntryQueue_Iterator_Advance(&it);
+ entry = pw_VariableLengthEntryQueue_GetEntry(&it);
+ ASSERT_EQ(entry.size_1, sizeof(uint32_t) + 5);
+ ASSERT_EQ(entry.size_2, 0u);
+
+ pw_VariableLengthEntryQueue_Iterator_Advance(&it);
+ entry = pw_VariableLengthEntryQueue_GetEntry(&it);
+ ASSERT_EQ(entry.size_1, sizeof(uint32_t) + 4);
+ ASSERT_EQ(entry.size_2, 0u);
+
+ pw_VariableLengthEntryQueue_Iterator_Advance(&it);
+ pw_VariableLengthEntryQueue_Iterator end =
+ pw_VariableLengthEntryQueue_End(buffer);
+ ASSERT_EQ(pw_VariableLengthEntryQueue_Iterator_Equal(&it, &end), true);
+
+ return "passed";
+}
diff --git a/pw_tokenizer/tokenize_c99_test_entry_point.cc b/pw_tokenizer/tokenize_c99_test_entry_point.cc
new file mode 100644
index 000000000..78d5cc293
--- /dev/null
+++ b/pw_tokenizer/tokenize_c99_test_entry_point.cc
@@ -0,0 +1,23 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "gtest/gtest.h"
+
+extern "C" const char* RunTestAndReturnPassed(void);
+
+namespace {
+
+TEST(TokenizeC99, RunTest) { EXPECT_STREQ(RunTestAndReturnPassed(), "passed"); }
+
+} // namespace
diff --git a/pw_tokenizer/ts/detokenizer.ts b/pw_tokenizer/ts/detokenizer.ts
index fe6ea910a..8137d3471 100644
--- a/pw_tokenizer/ts/detokenizer.ts
+++ b/pw_tokenizer/ts/detokenizer.ts
@@ -13,10 +13,10 @@
// the License.
/** Decodes and detokenizes strings from binary or Base64 input. */
-import {Buffer} from 'buffer';
-import {Frame} from 'pigweedjs/pw_hdlc';
-import {TokenDatabase} from './token_database';
-import {PrintfDecoder} from './printf_decoder';
+import { Buffer } from 'buffer';
+import { Frame } from 'pigweedjs/pw_hdlc';
+import { TokenDatabase } from './token_database';
+import { PrintfDecoder } from './printf_decoder';
const MAX_RECURSIONS = 9;
const BASE64CHARS = '[A-Za-z0-9+/-_]';
@@ -27,7 +27,7 @@ const PATTERN = new RegExp(
`(?:${BASE64CHARS}{4})*` +
// The last block of 4 chars may have one or two padding chars (=).
`(?:${BASE64CHARS}{3}=|${BASE64CHARS}{2}==)?`,
- 'g'
+ 'g',
);
interface TokenAndArgs {
@@ -61,7 +61,7 @@ export class Detokenizer {
* returned as string as-is.
*/
detokenizeUint8Array(data: Uint8Array): string {
- const {token, args} = this.decodeUint8Array(data);
+ const { token, args } = this.decodeUint8Array(data);
// Parse arguments if this is printf-style text.
const format = this.database.get(token);
if (format) {
@@ -80,7 +80,7 @@ export class Detokenizer {
*/
detokenizeBase64(
tokenizedFrame: Frame,
- maxRecursion: number = MAX_RECURSIONS
+ maxRecursion: number = MAX_RECURSIONS,
): string {
const base64String = new TextDecoder().decode(tokenizedFrame.data);
return this.detokenizeBase64String(base64String, maxRecursion);
@@ -88,16 +88,16 @@ export class Detokenizer {
private detokenizeBase64String(
base64String: string,
- recursions: number
+ recursions: number,
): string {
- return base64String.replace(PATTERN, base64Substring => {
- const {token, args} = this.decodeBase64TokenFrame(base64Substring);
+ return base64String.replace(PATTERN, (base64Substring) => {
+ const { token, args } = this.decodeBase64TokenFrame(base64Substring);
const format = this.database.get(token);
// Parse arguments if this is printf-style text.
if (format) {
const decodedOriginal = new PrintfDecoder().decode(
String(format),
- args
+ args,
);
// Detokenize nested Base64 tokens and their arguments.
if (recursions > 0) {
@@ -110,14 +110,13 @@ export class Detokenizer {
}
private decodeUint8Array(data: Uint8Array): TokenAndArgs {
- const token = new DataView(
- data.buffer,
- data.byteOffset,
- 4
- ).getUint32(0, true);
+ const token = new DataView(data.buffer, data.byteOffset, 4).getUint32(
+ 0,
+ true,
+ );
const args = new Uint8Array(data.buffer.slice(data.byteOffset + 4));
- return {token, args};
+ return { token, args };
}
private decodeBase64TokenFrame(base64Data: string): TokenAndArgs {
@@ -125,15 +124,15 @@ export class Detokenizer {
const prefixRemoved = base64Data.slice(1);
const noBase64 = Buffer.from(prefixRemoved, 'base64').toString('binary');
// Convert back to bytes and return token and arguments.
- const bytes = noBase64.split('').map(ch => ch.charCodeAt(0));
+ const bytes = noBase64.split('').map((ch) => ch.charCodeAt(0));
const uIntArray = new Uint8Array(bytes);
const token = new DataView(
uIntArray.buffer,
uIntArray.byteOffset,
- 4
+ 4,
).getUint32(0, true);
const args = new Uint8Array(bytes.slice(4));
- return {token, args};
+ return { token, args };
}
}
diff --git a/pw_tokenizer/ts/detokenizer_test.ts b/pw_tokenizer/ts/detokenizer_test.ts
index 1be4de0ae..0340f260e 100644
--- a/pw_tokenizer/ts/detokenizer_test.ts
+++ b/pw_tokenizer/ts/detokenizer_test.ts
@@ -14,8 +14,8 @@
/* eslint-env browser */
-import {Frame, Encoder, Decoder} from 'pigweedjs/pw_hdlc';
-import {Detokenizer} from './detokenizer';
+import { Frame, Encoder, Decoder } from 'pigweedjs/pw_hdlc';
+import { Detokenizer } from './detokenizer';
const CSV = `
64636261, ,"regular token"
@@ -50,18 +50,18 @@ describe('Detokenizer', () => {
it('failure to detokenize returns original string', () => {
expect(detokenizer.detokenize(generateFrame('aabbcc'))).toEqual('aabbcc');
expect(detokenizer.detokenizeBase64(generateFrame('$8zP7hg=='))).toEqual(
- '$8zP7hg=='
+ '$8zP7hg==',
);
});
it('recursive detokenize all nested base64 tokens', () => {
expect(
detokenizer.detokenizeBase64(
generateFrame(
- '$PNNrDQkkN1lZZFJRPT0lJGIxNFlsd2trTjFsWlpGSlJQVDBGUTJGdFpXeFlwSENkUHc9PQ=='
- )
- )
+ '$PNNrDQkkN1lZZFJRPT0lJGIxNFlsd2trTjFsWlpGSlJQVDBGUTJGdFpXeFlwSENkUHc9PQ==',
+ ),
+ ),
).toEqual(
- 'Regular Token: Cat and Nested Token: (token: Cat, string: Camel, int: 44, float: 1.2300000190734863)'
+ 'Regular Token: Cat and Nested Token: (token: Cat, string: Camel, int: 44, float: 1.2300000190734863)',
);
});
@@ -69,12 +69,12 @@ describe('Detokenizer', () => {
expect(
detokenizer.detokenizeBase64(
generateFrame(
- '$PNNrDQkkN1lZZFJRPT0lJGIxNFlsd2trTjFsWlpGSlJQVDBGUTJGdFpXeFlwSENkUHc9PQ=='
+ '$PNNrDQkkN1lZZFJRPT0lJGIxNFlsd2trTjFsWlpGSlJQVDBGUTJGdFpXeFlwSENkUHc9PQ==',
),
- 1
- )
+ 1,
+ ),
).toEqual(
- 'Regular Token: Cat and Nested Token: (token: $7YYdRQ==, string: Camel, int: 44, float: 1.2300000190734863)'
+ 'Regular Token: Cat and Nested Token: (token: $7YYdRQ==, string: Camel, int: 44, float: 1.2300000190734863)',
);
});
});
diff --git a/pw_tokenizer/ts/index.ts b/pw_tokenizer/ts/index.ts
index 1989b0592..a90ffa7d6 100644
--- a/pw_tokenizer/ts/index.ts
+++ b/pw_tokenizer/ts/index.ts
@@ -12,5 +12,5 @@
// License for the specific language governing permissions and limitations under
// the License.
-export {Detokenizer} from './detokenizer';
-export {PrintfDecoder} from './printf_decoder';
+export { Detokenizer } from './detokenizer';
+export { PrintfDecoder } from './printf_decoder';
diff --git a/pw_tokenizer/ts/int_testdata.ts b/pw_tokenizer/ts/int_testdata.ts
index 36b5a21be..1fe01b494 100644
--- a/pw_tokenizer/ts/int_testdata.ts
+++ b/pw_tokenizer/ts/int_testdata.ts
@@ -13,40 +13,64 @@
// the License.
const IntDB = [
- ["%d", "-128", "%u", "4294967168", '\xff\x01'],
- ["%d", "-10", "%u", "4294967286", '\x13'],
- ["%d", "-9", "%u", "4294967287", '\x11'],
- ["%d", "-8", "%u", "4294967288", '\x0f'],
- ["%d", "-7", "%u", "4294967289", '\x0d'],
- ["%d", "-6", "%u", "4294967290", '\x0b'],
- ["%d", "-5", "%u", "4294967291", '\x09'],
- ["%d", "-4", "%u", "4294967292", '\x07'],
- ["%d", "-3", "%u", "4294967293", '\x05'],
- ["%d", "-2", "%u", "4294967294", '\x03'],
- ["%d", "-1", "%u", "4294967295", '\x01'],
- ["%d", "0", "%u", "0", '\x00'],
- ["%d", "1", "%u", "1", '\x02'],
- ["%d", "2", "%u", "2", '\x04'],
- ["%d", "3", "%u", "3", '\x06'],
- ["%d", "4", "%u", "4", '\x08'],
- ["%d", "5", "%u", "5", '\x0a'],
- ["%d", "6", "%u", "6", '\x0c'],
- ["%d", "7", "%u", "7", '\x0e'],
- ["%d", "8", "%u", "8", '\x10'],
- ["%d", "9", "%u", "9", '\x12'],
- ["%d", "10", "%u", "10", '\x14'],
- ["%d", "127", "%u", "127", '\xfe\x01'],
- ["%d", "-32768", "%u", "4294934528", '\xff\xff\x03'],
- ["%d", "652344632", "%u", "652344632", '\xf0\xf4\x8f\xee\x04'],
- ["%d", "18567", "%u", "18567", '\x8e\xa2\x02'],
- ["%d", "-14", "%u", "4294967282", '\x1b'],
- ["%d", "-2147483648", "%u", "2147483648", '\xff\xff\xff\xff\x0f'],
- ["%ld", "-14", "%lu", "4294967282", '\x1b'],
- ["%d", "2075650855", "%u", "2075650855", '\xce\xac\xbf\xbb\x0f'],
- ["%lld", "5922204476835468009", "%llu", "5922204476835468009", '\xd2\xcb\x8c\x90\x86\xe6\xf2\xaf\xa4\x01'],
- ["%lld", "-9223372036854775808", "%llu", "9223372036854775808", '\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
- ["%lld", "3273441488341945355", "%llu", "3273441488341945355", '\x96\xb0\xae\x9a\x96\xec\xcc\xed\x5a'],
- ["%lld", "-9223372036854775807", "%llu", "9223372036854775809", '\xfd\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
-]
+ ['%d', '-128', '%u', '4294967168', '\xff\x01'],
+ ['%d', '-10', '%u', '4294967286', '\x13'],
+ ['%d', '-9', '%u', '4294967287', '\x11'],
+ ['%d', '-8', '%u', '4294967288', '\x0f'],
+ ['%d', '-7', '%u', '4294967289', '\x0d'],
+ ['%d', '-6', '%u', '4294967290', '\x0b'],
+ ['%d', '-5', '%u', '4294967291', '\x09'],
+ ['%d', '-4', '%u', '4294967292', '\x07'],
+ ['%d', '-3', '%u', '4294967293', '\x05'],
+ ['%d', '-2', '%u', '4294967294', '\x03'],
+ ['%d', '-1', '%u', '4294967295', '\x01'],
+ ['%d', '0', '%u', '0', '\x00'],
+ ['%d', '1', '%u', '1', '\x02'],
+ ['%d', '2', '%u', '2', '\x04'],
+ ['%d', '3', '%u', '3', '\x06'],
+ ['%d', '4', '%u', '4', '\x08'],
+ ['%d', '5', '%u', '5', '\x0a'],
+ ['%d', '6', '%u', '6', '\x0c'],
+ ['%d', '7', '%u', '7', '\x0e'],
+ ['%d', '8', '%u', '8', '\x10'],
+ ['%d', '9', '%u', '9', '\x12'],
+ ['%d', '10', '%u', '10', '\x14'],
+ ['%d', '127', '%u', '127', '\xfe\x01'],
+ ['%d', '-32768', '%u', '4294934528', '\xff\xff\x03'],
+ ['%d', '652344632', '%u', '652344632', '\xf0\xf4\x8f\xee\x04'],
+ ['%d', '18567', '%u', '18567', '\x8e\xa2\x02'],
+ ['%d', '-14', '%u', '4294967282', '\x1b'],
+ ['%d', '-2147483648', '%u', '2147483648', '\xff\xff\xff\xff\x0f'],
+ ['%ld', '-14', '%lu', '4294967282', '\x1b'],
+ ['%d', '2075650855', '%u', '2075650855', '\xce\xac\xbf\xbb\x0f'],
+ [
+ '%lld',
+ '5922204476835468009',
+ '%llu',
+ '5922204476835468009',
+ '\xd2\xcb\x8c\x90\x86\xe6\xf2\xaf\xa4\x01',
+ ],
+ [
+ '%lld',
+ '-9223372036854775808',
+ '%llu',
+ '9223372036854775808',
+ '\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01',
+ ],
+ [
+ '%lld',
+ '3273441488341945355',
+ '%llu',
+ '3273441488341945355',
+ '\x96\xb0\xae\x9a\x96\xec\xcc\xed\x5a',
+ ],
+ [
+ '%lld',
+ '-9223372036854775807',
+ '%llu',
+ '9223372036854775809',
+ '\xfd\xff\xff\xff\xff\xff\xff\xff\xff\x01',
+ ],
+];
export default IntDB;
diff --git a/pw_tokenizer/ts/printf_decoder.ts b/pw_tokenizer/ts/printf_decoder.ts
index 7b0203031..adf815908 100644
--- a/pw_tokenizer/ts/printf_decoder.ts
+++ b/pw_tokenizer/ts/printf_decoder.ts
@@ -13,9 +13,10 @@
// the License.
/** Decodes arguments and formats them with the provided format string. */
-import Long from "long";
+import Long from 'long';
-const SPECIFIER_REGEX = /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g;
+const SPECIFIER_REGEX =
+ /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g;
// Conversion specifiers by type; n is not supported.
const SIGNED_INT = 'di'.split('');
const UNSIGNED_INT = 'oxXup'.split('');
@@ -37,7 +38,7 @@ interface DecodedArg {
}
// ZigZag decode function from protobuf's wire_format module.
-function zigzagDecode(value: Long, unsigned: boolean = false): Long {
+function zigzagDecode(value: Long, unsigned = false): Long {
// 64 bit math is:
// signmask = (zigzag & 1) ? -1 : 0;
// twosComplement = (zigzag >> 1) ^ signmask;
@@ -45,17 +46,18 @@ function zigzagDecode(value: Long, unsigned: boolean = false): Long {
// To work with 32 bit, we can operate on both but "carry" the lowest bit
// from the high word by shifting it up 31 bits to be the most significant bit
// of the low word.
- var bitsLow = value.low, bitsHigh = value.high;
- var signFlipMask = -(bitsLow & 1);
+ let bitsLow = value.low,
+ bitsHigh = value.high;
+ const signFlipMask = -(bitsLow & 1);
bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask;
bitsHigh = (bitsHigh >>> 1) ^ signFlipMask;
return new Long(bitsLow, bitsHigh, unsigned);
-};
+}
export class PrintfDecoder {
// Reads a unicode string from the encoded data.
private decodeString(args: Uint8Array): DecodedArg {
- if (args.length === 0) return {size: 0, value: null};
+ if (args.length === 0) return { size: 0, value: null };
let sizeAndStatus = args[0];
let status = DecodedStatusFlags.OK;
@@ -71,32 +73,37 @@ export class PrintfDecoder {
}
const decoded = new TextDecoder().decode(data);
- return {size: rawData.length, value: decoded};
+ return { size: rawData.length, value: decoded };
}
private decodeSignedInt(args: Uint8Array): DecodedArg {
return this._decodeInt(args);
}
- private _decodeInt(args: Uint8Array, unsigned: boolean = false): DecodedArg {
- if (args.length === 0) return {size: 0, value: null};
+ private _decodeInt(args: Uint8Array, unsigned = false): DecodedArg {
+ if (args.length === 0) return { size: 0, value: null };
let count = 0;
let result = new Long(0);
let shift = 0;
for (count = 0; count < args.length; count++) {
const byte = args[count];
- result = result.or((Long.fromInt(byte, unsigned).and(0x7f)).shiftLeft(shift));
+ result = result.or(
+ Long.fromInt(byte, unsigned).and(0x7f).shiftLeft(shift),
+ );
if (!(byte & 0x80)) {
- return {value: zigzagDecode(result, unsigned), size: count + 1};
+ return { value: zigzagDecode(result, unsigned), size: count + 1 };
}
shift += 7;
if (shift >= 64) break;
}
- return {size: 0, value: null};
+ return { size: 0, value: null };
}
- private decodeUnsignedInt(args: Uint8Array, lengthSpecifier: string): DecodedArg {
+ private decodeUnsignedInt(
+ args: Uint8Array,
+ lengthSpecifier: string,
+ ): DecodedArg {
const arg = this._decodeInt(args, true);
const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32;
@@ -105,9 +112,8 @@ export class PrintfDecoder {
if (arg.value !== null) {
let num = arg.value as Long;
if (bits === 32) {
- num = num.and((Long.fromInt(1).shiftLeft(bits)).add(-1));
- }
- else {
+ num = num.and(Long.fromInt(1).shiftLeft(bits).add(-1));
+ } else {
num = num.and(-1);
}
arg.value = num.toString();
@@ -126,17 +132,23 @@ export class PrintfDecoder {
}
private decodeFloat(args: Uint8Array, precision: string): DecodedArg {
- if (args.length < 4) return {size: 0, value: ''};
+ if (args.length < 4) return { size: 0, value: '' };
const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32(
0,
- true
+ true,
);
- if (precision) return {size: 4, value: floatValue.toFixed(parseInt(precision))}
- return {size: 4, value: floatValue};
+ if (precision)
+ return { size: 4, value: floatValue.toFixed(parseInt(precision)) };
+ return { size: 4, value: floatValue };
}
- private format(specifierType: string, args: Uint8Array, precision: string, lengthSpecifier: string): DecodedArg {
- if (specifierType == '%') return {size: 0, value: '%'}; // literal %
+ private format(
+ specifierType: string,
+ args: Uint8Array,
+ precision: string,
+ lengthSpecifier: string,
+ ): DecodedArg {
+ if (specifierType == '%') return { size: 0, value: '%' }; // literal %
if (specifierType === 's') {
return this.decodeString(args);
}
@@ -154,17 +166,29 @@ export class PrintfDecoder {
}
// Unsupported specifier, return as-is
- return {size: 0, value: '%' + specifierType};
+ return { size: 0, value: '%' + specifierType };
}
decode(formatString: string, args: Uint8Array): string {
return formatString.replace(
SPECIFIER_REGEX,
- (_specifier, _precisionFull, precision, lengthSpecifier, specifierType) => {
- const decodedArg = this.format(specifierType, args, precision, lengthSpecifier);
+ (
+ _specifier,
+ _precisionFull,
+ precision,
+ lengthSpecifier,
+ specifierType,
+ ) => {
+ const decodedArg = this.format(
+ specifierType,
+ args,
+ precision,
+ lengthSpecifier,
+ );
args = args.slice(decodedArg.size);
if (decodedArg === null) return '';
return String(decodedArg.value);
- });
+ },
+ );
}
}
diff --git a/pw_tokenizer/ts/printf_decoder_test.ts b/pw_tokenizer/ts/printf_decoder_test.ts
index db28e488a..938484824 100644
--- a/pw_tokenizer/ts/printf_decoder_test.ts
+++ b/pw_tokenizer/ts/printf_decoder_test.ts
@@ -13,7 +13,7 @@
// the License.
/* eslint-env browser */
-import {PrintfDecoder} from './printf_decoder';
+import { PrintfDecoder } from './printf_decoder';
import IntDB from './int_testdata';
function argFromString(arg: string): Uint8Array {
@@ -22,7 +22,7 @@ function argFromString(arg: string): Uint8Array {
}
function argFromStringBinary(arg: string): Uint8Array {
- return new Uint8Array(arg.split('').map(ch => ch.charCodeAt(0)));
+ return new Uint8Array(arg.split('').map((ch) => ch.charCodeAt(0)));
}
function argsConcat(...args: Uint8Array[]): Uint8Array {
@@ -43,13 +43,13 @@ describe('PrintfDecoder', () => {
it('formats string correctly', () => {
expect(printfDecoder.decode('Hello %s', argFromString('Computer'))).toEqual(
- 'Hello Computer'
+ 'Hello Computer',
);
expect(
printfDecoder.decode(
'Hello %s and %s',
- argsConcat(argFromString('Mac'), argFromString('PC'))
- )
+ argsConcat(argFromString('Mac'), argFromString('PC')),
+ ),
).toEqual('Hello Mac and PC');
});
@@ -57,9 +57,12 @@ describe('PrintfDecoder', () => {
expect(
printfDecoder.decode(
'Hello %s and %u',
- argsConcat(argFromString('Computer'), argFromStringBinary('\xff\xff\x03'))
- )).toEqual(
- 'Hello Computer and 4294934528');
+ argsConcat(
+ argFromString('Computer'),
+ argFromStringBinary('\xff\xff\x03'),
+ ),
+ ),
+ ).toEqual('Hello Computer and 4294934528');
});
it('formats integers correctly', () => {
@@ -67,15 +70,13 @@ describe('PrintfDecoder', () => {
const testcase = IntDB[index];
// Test signed
expect(
- printfDecoder
- .decode(testcase[0], argFromStringBinary(testcase[4])))
- .toEqual(testcase[1]);
+ printfDecoder.decode(testcase[0], argFromStringBinary(testcase[4])),
+ ).toEqual(testcase[1]);
// Test unsigned
expect(
- printfDecoder
- .decode(testcase[2], argFromStringBinary(testcase[4])))
- .toEqual(testcase[3]);
+ printfDecoder.decode(testcase[2], argFromStringBinary(testcase[4])),
+ ).toEqual(testcase[3]);
}
});
@@ -83,8 +84,8 @@ describe('PrintfDecoder', () => {
expect(
printfDecoder.decode(
'Hello %s and %s',
- argsConcat(argFromString('Mac'), argFromString('PC'))
- )
+ argsConcat(argFromString('Mac'), argFromString('PC')),
+ ),
).toEqual('Hello Mac and PC');
});
@@ -92,31 +93,34 @@ describe('PrintfDecoder', () => {
const arg = argFromStringBinary('\xff\xff\x03');
expect(printfDecoder.decode('Number %d', arg)).toEqual('Number -32768');
expect(
- printfDecoder.decode('Numbers %u and %d', argsConcat(arg, arg))
+ printfDecoder.decode('Numbers %u and %d', argsConcat(arg, arg)),
).toEqual('Numbers 4294934528 and -32768');
expect(printfDecoder.decode('Growth is %u%', arg)).toEqual(
- 'Growth is 4294934528%'
+ 'Growth is 4294934528%',
);
});
it('formats char correctly', () => {
expect(
- printfDecoder.decode('Battery: 100%c', argFromStringBinary('\x4a'))
+ printfDecoder.decode('Battery: 100%c', argFromStringBinary('\x4a')),
).toEqual('Battery: 100%');
expect(
- printfDecoder.decode('Price: %c97.99', argFromStringBinary('\x48'))
+ printfDecoder.decode('Price: %c97.99', argFromStringBinary('\x48')),
).toEqual('Price: $97.99');
});
it('formats floats correctly', () => {
expect(
- printfDecoder.decode('Value: %f', argFromStringBinary('\xdb\x0f\x49\x40'))
+ printfDecoder.decode(
+ 'Value: %f',
+ argFromStringBinary('\xdb\x0f\x49\x40'),
+ ),
).toEqual('Value: 3.1415927410125732');
expect(
printfDecoder.decode(
'Value: %.5f',
- argFromStringBinary('\xdb\x0f\x49\x40')
- )
+ argFromStringBinary('\xdb\x0f\x49\x40'),
+ ),
).toEqual('Value: 3.14159');
});
});
diff --git a/pw_tokenizer/ts/token_database.ts b/pw_tokenizer/ts/token_database.ts
index bc5bcf56f..ab2b2f8a6 100644
--- a/pw_tokenizer/ts/token_database.ts
+++ b/pw_tokenizer/ts/token_database.ts
@@ -31,7 +31,7 @@ export class TokenDatabase {
private parseTokensToTokensMap(csv: string[]) {
for (const [lineNumber, line] of Object.entries(
- csv.map(line => line.split(/,/))
+ csv.map((line) => line.split(/,/)),
)) {
if (!line[0] || !line[2]) {
continue;
@@ -41,8 +41,8 @@ export class TokenDatabase {
console.error(
new Error(
`TokenDatabase number ${line[0]} at line ` +
- `${lineNumber} is not a valid hex number`
- )
+ `${lineNumber} is not a valid hex number`,
+ ),
);
continue;
}
diff --git a/pw_toolchain/BUILD.bazel b/pw_toolchain/BUILD.bazel
index 5f160c1f7..512ea27e1 100644
--- a/pw_toolchain/BUILD.bazel
+++ b/pw_toolchain/BUILD.bazel
@@ -17,7 +17,6 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load(":rust_toolchain.bzl", "pw_rust_toolchain")
package(default_visibility = ["//visibility:public"])
@@ -43,23 +42,5 @@ pw_cc_library(
srcs = ["wrap_abort.cc"],
linkopts = ["-Wl,--wrap=abort"],
deps = ["//pw_assert"],
-)
-
-# Define rust toolchains that are compatable with @bazel_embedded.
-pw_rust_toolchain(
- name = "thumbv7m_rust_linux_x86_64",
- exec_cpu = "x86_64",
- exec_os = "linux",
- exec_triple = "x86_64-unknown-linux-gnu",
- rust_target_triple = "thumbv7m-none-eabi",
- target_cpu = "armv7-m",
-)
-
-pw_rust_toolchain(
- name = "thumbv6m_rust_linux_x86_64",
- exec_cpu = "x86_64",
- exec_os = "linux",
- exec_triple = "x86_64-unknown-linux-gnu",
- rust_target_triple = "thumbv6m-none-eabi",
- target_cpu = "armv6-m",
+ alwayslink = 1,
)
diff --git a/pw_toolchain/BUILD.gn b/pw_toolchain/BUILD.gn
index a62608714..c0cff8a94 100644
--- a/pw_toolchain/BUILD.gn
+++ b/pw_toolchain/BUILD.gn
@@ -45,6 +45,10 @@ config("public_include_path") {
include_dirs = [ "public" ]
}
+group("builtins") {
+ deps = [ "$dir_pw_third_party/llvm_builtins" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_toolchain/CMakeLists.txt b/pw_toolchain/CMakeLists.txt
index 6570d8687..3ea428a9f 100644
--- a/pw_toolchain/CMakeLists.txt
+++ b/pw_toolchain/CMakeLists.txt
@@ -36,6 +36,7 @@ pw_add_test(pw_toolchain.no_destructor_test
SOURCES
no_destructor_test.cc
PRIVATE_DEPS
+ pw_assert.check
pw_toolchain.no_destructor
GROUPS
modules
diff --git a/pw_toolchain/arm_clang/BUILD.gn b/pw_toolchain/arm_clang/BUILD.gn
index 966014284..68c2e7308 100644
--- a/pw_toolchain/arm_clang/BUILD.gn
+++ b/pw_toolchain/arm_clang/BUILD.gn
@@ -42,8 +42,7 @@ cortex_m_hardware_fpu_v5_sp_flags =
# Default config added to all the ARM cortex M targets to link `nosys` library.
config("nosys") {
- # TODO(prabhukr): libs = ["nosys"] did not work as expected (pwrev/133110).
- ldflags = [ "-lnosys" ]
+ libs = [ "nosys" ]
}
config("enable_float_printf") {
diff --git a/pw_toolchain/arm_clang/clang_config.gni b/pw_toolchain/arm_clang/clang_config.gni
index de212b96a..7c979de4b 100644
--- a/pw_toolchain/arm_clang/clang_config.gni
+++ b/pw_toolchain/arm_clang/clang_config.gni
@@ -79,5 +79,12 @@ template("pw_clang_arm_config") {
cflags += _arm_flags.cflags
ldflags += _arm_flags.cflags
ldflags += _arm_flags.ldflags
+
+ libs = [
+ "c_nano",
+ "m",
+ "gcc",
+ "stdc++_nano",
+ ]
}
}
diff --git a/pw_toolchain/arm_clang/clang_flags.cmake b/pw_toolchain/arm_clang/clang_flags.cmake
new file mode 100644
index 000000000..a66045ccc
--- /dev/null
+++ b/pw_toolchain/arm_clang/clang_flags.cmake
@@ -0,0 +1,66 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Clang isn't a plug-and-play experience for Cortex-M baremetal targets; it's
+# missing C runtime libraries, C/C++ standard libraries, and a few other
+# things. This template uses the provided cflags, asmflags, ldflags, etc. to
+# generate a config that pulls the missing components from an arm-none-eabi-gcc
+# compiler on the system PATH. The end result is a clang-based compiler that
+# pulls in gcc-provided headers and libraries to complete the toolchain.
+#
+# This is effectively meant to be the cmake equivalent of clang_config.gni
+# which contains helper tools for getting these flags.
+function(_pw_get_clang_flags OUTPUT_VARIABLE TYPE)
+ execute_process(
+ COMMAND python
+ $ENV{PW_ROOT}/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
+ ${TYPE} -- ${ARGN}
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ OUTPUT_VARIABLE _result
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ )
+ set(${OUTPUT_VARIABLE} ${_result} PARENT_SCOPE)
+endfunction()
+
+# This returns the compile flags needed for compiling with clang as a string.
+#
+# Usage:
+#
+# pw_get_clang_compile_flags(OUTPUT_VARIABLE ARCH_FLAG [ARCH_FLAG ...])
+#
+# Example:
+#
+# # Retrieve the compile flags needed for this arch and store them in "result".
+# pw_get_clang_compile_flags(result -mcpu=cortex-m33 -mthumb -mfloat-abi=hard)
+#
+function(pw_get_clang_compile_flags OUTPUT_VARIABLE)
+ _pw_get_clang_flags(_result --cflags ${ARGN})
+ set(${OUTPUT_VARIABLE} ${_result} PARENT_SCOPE)
+endfunction()
+
+# This returns the link flags needed for compiling with clang as a string.
+#
+# Usage:
+#
+# pw_get_clang_link_flags(OUTPUT_VARIABLE ARCH_FLAG [ARCH_FLAG ...])
+#
+# Example:
+#
+# # Retrieve the compile flags needed for this arch and store them in "result".
+# pw_get_clang_link_flags(result -mcpu=cortex-m33 -mthumb -mfloat-abi=hard)
+#
+function(pw_get_clang_link_flags OUTPUT_VARIABLE)
+ _pw_get_clang_flags(_result --ldflags ${ARGN})
+ set(${OUTPUT_VARIABLE} ${_result} PARENT_SCOPE)
+endfunction()
diff --git a/pw_toolchain/arm_clang/toolchains.gni b/pw_toolchain/arm_clang/toolchains.gni
index 402082159..b1e824baa 100644
--- a/pw_toolchain/arm_clang/toolchains.gni
+++ b/pw_toolchain/arm_clang/toolchains.gni
@@ -14,13 +14,14 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/defaults.gni")
import("$dir_pw_toolchain/clang_tools.gni")
# Specifies the tools used by host Clang toolchains.
_arm_clang_toolchain = {
forward_variables_from(pw_toolchain_clang_tools, "*")
- link_whole_archive = true
+ link_group = true
# Enable static analysis for arm clang based toolchains.
static_analysis = {
@@ -75,168 +76,192 @@ pw_toolchain_arm_clang = {
name = "arm_clang_cortex_m0plus_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m0plus + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m0plus +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m0plus_speed_optimized = {
name = "arm_clang_cortex_m0plus_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m0plus + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m0plus +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m0plus_size_optimized = {
name = "arm_clang_cortex_m0plus_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m0plus + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m0plus +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m3_debug = {
name = "arm_clang_cortex_m3_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m3 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m3 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m3_speed_optimized = {
name = "arm_clang_cortex_m3_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m3 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m3 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m3_size_optimized = {
name = "arm_clang_cortex_m3_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m3 + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m3 +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m4_debug = {
name = "arm_clang_cortex_m4_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m4 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m4_speed_optimized = {
name = "arm_clang_cortex_m4_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m4 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m4_size_optimized = {
name = "arm_clang_cortex_m4_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4 + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m4 +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m4f_debug = {
name = "arm_clang_cortex_m4f_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4f + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m4f +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m4f_speed_optimized = {
name = "arm_clang_cortex_m4f_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4f + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m4f +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m4f_size_optimized = {
name = "arm_clang_cortex_m4f_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4f + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m4f +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m7_debug = {
name = "arm_clang_cortex_m7_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m7 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m7 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m7_speed_optimized = {
name = "arm_clang_cortex_m7_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m7 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m7 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m7_size_optimized = {
name = "arm_clang_cortex_m7_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m7 + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m7 +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m7f_debug = {
name = "arm_clang_cortex_m7f_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m7f + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m7f +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m7f_speed_optimized = {
name = "arm_clang_cortex_m7f_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m7f + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m7f +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m7f_size_optimized = {
name = "arm_clang_cortex_m7f_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m7f + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m7f +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m33_debug = {
name = "arm_clang_cortex_m33_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m33 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m33 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m33_speed_optimized = {
name = "arm_clang_cortex_m33_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m33 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m33 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m33_size_optimized = {
name = "arm_clang_cortex_m33_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m33 + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m33 +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m33f_debug = {
name = "arm_clang_cortex_m33f_debug"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m33f + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _cortex_m33f +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m33f_speed_optimized = {
name = "arm_clang_cortex_m33f_speed_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m33f + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _cortex_m33f +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m33f_size_optimized = {
name = "arm_clang_cortex_m33f_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m33f + [ "$dir_pw_build:optimize_size_clang" ]
+ default_configs = pigweed_default_configs + _cortex_m33f +
+ [ "$dir_pw_build:optimize_size_clang" ]
}
}
}
diff --git a/pw_toolchain/arm_gcc/BUILD.bazel b/pw_toolchain/arm_gcc/BUILD.bazel
index 5e3475134..e93e104b0 100644
--- a/pw_toolchain/arm_gcc/BUILD.bazel
+++ b/pw_toolchain/arm_gcc/BUILD.bazel
@@ -13,6 +13,11 @@
# the License.
load(
+ "@pw_toolchain//cc_toolchain:defs.bzl",
+ "pw_cc_toolchain",
+ "pw_cc_toolchain_feature",
+)
+load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
)
@@ -28,8 +33,9 @@ pw_cc_library(
"-Wl,--wrap=__sseek",
"-Wl,--wrap=__sclose",
],
- visibility = ["//visibility:private"],
+ visibility = ["//visibility:public"],
deps = ["//pw_assert"],
+ alwayslink = 1,
)
pw_cc_library(
@@ -40,3 +46,240 @@ pw_cc_library(
"//pw_toolchain:wrap_abort",
],
)
+
+# Although we use similar warnings for clang and arm_gcc, we don't have one
+# centralized list, since we might want to use different warnings based on the
+# compiler in the future.
+pw_cc_toolchain_feature(
+ name = "warnings",
+ copts = [
+ "-Wall",
+ "-Wextra",
+ # Make all warnings errors, except for the exemptions below.
+ "-Werror",
+ "-Wno-error=cpp", # preprocessor #warning statement
+ "-Wno-error=deprecated-declarations", # [[deprecated]] attribute
+ ],
+)
+
+pw_cc_toolchain_feature(
+ name = "sysroot",
+ builtin_sysroot = "external/gcc_arm_none_eabi_toolchain",
+ cxx_builtin_include_directories = [
+ "%sysroot%/arm-none-eabi/include/newlib-nano",
+ "%sysroot%/arm-none-eabi/include/c++/12.2.1",
+ "%sysroot%/arm-none-eabi/include/c++/12.2.1/arm-none-eabi",
+ "%sysroot%/arm-none-eabi/include/c++/12.2.1/backward",
+ "%sysroot%/lib/gcc/arm-none-eabi/12.2.1/include",
+ "%sysroot%/lib/gcc/arm-none-eabi/12.2.1/include-fixed",
+ "%sysroot%/arm-none-eabi/include",
+ ],
+)
+
+pw_cc_toolchain_feature(
+ name = "cortex_common",
+ asmopts = [
+ "-mabi=aapcs",
+ "-mthumb",
+ ],
+ copts = [
+ "-ffreestanding",
+ "-Wno-psabi",
+ "-specs=nano.specs",
+ "-specs=nosys.specs",
+ ],
+ linkopts = [
+ "-Wl,--gc-sections",
+ "-specs=nano.specs",
+ "-specs=nosys.specs",
+ "-lstdc++",
+ "-lnosys",
+ "-lc",
+ "-lm",
+ "-Wl,--no-warn-rwx-segment",
+ ],
+)
+
+_MCPU = [
+ "cortex-m0",
+ "cortex-m3",
+ "cortex-m4",
+ "cortex-m4+nofp",
+ "cortex-m33",
+ "cortex-m33+nofp",
+]
+
+_CORTEX_M0_OPTS = [
+ "-mcpu=cortex-m0",
+ "-mfloat-abi=soft",
+]
+
+pw_cc_toolchain_feature(
+ name = "cortex-m0",
+ asmopts = _CORTEX_M0_OPTS,
+ copts = _CORTEX_M0_OPTS,
+ linkopts = _CORTEX_M0_OPTS,
+)
+
+_CORTEX_M3_OPTS = [
+ "-mcpu=cortex-m3",
+ "-mfloat-abi=soft",
+]
+
+pw_cc_toolchain_feature(
+ name = "cortex-m3",
+ asmopts = _CORTEX_M3_OPTS,
+ copts = _CORTEX_M3_OPTS,
+ linkopts = _CORTEX_M3_OPTS,
+)
+
+_CORTEX_M4_OPTS = [
+ "-mcpu=cortex-m4",
+ "-mfloat-abi=hard",
+]
+
+pw_cc_toolchain_feature(
+ name = "cortex-m4",
+ asmopts = _CORTEX_M4_OPTS,
+ copts = _CORTEX_M4_OPTS,
+ linkopts = _CORTEX_M4_OPTS,
+)
+
+_CORTEX_M4_NOFP_OPTS = [
+ "-mcpu=cortex-m4+nofp",
+ "-mfloat-abi=soft",
+]
+
+pw_cc_toolchain_feature(
+ name = "cortex-m4+nofp",
+ asmopts = _CORTEX_M4_NOFP_OPTS,
+ copts = _CORTEX_M4_NOFP_OPTS,
+ linkopts = _CORTEX_M4_NOFP_OPTS,
+)
+
+_CORTEX_M33_OPTS = [
+ "-mcpu=cortex-m33",
+ "-mfloat-abi=hard",
+]
+
+pw_cc_toolchain_feature(
+ name = "cortex-m33",
+ asmopts = _CORTEX_M33_OPTS,
+ copts = _CORTEX_M33_OPTS,
+ linkopts = _CORTEX_M33_OPTS,
+)
+
+_CORTEX_M33_NOFP_OPTS = [
+ "-mcpu=cortex-m33+nofp",
+ "-mfloat-abi=soft",
+]
+
+pw_cc_toolchain_feature(
+ name = "cortex-m33+nofp",
+ asmopts = _CORTEX_M33_NOFP_OPTS,
+ copts = _CORTEX_M33_NOFP_OPTS,
+ linkopts = _CORTEX_M33_NOFP_OPTS,
+)
+
+# Using a list comprehension here to avoid the mind-numbing boilerplate.
+#
+# TODO(tpudlik): We ought to refactor the pw_cc_toolchain API so that the
+# *_files and tools don't need to be retyped every time you want to create a
+# variant with different feature deps.
+[pw_cc_toolchain(
+ name = "arm_gcc_toolchain_" + mcpu,
+ abi_libc_version = "unknown",
+ abi_version = "unknown",
+ all_files = "@gcc_arm_none_eabi_toolchain//:all",
+ ar = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-ar",
+ ar_files = "@gcc_arm_none_eabi_toolchain//:all",
+ as_files = "@gcc_arm_none_eabi_toolchain//:all",
+ compiler = "unknown",
+ compiler_files = "@gcc_arm_none_eabi_toolchain//:all",
+ coverage_files = "@gcc_arm_none_eabi_toolchain//:all",
+ cpp = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-gcc",
+ dwp_files = "@gcc_arm_none_eabi_toolchain//:all",
+ feature_deps = [
+ "@pw_toolchain//features:o2",
+ "@pw_toolchain//features:c++17",
+ "@pw_toolchain//features:debugging",
+ "@pw_toolchain//features:reduced_size",
+ "@pw_toolchain//features:no_canonical_prefixes",
+ "@pw_toolchain//features:no_rtti",
+ "@pw_toolchain//features:wno_register",
+ "@pw_toolchain//features:wnon_virtual_dtor",
+ ":" + mcpu,
+ ":sysroot",
+ ":cortex_common",
+ ":warnings",
+ ],
+ gcc = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-gcc",
+ gcov = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-gcov",
+ host_system_name = "unknown",
+ ld = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-ld",
+ linker_files = "@gcc_arm_none_eabi_toolchain//:all",
+ objcopy = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-objcopy",
+ objcopy_files = "@gcc_arm_none_eabi_toolchain//:all",
+ objdump = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-objdump",
+ strip = "@gcc_arm_none_eabi_toolchain//:bin/arm-none-eabi-strip",
+ strip_files = "@gcc_arm_none_eabi_toolchain//:all",
+ supports_param_files = 0,
+ target_cpu = "unknown",
+ target_libc = "unknown",
+ target_system_name = "unknown",
+ toolchain_identifier = "arm-gcc-toolchain",
+) for mcpu in _MCPU]
+
+toolchain(
+ name = "arm_gcc_cc_toolchain_cortex-m0",
+ target_compatible_with = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m0",
+ ],
+ toolchain = ":arm_gcc_toolchain_cortex-m0",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+toolchain(
+ name = "arm_gcc_cc_toolchain_cortex-m3",
+ target_compatible_with = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m3",
+ ],
+ toolchain = ":arm_gcc_toolchain_cortex-m3",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+toolchain(
+ name = "arm_gcc_cc_toolchain_cortex-m4",
+ target_compatible_with = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m4",
+ ],
+ toolchain = ":arm_gcc_toolchain_cortex-m4",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+toolchain(
+ name = "arm_gcc_cc_toolchain_cortex-m4+nofp",
+ target_compatible_with = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m4+nofp",
+ ],
+ toolchain = ":arm_gcc_toolchain_cortex-m4+nofp",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+toolchain(
+ name = "arm_gcc_cc_toolchain_cortex-m33",
+ target_compatible_with = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m33",
+ ],
+ toolchain = ":arm_gcc_toolchain_cortex-m33",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+toolchain(
+ name = "arm_gcc_cc_toolchain_cortex-m33+nofp",
+ target_compatible_with = [
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m33+nofp",
+ ],
+ toolchain = ":arm_gcc_toolchain_cortex-m33+nofp",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
diff --git a/pw_toolchain/arm_gcc/BUILD.gn b/pw_toolchain/arm_gcc/BUILD.gn
index 436c4de83..f0a9d7e3c 100644
--- a/pw_toolchain/arm_gcc/BUILD.gn
+++ b/pw_toolchain/arm_gcc/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("//build_overrides/pigweed_environment.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
# Disable obnoxious ABI warning.
#
@@ -34,6 +35,16 @@ config("disable_psabi_warning") {
cflags = [ "-Wno-psabi" ]
}
+# binutils 2.39 introduced new warnings that trigger on embedded ARM GCC builds:
+#
+# warning: *.elf has a LOAD segment with RWX permissions
+#
+# Disable these warnings --no-warn-rwx-segment. For details see:
+# https://www.redhat.com/en/blog/linkers-warnings-about-executable-stacks-and-segments
+config("disable_rwx_segment_warning") {
+ ldflags = [ "-Wl,--no-warn-rwx-segment" ]
+}
+
config("cortex_common") {
asmflags = [
"-mabi=aapcs",
@@ -85,6 +96,12 @@ config("cortex_m33") {
ldflags = cflags
}
+config("cortex_a32") {
+ cflags = [ "-mcpu=cortex-a32" ]
+ asmflags = cflags
+ ldflags = cflags
+}
+
config("cortex_software_fpu") {
cflags = [ "-mfloat-abi=soft" ]
asmflags = cflags
@@ -135,14 +152,18 @@ pw_source_set("newlib_os_interface_stubs") {
all_dependent_configs = [ ":wrap_newlib_stdio_functions" ]
sources = [ "newlib_os_interface_stubs.cc" ]
deps = [ dir_pw_assert ]
- visibility = [ ":*" ]
}
# Basic libraries any arm-none-eabi-gcc target should use. This library should
# be included in pw_build_LINK_DEPS.
group("arm_none_eabi_gcc_support") {
- deps = [
- ":newlib_os_interface_stubs",
- "$dir_pw_toolchain:wrap_abort",
- ]
+ deps = [ "$dir_pw_toolchain:wrap_abort" ]
+
+ # TODO: b/301079199 - Stubs are not yet usable on clang.
+ # AttempedToInvokeUnsupportedNewlibOsInterfaceFunction (write_) is triggering,
+ # but not sure why yet.
+ # TODO: b/301262374 - Provide a better way to detect the compiler type.
+ if (get_path_info(pw_toolchain_SCOPE.cc, "file") != "clang") {
+ deps += [ ":newlib_os_interface_stubs" ]
+ }
}
diff --git a/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc b/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc
index 03184e417..a09b3f48e 100644
--- a/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc
+++ b/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc
@@ -86,6 +86,7 @@ PW_DISABLE_NEWLIB_FUNCTION(getpid, void)
// documentation since isatty is called indirectly by snprintf.
extern "C" int _isatty(int) { return 1; }
+PW_DISABLE_NEWLIB_FUNCTION(gettimeofday, struct timeval*, struct timezone*)
PW_DISABLE_NEWLIB_FUNCTION(kill, int, int)
PW_DISABLE_NEWLIB_FUNCTION(link, char*, char*)
PW_DISABLE_NEWLIB_FUNCTION(lseek, int, int, int)
diff --git a/pw_toolchain/arm_gcc/toolchains.gni b/pw_toolchain/arm_gcc/toolchains.gni
index c7a138564..48fc3ab46 100644
--- a/pw_toolchain/arm_gcc/toolchains.gni
+++ b/pw_toolchain/arm_gcc/toolchains.gni
@@ -14,12 +14,66 @@
import("//build_overrides/pigweed.gni")
import("//build_overrides/pigweed_environment.gni")
+
+import("$dir_pw_build/defaults.gni")
import("$dir_pw_toolchain/rbe.gni")
+_default_compiler_prefix = ""
+if (defined(pw_env_setup_CIPD_ARM)) {
+ _default_compiler_prefix = pw_env_setup_CIPD_ARM + "/bin/"
+}
+
+declare_args() {
+ # This flag allows you to specify a prefix for ARM GCC tools use when
+ # compiling with an arm-none-eabi toolchain. This is useful for debugging
+ # toolchain-related issues, or for building with an externally-provided
+ # toolchain.
+ #
+ # Pigweed toolchains should NOT override this variable so projects or users
+ # can control it via `.gn` or by setting it as a regular gn argument (e.g.
+ # `gn gen --args='pw_toolchain_ARM_NONE_EABI_PREFIX=/path/to/my-'`).
+ #
+ # Examples:
+ # pw_toolchain_ARM_NONE_EABI_PREFIX = ""
+ # command: "arm-none-eabi-gcc" (from PATH)
+ #
+ # pw_toolchain_ARM_NONE_EABI_PREFIX = "my-"
+ # command: "my-arm-none-eabi-gcc" (from PATH)
+ #
+ # pw_toolchain_ARM_NONE_EABI_PREFIX = "/bin/my-"
+ # command: "/bin/my-arm-none-eabi-gcc" (absolute path)
+ #
+ # pw_toolchain_ARM_NONE_EABI_PREFIX = "//environment/gcc_next/"
+ # command: "../environment/gcc_next/arm-none-eabi-gcc" (relative path)
+ #
+ # GN templates should use `arm_gcc_toolchain_tools.*` to get the intended
+ # command string rather than relying directly on
+ # pw_toolchain_ARM_NONE_EABI_PREFIX.
+ #
+ # If the prefix begins with "//", it will be rebased to be relative to the
+ # root build directory.
+ pw_toolchain_ARM_NONE_EABI_PREFIX = _default_compiler_prefix
+}
+
# Specifies the tools used by ARM GCC toolchains.
arm_gcc_toolchain_tools = {
_rbe_debug_flag = ""
_local_tool_name_root = "arm-none-eabi-"
+
+ _toolchain_prefix = pw_toolchain_ARM_NONE_EABI_PREFIX
+ if (_toolchain_prefix != "") {
+ # If the prefix is a GN-absolute path, rebase it so it's relative to the
+ # root of the build directory.
+ _split_prefix = string_split(_toolchain_prefix, "//")
+ if (_split_prefix[0] == "") {
+ _toolchain_prefix = rebase_path(_toolchain_prefix, root_build_dir)
+ }
+ _local_tool_name_root = _toolchain_prefix + _local_tool_name_root
+ }
+ if (host_os == "win") {
+ _local_tool_name_root = string_replace(_local_tool_name_root, "/", "\\")
+ }
+
if (pw_toolchain_USE_RBE) {
if (pw_toolchain_RBE_DEBUG) {
_rbe_debug_flag = " -v "
@@ -43,11 +97,14 @@ arm_gcc_toolchain_tools = {
ar = _local_tool_name_root + "ar"
ld = _local_tool_name_root + "g++"
- link_whole_archive = true
+ link_group = true
}
# Common configs shared by all ARM GCC toolchains.
-_arm_gcc = [ "$dir_pw_toolchain/arm_gcc:disable_psabi_warning" ]
+_arm_gcc = [
+ "$dir_pw_toolchain/arm_gcc:disable_psabi_warning",
+ "$dir_pw_toolchain/arm_gcc:disable_rwx_segment_warning",
+]
_cortex_m0plus = [
"$dir_pw_toolchain/arm_gcc:cortex_common",
@@ -98,198 +155,229 @@ _cortex_m33f = [
"$dir_pw_toolchain/arm_gcc:cortex_hardware_fpu_v5_sp",
]
+_cortex_a32 = [
+ "$dir_pw_toolchain/arm_gcc:cortex_common",
+ "$dir_pw_toolchain/arm_gcc:cortex_common",
+ "$dir_pw_toolchain/arm_gcc:cortex_a32",
+]
+
# Describes ARM GCC toolchains for specific targets.
pw_toolchain_arm_gcc = {
cortex_m0plus_debug = {
name = "arm_gcc_cortex_m0plus_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m0plus + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m0plus +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m0plus_speed_optimized = {
name = "arm_gcc_cortex_m0plus_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m0plus + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m0plus +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m0plus_size_optimized = {
name = "arm_gcc_cortex_m0plus_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m0plus + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m0plus +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m3_debug = {
name = "arm_gcc_cortex_m3_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m3 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m3 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m3_speed_optimized = {
name = "arm_gcc_cortex_m3_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m3 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m3 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m3_size_optimized = {
name = "arm_gcc_cortex_m3_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m3 + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m3 +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m4_debug = {
name = "arm_gcc_cortex_m4_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m4 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m4 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m4_speed_optimized = {
name = "arm_gcc_cortex_m4_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m4 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m4 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m4_size_optimized = {
name = "arm_gcc_cortex_m4_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m4 + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m4 +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m4f_debug = {
name = "arm_gcc_cortex_m4f_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m4f + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m4f +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m4f_speed_optimized = {
name = "arm_gcc_cortex_m4f_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m4f + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m4f +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m4f_size_optimized = {
name = "arm_gcc_cortex_m4f_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m4f + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m4f +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m7_debug = {
name = "arm_gcc_cortex_m7_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m7 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m7 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m7_speed_optimized = {
name = "arm_gcc_cortex_m7_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m7 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m7 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m7_size_optimized = {
name = "arm_gcc_cortex_m7_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m7 + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m7 +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m7f_debug = {
name = "arm_gcc_cortex_m7f_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m7f + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m7f +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m7f_speed_optimized = {
name = "arm_gcc_cortex_m7f_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m7f + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m7f +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m7f_size_optimized = {
name = "arm_gcc_cortex_m7f_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m7f + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m7f +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m33_debug = {
name = "arm_gcc_cortex_m33_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m33 + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m33 +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m33_speed_optimized = {
name = "arm_gcc_cortex_m33_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m33 + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m33 +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m33_size_optimized = {
name = "arm_gcc_cortex_m33_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m33 + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m33 +
+ [ "$dir_pw_build:optimize_size" ]
}
}
cortex_m33f_debug = {
name = "arm_gcc_cortex_m33f_debug"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m33f + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m33f +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
cortex_m33f_speed_optimized = {
name = "arm_gcc_cortex_m33f_speed_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m33f + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m33f +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
cortex_m33f_size_optimized = {
name = "arm_gcc_cortex_m33f_size_optimized"
forward_variables_from(arm_gcc_toolchain_tools, "*")
defaults = {
- default_configs =
- _arm_gcc + _cortex_m33f + [ "$dir_pw_build:optimize_size" ]
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_m33f +
+ [ "$dir_pw_build:optimize_size" ]
+ }
+ }
+
+ cortex_a32_debug = {
+ name = "arm_gcc_cortex_a32_debug"
+ forward_variables_from(arm_gcc_toolchain_tools, "*")
+ defaults = {
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_a32 +
+ [ "$dir_pw_build:optimize_debugging" ]
+ }
+ }
+ cortex_a32_speed_optimized = {
+ name = "arm_gcc_cortex_a32_speed_optimized"
+ forward_variables_from(arm_gcc_toolchain_tools, "*")
+ defaults = {
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_a32 +
+ [ "$dir_pw_build:optimize_speed" ]
+ }
+ }
+ cortex_a32_size_optimized = {
+ name = "arm_gcc_cortex_a32_size_optimized"
+ forward_variables_from(arm_gcc_toolchain_tools, "*")
+ defaults = {
+ default_configs = pigweed_default_configs + _arm_gcc + _cortex_a32 +
+ [ "$dir_pw_build:optimize_size" ]
}
}
}
@@ -322,4 +410,12 @@ pw_toolchain_arm_gcc_list = [
pw_toolchain_arm_gcc.cortex_m33f_debug,
pw_toolchain_arm_gcc.cortex_m33f_speed_optimized,
pw_toolchain_arm_gcc.cortex_m33f_size_optimized,
+ pw_toolchain_arm_gcc.cortex_a32_debug,
+ pw_toolchain_arm_gcc.cortex_a32_speed_optimized,
+ pw_toolchain_arm_gcc.cortex_a32_size_optimized,
]
+
+# Configs that require Arm GCC 12 or newer to use. Downstream projects that use
+# older compilers should remove these from default_configs.
+pw_toolchain_arm_gcc_12_configs =
+ [ "$dir_pw_toolchain/arm_gcc:disable_rwx_segment_warning" ]
diff --git a/pw_toolchain/clang_tools.gni b/pw_toolchain/clang_tools.gni
index 56796f4e4..5b438a594 100644
--- a/pw_toolchain/clang_tools.gni
+++ b/pw_toolchain/clang_tools.gni
@@ -15,17 +15,54 @@ import("//build_overrides/pigweed.gni")
import("//build_overrides/pigweed_environment.gni")
import("$dir_pw_toolchain/rbe.gni")
+_default_llvm_prefix = ""
+_default_rust_prefix = ""
+
+# If Pigweed's CIPD environment setup was run, assume a LLVM toolchain and Rust
+# compiler are present there.
+if (defined(pw_env_setup_CIPD_PIGWEED)) {
+ _default_llvm_prefix = pw_env_setup_CIPD_PIGWEED + "/bin/"
+ _default_rust_prefix = pw_env_setup_CIPD_PIGWEED + "/bin/"
+}
+
declare_args() {
- # This flag allows you to specify the root directory of the clang, clang++,
+ # This flag allows you to specify a prefix to use for clang, clang++,
# and llvm-ar binaries to use when compiling with a clang-based toolchain.
# This is useful for debugging toolchain-related issues by building with an
# externally-provided toolchain.
- pw_toolchain_CLANG_PREFIX =
- rebase_path(pw_env_setup_CIPD_PIGWEED + "/bin/", root_build_dir)
+ #
+ # Pigweed toolchains should NOT override this variable so projects or users
+ # can control it via `.gn` or by setting it as a regular gn argument (e.g.
+ # `gn gen --args='pw_toolchain_CLANG_PREFIX=/path/to/my-llvm-'`).
+ #
+ # Examples:
+ # pw_toolchain_CLANG_PREFIX = ""
+ # command: "clang" (from PATH)
+ #
+ # pw_toolchain_CLANG_PREFIX = "my-"
+ # command: "my-clang" (from PATH)
+ #
+ # pw_toolchain_CLANG_PREFIX = "/bin/my-"
+ # command: "/bin/my-clang" (absolute path)
+ #
+ # pw_toolchain_CLANG_PREFIX = "//environment/clang_next/"
+ # command: "../environment/clang_next/clang" (relative path)
+ #
+ # GN templates should use `pw_toolchain_clang_tools.*` to get the intended
+ # command string rather than relying directly on pw_toolchain_CLANG_PREFIX.
+ #
+ # If the prefix begins with "//", it will be rebased to be relative to the
+ # root build directory.
+ pw_toolchain_CLANG_PREFIX = _default_llvm_prefix
- # This flag allows you to specify the root directory of the rustc binary.
- pw_toolchain_RUST_PREFIX =
- rebase_path(pw_env_setup_CIPD_PIGWEED + "/rust/bin/", root_build_dir)
+ # This flag allows you to specify a prefix for rustc.
+ #
+ # This follows the same rules as pw_toolchain_CLANG_PREFIX, see above for
+ # more information.
+ #
+ # If the prefix begins with "//", it will be rebased to be relative to the
+ # root build directory.
+ pw_toolchain_RUST_PREFIX = _default_rust_prefix
}
pw_toolchain_clang_tools = {
@@ -33,17 +70,42 @@ pw_toolchain_clang_tools = {
cc = "clang"
cxx = "clang++"
ld = cxx
+ llvm_cov = "llvm-cov"
+ llvm_profdata = "llvm-profdata"
rustc = "rustc"
- if (pw_toolchain_CLANG_PREFIX != "") {
- ar = pw_toolchain_CLANG_PREFIX + ar
- cc = pw_toolchain_CLANG_PREFIX + cc
- cxx = pw_toolchain_CLANG_PREFIX + cxx
- ld = pw_toolchain_CLANG_PREFIX + ld
+ _toolchain_prefix = pw_toolchain_CLANG_PREFIX
+ if (_toolchain_prefix != "") {
+ # If the prefix is a GN-absolute path, rebase it so it's relative to the
+ # root of the build directory.
+ _split_prefix = string_split(_toolchain_prefix, "//")
+ if (_split_prefix[0] == "") {
+ _toolchain_prefix = rebase_path(_toolchain_prefix, root_build_dir)
+ }
+ if (host_os == "win") {
+ _toolchain_prefix = "./" + _toolchain_prefix
+ _toolchain_prefix = string_replace(_toolchain_prefix, "/", "\\")
+ }
+ ar = _toolchain_prefix + ar
+ cc = _toolchain_prefix + cc
+ cxx = _toolchain_prefix + cxx
+ ld = _toolchain_prefix + ld
+ llvm_cov = _toolchain_prefix + llvm_cov
+ llvm_profdata = _toolchain_prefix + llvm_profdata
}
- if (pw_toolchain_RUST_PREFIX != "") {
- rustc = pw_toolchain_RUST_PREFIX + rustc
+ _rust_prefix = pw_toolchain_RUST_PREFIX
+ if (host_os == "win") {
+ _rust_prefix = string_replace(_rust_prefix, "/", "\\")
+ }
+ if (_rust_prefix != "") {
+ # If the prefix is a GN-absolute path, rebase it so it's relative to the
+ # root of the build directory.
+ _split_rust_prefix = string_split(_rust_prefix, "//")
+ if (_split_rust_prefix[0] == "") {
+ _rust_prefix = rebase_path(_rust_prefix, root_build_dir)
+ }
+ rustc = _rust_prefix + rustc
}
if (pw_toolchain_USE_RBE) {
diff --git a/pw_toolchain/docs.rst b/pw_toolchain/docs.rst
index 46958c132..08d5a400d 100644
--- a/pw_toolchain/docs.rst
+++ b/pw_toolchain/docs.rst
@@ -85,12 +85,12 @@ dependency to the generated ``.d`` files for the
.. code-block::
- static_analysis = {
- clang_tidy_path = "//third_party/ctcache/clang-tidy"
- _clang_tidy_cfg_path = rebase_path("//.clang-tidy", root_build_dir)
- cc_post = "echo '-: $_clang_tidy_cfg_path' >> {{output}}.d"
- cxx_post = "echo '-: $_clang_tidy_cfg_path' >> {{output}}.d"
- }
+ static_analysis = {
+ clang_tidy_path = "//third_party/ctcache/clang-tidy"
+ _clang_tidy_cfg_path = rebase_path("//.clang-tidy", root_build_dir)
+ cc_post = "echo '-: $_clang_tidy_cfg_path' >> {{output}}.d"
+ cxx_post = "echo '-: $_clang_tidy_cfg_path' >> {{output}}.d"
+ }
Excluding files from checks
===========================
@@ -211,56 +211,30 @@ Newlib OS interface
`Newlib <https://sourceware.org/newlib/>`_, the C Standard Library
implementation provided with ``arm-none-eabi-gcc``, defines a set of `OS
interface functions <https://sourceware.org/newlib/libc.html#Stubs>`_ that
-should be implemented. A default is provided if these functions are not
-implemented, but using the default results in a compiler warning.
+should be implemented. Newlib provides default implementations, but using these
+results in linker warnings like the following:
+
+.. code-block:: none
+
+ readr.c:(.text._read_r+0x10): warning: _read is not implemented and will always fail
Most of the OS interface functions should never be called in embedded builds.
The ``pw_toolchain/arg_gcc:newlib_os_interface_stubs`` library, which is
provided through ``pw_toolchain/arm_gcc:arm_none_eabi_gcc_support``, implements
-these functions and forces a linker error if they are used. It also wraps some
-functions related to use of ``stdout`` and ``stderr`` that abort if they are
-called.
+these functions and forces a linker error if they are used. It also
+automatically includes a wrapper for ``abort`` for use of ``stdout`` and
+``stderr`` which abort if they are called.
+
+If you need to use your own wrapper for ``abort``, include the library directly
+using ``pw_toolchain/arm_gcc:newlib_os_interface_stubs``.
pw_toolchain/no_destructor.h
============================
-.. cpp:class:: template <typename T> pw::NoDestructor
-
- Helper type to create a function-local static variable of type ``T`` when
- ``T`` has a non-trivial destructor. Storing a ``T`` in a
- ``pw::NoDestructor<T>`` will prevent ``~T()`` from running, even when the
- variable goes out of scope.
-
- This class is useful when a variable has static storage duration but its type
- has a non-trivial destructor. Destructor ordering is not defined and can
- cause issues in multithreaded environments. Additionally, removing destructor
- calls can save code size.
-
- Except in generic code, do not use ``pw::NoDestructor<T>`` with trivially
- destructible types. Use the type directly instead. If the variable can be
- constexpr, make it constexpr.
-
- ``pw::NoDestructor<T>`` provides a similar API to std::optional. Use ``*`` or
- ``->`` to access the wrapped type.
-
- ``pw::NoDestructor<T>`` is based on Chromium's ``base::NoDestructor<T>`` in
- `src/base/no_destructor.h <https://chromium.googlesource.com/chromium/src/base/+/5ea6e31f927aa335bfceb799a2007c7f9007e680/no_destructor.h>`_.
-
- In Clang, ``pw::NoDestructor`` can be replaced with the `[[clang::no_destroy]]
- <https://clang.llvm.org/docs/AttributeReference.html#no-destroy>`_.
- attribute.
-
-Example usage
--------------
-.. code-block:: cpp
-
- pw::sync::Mutex& GetMutex() {
- // Use NoDestructor to avoid running the mutex destructor when exit-time
- // destructors run.
- static const pw::NoDestructor<pw::sync::Mutex> global_mutex;
- return *global_mutex;
- }
-
-.. warning::
-
- Misuse of :cpp:class:`pw::NoDestructor` can cause resource leaks and other
- problems. Only skip destructors when you know it is safe to do so.
+.. doxygenclass:: pw::NoDestructor
+
+builtins
+========
+builtins are LLVM's equivalent of libgcc, the compiler will insert calls to
+these routines. Setting the ``dir_pw_third_party_builtins`` gn var to your
+compiler-rt/builtins checkout will enable building builtins from source instead
+of relying on the shipped libgcc.
diff --git a/pw_toolchain/generate_toolchain.gni b/pw_toolchain/generate_toolchain.gni
index ff12f0b70..755a6d121 100644
--- a/pw_toolchain/generate_toolchain.gni
+++ b/pw_toolchain/generate_toolchain.gni
@@ -44,6 +44,8 @@ declare_args() {
# all object files when resolving symbols.
# link_group: (optional) Boolean indicating if the linker should use
# a group to resolve circular dependencies between artifacts.
+# link_generate_map_file: (optional) Boolean indicating if to add linker
+# flags to generate a mapfile. Defaults to true.
# generate_from: (optional) The full target name of the toolchain that can
# trigger this toolchain to be generated. GN only allows one toolchain to
# be generated at a given target path, so if multiple toolchains parse the
@@ -64,7 +66,7 @@ declare_args() {
# current_os: The OS of the toolchain. Defaults to "".
# Well known values include "win", "mac", "linux", "android", and "ios".
#
-# TODO(b/234891809): This should be renamed to pw_generate_toolchain.
+# TODO: b/234891809 - This should be renamed to pw_generate_toolchain.
template("generate_toolchain") {
assert(defined(invoker.defaults), "toolchain is missing 'defaults'")
@@ -247,7 +249,6 @@ template("generate_toolchain") {
_link_outfile =
"{{output_dir}}/{{target_output_name}}{{output_extension}}"
- _link_mapfile = "{{output_dir}}/{{target_output_name}}.map"
if (defined(invoker.ld)) {
_link_flags = [
invoker.ld,
@@ -260,16 +261,30 @@ template("generate_toolchain") {
]
}
- if (toolchain_os == "mac" || toolchain_os == "ios") {
- _link_flags += [
- # Output a map file that shows symbols and their location.
- "-Wl,-map,$_link_mapfile",
- ]
+ if (defined(invoker.link_generate_map_file)) {
+ _link_generate_map_file = invoker.link_generate_map_file
} else {
- _link_flags += [
- # Output a map file that shows symbols and their location.
- "-Wl,-Map,$_link_mapfile",
- ]
+ _link_generate_map_file = true
+ }
+
+ _link_outputs = [ _link_outfile ]
+
+ if (_link_generate_map_file) {
+ _link_mapfile = "{{output_dir}}/{{target_output_name}}.map"
+
+ if (toolchain_os == "mac" || toolchain_os == "ios") {
+ _link_flags += [
+ # Output a map file that shows symbols and their location.
+ "-Wl,-map,$_link_mapfile",
+ ]
+ } else {
+ _link_flags += [
+ # Output a map file that shows symbols and their location.
+ "-Wl,-Map,$_link_mapfile",
+ "-Wl,--cref",
+ ]
+ }
+ _link_outputs += [ _link_mapfile ]
}
_rsp_file = "$_link_outfile.rsp"
@@ -315,10 +330,7 @@ template("generate_toolchain") {
rspfile = _rsp_file
rspfile_content = _rsp_command
description = "ld $_link_outfile"
- outputs = [
- _link_outfile,
- _link_mapfile,
- ]
+ outputs = _link_outputs
default_output_dir = "{{target_out_dir}}/bin"
if (defined(invoker.final_binary_extension)) {
@@ -335,10 +347,7 @@ template("generate_toolchain") {
rspfile = _rsp_file
rspfile_content = _rsp_command
description = "ld -shared $_link_outfile"
- outputs = [
- _link_outfile,
- _link_mapfile,
- ]
+ outputs = _link_outputs
default_output_dir = "{{target_out_dir}}/lib"
default_output_extension = ".so"
}
@@ -378,10 +387,18 @@ template("generate_toolchain") {
_generate_rust_tools = defined(invoker.rustc)
if (_generate_rust_tools) {
+ if (toolchain_os == "mac") {
+ _dylib_extension = ".dylib"
+ } else if (toolchain_os == "win") {
+ _dylib_extension = ".dll"
+ } else {
+ _dylib_extension = ".so"
+ }
+
_rustc_command = string_join(
" ",
[
- # TODO(b/234872510): Ensure this works with Windows.
+ # TODO: b/234872510 - Ensure this works with Windows.
"RUST_BACKTRACE=1",
"{{rustenv}}",
invoker.rustc,
@@ -416,6 +433,16 @@ template("generate_toolchain") {
command = _rustc_command
outputs = [ _output ]
}
+
+ tool("rust_macro") {
+ description = "rustc {{output}}"
+ default_output_dir = "{{target_out_dir}}/lib"
+ depfile = "{{output}}.d"
+ output_prefix = "lib"
+ default_output_extension = _dylib_extension
+ command = _rustc_command
+ outputs = [ _output ]
+ }
}
}
diff --git a/pw_toolchain/host_clang/BUILD.bazel b/pw_toolchain/host_clang/BUILD.bazel
new file mode 100644
index 000000000..7f8009787
--- /dev/null
+++ b/pw_toolchain/host_clang/BUILD.bazel
@@ -0,0 +1,265 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "@pw_toolchain//cc_toolchain:defs.bzl",
+ "ALL_CPP_COMPILER_ACTIONS",
+ "ALL_C_COMPILER_ACTIONS",
+ "pw_cc_flag_set",
+ "pw_cc_toolchain",
+ "pw_cc_toolchain_feature",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+filegroup(name = "empty")
+
+pw_cc_toolchain_feature(
+ name = "macos_stdlib",
+ cxx_builtin_include_directories = [
+ "%package(@llvm_toolchain//)%/include/c++/v1",
+ "%package(@llvm_toolchain//)%/lib/clang/17/include",
+ ],
+ linker_files = ["@llvm_toolchain//:lib/libc++.a"],
+ target_compatible_with = ["@platforms//os:macos"],
+)
+
+pw_cc_toolchain_feature(
+ name = "linux_sysroot",
+ builtin_sysroot = "external/linux_sysroot",
+ cxx_builtin_include_directories = [
+ "%package(@llvm_toolchain//)%/include/x86_64-unknown-linux-gnu/c++/v1",
+ "%package(@llvm_toolchain//)%/include/c++/v1",
+ "%package(@llvm_toolchain//)%/lib/clang/17/include",
+ "%sysroot%/usr/local/include",
+ "%sysroot%/usr/include/x86_64-linux-gnu",
+ "%sysroot%/usr/include",
+ ],
+ linkopts = [
+ "-pthread",
+ "-stdlib=libc++",
+ "--rtlib=compiler-rt",
+ "--unwindlib=libunwind",
+ ],
+)
+
+# Although we use similar warnings for clang and arm_gcc, we don't have one
+# centralized list, since we might want to use different warnings based on the
+# compiler in the future.
+pw_cc_flag_set(
+ name = "warnings",
+ actions = ALL_C_COMPILER_ACTIONS + ALL_CPP_COMPILER_ACTIONS,
+ flags = [
+ "-Wall",
+ "-Wextra",
+ # Make all warnings errors, except for the exemptions below.
+ "-Werror",
+ "-Wno-error=cpp", # preprocessor #warning statement
+ "-Wno-error=deprecated-declarations", # [[deprecated]] attribute
+ ],
+)
+
+pw_cc_flag_set(
+ name = "no_unknown_warning_option",
+ actions = ALL_C_COMPILER_ACTIONS + ALL_CPP_COMPILER_ACTIONS,
+ flags = [
+ "-Wno-unknown-warning-option",
+ ],
+)
+
+filegroup(
+ name = "all_linux_files",
+ srcs = [
+ "@linux_sysroot//:all",
+ "@llvm_toolchain//:all",
+ ],
+)
+
+pw_cc_toolchain(
+ name = "host_toolchain_macos",
+ abi_libc_version = "unknown",
+ abi_version = "unknown",
+ action_config_flag_sets = [
+ ":warnings",
+ ],
+ all_files = "@llvm_toolchain//:all",
+ ar = "@llvm_toolchain//:bin/llvm-ar",
+ ar_files = "@llvm_toolchain//:all",
+ as_files = "@llvm_toolchain//:all",
+ compiler = "unknown",
+ compiler_files = "@llvm_toolchain//:all",
+ coverage_files = "@llvm_toolchain//:all",
+ cpp = "@llvm_toolchain//:bin/clang++",
+ dwp_files = "@llvm_toolchain//:all",
+ feature_deps = [
+ "@pw_toolchain//features:no_default_cpp_stdlib",
+ ":macos_stdlib",
+ "@pw_toolchain//features/macos:macos_sysroot",
+ "@pw_toolchain//features:c++17",
+ "@pw_toolchain//features:debugging",
+ "@pw_toolchain//features:reduced_size",
+ "@pw_toolchain//features:no_canonical_prefixes",
+ "@pw_toolchain//features:no_rtti",
+ "@pw_toolchain//features:wno_register",
+ "@pw_toolchain//features:wnon_virtual_dtor",
+ ],
+ gcc = "@llvm_toolchain//:bin/clang",
+ gcov = "@llvm_toolchain//:bin/llvm-cov",
+ host_system_name = "unknown",
+ ld = "@llvm_toolchain//:bin/clang++",
+ linker_files = "@llvm_toolchain//:all",
+ objcopy = "@llvm_toolchain//:bin/llvm-objcopy",
+ objcopy_files = "@llvm_toolchain//:all",
+ objdump = "@llvm_toolchain//:bin/llvm-objdump",
+ strip = "@llvm_toolchain//:bin/llvm-strip",
+ strip_files = "@llvm_toolchain//:all",
+ supports_param_files = 0,
+ # The implementations of some "legacy features" built into Bazel use
+ # `target_libc` to determine if a toolchain targets MacOS,
+ # https://github.com/bazelbuild/bazel/blob/release-7.0.0-pre.20230816.3rc1/src/main/java/com/google/devtools/build/lib/rules/cpp/CcModule.java#L1301-L1304
+ target_cpu = "darwin",
+ target_libc = "macosx",
+ target_system_name = "unknown",
+ toolchain_identifier = "host-toolchain-macos",
+)
+
+toolchain(
+ name = "host_cc_toolchain_macos",
+ exec_compatible_with = [
+ "@platforms//os:macos",
+ ],
+ target_compatible_with = [
+ "@platforms//os:macos",
+ ],
+ toolchain = ":host_toolchain_macos",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+pw_cc_toolchain(
+ name = "host_toolchain_linux",
+ abi_libc_version = "unknown",
+ abi_version = "unknown",
+ action_config_flag_sets = [
+ ":warnings",
+ ],
+ all_files = ":all_linux_files",
+ ar = "@llvm_toolchain//:bin/llvm-ar",
+ ar_files = ":all_linux_files",
+ as_files = ":all_linux_files",
+ compiler = "unknown",
+ compiler_files = ":all_linux_files",
+ coverage_files = ":all_linux_files",
+ cpp = "@llvm_toolchain//:bin/clang++",
+ dwp_files = ":all_linux_files",
+ feature_deps = [
+ ":linux_sysroot",
+ "@pw_toolchain//features:c++17",
+ "@pw_toolchain//features:debugging",
+ "@pw_toolchain//features:reduced_size",
+ "@pw_toolchain//features:no_canonical_prefixes",
+ "@pw_toolchain//features:no_rtti",
+ "@pw_toolchain//features:wno_register",
+ "@pw_toolchain//features:wnon_virtual_dtor",
+ ],
+ gcc = "@llvm_toolchain//:bin/clang",
+ gcov = "@llvm_toolchain//:bin/llvm-cov",
+ host_system_name = "unknown",
+ ld = "@llvm_toolchain//:bin/clang++",
+ linker_files = ":all_linux_files",
+ objcopy = "@llvm_toolchain//:bin/llvm-objcopy",
+ objcopy_files = ":all_linux_files",
+ objdump = "@llvm_toolchain//:bin/llvm-objdump",
+ strip = "@llvm_toolchain//:bin/llvm-strip",
+ strip_files = ":all_linux_files",
+ supports_param_files = 0,
+ target_cpu = "unknown",
+ target_libc = "unknown",
+ target_system_name = "unknown",
+ toolchain_identifier = "host-toolchain-linux",
+)
+
+# A toolchain for Kythe. Identical to the regular Linux toolchain except for
+# one extra feature, ":no_unknown_warning_option".
+pw_cc_toolchain(
+ name = "host_toolchain_linux_kythe",
+ abi_libc_version = "unknown",
+ abi_version = "unknown",
+ action_config_flag_sets = [
+ ":warnings",
+ ":no_unknown_warning_option",
+ ],
+ all_files = ":all_linux_files",
+ ar = "@llvm_toolchain//:bin/llvm-ar",
+ ar_files = ":all_linux_files",
+ as_files = ":all_linux_files",
+ compiler = "unknown",
+ compiler_files = ":all_linux_files",
+ coverage_files = ":all_linux_files",
+ cpp = "@llvm_toolchain//:bin/clang++",
+ dwp_files = ":all_linux_files",
+ feature_deps = [
+ ":linux_sysroot",
+ "@pw_toolchain//features:c++17",
+ "@pw_toolchain//features:debugging",
+ "@pw_toolchain//features:reduced_size",
+ "@pw_toolchain//features:no_canonical_prefixes",
+ "@pw_toolchain//features:no_rtti",
+ "@pw_toolchain//features:wno_register",
+ "@pw_toolchain//features:wnon_virtual_dtor",
+ ],
+ gcc = "@llvm_toolchain//:bin/clang",
+ gcov = "@llvm_toolchain//:bin/llvm-cov",
+ host_system_name = "unknown",
+ ld = "@llvm_toolchain//:bin/clang++",
+ linker_files = ":all_linux_files",
+ objcopy = "@llvm_toolchain//:bin/llvm-objcopy",
+ objcopy_files = ":all_linux_files",
+ objdump = "@llvm_toolchain//:bin/llvm-objdump",
+ strip = "@llvm_toolchain//:bin/llvm-strip",
+ strip_files = ":all_linux_files",
+ supports_param_files = 0,
+ target_cpu = "unknown",
+ target_libc = "unknown",
+ target_system_name = "unknown",
+ toolchain_identifier = "host-toolchain-linux",
+)
+
+toolchain(
+ name = "host_cc_toolchain_linux",
+ exec_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ target_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ toolchain = ":host_toolchain_linux",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
+
+toolchain(
+ name = "host_cc_toolchain_linux_kythe",
+ exec_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ target_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ # This toolchain will only be selected in toolchain resolution if this
+ # config_setting is active.
+ target_settings = ["//pw_build:kythe"],
+ toolchain = ":host_toolchain_linux_kythe",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+)
diff --git a/pw_toolchain/host_clang/BUILD.gn b/pw_toolchain/host_clang/BUILD.gn
index eeebe1231..abcaf6863 100644
--- a/pw_toolchain/host_clang/BUILD.gn
+++ b/pw_toolchain/host_clang/BUILD.gn
@@ -15,12 +15,42 @@
import("//build_overrides/pigweed.gni")
import("//build_overrides/pigweed_environment.gni")
+import("toolchains.gni")
+
+config("coverage") {
+ cflags = [
+ "-fprofile-instr-generate",
+ "-fcoverage-mapping",
+ ]
+
+ if (pw_toolchain_PROFILE_SOURCE_FILES != []) {
+ _profile_source_files = []
+ foreach(file, pw_toolchain_PROFILE_SOURCE_FILES) {
+ file = rebase_path(file, root_build_dir)
+ file = string_replace(file, "/", "\/")
+ file = string_replace(file, ".", "\.")
+ _profile_source_files += [ "src:$file" ]
+ }
+
+ _profile_file = "$root_build_dir/profile-source-files.list"
+ write_file(_profile_file, _profile_source_files)
+ cflags += [ "-fprofile-list=" + rebase_path(_profile_file, root_build_dir) ]
+ }
+
+ ldflags = cflags
+}
+
# See https://github.com/google/sanitizers
config("sanitize_address") {
cflags = [ "-fsanitize=address" ]
ldflags = cflags
}
+# This is a deprecated config, use "coverage" config instead.
+config("sanitize_coverage") {
+ configs = [ ":coverage" ]
+}
+
config("sanitize_memory") {
cflags = [
"-fsanitize=memory",
@@ -83,14 +113,6 @@ config("sanitize_thread") {
ldflags = cflags
}
-config("sanitize_coverage") {
- cflags = [
- "-fprofile-instr-generate",
- "-fcoverage-mapping",
- ]
- ldflags = cflags
-}
-
# Locate XCode's sysroot for Clang.
config("xcode_sysroot") {
if (current_os == "mac") {
@@ -107,7 +129,7 @@ config("xcode_sysroot") {
}
config("linux_sysroot") {
- if (current_os == "linux") {
+ if (current_os == "linux" && defined(pw_env_setup_CIPD_PIGWEED)) {
cflags = [ "--sysroot=" +
rebase_path(pw_env_setup_CIPD_PIGWEED, root_build_dir) +
"/clang_sysroot/" ]
@@ -122,7 +144,7 @@ config("linux_sysroot") {
#
# Pull the appropriate paths from our Pigweed env setup.
config("no_system_libcpp") {
- if (current_os == "mac") {
+ if (current_os == "mac" && defined(pw_env_setup_CIPD_PIGWEED)) {
install_dir = pw_env_setup_CIPD_PIGWEED
assert(install_dir != "",
"You forgot to activate the Pigweed environment; " +
diff --git a/pw_toolchain/host_clang/toolchain.cmake b/pw_toolchain/host_clang/toolchain.cmake
index d84db47ba..37d8586c1 100644
--- a/pw_toolchain/host_clang/toolchain.cmake
+++ b/pw_toolchain/host_clang/toolchain.cmake
@@ -29,6 +29,8 @@ include($ENV{PW_ROOT}/pw_trace/backend.cmake)
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
+pw_add_global_compile_options(-std=c++20 LANGUAGES CXX)
+
# Configure backend for assert facade.
pw_set_backend(pw_assert.check pw_assert.print_and_abort_check_backend)
pw_set_backend(pw_assert.assert pw_assert.print_and_abort_assert_backend)
@@ -67,6 +69,7 @@ pw_set_backend(pw_thread.id pw_thread_stl.id)
pw_set_backend(pw_thread.yield pw_thread_stl.yield)
pw_set_backend(pw_thread.sleep pw_thread_stl.sleep)
pw_set_backend(pw_thread.thread pw_thread_stl.thread)
+pw_set_backend(pw_thread.test_thread_context pw_thread_stl.test_thread_context)
# TODO(ewout): Migrate this to match GN's tokenized trace setup.
pw_set_backend(pw_trace pw_trace.null)
diff --git a/pw_toolchain/host_clang/toolchains.gni b/pw_toolchain/host_clang/toolchains.gni
index 3e847c4af..3ee4c4fed 100644
--- a/pw_toolchain/host_clang/toolchains.gni
+++ b/pw_toolchain/host_clang/toolchains.gni
@@ -14,11 +14,12 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/defaults.gni")
import("$dir_pw_toolchain/clang_tools.gni")
declare_args() {
# Sets the sanitizer to pass to clang. Valid values are "address", "memory",
- # "thread", "undefined", "undefined_heuristic", and "coverage".
+ # "thread", "undefined", "undefined_heuristic".
pw_toolchain_SANITIZERS = []
# Indicates if this toolchain supports generating coverage reports from
@@ -26,8 +27,13 @@ declare_args() {
#
# For example, the static analysis toolchains that run `clang-tidy` instead
# of the test binary itself cannot generate coverage reports.
+ #
+ # This is typically set by individual toolchains and not by GN args.
pw_toolchain_COVERAGE_ENABLED = false
+ # List of source files to selectively collect coverage.
+ pw_toolchain_PROFILE_SOURCE_FILES = []
+
# Indicates if this toolchain supports building fuzzers. This is typically
# set by individual toolchains and not by GN args.
pw_toolchain_FUZZING_ENABLED = false
@@ -40,43 +46,32 @@ declare_args() {
# Specifies the tools used by host Clang toolchains.
_host_clang_toolchain = {
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- # OSS-Fuzz sets compiler and linker paths. See
- # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
-
- # Just use the "llvm-ar" on the system path.
- ar = "llvm-ar"
- cc = getenv("CC")
- cxx = getenv("CXX")
- } else {
- forward_variables_from(pw_toolchain_clang_tools, "*")
- }
-
+ forward_variables_from(pw_toolchain_clang_tools, "*")
is_host_toolchain = true
-
static_analysis = {
- # Enable static analysis for host clang based toolchains,
- # even with OSS-Fuzz enabled.
+ # Enable static analysis for host clang based toolchains.
enabled = true
}
}
# Common default scope shared by all host Clang toolchains.
_defaults = {
- # TODO(b/234888755) amend toolchain declaration process to
+ # TODO: b/234888755 - amend toolchain declaration process to
# remove this hack.
default_configs = []
- default_configs = [
- "$dir_pw_build:extra_debugging",
- "$dir_pw_toolchain/host_clang:no_system_libcpp",
- "$dir_pw_toolchain/host_clang:xcode_sysroot",
- ]
+ default_configs = pigweed_default_configs + [
+ "$dir_pw_build:extra_debugging",
+ "$dir_pw_toolchain/host_clang:no_system_libcpp",
+ "$dir_pw_toolchain/host_clang:xcode_sysroot",
+ ]
# OSS-Fuzz uses -stdlib=libc++, which isn't included in the CIPD-provided
# Linux sysroot (it instead provides libstdc++).
if (!pw_toolchain_OSS_FUZZ_ENABLED) {
default_configs += [ "$dir_pw_toolchain/host_clang:linux_sysroot" ]
}
+
+ pw_build_LINK_DEPS = [ dir_pw_libc ]
}
pw_toolchain_host_clang = {
@@ -90,9 +85,6 @@ pw_toolchain_host_clang = {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -106,9 +98,6 @@ pw_toolchain_host_clang = {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -122,29 +111,47 @@ pw_toolchain_host_clang = {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
fuzz = {
name = "host_clang_fuzz"
- forward_variables_from(_host_clang_toolchain, "*")
+ cc = ""
+ cxx = ""
+ forward_variables_from(_host_clang_toolchain,
+ "*",
+ [
+ "cc",
+ "cxx",
+ ])
+
+ # OSS-Fuzz sets compiler paths. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ cc = getenv("CC")
+ cxx = getenv("CXX")
+ }
+ if (cc == "") {
+ cc = _host_clang_toolchain.cc
+ }
+ if (cxx == "") {
+ cxx = _host_clang_toolchain.cxx
+ }
defaults = {
forward_variables_from(_defaults, "*")
pw_toolchain_FUZZING_ENABLED = true
- default_configs += [ "$dir_pw_fuzzer:instrumentation" ]
-
- # Always disable coverage generation.
- pw_toolchain_COVERAGE_ENABLED = false
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ default_configs += [ "$dir_pw_fuzzer:oss_fuzz_instrumentation" ]
+ } else {
+ default_configs += [ "$dir_pw_fuzzer:instrumentation" ]
+ }
# Fuzz faster.
default_configs += [ "$dir_pw_build:optimize_speed" ]
+ # Default to ASan for fuzzing, which is what we typically care about.
if (pw_toolchain_SANITIZERS == []) {
- # Default to ASan for fuzzing, which is what we typically care about.
pw_toolchain_SANITIZERS = [ "address" ]
}
foreach(sanitizer, pw_toolchain_SANITIZERS) {
@@ -163,9 +170,6 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_address" ]
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -178,9 +182,6 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_undefined" ]
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -194,9 +195,6 @@ pw_toolchain_host_clang = {
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic" ]
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -209,9 +207,6 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_memory" ]
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -224,9 +219,6 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_thread" ]
-
- # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
- pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -238,14 +230,10 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
- default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_coverage" ]
+ default_configs += [ "$dir_pw_toolchain/host_clang:coverage" ]
# Enable PW toolchain arguments for coverage. This will only apply to
# binaries built using this toolchain.
- #
- # "coverage" works with "address", "memory", "thread", "undefined", and
- # "undefined_heuristic" sanitizers.
- pw_toolchain_SANITIZERS += [ "coverage" ]
pw_toolchain_COVERAGE_ENABLED = true
}
}
diff --git a/pw_toolchain/host_gcc/toolchain.cmake b/pw_toolchain/host_gcc/toolchain.cmake
index ed011e21c..019c3ae95 100644
--- a/pw_toolchain/host_gcc/toolchain.cmake
+++ b/pw_toolchain/host_gcc/toolchain.cmake
@@ -29,6 +29,8 @@ include($ENV{PW_ROOT}/pw_trace/backend.cmake)
set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
+pw_add_global_compile_options(-std=c++20 LANGUAGES CXX)
+
# Configure backend for assert facade.
pw_set_backend(pw_assert.check pw_assert.print_and_abort_check_backend)
pw_set_backend(pw_assert.assert pw_assert.print_and_abort_assert_backend)
diff --git a/pw_toolchain/host_gcc/toolchains.gni b/pw_toolchain/host_gcc/toolchains.gni
index 66144bbc2..901d08389 100644
--- a/pw_toolchain/host_gcc/toolchains.gni
+++ b/pw_toolchain/host_gcc/toolchains.gni
@@ -14,6 +14,8 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/defaults.gni")
+
# Specifies the tools used by host GCC toolchains.
_host_gcc_toolchain = {
ar = "ar"
@@ -34,7 +36,8 @@ pw_toolchain_host_gcc = {
name = "host_gcc_debug"
forward_variables_from(_host_gcc_toolchain, "*")
defaults = {
- default_configs = _configs + [ "$dir_pw_build:optimize_debugging" ]
+ default_configs = pigweed_default_configs + _configs +
+ [ "$dir_pw_build:optimize_debugging" ]
}
}
@@ -42,7 +45,8 @@ pw_toolchain_host_gcc = {
name = "host_gcc_speed_optimized"
forward_variables_from(_host_gcc_toolchain, "*")
defaults = {
- default_configs = _configs + [ "$dir_pw_build:optimize_speed" ]
+ default_configs = pigweed_default_configs + _configs +
+ [ "$dir_pw_build:optimize_speed" ]
}
}
@@ -50,7 +54,8 @@ pw_toolchain_host_gcc = {
name = "host_gcc_size_optimized"
forward_variables_from(_host_gcc_toolchain, "*")
defaults = {
- default_configs = _configs + [ "$dir_pw_build:optimize_size" ]
+ default_configs =
+ pigweed_default_configs + _configs + [ "$dir_pw_build:optimize_size" ]
}
}
}
diff --git a/pw_toolchain/no_destructor_test.cc b/pw_toolchain/no_destructor_test.cc
index 936cedd31..38f3c5991 100644
--- a/pw_toolchain/no_destructor_test.cc
+++ b/pw_toolchain/no_destructor_test.cc
@@ -43,6 +43,13 @@ class CrashInDestructor {
~CrashInDestructor() { PW_CRASH("This destructor should never execute!"); }
};
+class TrivialDestructor {
+ public:
+ TrivialDestructor(int initial_value) : value(initial_value) {}
+
+ int value;
+};
+
TEST(NoDestructor, ShouldNotCallDestructor) {
bool destructor_called = false;
@@ -63,6 +70,14 @@ TEST(NoDestructor, MemberAccess) {
EXPECT_EQ(no_destructor.operator->(), no_destructor->MyAddress());
}
+TEST(NoDestructor, TrivialDestructor) {
+ NoDestructor<TrivialDestructor> no_destructor(555);
+
+ EXPECT_EQ(no_destructor->value, 555);
+ no_destructor->value = 123;
+ EXPECT_EQ(no_destructor->value, 123);
+}
+
TEST(NoDestructor, TrivialType) {
NoDestructor<int> no_destructor;
diff --git a/pw_toolchain/public/pw_toolchain/no_destructor.h b/pw_toolchain/public/pw_toolchain/no_destructor.h
index 6986a08a5..dc9ee0aa5 100644
--- a/pw_toolchain/public/pw_toolchain/no_destructor.h
+++ b/pw_toolchain/public/pw_toolchain/no_destructor.h
@@ -19,36 +19,43 @@
namespace pw {
-// Helper type to create a function-local static variable of type T when T has a
-// non-trivial destructor. Storing a T in a pw::NoDestructor<T> will prevent
-// ~T() from running, even when the variable goes out of scope.
-//
-// This class is useful when a variable has static storage duration but its type
-// has a non-trivial destructor. Destructor ordering is not defined and can
-// cause issues in multithreaded environments. Additionally, removing destructor
-// calls can save code size.
-//
-// Except in generic code, do not use pw::NoDestructor<T> with trivially
-// destructible types. Use the type directly instead. If the variable can be
-// constexpr, make it constexpr.
-//
-// pw::NoDestructor<T> provides a similar API to std::optional. Use * or -> to
-// access the wrapped type.
-//
-// Example usage:
-//
-// pw::sync::Mutex& GetMutex() {
-// // Use NoDestructor to avoid running the mutex destructor when exit-time
-// // destructors run.
-// static const pw::NoDestructor<pw::sync::Mutex> global_mutex;
-// return *global_mutex;
-// }
-//
-// WARNING: Misuse of NoDestructor can cause memory leaks and other problems.
-// Only skip destructors when you know it is safe to do so.
-//
-// In Clang, pw::NoDestructor can be replaced with the [[clang::no_destroy]]
-// attribute.
+/// Helper type to create a function-local static variable of type `T` when `T`
+/// has a non-trivial destructor. Storing a `T` in a `pw::NoDestructor<T>` will
+/// prevent `~T()` from running, even when the variable goes out of scope.
+///
+/// This class is useful when a variable has static storage duration but its
+/// type has a non-trivial destructor. Destructor ordering is not defined and
+/// can cause issues in multithreaded environments. Additionally, removing
+/// destructor calls can save code size.
+///
+/// Except in generic code, do not use `pw::NoDestructor<T>` with trivially
+/// destructible types. Use the type directly instead. If the variable can be
+/// `constexpr`, make it `constexpr`.
+///
+/// `pw::NoDestructor<T>` provides a similar API to `std::optional`. Use `*` or
+/// `->` to access the wrapped type.
+///
+/// Example usage:
+/// @code{.cpp}
+///
+/// pw::sync::Mutex& GetMutex() {
+/// // Use NoDestructor to avoid running the mutex destructor when exit-time
+/// // destructors run.
+/// static const pw::NoDestructor<pw::sync::Mutex> global_mutex;
+/// return *global_mutex;
+/// }
+///
+/// @endcode
+///
+/// In Clang, `pw::NoDestructor` can be replaced with the
+/// <a href="https://clang.llvm.org/docs/AttributeReference.html#no-destroy">
+/// [[clang::no_destroy]]</a> attribute. `pw::NoDestructor<T>` is similar to
+/// Chromium’s `base::NoDestructor<T>` in <a
+/// href="https://chromium.googlesource.com/chromium/src/base/+/5ea6e31f927aa335bfceb799a2007c7f9007e680/no_destructor.h">
+/// src/base/no_destructor.h</a>.
+///
+/// @warning Misuse of NoDestructor can cause memory leaks and other problems.
+/// Only skip destructors when you know it is safe to do so.
template <typename T>
class NoDestructor {
public:
diff --git a/pw_toolchain/py/BUILD.gn b/pw_toolchain/py/BUILD.gn
index cc8a554d3..9727a7920 100644
--- a/pw_toolchain/py/BUILD.gn
+++ b/pw_toolchain/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_toolchain/__init__.py",
@@ -29,6 +28,7 @@ pw_python_package("py") {
"pw_toolchain/clang_tidy.py",
"pw_toolchain/copy_with_metadata.py",
]
+ python_deps = [ "$dir_pw_cli/py" ]
tests = [ "clang_tidy_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
diff --git a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
index 5a02f3896..7b4209112 100644
--- a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
+++ b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
@@ -175,11 +175,6 @@ def get_ldflags(compiler_info: Dict[str, str]) -> List[str]:
+ str(
Path(compiler_info['sysroot']) / 'lib' / compiler_info['multi_dir']
),
- # Add libraries to link.
- '-lc_nano',
- '-lm',
- '-lgcc',
- '-lstdc++_nano',
]
# Add C runtime object files.
diff --git a/pw_toolchain/py/pw_toolchain/clang_tidy.py b/pw_toolchain/py/pw_toolchain/clang_tidy.py
index 868a68ac4..9950b55fe 100644
--- a/pw_toolchain/py/pw_toolchain/clang_tidy.py
+++ b/pw_toolchain/py/pw_toolchain/clang_tidy.py
@@ -31,6 +31,8 @@ import subprocess
import sys
from typing import Iterable, List, Optional, Union
+import pw_cli.env
+
_LOG = logging.getLogger(__name__)
@@ -144,7 +146,10 @@ def run_clang_tidy(
extra_args: List[str],
) -> int:
"""Executes clang_tidy via subprocess. Returns true if no failures."""
- command: List[Union[str, Path]] = [clang_tidy, source_file, '--use-color']
+ command: List[Union[str, Path]] = [clang_tidy, source_file]
+
+ if pw_cli.env.pigweed_environment().PW_USE_COLOR:
+ command.append('--use-color')
if not verbose:
command.append('--quiet')
diff --git a/pw_toolchain/py/setup.py b/pw_toolchain/py/setup.py
deleted file mode 100644
index ecfac6ea9..000000000
--- a/pw_toolchain/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_toolchain"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_toolchain/rust/BUILD.bazel b/pw_toolchain/rust/BUILD.bazel
new file mode 100644
index 000000000..0a41c4fca
--- /dev/null
+++ b/pw_toolchain/rust/BUILD.bazel
@@ -0,0 +1,22 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(":defs.bzl", "pw_rust_declare_toolchain_targets")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# TODO: b/296459700 - Make this modular and configurable.
+pw_rust_declare_toolchain_targets()
diff --git a/pw_toolchain/rust/defs.bzl b/pw_toolchain/rust/defs.bzl
new file mode 100644
index 000000000..f3f58d218
--- /dev/null
+++ b/pw_toolchain/rust/defs.bzl
@@ -0,0 +1,216 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Utilities for declaring Rust toolchains that are compatible with Arm gcc."""
+
+load("@rules_rust//rust:toolchain.bzl", "rust_analyzer_toolchain", "rust_toolchain")
+load("//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl", "cipd_repository")
+
+HOSTS = [
+ {
+ "cpu": "aarch64",
+ "cipd_arch": "arm64",
+ "os": "linux",
+ "triple": "aarch64-unknown-linux-gnu",
+ "dylib_ext": ".so",
+ },
+ {
+ "cpu": "x86_64",
+ "cipd_arch": "amd64",
+ "os": "linux",
+ "triple": "x86_64-unknown-linux-gnu",
+ "dylib_ext": ".so",
+ },
+ {
+ "cpu": "aarch64",
+ "cipd_arch": "arm64",
+ "os": "macos",
+ "triple": "aarch64-apple-darwin",
+ "dylib_ext": ".dylib",
+ },
+ {
+ "cpu": "x86_64",
+ "cipd_arch": "amd64",
+ "os": "macos",
+ "triple": "x86_64-apple-darwin",
+ "dylib_ext": ".dylib",
+ },
+]
+
+EXTRA_TARGETS = [
+ {
+ "cpu": "armv6-m",
+ "triple": "thumbv6m-none-eabi",
+ },
+ {
+ "cpu": "armv7-m",
+ "triple": "thumbv7m-none-eabi",
+ },
+ {
+ "cpu": "armv7e-m",
+ "triple": "thumbv7m-none-eabi",
+ },
+]
+
+# buildifier: disable=unnamed-macro
+def pw_rust_register_toolchain_and_target_repos(cipd_tag):
+ """Declare and register CIPD repos for Rust toolchain and target rupport.
+
+ Args:
+ cipd_tag: Tag with which to select specific package versions.
+ """
+ for host in HOSTS:
+ cipd_os = host["os"]
+ if cipd_os == "macos":
+ cipd_os = "mac"
+
+ cipd_repository(
+ name = "rust_toolchain_host_{}_{}".format(host["os"], host["cpu"]),
+ build_file = "//pw_toolchain/rust:rust_toolchain.BUILD",
+ path = "fuchsia/third_party/rust/host/{}-{}".format(cipd_os, host["cipd_arch"]),
+ tag = cipd_tag,
+ )
+
+ cipd_repository(
+ name = "rust_toolchain_target_{}".format(host["triple"]),
+ build_file = "//pw_toolchain/rust:rust_stdlib.BUILD",
+ path = "fuchsia/third_party/rust/target/{}".format(host["triple"]),
+ tag = cipd_tag,
+ )
+
+ for target in EXTRA_TARGETS:
+ cipd_repository(
+ name = "rust_toolchain_target_{}".format(target["triple"]),
+ build_file = "//pw_toolchain/rust:rust_stdlib.BUILD",
+ path = "fuchsia/third_party/rust/target/{}".format(target["triple"]),
+ tag = cipd_tag,
+ )
+
+# buildifier: disable=unnamed-macro
+def pw_rust_register_toolchains():
+ """Register Rust Toolchains
+
+ For this registration to be valid one must
+ 1. Call `pw_rust_register_toolchain_and_target_repos(tag)` pervisouly in the
+ WORKSPACE file.
+ 2. Call `pw_rust_declare_toolchain_targets()` from
+ `//pw_toolchain/rust/BUILD.bazel`.
+ """
+ for host in HOSTS:
+ native.register_toolchains(
+ "//pw_toolchain/rust:host_rust_toolchain_{}_{}".format(host["os"], host["cpu"]),
+ "//pw_toolchain/rust:host_rust_analyzer_toolchain_{}_{}".format(host["os"], host["cpu"]),
+ )
+ for target in EXTRA_TARGETS:
+ native.register_toolchains(
+ "//pw_toolchain/rust:{}_{}_rust_toolchain_{}_{}".format(host["os"], host["cpu"], target["triple"], target["cpu"]),
+ )
+
+# buildifier: disable=unnamed-macro
+def pw_rust_declare_toolchain_targets():
+ """Declare rust toolchain targets"""
+ for host in HOSTS:
+ _pw_rust_host_toolchain(
+ name = "host_rust_toolchain_{}_{}".format(host["os"], host["cpu"]),
+ analyzer_toolchain_name = "host_rust_analyzer_toolchain_{}_{}".format(host["os"], host["cpu"]),
+ compatible_with = [
+ "@platforms//cpu:{}".format(host["cpu"]),
+ "@platforms//os:{}".format(host["os"]),
+ ],
+ dylib_ext = host["dylib_ext"],
+ target_repo = "@rust_toolchain_target_{}".format(host["triple"]),
+ toolchain_repo = "@rust_toolchain_host_{}_{}".format(host["os"], host["cpu"]),
+ triple = host["triple"],
+ )
+ for target in EXTRA_TARGETS:
+ _pw_rust_toolchain(
+ name = "{}_{}_rust_toolchain_{}_{}".format(host["os"], host["cpu"], target["triple"], target["cpu"]),
+ exec_triple = host["triple"],
+ target_triple = target["triple"],
+ target_repo = "@rust_toolchain_target_{}".format(target["triple"]),
+ toolchain_repo = "@rust_toolchain_host_{}_{}".format(host["os"], host["cpu"]),
+ dylib_ext = "*.so",
+ exec_compatible_with = [
+ "@platforms//cpu:{}".format(host["cpu"]),
+ "@platforms//os:{}".format(host["os"]),
+ ],
+ target_compatible_with = [
+ "@platforms//cpu:{}".format(target["cpu"]),
+ ],
+ )
+
+def _pw_rust_toolchain(
+ name,
+ exec_triple,
+ target_triple,
+ toolchain_repo,
+ target_repo,
+ dylib_ext,
+ exec_compatible_with,
+ target_compatible_with):
+ rust_toolchain(
+ name = "{}_rust_toolchain".format(name),
+ binary_ext = "",
+ default_edition = "2021",
+ dylib_ext = dylib_ext,
+ exec_triple = exec_triple,
+ rust_doc = "{}//:bin/rustdoc".format(toolchain_repo),
+ rust_std = "{}//:rust_std".format(target_repo),
+ rustc = "{}//:bin/rustc".format(toolchain_repo),
+ rustc_lib = "{}//:rustc_lib".format(toolchain_repo),
+ staticlib_ext = ".a",
+ stdlib_linkflags = [],
+ target_triple = target_triple,
+ )
+ native.toolchain(
+ name = name,
+ exec_compatible_with = exec_compatible_with,
+ target_compatible_with = target_compatible_with,
+ toolchain = ":{}_rust_toolchain".format(name),
+ toolchain_type = "@rules_rust//rust:toolchain",
+ )
+
+def _pw_rust_host_toolchain(
+ name,
+ analyzer_toolchain_name,
+ triple,
+ toolchain_repo,
+ target_repo,
+ dylib_ext,
+ compatible_with):
+ _pw_rust_toolchain(
+ name = name,
+ exec_triple = triple,
+ target_triple = triple,
+ toolchain_repo = toolchain_repo,
+ target_repo = target_repo,
+ dylib_ext = dylib_ext,
+ exec_compatible_with = compatible_with,
+ target_compatible_with = compatible_with,
+ )
+
+ rust_analyzer_toolchain(
+ name = "{}_rust_analyzer_toolchain".format(analyzer_toolchain_name),
+ proc_macro_srv = "{}//:libexec/rust-analyzer-proc-macro-srv".format(toolchain_repo),
+ rustc = "{}//:bin/rustc".format(toolchain_repo),
+ rustc_srcs = "{}//:rustc_srcs".format(toolchain_repo),
+ visibility = ["//visibility:public"],
+ )
+
+ native.toolchain(
+ name = analyzer_toolchain_name,
+ exec_compatible_with = compatible_with,
+ target_compatible_with = compatible_with,
+ toolchain = ":{}_rust_analyzer_toolchain".format(analyzer_toolchain_name),
+ toolchain_type = "@rules_rust//rust/rust_analyzer:toolchain_type",
+ )
diff --git a/pw_toolchain/rust/rust_stdlib.BUILD b/pw_toolchain/rust/rust_stdlib.BUILD
new file mode 100644
index 000000000..b92a116fa
--- /dev/null
+++ b/pw_toolchain/rust/rust_stdlib.BUILD
@@ -0,0 +1,31 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_rust//rust:toolchain.bzl", "rust_stdlib_filegroup")
+
+exports_files(glob(["**"]))
+
+filegroup(
+ name = "all",
+ srcs = glob(["**"]),
+ visibility = ["//visibility:public"],
+)
+
+rust_stdlib_filegroup(
+ name = "rust_std",
+ # This is globbing over the target triple. Ideally, only the relevant target
+ # tripple is part of this filegroup.
+ srcs = glob(["lib/rustlib/*/lib/*"]),
+ visibility = ["//visibility:public"],
+)
diff --git a/pw_toolchain/rust/rust_toolchain.BUILD b/pw_toolchain/rust/rust_toolchain.BUILD
new file mode 100644
index 000000000..f8f3bf0de
--- /dev/null
+++ b/pw_toolchain/rust/rust_toolchain.BUILD
@@ -0,0 +1,33 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+exports_files(glob(["**"]))
+
+filegroup(
+ name = "all",
+ srcs = glob(["**"]),
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "rustc_lib",
+ srcs = glob(["lib/*.so"]),
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "rustc_srcs",
+ srcs = glob(["lib/rustlib/src/rust/src/**"]),
+ visibility = ["//visibility:public"],
+)
diff --git a/pw_toolchain/rust_toolchain.bzl b/pw_toolchain/rust_toolchain.bzl
deleted file mode 100644
index 32b5ce48b..000000000
--- a/pw_toolchain/rust_toolchain.bzl
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright 2023 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""Utilities for declaring Rust toolchains that are compatible with @bazel_embedded"""
-
-load("@rules_rust//rust:toolchain.bzl", "rust_toolchain")
-
-def pw_rust_toolchain(name, exec_cpu, exec_os, target_cpu, rust_target_triple, exec_triple):
- proxy_toolchain = "@rust_{}_{}__{}__stable_tools".format(exec_os, exec_cpu, rust_target_triple)
-
- rust_toolchain(
- name = "{}_impl".format(name),
- binary_ext = "",
- dylib_ext = ".so",
- exec_triple = exec_triple,
- os = "none",
- rust_doc = "{}//:rustdoc".format(proxy_toolchain),
- rust_std = "{}//:rust_std-{}".format(proxy_toolchain, rust_target_triple),
- rustc = "{}//:rustc".format(proxy_toolchain),
- rustc_lib = "{}//:rustc_lib".format(proxy_toolchain),
- staticlib_ext = ".a",
- stdlib_linkflags = [],
- target_triple = rust_target_triple,
- )
-
- native.toolchain(
- name = name,
- exec_compatible_with = [
- "@platforms//cpu:{}".format(exec_cpu),
- "@platforms//os:{}".format(exec_os),
- ],
- target_compatible_with = [
- "@platforms//cpu:{}".format(target_cpu),
- "@bazel_embedded//constraints/fpu:none",
- ],
- toolchain = ":{}_impl".format(name),
- toolchain_type = "@rules_rust//rust:toolchain",
- )
diff --git a/pw_toolchain/static_analysis_toolchain.gni b/pw_toolchain/static_analysis_toolchain.gni
index 9aa0fa46b..5aa5abfc6 100644
--- a/pw_toolchain/static_analysis_toolchain.gni
+++ b/pw_toolchain/static_analysis_toolchain.gni
@@ -15,6 +15,10 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
+import("$dir_pw_third_party/boringssl/boringssl.gni")
+import("$dir_pw_third_party/chre/chre.gni")
+import("$dir_pw_third_party/googletest/googletest.gni")
+import("$dir_pw_third_party/mbedtls/mbedtls.gni")
import("$dir_pw_toolchain/universal_tools.gni")
declare_args() {
@@ -33,9 +37,9 @@ declare_args() {
# are switched from -I to -isystem, which causes clang-tidy to ignore them.
# Unfortunately, clang-tidy provides no other way to filter header files.
#
- # For example, the following ignores header files in "mbedtls/include":
+ # For example, the following ignores header files in "repo/include":
#
- # pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = ["mbedtls/include"]
+ # pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = ["repo/include"]
#
# While the following ignores all third-party header files:
#
@@ -44,6 +48,15 @@ declare_args() {
pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS = []
}
+# Third-party software with Pigweed-supported build files that do not pass all
+# clang-tidy checks.
+_excluded_third_party_dirs = [
+ dir_pw_third_party_mbedtls,
+ dir_pw_third_party_boringssl,
+ dir_pw_third_party_googletest,
+ dir_pw_third_party_chre,
+]
+
# Creates a toolchain target for static analysis.
#
# The generated toolchain runs clang-tidy on all source files that are not
@@ -76,6 +89,20 @@ template("pw_static_analysis_toolchain") {
assert(_static_analysis_args.enabled,
"static_analysis.enabled must be true to use this toolchain.")
+ _skipped_regexps = []
+ _skipped_include_paths = []
+ foreach(third_party_dir, _excluded_third_party_dirs) {
+ if (third_party_dir != "") {
+ _skipped_include_paths += [
+ third_party_dir + "/include",
+ third_party_dir,
+ ]
+ }
+ }
+
+ _skipped_regexps += pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES
+ _skipped_include_paths += pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS
+
# Clang tidy is invoked by a wrapper script which implements the missing
# option --source-filter.
_clang_tidy_py_path =
@@ -84,11 +111,11 @@ template("pw_static_analysis_toolchain") {
_clang_tidy_py = "${python_path} ${_clang_tidy_py_path}"
_source_root = rebase_path("//", root_build_dir)
_source_exclude = ""
- foreach(pattern, pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES) {
+ foreach(pattern, _skipped_regexps) {
_source_exclude = _source_exclude + " --source-exclude '${pattern}'"
}
_skip_include_path = ""
- foreach(pattern, pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS) {
+ foreach(pattern, _skipped_include_paths) {
_skip_include_path =
_skip_include_path + " --skip-include-path '${pattern}'"
}
diff --git a/pw_toolchain/traits.gni b/pw_toolchain/traits.gni
index fee99dd23..ed4854754 100644
--- a/pw_toolchain/traits.gni
+++ b/pw_toolchain/traits.gni
@@ -53,5 +53,5 @@ assert(
"unsupported value. The toolchain must set it to one of the " +
"pw_toolchain_STANDARD constants (e.g. pw_toolchain_STANDARD.CXX17).")
-assert(pw_toolchain_CXX_STANDARD >= pw_toolchain_STANDARD.CXX14,
- "Pigweed requires at least C++14.")
+assert(pw_toolchain_CXX_STANDARD >= pw_toolchain_STANDARD.CXX17,
+ "Pigweed requires at least C++17.")
diff --git a/pw_toolchain_bazel/BUILD.gn b/pw_toolchain_bazel/BUILD.gn
new file mode 100644
index 000000000..bf6234783
--- /dev/null
+++ b/pw_toolchain_bazel/BUILD.gn
@@ -0,0 +1,31 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+# This module is Bazel-only! GN is only used for documentation and to satisfy
+# requirements that a test group is present.
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [
+ "api.rst",
+ "docs.rst",
+ ]
+}
diff --git a/third_party/rules_proto_grpc/BUILD.bazel b/pw_toolchain_bazel/WORKSPACE.bazel
index a524793e9..3aba5abc5 100644
--- a/third_party/rules_proto_grpc/BUILD.bazel
+++ b/pw_toolchain_bazel/WORKSPACE.bazel
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -12,4 +12,4 @@
# License for the specific language governing permissions and limitations under
# the License.
-exports_files(["internal_proto.bzl"])
+workspace(name = "pw_toolchain")
diff --git a/pw_toolchain_bazel/api.rst b/pw_toolchain_bazel/api.rst
new file mode 100644
index 000000000..e0e61d33f
--- /dev/null
+++ b/pw_toolchain_bazel/api.rst
@@ -0,0 +1,298 @@
+.. _module-pw_toolchain_bazel-api:
+
+=============
+API reference
+=============
+
+.. py:class:: pw_cc_toolchain
+
+ This rule is the core of a C/C++ toolchain definition. Critically, it is
+ intended to fully specify the following:
+
+ * Which tools to use for various compile/link actions.
+ * Which `well-known features <https://bazel.build/docs/cc-toolchain-config-reference#wellknown-features>`_
+ are supported.
+ * Which flags to apply to various actions.
+
+ .. py:attribute:: feature_deps
+ :type: List[label]
+
+ ``pw_cc_toolchain_feature`` labels that provide features for this toolchain.
+
+ .. py:attribute:: ar
+ :type: File
+
+ Path to the tool to use for ``ar`` (static link) actions.
+
+ .. py:attribute:: cpp
+ :type: File
+
+ Path to the tool to use for C++ compile actions.
+
+ .. py:attribute:: gcc
+ :type: File
+
+ Path to the tool to use for C compile actions.
+
+ .. py:attribute:: gcov
+ :type: File
+
+ Path to the tool to use for generating code coverage data.
+
+ .. py:attribute:: ld
+ :type: File
+
+ Path to the tool to use for link actions.
+
+ .. py:attribute:: strip
+ :type: File
+
+ Path to the tool to use for strip actions.
+
+ .. py:attribute:: objcopy
+ :type: File
+
+ Path to the tool to use for objcopy actions.
+
+ .. py:attribute:: objdump
+ :type: File
+
+ Path to the tool to use for objdump actions.
+
+ .. py:attribute:: action_config_flag_sets
+ :type: List[label]
+
+ List of flag sets to apply to the respective ``action_config``\s. The vast
+ majority of labels listed here will point to :py:class:`pw_cc_flag_set`
+ rules.
+
+ .. py:attribute:: toolchain_identifier
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: host_system_name
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: target_system_name
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: target_cpu
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: target_libc
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: compiler
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: abi_version
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: abi_libc_version
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+ .. py:attribute:: cc_target_os
+ :type: str
+
+ See `cc_common.create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_\.
+
+.. py:class:: pw_cc_flag_set
+
+ Declares an ordered set of flags bound to a set of actions.
+
+ Flag sets can be attached to a :py:class:`pw_cc_toolchain` via
+ :py:attr:`pw_cc_toolchain.action_config_flag_sets`\.
+
+ Examples:
+
+ .. code-block:: py
+
+ pw_cc_flag_set(
+ name = "warnings_as_errors",
+ flags = ["-Werror"],
+ )
+
+ pw_cc_flag_set(
+ name = "layering_check",
+ flag_groups = [
+ ":strict_module_headers",
+ ":dependent_module_map_files",
+ ],
+ )
+
+ .. inclusive-language: disable
+
+ Note: In the vast majority of cases, alphabetical sorting is not desirable
+ for the :py:attr:`pw_cc_flag_set.flags` and
+ :py:attr:`pw_cc_flag_set.flag_groups` attributes.
+ `Buildifier <https://github.com/bazelbuild/buildtools/blob/master/buildifier/README.md>`_
+ shouldn't ever try to sort these, but in the off chance it starts to these
+ members should be listed as exceptions in the ``SortableDenylist``.
+
+ .. inclusive-language: enable
+
+ .. py:attribute:: actions
+ :type: List[str]
+
+ A list of action names that this flag set applies to.
+
+ .. inclusive-language: disable
+
+ Valid choices are listed at
+ `@rules_cc//cc:action_names.bzl <https://github.com/bazelbuild/bazel/blob/master/tools/build_defs/cc/action_names.bzl>`_\.
+
+ .. inclusive-language: enable
+
+ It is possible for some needed action names to not be enumerated in this list,
+ so there is not rigid validation for these strings. Prefer using constants
+ rather than manually typing action names.
+
+ .. py:attribute:: flags
+ :type: List[str]
+
+ Flags that should be applied to the specified actions.
+
+ These are evaluated in order, with earlier flags appearing earlier in the
+ invocation of the underlying tool. If you need expansion logic, prefer
+ enumerating flags in a :py:class:`pw_cc_flag_group` or create a custom
+ rule that provides ``FlagGroupInfo``.
+
+ Note: :py:attr:`pw_cc_flag_set.flags` and
+ :py:attr:`pw_cc_flag_set.flag_groups` are mutually exclusive.
+
+ .. py:attribute:: flag_groups
+ :type: List[label]
+
+ Labels pointing to :py:class:`pw_cc_flag_group` rules.
+
+ This is intended to be compatible with any other rules that provide
+ ``FlagGroupInfo``. These are evaluated in order, with earlier flag groups
+ appearing earlier in the invocation of the underlying tool.
+
+ Note: :py:attr:`pw_cc_flag_set.flag_groups` and
+ :py:attr:`pw_cc_flag_set.flags` are mutually exclusive.
+
+.. py:class:: pw_cc_flag_group
+
+ Declares an (optionally parametric) ordered set of flags.
+
+ :py:class:`pw_cc_flag_group` rules are expected to be consumed exclusively by
+ :py:class:`pw_cc_flag_set` rules. Though simple lists of flags can be
+ expressed by populating ``flags`` on a :py:class:`pw_cc_flag_set`,
+ :py:class:`pw_cc_flag_group` provides additional power in the following two
+ ways:
+
+ 1. Iteration and conditional expansion. Using
+ :py:attr:`pw_cc_flag_group.iterate_over`,
+ :py:attr:`pw_cc_flag_group.expand_if_available`\, and
+ :py:attr:`pw_cc_flag_group.expand_if_not_available`\, more complex
+ flag expressions can be made. This is critical for implementing things
+ like the ``libraries_to_link`` feature, where library names are
+ transformed into flags that end up in the final link invocation.
+
+ Note: ``expand_if_equal``, ``expand_if_true``, and ``expand_if_false``
+ are not yet supported.
+
+ 2. Flags are tool-independent. A :py:class:`pw_cc_flag_group` expresses
+ ordered flags that may be reused across various
+ :py:class:`pw_cc_flag_set` rules. This is useful for cases where multiple
+ :py:class:`pw_cc_flag_set` rules must be created to implement a feature
+ for which flags are slightly different depending on the action (e.g.
+ compile vs link). Common flags can be expressed in a shared
+ :py:class:`pw_cc_flag_group`, and the differences can be relegated to
+ separate :py:class:`pw_cc_flag_group` instances.
+
+ Examples:
+
+ .. code-block:: py
+
+ pw_cc_flag_group(
+ name = "user_compile_flag_expansion",
+ flags = ["%{user_compile_flags}"],
+ iterate_over = "user_compile_flags",
+ expand_if_available = "user_compile_flags",
+ )
+
+ # This flag_group might be referenced from various FDO-related
+ # `pw_cc_flag_set` rules. More importantly, the flag sets pulling this in
+ # may apply to different sets of actions.
+ pw_cc_flag_group(
+ name = "fdo_profile_correction",
+ flags = ["-fprofile-correction"],
+ expand_if_available = "fdo_profile_path",
+ )
+
+ .. py:attribute:: flags
+ :type: List[str]
+
+ List of flags provided by this rule.
+
+ For extremely complex expressions of flags that require nested flag groups
+ with multiple layers of expansion, prefer creating a custom rule in
+ `Starlark <https://bazel.build/rules/language>`_ that provides
+ ``FlagGroupInfo`` or ``FlagSetInfo``.
+
+
+ .. py:attribute:: iterate_over
+ :type: str
+
+ Expands :py:attr:`pw_cc_flag_group.flags` for items in the named list.
+
+ Toolchain actions have various variables accessible as names that can be
+ used to guide flag expansions. For variables that are lists,
+ :py:attr:`pw_cc_flag_group.iterate_over` must be used to expand the list into a series of flags.
+
+ Note that :py:attr:`pw_cc_flag_group.iterate_over` is the string name of a
+ build variable, and not an actual list. Valid options are listed in the
+ `C++ Toolchain Configuration <https://bazel.build/docs/cc-toolchain-config-reference#cctoolchainconfiginfo-build-variables>`_
+ reference.
+
+
+
+ Note that the flag expansion stamps out the entire list of flags in
+ :py:attr:`pw_cc_flag_group.flags` once for each item in the list.
+
+ Example:
+
+ .. code-block:: py
+
+ # Expands each path in ``system_include_paths`` to a series of
+ # ``-isystem`` includes.
+ #
+ # Example input:
+ # system_include_paths = ["/usr/local/include", "/usr/include"]
+ #
+ # Expected result:
+ # "-isystem /usr/local/include -isystem /usr/include"
+ pw_cc_flag_group(
+ name = "system_include_paths",
+ flags = ["-isystem", "%{system_include_paths}"],
+ iterate_over = "system_include_paths",
+ )
+
+ .. py:attribute:: expand_if_available
+ :type: str
+
+ Expands the expression in :py:attr:`pw_cc_flag_group.flags` if the
+ specified build variable is set.
+
+ .. py:attribute:: expand_if_not_available
+ :type: str
+
+ Expands the expression in :py:attr:`pw_cc_flag_group.flags` if the
+ specified build variable is **NOT** set.
diff --git a/pw_toolchain_bazel/cc_toolchain/BUILD.bazel b/pw_toolchain_bazel/cc_toolchain/BUILD.bazel
new file mode 100644
index 000000000..18b0865da
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/BUILD.bazel
@@ -0,0 +1,15 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
diff --git a/pw_toolchain_bazel/cc_toolchain/defs.bzl b/pw_toolchain_bazel/cc_toolchain/defs.bzl
new file mode 100644
index 000000000..b5340c54b
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/defs.bzl
@@ -0,0 +1,57 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Toolchain configuration for Bazel."""
+
+load(
+ "//cc_toolchain/private:cc_toolchain.bzl",
+ _OBJ_COPY_ACTION_NAME = "OBJ_COPY_ACTION_NAME",
+ _OBJ_DUMP_ACTION_NAME = "OBJ_DUMP_ACTION_NAME",
+ _pw_cc_toolchain = "pw_cc_toolchain",
+)
+load(
+ "//cc_toolchain/private:flag_set.bzl",
+ _pw_cc_flag_group = "pw_cc_flag_group",
+ _pw_cc_flag_set = "pw_cc_flag_set",
+)
+load(
+ "//cc_toolchain/private:toolchain_feature.bzl",
+ _pw_cc_toolchain_feature = "pw_cc_toolchain_feature",
+)
+load(
+ "//cc_toolchain/private:utils.bzl",
+ _ALL_AR_ACTIONS = "ALL_AR_ACTIONS",
+ _ALL_ASM_ACTIONS = "ALL_ASM_ACTIONS",
+ _ALL_CPP_COMPILER_ACTIONS = "ALL_CPP_COMPILER_ACTIONS",
+ _ALL_C_COMPILER_ACTIONS = "ALL_C_COMPILER_ACTIONS",
+ _ALL_LINK_ACTIONS = "ALL_LINK_ACTIONS",
+)
+
+ALL_ASM_ACTIONS = _ALL_ASM_ACTIONS
+ALL_C_COMPILER_ACTIONS = _ALL_C_COMPILER_ACTIONS
+ALL_CPP_COMPILER_ACTIONS = _ALL_CPP_COMPILER_ACTIONS
+ALL_LINK_ACTIONS = _ALL_LINK_ACTIONS
+ALL_AR_ACTIONS = _ALL_AR_ACTIONS
+
+# TODO(b/301004620): Remove when bazel 7 is released and this constant exists in
+# ACTION_NAMES
+OBJ_COPY_ACTION_NAME = _OBJ_COPY_ACTION_NAME
+OBJ_DUMP_ACTION_NAME = _OBJ_DUMP_ACTION_NAME
+
+pw_cc_flag_group = _pw_cc_flag_group
+pw_cc_flag_set = _pw_cc_flag_set
+
+pw_cc_toolchain = _pw_cc_toolchain
+
+# TODO: b/309533028 - This is deprecated, and will soon be removed.
+pw_cc_toolchain_feature = _pw_cc_toolchain_feature
diff --git a/pw_ide/py/setup.py b/pw_toolchain_bazel/cc_toolchain/private/BUILD.bazel
index 9807aa4e8..6ae1dbd05 100644
--- a/pw_ide/py/setup.py
+++ b/pw_toolchain_bazel/cc_toolchain/private/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,8 +11,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_ide"""
-import setuptools # type: ignore
+package(default_visibility = ["//:__subpackages__"])
-setuptools.setup() # Package definition in setup.cfg
+licenses(["notice"])
diff --git a/pw_toolchain_bazel/cc_toolchain/private/cc_toolchain.bzl b/pw_toolchain_bazel/cc_toolchain/private/cc_toolchain.bzl
new file mode 100644
index 000000000..d36ec8e96
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/private/cc_toolchain.bzl
@@ -0,0 +1,417 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Implementation of the pw_cc_toolchain rule."""
+
+load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
+load(
+ "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
+ "FlagSetInfo",
+ "action_config",
+ "feature",
+ "flag_group",
+ "flag_set",
+ "tool",
+ "variable_with_value",
+)
+load(
+ "//cc_toolchain/private:providers.bzl",
+ "ToolchainFeatureInfo",
+)
+load(
+ "//cc_toolchain/private:utils.bzl",
+ "ALL_ASM_ACTIONS",
+ "ALL_CPP_COMPILER_ACTIONS",
+ "ALL_C_COMPILER_ACTIONS",
+ "ALL_LINK_ACTIONS",
+ "check_deps",
+)
+
+# TODO(b/301004620): Remove when bazel 7 is released and this constant exists in
+# ACTION_NAMES
+OBJ_COPY_ACTION_NAME = "objcopy_embed_data"
+OBJ_DUMP_ACTION_NAME = "objdump_embed_data"
+
+PW_CC_TOOLCHAIN_CONFIG_ATTRS = {
+ "feature_deps": "`pw_cc_toolchain_feature` labels that provide features for this toolchain",
+ "ar": "Path to the tool to use for `ar` (static link) actions",
+ "cpp": "Path to the tool to use for C++ compile actions",
+ "gcc": "Path to the tool to use for C compile actions",
+ "gcov": "Path to the tool to use for generating code coverage data",
+ "ld": "Path to the tool to use for link actions",
+ "strip": "Path to the tool to use for strip actions",
+ "objcopy": "Path to the tool to use for objcopy actions",
+ "objdump": "Path to the tool to use for objdump actions",
+ "action_config_flag_sets": "List of flag sets to apply to the respective `action_config`s",
+
+ # Attributes originally part of create_cc_toolchain_config_info.
+ "toolchain_identifier": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "host_system_name": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "target_system_name": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "target_cpu": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "target_libc": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "compiler": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "abi_version": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "abi_libc_version": "See documentation for cc_common.create_cc_toolchain_config_info()",
+ "cc_target_os": "See documentation for cc_common.create_cc_toolchain_config_info()",
+}
+
+PW_CC_TOOLCHAIN_SHARED_ATTRS = ["toolchain_identifier"]
+
+PW_CC_TOOLCHAIN_BLOCKED_ATTRS = {
+ "toolchain_config": "pw_cc_toolchain includes a generated toolchain config",
+ "artifact_name_patterns": "pw_cc_toolchain does not yet support artifact name patterns",
+ "features": "Use feature_deps to add pw_cc_toolchain_feature deps to the toolchain",
+ "action_configs": "pw_cc_toolchain does not yet support action configs, use the \"ar\", \"cpp\", \"gcc\", \"gcov\", \"ld\", and \"strip\" attributes to set toolchain tools",
+ "cxx_builtin_include_directories": "Use a pw_cc_toolchain_feature to add cxx_builtin_include_directories",
+ "tool_paths": "pw_cc_toolchain does not support tool_paths, use \"ar\", \"cpp\", \"gcc\", \"gcov\", \"ld\", and \"strip\" attributes to set toolchain tools",
+ "make_variables": "pw_cc_toolchain does not yet support make variables",
+ "builtin_sysroot": "Use a pw_cc_toolchain_feature to add a builtin_sysroot",
+}
+
+def _action_configs(action_tool, action_list, flag_sets_by_action):
+ """Binds a tool to an action.
+
+ Args:
+ action_tool (File): Tool to bind to the specified actions.
+ action_list (List[str]): List of actions to bind to the specified tool.
+ flag_sets_by_action: Dictionary mapping action names to lists of applicable flag sets.
+
+ Returns:
+ action_config: A action_config binding the provided tool to the
+ specified actions.
+ """
+ return [
+ action_config(
+ action_name = action,
+ tools = [
+ tool(
+ tool = action_tool,
+ ),
+ ],
+ flag_sets = flag_sets_by_action.get(action, default = []),
+ )
+ for action in action_list
+ ]
+
+def _archiver_flags_feature(is_mac):
+ """Returns our implementation of the legacy archiver_flags feature.
+
+ We provide our own implementation of the archiver_flags. The default
+ implementation of this legacy feature at
+ https://github.com/bazelbuild/bazel/blob/252d36384b8b630d77d21fac0d2c5608632aa393/src/main/java/com/google/devtools/build/lib/rules/cpp/CppActionConfigs.java#L620-L660
+ contains a bug that prevents it from working with llvm-libtool-darwin only
+ fixed in
+ https://github.com/bazelbuild/bazel/commit/ae7cfa59461b2c694226be689662d387e9c38427,
+ which has not yet been released.
+
+ However, we don't merely fix the bug. Part of the Pigweed build involves
+ linking some empty libraries (with no object files). This leads to invoking
+ the archiving tool with no input files. Such an invocation is considered a
+ success by llvm-ar, but not by llvm-libtool-darwin. So for now, we use
+ flags appropriate for llvm-ar here, even on MacOS.
+
+ Args:
+ is_mac: Does the toolchain this feature will be included in target MacOS?
+
+ Returns:
+ The archiver_flags feature.
+ """
+
+ # TODO: b/297413805 - Remove this implementation.
+ return feature(
+ name = "archiver_flags",
+ flag_sets = [
+ flag_set(
+ actions = [
+ ACTION_NAMES.cpp_link_static_library,
+ ],
+ flag_groups = [
+ flag_group(
+ flags = _archiver_flags(is_mac),
+ ),
+ flag_group(
+ expand_if_available = "output_execpath",
+ flags = ["%{output_execpath}"],
+ ),
+ ],
+ ),
+ flag_set(
+ actions = [
+ ACTION_NAMES.cpp_link_static_library,
+ ],
+ flag_groups = [
+ flag_group(
+ expand_if_available = "libraries_to_link",
+ iterate_over = "libraries_to_link",
+ flag_groups = [
+ flag_group(
+ expand_if_equal = variable_with_value(
+ name = "libraries_to_link.type",
+ value = "object_file",
+ ),
+ flags = ["%{libraries_to_link.name}"],
+ ),
+ flag_group(
+ expand_if_equal = variable_with_value(
+ name = "libraries_to_link.type",
+ value = "object_file_group",
+ ),
+ flags = ["%{libraries_to_link.object_files}"],
+ iterate_over = "libraries_to_link.object_files",
+ ),
+ ],
+ ),
+ ],
+ ),
+ ],
+ )
+
+def _archiver_flags(is_mac):
+ """Returns flags for llvm-ar."""
+ if is_mac:
+ return ["--format=darwin", "rcs"]
+ else:
+ return ["rcsD"]
+
+def _strip_actions(flag_set_to_copy):
+ """Copies a flag_set, stripping `actions`.
+
+ Args:
+ flag_set_to_copy: The base flag_set to copy.
+ Returns:
+ flag_set with empty `actions` list.
+ """
+ return flag_set(
+ with_features = flag_set_to_copy.with_features,
+ flag_groups = flag_set_to_copy.flag_groups,
+ )
+
+def _create_action_flag_set_map(flag_sets):
+ """Creates a mapping of action names to flag sets.
+
+ Args:
+ flag_sets: the flag sets to expand.
+ Returns:
+ Dictionary mapping action names to lists of FlagSetInfo providers.
+ """
+ flag_sets_by_action = {}
+ for fs in flag_sets:
+ handled_actions = {}
+ for action in fs.actions:
+ if action not in flag_sets_by_action:
+ flag_sets_by_action[action] = []
+
+ # Dedupe action set list.
+ if action not in handled_actions:
+ handled_actions[action] = True
+ flag_sets_by_action[action].append(_strip_actions(fs))
+ return flag_sets_by_action
+
+def _pw_cc_toolchain_config_impl(ctx):
+ """Rule that provides a CcToolchainConfigInfo.
+
+ Args:
+ ctx: The context of the current build rule.
+
+ Returns:
+ CcToolchainConfigInfo
+ """
+ check_deps(ctx)
+
+ flag_sets_by_action = _create_action_flag_set_map([dep[FlagSetInfo] for dep in ctx.attr.action_config_flag_sets])
+
+ all_actions = []
+ all_actions += _action_configs(ctx.executable.gcc, ALL_ASM_ACTIONS, flag_sets_by_action)
+ all_actions += _action_configs(ctx.executable.gcc, ALL_C_COMPILER_ACTIONS, flag_sets_by_action)
+ all_actions += _action_configs(ctx.executable.cpp, ALL_CPP_COMPILER_ACTIONS, flag_sets_by_action)
+ all_actions += _action_configs(ctx.executable.cpp, ALL_LINK_ACTIONS, flag_sets_by_action)
+
+ all_actions += [
+ action_config(
+ action_name = ACTION_NAMES.cpp_link_static_library,
+ implies = ["archiver_flags", "linker_param_file"],
+ tools = [
+ tool(
+ tool = ctx.executable.ar,
+ ),
+ ],
+ flag_sets = flag_sets_by_action.get(ACTION_NAMES.cpp_link_static_library, default = []),
+ ),
+ action_config(
+ action_name = ACTION_NAMES.llvm_cov,
+ tools = [
+ tool(
+ tool = ctx.executable.gcov,
+ ),
+ ],
+ flag_sets = flag_sets_by_action.get(ACTION_NAMES.llvm_cov, default = []),
+ ),
+ action_config(
+ action_name = OBJ_COPY_ACTION_NAME,
+ tools = [
+ tool(
+ tool = ctx.executable.objcopy,
+ ),
+ ],
+ flag_sets = flag_sets_by_action.get(OBJ_COPY_ACTION_NAME, default = []),
+ ),
+ action_config(
+ action_name = OBJ_DUMP_ACTION_NAME,
+ tools = [
+ tool(
+ tool = ctx.executable.objdump,
+ ),
+ ],
+ flag_sets = flag_sets_by_action.get(OBJ_DUMP_ACTION_NAME, default = []),
+ ),
+ action_config(
+ action_name = ACTION_NAMES.strip,
+ tools = [
+ tool(
+ tool = ctx.executable.strip,
+ ),
+ ],
+ flag_sets = flag_sets_by_action.get(ACTION_NAMES.strip, default = []),
+ ),
+ ]
+
+ features = [dep[ToolchainFeatureInfo].feature for dep in ctx.attr.feature_deps]
+ features.append(_archiver_flags_feature(ctx.attr.target_libc == "macosx"))
+ builtin_include_dirs = []
+ for dep in ctx.attr.feature_deps:
+ builtin_include_dirs.extend(dep[ToolchainFeatureInfo].cxx_builtin_include_directories)
+
+ sysroot_dir = None
+ for dep in ctx.attr.feature_deps:
+ dep_sysroot = dep[ToolchainFeatureInfo].builtin_sysroot
+ if dep_sysroot:
+ if sysroot_dir:
+ fail("Failed to set sysroot at `{}`, already have sysroot at `{}` ".format(dep_sysroot, sysroot_dir))
+ sysroot_dir = dep_sysroot
+
+ return cc_common.create_cc_toolchain_config_info(
+ ctx = ctx,
+ action_configs = all_actions,
+ features = features,
+ cxx_builtin_include_directories = builtin_include_dirs,
+ toolchain_identifier = ctx.attr.toolchain_identifier,
+ host_system_name = ctx.attr.host_system_name,
+ target_system_name = ctx.attr.target_system_name,
+ target_cpu = ctx.attr.target_cpu,
+ target_libc = ctx.attr.target_libc,
+ compiler = ctx.attr.compiler,
+ abi_version = ctx.attr.abi_version,
+ abi_libc_version = ctx.attr.abi_libc_version,
+ builtin_sysroot = sysroot_dir,
+ cc_target_os = ctx.attr.cc_target_os,
+ )
+
+pw_cc_toolchain_config = rule(
+ implementation = _pw_cc_toolchain_config_impl,
+ attrs = {
+ # Attributes new to this rule.
+ "feature_deps": attr.label_list(),
+ "gcc": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "ld": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "ar": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "cpp": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "gcov": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "objcopy": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "objdump": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "strip": attr.label(allow_single_file = True, executable = True, cfg = "exec"),
+ "action_config_flag_sets": attr.label_list(),
+
+ # Attributes from create_cc_toolchain_config_info.
+ "toolchain_identifier": attr.string(),
+ "host_system_name": attr.string(),
+ "target_system_name": attr.string(),
+ "target_cpu": attr.string(),
+ "target_libc": attr.string(),
+ "compiler": attr.string(),
+ "abi_version": attr.string(),
+ "abi_libc_version": attr.string(),
+ "cc_target_os": attr.string(),
+ },
+ provides = [CcToolchainConfigInfo],
+)
+
+def _check_args(rule_label, kwargs):
+ """Checks that args provided to pw_cc_toolchain are valid.
+
+ Args:
+ rule_label: The label of the pw_cc_toolchain rule.
+ kwargs: All attributes supported by pw_cc_toolchain.
+
+ Returns:
+ None
+ """
+ for attr_name, msg in PW_CC_TOOLCHAIN_BLOCKED_ATTRS.items():
+ if attr_name in kwargs:
+ fail(
+ "Toolchain {} has an invalid attribute \"{}\": {}".format(
+ rule_label,
+ attr_name,
+ msg,
+ ),
+ )
+
+def _split_args(kwargs, filter_dict):
+ """Splits kwargs into two dictionaries guided by a filter.
+
+ All items in the kwargs dictionary whose keys are present in the filter
+ dictionary are returned as a new dictionary as the first item in the tuple.
+ All remaining arguments are returned as a dictionary in the second item of
+ the tuple.
+
+ Args:
+ kwargs: Dictionary of args to split.
+ filter_dict: The dictionary used as the filter.
+
+ Returns
+ Tuple[Dict, Dict]
+ """
+ filtered_args = {}
+ remainder = {}
+
+ for attr_name, val in kwargs.items():
+ if attr_name in filter_dict:
+ filtered_args[attr_name] = val
+ else:
+ remainder[attr_name] = val
+
+ return filtered_args, remainder
+
+def pw_cc_toolchain(**kwargs):
+ """A bound cc_toolchain and pw_cc_toolchain_config pair.
+
+ Args:
+ **kwargs: All attributes supported by cc_toolchain and pw_cc_toolchain_config.
+ """
+
+ _check_args(native.package_relative_label(kwargs["name"]), kwargs)
+
+ cc_toolchain_config_args, cc_toolchain_args = _split_args(kwargs, PW_CC_TOOLCHAIN_CONFIG_ATTRS)
+
+ # Bind pw_cc_toolchain_config and the cc_toolchain.
+ config_name = "{}_config".format(cc_toolchain_args["name"])
+ cc_toolchain_config_args["name"] = config_name
+ cc_toolchain_args["toolchain_config"] = ":{}".format(config_name)
+
+ # Copy over arguments that should be shared by both rules.
+ for arg_name in PW_CC_TOOLCHAIN_SHARED_ATTRS:
+ if arg_name in cc_toolchain_config_args:
+ cc_toolchain_args[arg_name] = cc_toolchain_config_args[arg_name]
+
+ pw_cc_toolchain_config(**cc_toolchain_config_args)
+ native.cc_toolchain(**cc_toolchain_args)
diff --git a/pw_toolchain_bazel/cc_toolchain/private/flag_set.bzl b/pw_toolchain_bazel/cc_toolchain/private/flag_set.bzl
new file mode 100644
index 000000000..23094e864
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/private/flag_set.bzl
@@ -0,0 +1,225 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Implementation of pw_cc_flag_set and pw_cc_flag_group."""
+
+load(
+ "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
+ "FlagGroupInfo",
+ "FlagSetInfo",
+ "flag_group",
+ "flag_set",
+)
+
+def _pw_cc_flag_group_impl(ctx):
+ """Implementation for pw_cc_flag_group."""
+
+ # If these are empty strings, they are handled differently than if they
+ # are None. Rather than an explicit error or breakage, there's just silent
+ # behavioral differences. Ideally, these attributes default to `None`, but
+ # that is not supported with string types. Since these have no practical
+ # meaning if they are empty strings, just remap empty strings to `None`.
+ #
+ # A minimal reproducer of this behavior with some useful analysis is
+ # provided here:
+ #
+ # https://github.com/armandomontanez/bazel_reproducers/tree/main/flag_group_with_empty_strings
+ iterate_over = ctx.attr.iterate_over if ctx.attr.iterate_over else None
+ expand_if = ctx.attr.expand_if_available if ctx.attr.expand_if_available else None
+ expand_if_not = ctx.attr.expand_if_not_available if ctx.attr.expand_if_not_available else None
+ return flag_group(
+ flags = ctx.attr.flags,
+ iterate_over = iterate_over,
+ expand_if_available = expand_if,
+ expand_if_not_available = expand_if_not,
+ )
+
+pw_cc_flag_group = rule(
+ implementation = _pw_cc_flag_group_impl,
+ attrs = {
+ "flags": attr.string_list(
+ doc = """List of flags provided by this rule.
+
+For extremely complex expressions of flags that require nested flag groups with
+multiple layers of expansion, prefer creating a custom rule in Starlark that
+provides `FlagGroupInfo` or `FlagSetInfo`.
+""",
+ ),
+ "iterate_over": attr.string(
+ doc = """Expands `flags` for items in the named list.
+
+Toolchain actions have various variables accessible as names that can be used
+to guide flag expansions. For variables that are lists, `iterate_over` must be
+used to expand the list into a series of flags.
+
+Note that `iterate_over` is the string name of a build variable, and not an
+actual list. Valid options are listed at:
+
+ https://bazel.build/docs/cc-toolchain-config-reference#cctoolchainconfiginfo-build-variables
+
+Note that the flag expansion stamps out the entire list of flags in `flags`
+once for each item in the list.
+
+Example:
+
+ # Expands each path in `system_include_paths` to a series of `-isystem`
+ # includes.
+ #
+ # Example input:
+ # system_include_paths = ["/usr/local/include", "/usr/include"]
+ #
+ # Expected result:
+ # "-isystem /usr/local/include -isystem /usr/include"
+ pw_cc_flag_group(
+ name = "system_include_paths",
+ flags = ["-isystem", "%{system_include_paths}"],
+ iterate_over = "system_include_paths",
+ )
+""",
+ ),
+ "expand_if_available": attr.string(
+ doc = "Expands the expression in `flags` if the specified build variable is set.",
+ ),
+ "expand_if_not_available": attr.string(
+ doc = "Expands the expression in `flags` if the specified build variable is NOT set.",
+ ),
+ },
+ provides = [FlagGroupInfo],
+ doc = """Declares an (optionally parametric) ordered set of flags.
+
+`pw_cc_flag_group` rules are expected to be consumed exclusively by
+`pw_cc_flag_set` rules. Though simple lists of flags can be expressed by
+populating `flags` on a `pw_cc_flag_set`, `pw_cc_flag_group` provides additional
+power in the following two ways:
+
+ 1. Iteration and conditional expansion. Using `iterate_over`,
+ `expand_if_available`, and `expand_if_not_available`, more complex flag
+ expressions can be made. This is critical for implementing things like
+ the `libraries_to_link` feature, where library names are transformed
+ into flags that end up in the final link invocation.
+
+ Note: `expand_if_equal`, `expand_if_true`, and `expand_if_false` are not
+ yet supported.
+
+ 2. Flags are tool-independent. A `pw_cc_flag_group` expresses ordered flags
+ that may be reused across various `pw_cc_flag_set` rules. This is useful
+ for cases where multiple `pw_cc_flag_set` rules must be created to
+ implement a feature for which flags are slightly different depending on
+ the action (e.g. compile vs link). Common flags can be expressed in a
+ shared `pw_cc_flag_group`, and the differences can be relegated to
+ separate `pw_cc_flag_group` instances.
+
+Examples:
+
+ pw_cc_flag_group(
+ name = "user_compile_flag_expansion",
+ flags = ["%{user_compile_flags}"],
+ iterate_over = "user_compile_flags",
+ expand_if_available = "user_compile_flags",
+ )
+
+ # This flag_group might be referenced from various FDO-related
+ # `pw_cc_flag_set` rules. More importantly, the flag sets pulling this in
+ # may apply to different sets of actions.
+ pw_cc_flag_group(
+ name = "fdo_profile_correction",
+ flags = ["-fprofile-correction"],
+ expand_if_available = "fdo_profile_path",
+ )
+""",
+)
+
+def _pw_cc_flag_set_impl(ctx):
+ """Implementation for pw_cc_flag_set."""
+ if ctx.attr.flags and ctx.attr.flag_groups:
+ fail("{} specifies both `flag_groups` and `flags`, but only one can be specified. Consider splitting into two `pw_cc_flag_set` rules to make the intended order clearer.".format(ctx.label))
+ flag_groups = []
+ if ctx.attr.flags:
+ flag_groups.append(flag_group(flags = ctx.attr.flags))
+ elif ctx.attr.flag_groups:
+ for dep in ctx.attr.flag_groups:
+ if not dep[FlagGroupInfo]:
+ fail("{} in `flag_groups` of {} does not provide FlagGroupInfo".format(dep.label, ctx.label))
+
+ flag_groups = [dep[FlagGroupInfo] for dep in ctx.attr.flag_groups]
+ return flag_set(
+ actions = ctx.attr.actions,
+ flag_groups = flag_groups,
+ )
+
+pw_cc_flag_set = rule(
+ implementation = _pw_cc_flag_set_impl,
+ attrs = {
+ "actions": attr.string_list(
+ mandatory = True,
+ # inclusive-language: disable
+ doc = """A list of action names that this flag set applies to.
+
+Valid choices are listed here:
+
+ https://github.com/bazelbuild/bazel/blob/master/tools/build_defs/cc/action_names.bzl
+
+It is possible for some needed action names to not be enumerated in this list,
+so there is not rigid validation for these strings. Prefer using constants
+rather than manually typing action names.
+""",
+ # inclusive-language: enable
+ ),
+ "flag_groups": attr.label_list(
+ doc = """Labels pointing to `pw_cc_flag_group` rules.
+
+This is intended to be compatible with any other rules that provide
+`FlagGroupInfo`. These are evaluated in order, with earlier flag groups
+appearing earlier in the invocation of the underlying tool.
+
+Note: `flag_groups` and `flags` are mutually exclusive.
+""",
+ ),
+ "flags": attr.string_list(
+ doc = """Flags that should be applied to the specified actions.
+
+These are evaluated in order, with earlier flags appearing earlier in the
+invocation of the underlying tool. If you need expansion logic, prefer
+enumerating flags in a `pw_cc_flag_group` or create a custom rule that provides
+`FlagGroupInfo`.
+
+Note: `flags` and `flag_groups` are mutually exclusive.
+""",
+ ),
+ },
+ provides = [FlagSetInfo],
+ doc = """Declares an ordered set of flags bound to a set of actions.
+
+Flag sets can be attached to a `pw_cc_toolchain` via `action_config_flag_sets`.
+
+Examples:
+
+ pw_cc_flag_set(
+ name = "warnings_as_errors",
+ flags = ["-Werror"],
+ )
+
+ pw_cc_flag_set(
+ name = "layering_check",
+ flag_groups = [
+ ":strict_module_headers",
+ ":dependent_module_map_files",
+ ],
+ )
+
+Note: In the vast majority of cases, alphabetical sorting is not desirable for
+the `flags` and `flag_groups` attributes. Buildifier shouldn't ever try to sort
+these, but in the off chance it starts to these members should be listed as
+exceptions in the `SortableDenylist`.
+""",
+)
diff --git a/pw_toolchain_bazel/cc_toolchain/private/providers.bzl b/pw_toolchain_bazel/cc_toolchain/private/providers.bzl
new file mode 100644
index 000000000..a63a7d17a
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/private/providers.bzl
@@ -0,0 +1,23 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""All shared providers that act as an API between toolchain-related rules."""
+
+ToolchainFeatureInfo = provider(
+ doc = "A provider containing cc_toolchain features and related fields.",
+ fields = {
+ "feature": "feature: A group of build flags structured as a toolchain feature.",
+ "cxx_builtin_include_directories": "List[str]: Builtin C/C++ standard library include directories.",
+ "builtin_sysroot": "str: Path to the sysroot directory. Use `external/[repo_name]` for sysroots provided as an external repository.",
+ },
+)
diff --git a/pw_toolchain_bazel/cc_toolchain/private/toolchain_feature.bzl b/pw_toolchain_bazel/cc_toolchain/private/toolchain_feature.bzl
new file mode 100644
index 000000000..e16d331da
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/private/toolchain_feature.bzl
@@ -0,0 +1,215 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Implementation of the pw_cc_toolchain_feature rule."""
+
+load(
+ "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
+ "feature",
+ "flag_group",
+ "flag_set",
+)
+load(
+ "//cc_toolchain/private:providers.bzl",
+ "ToolchainFeatureInfo",
+)
+load("//cc_toolchain/private:utils.bzl", "ACTION_MAP")
+
+TOOLCHAIN_FEATURE_INFO_ATTRS = {
+ "aropts": "List[str]: Flags to pass to all archive actions",
+ "asmopts": "List[str]: Flags to pass to assembler actions",
+ "copts": "List[str]: Flags to pass to all C/C++ compile actions",
+ "conlyopts": "List[str]: Flags to pass to C compile actions",
+ "cxxopts": "List[str]: Flags to pass to C++ compile actions",
+ "linkopts": "List[str]: Flags to pass to C compile actions",
+ "linker_files": "List[File]: Files to link",
+ "cxx_builtin_include_directories": "List[str]: Paths to C++ standard library include directories",
+ "builtin_sysroot": "str: Path to the directory containing the sysroot",
+}
+
+def _dict_to_str(dict_to_stringify):
+ """Converts a dictionary to a multi-line string.
+
+ Args:
+ dict_to_stringify (Dict[str, str]): Dictionary to stringify.
+
+ Returns:
+ str: Multi-line string representing the dictionary, or {empty}.
+ """
+ result = []
+ for key in dict_to_stringify.keys():
+ result.append(" {}: {}".format(key, dict_to_stringify[key]))
+ if not result:
+ return "{empty}"
+ return "\n".join(["{"] + result + ["}"])
+
+def _feature_flag_set(actions, flags):
+ """Transforms a list of flags and actions into a flag_set.
+
+ Args:
+ actions (List[str]): Actions that the provided flags will be applied to.
+ flags (List[str]): List of flags to apply to the specified actions.
+
+ Returns:
+ flag_set: A flag_set binding the provided flags to the specified
+ actions.
+ """
+ return flag_set(
+ actions = actions,
+ flag_groups = ([
+ flag_group(
+ flags = flags,
+ ),
+ ]),
+ )
+
+def _check_args(ctx, **kwargs):
+ """Checks that args provided to build_toolchain_feature_info are valid.
+
+ Args:
+ ctx: The context of the current build rule.
+ **kwargs: All attributes supported by pw_cc_toolchain_feature.
+
+ Returns:
+ None
+ """
+ for key in kwargs.keys():
+ if key not in TOOLCHAIN_FEATURE_INFO_ATTRS:
+ fail(
+ "Unknown attribute \"{}\" used by {}. Valid attributes are:\n{}".format(
+ key,
+ ctx.label,
+ _dict_to_str(TOOLCHAIN_FEATURE_INFO_ATTRS),
+ ),
+ )
+
+def _build_flag_sets(**kwargs):
+ """Transforms a dictionary of arguments into a list of flag_sets.
+
+ Args:
+ **kwargs: All attributes supported by pw_cc_toolchain_feature.
+
+ Returns:
+ List[flag_set]: A list of flag_sets that bind all provided flags to
+ their appropriate actions.
+ """
+ all_flags = []
+ for action in ACTION_MAP.keys():
+ if kwargs[action]:
+ all_flags.append(_feature_flag_set(ACTION_MAP[action], kwargs[action]))
+ return all_flags
+
+def _initialize_args(**kwargs):
+ """Initializes build_toolchain_feature_info arguments.
+
+ Args:
+ **kwargs: All attributes supported by pw_cc_toolchain_feature.
+
+ Returns:
+ Dict[str, Any]: Dictionary containing arguments default initialized to
+ be compatible with _build_flag_sets.
+ """
+ initialized_args = {}
+ for action in ACTION_MAP.keys():
+ if action in kwargs:
+ initialized_args[action] = kwargs[action]
+ else:
+ initialized_args[action] = []
+
+ if "linker_files" in kwargs:
+ linker_files = kwargs["linker_files"]
+ linker_flags = [file.path for file in linker_files]
+
+ initialized_args["linkopts"] = initialized_args["linkopts"] + linker_flags
+ initialized_args["linker_files"] = depset(linker_files)
+ else:
+ initialized_args["linker_files"] = depset()
+
+ if "cxx_builtin_include_directories" in kwargs:
+ initialized_args["cxx_builtin_include_directories"] = kwargs["cxx_builtin_include_directories"]
+ else:
+ initialized_args["cxx_builtin_include_directories"] = []
+
+ if "builtin_sysroot" in kwargs:
+ initialized_args["builtin_sysroot"] = kwargs["builtin_sysroot"]
+ else:
+ initialized_args["builtin_sysroot"] = None
+ return initialized_args
+
+def build_toolchain_feature_info(ctx, **kwargs):
+ """Builds a ToolchainFeatureInfo provider.
+
+ Args:
+ ctx: The context of the current build rule.
+ **kwargs: All attributes supported by pw_cc_toolchain_feature.
+
+ Returns:
+ ToolchainFeatureInfo, DefaultInfo: All providers supported by
+ pw_cc_toolchain_feature.
+ """
+ _check_args(ctx, **kwargs)
+
+ initialized_args = _initialize_args(**kwargs)
+
+ new_feature = feature(
+ name = ctx.attr.name,
+ enabled = True,
+ flag_sets = _build_flag_sets(**initialized_args),
+ )
+
+ return [
+ ToolchainFeatureInfo(
+ feature = new_feature,
+ cxx_builtin_include_directories = initialized_args["cxx_builtin_include_directories"],
+ builtin_sysroot = initialized_args["builtin_sysroot"],
+ ),
+ DefaultInfo(files = initialized_args["linker_files"]),
+ ]
+
+def _pw_cc_toolchain_feature_impl(ctx):
+ """Rule that provides ToolchainFeatureInfo.
+
+ Args:
+ ctx: The context of the current build rule.
+
+ Returns:
+ ToolchainFeatureInfo, DefaultInfo
+ """
+ return build_toolchain_feature_info(
+ ctx = ctx,
+ aropts = ctx.attr.aropts,
+ asmopts = ctx.attr.asmopts,
+ copts = ctx.attr.copts,
+ conlyopts = ctx.attr.conlyopts,
+ cxxopts = ctx.attr.cxxopts,
+ linkopts = ctx.attr.linkopts,
+ linker_files = ctx.files.linker_files,
+ cxx_builtin_include_directories = ctx.attr.cxx_builtin_include_directories,
+ builtin_sysroot = ctx.attr.builtin_sysroot,
+ )
+
+pw_cc_toolchain_feature = rule(
+ implementation = _pw_cc_toolchain_feature_impl,
+ attrs = {
+ "aropts": attr.string_list(),
+ "asmopts": attr.string_list(),
+ "copts": attr.string_list(),
+ "conlyopts": attr.string_list(),
+ "cxxopts": attr.string_list(),
+ "linkopts": attr.string_list(),
+ "linker_files": attr.label_list(allow_files = True),
+ "cxx_builtin_include_directories": attr.string_list(),
+ "builtin_sysroot": attr.string(),
+ },
+ provides = [ToolchainFeatureInfo, DefaultInfo],
+)
diff --git a/pw_toolchain_bazel/cc_toolchain/private/utils.bzl b/pw_toolchain_bazel/cc_toolchain/private/utils.bzl
new file mode 100644
index 000000000..7030f2498
--- /dev/null
+++ b/pw_toolchain_bazel/cc_toolchain/private/utils.bzl
@@ -0,0 +1,73 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Private utilities and global variables."""
+
+load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
+load(
+ "@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl",
+ "FlagSetInfo",
+)
+load(
+ "//cc_toolchain/private:providers.bzl",
+ "ToolchainFeatureInfo",
+)
+
+# A potentially more complete set of these constants are available at
+# @rules_cc//cc:action_names.bzl, but it's not clear if they should be depended
+# on.
+ALL_ASM_ACTIONS = [
+ ACTION_NAMES.assemble,
+ ACTION_NAMES.preprocess_assemble,
+]
+ALL_C_COMPILER_ACTIONS = [
+ ACTION_NAMES.c_compile,
+ ACTION_NAMES.cc_flags_make_variable,
+]
+ALL_CPP_COMPILER_ACTIONS = [
+ ACTION_NAMES.cpp_compile,
+ ACTION_NAMES.cpp_header_parsing,
+]
+ALL_LINK_ACTIONS = [
+ ACTION_NAMES.cpp_link_executable,
+ ACTION_NAMES.cpp_link_dynamic_library,
+ ACTION_NAMES.cpp_link_nodeps_dynamic_library,
+]
+ALL_AR_ACTIONS = [
+ ACTION_NAMES.cpp_link_static_library,
+]
+
+ACTION_MAP = {
+ "aropts": ALL_AR_ACTIONS,
+ "asmopts": ALL_ASM_ACTIONS,
+ "copts": ALL_C_COMPILER_ACTIONS + ALL_CPP_COMPILER_ACTIONS,
+ "conlyopts": ALL_C_COMPILER_ACTIONS,
+ "cxxopts": ALL_CPP_COMPILER_ACTIONS,
+ "linkopts": ALL_LINK_ACTIONS,
+}
+
+def _check_dep_provides(ctx_label, dep, provider, what_provides):
+ if provider not in dep:
+ fail(
+ "{} listed as a dependency of {}, but it's not a {}".format(
+ dep.label,
+ ctx_label,
+ what_provides,
+ ),
+ )
+
+def check_deps(ctx):
+ for dep in ctx.attr.feature_deps:
+ _check_dep_provides(ctx.label, dep, ToolchainFeatureInfo, "pw_cc_toolchain_feature")
+ for dep in ctx.attr.action_config_flag_sets:
+ _check_dep_provides(ctx.label, dep, FlagSetInfo, "pw_cc_flag_set")
diff --git a/pw_toolchain_bazel/constraints/arm_mcpu/BUILD.bazel b/pw_toolchain_bazel/constraints/arm_mcpu/BUILD.bazel
new file mode 100644
index 000000000..4983dac9a
--- /dev/null
+++ b/pw_toolchain_bazel/constraints/arm_mcpu/BUILD.bazel
@@ -0,0 +1,80 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# The target Arm processor.
+#
+# The values of this constraint_setting correspond to valid values of the -mcpu
+# flag for Arm gcc. See
+# https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html#index-mcpu-2. These
+# values are intended to be used in `target_compatible_with` attributes of
+# toolchains which specify the corresponding value of -mcpu.
+#
+# The constraint_values are not currently exhaustively enumerated (only a few
+# of the legal -mcpu values have corresponding constraint_values). The intent
+# is for additional values to be added when needed.
+#
+# The intent is to support only (processor type + optional architectural
+# extensions) forms of -mcpu (e.g., cortex-m4 or cortex-m4+nofp), not march
+# values (e.g., armv7e-m). This is because it is recommended to use (processor
+# type + optional architectural extensions) when configuring Arm GCC (see
+# https://community.arm.com/arm-community-blogs/b/tools-software-ides-blog/posts/compiler-flags-across-architectures-march-mtune-and-mcpu).
+# In addition, the march values can already be captured using @platforms//cpu.
+constraint_setting(
+ name = "mcpu",
+ default_constraint_value = "none",
+)
+
+constraint_value(
+ name = "none",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m0",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m3",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m4",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m4+nofp",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m7",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m33",
+ constraint_setting = ":mcpu",
+)
+
+constraint_value(
+ name = "cortex-m33+nofp",
+ constraint_setting = ":mcpu",
+)
diff --git a/pw_toolchain_bazel/docs.rst b/pw_toolchain_bazel/docs.rst
new file mode 100644
index 000000000..075dc3f7b
--- /dev/null
+++ b/pw_toolchain_bazel/docs.rst
@@ -0,0 +1,59 @@
+.. _module-pw_toolchain_bazel:
+
+==================
+pw_toolchain_bazel
+==================
+
+.. pigweed-module::
+ :name: pw_toolchain_bazel
+ :tagline: Modular Bazel C/C++ toolchain API
+ :status: unstable
+ :languages: Starlark
+
+Assembling a complete, hermetic toolchain with Bazel using the native primitives
+can be quite challenging. Additionally, Bazel's native API for declaring C/C++
+toolchains doesn't inherently encourage modularity or reusability.
+
+``pw_toolchain_bazel`` provides a suite of building blocks that make the process
+of assembling a complete, hermetic toolchain significantly easier. The Bazel
+rules introduced by this module push the vast majority of a toolchain's
+declaration into build files, and encourages reusability through sharing of
+flag groups, tools, and toolchain feature implementations.
+
+While this module does **not** provide a hermetic toolchain, Pigweed provides
+`fully instantiated and supported toolchains <https://cs.opensource.google/pigweed/pigweed/+/main:pw_toolchain/host_clang/BUILD.bazel>`_
+that are a useful reference for building your own toolchain.
+
+.. warning::
+ `b/309533028 <https://issues.pigweed.dev/309533028>`_\: This module is under
+ construction and is subject to major breaking changes.
+
+.. grid:: 1
+
+ .. grid-item-card:: :octicon:`info` API reference
+ :link: module-pw_toolchain_bazel-api
+ :link-type: ref
+ :class-item: sales-pitch-cta-primary
+
+ Detailed reference information about the pw_toolchain_bazel API.
+
+.. grid:: 1
+
+ .. grid-item-card:: :octicon:`file` Original SEED
+ :link: seed-0113
+ :link-type: ref
+ :class-item: sales-pitch-cta-secondary
+
+ SEED-0113: Add modular Bazel C/C++ toolchain API
+
+------------
+Dependencies
+------------
+This module is not permitted to have dependencies on other modules. When this
+module stabilizes, it will be broken out into a separate repository.
+
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+
+ API reference <api>
diff --git a/pw_toolchain_bazel/features/BUILD.bazel b/pw_toolchain_bazel/features/BUILD.bazel
new file mode 100644
index 000000000..c41e01396
--- /dev/null
+++ b/pw_toolchain_bazel/features/BUILD.bazel
@@ -0,0 +1,87 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("//cc_toolchain:defs.bzl", "pw_cc_toolchain_feature")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Optimization level option
+pw_cc_toolchain_feature(
+ name = "o2",
+ copts = ["-O2"],
+ linkopts = ["-O2"],
+)
+
+# Disables linking of the default C++ standard library to allow linking of a
+# different version.
+pw_cc_toolchain_feature(
+ name = "no_default_cpp_stdlib",
+ linkopts = ["-nostdlib++"],
+)
+
+# Prevent relative paths from being converted to absolute paths.
+pw_cc_toolchain_feature(
+ name = "no_canonical_prefixes",
+ copts = [
+ "-no-canonical-prefixes",
+ ],
+)
+
+# Compile without runtime type information (RTTI). This produces smaller binaries.
+pw_cc_toolchain_feature(
+ name = "no_rtti",
+ cxxopts = [
+ "-fno-rtti",
+ ],
+)
+
+# Allow uses of the register keyword, which may appear in C headers.
+pw_cc_toolchain_feature(
+ name = "wno_register",
+ cxxopts = [
+ "-Wno-register",
+ ],
+)
+
+# Compile for the C++17 standard.
+pw_cc_toolchain_feature(
+ name = "c++17",
+ cxxopts = ["-std=c++17"],
+ linkopts = ["-std=c++17"],
+)
+
+# Issue a warning when a class appears to be polymorphic, yet it declares a
+# non-virtual destructor
+pw_cc_toolchain_feature(
+ name = "wnon_virtual_dtor",
+ cxxopts = ["-Wnon-virtual-dtor"],
+)
+
+# Standard compiler flags to reduce output binary size.
+pw_cc_toolchain_feature(
+ name = "reduced_size",
+ copts = [
+ "-fno-common",
+ "-fno-exceptions",
+ "-ffunction-sections",
+ "-fdata-sections",
+ ],
+)
+
+pw_cc_toolchain_feature(
+ name = "debugging",
+ copts = ["-g"],
+)
diff --git a/pw_toolchain_bazel/features/macos/BUILD.bazel b/pw_toolchain_bazel/features/macos/BUILD.bazel
new file mode 100644
index 000000000..dc28cfad6
--- /dev/null
+++ b/pw_toolchain_bazel/features/macos/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("//features/macos/private:xcode_command_line_tools.bzl", "pw_macos_sysroot")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Provides the `--sysroot` flag for macOS using Xcode command line tools.
+pw_macos_sysroot(
+ name = "macos_sysroot",
+ # This repository is generated by xcode_command_line_tools_repository()
+ # in the build's WORKSPACE file.
+ sdk = "@pw_xcode_command_line_tools//:default",
+ target_compatible_with = ["@platforms//os:macos"],
+)
diff --git a/pw_toolchain_bazel/features/macos/generate_xcode_repository.bzl b/pw_toolchain_bazel/features/macos/generate_xcode_repository.bzl
new file mode 100644
index 000000000..2ab6557dc
--- /dev/null
+++ b/pw_toolchain_bazel/features/macos/generate_xcode_repository.bzl
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Generates an Xcode repository that enables //features/macos:macos_sysroot."""
+
+load(
+ "//features/macos/private:xcode_command_line_tools.bzl",
+ "pw_xcode_repository",
+)
+
+def pw_xcode_command_line_tools_repository():
+ """Call this from a WORKSPACE file."""
+ pw_xcode_repository(
+ name = "pw_xcode_command_line_tools",
+ )
diff --git a/pw_toolchain_bazel/features/macos/private/BUILD.bazel b/pw_toolchain_bazel/features/macos/private/BUILD.bazel
new file mode 100644
index 000000000..0ee7bfae8
--- /dev/null
+++ b/pw_toolchain_bazel/features/macos/private/BUILD.bazel
@@ -0,0 +1,15 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:private"])
diff --git a/pw_toolchain_bazel/features/macos/private/xcode_command_line_tools.bzl b/pw_toolchain_bazel/features/macos/private/xcode_command_line_tools.bzl
new file mode 100644
index 000000000..dbd741e2d
--- /dev/null
+++ b/pw_toolchain_bazel/features/macos/private/xcode_command_line_tools.bzl
@@ -0,0 +1,116 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Xcode configuration for Bazel build.
+
+This replaces xcode_configure, but only intends to work with macOS host builds,
+and exclusively attempts to support xcode command-line tools.
+"""
+
+load("//cc_toolchain/private:providers.bzl", "ToolchainFeatureInfo") # buildifier: disable=bzl-visibility
+load("//cc_toolchain/private:toolchain_feature.bzl", "build_toolchain_feature_info") # buildifier: disable=bzl-visibility
+
+XcodeSdkInfo = provider(
+ doc = "A simple provider that provides the path to the macOS Xcode SDK",
+ fields = {
+ "sdk_path": "str: Path to the macOS sdk",
+ },
+)
+
+def _pw_xcode_repository_impl(repository_ctx):
+ """Generated repository containing a pw_xcode_info target.
+
+ Args:
+ repository_ctx: The context of the current repository.
+
+ Returns:
+ None
+ """
+
+ # This is required to make a repository, so make a stub for all other
+ # operating systems.
+ if repository_ctx.os.name != "mac os x":
+ lines = [
+ "filegroup(",
+ " name = \"default\",",
+ " visibility = [\"@pw_toolchain//features/macos:__pkg__\"],",
+ ")",
+ ]
+ repository_ctx.file("BUILD", "\n".join(lines))
+ return
+
+ xcrun_result = repository_ctx.execute(["/usr/bin/xcrun", "--show-sdk-path"])
+ if xcrun_result.return_code != 0:
+ fail("Failed locating Xcode SDK: {}".format(xcrun_result.stderr))
+
+ sdk_path = xcrun_result.stdout.replace("\n", "")
+ lines = [
+ "load(\"@pw_toolchain//features/macos/private:xcode_command_line_tools.bzl\", \"pw_xcode_info\")",
+ "pw_xcode_info(",
+ " name = \"default\",",
+ " sdk_path = \"{}\",".format(sdk_path),
+ " visibility = [\"@pw_toolchain//features/macos:__pkg__\"],",
+ ")",
+ ]
+
+ if xcrun_result.return_code == 0:
+ repository_ctx.file("BUILD", "\n".join(lines))
+
+pw_xcode_repository = repository_rule(
+ _pw_xcode_repository_impl,
+ attrs = {},
+ doc = "Initializes a macOS SDK repository",
+)
+
+def _xcode_info_impl(ctx):
+ """Rule that provides XcodeSdkInfo.
+
+ Args:
+ ctx: The context of the current build rule.
+
+ Returns:
+ XcodeSdkInfo
+ """
+ return [XcodeSdkInfo(sdk_path = ctx.attr.sdk_path)]
+
+pw_xcode_info = rule(
+ implementation = _xcode_info_impl,
+ attrs = {
+ "sdk_path": attr.string(),
+ },
+ provides = [XcodeSdkInfo],
+)
+
+def _pw_macos_sysroot_impl(ctx):
+ """Rule that provides an Xcode-provided sysroot as ToolchainFeatureInfo.
+
+ Args:
+ ctx: The context of the current build rule.
+
+ Returns:
+ ToolchainFeatureInfo
+ """
+ sdk_path = ctx.attr.sdk[XcodeSdkInfo].sdk_path
+ return build_toolchain_feature_info(
+ ctx = ctx,
+ cxx_builtin_include_directories = ["%sysroot%/usr/include"],
+ builtin_sysroot = sdk_path,
+ )
+
+pw_macos_sysroot = rule(
+ implementation = _pw_macos_sysroot_impl,
+ attrs = {
+ "sdk": attr.label(),
+ },
+ provides = [ToolchainFeatureInfo],
+)
diff --git a/pw_trace/BUILD.bazel b/pw_trace/BUILD.bazel
index 8f90e4ed3..f88d15c92 100644
--- a/pw_trace/BUILD.bazel
+++ b/pw_trace/BUILD.bazel
@@ -38,15 +38,20 @@ pw_cc_facade(
pw_cc_library(
name = "pw_trace",
+ hdrs = [
+ "public/pw_trace/internal/trace_internal.h",
+ "public/pw_trace/trace.h",
+ ],
+ includes = ["public"],
deps = [
- ":facade",
- "@pigweed_config//:pw_trace_backend",
+ "//pw_preprocessor",
+ "@pigweed//targets:pw_trace_backend",
],
)
pw_cc_library(
name = "backend_multiplexer",
- visibility = ["@pigweed_config//:__pkg__"],
+ visibility = ["@pigweed//targets:__pkg__"],
deps = ["//pw_trace:null"],
)
@@ -133,7 +138,7 @@ pw_cc_library(
srcs = ["example/sample_app.cc"],
hdrs = ["example/public/pw_trace/example/sample_app.h"],
includes = ["example/public"],
- # TODO(b/258071921): Fix puzzling compiler errors
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
"//pw_ring_buffer",
@@ -144,7 +149,7 @@ pw_cc_library(
pw_cc_binary(
name = "trace_example_basic",
srcs = ["example/basic.cc"],
- # TODO(b/258071921): Fix puzzling compiler errors
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
":pw_trace_sample_app",
diff --git a/pw_trace/example/basic.cc b/pw_trace/example/basic.cc
index b8d8bb8aa..066e513ee 100644
--- a/pw_trace/example/basic.cc
+++ b/pw_trace/example/basic.cc
@@ -24,4 +24,4 @@ int main() {
PW_LOG_ERROR("Running basic trace example...\n");
RunTraceSampleApp();
return 0;
-} \ No newline at end of file
+}
diff --git a/pw_trace/example/public/pw_trace/example/sample_app.h b/pw_trace/example/public/pw_trace/example/sample_app.h
index fbc5fc36b..b5f9d3e94 100644
--- a/pw_trace/example/public/pw_trace/example/sample_app.h
+++ b/pw_trace/example/public/pw_trace/example/sample_app.h
@@ -17,4 +17,4 @@
#pragma once
// Run the trace sample app code, this will not return.
-void RunTraceSampleApp(); \ No newline at end of file
+void RunTraceSampleApp();
diff --git a/pw_trace/example/sample_app.cc b/pw_trace/example/sample_app.cc
index 016f7787d..b7cb39518 100644
--- a/pw_trace/example/sample_app.cc
+++ b/pw_trace/example/sample_app.cc
@@ -79,7 +79,7 @@ class ProcessingTask : public SimpleRunnable {
pw::span<std::byte> buf_span = pw::span<std::byte>(
reinterpret_cast<std::byte*>(jobs_buffer_), sizeof(jobs_buffer_));
jobs_.SetBuffer(buf_span)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
const char* Name() const override { return "Processing Task"; }
bool ShouldRun() override { return jobs_.EntryCount() > 0; }
@@ -92,9 +92,9 @@ class ProcessingTask : public SimpleRunnable {
// Get the next job from the queue.
jobs_.PeekFront(job_bytes.bytes, &bytes_read)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
jobs_.PopFront()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
Job& job = job_bytes.job;
// Process the job
@@ -139,7 +139,7 @@ class ProcessingTask : public SimpleRunnable {
void AddJobInternal(uint32_t job_id, uint8_t value) {
JobBytes job{.job = {.job_id = job_id, .value = value}};
jobs_.PushBack(job.bytes)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
} processing_task;
diff --git a/pw_trace/public/pw_trace/internal/null.h b/pw_trace/public/pw_trace/internal/null.h
index 155b54adb..37dcae3b5 100644
--- a/pw_trace/public/pw_trace/internal/null.h
+++ b/pw_trace/public/pw_trace/internal/null.h
@@ -42,11 +42,11 @@ PW_EXTERN_C_START
//
// These two functions are used in PW_TRACE and PW_TRACE_DATA.
-static inline void pw_trace_Ignored(int event_type,
- uint8_t flags,
- const char* label,
- const char* group,
- uint32_t trace_id) {
+static inline void _pw_trace_Ignored(int event_type,
+ uint8_t flags,
+ const char* label,
+ const char* group,
+ uint32_t trace_id) {
(void)event_type;
(void)flags;
(void)label;
@@ -54,13 +54,13 @@ static inline void pw_trace_Ignored(int event_type,
(void)trace_id;
}
-static inline void pw_trace_data_Ignored(int event_type,
+static inline void _pw_trace_IgnoredData(int event_type,
uint8_t flags,
const char* label,
const char* group,
uint32_t trace_id,
const char* type,
- const char* data,
+ const void* data,
size_t size) {
(void)event_type;
(void)flags;
@@ -75,9 +75,9 @@ static inline void pw_trace_data_Ignored(int event_type,
PW_EXTERN_C_END
#define PW_TRACE(event_type, flags, label, group, trace_id) \
- pw_trace_Ignored(event_type, flags, label, group, trace_id)
+ _pw_trace_Ignored(event_type, flags, label, group, trace_id)
#define PW_TRACE_DATA( \
event_type, flags, label, group, trace_id, type, data, size) \
- pw_trace_data_Ignored( \
+ _pw_trace_IgnoredData( \
event_type, flags, label, group, trace_id, type, data, size)
diff --git a/pw_trace/py/BUILD.gn b/pw_trace/py/BUILD.gn
index dd50814ce..733ad1c10 100644
--- a/pw_trace/py/BUILD.gn
+++ b/pw_trace/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_trace/__init__.py",
diff --git a/pw_trace/py/pw_trace/trace.py b/pw_trace/py/pw_trace/trace.py
index 571d66f0c..7535520c9 100755
--- a/pw_trace/py/pw_trace/trace.py
+++ b/pw_trace/py/pw_trace/trace.py
@@ -73,16 +73,21 @@ def event_has_trace_id(event_type):
def decode_struct_fmt_args(event):
"""Decodes the trace's event data for struct-formatted data"""
args = {}
- # we assume all data is packed, little-endian ordering if not specified
+ # We assume all data is packed, little-endian ordering if not specified.
struct_fmt = event.data_fmt[len("@pw_py_struct_fmt:") :]
if not struct_fmt.startswith(_ORDERING_CHARS):
struct_fmt = "<" + struct_fmt
try:
- # needed in case the buffer is larger than expected
+ # Assert is needed in case the buffer is larger than expected.
assert struct.calcsize(struct_fmt) == len(event.data)
items = struct.unpack_from(struct_fmt, event.data)
for i, item in enumerate(items):
- args["data_" + str(i)] = item
+ # Try to decode the item in case it is a byte array (string) since
+ # the JSON lib cannot serialize byte arrays.
+ try:
+ args["data_" + str(i)] = item.decode()
+ except (UnicodeDecodeError, AttributeError):
+ args["data_" + str(i)] = item
except (AssertionError, struct.error):
args["error"] = (
f"Mismatched struct/data format {event.data_fmt} "
@@ -98,7 +103,7 @@ def decode_map_fmt_args(event):
args = {}
fmt = event.data_fmt[len("@pw_py_map_fmt:") :]
- # we assume all data is packed, little-endian ordering if not specified
+ # We assume all data is packed, little-endian ordering if not specified.
if not fmt.startswith(_ORDERING_CHARS):
fmt = '<' + fmt
@@ -115,11 +120,16 @@ def decode_map_fmt_args(event):
args["error"] = f"Invalid map format {event.data_fmt}"
else:
try:
- # needed in case the buffer is larger than expected
+ # Assert is needed in case the buffer is larger than expected.
assert struct.calcsize(fmt_bytes) == len(event.data)
items = struct.unpack_from(fmt_bytes, event.data)
for i, item in enumerate(items):
- args[names[i]] = item
+ # Try to decode the item in case it is a byte array (string)
+ # since the JSON lib cannot serialize byte arrays.
+ try:
+ args[names[i]] = item.decode()
+ except (UnicodeDecodeError, AttributeError):
+ args[names[i]] = item
except (AssertionError, struct.error):
args["error"] = (
f"Mismatched map/data format {event.data_fmt} "
diff --git a/pw_trace/py/setup.py b/pw_trace/py/setup.py
deleted file mode 100644
index 9e3cda1f5..000000000
--- a/pw_trace/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_trace"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_trace/py/trace_test.py b/pw_trace/py/trace_test.py
index 60249623d..181824553 100755
--- a/pw_trace/py/trace_test.py
+++ b/pw_trace/py/trace_test.py
@@ -194,8 +194,8 @@ class TestTraceGenerateJson(unittest.TestCase):
label="counter",
timestamp_us=10,
has_data=True,
- data_fmt="@pw_py_struct_fmt:Hl",
- data=struct.pack("<Hl", 5, 2),
+ data_fmt="@pw_py_struct_fmt:Hl3s",
+ data=struct.pack("<Hl3s", 5, 2, b'abc'),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -207,7 +207,7 @@ class TestTraceGenerateJson(unittest.TestCase):
"name": "counter",
"ts": 10,
"s": "p",
- "args": {"data_0": 5, "data_1": 2},
+ "args": {"data_0": 5, "data_1": 2, "data_2": 'abc'},
},
)
@@ -298,8 +298,8 @@ class TestTraceGenerateJson(unittest.TestCase):
label="label",
timestamp_us=10,
has_data=True,
- data_fmt="@pw_py_map_fmt:{Field: l, Field2: l }",
- data=struct.pack("<ll", 20, 40),
+ data_fmt="@pw_py_map_fmt:{Field: l, Field2: l , Field3: 3s}",
+ data=struct.pack("<ll3s", 20, 40, b'abc'),
)
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
@@ -311,7 +311,7 @@ class TestTraceGenerateJson(unittest.TestCase):
"name": "label",
"ts": 10,
"s": "p",
- "args": {"Field": 20, "Field2": 40},
+ "args": {"Field": 20, "Field2": 40, "Field3": 'abc'},
},
)
diff --git a/pw_trace_tokenized/BUILD.bazel b/pw_trace_tokenized/BUILD.bazel
index 26f1302a8..72c5357dc 100644
--- a/pw_trace_tokenized/BUILD.bazel
+++ b/pw_trace_tokenized/BUILD.bazel
@@ -18,16 +18,35 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "headers",
+ name = "linux_config_overrides",
+ hdrs = ["linux_config_overrides.h"],
+ tags = ["manual"],
+)
+
+pw_cc_library(
+ name = "config",
hdrs = [
"public/pw_trace_tokenized/config.h",
+ ],
+ includes = [
+ "public",
+ ],
+)
+
+pw_cc_library(
+ name = "pw_trace_tokenized",
+ srcs = [
+ "trace.cc",
+ ],
+ hdrs = [
"public/pw_trace_tokenized/internal/trace_tokenized_internal.h",
"public/pw_trace_tokenized/trace_callback.h",
"public/pw_trace_tokenized/trace_tokenized.h",
@@ -38,30 +57,51 @@ pw_cc_library(
"public_overrides",
],
deps = [
+ ":config",
+ "//pw_assert",
+ "//pw_log",
"//pw_preprocessor",
+ "//pw_status",
"//pw_tokenizer",
+ "//pw_trace:facade",
+ "//pw_varint",
],
)
pw_cc_library(
- name = "linux_config_overrides",
- hdrs = ["linux_config_overrides.h"],
- tags = ["manual"],
+ name = "base_trace_service",
+ srcs = [
+ "base_trace_service.cc",
+ ],
+ hdrs = [
+ "public/pw_trace_tokenized/base_trace_service.h",
+ ],
+ includes = [
+ "public",
+ ],
+ deps = [
+ ":buffer",
+ ":pw_trace_tokenized",
+ "//pw_ring_buffer",
+ "//pw_stream",
+ ],
)
pw_cc_library(
- name = "pw_trace_tokenized",
+ name = "trace_service_pwpb",
srcs = [
- "trace.cc",
+ "trace_service_pwpb.cc",
+ ],
+ hdrs = [
+ "public/pw_trace_tokenized/trace_service_pwpb.h",
+ ],
+ includes = [
+ "public",
],
deps = [
- ":headers",
- "//pw_assert",
- "//pw_log",
- "//pw_status",
- "//pw_tokenizer",
- "//pw_trace:facade",
- "//pw_varint",
+ ":base_trace_service",
+ ":protos_cc.pwpb_rpc",
+ "//pw_chrono:system_clock",
],
)
@@ -76,7 +116,7 @@ pw_cc_library(
includes = [
"public",
],
- # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ # TODO: b/260641850 - Get pw_trace_tokenized building in Bazel.
tags = ["manual"],
deps = [
":buffer",
@@ -87,29 +127,20 @@ pw_cc_library(
)
pw_cc_library(
- name = "trace_buffer_headers",
+ name = "buffer",
+ srcs = [
+ "trace_buffer.cc",
+ ],
hdrs = [
"public/pw_trace_tokenized/trace_buffer.h",
],
includes = [
"public",
],
- deps = [
- ":pw_trace_tokenized",
- "//pw_ring_buffer",
- "//pw_status",
- ],
-)
-
-pw_cc_library(
- name = "buffer",
- srcs = [
- "trace_buffer.cc",
- ],
- # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ # TODO: b/260641850 - Get pw_trace_tokenized building in Bazel.
tags = ["manual"],
deps = [
- ":trace_buffer_headers",
+ ":pw_trace_tokenized",
"//pw_ring_buffer",
"//pw_status",
],
@@ -123,10 +154,10 @@ pw_cc_library(
hdrs = [
"public/pw_trace_tokenized/trace_buffer_log.h",
],
- # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ # TODO: b/260641850 - Get pw_trace_tokenized building in Bazel.
tags = ["manual"],
deps = [
- ":trace_buffer_headers",
+ ":buffer",
"//pw_base64",
"//pw_log",
"//pw_string",
@@ -138,10 +169,14 @@ proto_library(
srcs = [
"pw_trace_protos/trace.proto",
"pw_trace_protos/trace_rpc.proto",
+ "pw_trace_protos/trace_service.proto",
+ ],
+ deps = [
+ "//pw_chrono:chrono_proto",
],
# TODO(tpudlik): We should provide trace_rpc.options to nanopb here, but the
# current proto codegen implementation provides no mechanism for doing so.
- # inputs = [ "pw_trace_protos/trace_rpc.options" ]
+ # inputs = [ "pw_trace_protos/trace_rpc.options", "pw_trace_protos/trace_service.options"]
)
pw_proto_library(
@@ -149,6 +184,13 @@ pw_proto_library(
deps = [":protos"],
)
+py_proto_library(
+ name = "proto_py",
+ # TODO(b/241456982): Get this target to build.
+ tags = ["manual"],
+ deps = [":protos"],
+)
+
pw_cc_library(
name = "pw_trace_tokenized_fake_time",
srcs = [
@@ -168,7 +210,7 @@ pw_cc_test(
"pw_trace_test",
"pw_trace_test/public_overrides",
],
- # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ # TODO: b/260641850 - Get pw_trace_tokenized building in Bazel.
tags = ["manual"],
deps = [
":pw_trace_tokenized",
@@ -182,7 +224,7 @@ pw_cc_test(
srcs = [
"trace_buffer_test.cc",
],
- # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ # TODO: b/260641850 - Get pw_trace_tokenized building in Bazel.
tags = ["manual"],
deps = [
":buffer",
@@ -197,7 +239,7 @@ pw_cc_test(
srcs = [
"trace_buffer_log_test.cc",
],
- # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ # TODO: b/260641850 - Get pw_trace_tokenized building in Bazel.
tags = ["manual"],
deps = [
":buffer_log",
@@ -207,6 +249,22 @@ pw_cc_test(
],
)
+pw_cc_test(
+ name = "trace_service_pwpb_test",
+ srcs = [
+ "trace_service_pwpb_test.cc",
+ ],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
+ deps = [
+ ":pw_trace_host_trace_time",
+ ":trace_service_pwpb",
+ "//pw_chrono:system_clock",
+ "//pw_rpc/pwpb:test_method_context",
+ "//pw_trace",
+ ],
+)
+
pw_cc_library(
name = "pw_trace_host_trace_time",
srcs = ["host_trace_time.cc"],
@@ -218,13 +276,16 @@ pw_cc_library(
name = "pw_trace_example_to_file",
hdrs = ["example/public/pw_trace_tokenized/example/trace_to_file.h"],
includes = ["example/public"],
- deps = ["//pw_trace"],
+ deps = [
+ ":pw_trace_tokenized",
+ "//pw_trace",
+ ],
)
pw_cc_binary(
name = "trace_tokenized_example_basic",
srcs = ["example/basic.cc"],
- # TODO(b/258071921): Fix puzzling compiler errors
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
":pw_trace_example_to_file",
@@ -237,7 +298,7 @@ pw_cc_binary(
pw_cc_binary(
name = "trace_tokenized_example_trigger",
srcs = ["example/trigger.cc"],
- # TODO(b/258071921): Fix puzzling compiler errors
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
":pw_trace_example_to_file",
@@ -250,7 +311,7 @@ pw_cc_binary(
pw_cc_binary(
name = "trace_tokenized_example_filter",
srcs = ["example/filter.cc"],
- # TODO(b/258071921): Fix puzzling compiler errors
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
":pw_trace_example_to_file",
@@ -263,7 +324,7 @@ pw_cc_binary(
pw_cc_library(
name = "trace_tokenized_example_rpc",
srcs = ["example/rpc.cc"],
- # TODO(b/258071921): Fix puzzling compiler errors
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
"//pw_hdlc",
@@ -278,6 +339,7 @@ pw_cc_library(
pw_cc_library(
name = "trace_tokenized_example_linux_group_by_tid",
srcs = ["example/linux_group_by_tid.cc"],
+ # TODO: b/258071921 - Fix puzzling compiler errors
tags = ["manual"],
deps = [
":pw_trace_example_to_file",
diff --git a/pw_trace_tokenized/BUILD.gn b/pw_trace_tokenized/BUILD.gn
index 7af4ddfd9..c2c928e91 100644
--- a/pw_trace_tokenized/BUILD.gn
+++ b/pw_trace_tokenized/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_third_party/nanopb/nanopb.gni")
+import("$dir_pw_trace/backend.gni")
import("$dir_pw_unit_test/test.gni")
import("config.gni")
@@ -30,6 +31,13 @@ config("backend_config") {
include_dirs = [ "public_overrides" ]
}
+# Some tests in this file do not compile unless pw_trace_tokenized is the trace
+# backend.
+_pw_trace_tokenized_is_selected =
+ get_label_info(pw_trace_BACKEND, "label_no_toolchain") ==
+ get_label_info(":pw_trace_tokenized", "label_no_toolchain") &&
+ pw_trace_tokenizer_time != ""
+
pw_source_set("config") {
public_deps = [ pw_trace_CONFIG ]
public_configs = [ ":public_include_path" ]
@@ -54,6 +62,7 @@ pw_test_group("tests") {
":trace_tokenized_test",
":tokenized_trace_buffer_test",
":tokenized_trace_buffer_log_test",
+ ":trace_service_pwpb_test",
]
}
@@ -76,7 +85,7 @@ pw_source_set("pw_trace_tokenized") {
}
pw_test("trace_tokenized_test") {
- enable_if = pw_trace_tokenizer_time != ""
+ enable_if = _pw_trace_tokenized_is_selected
deps = [
":core",
"$dir_pw_trace",
@@ -93,8 +102,14 @@ pw_proto_library("protos") {
sources = [
"pw_trace_protos/trace.proto",
"pw_trace_protos/trace_rpc.proto",
+ "pw_trace_protos/trace_service.proto",
+ ]
+ inputs = [
+ "pw_trace_protos/trace_rpc.options",
+ "pw_trace_protos/trace_service.options",
]
- inputs = [ "pw_trace_protos/trace_rpc.options" ]
+ python_package = "py"
+ deps = [ "$dir_pw_chrono:protos" ]
}
pw_source_set("trace_rpc_service") {
@@ -112,6 +127,46 @@ pw_source_set("trace_rpc_service") {
]
}
+pw_source_set("base_trace_service") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":core",
+ ":tokenized_trace_buffer",
+ ]
+ deps = [
+ "$dir_pw_ring_buffer",
+ "$dir_pw_stream",
+ ]
+ sources = [
+ "base_trace_service.cc",
+ "public/pw_trace_tokenized/base_trace_service.h",
+ ]
+}
+
+pw_source_set("trace_service_pwpb") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":base_trace_service",
+ ":protos.pwpb_rpc",
+ ]
+ deps = [ "$dir_pw_chrono:system_clock" ]
+ sources = [
+ "public/pw_trace_tokenized/trace_service_pwpb.h",
+ "trace_service_pwpb.cc",
+ ]
+}
+
+pw_test("trace_service_pwpb_test") {
+ enable_if = _pw_trace_tokenized_is_selected
+ deps = [
+ ":trace_service_pwpb",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_rpc/pwpb:test_method_context",
+ "$dir_pw_trace",
+ ]
+ sources = [ "trace_service_pwpb_test.cc" ]
+}
+
pw_source_set("tokenized_trace_buffer") {
deps = [ ":core" ]
public_deps = [
@@ -131,7 +186,7 @@ pw_source_set("tokenized_trace_buffer") {
}
pw_test("tokenized_trace_buffer_test") {
- enable_if = pw_trace_tokenizer_time != ""
+ enable_if = _pw_trace_tokenized_is_selected
deps = [
":tokenized_trace_buffer",
"$dir_pw_trace",
@@ -152,7 +207,7 @@ pw_source_set("tokenized_trace_buffer_log") {
}
pw_test("tokenized_trace_buffer_log_test") {
- enable_if = pw_trace_tokenizer_time != ""
+ enable_if = _pw_trace_tokenized_is_selected
deps = [
":tokenized_trace_buffer_log",
"$dir_pw_trace",
@@ -206,7 +261,7 @@ config("trace_example_config") {
}
pw_source_set("trace_example_to_file") {
- deps = [ ":pw_trace_tokenized" ]
+ public_deps = [ ":pw_trace_tokenized" ]
public_configs = [ ":trace_example_config" ]
public = [ "example/public/pw_trace_tokenized/example/trace_to_file.h" ]
}
@@ -249,6 +304,7 @@ if (dir_pw_third_party_nanopb == "") {
pw_executable("trace_tokenized_example_rpc") {
sources = [ "example/rpc.cc" ]
deps = [
+ ":pw_trace_tokenized",
":trace_rpc_service",
"$dir_pw_hdlc",
"$dir_pw_log",
diff --git a/pw_trace_tokenized/CMakeLists.txt b/pw_trace_tokenized/CMakeLists.txt
index e8780f6a4..bc6268f90 100644
--- a/pw_trace_tokenized/CMakeLists.txt
+++ b/pw_trace_tokenized/CMakeLists.txt
@@ -69,8 +69,12 @@ pw_add_library(pw_trace_tokenized.trace_buffer STATIC
pw_proto_library(pw_trace_tokenized.protos
SOURCES
pw_trace_protos/trace_rpc.proto
+ pw_trace_protos/trace_service.proto
INPUTS
pw_trace_protos/trace_rpc.options
+ pw_trace_protos/trace_service.options
+ DEPS
+ pw_chrono.protos
)
pw_add_library(pw_trace_tokenized.rpc_service STATIC
@@ -87,3 +91,25 @@ pw_add_library(pw_trace_tokenized.rpc_service STATIC
pw_tokenizer
pw_status
)
+
+pw_add_library(pw_trace_tokenized.base_trace_service STATIC
+ SOURCES
+ base_trace_service.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_stream
+ pw_ring_buffer
+ PUBLIC_DEPS
+ pw_trace_tokenized
+ pw_trace_tokenized.trace_buffer
+)
+
+pw_add_library(pw_trace_tokenized.trace_service_pwpb STATIC
+ SOURCES
+ trace_service_pwpb.cc
+ PRIVATE_DEPS
+ pw_chrono.system_clock
+ PUBLIC_DEPS
+ pw_trace_tokenized.base_trace_service
+ pw_trace_tokenized.protos.pwpb_rpc
+)
diff --git a/pw_trace_tokenized/base_trace_service.cc b/pw_trace_tokenized/base_trace_service.cc
new file mode 100644
index 000000000..56331f377
--- /dev/null
+++ b/pw_trace_tokenized/base_trace_service.cc
@@ -0,0 +1,76 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_trace_tokenized/base_trace_service.h"
+
+#include "pw_log/log.h"
+#include "pw_trace_tokenized/trace_buffer.h"
+
+namespace pw::trace {
+BaseTraceService::BaseTraceService(TokenizedTracer& tokenized_tracer,
+ stream::Writer& trace_writer)
+ : tokenized_tracer_(tokenized_tracer), trace_writer_(trace_writer) {
+ tokenized_tracer_.Enable(false);
+}
+
+Status BaseTraceService::Start() {
+ PW_LOG_INFO("Starting Tracing");
+
+ if (tokenized_tracer_.IsEnabled()) {
+ PW_LOG_INFO("Tracing already started");
+ return Status::FailedPrecondition();
+ }
+
+ tokenized_tracer_.Enable(true);
+
+ return OkStatus();
+}
+
+Status BaseTraceService::Stop() {
+ PW_LOG_INFO("Stopping Tracing");
+
+ if (!tokenized_tracer_.IsEnabled()) {
+ PW_LOG_INFO("Tracing not started");
+ return Status::FailedPrecondition();
+ }
+
+ tokenized_tracer_.Enable(false);
+
+ auto ring_buffer = trace::GetBuffer();
+
+ if (ring_buffer->EntryCount() == 0) {
+ PW_LOG_WARN("EntryCount(%zu)", ring_buffer->EntryCount());
+ return Status::Unavailable();
+ } else if (ring_buffer->CheckForCorruption() != OkStatus()) {
+ PW_LOG_ERROR("EntryCount(%zu), Corruption(%d)",
+ ring_buffer->EntryCount(),
+ ring_buffer->CheckForCorruption().code());
+ return Status::Unavailable();
+ }
+
+ PW_LOG_INFO("EntryCount(%zu)", ring_buffer->EntryCount());
+
+ ConstByteSpan inner_trace_span = trace::DeringAndViewRawBuffer();
+ if (auto status = trace_writer_.Write(inner_trace_span);
+ status != OkStatus()) {
+ PW_LOG_ERROR("Failed to write trace data: %d)", status.code());
+ return status;
+ }
+
+ ring_buffer->Clear();
+
+ return OkStatus();
+}
+
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/docs.rst b/pw_trace_tokenized/docs.rst
index f58dc62c1..22363acdf 100644
--- a/pw_trace_tokenized/docs.rst
+++ b/pw_trace_tokenized/docs.rst
@@ -153,13 +153,10 @@ be done in one of a few ways.
1. Create a file with the default time functions, and provide as build variable
``pw_trace_tokenized_time``, which will get pulled in as a dependency.
2. Provide time functions elsewhere in project, and ensure they are included.
-3. Redefine the trace time macros to something else, other then the default
- trace time functions.
+3. Provide definitions of the following trace time functions.
.. cpp:function:: PW_TRACE_TIME_TYPE pw_trace_GetTraceTime()
-.. cpp:function:: PW_TRACE_GET_TIME()
.. cpp:function:: size_t pw_trace_GetTraceTimeTicksPerSecond()
-.. cpp:function:: PW_TRACE_GET_TIME_TICKS_PER_SECOND()
------
@@ -205,7 +202,7 @@ are surrounded by 'begin' and 'end' tags.
Ex. Invoking PW_TRACE_INSTANT with 'test1' and 'test2', then calling this
function would produce this in the output logs:
-.. code:: sh
+.. code-block:: sh
[TRACE] begin
[TRACE] data: BWdDMRoABWj52YMB
diff --git a/pw_trace_tokenized/example/basic.cc b/pw_trace_tokenized/example/basic.cc
index a3894e331..c0edae146 100644
--- a/pw_trace_tokenized/example/basic.cc
+++ b/pw_trace_tokenized/example/basic.cc
@@ -30,6 +30,7 @@
#include "pw_log/log.h"
#include "pw_trace/example/sample_app.h"
#include "pw_trace_tokenized/example/trace_to_file.h"
+#include "pw_trace_tokenized/trace_callback.h"
int main(int argc, char** argv) { // Take filename as arg
if (argc != 2) {
@@ -41,7 +42,7 @@ int main(int argc, char** argv) { // Take filename as arg
PW_TRACE_SET_ENABLED(true);
// Dump trace data to the file passed in.
- pw::trace::TraceToFile trace_to_file(argv[1]);
+ pw::trace::TraceToFile trace_to_file(pw::trace::GetCallbacks(), argv[1]);
PW_LOG_INFO("Running basic trace example...\n");
RunTraceSampleApp();
diff --git a/pw_trace_tokenized/example/filter.cc b/pw_trace_tokenized/example/filter.cc
index 8461f560a..a16efb478 100644
--- a/pw_trace_tokenized/example/filter.cc
+++ b/pw_trace_tokenized/example/filter.cc
@@ -51,14 +51,14 @@ int main(int argc, char** argv) { // Take filename as arg
}
// Register filter callback
- pw::trace::Callbacks::Instance()
- .RegisterEventCallback(TraceEventCallback)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ pw::trace::Callbacks& callbacks = pw::trace::GetCallbacks();
+ callbacks.RegisterEventCallback(TraceEventCallback)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
PW_TRACE_SET_ENABLED(true); // Start with tracing enabled
// Dump trace data to the file passed in.
- pw::trace::TraceToFile trace_to_file(argv[1]);
+ pw::trace::TraceToFile trace_to_file(callbacks, argv[1]);
PW_LOG_INFO("Running filter example...");
RunTraceSampleApp();
diff --git a/pw_trace_tokenized/example/linux_group_by_tid.cc b/pw_trace_tokenized/example/linux_group_by_tid.cc
index 6c8a204a3..9b7785af6 100644
--- a/pw_trace_tokenized/example/linux_group_by_tid.cc
+++ b/pw_trace_tokenized/example/linux_group_by_tid.cc
@@ -25,6 +25,8 @@
#include "pw_log/log.h"
#include "pw_trace/trace.h"
#include "pw_trace_tokenized/example/trace_to_file.h"
+#include "pw_trace_tokenized/trace_callback.h"
+#include "pw_trace_tokenized/trace_tokenized.h"
// Example for annotating trace events with thread id.
// The platform annotates instants and duration events with the thread id if the
@@ -84,10 +86,11 @@ int main(int argc, char** argv) {
PW_TRACE_SET_ENABLED(true);
// Dump trace data to the file passed in.
- pw::trace::TraceToFile trace_to_file{argv[1]};
+ pw::trace::TraceToFile trace_to_file(pw::trace::GetCallbacks(), argv[1]);
// Register platform callback
- pw::trace::RegisterCallbackWhenCreated{TraceEventCallback};
+ pw::trace::RegisterCallbackWhenCreated(pw::trace::GetCallbacks(),
+ TraceEventCallback);
PW_LOG_INFO("Running threaded trace example...\n");
RunThreadedTraceSampleApp();
diff --git a/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h b/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h
index c392a3853..97a8bb801 100644
--- a/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h
+++ b/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h
@@ -27,21 +27,21 @@ namespace trace {
class TraceToFile {
public:
- TraceToFile(const char* file_name) {
- Callbacks::Instance()
+ TraceToFile(Callbacks& callbacks, const char* file_name)
+ : callbacks_(callbacks) {
+ callbacks_
.RegisterSink(TraceSinkStartBlock,
TraceSinkAddBytes,
TraceSinkEndBlock,
&out_,
&sink_handle_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
out_.open(file_name, std::ios::out | std::ios::binary);
}
~TraceToFile() {
- Callbacks::Instance()
- .UnregisterSink(sink_handle_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ callbacks_.UnregisterSink(sink_handle_)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
out_.close();
}
@@ -64,8 +64,9 @@ class TraceToFile {
}
private:
+ Callbacks& callbacks_;
std::ofstream out_;
- CallbacksImpl::SinkHandle sink_handle_;
+ Callbacks::SinkHandle sink_handle_;
};
} // namespace trace
diff --git a/pw_trace_tokenized/example/rpc.cc b/pw_trace_tokenized/example/rpc.cc
index 5041bc3c2..56e68e812 100644
--- a/pw_trace_tokenized/example/rpc.cc
+++ b/pw_trace_tokenized/example/rpc.cc
@@ -15,25 +15,32 @@
/*
NOTE
-To use this example you need to enable nanopb, one option is to set this in
-either your out/args.gn or the root .gn:
-default_args = {
- dir_pw_third_party_nanopb = "<path to nanopb repo>"
-}
+To use this example you need to enable nanopb. One option is to first install
+the nanopb package by running the command:
+
+pw package install nanopb
+
+Next add nanopb to your args.gn by running the command
+
+gn args out
+
+Then add the following line to that text file:
+
+dir_pw_third_party_nanopb = getenv("PW_PACKAGE_ROOT") + "/nanopb"
BUILD
-ninja -C out
+ninja -C out \
pw_strict_host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
RUN
./out/pw_strict_host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
DECODE
-python pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
- -s localhost:33000
- -o trace.json
- -t
- out/pw_strict_host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc
+python pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py \
+ -s localhost:33000 \
+ -o trace.json \
+ -t \
+ out/pw_strict_host_clang_debug/obj/pw_trace_tokenized/bin/trace_tokenized_example_rpc\
pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
VIEW
@@ -46,12 +53,12 @@ In chrome navigate to chrome://tracing, and load the trace.json file.
#include "pw_rpc/server.h"
#include "pw_rpc_system_server/rpc_server.h"
#include "pw_trace/example/sample_app.h"
-#include "pw_trace/trace.h"
#include "pw_trace_tokenized/trace_rpc_service_nanopb.h"
+#include "pw_trace_tokenized/trace_tokenized.h"
namespace {
-pw::trace::TraceService trace_service;
+pw::trace::TraceService trace_service(pw::trace::GetTokenizedTracer());
void RpcThread() {
pw::rpc::system_server::Init();
diff --git a/pw_trace_tokenized/example/trigger.cc b/pw_trace_tokenized/example/trigger.cc
index cc7c3936f..62d4425b5 100644
--- a/pw_trace_tokenized/example/trigger.cc
+++ b/pw_trace_tokenized/example/trigger.cc
@@ -74,16 +74,17 @@ int main(int argc, char** argv) { // Take filename as arg
}
// Register trigger callback
- pw::trace::Callbacks::Instance()
+ pw::trace::Callbacks& callbacks = pw::trace::GetCallbacks();
+ callbacks
.RegisterEventCallback(TraceEventCallback,
- pw::trace::CallbacksImpl::kCallOnEveryEvent)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ pw::trace::Callbacks::kCallOnEveryEvent)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
// Ensure tracing is off at start, the trigger will turn it on.
PW_TRACE_SET_ENABLED(false);
// Dump trace data to the file passed in.
- pw::trace::TraceToFile trace_to_file(argv[1]);
+ pw::trace::TraceToFile trace_to_file(callbacks, argv[1]);
PW_LOG_INFO("Running trigger example...");
RunTraceSampleApp();
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/base_trace_service.h b/pw_trace_tokenized/public/pw_trace_tokenized/base_trace_service.h
new file mode 100644
index 000000000..d6f219a36
--- /dev/null
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/base_trace_service.h
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstdint>
+#include <optional>
+
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+#include "pw_trace_tokenized/trace_tokenized.h"
+
+namespace pw::trace {
+class BaseTraceService {
+ public:
+ BaseTraceService(TokenizedTracer& tokenized_tracer,
+ stream::Writer& trace_writer);
+
+ void SetTransferId(uint32_t id) { transfer_id_ = id; }
+
+ protected:
+ Status Start();
+ Status Stop();
+
+ private:
+ TokenizedTracer& tokenized_tracer_;
+ stream::Writer& trace_writer_;
+
+ protected:
+ std::optional<uint32_t> transfer_id_;
+};
+
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/config.h b/pw_trace_tokenized/public/pw_trace_tokenized/config.h
index 4167acee4..02ae09c5a 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/config.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/config.h
@@ -35,23 +35,13 @@
#define PW_TRACE_TIME_TYPE uint32_t
#endif // PW_TRACE_TIME_TYPE
-// PW_TRACE_GET_TIME is the macro which is called to get the current time for a
-// trace event. It's default is to use pw_trace_GetTraceTime() which needs to be
-// provided by the platform.
-#ifndef PW_TRACE_GET_TIME
-#define PW_TRACE_GET_TIME() pw_trace_GetTraceTime()
+// pw_trace_GetTraceTime() is the macro which is called to get the current time
+// for a trace event. It must be provided by the platform.
extern PW_TRACE_TIME_TYPE pw_trace_GetTraceTime(void);
-#endif // PW_TRACE_GET_TIME
-
-// PW_TRACE_GET_TIME_TICKS_PER_SECOND is the macro which is called to determine
-// the unit of the trace time. It's default is to use
-// pw_trace_GetTraceTimeTicksPerSecond() which needs to be provided by the
-// platform.
-#ifndef PW_TRACE_GET_TIME_TICKS_PER_SECOND
-#define PW_TRACE_GET_TIME_TICKS_PER_SECOND() \
- pw_trace_GetTraceTimeTicksPerSecond()
+
+// pw_trace_GetTraceTimeTicksPerSecond() is the function which is called to
+// determine the unit of the trace time. It must be provided by the platform.
extern size_t pw_trace_GetTraceTimeTicksPerSecond(void);
-#endif // PW_TRACE_GET_TIME_TICKS_PER_SECOND
// PW_TRACE_GET_TIME_DELTA is te macro which is called to determine
// the delta between two PW_TRACE_TIME_TYPE variables. It should return a
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h
index 48ac84811..1140f75ad 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h
@@ -119,7 +119,9 @@ PW_EXTERN_C_END
namespace pw {
namespace trace {
-class CallbacksImpl {
+// C++ API to the tokenized trace callback system
+// Example: pw::trace::GetTraceCallbacks().UnregisterAllSinks();
+class Callbacks {
public:
enum CallOnEveryEvent {
kCallOnlyWhenEnabled = PW_TRACE_CALL_ONLY_WHEN_ENABLED,
@@ -180,16 +182,8 @@ class CallbacksImpl {
}
};
-// A singleton object of the CallbacksImpl class which can be used to
-// interface with trace using the C++ API.
-// Example: pw::trace::Callbacks::Instance().UnregisterAllSinks();
-class Callbacks {
- public:
- static CallbacksImpl& Instance() { return instance_; }
-
- private:
- static CallbacksImpl instance_;
-};
+// Returns a reference of the tokenized trace callbacks
+Callbacks& GetCallbacks();
// This is a convenience class to register the callback when the object is
// created. For example if the callback should always be registered this can be
@@ -197,22 +191,28 @@ class Callbacks {
class RegisterCallbackWhenCreated {
public:
RegisterCallbackWhenCreated(
- CallbacksImpl::EventCallback event_callback,
- CallbacksImpl::CallOnEveryEvent called_on_every_event =
- CallbacksImpl::kCallOnlyWhenEnabled,
- void* user_data = nullptr) {
- Callbacks::Instance()
+ Callbacks& callbacks,
+ Callbacks::EventCallback event_callback,
+ Callbacks::CallOnEveryEvent called_on_every_event =
+ Callbacks::kCallOnlyWhenEnabled,
+ void* user_data = nullptr)
+ : callbacks_(callbacks) {
+ callbacks_
.RegisterEventCallback(event_callback, called_on_every_event, user_data)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
- RegisterCallbackWhenCreated(CallbacksImpl::SinkStartBlock sink_start,
- CallbacksImpl::SinkAddBytes sink_add_bytes,
- CallbacksImpl::SinkEndBlock sink_end,
- void* user_data = nullptr) {
- Callbacks::Instance()
- .RegisterSink(sink_start, sink_add_bytes, sink_end, user_data)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ RegisterCallbackWhenCreated(Callbacks& callbacks,
+ Callbacks::SinkStartBlock sink_start,
+ Callbacks::SinkAddBytes sink_add_bytes,
+ Callbacks::SinkEndBlock sink_end,
+ void* user_data = nullptr)
+ : callbacks_(callbacks) {
+ callbacks_.RegisterSink(sink_start, sink_add_bytes, sink_end, user_data)
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
+
+ private:
+ Callbacks& callbacks_;
};
} // namespace trace
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h
index 76aab91d7..f5c554b85 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_rpc_service_nanopb.h
@@ -17,9 +17,13 @@
namespace pw::trace {
+class TokenizedTracer;
+
class TraceService final
: public pw_rpc::nanopb::TraceService::Service<TraceService> {
public:
+ TraceService(TokenizedTracer& tokenized_tracer);
+
pw::Status Enable(const pw_trace_TraceEnableMessage& request,
pw_trace_TraceEnableMessage& response);
@@ -28,6 +32,9 @@ class TraceService final
void GetTraceData(const pw_trace_Empty& request,
ServerWriter<pw_trace_TraceDataMessage>& writer);
+
+ private:
+ TokenizedTracer& tokenized_tracer_;
};
} // namespace pw::trace
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_service_pwpb.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_service_pwpb.h
new file mode 100644
index 000000000..c915dfb34
--- /dev/null
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_service_pwpb.h
@@ -0,0 +1,38 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_trace_protos/trace_service.rpc.pwpb.h"
+#include "pw_trace_tokenized/base_trace_service.h"
+
+namespace pw::trace {
+
+class TraceService final
+ : public proto::pw_rpc::pwpb::TraceService::Service<TraceService>,
+ public BaseTraceService {
+ public:
+ TraceService(TokenizedTracer& tokenized_tracer, stream::Writer& trace_writer);
+
+ Status Start(const proto::pwpb::StartRequest::Message& request,
+ proto::pwpb::StartResponse::Message& response);
+
+ Status Stop(const proto::pwpb::StopRequest::Message& request,
+ proto::pwpb::StopResponse::Message& response);
+
+ Status GetClockParameters(
+ const proto::pwpb::ClockParametersRequest::Message& request,
+ proto::pwpb::ClockParametersResponse::Message& response);
+};
+
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
index 65e52b5ff..c80b626bd 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
@@ -116,8 +116,12 @@ class TraceQueue {
} // namespace internal
-class TokenizedTraceImpl {
+// C++ API interfact to the tokenized tracer
+// Example: pw::trace::GetTokenizedTracer().Enable(true);
+class Callbacks;
+class TokenizedTracer {
public:
+ TokenizedTracer(Callbacks& callbacks) : callbacks_(callbacks) {}
void Enable(bool enable) {
if (enable != enabled_ && enable) {
event_queue_.Clear();
@@ -139,21 +143,14 @@ class TokenizedTraceImpl {
PW_TRACE_TIME_TYPE last_trace_time_ = 0;
bool enabled_ = false;
TraceQueue event_queue_;
+ Callbacks& callbacks_;
void HandleNextItemInQueue(
const volatile TraceQueue::QueueEventBlock* event_block);
};
-// A singleton object of the TokenizedTraceImpl class which can be used to
-// interface with trace using the C++ API.
-// Example: pw::trace::TokenizedTrace::Instance().Enable(true);
-class TokenizedTrace {
- public:
- static TokenizedTraceImpl& Instance() { return instance_; }
-
- private:
- static TokenizedTraceImpl instance_;
-};
+// Returns a reference of the global tokenized tracer
+TokenizedTracer& GetTokenizedTracer();
} // namespace trace
} // namespace pw
diff --git a/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto b/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
index 39971e7c3..2b9b3bc28 100644
--- a/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
+++ b/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto
@@ -15,6 +15,10 @@ syntax = "proto3";
package pw.trace;
+// TODO: b/309643763 - This service has been deprecated in favor of the tracing
+// service defined in trace_service.proto
+// This service will be deleted once existing clients have been migrated awaw
+// from it, so please don't use this for any new projects.
service TraceService {
rpc Enable(TraceEnableMessage) returns (TraceEnableMessage) {}
rpc IsEnabled(Empty) returns (TraceEnableMessage) {}
diff --git a/pw_trace_tokenized/pw_trace_protos/trace_service.options b/pw_trace_tokenized/pw_trace_protos/trace_service.options
new file mode 100644
index 000000000..714d9397f
--- /dev/null
+++ b/pw_trace_tokenized/pw_trace_protos/trace_service.options
@@ -0,0 +1,13 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
diff --git a/pw_trace_tokenized/pw_trace_protos/trace_service.proto b/pw_trace_tokenized/pw_trace_protos/trace_service.proto
new file mode 100644
index 000000000..62be6fe9b
--- /dev/null
+++ b/pw_trace_tokenized/pw_trace_protos/trace_service.proto
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+syntax = "proto3";
+
+package pw.trace.proto;
+
+import "pw_chrono_protos/chrono.proto";
+
+// TODO: b/309643763 - This is the prefered service for tracing, and the service
+// defined in trace_rpc.proto has been deprecated and will be removed once
+// existings clients migrate to this service.
+service TraceService {
+ // Start will enable tracing, populating the trace ring buffer.
+ rpc Start(StartRequest) returns (StartResponse) {}
+
+ // On stop the ring buffer will be written to the configured
+ // stream. No data is written to the stream until Stop is called.
+ rpc Stop(StopRequest) returns (StopResponse) {}
+
+ // Returns the clock paramaters of the system.
+ rpc GetClockParameters(ClockParametersRequest)
+ returns (ClockParametersResponse) {}
+}
+
+message StartRequest {}
+
+message StartResponse {}
+
+message StopRequest {}
+
+message StopResponse {
+ // as a convenience, the file id is returned on stop which can be
+ // used to start a transfer directly, rather that requiring a user
+ // list the files to obtain the file id.
+ optional uint32 file_id = 1;
+}
+
+message ClockParametersRequest {}
+
+message ClockParametersResponse {
+ pw.chrono.ClockParameters clock_parameters = 1;
+}
diff --git a/pw_trace_tokenized/py/BUILD.gn b/pw_trace_tokenized/py/BUILD.gn
index 49ba7e0c5..4ad26412b 100644
--- a/pw_trace_tokenized/py/BUILD.gn
+++ b/pw_trace_tokenized/py/BUILD.gn
@@ -17,11 +17,12 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
pw_python_package("py") {
- setup = [
- "pyproject.toml",
- "setup.cfg",
- "setup.py",
- ]
+ generate_setup = {
+ metadata = {
+ name = "pw_trace_tokenized"
+ version = "0.0.1"
+ }
+ }
sources = [
"pw_trace_tokenized/__init__.py",
"pw_trace_tokenized/get_trace.py",
@@ -35,4 +36,5 @@ pw_python_package("py") {
]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
+ proto_library = "..:protos"
}
diff --git a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
index 041c6bcdf..f9990d864 100755
--- a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
+++ b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
@@ -36,7 +36,12 @@ from typing import Collection, Iterable, Iterator
import serial
from pw_tokenizer import database
from pw_trace import trace
-from pw_hdlc.rpc import HdlcRpcClient, default_channels
+from pw_hdlc.rpc import (
+ HdlcRpcClient,
+ default_channels,
+ SerialReader,
+ SocketReader,
+)
from pw_trace_tokenized import trace_tokenized
_LOG = logging.getLogger('pw_trace_tokenizer')
@@ -105,18 +110,18 @@ def get_hdlc_rpc_client(
# use it so it isn't specific to HDLC
if socket_addr is None:
serial_device = serial.Serial(device, baudrate, timeout=1)
- read = lambda: serial_device.read(8192)
- write = serial_device.write
+ reader = SerialReader(serial_device)
+ write_function = serial_device.write
else:
try:
socket_device = SocketClientImpl(socket_addr)
- read = socket_device.read
- write = socket_device.write
+ reader = SocketReader(socket_device.socket, PW_RPC_MAX_PACKET_SIZE)
+ write_function = socket_device.write
except ValueError:
_LOG.exception('Failed to initialize socket at %s', socket_addr)
return 1
- return HdlcRpcClient(read, protos, default_channels(write))
+ return HdlcRpcClient(reader, protos, default_channels(write_function))
def get_trace_data_from_device(client):
diff --git a/pw_trace_tokenized/py/setup.py b/pw_trace_tokenized/py/setup.py
deleted file mode 100644
index 7deec831c..000000000
--- a/pw_trace_tokenized/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_trace_tokenized"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_trace_tokenized/trace.cc b/pw_trace_tokenized/trace.cc
index 5547c6298..0894def70 100644
--- a/pw_trace_tokenized/trace.cc
+++ b/pw_trace_tokenized/trace.cc
@@ -24,21 +24,24 @@
namespace pw {
namespace trace {
-TokenizedTraceImpl TokenizedTrace::instance_;
-CallbacksImpl Callbacks::instance_;
+Callbacks callbacks;
+Callbacks& GetCallbacks() { return callbacks; }
+
+TokenizedTracer tokenized_tracer(GetCallbacks());
+TokenizedTracer& GetTokenizedTracer() { return tokenized_tracer; }
using TraceEvent = pw_trace_tokenized_TraceEvent;
-void TokenizedTraceImpl::HandleTraceEvent(uint32_t trace_token,
- EventType event_type,
- const char* module,
- uint32_t trace_id,
- uint8_t flags,
- const void* data_buffer,
- size_t data_size) {
+void TokenizedTracer::HandleTraceEvent(uint32_t trace_token,
+ EventType event_type,
+ const char* module,
+ uint32_t trace_id,
+ uint8_t flags,
+ const void* data_buffer,
+ size_t data_size) {
// Early exit if disabled and no callbacks are register to receive events
// while disabled.
- if (!enabled_ && Callbacks::Instance().GetCalledOnEveryEventCount() == 0) {
+ if (!enabled_ && callbacks_.GetCalledOnEveryEventCount() == 0) {
return;
}
@@ -54,16 +57,16 @@ void TokenizedTraceImpl::HandleTraceEvent(uint32_t trace_token,
// Call any event callback which is registered to receive every event.
pw_trace_TraceEventReturnFlags ret_flags = 0;
- ret_flags |= Callbacks::Instance().CallEventCallbacks(
- CallbacksImpl::kCallOnEveryEvent, &event);
+ ret_flags |=
+ callbacks_.CallEventCallbacks(Callbacks::kCallOnEveryEvent, &event);
// Return if disabled.
if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
return;
}
// Call any event callback not already called.
- ret_flags |= Callbacks::Instance().CallEventCallbacks(
- CallbacksImpl::kCallOnlyWhenEnabled, &event);
+ ret_flags |=
+ callbacks_.CallEventCallbacks(Callbacks::kCallOnlyWhenEnabled, &event);
// Return if disabled (from a callback) or if a callback has indicated the
// sample should be skipped.
if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
@@ -103,7 +106,7 @@ void TokenizedTraceImpl::HandleTraceEvent(uint32_t trace_token,
}
}
-void TokenizedTraceImpl::HandleNextItemInQueue(
+void TokenizedTracer::HandleNextItemInQueue(
const volatile TraceQueue::QueueEventBlock* event_block) {
// Get next item in queue
uint32_t trace_token = event_block->trace_token;
@@ -140,27 +143,27 @@ void TokenizedTraceImpl::HandleNextItemInQueue(
}
// Send encoded output to any registered trace sinks.
- Callbacks::Instance().CallSinks(
+ callbacks_.CallSinks(
span<const std::byte>(header, header_size),
span<const std::byte>(reinterpret_cast<const std::byte*>(data_buffer),
data_size));
}
-pw_trace_TraceEventReturnFlags CallbacksImpl::CallEventCallbacks(
+pw_trace_TraceEventReturnFlags Callbacks::CallEventCallbacks(
CallOnEveryEvent called_on_every_event, TraceEvent* event) {
pw_trace_TraceEventReturnFlags ret_flags = 0;
for (size_t i = 0; i < PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS; i++) {
if (event_callbacks_[i].callback &&
event_callbacks_[i].called_on_every_event == called_on_every_event) {
- ret_flags |= Callbacks::Instance().GetEventCallback(i)->callback(
- event_callbacks_[i].user_data, event);
+ ret_flags |=
+ GetEventCallback(i)->callback(event_callbacks_[i].user_data, event);
}
}
return ret_flags;
}
-void CallbacksImpl::CallSinks(span<const std::byte> header,
- span<const std::byte> data) {
+void Callbacks::CallSinks(span<const std::byte> header,
+ span<const std::byte> data) {
for (size_t sink_idx = 0; sink_idx < PW_TRACE_CONFIG_MAX_SINKS; sink_idx++) {
void* user_data = sink_callbacks_[sink_idx].user_data;
if (sink_callbacks_[sink_idx].start_block) {
@@ -181,11 +184,11 @@ void CallbacksImpl::CallSinks(span<const std::byte> header,
}
}
-pw::Status CallbacksImpl::RegisterSink(SinkStartBlock start_func,
- SinkAddBytes add_bytes_func,
- SinkEndBlock end_block_func,
- void* user_data,
- SinkHandle* handle) {
+pw::Status Callbacks::RegisterSink(SinkStartBlock start_func,
+ SinkAddBytes add_bytes_func,
+ SinkEndBlock end_block_func,
+ void* user_data,
+ SinkHandle* handle) {
pw_Status status = PW_STATUS_RESOURCE_EXHAUSTED;
PW_TRACE_LOCK();
for (size_t sink_idx = 0; sink_idx < PW_TRACE_CONFIG_MAX_SINKS; sink_idx++) {
@@ -205,7 +208,7 @@ pw::Status CallbacksImpl::RegisterSink(SinkStartBlock start_func,
return status;
}
-pw::Status CallbacksImpl::UnregisterSink(SinkHandle handle) {
+pw::Status Callbacks::UnregisterSink(SinkHandle handle) {
PW_TRACE_LOCK();
if (handle >= PW_TRACE_CONFIG_MAX_SINKS) {
return PW_STATUS_INVALID_ARGUMENT;
@@ -217,22 +220,22 @@ pw::Status CallbacksImpl::UnregisterSink(SinkHandle handle) {
return PW_STATUS_OK;
}
-pw::Status CallbacksImpl::UnregisterAllSinks() {
+pw::Status Callbacks::UnregisterAllSinks() {
for (size_t sink_idx = 0; sink_idx < PW_TRACE_CONFIG_MAX_SINKS; sink_idx++) {
UnregisterSink(sink_idx)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
return PW_STATUS_OK;
}
-CallbacksImpl::SinkCallbacks* CallbacksImpl::GetSink(SinkHandle handle) {
+Callbacks::SinkCallbacks* Callbacks::GetSink(SinkHandle handle) {
if (handle >= PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS) {
return nullptr;
}
return &sink_callbacks_[handle];
}
-pw::Status CallbacksImpl::RegisterEventCallback(
+pw::Status Callbacks::RegisterEventCallback(
EventCallback callback,
CallOnEveryEvent called_on_every_event,
void* user_data,
@@ -256,7 +259,7 @@ pw::Status CallbacksImpl::RegisterEventCallback(
return status;
}
-pw::Status CallbacksImpl::UnregisterEventCallback(EventCallbackHandle handle) {
+pw::Status Callbacks::UnregisterEventCallback(EventCallbackHandle handle) {
PW_TRACE_LOCK();
if (handle >= PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS) {
return PW_STATUS_INVALID_ARGUMENT;
@@ -270,15 +273,15 @@ pw::Status CallbacksImpl::UnregisterEventCallback(EventCallbackHandle handle) {
return PW_STATUS_OK;
}
-pw::Status CallbacksImpl::UnregisterAllEventCallbacks() {
+pw::Status Callbacks::UnregisterAllEventCallbacks() {
for (size_t i = 0; i < PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS; i++) {
UnregisterEventCallback(i)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
return PW_STATUS_OK;
}
-CallbacksImpl::EventCallbacks* CallbacksImpl::GetEventCallback(
+Callbacks::EventCallbacks* Callbacks::GetEventCallback(
EventCallbackHandle handle) {
if (handle >= PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS) {
return nullptr;
@@ -290,9 +293,9 @@ CallbacksImpl::EventCallbacks* CallbacksImpl::GetEventCallback(
PW_EXTERN_C_START
-void pw_trace_Enable(bool enable) { TokenizedTrace::Instance().Enable(enable); }
+void pw_trace_Enable(bool enable) { GetTokenizedTracer().Enable(enable); }
-bool pw_trace_IsEnabled() { return TokenizedTrace::Instance().IsEnabled(); }
+bool pw_trace_IsEnabled() { return GetTokenizedTracer().IsEnabled(); }
void pw_trace_TraceEvent(uint32_t trace_token,
pw_trace_EventType event_type,
@@ -301,7 +304,7 @@ void pw_trace_TraceEvent(uint32_t trace_token,
uint8_t flags,
const void* data_buffer,
size_t data_size) {
- TokenizedTrace::Instance().HandleTraceEvent(
+ GetTokenizedTracer().HandleTraceEvent(
trace_token, event_type, module, trace_id, flags, data_buffer, data_size);
}
@@ -310,14 +313,14 @@ pw_Status pw_trace_RegisterSink(pw_trace_SinkStartBlock start_func,
pw_trace_SinkEndBlock end_block_func,
void* user_data,
pw_trace_SinkHandle* handle) {
- return Callbacks::Instance()
+ return GetCallbacks()
.RegisterSink(
start_func, add_bytes_func, end_block_func, user_data, handle)
.code();
}
pw_Status pw_trace_UnregisterSink(pw_trace_EventCallbackHandle handle) {
- return Callbacks::Instance().UnregisterSink(handle).code();
+ return GetCallbacks().UnregisterSink(handle).code();
}
pw_Status pw_trace_RegisterEventCallback(
@@ -325,10 +328,10 @@ pw_Status pw_trace_RegisterEventCallback(
pw_trace_ShouldCallOnEveryEvent called_on_every_event,
void* user_data,
pw_trace_EventCallbackHandle* handle) {
- return Callbacks::Instance()
+ return GetCallbacks()
.RegisterEventCallback(
callback,
- static_cast<CallbacksImpl::CallOnEveryEvent>(called_on_every_event),
+ static_cast<Callbacks::CallOnEveryEvent>(called_on_every_event),
user_data,
handle)
.code();
@@ -336,7 +339,7 @@ pw_Status pw_trace_RegisterEventCallback(
pw_Status pw_trace_UnregisterEventCallback(
pw_trace_EventCallbackHandle handle) {
- return Callbacks::Instance().UnregisterEventCallback(handle).code();
+ return GetCallbacks().UnregisterEventCallback(handle).code();
}
PW_EXTERN_C_END
diff --git a/pw_trace_tokenized/trace_buffer.cc b/pw_trace_tokenized/trace_buffer.cc
index b1de8e717..f554f98c6 100644
--- a/pw_trace_tokenized/trace_buffer.cc
+++ b/pw_trace_tokenized/trace_buffer.cc
@@ -25,13 +25,13 @@ namespace {
class TraceBuffer {
public:
- TraceBuffer() {
+ TraceBuffer(Callbacks& callbacks) : callbacks_(callbacks) {
ring_buffer_.SetBuffer(raw_buffer_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
- Callbacks::Instance()
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
+ callbacks_
.RegisterSink(
TraceSinkStartBlock, TraceSinkAddBytes, TraceSinkEndBlock, this)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
static void TraceSinkStartBlock(void* user_data, size_t size) {
@@ -64,7 +64,7 @@ class TraceBuffer {
buffer->ring_buffer_
.PushBack(span<const std::byte>(&buffer->current_block_[0],
buffer->block_size_))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
pw::ring_buffer::PrefixedEntryRingBuffer& RingBuffer() {
@@ -73,11 +73,12 @@ class TraceBuffer {
ConstByteSpan DeringAndViewRawBuffer() {
ring_buffer_.Dering()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return ByteSpan(raw_buffer_, ring_buffer_.TotalUsedBytes());
}
private:
+ Callbacks& callbacks_;
uint16_t block_size_ = 0;
uint16_t block_idx_ = 0;
std::byte current_block_[PW_TRACE_BUFFER_MAX_BLOCK_SIZE_BYTES];
@@ -86,7 +87,7 @@ class TraceBuffer {
};
#if PW_TRACE_BUFFER_SIZE_BYTES > 0
-TraceBuffer trace_buffer_instance;
+TraceBuffer trace_buffer_instance(GetCallbacks());
#endif // PW_TRACE_BUFFER_SIZE_BYTES > 0
} // namespace
diff --git a/pw_trace_tokenized/trace_buffer_log.cc b/pw_trace_tokenized/trace_buffer_log.cc
index 8a58cc0dc..30e78ac0f 100644
--- a/pw_trace_tokenized/trace_buffer_log.cc
+++ b/pw_trace_tokenized/trace_buffer_log.cc
@@ -55,7 +55,7 @@ pw::Status DumpTraceBufferToLog() {
while (trace_buffer->PeekFront(span(entry_buffer).subspan(1), &bytes_read) !=
pw::Status::OutOfRange()) {
trace_buffer->PopFront()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
entry_buffer[0] = static_cast<std::byte>(bytes_read);
// The entry buffer is formatted as (size, entry) with an extra byte as
// a header to the entry. The calcuation of bytes_read + 1 represents
diff --git a/pw_trace_tokenized/trace_buffer_log_test.cc b/pw_trace_tokenized/trace_buffer_log_test.cc
index 4f89fcf71..85827907f 100644
--- a/pw_trace_tokenized/trace_buffer_log_test.cc
+++ b/pw_trace_tokenized/trace_buffer_log_test.cc
@@ -23,7 +23,7 @@ namespace pw::trace {
namespace {
TEST(TokenizedTrace, DumpSmallBuffer) {
- // TODO(b/235283406): This test only verifies that the dump function does not
+ // TODO: b/235283406 - This test only verifies that the dump function does not
// crash, and requires manual inspection to confirm that the log output is
// correct. When there is support to mock and verify the calls to pw_log,
// these tests should be improved to validate the output.
@@ -34,7 +34,7 @@ TEST(TokenizedTrace, DumpSmallBuffer) {
}
TEST(TokenizedTrace, DumpLargeBuffer) {
- // TODO(b/235283406): This test only verifies that the dump function does not
+ // TODO: b/235283406 - This test only verifies that the dump function does not
// crash, and requires manual inspection to confirm that the log output is
// correct. When there is support to mock and verify the calls to pw_log,
// these tests should be improved to validate the output.
diff --git a/pw_trace_tokenized/trace_rpc_service_nanopb.cc b/pw_trace_tokenized/trace_rpc_service_nanopb.cc
index 38e8d8205..6452163f6 100644
--- a/pw_trace_tokenized/trace_rpc_service_nanopb.cc
+++ b/pw_trace_tokenized/trace_rpc_service_nanopb.cc
@@ -16,22 +16,24 @@
#include "pw_trace_tokenized/trace_rpc_service_nanopb.h"
#include "pw_log/log.h"
-#include "pw_preprocessor/util.h"
#include "pw_trace_tokenized/trace_buffer.h"
#include "pw_trace_tokenized/trace_tokenized.h"
namespace pw::trace {
+TraceService::TraceService(TokenizedTracer& tokenized_tracer)
+ : tokenized_tracer_(tokenized_tracer) {}
+
pw::Status TraceService::Enable(const pw_trace_TraceEnableMessage& request,
pw_trace_TraceEnableMessage& response) {
- TokenizedTrace::Instance().Enable(request.enable);
- response.enable = TokenizedTrace::Instance().IsEnabled();
+ tokenized_tracer_.Enable(request.enable);
+ response.enable = tokenized_tracer_.IsEnabled();
return PW_STATUS_OK;
}
pw::Status TraceService::IsEnabled(const pw_trace_Empty&,
pw_trace_TraceEnableMessage& response) {
- response.enable = TokenizedTrace::Instance().IsEnabled();
+ response.enable = tokenized_tracer_.IsEnabled();
return PW_STATUS_OK;
}
@@ -45,7 +47,7 @@ void TraceService::GetTraceData(
while (trace_buffer->PeekFront(as_writable_bytes(span(buffer.data.bytes)),
&size) != pw::Status::OutOfRange()) {
trace_buffer->PopFront()
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
buffer.data.size = size;
pw::Status status = writer.Write(buffer);
if (!status.ok()) {
@@ -54,6 +56,6 @@ void TraceService::GetTraceData(
break;
}
}
- writer.Finish().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ writer.Finish().IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
} // namespace pw::trace
diff --git a/pw_trace_tokenized/trace_service_pwpb.cc b/pw_trace_tokenized/trace_service_pwpb.cc
new file mode 100644
index 000000000..af4b90a7c
--- /dev/null
+++ b/pw_trace_tokenized/trace_service_pwpb.cc
@@ -0,0 +1,75 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_trace_tokenized/trace_service_pwpb.h"
+
+#include "pw_chrono/system_clock.h"
+
+namespace pw::trace {
+
+TraceService::TraceService(TokenizedTracer& tokenized_tracer,
+ stream::Writer& trace_writer)
+ : BaseTraceService(tokenized_tracer, trace_writer) {}
+
+Status TraceService::Start(
+ const proto::pwpb::StartRequest::Message& /*request*/,
+ proto::pwpb::StartResponse::Message& /*response*/) {
+ return BaseTraceService::Start();
+}
+
+Status TraceService::Stop(const proto::pwpb::StopRequest::Message& /*request*/,
+ proto::pwpb::StopResponse::Message& response) {
+ if (auto status = BaseTraceService::Stop(); status != pw::OkStatus()) {
+ return status;
+ }
+
+ response.file_id = transfer_id_;
+ return pw::OkStatus();
+}
+
+Status TraceService::GetClockParameters(
+ const proto::pwpb::ClockParametersRequest::Message& /*request*/,
+ proto::pwpb::ClockParametersResponse::Message& response) {
+ response.clock_parameters.tick_period_seconds_numerator =
+ PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMERATOR;
+ response.clock_parameters.tick_period_seconds_denominator =
+ PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR;
+
+ switch (chrono::SystemClock::epoch) {
+ case chrono::Epoch::kUnknown:
+ response.clock_parameters.epoch_type =
+ chrono::pwpb::EpochType::Enum::kUnknown;
+ break;
+ case chrono::Epoch::kTimeSinceBoot:
+ response.clock_parameters.epoch_type =
+ chrono::pwpb::EpochType::Enum::kTimeSinceBoot;
+ break;
+ case chrono::Epoch::kUtcWallClock:
+ response.clock_parameters.epoch_type =
+ chrono::pwpb::EpochType::Enum::kUtcWallClock;
+ break;
+ case chrono::Epoch::kGpsWallClock:
+ response.clock_parameters.epoch_type =
+ chrono::pwpb::EpochType::Enum::kGpsWallClock;
+ break;
+ case chrono::Epoch::kTaiWallClock:
+ response.clock_parameters.epoch_type =
+ chrono::pwpb::EpochType::Enum::kTaiWallClock;
+ break;
+ }
+
+ return pw::OkStatus();
+}
+
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/trace_service_pwpb_test.cc b/pw_trace_tokenized/trace_service_pwpb_test.cc
new file mode 100644
index 000000000..008210193
--- /dev/null
+++ b/pw_trace_tokenized/trace_service_pwpb_test.cc
@@ -0,0 +1,130 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_trace_tokenized/trace_service_pwpb.h"
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_stream/memory_stream.h"
+#include "pw_trace/trace.h"
+#include "pw_trace_tokenized/trace_tokenized.h"
+
+namespace pw::trace {
+
+class TraceServiceTest : public ::testing::Test {
+ public:
+ TraceServiceTest() {}
+
+ static constexpr uint32_t kTraceTransferHandlerId = 7;
+};
+
+TEST_F(TraceServiceTest, Start) {
+ auto& tracer = trace::GetTokenizedTracer();
+
+ std::array<std::byte, PW_TRACE_BUFFER_SIZE_BYTES> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ PW_PWPB_TEST_METHOD_CONTEXT(TraceService, Start)
+ context(tracer, writer);
+
+ ASSERT_FALSE(tracer.IsEnabled());
+ ASSERT_EQ(context.call({}), OkStatus());
+ ASSERT_TRUE(tracer.IsEnabled());
+
+ // multiple calls to start are disallowed
+ ASSERT_EQ(context.call({}), Status::FailedPrecondition());
+}
+
+TEST_F(TraceServiceTest, Stop) {
+ auto& tracer = trace::GetTokenizedTracer();
+
+ std::array<std::byte, PW_TRACE_BUFFER_SIZE_BYTES> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ PW_PWPB_TEST_METHOD_CONTEXT(TraceService, Stop)
+ context(tracer, writer);
+ context.service().SetTransferId(kTraceTransferHandlerId);
+
+ tracer.Enable(true);
+ PW_TRACE_INSTANT("TestTrace");
+
+ ASSERT_EQ(context.call({}), OkStatus());
+ ASSERT_FALSE(tracer.IsEnabled());
+ EXPECT_EQ(kTraceTransferHandlerId, context.response().file_id);
+ EXPECT_LT(0u, writer.bytes_written());
+}
+
+TEST_F(TraceServiceTest, StopNoTransferHandlerId) {
+ auto& tracer = trace::GetTokenizedTracer();
+
+ std::array<std::byte, PW_TRACE_BUFFER_SIZE_BYTES> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ PW_PWPB_TEST_METHOD_CONTEXT(TraceService, Stop)
+ context(tracer, writer);
+
+ tracer.Enable(true);
+ PW_TRACE_INSTANT("TestTrace");
+
+ ASSERT_EQ(context.call({}), OkStatus());
+ ASSERT_FALSE(tracer.IsEnabled());
+ EXPECT_FALSE(context.response().file_id.has_value());
+ EXPECT_LT(0u, writer.bytes_written());
+}
+
+TEST_F(TraceServiceTest, StopNotStarted) {
+ auto& tracer = trace::GetTokenizedTracer();
+
+ std::array<std::byte, PW_TRACE_BUFFER_SIZE_BYTES> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ PW_PWPB_TEST_METHOD_CONTEXT(TraceService, Stop)
+ context(tracer, writer);
+
+ // stopping while tracing is disabled results in FailedPrecondition
+ ASSERT_EQ(context.call({}), Status::FailedPrecondition());
+}
+
+TEST_F(TraceServiceTest, StopNoData) {
+ auto& tracer = trace::GetTokenizedTracer();
+
+ std::array<std::byte, PW_TRACE_BUFFER_SIZE_BYTES> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ PW_PWPB_TEST_METHOD_CONTEXT(TraceService, Stop)
+ context(tracer, writer);
+
+ tracer.Enable(true);
+
+ // stopping with no trace data results in Unavailable
+ ASSERT_EQ(context.call({}), Status::Unavailable());
+}
+
+TEST_F(TraceServiceTest, GetClockParameters) {
+ auto& tracer = trace::GetTokenizedTracer();
+
+ std::array<std::byte, PW_TRACE_BUFFER_SIZE_BYTES> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+
+ PW_PWPB_TEST_METHOD_CONTEXT(TraceService, GetClockParameters)
+ context(tracer, writer);
+
+ ASSERT_EQ(context.call({}), OkStatus());
+ EXPECT_EQ(PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMERATOR,
+ context.response().clock_parameters.tick_period_seconds_numerator);
+ EXPECT_EQ(
+ PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR,
+ context.response().clock_parameters.tick_period_seconds_denominator);
+ EXPECT_EQ(
+ static_cast<int32_t>(chrono::SystemClock::epoch),
+ static_cast<int32_t>(*context.response().clock_parameters.epoch_type));
+}
+
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/trace_test.cc b/pw_trace_tokenized/trace_test.cc
index 72457ab96..8a74e2ca0 100644
--- a/pw_trace_tokenized/trace_test.cc
+++ b/pw_trace_tokenized/trace_test.cc
@@ -54,27 +54,25 @@ class TraceTestInterface {
}
};
- TraceTestInterface() {
+ TraceTestInterface() : callbacks_(pw::trace::GetCallbacks()) {
PW_TRACE_SET_ENABLED(true);
EXPECT_EQ(pw::OkStatus(),
- pw::trace::Callbacks::Instance().RegisterSink(TraceSinkStartBlock,
- TraceSinkAddBytes,
- TraceSinkEndBlock,
- this,
- &sink_handle_));
+ callbacks_.RegisterSink(TraceSinkStartBlock,
+ TraceSinkAddBytes,
+ TraceSinkEndBlock,
+ this,
+ &sink_handle_));
EXPECT_EQ(pw::OkStatus(),
- pw::trace::Callbacks::Instance().RegisterEventCallback(
+ callbacks_.RegisterEventCallback(
TraceEventCallback,
- pw::trace::CallbacksImpl::kCallOnlyWhenEnabled,
+ pw::trace::Callbacks::kCallOnlyWhenEnabled,
this,
&event_callback_handle_));
}
~TraceTestInterface() {
+ EXPECT_EQ(pw::OkStatus(), callbacks_.UnregisterSink(sink_handle_));
EXPECT_EQ(pw::OkStatus(),
- pw::trace::Callbacks::Instance().UnregisterSink(sink_handle_));
- EXPECT_EQ(pw::OkStatus(),
- pw::trace::Callbacks::Instance().UnregisterEventCallback(
- event_callback_handle_));
+ callbacks_.UnregisterEventCallback(event_callback_handle_));
}
// ActionOnEvent will perform a specific action within the callback when an
// event matches one of the characteristics of event_match_.
@@ -162,8 +160,9 @@ class TraceTestInterface {
size_t sink_block_size_;
size_t sink_bytes_received_;
std::deque<TraceInfo> buffer_;
- pw::trace::CallbacksImpl::SinkHandle sink_handle_;
- pw::trace::CallbacksImpl::EventCallbackHandle event_callback_handle_;
+ pw::trace::Callbacks& callbacks_;
+ pw::trace::Callbacks::SinkHandle sink_handle_;
+ pw::trace::Callbacks::EventCallbackHandle event_callback_handle_;
};
} // namespace
diff --git a/pw_transfer/BUILD.bazel b/pw_transfer/BUILD.bazel
index 577d658c8..1a8bc16a2 100644
--- a/pw_transfer/BUILD.bazel
+++ b/pw_transfer/BUILD.bazel
@@ -12,10 +12,10 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("//pw_build:pigweed.bzl", "pw_cc_binary", "pw_cc_library", "pw_cc_test")
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
+load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -183,6 +183,10 @@ pw_cc_test(
pw_cc_test(
name = "transfer_thread_test",
srcs = ["transfer_thread_test.cc"],
+ target_compatible_with = select({
+ "//pw_unit_test:light_setting": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+ }),
deps = [
":pw_transfer",
":test_helpers",
@@ -223,21 +227,6 @@ pw_cc_test(
],
)
-pw_cc_binary(
- name = "test_rpc_server",
- srcs = ["test_rpc_server.cc"],
- deps = [
- ":atomic_file_transfer_handler",
- ":pw_transfer",
- ":test_server_pwpb.raw_rpc",
- "//pw_log",
- "//pw_rpc/system_server",
- "//pw_stream:std_file_stream",
- "//pw_thread:thread",
- "//pw_work_queue",
- ],
-)
-
proto_library(
name = "transfer_proto",
srcs = [
@@ -259,20 +248,3 @@ java_lite_proto_library(
name = "transfer_proto_java_lite",
deps = [":transfer_proto"],
)
-
-proto_library(
- name = "test_server",
- srcs = [
- "test_server.proto",
- ],
- import_prefix = "pw_transfer_test",
- strip_import_prefix = "/pw_transfer",
- deps = [
- "//pw_protobuf:common_proto",
- ],
-)
-
-pw_proto_library(
- name = "test_server_pwpb",
- deps = [":test_server"],
-)
diff --git a/pw_transfer/BUILD.gn b/pw_transfer/BUILD.gn
index 9b1eb7fff..e4e15d8af 100644
--- a/pw_transfer/BUILD.gn
+++ b/pw_transfer/BUILD.gn
@@ -159,39 +159,37 @@ pw_proto_library("proto") {
}
pw_test_group("tests") {
- tests = []
-
- # pw_transfer requires threading.
- if (pw_thread_THREAD_BACKEND != "") {
- tests += [
- ":chunk_test",
- ":client_test",
- ":transfer_thread_test",
- ]
-
- # TODO(b/235345886): Fix transfer tests on Windows and non-host builds.
- if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
- pw_toolchain_SCOPE.is_host_toolchain && host_os != "win") {
- tests += [
- ":handler_test",
- ":atomic_file_transfer_handler_test",
- ":transfer_test",
- ]
- }
- }
+ tests = [
+ ":chunk_test",
+ ":client_test",
+ ":transfer_thread_test",
+ ":handler_test",
+ ":atomic_file_transfer_handler_test",
+ ":transfer_test",
+ ]
}
+# TODO: b/235345886 - Fix transfer tests on Windows and non-host builds.
+_is_host_toolchain = defined(pw_toolchain_SCOPE.is_host_toolchain) &&
+ pw_toolchain_SCOPE.is_host_toolchain
+not_needed([ "_is_host_toolchain" ])
+
pw_test("chunk_test") {
+ enable_if = pw_thread_THREAD_BACKEND != ""
sources = [ "chunk_test.cc" ]
deps = [ ":core" ]
}
pw_test("handler_test") {
+ enable_if =
+ pw_thread_THREAD_BACKEND != "" && _is_host_toolchain && host_os != "win"
sources = [ "handler_test.cc" ]
deps = [ ":pw_transfer" ]
}
pw_test("atomic_file_transfer_handler_test") {
+ enable_if =
+ pw_thread_THREAD_BACKEND != "" && _is_host_toolchain && host_os != "win"
sources = [ "atomic_file_transfer_handler_test.cc" ]
deps = [
":atomic_file_transfer_handler",
@@ -203,7 +201,8 @@ pw_test("atomic_file_transfer_handler_test") {
}
pw_test("transfer_test") {
- enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ _is_host_toolchain && host_os != "win"
sources = [ "transfer_test.cc" ]
deps = [
":proto.pwpb",
@@ -218,7 +217,8 @@ pw_test("transfer_test") {
}
pw_test("transfer_thread_test") {
- enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_unit_test:light"
sources = [ "transfer_thread_test.cc" ]
deps = [
":core",
@@ -246,7 +246,10 @@ pw_test("client_test") {
}
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "api.rst",
+ "docs.rst",
+ ]
inputs = [
"transfer.proto",
"read.svg",
@@ -254,26 +257,6 @@ pw_doc_group("docs") {
]
}
-pw_proto_library("test_server_proto") {
- sources = [ "test_server.proto" ]
- prefix = "pw_transfer_test"
- deps = [ "$dir_pw_protobuf:common_protos" ]
-}
-
-pw_executable("test_rpc_server") {
- sources = [ "test_rpc_server.cc" ]
- deps = [
- ":atomic_file_transfer_handler",
- ":pw_transfer",
- ":test_server_proto.raw_rpc",
- "$dir_pw_rpc/system_server",
- "$dir_pw_rpc/system_server:socket",
- "$dir_pw_stream:std_file_stream",
- "$dir_pw_thread:thread",
- dir_pw_log,
- ]
-}
-
pw_executable("integration_test_server") {
sources = [ "integration_test/server.cc" ]
deps = [
@@ -303,62 +286,62 @@ pw_executable("integration_test_client") {
]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("integration_test_python_client") {
sources = [ "integration_test/python_client.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("integration_test_proxy") {
sources = [ "integration_test/proxy.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("integration_test_proxy_test") {
sources = [ "integration_test/proxy_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("integration_test_fixture") {
sources = [ "integration_test/test_fixture.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("cross_language_small_test") {
sources = [ "integration_test/cross_language_small_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("cross_language_medium_read_test") {
sources = [ "integration_test/cross_language_medium_read_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("cross_language_medium_write_test") {
sources = [ "integration_test/cross_language_medium_write_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("cross_language_large_read_test") {
sources = [ "integration_test/cross_language_large_read_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("cross_language_large_write_test") {
sources = [ "integration_test/cross_language_large_write_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("multi_transfer_test") {
sources = [ "integration_test/multi_transfer_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("expected_errors_test") {
sources = [ "integration_test/expected_errors_test.py" ]
}
-# TODO(b/228516801): Make this actually work; this is just a placeholder.
+# TODO: b/228516801 - Make this actually work; this is just a placeholder.
pw_python_script("legacy_binaries_test") {
sources = [ "integration_test/legacy_binaries_test.py" ]
}
diff --git a/pw_transfer/OWNERS b/pw_transfer/OWNERS
index d96cbc68d..34fbf1bf0 100644
--- a/pw_transfer/OWNERS
+++ b/pw_transfer/OWNERS
@@ -1 +1,2 @@
+frolv@google.com
hepler@google.com
diff --git a/pw_transfer/api.rst b/pw_transfer/api.rst
new file mode 100644
index 000000000..33ca6212d
--- /dev/null
+++ b/pw_transfer/api.rst
@@ -0,0 +1,12 @@
+.. _module-pw_transfer-api:
+
+=========================
+pw_transfer API reference
+=========================
+.. note::
+
+ This API reference is a work-in-progress. The full ``pw_transfer`` API is
+ not yet documented on this page.
+
+.. doxygennamespace:: pw::transfer
+ :members:
diff --git a/pw_transfer/chunk.cc b/pw_transfer/chunk.cc
index cf5133ed3..900c66efc 100644
--- a/pw_transfer/chunk.cc
+++ b/pw_transfer/chunk.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -27,9 +27,12 @@ Result<Chunk::Identifier> Chunk::ExtractIdentifier(ConstByteSpan message) {
protobuf::Decoder decoder(message);
uint32_t session_id = 0;
- uint32_t resource_id = 0;
+ uint32_t desired_session_id = 0;
+ bool legacy = true;
- while (decoder.Next().ok()) {
+ Status status;
+
+ while ((status = decoder.Next()).ok()) {
ProtoChunk::Fields field =
static_cast<ProtoChunk::Fields>(decoder.FieldNumber());
@@ -42,19 +45,27 @@ Result<Chunk::Identifier> Chunk::ExtractIdentifier(ConstByteSpan message) {
} else if (field == ProtoChunk::Fields::kSessionId) {
// A session_id field always takes precedence over transfer_id.
PW_TRY(decoder.ReadUint32(&session_id));
- } else if (field == ProtoChunk::Fields::kResourceId) {
- PW_TRY(decoder.ReadUint32(&resource_id));
+ legacy = false;
+ } else if (field == ProtoChunk::Fields::kDesiredSessionId) {
+ PW_TRY(decoder.ReadUint32(&desired_session_id));
}
}
- // Always prioritize a resource_id if one is set. Resource IDs should only be
- // set in cases where the transfer session ID has not yet been negotiated.
- if (resource_id != 0) {
- return Identifier::Resource(resource_id);
+ if (!status.IsOutOfRange()) {
+ return Status::DataLoss();
+ }
+
+ if (desired_session_id != 0) {
+ // Can't have both a desired and regular session_id.
+ if (!legacy && session_id != 0) {
+ return Status::DataLoss();
+ }
+ return Identifier::Desired(desired_session_id);
}
if (session_id != 0) {
- return Identifier::Session(session_id);
+ return legacy ? Identifier::Legacy(session_id)
+ : Identifier::Session(session_id);
}
return Status::DataLoss();
@@ -78,6 +89,8 @@ Result<Chunk> Chunk::Parse(ConstByteSpan message) {
// window_end_offset from it once parsing is complete.
uint32_t pending_bytes = 0;
+ bool has_session_id = false;
+
while ((status = decoder.Next()).ok()) {
ProtoChunk::Fields field =
static_cast<ProtoChunk::Fields>(decoder.FieldNumber());
@@ -99,7 +112,7 @@ Result<Chunk> Chunk::Parse(ConstByteSpan message) {
if (chunk.protocol_version_ == ProtocolVersion::kUnknown) {
chunk.protocol_version_ = ProtocolVersion::kVersionTwo;
}
-
+ has_session_id = true;
PW_TRY(decoder.ReadUint32(&chunk.session_id_));
break;
@@ -164,10 +177,20 @@ Result<Chunk> Chunk::Parse(ConstByteSpan message) {
chunk.protocol_version_ = static_cast<ProtocolVersion>(value);
break;
+ case ProtoChunk::Fields::kDesiredSessionId:
+ PW_TRY(decoder.ReadUint32(&value));
+ chunk.desired_session_id_ = value;
+ break;
+
// Silently ignore any unrecognized fields.
}
}
+ if (chunk.desired_session_id_.has_value() && has_session_id) {
+ // Setting both session_id and desired_session_id is not permitted.
+ return Status::DataLoss();
+ }
+
if (chunk.protocol_version_ == ProtocolVersion::kUnknown) {
// If no fields in the chunk specified its protocol version, assume it is a
// legacy chunk.
@@ -201,9 +224,15 @@ Result<ConstByteSpan> Chunk::Encode(ByteSpan buffer) const {
if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
if (session_id_ != 0) {
+ PW_CHECK(!desired_session_id_.has_value(),
+ "A chunk cannot set both a desired and regular session ID");
encoder.WriteSessionId(session_id_).IgnoreError();
}
+ if (desired_session_id_.has_value()) {
+ encoder.WriteDesiredSessionId(desired_session_id_.value()).IgnoreError();
+ }
+
if (resource_id_.has_value()) {
encoder.WriteResourceId(resource_id_.value()).IgnoreError();
}
@@ -297,6 +326,10 @@ size_t Chunk::EncodedSize() const {
size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kResourceId,
resource_id_.value());
}
+ if (desired_session_id_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kDesiredSessionId,
+ desired_session_id_.value());
+ }
}
if (offset_ != 0) {
diff --git a/pw_transfer/client.cc b/pw_transfer/client.cc
index 95abb81a3..bbd3d0de7 100644
--- a/pw_transfer/client.cc
+++ b/pw_transfer/client.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -24,6 +24,7 @@ Status Client::Read(uint32_t resource_id,
stream::Writer& output,
CompletionFunc&& on_completion,
chrono::SystemClock::duration timeout,
+ chrono::SystemClock::duration initial_chunk_timeout,
ProtocolVersion protocol_version) {
if (on_completion == nullptr ||
protocol_version == ProtocolVersion::kUnknown) {
@@ -49,6 +50,7 @@ Status Client::Read(uint32_t resource_id,
max_parameters_,
std::move(on_completion),
timeout,
+ initial_chunk_timeout,
max_retries_,
max_lifetime_retries_);
return OkStatus();
@@ -58,6 +60,7 @@ Status Client::Write(uint32_t resource_id,
stream::Reader& input,
CompletionFunc&& on_completion,
chrono::SystemClock::duration timeout,
+ chrono::SystemClock::duration initial_chunk_timeout,
ProtocolVersion protocol_version) {
if (on_completion == nullptr ||
protocol_version == ProtocolVersion::kUnknown) {
@@ -83,6 +86,7 @@ Status Client::Write(uint32_t resource_id,
max_parameters_,
std::move(on_completion),
timeout,
+ initial_chunk_timeout,
max_retries_,
max_lifetime_retries_);
diff --git a/pw_transfer/client_test.cc b/pw_transfer/client_test.cc
index 1b16d9142..323570dcf 100644
--- a/pw_transfer/client_test.cc
+++ b/pw_transfer/client_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -21,7 +21,6 @@
#include "pw_bytes/array.h"
#include "pw_rpc/raw/client_testing.h"
#include "pw_rpc/test_helpers.h"
-#include "pw_thread/sleep.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
#include "pw_transfer_private/chunk_testing.h"
@@ -611,8 +610,8 @@ TEST_F(ReadTransfer, ResendsParametersIfSentRepeatedChunkDuringRecovery) {
EXPECT_EQ(transfer_status, OkStatus());
}
-constexpr chrono::SystemClock::duration kTestTimeout =
- std::chrono::milliseconds(50);
+// Use a long timeout to avoid accidentally triggering timeouts.
+constexpr chrono::SystemClock::duration kTestTimeout = std::chrono::seconds(30);
constexpr uint8_t kTestRetries = 3;
TEST_F(ReadTransfer, Timeout_ResendsCurrentParameters) {
@@ -780,7 +779,7 @@ TEST_F(ReadTransfer, Timeout_EndsTransferAfterMaxRetries) {
EXPECT_EQ(transfer_status, Status::Unknown());
}
- // Sleep one more time after the final retry. The client should cancel the
+ // Time out one more time after the final retry. The client should cancel the
// transfer at this point. As no packets were received from the server, no
// final status chunk should be sent.
transfer_thread_.SimulateClientTimeout(14);
@@ -788,9 +787,10 @@ TEST_F(ReadTransfer, Timeout_EndsTransferAfterMaxRetries) {
EXPECT_EQ(transfer_status, Status::DeadlineExceeded());
- // After finishing the transfer, nothing else should be sent. Verify this by
- // waiting for a bit.
- this_thread::sleep_for(kTestTimeout * 4);
+ // After finishing the transfer, nothing else should be sent.
+ transfer_thread_.SimulateClientTimeout(14);
+ transfer_thread_.SimulateClientTimeout(14);
+ transfer_thread_.SimulateClientTimeout(14);
ASSERT_EQ(payloads.size(), 4u);
}
@@ -1466,7 +1466,7 @@ TEST_F(WriteTransfer, Timeout_EndsTransferAfterMaxRetries) {
EXPECT_EQ(transfer_status, Status::Unknown());
}
- // Sleep one more time after the final retry. The client should cancel the
+ // Time out one more time after the final retry. The client should cancel the
// transfer at this point. As no packets were received from the server, no
// final status chunk should be sent.
transfer_thread_.SimulateClientTimeout(13);
@@ -1474,9 +1474,10 @@ TEST_F(WriteTransfer, Timeout_EndsTransferAfterMaxRetries) {
EXPECT_EQ(transfer_status, Status::DeadlineExceeded());
- // After finishing the transfer, nothing else should be sent. Verify this by
- // waiting for a bit.
- this_thread::sleep_for(kTestTimeout * 4);
+ // After finishing the transfer, nothing else should be sent.
+ transfer_thread_.SimulateClientTimeout(13);
+ transfer_thread_.SimulateClientTimeout(13);
+ transfer_thread_.SimulateClientTimeout(13);
ASSERT_EQ(payloads.size(), 4u);
// Ensure we don't leave a dangling reference to transfer_status.
@@ -1580,12 +1581,14 @@ TEST_F(WriteTransfer, ManualCancel) {
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
// Get a response from the server, then cancel the transfer.
+ // This must request a smaller chunk than the entire available write data to
+ // prevent the client from trying to send an additional finish chunk.
context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
.set_session_id(15)
.set_offset(0)
- .set_window_end_offset(64)
- .set_max_chunk_size_bytes(32)));
+ .set_window_end_offset(16)
+ .set_max_chunk_size_bytes(16)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 2u);
@@ -1644,6 +1647,7 @@ TEST_F(ReadTransfer, Version2_SingleChunk) {
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1659,17 +1663,16 @@ TEST_F(ReadTransfer, Version2_SingleChunk) {
Chunk chunk = DecodeChunk(payloads[0]);
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
- // The server responds with a START_ACK, continuing the version 2 handshake
- // and assigning a session_id to the transfer.
+ // The server responds with a START_ACK, continuing the version 2 handshake.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(29)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1680,7 +1683,8 @@ TEST_F(ReadTransfer, Version2_SingleChunk) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_FALSE(chunk.desired_session_id().has_value());
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1690,7 +1694,7 @@ TEST_F(ReadTransfer, Version2_SingleChunk) {
// transfer.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(29)
+ .set_session_id(1)
.set_offset(0)
.set_payload(kData32)
.set_remaining_bytes(0)));
@@ -1699,7 +1703,7 @@ TEST_F(ReadTransfer, Version2_SingleChunk) {
ASSERT_EQ(payloads.size(), 3u);
chunk = DecodeChunk(payloads.back());
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
ASSERT_TRUE(chunk.status().has_value());
@@ -1711,7 +1715,7 @@ TEST_F(ReadTransfer, Version2_SingleChunk) {
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(29)));
+ .set_session_id(1)));
}
TEST_F(ReadTransfer, Version2_ServerRunsLegacy) {
@@ -1724,6 +1728,7 @@ TEST_F(ReadTransfer, Version2_ServerRunsLegacy) {
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1739,7 +1744,7 @@ TEST_F(ReadTransfer, Version2_ServerRunsLegacy) {
Chunk chunk = DecodeChunk(payloads[0]);
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1760,6 +1765,7 @@ TEST_F(ReadTransfer, Version2_ServerRunsLegacy) {
ASSERT_EQ(payloads.size(), 2u);
chunk = DecodeChunk(payloads.back());
+ EXPECT_FALSE(chunk.desired_session_id().has_value());
EXPECT_EQ(chunk.session_id(), 3u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kLegacy);
@@ -1781,6 +1787,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1796,7 +1803,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1804,13 +1811,13 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
// Wait for the timeout to expire without doing anything. The client should
// resend the initial chunk.
- transfer_thread_.SimulateClientTimeout(3);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 2u);
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1819,7 +1826,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
// This time, the server responds, continuing the handshake and transfer.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(31)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1828,7 +1835,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1836,7 +1843,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(31)
+ .set_session_id(1)
.set_offset(0)
.set_payload(kData32)
.set_remaining_bytes(0)));
@@ -1845,7 +1852,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
ASSERT_EQ(payloads.size(), 4u);
chunk = DecodeChunk(payloads.back());
- EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
ASSERT_TRUE(chunk.status().has_value());
@@ -1857,7 +1864,7 @@ TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(31)));
+ .set_session_id(1)));
}
TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
@@ -1870,6 +1877,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1885,7 +1893,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1895,7 +1903,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
// and assigning a session_id to the transfer.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(33)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1906,7 +1914,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1914,13 +1922,13 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
// Wait for the timeout to expire without doing anything. The client should
// resend the confirmation chunk.
- transfer_thread_.SimulateClientTimeout(33);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 3u);
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1929,7 +1937,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
// The server responds and the transfer should continue normally.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(33)
+ .set_session_id(1)
.set_offset(0)
.set_payload(kData32)
.set_remaining_bytes(0)));
@@ -1938,7 +1946,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
ASSERT_EQ(payloads.size(), 4u);
chunk = DecodeChunk(payloads.back());
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
ASSERT_TRUE(chunk.status().has_value());
@@ -1950,7 +1958,7 @@ TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(33)));
+ .set_session_id(1)));
}
TEST_F(ReadTransfer, Version2_ServerErrorDuringHandshake) {
@@ -1963,6 +1971,7 @@ TEST_F(ReadTransfer, Version2_ServerErrorDuringHandshake) {
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1978,7 +1987,7 @@ TEST_F(ReadTransfer, Version2_ServerErrorDuringHandshake) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -1986,7 +1995,7 @@ TEST_F(ReadTransfer, Version2_ServerErrorDuringHandshake) {
// The server responds to the start request with an error.
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(Chunk::Final(
- ProtocolVersion::kVersionTwo, 3, Status::Unauthenticated())));
+ ProtocolVersion::kVersionTwo, 1, Status::Unauthenticated())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(payloads.size(), 1u);
@@ -2003,6 +2012,7 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2018,17 +2028,16 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
Chunk chunk = DecodeChunk(payloads[0]);
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
- // The server responds with a START_ACK, continuing the version 2 handshake
- // and assigning a session_id to the transfer.
+ // The server responds with a START_ACK, continuing the version 2 handshake.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(29)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2039,7 +2048,7 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -2049,7 +2058,7 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
// transfer.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(29)
+ .set_session_id(1)
.set_offset(0)
.set_payload(kData32)
.set_remaining_bytes(0)));
@@ -2058,7 +2067,7 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
ASSERT_EQ(payloads.size(), 3u);
chunk = DecodeChunk(payloads.back());
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
ASSERT_TRUE(chunk.status().has_value());
@@ -2070,14 +2079,14 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
// Time out instead of sending a completion ACK. THe transfer should resend
// its completion chunk.
- transfer_thread_.SimulateClientTimeout(29);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 4u);
// Reset transfer_status to check whether the handler is called again.
transfer_status = Status::Unknown();
chunk = DecodeChunk(payloads.back());
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
ASSERT_TRUE(chunk.status().has_value());
@@ -2090,11 +2099,11 @@ TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
// Send a completion ACK to end the transfer.
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(29)));
+ .set_session_id(1)));
transfer_thread_.WaitUntilEventIsProcessed();
// No further chunks should be sent following the ACK.
- transfer_thread_.SimulateClientTimeout(29);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 4u);
}
@@ -2109,6 +2118,7 @@ TEST_F(ReadTransfer,
writer,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2124,7 +2134,7 @@ TEST_F(ReadTransfer,
Chunk chunk = DecodeChunk(payloads[0]);
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -2134,7 +2144,7 @@ TEST_F(ReadTransfer,
// and assigning a session_id to the transfer.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(29)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2145,7 +2155,7 @@ TEST_F(ReadTransfer,
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 64u);
@@ -2155,7 +2165,7 @@ TEST_F(ReadTransfer,
// transfer.
context_.server().SendServerStream<Transfer::Read>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(29)
+ .set_session_id(1)
.set_offset(0)
.set_payload(kData32)
.set_remaining_bytes(0)));
@@ -2164,7 +2174,7 @@ TEST_F(ReadTransfer,
ASSERT_EQ(payloads.size(), 3u);
chunk = DecodeChunk(payloads.back());
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
ASSERT_TRUE(chunk.status().has_value());
@@ -2177,16 +2187,16 @@ TEST_F(ReadTransfer,
// Time out instead of sending a completion ACK. THe transfer should resend
// its completion chunk at first, then terminate after the maximum number of
// retries.
- transfer_thread_.SimulateClientTimeout(29);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 4u); // Retry 1.
- transfer_thread_.SimulateClientTimeout(29);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 5u); // Retry 2.
- transfer_thread_.SimulateClientTimeout(29);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 6u); // Retry 3.
- transfer_thread_.SimulateClientTimeout(29);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 6u); // No more retries; transfer has ended.
}
@@ -2200,6 +2210,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
reader,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2212,14 +2223,13 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
- // The server responds with a START_ACK, continuing the version 2 handshake
- // and assigning a session_id to the transfer.
+ // The server responds with a START_ACK, continuing the version 2 handshake.
context_.server().SendServerStream<Transfer::Write>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(29)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2229,7 +2239,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
// The server can then begin the data transfer by sending its transfer
@@ -2237,7 +2247,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
rpc::test::WaitForPackets(context_.output(), 2, [this] {
context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit)
- .set_session_id(29)
+ .set_session_id(1)
.set_offset(0)
.set_window_end_offset(64)
.set_max_chunk_size_bytes(32)));
@@ -2248,7 +2258,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
chunk = DecodeChunk(payloads[2]);
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_TRUE(chunk.has_payload());
EXPECT_EQ(std::memcmp(
@@ -2258,7 +2268,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
chunk = DecodeChunk(payloads[3]);
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
ASSERT_TRUE(chunk.remaining_bytes().has_value());
EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
@@ -2266,7 +2276,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
// Send the final status chunk to complete the transfer.
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 29, OkStatus())));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
// Client should acknowledge the completion of the transfer.
@@ -2275,7 +2285,7 @@ TEST_F(WriteTransfer, Version2_SingleChunk) {
chunk = DecodeChunk(payloads[4]);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletionAck);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -2290,6 +2300,7 @@ TEST_F(WriteTransfer, Version2_ServerRunsLegacy) {
reader,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2302,7 +2313,7 @@ TEST_F(WriteTransfer, Version2_ServerRunsLegacy) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
// Instead of continuing the handshake with a START_ACK, the server
@@ -2357,6 +2368,7 @@ TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
reader,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2369,25 +2381,25 @@ TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
// Time out waiting for a server response. The client should resend the
// initial packet.
- transfer_thread_.SimulateClientTimeout(3);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 2u);
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
// This time, respond with the correct continuation packet. The transfer
// should resume and complete normally.
context_.server().SendServerStream<Transfer::Write>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(31)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2396,13 +2408,13 @@ TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
rpc::test::WaitForPackets(context_.output(), 2, [this] {
context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit)
- .set_session_id(31)
+ .set_session_id(1)
.set_offset(0)
.set_window_end_offset(64)
.set_max_chunk_size_bytes(32)));
@@ -2413,7 +2425,7 @@ TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
chunk = DecodeChunk(payloads[3]);
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_TRUE(chunk.has_payload());
EXPECT_EQ(std::memcmp(
@@ -2423,14 +2435,14 @@ TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
chunk = DecodeChunk(payloads[4]);
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.session_id(), 1u);
ASSERT_TRUE(chunk.remaining_bytes().has_value());
EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
EXPECT_EQ(transfer_status, Status::Unknown());
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 31, OkStatus())));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
// Client should acknowledge the completion of the transfer.
@@ -2439,7 +2451,7 @@ TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
chunk = DecodeChunk(payloads[5]);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletionAck);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -2454,6 +2466,7 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
reader,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2466,14 +2479,13 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
- // The server responds with a START_ACK, continuing the version 2 handshake
- // and assigning a session_id to the transfer.
+ // The server responds with a START_ACK, continuing the version 2 handshake.
context_.server().SendServerStream<Transfer::Write>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
- .set_session_id(33)
+ .set_session_id(1)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2483,18 +2495,18 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
// Time out waiting for a server response. The client should resend the
// initial packet.
- transfer_thread_.SimulateClientTimeout(33);
+ transfer_thread_.SimulateClientTimeout(1);
ASSERT_EQ(payloads.size(), 3u);
chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_FALSE(chunk.resource_id().has_value());
// This time, respond with the first transfer parameters chunk. The transfer
@@ -2502,7 +2514,7 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
rpc::test::WaitForPackets(context_.output(), 2, [this] {
context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit)
- .set_session_id(33)
+ .set_session_id(1)
.set_offset(0)
.set_window_end_offset(64)
.set_max_chunk_size_bytes(32)));
@@ -2513,7 +2525,7 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
chunk = DecodeChunk(payloads[3]);
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_TRUE(chunk.has_payload());
EXPECT_EQ(std::memcmp(
@@ -2523,14 +2535,14 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
chunk = DecodeChunk(payloads[4]);
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
ASSERT_TRUE(chunk.remaining_bytes().has_value());
EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
EXPECT_EQ(transfer_status, Status::Unknown());
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 33, OkStatus())));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
// Client should acknowledge the completion of the transfer.
@@ -2539,7 +2551,7 @@ TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
chunk = DecodeChunk(payloads[5]);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletionAck);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.session_id(), 1u);
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -2554,6 +2566,7 @@ TEST_F(WriteTransfer, Version2_ServerErrorDuringHandshake) {
reader,
[&transfer_status](Status status) { transfer_status = status; },
cfg::kDefaultChunkTimeout,
+ cfg::kDefaultChunkTimeout,
ProtocolVersion::kVersionTwo));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2566,12 +2579,12 @@ TEST_F(WriteTransfer, Version2_ServerErrorDuringHandshake) {
Chunk chunk = DecodeChunk(payloads.back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
- EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.desired_session_id(), 1u);
EXPECT_EQ(chunk.resource_id(), 3u);
// The server responds to the start request with an error.
context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
- Chunk::Final(ProtocolVersion::kVersionTwo, 3, Status::NotFound())));
+ Chunk::Final(ProtocolVersion::kVersionTwo, 1, Status::NotFound())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(payloads.size(), 1u);
diff --git a/pw_transfer/context.cc b/pw_transfer/context.cc
index 22686398c..8f64488a3 100644
--- a/pw_transfer/context.cc
+++ b/pw_transfer/context.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -33,6 +33,15 @@ void Context::HandleEvent(const Event& event) {
case EventType::kNewClientTransfer:
case EventType::kNewServerTransfer: {
if (active()) {
+ if (event.type == EventType::kNewServerTransfer &&
+ event.new_transfer.session_id == session_id_ &&
+ last_chunk_sent_ == Chunk::Type::kStartAck) {
+ // The client is retrying its initial chunk as the response may not
+ // have made it back. Re-send the handshake response without going
+ // through handler reinitialization.
+ RetryHandshake();
+ return;
+ }
Abort(Status::Aborted());
}
@@ -87,7 +96,7 @@ void Context::HandleEvent(const Event& event) {
void Context::InitiateTransferAsClient() {
PW_DCHECK(active());
- SetTimeout(chunk_timeout_);
+ SetTimeout(initial_chunk_timeout_);
PW_LOG_INFO("Starting transfer for resource %u",
static_cast<unsigned>(resource_id_));
@@ -104,7 +113,7 @@ void Context::InitiateTransferAsClient() {
if (type() == TransferType::kReceive) {
SendTransferParameters(TransmitAction::kBegin);
} else {
- SendInitialTransmitChunk();
+ SendInitialLegacyTransmitChunk();
}
LogTransferConfiguration();
@@ -113,6 +122,7 @@ void Context::InitiateTransferAsClient() {
// In newer protocol versions, begin the initial transfer handshake.
Chunk start_chunk(desired_protocol_version_, Chunk::Type::kStart);
+ start_chunk.set_desired_session_id(session_id_);
start_chunk.set_resource_id(resource_id_);
if (type() == TransferType::kReceive) {
@@ -159,11 +169,11 @@ bool Context::StartTransferAsServer(const NewTransferEvent& new_transfer) {
return true;
}
-void Context::SendInitialTransmitChunk() {
+void Context::SendInitialLegacyTransmitChunk() {
// A transmitter begins a transfer by sending the ID of the resource to which
// it wishes to write.
Chunk chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart);
- chunk.set_session_id(session_id_);
+ chunk.set_session_id(resource_id_);
EncodeAndSendChunk(chunk);
}
@@ -294,6 +304,7 @@ void Context::Initialize(const NewTransferEvent& new_transfer) {
last_chunk_sent_ = Chunk::Type::kStart;
last_chunk_offset_ = 0;
chunk_timeout_ = new_transfer.timeout;
+ initial_chunk_timeout_ = new_transfer.initial_timeout;
interchunk_delay_ = chrono::SystemClock::for_at_least(
std::chrono::microseconds(kDefaultChunkDelayMicroseconds));
next_timeout_ = kNoTimeout;
@@ -348,18 +359,11 @@ void Context::PerformInitialHandshake(const Chunk& chunk) {
break;
}
- // Response packet sent from a server to a client. Contains the assigned
- // session_id of the transfer.
+ // Response packet sent from a server to a client, confirming the protocol
+ // version and session_id of the transfer.
case Chunk::Type::kStartAck: {
UpdateLocalProtocolConfigurationFromPeer(chunk);
- // Accept the assigned session_id and tell the server that the transfer
- // can begin.
- session_id_ = chunk.session_id();
- PW_LOG_DEBUG("Transfer for resource %u was assigned session ID %u",
- static_cast<unsigned>(resource_id_),
- static_cast<unsigned>(session_id_));
-
Chunk start_ack_confirmation(configured_protocol_version_,
Chunk::Type::kStartAckConfirmation);
start_ack_confirmation.set_session_id(session_id_);
@@ -398,8 +402,8 @@ void Context::PerformInitialHandshake(const Chunk& chunk) {
case Chunk::Type::kData:
case Chunk::Type::kParametersRetransmit:
case Chunk::Type::kParametersContinue:
- // Update the local context's session ID in case it was expecting one to
- // be assigned by the server.
+ // Update the local session_id, which will map to the transfer_id of the
+ // legacy chunk.
session_id_ = chunk.session_id();
configured_protocol_version_ = ProtocolVersion::kLegacy;
@@ -944,7 +948,14 @@ void Context::HandleTimeout() {
// A timeout occurring in a transfer or handshake state indicates that no
// chunk has been received from the other side. The transfer should retry
// its previous operation.
- SetTimeout(chunk_timeout_); // Retry() clears the timeout if it fails
+ //
+ // The timeout is set immediately. Retry() will clear it if it fails.
+ if (transfer_state_ == TransferState::kInitiating &&
+ last_chunk_sent_ == Chunk::Type::kStart) {
+ SetTimeout(initial_chunk_timeout_);
+ } else {
+ SetTimeout(chunk_timeout_);
+ }
Retry();
break;
@@ -1004,7 +1015,7 @@ void Context::Retry() {
PW_LOG_DEBUG(
"Transmit transfer %u timed out waiting for initial parameters",
static_cast<unsigned>(session_id_));
- SendInitialTransmitChunk();
+ SendInitialLegacyTransmitChunk();
return;
}
@@ -1032,6 +1043,7 @@ void Context::RetryHandshake() {
// No protocol version is yet configured at the time of sending the start
// chunk, so we use the client's desired version instead.
retry_chunk.set_protocol_version(desired_protocol_version_)
+ .set_desired_session_id(session_id_)
.set_resource_id(resource_id_);
if (type() == TransferType::kReceive) {
SetTransferParameters(retry_chunk);
diff --git a/pw_transfer/docs.rst b/pw_transfer/docs.rst
index 01e1b30f6..d63890c2a 100644
--- a/pw_transfer/docs.rst
+++ b/pw_transfer/docs.rst
@@ -268,6 +268,16 @@ more details.
The default amount of time, in milliseconds, to wait for a chunk to arrive
before retrying. This can later be configured per-transfer.
+.. c:macro:: PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS
+
+ The default amount of time, in milliseconds, to wait for an initial server
+ response to a transfer before retrying. This can later be configured
+ per-transfer.
+
+ This is set separately to PW_TRANSFER_DEFAULT_TIMEOUT_MS as transfers may
+ require additional time for resource initialization (e.g. erasing a flash
+ region before writing to it).
+
.. c:macro:: PW_TRANSFER_DEFAULT_EXTEND_WINDOW_DIVISOR
The fractional position within a window at which a receive transfer should
@@ -397,8 +407,9 @@ defined by the implementers of the server-side transfer node.
The series of chunks exchanged in an individual transfer operation for a
resource constitute a transfer *session*. The session runs from its opening
chunk until either a terminating chunk is received or the transfer times out.
-Sessions are assigned unique IDs by the transfer server in response to an
-initiating chunk from the client.
+Sessions are assigned IDs by the client that starts them, which are unique over
+the RPC channel between the client and server, allowing the server to identify
+transfers across multiple clients.
Reliability
===========
@@ -421,15 +432,16 @@ resource being transferred, assign a session ID, and synchronize the protocol
version to use.
A read or write transfer for a resource is initiated by a transfer client. The
-client sends the ID of the resource to the server in a ``START`` chunk,
-indicating that it wishes to begin a new transfer. This chunk additionally
-encodes the protocol version which the client is configured to use.
+client sends the ID of the resource to the server alongside a unique session ID
+in a ``START`` chunk, indicating that it wishes to begin a new transfer. This
+chunk additionally encodes the protocol version which the client is configured
+to use.
Upon receiving a ``START`` chunk, the transfer server checks whether the
requested resource is available. If so, it prepares the resource for the
operation, which typically involves opening a data stream, alongside any
-additional user-specified setup. The server generates a session ID, then
-responds to the client with a ``START_ACK`` chunk containing the resource,
+additional user-specified setup. The server accepts the client's session ID,
+then responds to the client with a ``START_ACK`` chunk containing the resource,
session, and configured protocol version for the transfer.
Transfer completion
@@ -618,7 +630,7 @@ correctness of implementations in different languages.
To run the tests on your machine, run
-.. code:: bash
+.. code-block:: bash
$ bazel test --features=c++17 \
pw_transfer/integration_test:cross_language_small_test \
@@ -633,7 +645,7 @@ The integration tests permit injection of client/server/proxy binaries to use
when running the tests. This allows manual testing of older versions of
pw_transfer against newer versions.
-.. code:: bash
+.. code-block:: bash
# Test a newer version of pw_transfer against an old C++ client that was
# backed up to another directory.
@@ -651,7 +663,7 @@ binaries and the latest binaries.
The CIPD package contents can be created with this command:
-.. code::bash
+.. code-block::bash
$ bazel build --features=c++17 pw_transfer/integration_test:server \
pw_transfer/integration_test:cpp_client
@@ -672,6 +684,11 @@ By default, these tests are not run in CQ (on presubmit) because they are too
slow. However, you can request that the tests be run in presubmit on your
change by adding to following line to the commit message footer:
-.. code::
+.. code-block::
+
+ Cq-Include-Trybots: luci.pigweed.try:pigweed-integration-transfer
+
+.. toctree::
+ :hidden:
- Cq-Include-Trybots: luci.pigweed.try:pigweed-integration-transfer
+ API reference <api>
diff --git a/pw_transfer/integration_test/BUILD.bazel b/pw_transfer/integration_test/BUILD.bazel
index 5cb92ee7a..e90d6b067 100644
--- a/pw_transfer/integration_test/BUILD.bazel
+++ b/pw_transfer/integration_test/BUILD.bazel
@@ -12,9 +12,9 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("//pw_build:pigweed.bzl", "pw_cc_binary")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
+load("//pw_build:pigweed.bzl", "pw_cc_binary")
pw_cc_binary(
name = "server",
@@ -28,7 +28,6 @@ pw_cc_binary(
"//pw_stream",
"//pw_stream:std_file_stream",
"//pw_thread:thread",
- "//pw_thread_stl:thread_headers",
"//pw_transfer",
"@com_google_protobuf//:protobuf",
],
@@ -52,9 +51,11 @@ py_test(
"proxy.py",
"proxy_test.py",
],
+ imports = ["."],
main = "proxy_test.py",
deps = [
":config_pb2",
+ "//pw_hdlc/py:pw_hdlc",
"//pw_rpc:internal_packet_proto_pb2",
"//pw_transfer:transfer_proto_pb2",
"//pw_transfer/py:pw_transfer",
@@ -110,6 +111,7 @@ py_library(
":python_client",
":server",
],
+ imports = ["."],
deps = [
":config_pb2",
"//pw_protobuf:status_proto_pb2",
@@ -127,8 +129,11 @@ py_test(
srcs = [
"cross_language_large_write_test.py",
],
- # This test is not run in CQ because it's too slow.
- tags = ["manual"],
+ tags = [
+ # This test is not run in CQ because it's too slow.
+ "manual",
+ "integration",
+ ],
deps = [
":integration_test_fixture",
],
@@ -143,8 +148,11 @@ py_test(
srcs = [
"cross_language_large_read_test.py",
],
- # This test is not run in CQ because it's too slow.
- tags = ["manual"],
+ tags = [
+ # This test is not run in CQ because it's too slow.
+ "manual",
+ "integration",
+ ],
deps = [
":integration_test_fixture",
],
@@ -157,10 +165,12 @@ py_test(
srcs = [
"cross_language_medium_read_test.py",
],
+ tags = ["integration"],
deps = [
":config_pb2",
":integration_test_fixture",
"@com_google_protobuf//:protobuf_python",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -171,10 +181,12 @@ py_test(
srcs = [
"cross_language_medium_write_test.py",
],
+ tags = ["integration"],
deps = [
":config_pb2",
":integration_test_fixture",
"@com_google_protobuf//:protobuf_python",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -185,9 +197,11 @@ py_test(
srcs = [
"cross_language_small_test.py",
],
+ tags = ["integration"],
deps = [
":config_pb2",
":integration_test_fixture",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -198,9 +212,11 @@ py_test(
srcs = [
"multi_transfer_test.py",
],
+ tags = ["integration"],
deps = [
":config_pb2",
":integration_test_fixture",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -209,11 +225,13 @@ py_test(
name = "expected_errors_test",
timeout = "moderate",
srcs = ["expected_errors_test.py"],
+ tags = ["integration"],
deps = [
":config_pb2",
":integration_test_fixture",
"//pw_protobuf:status_proto_pb2",
"@com_google_protobuf//:protobuf_python",
+ "@python_packages_parameterized//:pkg",
],
)
@@ -225,10 +243,14 @@ py_test(
data = [
"@pw_transfer_test_binaries//:all",
],
+ tags = ["integration"],
+ # Legacy binaries were only built for linux-x86_64.
+ target_compatible_with = ["@platforms//os:linux"],
deps = [
":config_pb2",
":integration_test_fixture",
"//pw_protobuf:status_proto_pb2",
+ "@python_packages_parameterized//:pkg",
"@rules_python//python/runfiles",
],
)
@@ -259,5 +281,6 @@ py_binary(
"//pw_transfer:transfer_proto_pb2",
"//pw_transfer/py:pw_transfer",
"@com_google_protobuf//:protobuf_python",
+ "@python_packages_pyserial//:pkg",
],
)
diff --git a/pw_transfer/integration_test/client.cc b/pw_transfer/integration_test/client.cc
index 49bb3f267..1d932209c 100644
--- a/pw_transfer/integration_test/client.cc
+++ b/pw_transfer/integration_test/client.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -84,6 +84,15 @@ pw::Status PerformTransferActions(const pw::transfer::ClientConfig& config) {
transfer::Thread<2, 2> transfer_thread(chunk_buffer, encode_buffer);
thread::Thread system_thread(TransferThreadOptions(), transfer_thread);
+ // As much as we don't want to dynamically allocate an array,
+ // variable length arrays (VLA) are nonstandard, and a std::vector could cause
+ // references to go stale if the vector's underlying buffer is resized. This
+ // array of TransferResults needs to outlive the loop that performs the
+ // actual transfer actions due to how some references to TransferResult
+ // may persist beyond the lifetime of a transfer.
+ const int num_actions = config.transfer_actions().size();
+ auto transfer_results = std::make_unique<TransferResult[]>(num_actions);
+
pw::transfer::Client client(rpc::integration_test::client(),
rpc::integration_test::kChannelId,
transfer_thread);
@@ -92,8 +101,9 @@ pw::Status PerformTransferActions(const pw::transfer::ClientConfig& config) {
client.set_max_lifetime_retries(config.max_lifetime_retries());
Status status = pw::OkStatus();
- for (const pw::transfer::TransferAction& action : config.transfer_actions()) {
- TransferResult result;
+ for (int i = 0; i < num_actions; i++) {
+ const pw::transfer::TransferAction& action = config.transfer_actions()[i];
+ TransferResult& result = transfer_results[i];
// If no protocol version is specified, default to the latest version.
pw::transfer::ProtocolVersion protocol_version =
action.protocol_version() ==
@@ -114,6 +124,7 @@ pw::Status PerformTransferActions(const pw::transfer::ClientConfig& config) {
result.completed.release();
},
pw::transfer::cfg::kDefaultChunkTimeout,
+ pw::transfer::cfg::kDefaultInitialChunkTimeout,
protocol_version);
// Wait for the transfer to complete. We need to do this here so that the
// StdFileReader doesn't go out of scope.
@@ -131,6 +142,7 @@ pw::Status PerformTransferActions(const pw::transfer::ClientConfig& config) {
result.completed.release();
},
pw::transfer::cfg::kDefaultChunkTimeout,
+ pw::transfer::cfg::kDefaultInitialChunkTimeout,
protocol_version);
// Wait for the transfer to complete.
result.completed.acquire();
diff --git a/pw_transfer/integration_test/config.proto b/pw_transfer/integration_test/config.proto
index d120fc576..4fc7de336 100644
--- a/pw_transfer/integration_test/config.proto
+++ b/pw_transfer/integration_test/config.proto
@@ -49,7 +49,7 @@ message TransferAction {
// Expected final status of transfer operation.
//
- // TODO(b/241456982): This should be a pw.protobuf.StatusCode, but importing
+ // TODO: b/241456982 - This should be a pw.protobuf.StatusCode, but importing
// other Pigweed protos doesn't work in Bazel.
uint32 expected_status = 4;
diff --git a/pw_transfer/integration_test/cross_language_large_read_test.py b/pw_transfer/integration_test/cross_language_large_read_test.py
index 31d41e82e..37d51d9c2 100644
--- a/pw_transfer/integration_test/cross_language_large_read_test.py
+++ b/pw_transfer/integration_test/cross_language_large_read_test.py
@@ -35,7 +35,7 @@ import random
from google.protobuf import text_format
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import (
TransferConfig,
TransferIntegrationTest,
diff --git a/pw_transfer/integration_test/cross_language_large_write_test.py b/pw_transfer/integration_test/cross_language_large_write_test.py
index 0f195e2da..ee4c31878 100644
--- a/pw_transfer/integration_test/cross_language_large_write_test.py
+++ b/pw_transfer/integration_test/cross_language_large_write_test.py
@@ -35,7 +35,7 @@ import random
from google.protobuf import text_format
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import (
TransferConfig,
TransferIntegrationTest,
diff --git a/pw_transfer/integration_test/cross_language_medium_read_test.py b/pw_transfer/integration_test/cross_language_medium_read_test.py
index 8899d8f78..6adea14cd 100644
--- a/pw_transfer/integration_test/cross_language_medium_read_test.py
+++ b/pw_transfer/integration_test/cross_language_medium_read_test.py
@@ -37,7 +37,7 @@ import random
from google.protobuf import text_format
from pigweed.pw_transfer.integration_test import config_pb2
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness, TransferConfig
_ALL_LANGUAGES = ("cpp", "java", "python")
diff --git a/pw_transfer/integration_test/cross_language_medium_write_test.py b/pw_transfer/integration_test/cross_language_medium_write_test.py
index 5f8c314dc..a4e33db9b 100644
--- a/pw_transfer/integration_test/cross_language_medium_write_test.py
+++ b/pw_transfer/integration_test/cross_language_medium_write_test.py
@@ -37,7 +37,7 @@ import random
from google.protobuf import text_format
from pigweed.pw_transfer.integration_test import config_pb2
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness, TransferConfig
_ALL_LANGUAGES = ("cpp", "java", "python")
diff --git a/pw_transfer/integration_test/cross_language_small_test.py b/pw_transfer/integration_test/cross_language_small_test.py
index cfda8f0b4..fd05b7802 100644
--- a/pw_transfer/integration_test/cross_language_small_test.py
+++ b/pw_transfer/integration_test/cross_language_small_test.py
@@ -33,7 +33,7 @@ import itertools
from parameterized import parameterized
from pigweed.pw_transfer.integration_test import config_pb2
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness
_ALL_LANGUAGES = ("cpp", "java", "python")
diff --git a/pw_transfer/integration_test/expected_errors_test.py b/pw_transfer/integration_test/expected_errors_test.py
index 64d90a390..9a12b6879 100644
--- a/pw_transfer/integration_test/expected_errors_test.py
+++ b/pw_transfer/integration_test/expected_errors_test.py
@@ -41,7 +41,7 @@ from google.protobuf import text_format
from pigweed.pw_transfer.integration_test import config_pb2
from pigweed.pw_protobuf.pw_protobuf_protos import status_pb2
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness, TransferConfig
diff --git a/pw_transfer/integration_test/legacy_binaries_test.py b/pw_transfer/integration_test/legacy_binaries_test.py
index 7c6b8aa99..671260911 100644
--- a/pw_transfer/integration_test/legacy_binaries_test.py
+++ b/pw_transfer/integration_test/legacy_binaries_test.py
@@ -34,7 +34,7 @@ from parameterized import parameterized
import random
from pigweed.pw_transfer.integration_test import config_pb2
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness
from rules_python.python.runfiles import runfiles
@@ -254,7 +254,10 @@ class LegacyTransferIntegrationTest(test_fixture.TransferIntegrationTest):
class LegacyClientTransferIntegrationTests(LegacyTransferIntegrationTest):
r = runfiles.Create()
- client_binary = r.Rlocation("pw_transfer_test_binaries/cpp_client_528098d5")
+ client_binary = r.Rlocation(
+ "pw_transfer_test_binaries/cpp_client_528098d5",
+ "pw_transfer_test_binaries",
+ )
HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
cpp_client_binary=client_binary,
server_port=_SERVER_PORT,
@@ -265,7 +268,9 @@ class LegacyClientTransferIntegrationTests(LegacyTransferIntegrationTest):
class LegacyServerTransferIntegrationTests(LegacyTransferIntegrationTest):
r = runfiles.Create()
- server_binary = r.Rlocation("pw_transfer_test_binaries/server_528098d5")
+ server_binary = r.Rlocation(
+ "pw_transfer_test_binaries/server_528098d5", "pw_transfer_test_binaries"
+ )
HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
server_binary=server_binary,
server_port=_SERVER_PORT,
diff --git a/pw_transfer/integration_test/multi_transfer_test.py b/pw_transfer/integration_test/multi_transfer_test.py
index 88ec079c1..90c792466 100644
--- a/pw_transfer/integration_test/multi_transfer_test.py
+++ b/pw_transfer/integration_test/multi_transfer_test.py
@@ -34,7 +34,7 @@ from parameterized import parameterized
import random
from typing import List
-import test_fixture
+from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness, BasicTransfer
from pigweed.pw_transfer.integration_test import config_pb2
diff --git a/pw_transfer/integration_test/proxy.py b/pw_transfer/integration_test/proxy.py
index d9df6555c..553518f92 100644
--- a/pw_transfer/integration_test/proxy.py
+++ b/pw_transfer/integration_test/proxy.py
@@ -612,11 +612,11 @@ async def _main(server_port: int, client_port: int) -> None:
config = text_format.Parse(text_config, config_pb2.ProxyConfig())
# Instantiate the TCP server.
- server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
server_socket.setsockopt(
socket.SOL_SOCKET, socket.SO_RCVBUF, _RECEIVE_BUFFER_SIZE
)
- server_socket.bind(('localhost', client_port))
+ server_socket.bind(('', client_port))
server = await asyncio.start_server(
lambda reader, writer: _handle_connection(
server_port, config, reader, writer
diff --git a/pw_transfer/integration_test/python_client.py b/pw_transfer/integration_test/python_client.py
index 4c27189b0..2474cf121 100644
--- a/pw_transfer/integration_test/python_client.py
+++ b/pw_transfer/integration_test/python_client.py
@@ -19,7 +19,7 @@ import socket
import sys
from google.protobuf import text_format
-from pw_hdlc.rpc import HdlcRpcClient, default_channels
+from pw_hdlc.rpc import HdlcRpcClient, default_channels, SocketReader
from pw_status import Status
import pw_transfer
from pigweed.pw_transfer import transfer_pb2
@@ -32,6 +32,74 @@ _LOG.addHandler(logging.StreamHandler(sys.stdout))
HOSTNAME: str = "localhost"
+def _perform_transfer_action(
+ action: config_pb2.TransferAction, transfer_manager: pw_transfer.Manager
+) -> bool:
+ """Performs the transfer action and returns Truen on success."""
+ protocol_version = pw_transfer.ProtocolVersion(int(action.protocol_version))
+
+ # Default to the latest protocol version if none is specified.
+ if protocol_version == pw_transfer.ProtocolVersion.UNKNOWN:
+ protocol_version = pw_transfer.ProtocolVersion.LATEST
+
+ if (
+ action.transfer_type
+ == config_pb2.TransferAction.TransferType.WRITE_TO_SERVER
+ ):
+ try:
+ with open(action.file_path, 'rb') as f:
+ data = f.read()
+ except:
+ _LOG.critical("Failed to read input file '%s'", action.file_path)
+ return False
+
+ try:
+ transfer_manager.write(
+ action.resource_id,
+ data,
+ protocol_version=protocol_version,
+ )
+ except pw_transfer.client.Error as e:
+ if e.status != Status(action.expected_status):
+ _LOG.exception(
+ "Unexpected error encountered during write transfer"
+ )
+ return False
+ except:
+ _LOG.exception("Transfer (write to server) failed")
+ return False
+ elif (
+ action.transfer_type
+ == config_pb2.TransferAction.TransferType.READ_FROM_SERVER
+ ):
+ try:
+ data = transfer_manager.read(
+ action.resource_id,
+ protocol_version=protocol_version,
+ )
+ except pw_transfer.client.Error as e:
+ if e.status != Status(action.expected_status):
+ _LOG.exception(
+ "Unexpected error encountered during read transfer"
+ )
+ return False
+ return True
+ except:
+ _LOG.exception("Transfer (read from server) failed")
+ return False
+
+ try:
+ with open(action.file_path, 'wb') as f:
+ f.write(data)
+ except:
+ _LOG.critical("Failed to write output file '%s'", action.file_path)
+ return False
+ else:
+ _LOG.critical("Unknown transfer type: %d", action.transfer_type)
+ return False
+ return True
+
+
def _main() -> int:
if len(sys.argv) != 2:
_LOG.critical("Usage: PORT")
@@ -60,93 +128,36 @@ def _main() -> int:
_LOG.critical("Failed to connect to server at %s:%d", HOSTNAME, port)
return 1
- # Initialize an RPC client over the socket and set up the pw_transfer manager.
- rpc_client = HdlcRpcClient(
- lambda: rpc_socket.recv(4096),
- [transfer_pb2],
- default_channels(lambda data: rpc_socket.sendall(data)),
- lambda data: _LOG.info("%s", str(data)),
- )
- transfer_service = rpc_client.rpcs().pw.transfer.Transfer
- transfer_manager = pw_transfer.Manager(
- transfer_service,
- default_response_timeout_s=config.chunk_timeout_ms / 1000,
- initial_response_timeout_s=config.initial_chunk_timeout_ms / 1000,
- max_retries=config.max_retries,
- max_lifetime_retries=config.max_lifetime_retries,
- default_protocol_version=pw_transfer.ProtocolVersion.LATEST,
- )
-
- transfer_logger = logging.getLogger('pw_transfer')
- transfer_logger.setLevel(logging.DEBUG)
- transfer_logger.addHandler(logging.StreamHandler(sys.stdout))
-
- # Perform the requested transfer actions.
- for action in config.transfer_actions:
- protocol_version = pw_transfer.ProtocolVersion(
- int(action.protocol_version)
+ # Initialize an RPC client using a socket reader and set up the
+ # pw_transfer manager.
+ reader = SocketReader(rpc_socket, 4096)
+ with reader:
+ rpc_client = HdlcRpcClient(
+ reader,
+ [transfer_pb2],
+ default_channels(lambda data: rpc_socket.sendall(data)),
+ lambda data: _LOG.info("%s", str(data)),
)
-
- # Default to the latest protocol version if none is specified.
- if protocol_version == pw_transfer.ProtocolVersion.UNKNOWN:
- protocol_version = pw_transfer.ProtocolVersion.LATEST
-
- if (
- action.transfer_type
- == config_pb2.TransferAction.TransferType.WRITE_TO_SERVER
- ):
- try:
- with open(action.file_path, 'rb') as f:
- data = f.read()
- except:
- _LOG.critical(
- "Failed to read input file '%s'", action.file_path
- )
- return 1
-
- try:
- transfer_manager.write(
- action.resource_id, data, protocol_version=protocol_version
- )
- except pw_transfer.client.Error as e:
- if e.status != Status(action.expected_status):
- _LOG.exception(
- "Unexpected error encountered during write transfer"
- )
+ with rpc_client:
+ transfer_service = rpc_client.rpcs().pw.transfer.Transfer
+ transfer_manager = pw_transfer.Manager(
+ transfer_service,
+ default_response_timeout_s=config.chunk_timeout_ms / 1000,
+ initial_response_timeout_s=config.initial_chunk_timeout_ms
+ / 1000,
+ max_retries=config.max_retries,
+ max_lifetime_retries=config.max_lifetime_retries,
+ default_protocol_version=pw_transfer.ProtocolVersion.LATEST,
+ )
+
+ transfer_logger = logging.getLogger('pw_transfer')
+ transfer_logger.setLevel(logging.DEBUG)
+ transfer_logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ # Perform the requested transfer actions.
+ for action in config.transfer_actions:
+ if not _perform_transfer_action(action, transfer_manager):
return 1
- except:
- _LOG.exception("Transfer (write to server) failed")
- return 1
- elif (
- action.transfer_type
- == config_pb2.TransferAction.TransferType.READ_FROM_SERVER
- ):
- try:
- data = transfer_manager.read(
- action.resource_id, protocol_version=protocol_version
- )
- except pw_transfer.client.Error as e:
- if e.status != Status(action.expected_status):
- _LOG.exception(
- "Unexpected error encountered during read transfer"
- )
- return 1
- continue
- except:
- _LOG.exception("Transfer (read from server) failed")
- return 1
-
- try:
- with open(action.file_path, 'wb') as f:
- f.write(data)
- except:
- _LOG.critical(
- "Failed to write output file '%s'", action.file_path
- )
- return 1
- else:
- _LOG.critical("Unknown transfer type: %d", action.transfer_type)
- return 1
_LOG.info("All transfers completed successfully")
return 0
diff --git a/pw_transfer/integration_test/test_fixture.py b/pw_transfer/integration_test/test_fixture.py
index a7b297c77..60323c223 100644
--- a/pw_transfer/integration_test/test_fixture.py
+++ b/pw_transfer/integration_test/test_fixture.py
@@ -343,7 +343,7 @@ class TransferIntegrationTestHarness:
class BasicTransfer(NamedTuple):
id: int
- type: config_pb2.TransferAction.TransferType.ValueType
+ type: 'config_pb2.TransferAction.TransferType.ValueType'
data: bytes
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel b/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel
index 03a9a007e..dffe540cd 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel
@@ -39,6 +39,7 @@ java_library(
"//third_party/google_auto:value",
"@com_google_protobuf//java/lite",
"@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_guava_failureaccess",
"@maven//:com_google_guava_guava",
],
)
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java
index 15e3cd321..aa9389b77 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java
@@ -52,6 +52,7 @@ class ReadTransfer extends Transfer<byte[]> {
private int lastReceivedOffset = 0;
ReadTransfer(int resourceId,
+ int sessionId,
ProtocolVersion desiredProtocolVersion,
TransferInterface transferManager,
TransferTimeoutSettings timeoutSettings,
@@ -59,6 +60,7 @@ class ReadTransfer extends Transfer<byte[]> {
Consumer<TransferProgress> progressCallback,
BooleanSupplier shouldAbortCallback) {
super(resourceId,
+ sessionId,
desiredProtocolVersion,
transferManager,
timeoutSettings,
@@ -68,6 +70,10 @@ class ReadTransfer extends Transfer<byte[]> {
this.windowEndOffset = parameters.maxPendingBytes();
}
+ final TransferParameters getParametersForTest() {
+ return parameters;
+ }
+
@Override
State getWaitingForDataState() {
return new ReceivingData();
@@ -183,7 +189,7 @@ class ReadTransfer extends Transfer<byte[]> {
ByteBuffer result = ByteBuffer.allocate(totalDataSize);
dataChunks.forEach(result::put);
- getFuture().set(result.array());
+ set(result.array());
}
private VersionedChunk prepareTransferParameters(boolean extend) {
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
index fef83e59e..8183fb848 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
@@ -17,6 +17,7 @@ package dev.pigweed.pw_transfer;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static dev.pigweed.pw_transfer.TransferProgress.UNKNOWN_TRANSFER_SIZE;
+import com.google.common.util.concurrent.AbstractFuture;
import com.google.common.util.concurrent.SettableFuture;
import dev.pigweed.pw_log.Logger;
import dev.pigweed.pw_rpc.Status;
@@ -28,7 +29,7 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
/** Base class for tracking the state of a read or write transfer. */
-abstract class Transfer<T> {
+abstract class Transfer<T> extends AbstractFuture<T> {
private static final Logger logger = Logger.forClass(Transfer.class);
// Largest nanosecond instant. Used to block indefinitely when no transfers are pending.
@@ -38,15 +39,14 @@ abstract class Transfer<T> {
static final boolean VERBOSE_LOGGING = false;
private final int resourceId;
+ private final int sessionId;
private final ProtocolVersion desiredProtocolVersion;
private final TransferEventHandler.TransferInterface eventHandler;
- private final SettableFuture<T> future;
private final TransferTimeoutSettings timeoutSettings;
private final Consumer<TransferProgress> progressCallback;
private final BooleanSupplier shouldAbortCallback;
private final Instant startTime;
- private int sessionId = VersionedChunk.UNASSIGNED_SESSION_ID;
private ProtocolVersion configuredProtocolVersion = ProtocolVersion.UNKNOWN;
private Instant deadline = NO_TIMEOUT;
private State state;
@@ -68,23 +68,24 @@ abstract class Transfer<T> {
* @param shouldAbortCallback BooleanSupplier that returns true if a transfer should be aborted.
*/
Transfer(int resourceId,
+ int sessionId,
ProtocolVersion desiredProtocolVersion,
TransferInterface eventHandler,
TransferTimeoutSettings timeoutSettings,
Consumer<TransferProgress> progressCallback,
BooleanSupplier shouldAbortCallback) {
this.resourceId = resourceId;
+ this.sessionId = sessionId;
this.desiredProtocolVersion = desiredProtocolVersion;
this.eventHandler = eventHandler;
- this.future = SettableFuture.create();
this.timeoutSettings = timeoutSettings;
this.progressCallback = progressCallback;
this.shouldAbortCallback = shouldAbortCallback;
// If the future is cancelled, tell the TransferEventHandler to cancel the transfer.
- future.addListener(() -> {
- if (future.isCancelled()) {
+ addListener(() -> {
+ if (isCancelled()) {
eventHandler.cancelTransfer(this);
}
}, directExecutor());
@@ -92,7 +93,6 @@ abstract class Transfer<T> {
if (desiredProtocolVersion == ProtocolVersion.LEGACY) {
// Legacy transfers skip protocol negotiation stage and use the resource ID as the session ID.
configuredProtocolVersion = ProtocolVersion.LEGACY;
- assignSessionId(resourceId);
state = getWaitingForDataState();
} else {
state = new Initiating();
@@ -119,9 +119,8 @@ abstract class Transfer<T> {
return sessionId;
}
- private void assignSessionId(int newSessionId) {
- sessionId = newSessionId;
- eventHandler.assignSessionId(this);
+ final ProtocolVersion getDesiredProtocolVersionForTest() {
+ return desiredProtocolVersion;
}
/** Terminates the transfer without sending any packets. */
@@ -145,10 +144,6 @@ abstract class Transfer<T> {
deadline = Instant.now().plusNanos((long) timeoutMicros * 1000);
}
- final SettableFuture<T> getFuture() {
- return future;
- }
-
final void start() {
logger.atInfo().log(
"%s starting with parameters: default timeout %d ms, initial timeout %d ms, %d max retires",
@@ -157,7 +152,7 @@ abstract class Transfer<T> {
timeoutSettings.initialTimeoutMillis(),
timeoutSettings.maxRetries());
VersionedChunk.Builder chunk =
- VersionedChunk.createInitialChunk(desiredProtocolVersion, resourceId);
+ VersionedChunk.createInitialChunk(desiredProtocolVersion, resourceId, sessionId);
prepareInitialChunk(chunk);
try {
sendChunk(chunk.build());
@@ -244,7 +239,8 @@ abstract class Transfer<T> {
.setVersion(configuredProtocolVersion != ProtocolVersion.UNKNOWN ? configuredProtocolVersion
: desiredProtocolVersion)
.setType(type)
- .setSessionId(sessionId);
+ .setSessionId(sessionId)
+ .setResourceId(resourceId);
}
final VersionedChunk getLastChunkSent() {
@@ -393,8 +389,6 @@ abstract class Transfer<T> {
private class Initiating extends ActiveState {
@Override
public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
- assignSessionId(chunk.sessionId());
-
if (chunk.version() == ProtocolVersion.UNKNOWN) {
logger.atWarning().log(
"%s aborting due to unsupported protocol version: %s", Transfer.this, chunk);
@@ -495,7 +489,7 @@ abstract class Transfer<T> {
if (status.ok()) {
setFutureResult();
} else {
- future.setException(new TransferError(Transfer.this, status));
+ setException(new TransferError(Transfer.this, status));
}
}
@@ -503,7 +497,7 @@ abstract class Transfer<T> {
Completed(TransferError exception) {
cleanUp();
logger.atWarning().withCause(exception).log("%s terminated with exception", Transfer.this);
- future.setException(exception);
+ setException(exception);
}
private void cleanUp() {
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
index 248e50ea9..13779005a 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
@@ -35,7 +35,7 @@ public class TransferClient {
private final TransferEventHandler transferEventHandler;
private final Thread transferEventHandlerThread;
- private ProtocolVersion desiredProtocolVersion = ProtocolVersion.latest();
+ private ProtocolVersion desiredProtocolVersion = ProtocolVersion.LEGACY;
/**
* Creates a new transfer client for sending and receiving data with pw_transfer.
@@ -153,7 +153,22 @@ public class TransferClient {
transferEventHandlerThread.join();
}
- void waitUntilEventsAreProcessedForTest() {
+ // Functions for test use only.
+ // TODO: b/279808806 - These could be annotated with test-only visibility.
+
+ final void waitUntilEventsAreProcessedForTest() {
transferEventHandler.waitUntilEventsAreProcessedForTest();
}
+
+ final int getNextSessionIdForTest() {
+ return transferEventHandler.getNextSessionIdForTest();
+ }
+
+ final WriteTransfer getWriteTransferForTest(ListenableFuture<?> transferFuture) {
+ return (WriteTransfer) transferFuture;
+ }
+
+ final ReadTransfer getReadTransferForTest(ListenableFuture<?> transferFuture) {
+ return (ReadTransfer) transferFuture;
+ }
}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java
index f4e8189f7..a37d05117 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java
@@ -51,14 +51,18 @@ class TransferEventHandler {
private final BlockingQueue<Event> events = new LinkedBlockingQueue<>();
- // Map resource ID to transfer, and session ID to resource ID.
- private final Map<Integer, Transfer<?>> resourceIdToTransfer = new HashMap<>();
- private final Map<Integer, Integer> sessionToResourceId = new HashMap<>();
+ // Map session ID to transfer.
+ private final Map<Integer, Transfer<?>> sessionIdToTransfer = new HashMap<>();
+ // Legacy transfers only use the resource ID. The client assigns an arbitrary session ID that
+ // legacy servers ignore. The client then maps from the legacy ID to its local session ID.
+ private final Map<Integer, Integer> legacyIdToSessionId = new HashMap<>();
@Nullable private Call.ClientStreaming<Chunk> readStream = null;
@Nullable private Call.ClientStreaming<Chunk> writeStream = null;
private boolean processEvents = true;
+ private int nextSessionId = 1;
+
TransferEventHandler(MethodClient readMethod, MethodClient writeMethod) {
this.readMethod = readMethod;
this.writeMethod = writeMethod;
@@ -70,8 +74,8 @@ class TransferEventHandler {
byte[] data,
Consumer<TransferProgress> progressCallback,
BooleanSupplier shouldAbortCallback) {
- WriteTransfer transfer =
- new WriteTransfer(resourceId, desiredProtocolVersion, new TransferInterface() {
+ WriteTransfer transfer = new WriteTransfer(
+ resourceId, assignSessionId(), desiredProtocolVersion, new TransferInterface() {
@Override
Call.ClientStreaming<Chunk> getStream() throws ChannelOutputException {
if (writeStream == null) {
@@ -86,7 +90,7 @@ class TransferEventHandler {
}
}, settings, data, progressCallback, shouldAbortCallback);
startTransferAsClient(transfer);
- return transfer.getFuture();
+ return transfer;
}
ListenableFuture<byte[]> startReadTransferAsClient(int resourceId,
@@ -95,8 +99,8 @@ class TransferEventHandler {
TransferParameters parameters,
Consumer<TransferProgress> progressCallback,
BooleanSupplier shouldAbortCallback) {
- ReadTransfer transfer =
- new ReadTransfer(resourceId, desiredProtocolVersion, new TransferInterface() {
+ ReadTransfer transfer = new ReadTransfer(
+ resourceId, assignSessionId(), desiredProtocolVersion, new TransferInterface() {
@Override
Call.ClientStreaming<Chunk> getStream() throws ChannelOutputException {
if (readStream == null) {
@@ -111,19 +115,27 @@ class TransferEventHandler {
}
}, settings, parameters, progressCallback, shouldAbortCallback);
startTransferAsClient(transfer);
- return transfer.getFuture();
+ return transfer;
}
private void startTransferAsClient(Transfer<?> transfer) {
enqueueEvent(() -> {
- if (resourceIdToTransfer.containsKey(transfer.getResourceId())) {
- transfer.terminate(new TransferError("A transfer for resource ID "
- + transfer.getResourceId()
- + " is already in progress! Only one read/write transfer per resource is supported at a time",
- Status.ALREADY_EXISTS));
+ if (sessionIdToTransfer.containsKey(transfer.getSessionId())) {
+ throw new AssertionError("Duplicate session ID " + transfer.getSessionId());
+ }
+
+ // The v2 protocol supports multiple transfers for a single resource. For simplicity while
+ // supporting both protocols, only support a single transfer per resource.
+ if (legacyIdToSessionId.containsKey(transfer.getResourceId())) {
+ transfer.terminate(
+ new TransferError("A transfer for resource ID " + transfer.getResourceId()
+ + " is already in progress! Only one read/write transfer per resource is "
+ + "supported at a time",
+ Status.ALREADY_EXISTS));
return;
}
- resourceIdToTransfer.put(transfer.getResourceId(), transfer);
+ sessionIdToTransfer.put(transfer.getSessionId(), transfer);
+ legacyIdToSessionId.put(transfer.getResourceId(), transfer.getSessionId());
transfer.start();
});
}
@@ -159,7 +171,7 @@ class TransferEventHandler {
void stop() {
enqueueEvent(() -> {
logger.atFine().log("Terminating TransferEventHandler");
- resourceIdToTransfer.values().forEach(Transfer::handleTermination);
+ sessionIdToTransfer.values().forEach(Transfer::handleTermination);
processEvents = false;
});
}
@@ -175,6 +187,16 @@ class TransferEventHandler {
}
}
+ /** Generates the session ID to use for the next transfer. */
+ private int assignSessionId() {
+ return nextSessionId++;
+ }
+
+ /** Returns the session ID that will be used for the next transfer. */
+ final int getNextSessionIdForTest() {
+ return nextSessionId;
+ }
+
private void enqueueEvent(Event event) {
while (true) {
try {
@@ -199,14 +221,14 @@ class TransferEventHandler {
}
private void handleTimeouts() {
- for (Transfer<?> transfer : resourceIdToTransfer.values()) {
+ for (Transfer<?> transfer : sessionIdToTransfer.values()) {
transfer.handleTimeoutIfDeadlineExceeded();
}
}
private Instant getNextTimeout() {
Optional<Transfer<?>> transfer =
- resourceIdToTransfer.values().stream().min(Comparator.comparing(Transfer::getDeadline));
+ sessionIdToTransfer.values().stream().min(Comparator.comparing(Transfer::getDeadline));
return transfer.isPresent() ? transfer.get().getDeadline() : Transfer.NO_TIMEOUT;
}
@@ -228,22 +250,16 @@ class TransferEventHandler {
}
/**
- * Associates the transfer's session ID with its resource ID.
- *
- * Must be called on the transfer thread.
- */
- void assignSessionId(Transfer<?> transfer) {
- sessionToResourceId.put(transfer.getSessionId(), transfer.getResourceId());
- }
-
- /**
* Removes this transfer from the list of active transfers.
*
* Must be called on the transfer thread.
*/
+ // TODO(frolv): Investigate why this is occurring -- there shouldn't be any
+ // futures here.
+ @SuppressWarnings("FutureReturnValueIgnored")
void unregisterTransfer(Transfer<?> transfer) {
- resourceIdToTransfer.remove(transfer.getResourceId());
- sessionToResourceId.remove(transfer.getSessionId());
+ sessionIdToTransfer.remove(transfer.getSessionId());
+ legacyIdToSessionId.remove(transfer.getResourceId());
}
/**
@@ -263,25 +279,18 @@ class TransferEventHandler {
private abstract class ChunkHandler implements StreamObserver<Chunk> {
@Override
public final void onNext(Chunk chunkProto) {
- VersionedChunk chunk = VersionedChunk.fromMessage(chunkProto);
+ VersionedChunk chunk = VersionedChunk.fromMessage(chunkProto, legacyIdToSessionId);
enqueueEvent(() -> {
- Transfer<?> transfer = null;
- if (chunk.resourceId().isPresent()) {
- transfer = resourceIdToTransfer.get(chunk.resourceId().getAsInt());
- } else {
- Integer resourceId = sessionToResourceId.get(chunk.sessionId());
- if (resourceId != null) {
- transfer = resourceIdToTransfer.get(resourceId);
- }
- }
-
- if (transfer != null) {
- logger.atFinest().log("%s received chunk: %s", transfer, chunk);
- transfer.handleChunk(chunk);
- } else {
+ Transfer<?> transfer;
+ if (chunk.sessionId() == VersionedChunk.UNKNOWN_SESSION_ID
+ || (transfer = sessionIdToTransfer.get(chunk.sessionId())) == null) {
logger.atInfo().log("Ignoring unrecognized transfer chunk: %s", chunk);
+ return;
}
+
+ logger.atFinest().log("%s received chunk: %s", transfer, chunk);
+ transfer.handleChunk(chunk);
});
}
@@ -296,7 +305,7 @@ class TransferEventHandler {
resetStream();
// The transfers remove themselves from the Map during cleanup, iterate over a copied list.
- List<Transfer<?>> activeTransfers = new ArrayList<>(resourceIdToTransfer.values());
+ List<Transfer<?>> activeTransfers = new ArrayList<>(sessionIdToTransfer.values());
// FAILED_PRECONDITION indicates that the stream packet was not recognized as the stream is
// not open. This could occur if the server resets. Notify pending transfers that this has
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java
index f2c21b496..c11151fcd 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java
@@ -21,8 +21,8 @@ import dev.pigweed.pw_rpc.Service;
/** Provides a service definition for the pw_transfer service. */
public class TransferService {
private static final Service SERVICE = new Service("pw.transfer.Transfer",
- bidirectionalStreamingMethod("Read", Chunk.class, Chunk.class),
- bidirectionalStreamingMethod("Write", Chunk.class, Chunk.class));
+ bidirectionalStreamingMethod("Read", Chunk.parser(), Chunk.parser()),
+ bidirectionalStreamingMethod("Write", Chunk.parser(), Chunk.parser()));
public static Service get() {
return SERVICE;
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java
index 9fdfb4d1e..27261888b 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java
@@ -17,6 +17,7 @@ package dev.pigweed.pw_transfer;
import com.google.auto.value.AutoValue;
import com.google.protobuf.ByteString;
import dev.pigweed.pw_rpc.Status;
+import java.util.Map;
import java.util.OptionalInt;
import java.util.OptionalLong;
@@ -25,7 +26,8 @@ import java.util.OptionalLong;
*/
@AutoValue
abstract class VersionedChunk {
- public static final int UNASSIGNED_SESSION_ID = 0;
+ // Invalid session ID used for legacy chunks that do not map to a known session ID.
+ public static final int UNKNOWN_SESSION_ID = 0;
public abstract ProtocolVersion version();
@@ -49,9 +51,11 @@ abstract class VersionedChunk {
public abstract OptionalInt status();
+ public abstract OptionalInt desiredSessionId();
+
public static Builder builder() {
return new AutoValue_VersionedChunk.Builder()
- .setSessionId(UNASSIGNED_SESSION_ID)
+ .setSessionId(UNKNOWN_SESSION_ID)
.setOffset(0)
.setWindowEndOffset(0)
.setData(ByteString.EMPTY);
@@ -85,10 +89,12 @@ abstract class VersionedChunk {
abstract Builder setStatus(int statusCode);
+ abstract Builder setDesiredSessionId(int desiredSessionId);
+
public abstract VersionedChunk build();
}
- public static VersionedChunk fromMessage(Chunk chunk) {
+ public static VersionedChunk fromMessage(Chunk chunk, Map<Integer, Integer> legacyIdToSessionId) {
Builder builder = builder();
ProtocolVersion version;
@@ -119,7 +125,8 @@ abstract class VersionedChunk {
// For legacy chunks, use the transfer ID as both the resource and session IDs.
if (version == ProtocolVersion.LEGACY) {
- builder.setSessionId(chunk.getTransferId());
+ builder.setSessionId(
+ legacyIdToSessionId.getOrDefault(chunk.getTransferId(), UNKNOWN_SESSION_ID));
builder.setResourceId(chunk.getTransferId());
if (chunk.hasStatus()) {
builder.setType(Chunk.Type.COMPLETION);
@@ -128,6 +135,10 @@ abstract class VersionedChunk {
builder.setSessionId(chunk.getSessionId());
}
+ if (chunk.hasDesiredSessionId()) {
+ builder.setDesiredSessionId(chunk.getDesiredSessionId());
+ }
+
builder.setOffset((int) chunk.getOffset()).setData(chunk.getData());
if (chunk.hasResourceId()) {
@@ -154,8 +165,15 @@ abstract class VersionedChunk {
}
public static VersionedChunk.Builder createInitialChunk(
- ProtocolVersion desiredVersion, int resourceId) {
- return builder().setVersion(desiredVersion).setType(Chunk.Type.START).setResourceId(resourceId);
+ ProtocolVersion desiredVersion, int resourceId, int sessionId) {
+ VersionedChunk.Builder builder =
+ builder().setVersion(desiredVersion).setType(Chunk.Type.START).setResourceId(resourceId);
+
+ if (desiredVersion != ProtocolVersion.LEGACY) {
+ builder.setDesiredSessionId(sessionId);
+ }
+
+ return builder;
}
public Chunk toMessage() {
@@ -165,19 +183,24 @@ abstract class VersionedChunk {
.setWindowEndOffset(windowEndOffset())
.setData(data());
- resourceId().ifPresent(chunk::setResourceId);
remainingBytes().ifPresent(chunk::setRemainingBytes);
maxChunkSizeBytes().ifPresent(chunk::setMaxChunkSizeBytes);
minDelayMicroseconds().ifPresent(chunk::setMinDelayMicroseconds);
status().ifPresent(chunk::setStatus);
+ desiredSessionId().ifPresent(chunk::setDesiredSessionId);
+
+ // The resourceId is only needed for START chunks.
+ if (type() == Chunk.Type.START) {
+ chunk.setResourceId(resourceId().getAsInt()); // resourceId must be set for start chunks
+ }
// session_id did not exist in the legacy protocol, so don't send it.
- if (version() != ProtocolVersion.LEGACY && sessionId() != UNASSIGNED_SESSION_ID) {
+ if (version() != ProtocolVersion.LEGACY && sessionId() != UNKNOWN_SESSION_ID) {
chunk.setSessionId(sessionId());
}
if (shouldEncodeLegacyFields()) {
- chunk.setTransferId(resourceId().orElse(sessionId()));
+ chunk.setTransferId(resourceId().getAsInt()); // resourceId must be set for legacy transfers
if (chunk.getWindowEndOffset() != 0) {
chunk.setPendingBytes(chunk.getWindowEndOffset() - offset());
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java
index daf5213f7..d72149f5f 100644
--- a/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java
@@ -38,6 +38,7 @@ class WriteTransfer extends Transfer<Void> {
private final byte[] data;
protected WriteTransfer(int resourceId,
+ int sessionId,
ProtocolVersion desiredProtocolVersion,
TransferInterface transferManager,
TransferTimeoutSettings timeoutSettings,
@@ -45,6 +46,7 @@ class WriteTransfer extends Transfer<Void> {
Consumer<TransferProgress> progressCallback,
BooleanSupplier shouldAbortCallback) {
super(resourceId,
+ sessionId,
desiredProtocolVersion,
transferManager,
timeoutSettings,
@@ -124,7 +126,7 @@ class WriteTransfer extends Transfer<Void> {
@Override
void setFutureResult() {
updateProgress(data.length, data.length, data.length);
- getFuture().set(null);
+ set(null);
}
private void updateTransferParameters(VersionedChunk chunk) throws TransferAbortedException {
diff --git a/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java b/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
index 3a61b362a..898f0e9b0 100644
--- a/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
+++ b/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
@@ -92,11 +92,11 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
- assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.LEGACY));
+ assertThat(lastChunks()).containsExactly(initialLegacyReadChunk(1));
receiveReadServerError(Status.FAILED_PRECONDITION);
- assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.LEGACY));
+ assertThat(lastChunks()).containsExactly(initialLegacyReadChunk(1));
receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
.setOffset(0)
@@ -112,7 +112,7 @@ public final class TransferClientTest {
TransferParameters params = TransferParameters.create(50, 50, 0);
ListenableFuture<byte[]> future = transferClient.read(1, params);
- assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.LEGACY, params));
+ assertThat(lastChunks()).containsExactly(initialLegacyReadChunk(1, params));
receiveReadChunks(legacyDataChunk(1, TEST_DATA_100B, 0, 50));
@@ -140,7 +140,7 @@ public final class TransferClientTest {
receiveReadServerError(Status.FAILED_PRECONDITION);
}
- Chunk initialChunk = initialReadChunk(1, ProtocolVersion.LEGACY);
+ Chunk initialChunk = initialLegacyReadChunk(1);
assertThat(lastChunks())
.containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
@@ -207,7 +207,7 @@ public final class TransferClientTest {
TransferParameters params = TransferParameters.create(3, 2, 1);
ListenableFuture<byte[]> future = transferClient.read(99, params);
- assertThat(lastChunks()).containsExactly(initialReadChunk(99, ProtocolVersion.LEGACY, params));
+ assertThat(lastChunks()).containsExactly(initialLegacyReadChunk(99, params));
assertThat(future.cancel(true)).isTrue();
}
@@ -216,7 +216,7 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
- assertThat(lastChunks()).containsExactly(initialReadChunk(123, ProtocolVersion.LEGACY));
+ assertThat(lastChunks()).containsExactly(initialLegacyReadChunk(123));
receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123)
.setOffset(0)
@@ -291,7 +291,7 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
- assertThat(lastChunks()).containsExactly(initialReadChunk(123, ProtocolVersion.LEGACY));
+ assertThat(lastChunks()).containsExactly(initialLegacyReadChunk(123));
receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(30, 50)));
@@ -448,9 +448,8 @@ public final class TransferClientTest {
// read should have retried sending the transfer parameters 2 times, for a total of 3
assertThat(lastChunks())
- .containsExactly(initialReadChunk(123, ProtocolVersion.LEGACY),
- initialReadChunk(123, ProtocolVersion.LEGACY),
- initialReadChunk(123, ProtocolVersion.LEGACY));
+ .containsExactly(
+ initialLegacyReadChunk(123), initialLegacyReadChunk(123), initialLegacyReadChunk(123));
}
@Test
@@ -491,13 +490,11 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()));
+ assertThat(lastChunks()).containsExactly(initialLegacyWriteChunk(2, TEST_DATA_SHORT.size()));
receiveWriteServerError(Status.FAILED_PRECONDITION);
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()));
+ assertThat(lastChunks()).containsExactly(initialLegacyWriteChunk(2, TEST_DATA_SHORT.size()));
receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
.setOffset(0)
@@ -519,7 +516,7 @@ public final class TransferClientTest {
.setMaxChunkSizeBytes(50));
assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ .containsExactly(initialLegacyWriteChunk(2, TEST_DATA_100B.size()),
legacyDataChunk(2, TEST_DATA_100B, 0, 50));
receiveWriteServerError(Status.FAILED_PRECONDITION);
@@ -538,7 +535,7 @@ public final class TransferClientTest {
receiveWriteServerError(Status.FAILED_PRECONDITION);
}
- Chunk initialChunk = initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size());
+ Chunk initialChunk = initialLegacyWriteChunk(2, TEST_DATA_SHORT.size());
assertThat(lastChunks())
.containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
@@ -559,7 +556,7 @@ public final class TransferClientTest {
receiveWriteChunks(legacyFinalChunk(2, Status.OK));
- assertThat(lastChunks()).containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, 0));
+ assertThat(lastChunks()).containsExactly(initialLegacyWriteChunk(2, 0));
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
}
@@ -569,8 +566,7 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()));
+ assertThat(lastChunks()).containsExactly(initialLegacyWriteChunk(123, TEST_DATA_100B.size()));
receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
.setOffset(0)
@@ -615,8 +611,7 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()));
+ assertThat(lastChunks()).containsExactly(initialLegacyWriteChunk(123, TEST_DATA_100B.size()));
receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
.setOffset(0)
@@ -664,8 +659,7 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()));
+ assertThat(lastChunks()).containsExactly(initialLegacyWriteChunk(123, TEST_DATA_100B.size()));
receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
.setOffset(0)
@@ -734,7 +728,7 @@ public final class TransferClientTest {
.setMaxChunkSizeBytes(25));
assertThat(lastChunks())
- .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ .containsExactly(initialLegacyWriteChunk(123, TEST_DATA_100B.size()),
newLegacyChunk(Chunk.Type.DATA, 123).setOffset(100).setRemainingBytes(0).build());
assertThat(future.isDone()).isFalse();
}
@@ -800,7 +794,7 @@ public final class TransferClientTest {
.setMaxChunkSizeBytes(25));
assertThat(lastChunks())
- .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ .containsExactly(initialLegacyWriteChunk(123, TEST_DATA_100B.size()),
legacyFinalChunk(123, Status.OUT_OF_RANGE));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
@@ -874,10 +868,9 @@ public final class TransferClientTest {
// Client should have resent the last chunk (the initial chunk in this case) for each timeout.
assertThat(lastChunks())
- .containsExactly(
- initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()), // initial
- initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()), // retry 1
- initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size())); // retry 2
+ .containsExactly(initialLegacyWriteChunk(123, TEST_DATA_SHORT.size()), // initial
+ initialLegacyWriteChunk(123, TEST_DATA_SHORT.size()), // retry 1
+ initialLegacyWriteChunk(123, TEST_DATA_SHORT.size())); // retry 2
}
@Test
@@ -901,8 +894,7 @@ public final class TransferClientTest {
.setRemainingBytes(0)
.build();
assertThat(lastChunks())
- .containsExactly(
- initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()), // initial
+ .containsExactly(initialLegacyWriteChunk(123, TEST_DATA_SHORT.size()), // initial
data, // data chunk
data, // retry 1
data); // retry 2
@@ -944,7 +936,7 @@ public final class TransferClientTest {
assertThat(lastChunks())
.containsExactly(
// initial
- initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ initialLegacyWriteChunk(123, TEST_DATA_100B.size()),
// after 2, receive parameters: 40 from 0 by 20
legacyDataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
legacyDataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
@@ -966,29 +958,33 @@ public final class TransferClientTest {
public void read_singleChunk_successful() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(3, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
assertThat(future.isDone()).isFalse();
- assertThat(lastChunks()).containsExactly(initialReadChunk(3, ProtocolVersion.VERSION_TWO));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
- receiveReadChunks(newChunk(Chunk.Type.START_ACK, 321)
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(3)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
- assertThat(lastChunks()).containsExactly(readStartAckConfirmation(321, TRANSFER_PARAMETERS));
+ assertThat(lastChunks())
+ .containsExactly(readStartAckConfirmation(transfer.getSessionId(), TRANSFER_PARAMETERS));
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 321).setOffset(0).setData(TEST_DATA_SHORT).setRemainingBytes(0));
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
assertThat(lastChunks())
.containsExactly(Chunk.newBuilder()
.setType(Chunk.Type.COMPLETION)
- .setSessionId(321)
+ .setSessionId(transfer.getSessionId())
.setStatus(Status.OK.ordinal())
.build());
assertThat(future.isDone()).isFalse();
- receiveReadChunks(newChunk(Chunk.Type.COMPLETION_ACK, 321));
+ receiveReadChunks(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()));
assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
}
@@ -997,9 +993,10 @@ public final class TransferClientTest {
public void read_requestV2ReceiveLegacy() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
assertThat(future.isDone()).isFalse();
- assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
.setOffset(0)
@@ -1016,19 +1013,21 @@ public final class TransferClientTest {
public void read_failedPreconditionError_retriesInitialPacket() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
for (int i = 0; i < MAX_RETRIES; ++i) {
receiveReadServerError(Status.FAILED_PRECONDITION);
- assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
}
- receiveReadChunks(newChunk(Chunk.Type.START_ACK, 54321)
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(1)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
- assertThat(lastChunks()).containsExactly(readStartAckConfirmation(54321, TRANSFER_PARAMETERS));
+ assertThat(lastChunks())
+ .containsExactly(readStartAckConfirmation(transfer.getSessionId(), TRANSFER_PARAMETERS));
}
@Test
@@ -1036,11 +1035,11 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
TransferParameters params = TransferParameters.create(50, 50, 0);
ListenableFuture<byte[]> future = transferClient.read(1, params);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO, params));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
- receiveReadChunks(newChunk(Chunk.Type.START_ACK, 555)
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(1)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
@@ -1056,20 +1055,21 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
TransferParameters params = TransferParameters.create(50, 50, 0);
ListenableFuture<byte[]> future = transferClient.read(1, params);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO, params));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
- receiveReadChunks(newChunk(Chunk.Type.START_ACK, 555)
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(1)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
- assertThat(lastChunks()).containsExactly(readStartAckConfirmation(555, params));
+ assertThat(lastChunks())
+ .containsExactly(readStartAckConfirmation(transfer.getSessionId(), params));
- receiveReadChunks(dataChunk(555, TEST_DATA_100B, 0, 50));
+ receiveReadChunks(dataChunk(transfer.getSessionId(), TEST_DATA_100B, 0, 50));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 555)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(50)
.setWindowEndOffset(100)
.setMaxChunkSizeBytes(50)
@@ -1086,12 +1086,13 @@ public final class TransferClientTest {
public void read_failedPreconditionErrorMaxRetriesTimes_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
for (int i = 0; i < MAX_RETRIES; ++i) {
receiveReadServerError(Status.FAILED_PRECONDITION);
}
- Chunk initialChunk = initialReadChunk(1, ProtocolVersion.VERSION_TWO);
+ Chunk initialChunk = initialReadChunk(transfer);
assertThat(lastChunks())
.containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
@@ -1108,23 +1109,38 @@ public final class TransferClientTest {
public void read_singleChunk_ignoresUnknownIdOrWriteChunks() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(1);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
assertThat(future.isDone()).isFalse();
- performReadStartHandshake(1, 99);
+ performReadStartHandshake(transfer);
- receiveReadChunks(finalChunk(2, Status.OK),
- newChunk(Chunk.Type.DATA, 1).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0),
- newChunk(Chunk.Type.DATA, 3).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0));
- receiveWriteChunks(finalChunk(99, Status.INVALID_ARGUMENT),
- newChunk(Chunk.Type.DATA, 99).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0),
- newChunk(Chunk.Type.DATA, 2).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0));
+ receiveReadChunks(finalChunk(transfer.getSessionId() + 1, Status.OK),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId() + 2)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId() + 3)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0));
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.INVALID_ARGUMENT),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId() + 1)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0));
assertThat(future.isDone()).isFalse();
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 99).setOffset(0).setData(TEST_DATA_SHORT).setRemainingBytes(0));
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
- performReadCompletionHandshake(99, Status.OK);
+ performReadCompletionHandshake(transfer.getSessionId(), Status.OK);
assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
}
@@ -1133,10 +1149,11 @@ public final class TransferClientTest {
public void read_empty() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(2);
- performReadStartHandshake(2, 5678);
- receiveReadChunks(newChunk(Chunk.Type.DATA, 5678).setRemainingBytes(0));
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
+ performReadStartHandshake(transfer);
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId()).setRemainingBytes(0));
- performReadCompletionHandshake(5678, Status.OK);
+ performReadCompletionHandshake(transfer.getSessionId(), Status.OK);
assertThat(future.get()).isEqualTo(new byte[] {});
}
@@ -1146,9 +1163,9 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
TransferParameters params = TransferParameters.create(3, 2, 1);
ListenableFuture<byte[]> future = transferClient.read(99, params);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialReadChunk(99, ProtocolVersion.VERSION_TWO, params));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
assertThat(future.cancel(true)).isTrue();
}
@@ -1156,33 +1173,39 @@ public final class TransferClientTest {
public void read_severalChunks() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(7, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(7, 123, TRANSFER_PARAMETERS);
+ performReadStartHandshake(transfer);
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 20)).setRemainingBytes(70),
- newChunk(Chunk.Type.DATA, 123).setOffset(20).setData(range(20, 40)));
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(range(0, 20))
+ .setRemainingBytes(70),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(20).setData(range(20, 40)));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
.setOffset(40)
.setMaxChunkSizeBytes(30)
.setWindowEndOffset(90)
.build());
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(40).setData(range(40, 70)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(40).setData(range(40, 70)));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
.setOffset(70)
.setMaxChunkSizeBytes(30)
.setWindowEndOffset(120)
.build());
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 123).setOffset(70).setData(range(70, 100)).setRemainingBytes(0));
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(70)
+ .setData(range(70, 100))
+ .setRemainingBytes(0));
- performReadCompletionHandshake(123, Status.OK);
+ performReadCompletionHandshake(transfer.getSessionId(), Status.OK);
assertThat(future.get()).isEqualTo(TEST_DATA_100B.toByteArray());
}
@@ -1192,113 +1215,115 @@ public final class TransferClientTest {
createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
TransferParameters params = TransferParameters.create(50, 10, 0);
+ final int id = transferClient.getNextSessionIdForTest();
+
// Handshake
enqueueReadChunks(2, // Wait for read RPC open & START packet
- newChunk(Chunk.Type.START_ACK, 99)
+ newChunk(Chunk.Type.START_ACK, id)
.setResourceId(7)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
enqueueReadChunks(1, // Ignore the first START_ACK_CONFIRMATION
- newChunk(Chunk.Type.START_ACK, 99)
+ newChunk(Chunk.Type.START_ACK, id)
.setResourceId(7)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
// Window 1: server waits for START_ACK_CONFIRMATION, drops 2nd packet
enqueueReadChunks(1,
- newChunk(Chunk.Type.DATA, 99).setOffset(0).setData(range(0, 10)),
- newChunk(Chunk.Type.DATA, 99).setOffset(20).setData(range(20, 30)),
- newChunk(Chunk.Type.DATA, 99).setOffset(30).setData(range(30, 40)),
- newChunk(Chunk.Type.DATA, 99).setOffset(40).setData(range(40, 50)));
+ newChunk(Chunk.Type.DATA, id).setOffset(0).setData(range(0, 10)),
+ newChunk(Chunk.Type.DATA, id).setOffset(20).setData(range(20, 30)),
+ newChunk(Chunk.Type.DATA, id).setOffset(30).setData(range(30, 40)),
+ newChunk(Chunk.Type.DATA, id).setOffset(40).setData(range(40, 50)));
// Window 2: server waits for retransmit, drops 1st packet
enqueueReadChunks(1,
- newChunk(Chunk.Type.DATA, 99).setOffset(20).setData(range(20, 30)),
- newChunk(Chunk.Type.DATA, 99).setOffset(30).setData(range(30, 40)),
- newChunk(Chunk.Type.DATA, 99).setOffset(40).setData(range(40, 50)),
- newChunk(Chunk.Type.DATA, 99).setOffset(50).setData(range(50, 60)));
+ newChunk(Chunk.Type.DATA, id).setOffset(20).setData(range(20, 30)),
+ newChunk(Chunk.Type.DATA, id).setOffset(30).setData(range(30, 40)),
+ newChunk(Chunk.Type.DATA, id).setOffset(40).setData(range(40, 50)),
+ newChunk(Chunk.Type.DATA, id).setOffset(50).setData(range(50, 60)));
// Window 3: server waits for retransmit, drops last packet
enqueueReadChunks(1,
- newChunk(Chunk.Type.DATA, 99).setOffset(10).setData(range(10, 20)),
- newChunk(Chunk.Type.DATA, 99).setOffset(20).setData(range(20, 30)),
- newChunk(Chunk.Type.DATA, 99).setOffset(30).setData(range(30, 40)),
- newChunk(Chunk.Type.DATA, 99).setOffset(40).setData(range(40, 50)));
+ newChunk(Chunk.Type.DATA, id).setOffset(10).setData(range(10, 20)),
+ newChunk(Chunk.Type.DATA, id).setOffset(20).setData(range(20, 30)),
+ newChunk(Chunk.Type.DATA, id).setOffset(30).setData(range(30, 40)),
+ newChunk(Chunk.Type.DATA, id).setOffset(40).setData(range(40, 50)));
// Window 4: server waits for continue and retransmit, normal window.
enqueueReadChunks(2,
- newChunk(Chunk.Type.DATA, 99).setOffset(50).setData(range(50, 60)),
- newChunk(Chunk.Type.DATA, 99).setOffset(60).setData(range(60, 70)),
- newChunk(Chunk.Type.DATA, 99).setOffset(70).setData(range(70, 80)),
- newChunk(Chunk.Type.DATA, 99).setOffset(80).setData(range(80, 90)),
- newChunk(Chunk.Type.DATA, 99).setOffset(90).setData(range(90, 100)));
+ newChunk(Chunk.Type.DATA, id).setOffset(50).setData(range(50, 60)),
+ newChunk(Chunk.Type.DATA, id).setOffset(60).setData(range(60, 70)),
+ newChunk(Chunk.Type.DATA, id).setOffset(70).setData(range(70, 80)),
+ newChunk(Chunk.Type.DATA, id).setOffset(80).setData(range(80, 90)),
+ newChunk(Chunk.Type.DATA, id).setOffset(90).setData(range(90, 100)));
enqueueReadChunks(2, // Ignore continue and retransmit chunks, retry last packet in window
- newChunk(Chunk.Type.DATA, 99).setOffset(90).setData(range(90, 100)),
- newChunk(Chunk.Type.DATA, 99).setOffset(90).setData(range(90, 100)));
+ newChunk(Chunk.Type.DATA, id).setOffset(90).setData(range(90, 100)),
+ newChunk(Chunk.Type.DATA, id).setOffset(90).setData(range(90, 100)));
// Window 5: Final packet
enqueueReadChunks(2, // Receive two retries, then send final packet
- newChunk(Chunk.Type.DATA, 99).setOffset(100).setData(range(100, 110)).setRemainingBytes(0));
+ newChunk(Chunk.Type.DATA, id).setOffset(100).setData(range(100, 110)).setRemainingBytes(0));
enqueueReadChunks(1, // Ignore first COMPLETION packet
- newChunk(Chunk.Type.DATA, 99).setOffset(100).setData(range(100, 110)).setRemainingBytes(0));
- enqueueReadChunks(1, newChunk(Chunk.Type.COMPLETION_ACK, 99));
+ newChunk(Chunk.Type.DATA, id).setOffset(100).setData(range(100, 110)).setRemainingBytes(0));
+ enqueueReadChunks(1, newChunk(Chunk.Type.COMPLETION_ACK, id));
ListenableFuture<byte[]> future = transferClient.read(7, params);
- // assertThat(future.get()).isEqualTo(range(0, 110).toByteArray());
- while (!future.isDone()) {
- }
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
+ assertThat(transfer.getSessionId()).isEqualTo(id);
+ assertThat(future.get()).isEqualTo(range(0, 110).toByteArray());
assertThat(lastChunks())
.containsExactly(
// Handshake
- initialReadChunk(7, ProtocolVersion.VERSION_TWO, params),
- readStartAckConfirmation(99, params),
- readStartAckConfirmation(99, params),
+ initialReadChunk(transfer),
+ readStartAckConfirmation(id, params),
+ readStartAckConfirmation(id, params),
// Window 1: send one transfer parameters update after the drop
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(10)
.setWindowEndOffset(60)
.setMaxChunkSizeBytes(10)
.build(),
// Window 2: send one transfer parameters update after the drop
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(10)
.setWindowEndOffset(60)
.setMaxChunkSizeBytes(10)
.build(),
// Window 3: send one transfer parameters update after the drop, then continue packet
- newChunk(Chunk.Type.PARAMETERS_CONTINUE, 99) // Not seen by server
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, id) // Not seen by server
.setOffset(40)
.setWindowEndOffset(90)
.setMaxChunkSizeBytes(10)
.build(),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent after timeout
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id) // Sent after timeout
.setOffset(50)
.setWindowEndOffset(100)
.setMaxChunkSizeBytes(10)
.build(),
// Window 4: send one transfer parameters update after the drop, then continue packet
- newChunk(Chunk.Type.PARAMETERS_CONTINUE, 99) // Ignored by server
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, id) // Ignored by server
.setOffset(80)
.setWindowEndOffset(130)
.setMaxChunkSizeBytes(10)
.build(),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent after last packet
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id) // Sent after last packet
.setOffset(100)
.setWindowEndOffset(150)
.setMaxChunkSizeBytes(10)
.build(),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent due to repeated packet
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id) // Sent due to repeated packet
.setOffset(100)
.setWindowEndOffset(150)
.setMaxChunkSizeBytes(10)
.build(),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent due to repeated packet
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id) // Sent due to repeated packet
.setOffset(100)
.setWindowEndOffset(150)
.setMaxChunkSizeBytes(10)
.build(),
// Window 5: final packet and closing handshake
- newChunk(Chunk.Type.COMPLETION, 99).setStatus(Status.OK.ordinal()).build(),
- newChunk(Chunk.Type.COMPLETION, 99).setStatus(Status.OK.ordinal()).build());
+ newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.ordinal()).build(),
+ newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.ordinal()).build());
}
@Test
@@ -1306,21 +1331,31 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future =
transferClient.read(123, TRANSFER_PARAMETERS, progressCallback);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(123, 123, TRANSFER_PARAMETERS);
+ performReadStartHandshake(transfer);
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)),
- newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)),
- newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 60)).setRemainingBytes(5),
- newChunk(Chunk.Type.DATA, 123).setOffset(60).setData(range(60, 70)),
- newChunk(Chunk.Type.DATA, 123).setOffset(70).setData(range(70, 80)).setRemainingBytes(20),
- newChunk(Chunk.Type.DATA, 123).setOffset(90).setData(range(90, 100)),
- newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(0).setData(range(0, 30)),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(30).setData(range(30, 50)),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(50)
+ .setData(range(50, 60))
+ .setRemainingBytes(5),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(60).setData(range(60, 70)),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(70)
+ .setData(range(70, 80))
+ .setRemainingBytes(20),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(90).setData(range(90, 100)),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(0).setData(range(0, 30)));
lastChunks(); // Discard chunks; no need to inspect for this test
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 123).setOffset(80).setData(range(80, 100)).setRemainingBytes(0));
- performReadCompletionHandshake(123, Status.OK);
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0));
+ performReadCompletionHandshake(transfer.getSessionId(), Status.OK);
verify(progressCallback, times(6)).accept(progress.capture());
assertThat(progress.getAllValues())
@@ -1338,51 +1373,59 @@ public final class TransferClientTest {
public void read_rewindWhenPacketsSkipped() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(123, 123, TRANSFER_PARAMETERS);
+ performReadStartHandshake(transfer);
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(30, 50)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(50).setData(range(30, 50)));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setWindowEndOffset(50)
.setMaxChunkSizeBytes(30)
.setOffset(0)
.build());
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)),
- newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(0).setData(range(0, 30)),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(30).setData(range(30, 50)));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
.setOffset(30)
.setWindowEndOffset(80)
.setMaxChunkSizeBytes(30)
.build());
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 123).setOffset(80).setData(range(80, 100)).setRemainingBytes(0));
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(50)
.setWindowEndOffset(100)
.setMaxChunkSizeBytes(30)
.build());
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 80)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(50).setData(range(50, 80)));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
.setOffset(80)
.setWindowEndOffset(130)
.setMaxChunkSizeBytes(30)
.build());
- receiveReadChunks(
- newChunk(Chunk.Type.DATA, 123).setOffset(80).setData(range(80, 100)).setRemainingBytes(0));
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0));
- performReadCompletionHandshake(123, Status.OK);
+ performReadCompletionHandshake(transfer.getSessionId(), Status.OK);
assertThat(future.get()).isEqualTo(TEST_DATA_100B.toByteArray());
}
@@ -1392,15 +1435,16 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
for (int i = 0; i < 3; ++i) {
ListenableFuture<byte[]> future = transferClient.read(1);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(1, 100 + i);
+ performReadStartHandshake(transfer);
- receiveReadChunks(newChunk(Chunk.Type.DATA, 100 + i)
+ receiveReadChunks(newChunk(Chunk.Type.DATA, transfer.getSessionId())
.setOffset(0)
.setData(TEST_DATA_SHORT)
.setRemainingBytes(0));
- performReadCompletionHandshake(100 + i, Status.OK);
+ performReadCompletionHandshake(transfer.getSessionId(), Status.OK);
assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
}
@@ -1435,15 +1479,18 @@ public final class TransferClientTest {
public void read_sendErrorOnLaterPacket_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(1024, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(1024, 123, TRANSFER_PARAMETERS);
+ performReadStartHandshake(transfer);
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 20)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(0).setData(range(0, 20)));
ChannelOutputException exception = new ChannelOutputException("blah");
rpcClient.setChannelOutputException(exception);
- receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(20).setData(range(20, 50)));
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, transfer.getSessionId()).setOffset(20).setData(range(20, 50)));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
@@ -1454,21 +1501,22 @@ public final class TransferClientTest {
public void read_cancelFuture_abortsTransfer() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(1, 123, TRANSFER_PARAMETERS);
+ performReadStartHandshake(transfer);
assertThat(future.cancel(true)).isTrue();
- assertThat(lastChunks()).contains(finalChunk(123, Status.CANCELLED));
+ assertThat(lastChunks()).contains(finalChunk(transfer.getSessionId(), Status.CANCELLED));
}
@Test
public void read_immediateTransferProtocolError_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(123);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- // Resource ID will be set since session ID hasn't been assigned yet.
- receiveReadChunks(newChunk(Chunk.Type.COMPLETION, VersionedChunk.UNASSIGNED_SESSION_ID)
+ receiveReadChunks(newChunk(Chunk.Type.COMPLETION, transfer.getSessionId())
.setResourceId(123)
.setStatus(Status.ALREADY_EXISTS.ordinal()));
@@ -1482,10 +1530,11 @@ public final class TransferClientTest {
public void read_laterTransferProtocolError_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(123);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- performReadStartHandshake(123, 514);
+ performReadStartHandshake(transfer);
- receiveReadChunks(finalChunk(514, Status.ALREADY_EXISTS));
+ receiveReadChunks(finalChunk(transfer.getSessionId(), Status.ALREADY_EXISTS));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
@@ -1508,14 +1557,16 @@ public final class TransferClientTest {
public void read_serverRespondsWithUnknownVersion_invalidArgument() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(2, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialReadChunk(2, ProtocolVersion.VERSION_TWO, TRANSFER_PARAMETERS));
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
- receiveReadChunks(
- newChunk(Chunk.Type.START_ACK, 99).setResourceId(2).setProtocolVersion(600613));
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
+ .setResourceId(2)
+ .setProtocolVersion(600613));
- assertThat(lastChunks()).containsExactly(finalChunk(99, Status.INVALID_ARGUMENT));
+ assertThat(lastChunks())
+ .containsExactly(finalChunk(transfer.getSessionId(), Status.INVALID_ARGUMENT));
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
@@ -1525,6 +1576,7 @@ public final class TransferClientTest {
public void read_timeout() {
createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+ ReadTransfer transfer = transferClient.getReadTransferForTest(future);
// Call future.get() without sending any server-side packets.
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
@@ -1532,45 +1584,65 @@ public final class TransferClientTest {
// read should have retried sending the transfer parameters 2 times, for a total of 3
assertThat(lastChunks())
- .containsExactly(initialReadChunk(123, ProtocolVersion.VERSION_TWO),
- initialReadChunk(123, ProtocolVersion.VERSION_TWO),
- initialReadChunk(123, ProtocolVersion.VERSION_TWO));
+ .containsExactly(
+ initialReadChunk(transfer), initialReadChunk(transfer), initialReadChunk(transfer));
+ }
+
+ @Test
+ public void read_generatesUniqueSessionIds() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+
+ int sessionId1 = transferClient.getNextSessionIdForTest();
+ ReadTransfer transfer1 = transferClient.getReadTransferForTest(transferClient.read(123));
+ assertThat(sessionId1).isEqualTo(transfer1.getSessionId());
+
+ int sessionId2 = transferClient.getNextSessionIdForTest();
+ ReadTransfer transfer2 = transferClient.getReadTransferForTest(transferClient.read(456));
+ assertThat(sessionId2).isEqualTo(transfer2.getSessionId());
+
+ int sessionId3 = transferClient.getNextSessionIdForTest();
+ ReadTransfer transfer3 = transferClient.getReadTransferForTest(transferClient.read(789));
+ assertThat(sessionId3).isEqualTo(transfer3.getSessionId());
+
+ assertThat(sessionId1).isNotEqualTo(sessionId2);
+ assertThat(sessionId1).isNotEqualTo(sessionId3);
}
@Test
public void write_singleChunk() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
// Do the start handshake (equivalent to performWriteStartHandshake()).
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()));
- receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(2)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, transfer.getSessionId())
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(TEST_DATA_SHORT.size())
.build());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(1024)
.setMaxChunkSizeBytes(128));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 123)
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
.setOffset(0)
.setData(TEST_DATA_SHORT)
.setRemainingBytes(0)
.build());
- receiveWriteChunks(finalChunk(123, Status.OK));
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.OK));
- assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()).build());
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
}
@@ -1579,9 +1651,9 @@ public final class TransferClientTest {
public void write_requestV2ReceiveLegacy() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()));
receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
.setOffset(0)
@@ -1596,10 +1668,11 @@ public final class TransferClientTest {
public void write_platformTransferDisabled_aborted() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
assertThat(future.isDone()).isFalse();
shouldAbortFlag = true;
- receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 3).setResourceId(2));
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId()).setResourceId(2));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
@@ -1610,23 +1683,21 @@ public final class TransferClientTest {
public void write_failedPreconditionError_retriesInitialPacket() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()));
for (int i = 0; i < MAX_RETRIES; ++i) {
receiveWriteServerError(Status.FAILED_PRECONDITION);
- assertThat(lastChunks())
- .containsExactly(
- initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()));
}
- receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 54321)
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(2)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, 54321)
+ .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, transfer.getSessionId())
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(TEST_DATA_SHORT.size())
.build());
@@ -1636,11 +1707,11 @@ public final class TransferClientTest {
public void write_failedPreconditionError_abortsAfterInitialPacket() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()));
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, TEST_DATA_100B.size()));
- receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 4)
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
.setResourceId(2)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
@@ -1655,12 +1726,13 @@ public final class TransferClientTest {
public void write_failedPreconditionErrorMaxRetriesTimes_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
for (int i = 0; i < MAX_RETRIES; ++i) {
receiveWriteServerError(Status.FAILED_PRECONDITION);
}
- Chunk initialChunk = initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size());
+ Chunk initialChunk = initialWriteChunk(transfer, TEST_DATA_SHORT.size());
assertThat(lastChunks())
.containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
@@ -1677,12 +1749,14 @@ public final class TransferClientTest {
public void write_empty() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, new byte[] {});
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(2, 123, 0);
+ performWriteStartHandshake(transfer, 0);
- receiveWriteChunks(finalChunk(123, Status.OK));
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.OK));
- assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()).build());
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
}
@@ -1691,34 +1765,47 @@ public final class TransferClientTest {
public void write_severalChunks() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(500, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(500, 123, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(50)
.setMaxChunkSizeBytes(30)
.setMinDelayMicroseconds(1));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)).build(),
- newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(range(0, 30))
+ .build(),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(30)
+ .setData(range(30, 50))
+ .build());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(50)
.setWindowEndOffset(90)
.setMaxChunkSizeBytes(25));
assertThat(lastChunks())
- .containsExactly(
- newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 75)).build(),
- newChunk(Chunk.Type.DATA, 123).setOffset(75).setData(range(75, 90)).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(50)
+ .setData(range(50, 75))
+ .build(),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(75)
+ .setData(range(75, 90))
+ .build());
- receiveWriteChunks(
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(90).setWindowEndOffset(140));
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
+ .setOffset(90)
+ .setWindowEndOffset(140));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 123)
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
.setOffset(90)
.setData(range(90, 100))
.setRemainingBytes(0)
@@ -1726,9 +1813,10 @@ public final class TransferClientTest {
assertThat(future.isDone()).isFalse();
- receiveWriteChunks(finalChunk(123, Status.OK));
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.OK));
- assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()).build());
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
}
@@ -1737,32 +1825,43 @@ public final class TransferClientTest {
public void write_parametersContinue() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(321, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(321, 123, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(50)
.setMaxChunkSizeBytes(30)
.setMinDelayMicroseconds(1));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)).build(),
- newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(range(0, 30))
+ .build(),
+ newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(30)
+ .setData(range(30, 50))
+ .build());
- receiveWriteChunks(
- newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(30).setWindowEndOffset(80));
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
+ .setOffset(30)
+ .setWindowEndOffset(80));
// Transfer doesn't roll back to offset 30 but instead continues sending up to 80.
assertThat(lastChunks())
- .containsExactly(
- newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 80)).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(50)
+ .setData(range(50, 80))
+ .build());
- receiveWriteChunks(
- newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(80).setWindowEndOffset(130));
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
+ .setOffset(80)
+ .setWindowEndOffset(130));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 123)
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
.setOffset(80)
.setData(range(80, 100))
.setRemainingBytes(0)
@@ -1770,9 +1869,10 @@ public final class TransferClientTest {
assertThat(future.isDone()).isFalse();
- receiveWriteChunks(finalChunk(123, Status.OK));
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.OK));
- assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()).build());
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
}
@@ -1781,33 +1881,42 @@ public final class TransferClientTest {
public void write_continuePacketWithWindowEndBeforeOffsetIsIgnored() throws Exception {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(123, 555, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 555)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(90)
.setMaxChunkSizeBytes(90)
.setMinDelayMicroseconds(1));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 555).setOffset(0).setData(range(0, 90)).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(0)
+ .setData(range(0, 90))
+ .build());
receiveWriteChunks(
// This stale packet with a window end before the offset should be ignored.
- newChunk(Chunk.Type.PARAMETERS_CONTINUE, 555).setOffset(25).setWindowEndOffset(50),
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
+ .setOffset(25)
+ .setWindowEndOffset(50),
// Start from an arbitrary offset before the current, but extend the window to the end.
- newChunk(Chunk.Type.PARAMETERS_CONTINUE, 555).setOffset(80).setWindowEndOffset(100));
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, transfer.getSessionId())
+ .setOffset(80)
+ .setWindowEndOffset(100));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.DATA, 555)
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
.setOffset(90)
.setData(range(90, 100))
.setRemainingBytes(0)
.build());
- receiveWriteChunks(finalChunk(555, Status.OK));
- assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 555).build());
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.OK));
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()).build());
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
}
@@ -1817,15 +1926,18 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future =
transferClient.write(123, TEST_DATA_100B.toByteArray(), progressCallback);
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(123, 123, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(90)
.setMaxChunkSizeBytes(30),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(50).setWindowEndOffset(100),
- finalChunk(123, Status.OK));
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
+ .setOffset(50)
+ .setWindowEndOffset(100),
+ finalChunk(transfer.getSessionId(), Status.OK));
verify(progressCallback, times(6)).accept(progress.capture());
assertThat(progress.getAllValues())
@@ -1842,17 +1954,20 @@ public final class TransferClientTest {
public void write_asksForFinalOffset_sendsFinalPacket() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(123, 456, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 456)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(100)
.setWindowEndOffset(140)
.setMaxChunkSizeBytes(25));
assertThat(lastChunks())
- .containsExactly(
- newChunk(Chunk.Type.DATA, 456).setOffset(100).setRemainingBytes(0).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setOffset(100)
+ .setRemainingBytes(0)
+ .build());
}
@Test
@@ -1860,17 +1975,21 @@ public final class TransferClientTest {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
for (int i = 0; i < 3; ++i) {
ListenableFuture<Void> future = transferClient.write(6, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(6, 123, TEST_DATA_SHORT.size());
+ performWriteStartHandshake(transfer, TEST_DATA_SHORT.size());
- receiveWriteChunks(
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(0).setWindowEndOffset(50),
- finalChunk(123, Status.OK));
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
+ .setOffset(0)
+ .setWindowEndOffset(50),
+ finalChunk(transfer.getSessionId(), Status.OK));
assertThat(lastChunks())
- .containsExactly(
- newChunk(Chunk.Type.DATA, 123).setData(TEST_DATA_SHORT).setRemainingBytes(0).build(),
- newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ .containsExactly(newChunk(Chunk.Type.DATA, transfer.getSessionId())
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0)
+ .build(),
+ newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()).build());
future.get();
}
@@ -1905,13 +2024,16 @@ public final class TransferClientTest {
public void write_serviceRequestsNoData_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(7, 123, TEST_DATA_SHORT.size());
+ performWriteStartHandshake(transfer, TEST_DATA_SHORT.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(0));
+ receiveWriteChunks(
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId()).setOffset(0));
- assertThat(lastChunks()).containsExactly(finalChunk(123, Status.INVALID_ARGUMENT));
- receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, 123));
+ assertThat(lastChunks())
+ .containsExactly(finalChunk(transfer.getSessionId(), Status.INVALID_ARGUMENT));
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
@@ -1921,16 +2043,18 @@ public final class TransferClientTest {
public void write_invalidOffset_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(7, 123, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(101)
.setWindowEndOffset(141)
.setMaxChunkSizeBytes(25));
- assertThat(lastChunks()).containsExactly(finalChunk(123, Status.OUT_OF_RANGE));
- receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, 123));
+ assertThat(lastChunks())
+ .containsExactly(finalChunk(transfer.getSessionId(), Status.OUT_OF_RANGE));
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.OUT_OF_RANGE);
@@ -1940,13 +2064,14 @@ public final class TransferClientTest {
public void write_sendErrorOnLaterPacket_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(7, 123, TEST_DATA_SHORT.size());
+ performWriteStartHandshake(transfer, TEST_DATA_SHORT.size());
ChannelOutputException exception = new ChannelOutputException("blah");
rpcClient.setChannelOutputException(exception);
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(50)
.setMaxChunkSizeBytes(30));
@@ -1960,27 +2085,29 @@ public final class TransferClientTest {
public void write_cancelFuture_abortsTransfer() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(7, 123, TEST_DATA_100B.size());
+ performWriteStartHandshake(transfer, TEST_DATA_100B.size());
assertThat(future.cancel(true)).isTrue();
assertThat(future.isCancelled()).isTrue();
- receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transfer.getSessionId())
.setOffset(0)
.setWindowEndOffset(50)
.setMaxChunkSizeBytes(50));
- assertThat(lastChunks()).contains(finalChunk(123, Status.CANCELLED));
- receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, 123));
+ assertThat(lastChunks()).contains(finalChunk(transfer.getSessionId(), Status.CANCELLED));
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, transfer.getSessionId()));
}
@Test
public void write_immediateTransferProtocolError_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- receiveWriteChunks(newChunk(Chunk.Type.COMPLETION, VersionedChunk.UNASSIGNED_SESSION_ID)
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION, transfer.getSessionId())
.setResourceId(123)
.setStatus(Status.NOT_FOUND.ordinal()));
@@ -1994,10 +2121,11 @@ public final class TransferClientTest {
public void write_laterTransferProtocolError_aborts() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- performWriteStartHandshake(123, 123, TEST_DATA_SHORT.size());
+ performWriteStartHandshake(transfer, TEST_DATA_SHORT.size());
- receiveWriteChunks(finalChunk(123, Status.NOT_FOUND));
+ receiveWriteChunks(finalChunk(transfer.getSessionId(), Status.NOT_FOUND));
ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
@@ -2020,29 +2148,34 @@ public final class TransferClientTest {
public void write_unknownVersion_invalidArgument() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 3).setResourceId(2).setProtocolVersion(9));
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
+ .setResourceId(2)
+ .setProtocolVersion(9));
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()),
- finalChunk(3, Status.INVALID_ARGUMENT));
+ .containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()),
+ finalChunk(transfer.getSessionId(), Status.INVALID_ARGUMENT));
}
@Test
public void write_serverRespondsWithUnknownVersion_invalidArgument() {
createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, 100));
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, 100));
- receiveWriteChunks(
- newChunk(Chunk.Type.START_ACK, 99).setResourceId(2).setProtocolVersion(600613));
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
+ .setResourceId(2)
+ .setProtocolVersion(600613));
- assertThat(lastChunks()).containsExactly(finalChunk(99, Status.INVALID_ARGUMENT));
+ assertThat(lastChunks())
+ .containsExactly(finalChunk(transfer.getSessionId(), Status.INVALID_ARGUMENT));
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
@@ -2052,6 +2185,7 @@ public final class TransferClientTest {
public void write_timeoutAfterInitialChunk() {
createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
// Call future.get() without sending any server-side packets.
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
@@ -2059,10 +2193,9 @@ public final class TransferClientTest {
// Client should have resent the last chunk (the initial chunk in this case) for each timeout.
assertThat(lastChunks())
- .containsExactly(
- initialWriteChunk(123, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()), // initial
- initialWriteChunk(123, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()), // retry 1
- initialWriteChunk(123, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size())); // retry 2
+ .containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()), // initial
+ initialWriteChunk(transfer, TEST_DATA_SHORT.size()), // retry 1
+ initialWriteChunk(transfer, TEST_DATA_SHORT.size())); // retry 2
}
@Test
@@ -2071,25 +2204,25 @@ public final class TransferClientTest {
// Wait for two outgoing packets (Write RPC request and first chunk), then do the handshake.
enqueueWriteChunks(2,
- newChunk(Chunk.Type.START_ACK, 123).setResourceId(9),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.START_ACK, transferClient.getNextSessionIdForTest()).setResourceId(9),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, transferClient.getNextSessionIdForTest())
.setOffset(0)
.setWindowEndOffset(90)
.setMaxChunkSizeBytes(30));
ListenableFuture<Void> future = transferClient.write(9, TEST_DATA_SHORT.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
- Chunk data = newChunk(Chunk.Type.DATA, 123)
+ Chunk data = newChunk(Chunk.Type.DATA, transfer.getSessionId())
.setOffset(0)
.setData(TEST_DATA_SHORT)
.setRemainingBytes(0)
.build();
assertThat(lastChunks())
- .containsExactly(
- initialWriteChunk(9, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()), // initial
- newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .containsExactly(initialWriteChunk(transfer, TEST_DATA_SHORT.size()), // initial
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, transfer.getSessionId())
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(TEST_DATA_SHORT.size())
.build(),
@@ -2103,47 +2236,50 @@ public final class TransferClientTest {
createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
assertThat(MAX_RETRIES).isEqualTo(2); // This test assumes 2 retries
+ int id = transferClient.getNextSessionIdForTest();
+
// Wait for four outgoing packets (Write RPC request and START chunk + retry), then handshake.
enqueueWriteChunks(3,
- newChunk(Chunk.Type.START_ACK, 123)
+ newChunk(Chunk.Type.START_ACK, id)
.setResourceId(5)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
// Wait for start ack confirmation + 2 retries, then request three packets.
enqueueWriteChunks(3,
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(0)
.setWindowEndOffset(60)
.setMaxChunkSizeBytes(20));
// After two packets, request the remainder of the packets.
enqueueWriteChunks(
- 2, newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(20).setWindowEndOffset(200));
+ 2, newChunk(Chunk.Type.PARAMETERS_CONTINUE, id).setOffset(20).setWindowEndOffset(200));
// Wait for last 3 data packets, then 2 final packet retries.
enqueueWriteChunks(5,
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(80)
.setWindowEndOffset(200)
.setMaxChunkSizeBytes(20),
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(80)
.setWindowEndOffset(200)
.setMaxChunkSizeBytes(20));
// After the retry, confirm completed multiple times; additional packets should be dropped
enqueueWriteChunks(1,
- newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()),
- newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()),
- newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()),
- newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()));
+ newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.code()),
+ newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.code()),
+ newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.code()),
+ newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.code()));
ListenableFuture<Void> future = transferClient.write(5, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
final Chunk startAckConfirmation =
- newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, id)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(TEST_DATA_100B.size())
.build();
@@ -2151,25 +2287,25 @@ public final class TransferClientTest {
assertThat(lastChunks())
.containsExactly(
// initial handshake with retries
- initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
- initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ initialWriteChunk(transfer, TEST_DATA_100B.size()),
+ initialWriteChunk(transfer, TEST_DATA_100B.size()),
startAckConfirmation,
startAckConfirmation,
startAckConfirmation,
// send all data
- dataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
- dataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
- dataChunk(123, TEST_DATA_100B, 40, 60), // data 40-60
- dataChunk(123, TEST_DATA_100B, 60, 80), // data 60-80
- dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(id, TEST_DATA_100B, 0, 20), // data 0-20
+ dataChunk(id, TEST_DATA_100B, 20, 40), // data 20-40
+ dataChunk(id, TEST_DATA_100B, 40, 60), // data 40-60
+ dataChunk(id, TEST_DATA_100B, 60, 80), // data 60-80
+ dataChunk(id, TEST_DATA_100B, 80, 100), // data 80-100 (final)
// retry last packet two times
- dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
- dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(id, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(id, TEST_DATA_100B, 80, 100), // data 80-100 (final)
// respond to two PARAMETERS_RETRANSMIT packets
- dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
- dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(id, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(id, TEST_DATA_100B, 80, 100), // data 80-100 (final)
// respond to OK packet
- newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ newChunk(Chunk.Type.COMPLETION_ACK, id).build());
}
@Test
@@ -2177,98 +2313,105 @@ public final class TransferClientTest {
createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
assertThat(MAX_RETRIES).isEqualTo(2); // This test assumes 2 retries
+ int id = transferClient.getNextSessionIdForTest();
+
// Wait for two outgoing packets (Write RPC request and START chunk), then do the handshake.
enqueueWriteChunks(2,
- newChunk(Chunk.Type.START_ACK, 123)
+ newChunk(Chunk.Type.START_ACK, id)
.setResourceId(5)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
// Request two packets.
enqueueWriteChunks(1,
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(0)
.setWindowEndOffset(40)
.setMaxChunkSizeBytes(20));
// After the second retry, send more transfer parameters
enqueueWriteChunks(4,
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(40)
.setWindowEndOffset(120)
.setMaxChunkSizeBytes(40));
// After the first retry, send more transfer parameters
enqueueWriteChunks(3,
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(80)
.setWindowEndOffset(160)
.setMaxChunkSizeBytes(10));
// After the second retry, confirm completed
- enqueueWriteChunks(4, newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()));
- enqueueWriteChunks(1, newChunk(Chunk.Type.COMPLETION_ACK, 123));
+ enqueueWriteChunks(4, newChunk(Chunk.Type.COMPLETION, id).setStatus(Status.OK.code()));
+ enqueueWriteChunks(1, newChunk(Chunk.Type.COMPLETION_ACK, id));
ListenableFuture<Void> future = transferClient.write(5, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
assertThat(lastChunks())
.containsExactly(
// initial handshake
- initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
- newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ initialWriteChunk(transfer, TEST_DATA_100B.size()),
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, id)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(TEST_DATA_100B.size())
.build(),
// after 2, receive parameters: 40 from 0 by 20
- dataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
- dataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
- dataChunk(123, TEST_DATA_100B, 20, 40), // retry 1
- dataChunk(123, TEST_DATA_100B, 20, 40), // retry 2
+ dataChunk(id, TEST_DATA_100B, 0, 20), // data 0-20
+ dataChunk(id, TEST_DATA_100B, 20, 40), // data 20-40
+ dataChunk(id, TEST_DATA_100B, 20, 40), // retry 1
+ dataChunk(id, TEST_DATA_100B, 20, 40), // retry 2
// after 4, receive parameters: 80 from 40 by 40
- dataChunk(123, TEST_DATA_100B, 40, 80), // data 40-80
- dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100
- dataChunk(123, TEST_DATA_100B, 80, 100), // retry 1
+ dataChunk(id, TEST_DATA_100B, 40, 80), // data 40-80
+ dataChunk(id, TEST_DATA_100B, 80, 100), // data 80-100
+ dataChunk(id, TEST_DATA_100B, 80, 100), // retry 1
// after 3, receive parameters: 80 from 80 by 10
- dataChunk(123, TEST_DATA_100B, 80, 90), // data 80-90
- dataChunk(123, TEST_DATA_100B, 90, 100), // data 90-100
- dataChunk(123, TEST_DATA_100B, 90, 100), // retry 1
- dataChunk(123, TEST_DATA_100B, 90, 100), // retry 2
+ dataChunk(id, TEST_DATA_100B, 80, 90), // data 80-90
+ dataChunk(id, TEST_DATA_100B, 90, 100), // data 90-100
+ dataChunk(id, TEST_DATA_100B, 90, 100), // retry 1
+ dataChunk(id, TEST_DATA_100B, 90, 100), // retry 2
// after 4, receive final OK
- newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ newChunk(Chunk.Type.COMPLETION_ACK, id).build());
}
+
@Test
- public void write_maxLifetimeRetries() throws Exception {
+ public void write_maxLifetimeRetries() {
createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO, 5);
assertThat(MAX_RETRIES).isEqualTo(2); // This test assumes 2 retries
+ int id = transferClient.getNextSessionIdForTest();
+
// Wait for four outgoing packets (Write RPC request and START chunk + 2 retries)
enqueueWriteChunks(4, // 2 retries
- newChunk(Chunk.Type.START_ACK, 123)
+ newChunk(Chunk.Type.START_ACK, id)
.setResourceId(5)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
// Wait for start ack confirmation + 2 retries, then request three packets.
enqueueWriteChunks(3, // 2 retries
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id)
.setOffset(0)
.setWindowEndOffset(60)
.setMaxChunkSizeBytes(20));
// After 3 data packets, wait for two more retries, which should put this over the retry limit.
enqueueWriteChunks(5, // 2 retries
- newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123) // This packet should be ignored
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, id) // This packet should be ignored
.setOffset(80)
.setWindowEndOffset(200)
.setMaxChunkSizeBytes(20));
ListenableFuture<Void> future = transferClient.write(5, TEST_DATA_100B.toByteArray());
+ WriteTransfer transfer = transferClient.getWriteTransferForTest(future);
ExecutionException exception = assertThrows(ExecutionException.class, future::get);
assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
final Chunk startAckConfirmation =
- newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, id)
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(TEST_DATA_100B.size())
.build();
@@ -2276,19 +2419,19 @@ public final class TransferClientTest {
assertThat(lastChunks())
.containsExactly(
// initial chunk and 2 retries
- initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
- initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
- initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ initialWriteChunk(transfer, TEST_DATA_100B.size()),
+ initialWriteChunk(transfer, TEST_DATA_100B.size()),
+ initialWriteChunk(transfer, TEST_DATA_100B.size()),
// START_ACK_CONFIRMATION and 2 retries
startAckConfirmation,
startAckConfirmation,
startAckConfirmation,
// send all data
- dataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
- dataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
- dataChunk(123, TEST_DATA_100B, 40, 60), // data 40-60
+ dataChunk(id, TEST_DATA_100B, 0, 20), // data 0-20
+ dataChunk(id, TEST_DATA_100B, 20, 40), // data 20-40
+ dataChunk(id, TEST_DATA_100B, 40, 60), // data 40-60
// last packet retry, then hit the lifetime retry limit and abort
- dataChunk(123, TEST_DATA_100B, 40, 60)); // data 40-60
+ dataChunk(id, TEST_DATA_100B, 40, 60)); // data 40-60
}
private static ByteString range(int startInclusive, int endExclusive) {
@@ -2310,21 +2453,35 @@ public final class TransferClientTest {
return Chunk.newBuilder().setType(type).setSessionId(sessionId);
}
- private static Chunk initialReadChunk(int resourceId, ProtocolVersion version) {
- return initialReadChunk(resourceId, version, TRANSFER_PARAMETERS);
+ private static Chunk initialReadChunk(ReadTransfer transfer) {
+ Chunk.Builder chunk =
+ newLegacyChunk(Chunk.Type.START, transfer.getResourceId())
+ .setResourceId(transfer.getResourceId())
+ .setPendingBytes(transfer.getParametersForTest().maxPendingBytes())
+ .setWindowEndOffset(transfer.getParametersForTest().maxPendingBytes())
+ .setMaxChunkSizeBytes(transfer.getParametersForTest().maxChunkSizeBytes())
+ .setOffset(0);
+ if (transfer.getDesiredProtocolVersionForTest() != ProtocolVersion.LEGACY) {
+ chunk.setProtocolVersion(transfer.getDesiredProtocolVersionForTest().ordinal());
+ chunk.setDesiredSessionId(transfer.getSessionId());
+ }
+ if (transfer.getParametersForTest().chunkDelayMicroseconds() > 0) {
+ chunk.setMinDelayMicroseconds(transfer.getParametersForTest().chunkDelayMicroseconds());
+ }
+ return chunk.build();
}
- private static Chunk initialReadChunk(
- int resourceId, ProtocolVersion version, TransferParameters params) {
+ private static Chunk initialLegacyReadChunk(int resourceId) {
+ return initialLegacyReadChunk(resourceId, TRANSFER_PARAMETERS);
+ }
+
+ private static Chunk initialLegacyReadChunk(int resourceId, TransferParameters params) {
Chunk.Builder chunk = newLegacyChunk(Chunk.Type.START, resourceId)
.setResourceId(resourceId)
.setPendingBytes(params.maxPendingBytes())
.setWindowEndOffset(params.maxPendingBytes())
.setMaxChunkSizeBytes(params.maxChunkSizeBytes())
.setOffset(0);
- if (version != ProtocolVersion.LEGACY) {
- chunk.setProtocolVersion(version.ordinal());
- }
if (params.chunkDelayMicroseconds() > 0) {
chunk.setMinDelayMicroseconds(params.chunkDelayMicroseconds());
}
@@ -2343,12 +2500,20 @@ public final class TransferClientTest {
return chunk.build();
}
- private static Chunk initialWriteChunk(int resourceId, ProtocolVersion version, int size) {
- Chunk.Builder chunk = newLegacyChunk(Chunk.Type.START, resourceId)
- .setResourceId(resourceId)
+ private static Chunk initialLegacyWriteChunk(int resourceId, int size) {
+ return newLegacyChunk(Chunk.Type.START, resourceId)
+ .setResourceId(resourceId)
+ .setRemainingBytes(size)
+ .build();
+ }
+
+ private static Chunk initialWriteChunk(WriteTransfer transfer, int size) {
+ Chunk.Builder chunk = newLegacyChunk(Chunk.Type.START, transfer.getResourceId())
+ .setResourceId(transfer.getResourceId())
.setRemainingBytes(size);
- if (version != ProtocolVersion.LEGACY) {
+ if (transfer.getDesiredProtocolVersionForTest() != ProtocolVersion.LEGACY) {
chunk.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal());
+ chunk.setDesiredSessionId(transfer.getSessionId());
}
return chunk.build();
}
@@ -2415,20 +2580,16 @@ public final class TransferClientTest {
}
}
- private void performReadStartHandshake(int resourceId, int sessionId) {
- performReadStartHandshake(
- resourceId, sessionId, TransferClient.DEFAULT_READ_TRANSFER_PARAMETERS);
- }
+ private void performReadStartHandshake(ReadTransfer transfer) {
+ assertThat(lastChunks()).containsExactly(initialReadChunk(transfer));
- private void performReadStartHandshake(int resourceId, int sessionId, TransferParameters params) {
- assertThat(lastChunks())
- .containsExactly(initialReadChunk(resourceId, ProtocolVersion.VERSION_TWO, params));
-
- receiveReadChunks(newChunk(Chunk.Type.START_ACK, sessionId)
- .setResourceId(resourceId)
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
+ .setResourceId(transfer.getResourceId())
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
- assertThat(lastChunks()).containsExactly(readStartAckConfirmation(sessionId, params));
+ assertThat(lastChunks())
+ .containsExactly(
+ readStartAckConfirmation(transfer.getSessionId(), transfer.getParametersForTest()));
}
private void performReadCompletionHandshake(int sessionId, Status status) {
@@ -2442,16 +2603,15 @@ public final class TransferClientTest {
receiveReadChunks(newChunk(Chunk.Type.COMPLETION_ACK, sessionId));
}
- private void performWriteStartHandshake(int resourceId, int sessionId, int dataSize) {
- assertThat(lastChunks())
- .containsExactly(initialWriteChunk(resourceId, ProtocolVersion.VERSION_TWO, dataSize));
+ private void performWriteStartHandshake(WriteTransfer transfer, int dataSize) {
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(transfer, dataSize));
- receiveWriteChunks(newChunk(Chunk.Type.START_ACK, sessionId)
- .setResourceId(resourceId)
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, transfer.getSessionId())
+ .setResourceId(transfer.getResourceId())
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
assertThat(lastChunks())
- .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, sessionId)
+ .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, transfer.getSessionId())
.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
.setRemainingBytes(dataSize)
.build());
diff --git a/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h b/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h
index b108c5f52..22eb4c04d 100644
--- a/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h
+++ b/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h
@@ -24,12 +24,15 @@
namespace pw::transfer {
-// The AtomicFileTransferHandler is intended to be used as a transfer
-// handler for files. It ensures that the target file of the transfer is always
-// in a correct state. In particular, the transfer is first done to a temporary
-// file and once complete, the original targeted file is updated.
+/// `AtomicFileTransferHandler` is intended to be used as a transfer handler for
+/// files. It ensures that the target file of the transfer is always in a
+/// correct state. In particular, the transfer is first done to a temporary file
+/// and once complete, the original targeted file is updated.
class AtomicFileTransferHandler : public ReadWriteHandler {
public:
+ /// @param[in] resource_id An ID for the resource that's being transferred.
+ ///
+ /// @param[in] file_path The target file to update.
AtomicFileTransferHandler(uint32_t resource_id, std::string_view file_path)
: ReadWriteHandler(resource_id), path_(file_path) {}
@@ -38,15 +41,36 @@ class AtomicFileTransferHandler : public ReadWriteHandler {
delete;
~AtomicFileTransferHandler() override = default;
- // Function called prior to initializing a read transfer.
+ /// Prepares `AtomicFileTransferHandler` for a read transfer.
+ ///
+ /// @pre The read transfer has not been initialized before the call to this
+ /// method.
+ ///
+ /// @returns A `pw::Status` object indicating whether
+ /// `AtomicFileTransferHandler` is ready for the transfer.
Status PrepareRead() override;
- // Function called after a read transfer is done.
- // Status indicates whether transfer was done successfully.
+ /// Handler function that is called by the transfer thread after a read
+ /// transfer completes.
+ ///
+ /// @param[in] Status A `pw::Status` object provided by the transfer thread
+ /// indicating whether the transfer succeeded.
+ ///
+ /// @pre The read transfer is done before the call to this method.
void FinalizeRead(Status) override;
- // Function called prior to initializing a write transfer.
+ /// Prepares `AtomicFileTransferHandler` for a write transfer.
+ ///
+ /// @pre The write transfer has not been initialized before the call to this
+ /// method.
+ ///
+ /// @returns A `pw::Status` object indicating whether
+ /// `AtomicFileTransferHandler` is ready for the transfer.
Status PrepareWrite() override;
- // Function called after a write transfer is done.
- // Status indicates whether transfer was done successfully.
+ /// Indicates whether the write transfer was successful.
+ ///
+ /// @pre The write transfer is done.
+ ///
+ /// @returns A `pw::Status` object indicating whether the transfer data was
+ /// successfully written.
Status FinalizeWrite(Status) override;
private:
diff --git a/pw_transfer/public/pw_transfer/client.h b/pw_transfer/public/pw_transfer/client.h
index d7faa08ed..62931daf8 100644
--- a/pw_transfer/public/pw_transfer/client.h
+++ b/pw_transfer/public/pw_transfer/client.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -77,6 +77,8 @@ class Client {
stream::Writer& output,
CompletionFunc&& on_completion,
chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout,
+ chrono::SystemClock::duration initial_chunk_timeout =
+ cfg::kDefaultInitialChunkTimeout,
ProtocolVersion version = kDefaultProtocolVersion);
// Begins a new write transfer for the given resource ID. Data from the
@@ -88,6 +90,8 @@ class Client {
stream::Reader& input,
CompletionFunc&& on_completion,
chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout,
+ chrono::SystemClock::duration initial_chunk_timeout =
+ cfg::kDefaultInitialChunkTimeout,
ProtocolVersion version = kDefaultProtocolVersion);
// Terminates an ongoing transfer for the specified resource ID.
@@ -123,7 +127,7 @@ class Client {
private:
static constexpr ProtocolVersion kDefaultProtocolVersion =
- ProtocolVersion::kLatest;
+ ProtocolVersion::kLegacy;
using Transfer = pw_rpc::raw::Transfer;
diff --git a/pw_transfer/public/pw_transfer/internal/chunk.h b/pw_transfer/public/pw_transfer/internal/chunk.h
index ffb556d6e..f2e03c8e0 100644
--- a/pw_transfer/public/pw_transfer/internal/chunk.h
+++ b/pw_transfer/public/pw_transfer/internal/chunk.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -29,7 +29,8 @@ class Chunk {
class Identifier {
public:
constexpr bool is_session() const { return type_ == kSession; }
- constexpr bool is_resource() const { return !is_session(); }
+ constexpr bool is_desired_session() const { return type_ == kDesired; }
+ constexpr bool is_legacy() const { return type_ == kLegacy; }
constexpr uint32_t value() const { return value_; }
@@ -39,13 +40,17 @@ class Chunk {
static constexpr Identifier Session(uint32_t value) {
return Identifier(kSession, value);
}
- static constexpr Identifier Resource(uint32_t value) {
- return Identifier(kResource, value);
+ static constexpr Identifier Desired(uint32_t value) {
+ return Identifier(kDesired, value);
+ }
+ static constexpr Identifier Legacy(uint32_t value) {
+ return Identifier(kLegacy, value);
}
enum IdType {
kSession,
- kResource,
+ kDesired,
+ kLegacy,
};
constexpr Identifier(IdType type, uint32_t value)
@@ -90,6 +95,11 @@ class Chunk {
return *this;
}
+ constexpr Chunk& set_desired_session_id(uint32_t session_id) {
+ desired_session_id_ = session_id;
+ return *this;
+ }
+
constexpr Chunk& set_resource_id(uint32_t resource_id) {
resource_id_ = resource_id;
return *this;
@@ -138,7 +148,16 @@ class Chunk {
return *this;
}
- constexpr uint32_t session_id() const { return session_id_; }
+ constexpr uint32_t session_id() const {
+ if (desired_session_id_.has_value()) {
+ return desired_session_id_.value();
+ }
+ return session_id_;
+ }
+
+ constexpr std::optional<uint32_t> desired_session_id() const {
+ return desired_session_id_;
+ }
constexpr std::optional<uint32_t> resource_id() const {
if (is_legacy()) {
@@ -237,6 +256,7 @@ class Chunk {
private:
constexpr Chunk(ProtocolVersion version, std::optional<Type> type)
: session_id_(0),
+ desired_session_id_(std::nullopt),
resource_id_(std::nullopt),
window_end_offset_(0),
max_chunk_size_bytes_(std::nullopt),
@@ -263,6 +283,7 @@ class Chunk {
}
uint32_t session_id_;
+ std::optional<uint32_t> desired_session_id_;
std::optional<uint32_t> resource_id_;
uint32_t window_end_offset_;
std::optional<uint32_t> max_chunk_size_bytes_;
diff --git a/pw_transfer/public/pw_transfer/internal/client_context.h b/pw_transfer/public/pw_transfer/internal/client_context.h
index 9912aa4ac..aa7763eaa 100644
--- a/pw_transfer/public/pw_transfer/internal/client_context.h
+++ b/pw_transfer/public/pw_transfer/internal/client_context.h
@@ -27,13 +27,6 @@ class ClientContext final : public Context {
on_completion_ = std::move(on_completion);
}
- // In client-side transfer contexts, a session ID may not yet have been
- // assigned by the server, in which case resource_id is used as the context
- // identifier.
- constexpr uint32_t id() const {
- return session_id() == kUnassignedSessionId ? resource_id() : session_id();
- }
-
private:
Status FinalCleanup(Status status) override;
diff --git a/pw_transfer/public/pw_transfer/internal/config.h b/pw_transfer/public/pw_transfer/internal/config.h
index 9410d990b..04bfa78f8 100644
--- a/pw_transfer/public/pw_transfer/internal/config.h
+++ b/pw_transfer/public/pw_transfer/internal/config.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -54,6 +54,19 @@ static_assert(PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES >
static_assert(PW_TRANSFER_DEFAULT_TIMEOUT_MS > 0);
+// The default amount of time, in milliseconds, to wait for an initial server
+// response to a transfer before retrying. This can later be configured
+// per-transfer.
+//
+// This is set separately to PW_TRANSFER_DEFAULT_TIMEOUT_MS as transfers may
+// require additional time for resource initialization (e.g. erasing a flash
+// region before writing to it).
+#ifndef PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS
+#define PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS PW_TRANSFER_DEFAULT_TIMEOUT_MS
+#endif // PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS
+
+static_assert(PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS > 0);
+
// The fractional position within a window at which a receive transfer should
// extend its window size to minimize the amount of time the transmitter
// spends blocked.
@@ -74,7 +87,11 @@ inline constexpr uint16_t kDefaultMaxLifetimeRetries =
PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES;
inline constexpr chrono::SystemClock::duration kDefaultChunkTimeout =
- std::chrono::milliseconds(PW_TRANSFER_DEFAULT_TIMEOUT_MS);
+ chrono::SystemClock::for_at_least(
+ std::chrono::milliseconds(PW_TRANSFER_DEFAULT_TIMEOUT_MS));
+inline constexpr chrono::SystemClock::duration kDefaultInitialChunkTimeout =
+ chrono::SystemClock::for_at_least(
+ std::chrono::milliseconds(PW_TRANSFER_DEFAULT_INITIAL_TIMEOUT_MS));
inline constexpr uint32_t kDefaultExtendWindowDivisor =
PW_TRANSFER_DEFAULT_EXTEND_WINDOW_DIVISOR;
diff --git a/pw_transfer/public/pw_transfer/internal/context.h b/pw_transfer/public/pw_transfer/internal/context.h
index d72f81384..b4a80ec98 100644
--- a/pw_transfer/public/pw_transfer/internal/context.h
+++ b/pw_transfer/public/pw_transfer/internal/context.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -129,6 +129,7 @@ class Context {
last_chunk_sent_(Chunk::Type::kData),
last_chunk_offset_(0),
chunk_timeout_(chrono::SystemClock::duration::zero()),
+ initial_chunk_timeout_(chrono::SystemClock::duration::zero()),
interchunk_delay_(chrono::SystemClock::for_at_least(
std::chrono::microseconds(kDefaultChunkDelayMicroseconds))),
next_timeout_(kNoTimeout) {}
@@ -276,8 +277,8 @@ class Context {
// Processes a data chunk in a received while in the kWaiting state.
void HandleReceivedData(const Chunk& chunk);
- // Sends the first chunk in a transmit transfer.
- void SendInitialTransmitChunk();
+ // Sends the first chunk in a legacy transmit transfer.
+ void SendInitialLegacyTransmitChunk();
// Updates the current receive transfer parameters based on the context's
// configuration.
@@ -347,7 +348,7 @@ class Context {
// status chunk will be re-sent for every non-ACK chunk received,
// continually notifying the other end that the transfer is over.
static constexpr chrono::SystemClock::duration kFinalChunkAckTimeout =
- std::chrono::milliseconds(5000);
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(5000));
static constexpr chrono::SystemClock::time_point kNoTimeout =
chrono::SystemClock::time_point(chrono::SystemClock::duration(0));
@@ -391,6 +392,9 @@ class Context {
// How long to wait for a chunk from the other end.
chrono::SystemClock::duration chunk_timeout_;
+ // How long for a client to wait for an initial server response.
+ chrono::SystemClock::duration initial_chunk_timeout_;
+
// How long to delay between transmitting subsequent data chunks within a
// window.
chrono::SystemClock::duration interchunk_delay_;
diff --git a/pw_transfer/public/pw_transfer/internal/event.h b/pw_transfer/public/pw_transfer/internal/event.h
index 79a7853d1..472bc59bf 100644
--- a/pw_transfer/public/pw_transfer/internal/event.h
+++ b/pw_transfer/public/pw_transfer/internal/event.h
@@ -75,6 +75,7 @@ struct NewTransferEvent {
rpc::Writer* rpc_writer;
const TransferParameters* max_parameters;
chrono::SystemClock::duration timeout;
+ chrono::SystemClock::duration initial_timeout;
uint32_t max_retries;
uint32_t max_lifetime_retries;
TransferThread* transfer_thread;
@@ -109,7 +110,6 @@ struct EndTransferEvent {
struct SendStatusChunkEvent {
uint32_t session_id;
- bool set_resource_id;
ProtocolVersion protocol_version;
Status::Code status;
TransferStream stream;
diff --git a/pw_transfer/public/pw_transfer/internal/server_context.h b/pw_transfer/public/pw_transfer/internal/server_context.h
index 265b75aa9..ce9a78bbb 100644
--- a/pw_transfer/public/pw_transfer/internal/server_context.h
+++ b/pw_transfer/public/pw_transfer/internal/server_context.h
@@ -35,9 +35,6 @@ class ServerContext final : public Context {
// Returns the pointer to the current handler.
const Handler* handler() { return handler_; }
- // In server-side transfer contexts, a session ID always exists.
- constexpr uint32_t id() const { return session_id(); }
-
private:
// Ends the transfer with the given status, calling the handler's Finalize
// method. No chunks are sent.
diff --git a/pw_transfer/public/pw_transfer/transfer.h b/pw_transfer/public/pw_transfer/transfer.h
index 42a9d7bca..93b39abe3 100644
--- a/pw_transfer/public/pw_transfer/transfer.h
+++ b/pw_transfer/public/pw_transfer/transfer.h
@@ -65,8 +65,7 @@ class TransferService : public pw_rpc::raw::Transfer::Service<TransferService> {
thread_(transfer_thread),
chunk_timeout_(chunk_timeout),
max_retries_(max_retries),
- max_lifetime_retries_(max_lifetime_retries),
- next_session_id_(1) {}
+ max_lifetime_retries_(max_lifetime_retries) {}
TransferService(const TransferService&) = delete;
TransferService(TransferService&&) = delete;
@@ -124,17 +123,12 @@ class TransferService : public pw_rpc::raw::Transfer::Service<TransferService> {
private:
void HandleChunk(ConstByteSpan message, internal::TransferType type);
- // TODO(frolv): This could be more sophisticated and less predictable.
- uint32_t GenerateNewSessionId() { return next_session_id_++; }
-
internal::TransferParameters max_parameters_;
TransferThread& thread_;
chrono::SystemClock::duration chunk_timeout_;
uint8_t max_retries_;
uint32_t max_lifetime_retries_;
-
- uint32_t next_session_id_;
};
} // namespace pw::transfer
diff --git a/pw_transfer/public/pw_transfer/transfer_thread.h b/pw_transfer/public/pw_transfer/transfer_thread.h
index 5904c9506..93e724008 100644
--- a/pw_transfer/public/pw_transfer/transfer_thread.h
+++ b/pw_transfer/public/pw_transfer/transfer_thread.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -42,6 +42,7 @@ class TransferThread : public thread::ThreadCore {
ByteSpan encode_buffer)
: client_transfers_(client_transfers),
server_transfers_(server_transfers),
+ next_session_id_(1),
chunk_buffer_(chunk_buffer),
encode_buffer_(encode_buffer) {}
@@ -52,20 +53,19 @@ class TransferThread : public thread::ThreadCore {
const TransferParameters& max_parameters,
Function<void(Status)>&& on_completion,
chrono::SystemClock::duration timeout,
+ chrono::SystemClock::duration initial_timeout,
uint8_t max_retries,
uint32_t max_lifetime_retries) {
- uint32_t session_id = version == ProtocolVersion::kLegacy
- ? resource_id
- : Context::kUnassignedSessionId;
StartTransfer(type,
version,
- session_id,
+ Context::kUnassignedSessionId, // Assigned later.
resource_id,
/*raw_chunk=*/{},
stream,
max_parameters,
std::move(on_completion),
timeout,
+ initial_timeout,
max_retries,
max_lifetime_retries);
}
@@ -88,6 +88,7 @@ class TransferThread : public thread::ThreadCore {
max_parameters,
/*on_completion=*/nullptr,
timeout,
+ timeout,
max_retries,
max_lifetime_retries);
}
@@ -100,6 +101,17 @@ class TransferThread : public thread::ThreadCore {
ProcessChunk(EventType::kServerChunk, chunk);
}
+ void SendServerStatus(TransferType type,
+ uint32_t session_id,
+ ProtocolVersion version,
+ Status status) {
+ SendStatus(type == TransferType::kTransmit ? TransferStream::kServerRead
+ : TransferStream::kServerWrite,
+ session_id,
+ version,
+ status);
+ }
+
void EndClientTransfer(uint32_t session_id,
Status status,
bool send_status_chunk = false) {
@@ -179,7 +191,7 @@ class TransferThread : public thread::ThreadCore {
uint32_t session_id) {
auto transfer =
std::find_if(transfers.begin(), transfers.end(), [session_id](auto& c) {
- return c.initialized() && c.id() == session_id;
+ return c.initialized() && c.session_id() == session_id;
});
return transfer != transfers.end() ? &*transfer : nullptr;
}
@@ -244,6 +256,8 @@ class TransferThread : public thread::ThreadCore {
// Returns the earliest timeout among all active transfers, up to kMaxTimeout.
chrono::SystemClock::time_point GetNextTransferTimeout() const;
+ uint32_t AssignSessionId();
+
void StartTransfer(TransferType type,
ProtocolVersion version,
uint32_t session_id,
@@ -253,11 +267,17 @@ class TransferThread : public thread::ThreadCore {
const TransferParameters& max_parameters,
Function<void(Status)>&& on_completion,
chrono::SystemClock::duration timeout,
+ chrono::SystemClock::duration initial_timeout,
uint8_t max_retries,
uint32_t max_lifetime_retries);
void ProcessChunk(EventType type, ConstByteSpan chunk);
+ void SendStatus(TransferStream stream,
+ uint32_t session_id,
+ ProtocolVersion version,
+ Status status);
+
void EndTransfer(EventType type,
uint32_t session_id,
Status status,
@@ -284,6 +304,13 @@ class TransferThread : public thread::ThreadCore {
span<ClientContext> client_transfers_;
span<ServerContext> server_transfers_;
+ // Identifier to use for the next started transfer, unique over the RPC
+ // channel between the transfer client and server.
+ //
+ // TODO(frolv): If we ever support changing the RPC channel, this should be
+ // reset to 1.
+ uint32_t next_session_id_;
+
// All registered transfer handlers.
IntrusiveList<Handler> handlers_;
diff --git a/pw_transfer/pw_transfer_private/chunk_testing.h b/pw_transfer/pw_transfer_private/chunk_testing.h
index 7347f6b8c..d909ab500 100644
--- a/pw_transfer/pw_transfer_private/chunk_testing.h
+++ b/pw_transfer/pw_transfer_private/chunk_testing.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -13,6 +13,7 @@
// the License.
#pragma once
+#include "gtest/gtest.h"
#include "pw_bytes/span.h"
#include "pw_containers/vector.h"
#include "pw_transfer/internal/chunk.h"
diff --git a/pw_transfer/py/pw_transfer/chunk.py b/pw_transfer/py/pw_transfer/chunk.py
index 0bb0c485a..e527dafa4 100644
--- a/pw_transfer/py/pw_transfer/chunk.py
+++ b/pw_transfer/py/pw_transfer/chunk.py
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -52,8 +52,28 @@ class Chunk:
Wraps the generated protobuf Chunk class with protocol-aware field encoding
and decoding.
+
+ Attributes:
+ protocol_version: Version of the transfer protocol with which the chunk
+ is encoded.
+ chunk_type: Type of the chunk within the protocol.
+ session_id: ID for the transfer session to which the chunk belongs.
+ desired_session_id: For a v2 START chunk, the client-assigned session ID
+ to request from the server.
+ resource_id: For a v2 START chunk, ID of the resource to transfer.
+ offset: Offset of the data to be transferred.
+ window_end_offset: In a parameters chunk, end offset of the available
+ window.
+ data: Raw transfer data.
+ remaining_bytes: Optional number of bytes remaining in the transfer.
+ Set to 0 when the data is fully transferred.
+ max_chunk_size_bytes: Maximum number of bytes to send in a single data
+ chunk.
+ min_delay_microseconds: Delay between data chunks to be sent.
"""
+ # pylint: disable=too-many-instance-attributes
+
Type = transfer_pb2.Chunk.Type
# TODO(frolv): Figure out how to make the chunk type annotation work.
@@ -63,6 +83,7 @@ class Chunk:
protocol_version: ProtocolVersion,
chunk_type: Any,
session_id: int = 0,
+ desired_session_id: Optional[int] = None,
resource_id: Optional[int] = None,
offset: int = 0,
window_end_offset: int = 0,
@@ -72,9 +93,31 @@ class Chunk:
min_delay_microseconds: Optional[int] = None,
status: Optional[Status] = None,
):
+ """Creates a new transfer chunk.
+
+ Args:
+ protocol_version: Version of the transfer protocol with which to
+ encode the chunk.
+ chunk_type: Type of the chunk within the protocol.
+ session_id: ID for the transfer session to which the chunk belongs.
+ desired_session_id: For a v2 START chunk, the client-assigned
+ session ID to request from the server.
+ resource_id: For a v2 START chunk, ID of the resource to transfer.
+ offset: Offset of the data to be transferred.
+ window_end_offset: In a parameters chunk, end offset of the
+ available window.
+ data: Raw transfer data.
+ remaining_bytes: Optional number of bytes remaining in the transfer.
+ Set to 0 when the data is fully transferred.
+ max_chunk_size_bytes: Maximum number of bytes to send in a single
+ data chunk.
+ min_delay_microseconds: Delay between data chunks to be sent.
+ status: In a COMPLETION chunk, final status of the transfer.
+ """
self.protocol_version = protocol_version
self.type = chunk_type
self.session_id = session_id
+ self.desired_session_id = desired_session_id
self.resource_id = resource_id
self.offset = offset
self.window_end_offset = window_end_offset
@@ -122,6 +165,10 @@ class Chunk:
chunk.protocol_version = ProtocolVersion.LEGACY
chunk.session_id = message.transfer_id
+ if message.HasField('desired_session_id'):
+ chunk.protocol_version = ProtocolVersion.VERSION_TWO
+ chunk.desired_session_id = message.desired_session_id
+
if message.HasField('resource_id'):
chunk.resource_id = message.resource_id
@@ -165,8 +212,12 @@ class Chunk:
if self.protocol_version is ProtocolVersion.VERSION_TWO:
if self.session_id != 0:
+ assert self.desired_session_id is None
message.session_id = self.session_id
+ if self.desired_session_id is not None:
+ message.desired_session_id = self.desired_session_id
+
if self._should_encode_legacy_fields():
if self.resource_id is not None:
message.transfer_id = self.resource_id
@@ -208,10 +259,6 @@ class Chunk:
Depending on the protocol version and type of chunk, this may correspond
to one of several proto fields.
"""
- if self.resource_id is not None:
- # Always prioritize a resource_id over a session_id.
- return self.resource_id
-
return self.session_id
def requests_transmission_from_offset(self) -> bool:
diff --git a/pw_transfer/py/pw_transfer/client.py b/pw_transfer/py/pw_transfer/client.py
index 85bfc4575..6b2ab6dd2 100644
--- a/pw_transfer/py/pw_transfer/client.py
+++ b/pw_transfer/py/pw_transfer/client.py
@@ -14,6 +14,7 @@
"""Client for the pw_transfer service, which transmits data over pw_rpc."""
import asyncio
+import ctypes
import logging
import threading
from typing import Any, Dict, Optional, Union
@@ -60,7 +61,7 @@ class Manager: # pylint: disable=too-many-instance-attributes
initial_response_timeout_s: float = 4.0,
max_retries: int = 3,
max_lifetime_retries: int = 1500,
- default_protocol_version=ProtocolVersion.LATEST,
+ default_protocol_version=ProtocolVersion.LEGACY,
):
"""Initializes a Manager on top of a TransferService.
@@ -83,6 +84,7 @@ class Manager: # pylint: disable=too-many-instance-attributes
# Ongoing transfers in the service by resource ID.
self._read_transfers: _TransferDict = {}
self._write_transfers: _TransferDict = {}
+ self._next_session_id = ctypes.c_uint32(1)
# RPC streams for read and write transfers. These are shareable by
# multiple transfers of the same type.
@@ -139,7 +141,14 @@ class Manager: # pylint: disable=too-many-instance-attributes
if protocol_version is None:
protocol_version = self._default_protocol_version
+ session_id = (
+ resource_id
+ if protocol_version is ProtocolVersion.LEGACY
+ else self.assign_session_id()
+ )
+
transfer = ReadTransfer(
+ session_id,
resource_id,
self._send_read_chunk,
self._end_read_transfer,
@@ -190,7 +199,14 @@ class Manager: # pylint: disable=too-many-instance-attributes
if protocol_version is None:
protocol_version = self._default_protocol_version
+ session_id = (
+ resource_id
+ if protocol_version is ProtocolVersion.LEGACY
+ else self.assign_session_id()
+ )
+
transfer = WriteTransfer(
+ session_id,
resource_id,
data,
self._send_write_chunk,
@@ -217,6 +233,15 @@ class Manager: # pylint: disable=too-many-instance-attributes
assert self._write_stream is not None
self._write_stream.send(chunk.to_message())
+ def assign_session_id(self) -> int:
+ new_id = self._next_session_id.value
+
+ self._next_session_id = ctypes.c_uint32(self._next_session_id.value + 1)
+ if self._next_session_id.value == 0:
+ self._next_session_id = ctypes.c_uint32(1)
+
+ return new_id
+
def _start_event_loop_thread(self):
"""Entry point for event loop thread that starts an asyncio context."""
asyncio.set_event_loop(self._loop)
@@ -291,15 +316,17 @@ class Manager: # pylint: disable=too-many-instance-attributes
# Find a transfer for the chunk in the list of active transfers.
try:
- if chunk.resource_id is not None:
- # Prioritize a resource_id if one is set.
- transfer = transfers[chunk.resource_id]
+ if chunk.protocol_version is ProtocolVersion.LEGACY:
+ transfer = next(
+ t
+ for t in transfers.values()
+ if t.resource_id == chunk.session_id
+ )
else:
- # Otherwise, match against either resource or session ID.
transfer = next(
t for t in transfers.values() if t.id == chunk.id()
)
- except (KeyError, StopIteration):
+ except StopIteration:
_LOG.error(
'TransferManager received chunk for unknown transfer %d',
chunk.id(),
diff --git a/pw_transfer/py/pw_transfer/transfer.py b/pw_transfer/py/pw_transfer/transfer.py
index 11dc55636..1cbd0d5cd 100644
--- a/pw_transfer/py/pw_transfer/transfer.py
+++ b/pw_transfer/py/pw_transfer/transfer.py
@@ -1,4 +1,4 @@
-# Copyright 2022 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -119,10 +119,9 @@ class Transfer(abc.ABC):
# A transfer has fully completed.
COMPLETE = 5
- _UNASSIGNED_SESSION_ID = 0
-
def __init__( # pylint: disable=too-many-arguments
self,
+ session_id: int,
resource_id: int,
send_chunk: Callable[[Chunk], None],
end_transfer: Callable[['Transfer'], None],
@@ -136,7 +135,7 @@ class Transfer(abc.ABC):
self.status = Status.OK
self.done = threading.Event()
- self._session_id = self._UNASSIGNED_SESSION_ID
+ self._session_id = session_id
self._resource_id = resource_id
self._send_chunk_fn = send_chunk
@@ -187,6 +186,9 @@ class Transfer(abc.ABC):
resource_id=self._resource_id,
)
+ if self._desired_protocol_version is ProtocolVersion.VERSION_TWO:
+ initial_chunk.desired_session_id = self._session_id
+
# Regardless of the desired protocol version, set any additional fields
# on the opening chunk, in case the server only runs legacy.
self._set_initial_chunk_fields(initial_chunk)
@@ -197,9 +199,7 @@ class Transfer(abc.ABC):
@property
def id(self) -> int:
"""Returns the identifier for the active transfer."""
- if self._session_id != self._UNASSIGNED_SESSION_ID:
- return self._session_id
- return self._resource_id
+ return self._session_id
@property
def resource_id(self) -> int:
@@ -278,15 +278,13 @@ class Transfer(abc.ABC):
self._configured_protocol_version = ProtocolVersion.LEGACY
self._state = Transfer._State.WAITING
- # Update the transfer's session ID in case it was expecting one to
- # be assigned by the server.
+ # Update the transfer's session ID, which will map to the
+ # transfer_id of the legacy chunk.
self._session_id = chunk.session_id
await self._handle_data_chunk(chunk)
return
- self._session_id = chunk.session_id
-
self._configured_protocol_version = ProtocolVersion(
min(
self._desired_protocol_version.value,
@@ -426,6 +424,7 @@ class WriteTransfer(Transfer):
def __init__( # pylint: disable=too-many-arguments
self,
+ session_id: int,
resource_id: int,
data: bytes,
send_chunk: Callable[[Chunk], None],
@@ -438,6 +437,7 @@ class WriteTransfer(Transfer):
progress_callback: Optional[ProgressCallback] = None,
):
super().__init__(
+ session_id,
resource_id,
send_chunk,
end_transfer,
@@ -621,6 +621,7 @@ class ReadTransfer(Transfer):
def __init__( # pylint: disable=too-many-arguments
self,
+ session_id: int,
resource_id: int,
send_chunk: Callable[[Chunk], None],
end_transfer: Callable[[Transfer], None],
@@ -635,6 +636,7 @@ class ReadTransfer(Transfer):
progress_callback: Optional[ProgressCallback] = None,
):
super().__init__(
+ session_id,
resource_id,
send_chunk,
end_transfer,
diff --git a/pw_transfer/py/tests/transfer_test.py b/pw_transfer/py/tests/transfer_test.py
index f06bbf508..8b29293f3 100644
--- a/pw_transfer/py/tests/transfer_test.py
+++ b/pw_transfer/py/tests/transfer_test.py
@@ -34,6 +34,7 @@ except ImportError:
from pigweed.pw_transfer import transfer_pb2 # type: ignore
_TRANSFER_SERVICE_ID = ids.calculate('pw.transfer.Transfer')
+_FIRST_SESSION_ID = 1
# If the default timeout is too short, some tests become flaky on Windows.
DEFAULT_TIMEOUT_S = 0.3
@@ -788,14 +789,14 @@ class TransferManagerTest(unittest.TestCase):
(
transfer_pb2.Chunk(
resource_id=39,
- session_id=280,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
),
(
transfer_pb2.Chunk(
- session_id=280,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=0,
data=b'version two',
@@ -804,7 +805,7 @@ class TransferManagerTest(unittest.TestCase):
),
(
transfer_pb2.Chunk(
- session_id=280,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
),
),
@@ -819,6 +820,7 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=39,
resource_id=39,
+ desired_session_id=_FIRST_SESSION_ID,
pending_bytes=8192,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -826,7 +828,7 @@ class TransferManagerTest(unittest.TestCase):
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=280,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -835,7 +837,7 @@ class TransferManagerTest(unittest.TestCase):
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=280,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
@@ -877,6 +879,7 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=40,
resource_id=40,
+ desired_session_id=_FIRST_SESSION_ID,
pending_bytes=8192,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -907,14 +910,14 @@ class TransferManagerTest(unittest.TestCase):
(
transfer_pb2.Chunk(
resource_id=72,
- session_id=880,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
),
(
transfer_pb2.Chunk(
- session_id=880,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
offset=0,
window_end_offset=32,
@@ -924,7 +927,7 @@ class TransferManagerTest(unittest.TestCase):
(), # In response to the first data chunk.
(
transfer_pb2.Chunk(
- session_id=880,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
@@ -940,29 +943,31 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=72,
resource_id=72,
+ desired_session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=880,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=880,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=0,
data=b'write ve',
),
transfer_pb2.Chunk(
- session_id=880,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=8,
data=b'rsion 2',
remaining_bytes=0,
),
transfer_pb2.Chunk(
- session_id=880, type=transfer_pb2.Chunk.Type.COMPLETION_ACK
+ session_id=_FIRST_SESSION_ID,
+ type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
),
],
)
@@ -1010,6 +1015,7 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=76,
resource_id=76,
+ desired_session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
@@ -1032,7 +1038,7 @@ class TransferManagerTest(unittest.TestCase):
self.assertEqual(self._received_data(), b'write v... NOPE')
def test_v2_server_error(self) -> None:
- """Tests a timeout occurring during the opening handshake."""
+ """Tests a server error occurring during the opening handshake."""
manager = pw_transfer.Manager(
self._service,
@@ -1046,14 +1052,14 @@ class TransferManagerTest(unittest.TestCase):
(
transfer_pb2.Chunk(
resource_id=43,
- session_id=680,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
),
(
transfer_pb2.Chunk(
- session_id=680,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.DATA_LOSS.value,
),
@@ -1069,6 +1075,7 @@ class TransferManagerTest(unittest.TestCase):
[
transfer_pb2.Chunk(
transfer_id=43,
+ desired_session_id=_FIRST_SESSION_ID,
resource_id=43,
pending_bytes=8192,
max_chunk_size_bytes=1024,
@@ -1077,7 +1084,7 @@ class TransferManagerTest(unittest.TestCase):
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=680,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -1085,7 +1092,8 @@ class TransferManagerTest(unittest.TestCase):
),
# Client sends a COMPLETION_ACK in response to the server.
transfer_pb2.Chunk(
- session_id=680, type=transfer_pb2.Chunk.Type.COMPLETION_ACK
+ session_id=_FIRST_SESSION_ID,
+ type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
),
],
)
@@ -1110,6 +1118,7 @@ class TransferManagerTest(unittest.TestCase):
start_chunk = transfer_pb2.Chunk(
transfer_id=41,
resource_id=41,
+ desired_session_id=_FIRST_SESSION_ID,
pending_bytes=8192,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -1138,7 +1147,7 @@ class TransferManagerTest(unittest.TestCase):
(
transfer_pb2.Chunk(
resource_id=73,
- session_id=101,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
@@ -1147,7 +1156,7 @@ class TransferManagerTest(unittest.TestCase):
(), # Don't respond to the first START_ACK_CONFIRMATION retry.
(
transfer_pb2.Chunk(
- session_id=101,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
offset=0,
window_end_offset=32,
@@ -1157,7 +1166,7 @@ class TransferManagerTest(unittest.TestCase):
(), # In response to the first data chunk.
(
transfer_pb2.Chunk(
- session_id=101,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
@@ -1168,7 +1177,7 @@ class TransferManagerTest(unittest.TestCase):
manager.write(73, b'write timeout 2')
start_ack_confirmation = transfer_pb2.Chunk(
- session_id=101,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
protocol_version=ProtocolVersion.VERSION_TWO.value,
)
@@ -1179,6 +1188,7 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=73,
resource_id=73,
+ desired_session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
@@ -1186,20 +1196,21 @@ class TransferManagerTest(unittest.TestCase):
start_ack_confirmation, # Retry 1
start_ack_confirmation, # Retry 2
transfer_pb2.Chunk(
- session_id=101,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=0,
data=b'write ti',
),
transfer_pb2.Chunk(
- session_id=101,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=8,
data=b'meout 2',
remaining_bytes=0,
),
transfer_pb2.Chunk(
- session_id=101, type=transfer_pb2.Chunk.Type.COMPLETION_ACK
+ session_id=_FIRST_SESSION_ID,
+ type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
),
],
)
@@ -1220,14 +1231,14 @@ class TransferManagerTest(unittest.TestCase):
(
transfer_pb2.Chunk(
resource_id=47,
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
),
(
transfer_pb2.Chunk(
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=0,
data=b'version two',
@@ -1238,7 +1249,7 @@ class TransferManagerTest(unittest.TestCase):
# of a COMPLETION_ACK.
(
transfer_pb2.Chunk(
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=0,
data=b'version two',
@@ -1247,7 +1258,7 @@ class TransferManagerTest(unittest.TestCase):
),
(
transfer_pb2.Chunk(
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
),
),
@@ -1262,6 +1273,7 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=47,
resource_id=47,
+ desired_session_id=_FIRST_SESSION_ID,
pending_bytes=8192,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -1269,20 +1281,20 @@ class TransferManagerTest(unittest.TestCase):
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
max_chunk_size_bytes=1024,
window_end_offset=8192,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
# Completion should be re-sent following the repeated chunk.
transfer_pb2.Chunk(
- session_id=580,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
@@ -1305,14 +1317,14 @@ class TransferManagerTest(unittest.TestCase):
(
transfer_pb2.Chunk(
resource_id=47,
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
),
(
transfer_pb2.Chunk(
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.DATA,
offset=0,
data=b'dropped completion',
@@ -1331,6 +1343,7 @@ class TransferManagerTest(unittest.TestCase):
transfer_pb2.Chunk(
transfer_id=47,
resource_id=47,
+ desired_session_id=_FIRST_SESSION_ID,
pending_bytes=8192,
max_chunk_size_bytes=1024,
window_end_offset=8192,
@@ -1338,30 +1351,30 @@ class TransferManagerTest(unittest.TestCase):
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
max_chunk_size_bytes=1024,
window_end_offset=8192,
protocol_version=ProtocolVersion.VERSION_TWO.value,
),
transfer_pb2.Chunk(
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
# The completion should be retried per the usual retry flow.
transfer_pb2.Chunk(
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
transfer_pb2.Chunk(
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
transfer_pb2.Chunk(
- session_id=980,
+ session_id=_FIRST_SESSION_ID,
type=transfer_pb2.Chunk.Type.COMPLETION,
status=Status.OK.value,
),
@@ -1412,7 +1425,7 @@ class ProgressStatsTest(unittest.TestCase):
if __name__ == '__main__':
- # TODO(b/265975025): Only run this test in upstream Pigweed until the
+ # TODO: b/265975025 - Only run this test in upstream Pigweed until the
# occasional hangs are fixed.
if os.environ.get('PW_ROOT') and os.environ.get(
'PW_ROOT'
diff --git a/pw_transfer/test_rpc_server.cc b/pw_transfer/test_rpc_server.cc
deleted file mode 100644
index fa911b732..000000000
--- a/pw_transfer/test_rpc_server.cc
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright 2022 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-// Simple RPC server with the transfer service registered. Reads HDLC frames
-// with RPC packets through a socket. The transfer service reads and writes to
-// files within a given directory. The name of a file is its resource ID.
-
-#include <cstddef>
-#include <filesystem>
-#include <string>
-#include <thread>
-#include <variant>
-#include <vector>
-
-#include "pw_assert/check.h"
-#include "pw_log/log.h"
-#include "pw_rpc_system_server/rpc_server.h"
-#include "pw_rpc_system_server/socket.h"
-#include "pw_stream/std_file_stream.h"
-#include "pw_thread/detached_thread.h"
-#include "pw_thread_stl/options.h"
-#include "pw_transfer/atomic_file_transfer_handler.h"
-#include "pw_transfer/transfer.h"
-#include "pw_transfer_test/test_server.raw_rpc.pb.h"
-
-namespace pw::transfer {
-namespace {
-class TestServerService
- : public pw_rpc::raw::TestServer::Service<TestServerService> {
- public:
- TestServerService(TransferService& transfer_service)
- : transfer_service_(transfer_service) {}
-
- ~TestServerService() { UnregisterHandlers(); }
-
- void UnregisterHandlers() {
- for (auto handler : file_transfer_handlers_) {
- transfer_service_.UnregisterHandler(*handler);
- }
- }
-
- void set_directory(const char* directory) { directory_ = directory; }
-
- void ReloadTransferFiles(ConstByteSpan, rpc::RawUnaryResponder&) {
- LoadFileHandlers();
- }
-
- void LoadFileHandlers() {
- PW_LOG_INFO("Reloading file handlers from %s", directory_.c_str());
- UnregisterHandlers();
- file_transfer_handlers_.clear();
-
- for (const auto& entry : std::filesystem::directory_iterator(directory_)) {
- if (!entry.is_regular_file()) {
- continue;
- }
-
- int resource_id = std::atoi(entry.path().filename().c_str());
- if (resource_id > 0) {
- PW_LOG_DEBUG("Found transfer file %d", resource_id);
- auto handler = std::make_shared<AtomicFileTransferHandler>(
- resource_id, entry.path().c_str());
- transfer_service_.RegisterHandler(*handler);
- file_transfer_handlers_.emplace_back(handler);
- }
- }
- }
-
- private:
- TransferService& transfer_service_;
- std::string directory_;
- std::vector<std::shared_ptr<AtomicFileTransferHandler>>
- file_transfer_handlers_;
-};
-
-constexpr size_t kChunkSizeBytes = 256;
-constexpr size_t kMaxReceiveSizeBytes = 1024;
-
-std::array<std::byte, kChunkSizeBytes> chunk_buffer;
-std::array<std::byte, kChunkSizeBytes> encode_buffer;
-transfer::Thread<4, 4> transfer_thread(chunk_buffer, encode_buffer);
-TransferService transfer_service(transfer_thread, kMaxReceiveSizeBytes);
-TestServerService test_server_service(transfer_service);
-
-void RunServer(int socket_port, const char* directory) {
- rpc::system_server::set_socket_port(socket_port);
-
- test_server_service.set_directory(directory);
- test_server_service.LoadFileHandlers();
-
- rpc::system_server::Init();
- rpc::system_server::Server().RegisterService(test_server_service,
- transfer_service);
-
- thread::DetachedThread(thread::stl::Options(), transfer_thread);
-
- PW_LOG_INFO("Starting pw_rpc server");
- PW_CHECK_OK(rpc::system_server::Start());
-}
-
-} // namespace
-} // namespace pw::transfer
-
-int main(int argc, char* argv[]) {
- if (argc != 3) {
- PW_LOG_ERROR("Usage: %s PORT DIR", argv[0]);
- return 1;
- }
-
- pw::transfer::RunServer(std::atoi(argv[1]), argv[2]);
- return 0;
-}
diff --git a/pw_transfer/transfer.cc b/pw_transfer/transfer.cc
index 722ed8217..eb04aeeb1 100644
--- a/pw_transfer/transfer.cc
+++ b/pw_transfer/transfer.cc
@@ -30,11 +30,23 @@ void TransferService::HandleChunk(ConstByteSpan message,
}
if (chunk->IsInitialChunk()) {
- uint32_t session_id =
- chunk->is_legacy() ? chunk->session_id() : GenerateNewSessionId();
uint32_t resource_id =
chunk->is_legacy() ? chunk->session_id() : chunk->resource_id().value();
+ uint32_t session_id;
+ if (chunk->is_legacy()) {
+ session_id = chunk->session_id();
+ } else if (chunk->desired_session_id().has_value()) {
+ session_id = chunk->desired_session_id().value();
+ } else {
+ // Non-legacy start chunks are required to use desired_session_id.
+ thread_.SendServerStatus(type,
+ chunk->session_id(),
+ chunk->protocol_version(),
+ Status::DataLoss());
+ return;
+ }
+
thread_.StartServerTransfer(type,
chunk->protocol_version(),
session_id,
diff --git a/pw_transfer/transfer.proto b/pw_transfer/transfer.proto
index 839eb2cd2..2d3c5d4c3 100644
--- a/pw_transfer/transfer.proto
+++ b/pw_transfer/transfer.proto
@@ -164,14 +164,13 @@ message Chunk {
// TODO(konkers): Implement this behavior.
COMPLETION_ACK = 5;
- // Acknowledges a transfer start request, assigning a session ID for the
+ // Acknowledges a transfer start request, accepting the session ID for the
// transfer and optionally negotiating the protocol version. Sent from
// server to client.
START_ACK = 6;
- // Confirmation of a START_ACK's assigned session ID and negotiated
- // parameters, sent by the client to the server. Initiates the data transfer
- // proper.
+ // Confirmation of a START_ACK's negotiated parameters, sent by the client
+ // to the server. Initiates the data transfer proper.
START_ACK_CONFIRMATION = 7;
};
@@ -195,8 +194,8 @@ message Chunk {
// Write ← ID of transferable resource
optional uint32 resource_id = 11;
- // Unique identifier for a specific transfer session. Assigned by a transfer
- // service during the initial handshake phase, and persists for the remainder
+ // Unique identifier for a specific transfer session. Chosen by the transfer
+ // client during the initial handshake phase, and persists for the remainder
// of that transfer operation.
//
// Read → ID of transfer session
@@ -214,4 +213,15 @@ message Chunk {
// Write → Desired (START) or configured (START_ACK_CONFIRMATION) version.
// Write ← Configured protocol version (START_ACK).
optional uint32 protocol_version = 13;
+
+ // Unique identifier for a specific transfer session. Chosen by the transfer
+ // client during the initial handshake phase. This field is used to request a
+ // session during the handshake, after which the regular session_id field is
+ // used.
+ //
+ // Read → Requested ID of transfer session
+ // Read ← N/A
+ // Write → Requested ID of transfer session
+ // Write ← N/A
+ optional uint32 desired_session_id = 14;
}
diff --git a/pw_transfer/transfer_test.cc b/pw_transfer/transfer_test.cc
index 8b5312107..fcc4e1999 100644
--- a/pw_transfer/transfer_test.cc
+++ b/pw_transfer/transfer_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -106,6 +106,7 @@ class SimpleReadTransfer final : public ReadOnlyHandler {
};
constexpr auto kData = bytes::Initialized<32>([](size_t i) { return i; });
+constexpr uint32_t kArbitrarySessionId = 123;
class ReadTransfer : public ::testing::Test {
protected:
@@ -113,7 +114,10 @@ class ReadTransfer : public ::testing::Test {
: handler_(3, kData),
transfer_thread_(span(data_buffer_).first(max_chunk_size_bytes),
encode_buffer_),
- ctx_(transfer_thread_, 64),
+ ctx_(transfer_thread_,
+ 64,
+ // Use a long timeout to avoid accidentally triggering timeouts.
+ std::chrono::minutes(1)),
system_thread_(TransferThreadOptions(), transfer_thread_) {
ctx_.service().RegisterHandler(handler_);
@@ -393,10 +397,12 @@ TEST_F(ReadTransfer, MaxChunkSize_Client) {
}
TEST_F(ReadTransfer, HandlerIsClearedAfterTransfer) {
+ // Request an end offset smaller than the data size to prevent the server
+ // from sending a final chunk.
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
.set_session_id(3)
- .set_window_end_offset(64)
+ .set_window_end_offset(16)
.set_offset(0)));
ctx_.SendClientStream(
EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
@@ -412,10 +418,12 @@ TEST_F(ReadTransfer, HandlerIsClearedAfterTransfer) {
handler_.prepare_read_called = false;
handler_.finalize_read_called = false;
+ // Request an end offset smaller than the data size to prevent the server
+ // from sending a final chunk.
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
.set_session_id(3)
- .set_window_end_offset(64)
+ .set_window_end_offset(16)
.set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1605,6 +1613,7 @@ TEST_F(WriteTransferMaxBytes16, Service_SetMaxPendingBytes) {
TEST_F(ReadTransfer, Version2_SimpleTransfer) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1612,13 +1621,14 @@ TEST_F(ReadTransfer, Version2_SimpleTransfer) {
EXPECT_TRUE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_FALSE(chunk.desired_session_id().has_value());
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 3u);
// Complete the handshake by confirming the server's ACK and sending the first
@@ -1626,7 +1636,7 @@ TEST_F(ReadTransfer, Version2_SimpleTransfer) {
rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(64)
.set_offset(0)));
@@ -1640,7 +1650,7 @@ TEST_F(ReadTransfer, Version2_SimpleTransfer) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c1.type(), Chunk::Type::kData);
- EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.session_id(), kArbitrarySessionId);
EXPECT_EQ(c1.offset(), 0u);
ASSERT_TRUE(c1.has_payload());
ASSERT_EQ(c1.payload().size(), kData.size());
@@ -1650,12 +1660,12 @@ TEST_F(ReadTransfer, Version2_SimpleTransfer) {
Chunk c2 = DecodeChunk(ctx_.responses()[2]);
EXPECT_EQ(c2.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c2.type(), Chunk::Type::kData);
- EXPECT_EQ(c2.session_id(), 1u);
+ EXPECT_EQ(c2.session_id(), kArbitrarySessionId);
EXPECT_FALSE(c2.has_payload());
EXPECT_EQ(c2.remaining_bytes(), 0u);
- ctx_.SendClientStream(
- EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
+ ctx_.SendClientStream(EncodeChunk(Chunk::Final(
+ ProtocolVersion::kVersionTwo, kArbitrarySessionId, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -1665,6 +1675,7 @@ TEST_F(ReadTransfer, Version2_SimpleTransfer) {
TEST_F(ReadTransfer, Version2_MultiChunk) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1672,13 +1683,13 @@ TEST_F(ReadTransfer, Version2_MultiChunk) {
EXPECT_TRUE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 3u);
// Complete the handshake by confirming the server's ACK and sending the first
@@ -1686,7 +1697,7 @@ TEST_F(ReadTransfer, Version2_MultiChunk) {
rpc::test::WaitForPackets(ctx_.output(), 3, [this] {
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(64)
.set_max_chunk_size_bytes(16)
.set_offset(0)));
@@ -1699,7 +1710,7 @@ TEST_F(ReadTransfer, Version2_MultiChunk) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c1.type(), Chunk::Type::kData);
- EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.session_id(), kArbitrarySessionId);
EXPECT_EQ(c1.offset(), 0u);
ASSERT_TRUE(c1.has_payload());
ASSERT_EQ(c1.payload().size(), 16u);
@@ -1709,7 +1720,7 @@ TEST_F(ReadTransfer, Version2_MultiChunk) {
Chunk c2 = DecodeChunk(ctx_.responses()[2]);
EXPECT_EQ(c2.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c2.type(), Chunk::Type::kData);
- EXPECT_EQ(c2.session_id(), 1u);
+ EXPECT_EQ(c2.session_id(), kArbitrarySessionId);
EXPECT_EQ(c2.offset(), 16u);
ASSERT_TRUE(c2.has_payload());
ASSERT_EQ(c2.payload().size(), 16u);
@@ -1721,12 +1732,12 @@ TEST_F(ReadTransfer, Version2_MultiChunk) {
Chunk c3 = DecodeChunk(ctx_.responses()[3]);
EXPECT_EQ(c3.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c3.type(), Chunk::Type::kData);
- EXPECT_EQ(c3.session_id(), 1u);
+ EXPECT_EQ(c3.session_id(), kArbitrarySessionId);
EXPECT_FALSE(c3.has_payload());
EXPECT_EQ(c3.remaining_bytes(), 0u);
- ctx_.SendClientStream(
- EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
+ ctx_.SendClientStream(EncodeChunk(Chunk::Final(
+ ProtocolVersion::kVersionTwo, kArbitrarySessionId, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -1736,6 +1747,7 @@ TEST_F(ReadTransfer, Version2_MultiChunk) {
TEST_F(ReadTransfer, Version2_MultiParameters) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1743,20 +1755,20 @@ TEST_F(ReadTransfer, Version2_MultiParameters) {
EXPECT_TRUE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 3u);
// Complete the handshake by confirming the server's ACK and sending the first
// read transfer parameters.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(16)
.set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1766,7 +1778,7 @@ TEST_F(ReadTransfer, Version2_MultiParameters) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c1.type(), Chunk::Type::kData);
- EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.session_id(), kArbitrarySessionId);
EXPECT_EQ(c1.offset(), 0u);
ASSERT_TRUE(c1.has_payload());
ASSERT_EQ(c1.payload().size(), 16u);
@@ -1776,7 +1788,7 @@ TEST_F(ReadTransfer, Version2_MultiParameters) {
rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersContinue)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(64)
.set_offset(16)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1787,7 +1799,7 @@ TEST_F(ReadTransfer, Version2_MultiParameters) {
Chunk c2 = DecodeChunk(ctx_.responses()[2]);
EXPECT_EQ(c2.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c2.type(), Chunk::Type::kData);
- EXPECT_EQ(c2.session_id(), 1u);
+ EXPECT_EQ(c2.session_id(), kArbitrarySessionId);
EXPECT_EQ(c2.offset(), 16u);
ASSERT_TRUE(c2.has_payload());
ASSERT_EQ(c2.payload().size(), 16u);
@@ -1799,12 +1811,12 @@ TEST_F(ReadTransfer, Version2_MultiParameters) {
Chunk c3 = DecodeChunk(ctx_.responses()[3]);
EXPECT_EQ(c3.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c3.type(), Chunk::Type::kData);
- EXPECT_EQ(c3.session_id(), 1u);
+ EXPECT_EQ(c3.session_id(), kArbitrarySessionId);
EXPECT_FALSE(c3.has_payload());
EXPECT_EQ(c3.remaining_bytes(), 0u);
- ctx_.SendClientStream(
- EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
+ ctx_.SendClientStream(EncodeChunk(Chunk::Final(
+ ProtocolVersion::kVersionTwo, kArbitrarySessionId, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -1814,6 +1826,7 @@ TEST_F(ReadTransfer, Version2_MultiParameters) {
TEST_F(ReadTransfer, Version2_ClientTerminatesDuringHandshake) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1821,18 +1834,19 @@ TEST_F(ReadTransfer, Version2_ClientTerminatesDuringHandshake) {
EXPECT_TRUE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 3u);
// Send a terminating chunk instead of the third part of the handshake.
- ctx_.SendClientStream(EncodeChunk(Chunk::Final(
- ProtocolVersion::kVersionTwo, 1, Status::ResourceExhausted())));
+ ctx_.SendClientStream(EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo,
+ kArbitrarySessionId,
+ Status::ResourceExhausted())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -1842,6 +1856,7 @@ TEST_F(ReadTransfer, Version2_ClientTerminatesDuringHandshake) {
TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1849,20 +1864,20 @@ TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
EXPECT_TRUE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 3u);
// Complete the handshake by confirming the server's ACK and sending the first
// read transfer parameters.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(16)
.set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1872,7 +1887,7 @@ TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c1.type(), Chunk::Type::kData);
- EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.session_id(), kArbitrarySessionId);
EXPECT_EQ(c1.offset(), 0u);
ASSERT_TRUE(c1.has_payload());
ASSERT_EQ(c1.payload().size(), 16u);
@@ -1883,7 +1898,7 @@ TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
// server should terminate the transfer.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersContinue)
- .set_session_id(1)
+ .set_session_id(3)
.set_window_end_offset(64)
.set_offset(16)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1893,7 +1908,7 @@ TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
ASSERT_TRUE(chunk.status().has_value());
EXPECT_EQ(chunk.status().value(), Status::Internal());
@@ -1904,6 +1919,7 @@ TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
TEST_F(ReadTransfer, Version2_BadParametersInHandshake) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(3)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1915,14 +1931,14 @@ TEST_F(ReadTransfer, Version2_BadParametersInHandshake) {
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 3u);
// Complete the handshake, but send an invalid parameters chunk. The server
// should terminate the transfer.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(0)
.set_offset(0)));
@@ -1933,7 +1949,7 @@ TEST_F(ReadTransfer, Version2_BadParametersInHandshake) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(c1.type(), Chunk::Type::kCompletion);
- EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.session_id(), kArbitrarySessionId);
ASSERT_TRUE(c1.status().has_value());
EXPECT_EQ(c1.status().value(), Status::ResourceExhausted());
}
@@ -1941,6 +1957,7 @@ TEST_F(ReadTransfer, Version2_BadParametersInHandshake) {
TEST_F(ReadTransfer, Version2_InvalidResourceId) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(99)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1949,6 +1966,7 @@ TEST_F(ReadTransfer, Version2_InvalidResourceId) {
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.status().value(), Status::NotFound());
}
@@ -1959,6 +1977,7 @@ TEST_F(ReadTransfer, Version2_PrepareError) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(99)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1966,6 +1985,7 @@ TEST_F(ReadTransfer, Version2_PrepareError) {
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 99u);
EXPECT_EQ(chunk.status().value(), Status::DataLoss());
}
@@ -1973,6 +1993,7 @@ TEST_F(ReadTransfer, Version2_PrepareError) {
TEST_F(WriteTransfer, Version2_SimpleTransfer) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1980,19 +2001,19 @@ TEST_F(WriteTransfer, Version2_SimpleTransfer) {
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 7u);
// Complete the handshake by confirming the server's ACK.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
// Server should respond by sending its initial transfer parameters.
@@ -2001,7 +2022,7 @@ TEST_F(WriteTransfer, Version2_SimpleTransfer) {
chunk = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 32u);
ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
@@ -2010,7 +2031,7 @@ TEST_F(WriteTransfer, Version2_SimpleTransfer) {
// Send all of our data.
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(0)
.set_payload(kData)
.set_remaining_bytes(0)));
@@ -2021,14 +2042,14 @@ TEST_F(WriteTransfer, Version2_SimpleTransfer) {
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
ASSERT_TRUE(chunk.status().has_value());
EXPECT_EQ(chunk.status().value(), OkStatus());
// Send the completion acknowledgement.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
@@ -2041,6 +2062,7 @@ TEST_F(WriteTransfer, Version2_SimpleTransfer) {
TEST_F(WriteTransfer, Version2_Multichunk) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2048,19 +2070,19 @@ TEST_F(WriteTransfer, Version2_Multichunk) {
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 7u);
// Complete the handshake by confirming the server's ACK.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
// Server should respond by sending its initial transfer parameters.
@@ -2069,7 +2091,7 @@ TEST_F(WriteTransfer, Version2_Multichunk) {
chunk = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 32u);
ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
@@ -2078,12 +2100,12 @@ TEST_F(WriteTransfer, Version2_Multichunk) {
// Send all of our data across two chunks.
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(0)
.set_payload(span(kData).first(8))));
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(8)
.set_payload(span(kData).subspan(8))
.set_remaining_bytes(0)));
@@ -2094,14 +2116,14 @@ TEST_F(WriteTransfer, Version2_Multichunk) {
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
ASSERT_TRUE(chunk.status().has_value());
EXPECT_EQ(chunk.status().value(), OkStatus());
// Send the completion acknowledgement.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
@@ -2114,6 +2136,7 @@ TEST_F(WriteTransfer, Version2_Multichunk) {
TEST_F(WriteTransfer, Version2_ContinueParameters) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2121,19 +2144,19 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 7u);
// Complete the handshake by confirming the server's ACK.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
// Server should respond by sending its initial transfer parameters.
@@ -2142,7 +2165,7 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
chunk = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 32u);
ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
@@ -2151,7 +2174,7 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
// Send all of our data across several chunks.
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(0)
.set_payload(span(kData).first(8))));
@@ -2160,7 +2183,7 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(8)
.set_payload(span(kData).subspan(8, 8))));
@@ -2170,13 +2193,13 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kParametersContinue);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.offset(), 16u);
EXPECT_EQ(chunk.window_end_offset(), 32u);
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(16)
.set_payload(span(kData).subspan(16, 8))));
@@ -2186,13 +2209,13 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kParametersContinue);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.offset(), 24u);
EXPECT_EQ(chunk.window_end_offset(), 32u);
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_offset(24)
.set_payload(span(kData).subspan(24))
.set_remaining_bytes(0)));
@@ -2203,14 +2226,14 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
ASSERT_TRUE(chunk.status().has_value());
EXPECT_EQ(chunk.status().value(), OkStatus());
// Send the completion acknowledgement.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 5u);
@@ -2223,6 +2246,7 @@ TEST_F(WriteTransfer, Version2_ContinueParameters) {
TEST_F(WriteTransfer, Version2_ClientTerminatesDuringHandshake) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2230,18 +2254,20 @@ TEST_F(WriteTransfer, Version2_ClientTerminatesDuringHandshake) {
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 7u);
// Send an error chunk instead of completing the handshake.
- ctx_.SendClientStream(EncodeChunk(Chunk::Final(
- ProtocolVersion::kVersionTwo, 1, Status::FailedPrecondition())));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo,
+ kArbitrarySessionId,
+ Status::FailedPrecondition())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_write_called);
@@ -2251,6 +2277,7 @@ TEST_F(WriteTransfer, Version2_ClientTerminatesDuringHandshake) {
TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2258,19 +2285,19 @@ TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 7u);
// Complete the handshake by confirming the server's ACK.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
// Server should respond by sending its initial transfer parameters.
@@ -2279,7 +2306,7 @@ TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
chunk = DecodeChunk(ctx_.responses()[1]);
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.offset(), 0u);
EXPECT_EQ(chunk.window_end_offset(), 32u);
ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
@@ -2289,7 +2316,7 @@ TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
// instead.
ctx_.SendClientStream<64>(
EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
- .set_session_id(1)
+ .set_session_id(7)
.set_offset(0)
.set_payload(kData)
.set_remaining_bytes(0)));
@@ -2306,7 +2333,7 @@ TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
// Send the completion acknowledgement.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
- .set_session_id(1)));
+ .set_session_id(kArbitrarySessionId)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
@@ -2315,6 +2342,7 @@ TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
TEST_F(WriteTransfer, Version2_InvalidResourceId) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(99)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2323,6 +2351,8 @@ TEST_F(WriteTransfer, Version2_InvalidResourceId) {
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
+ EXPECT_FALSE(chunk.resource_id().has_value());
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.status().value(), Status::NotFound());
}
@@ -2448,6 +2478,7 @@ TEST_F(ReadTransferLowMaxRetries, FailsAfterLifetimeRetryCount) {
TEST_F(ReadTransferLowMaxRetries, Version2_FailsAfterLifetimeRetryCount) {
ctx_.SendClientStream(
EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
.set_resource_id(9)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2455,18 +2486,18 @@ TEST_F(ReadTransferLowMaxRetries, Version2_FailsAfterLifetimeRetryCount) {
EXPECT_TRUE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
- // First, the server responds with a START_ACK, assigning a session ID and
+ // First, the server responds with a START_ACK, accepting the session ID and
// confirming the protocol version.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
- EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
EXPECT_EQ(chunk.resource_id(), 9u);
// Time out twice. Server should retry both times.
- transfer_thread_.SimulateServerTimeout(1);
- transfer_thread_.SimulateServerTimeout(1);
+ transfer_thread_.SimulateServerTimeout(kArbitrarySessionId);
+ transfer_thread_.SimulateServerTimeout(kArbitrarySessionId);
ASSERT_EQ(ctx_.total_responses(), 3u);
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
@@ -2474,7 +2505,7 @@ TEST_F(ReadTransferLowMaxRetries, Version2_FailsAfterLifetimeRetryCount) {
// Complete the handshake, allowing the transfer to continue.
ctx_.SendClientStream(EncodeChunk(
Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
- .set_session_id(1)
+ .set_session_id(kArbitrarySessionId)
.set_window_end_offset(16)
.set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -2484,22 +2515,89 @@ TEST_F(ReadTransferLowMaxRetries, Version2_FailsAfterLifetimeRetryCount) {
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
// Time out three more times. The transfer should terminate.
- transfer_thread_.SimulateServerTimeout(1);
+ transfer_thread_.SimulateServerTimeout(kArbitrarySessionId);
ASSERT_EQ(ctx_.total_responses(), 5u);
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
- transfer_thread_.SimulateServerTimeout(1);
+ transfer_thread_.SimulateServerTimeout(kArbitrarySessionId);
ASSERT_EQ(ctx_.total_responses(), 6u);
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.type(), Chunk::Type::kData);
- transfer_thread_.SimulateServerTimeout(1);
+ transfer_thread_.SimulateServerTimeout(kArbitrarySessionId);
ASSERT_EQ(ctx_.total_responses(), 7u);
chunk = DecodeChunk(ctx_.responses().back());
EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
EXPECT_EQ(chunk.status(), Status::DeadlineExceeded());
}
+TEST_F(WriteTransfer, Version2_ClientRetriesOpeningChunk) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ // First, the server responds with a START_ACK, accepting the session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ // Reset prepare_write_called to ensure it isn't called again.
+ handler_.prepare_write_called = false;
+
+ // Client re-sends the same chunk instead of finishing the handshake.
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_desired_session_id(kArbitrarySessionId)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The server should re-send the same START_ACK without reinitializing the
+ // handler.
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ EXPECT_FALSE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+}
+
+TEST_F(WriteTransfer, Version2_RegularSessionIdInStartChunk) {
+ // Client incorrectly sets session_id instead of desired_session_id in its
+ // START chunk. Server should immediately respond with a protocol error.
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_session_id(kArbitrarySessionId)
+ .set_resource_id(99)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_FALSE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), kArbitrarySessionId);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.status().value(), Status::DataLoss());
+}
+
} // namespace
} // namespace pw::transfer::test
diff --git a/pw_transfer/transfer_thread.cc b/pw_transfer/transfer_thread.cc
index c0d03efa1..84bf92651 100644
--- a/pw_transfer/transfer_thread.cc
+++ b/pw_transfer/transfer_thread.cc
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -98,22 +98,32 @@ chrono::SystemClock::time_point TransferThread::GetNextTransferTimeout() const {
return timeout;
}
-void TransferThread::StartTransfer(TransferType type,
- ProtocolVersion version,
- uint32_t session_id,
- uint32_t resource_id,
- ConstByteSpan raw_chunk,
- stream::Stream* stream,
- const TransferParameters& max_parameters,
- Function<void(Status)>&& on_completion,
- chrono::SystemClock::duration timeout,
- uint8_t max_retries,
- uint32_t max_lifetime_retries) {
+void TransferThread::StartTransfer(
+ TransferType type,
+ ProtocolVersion version,
+ uint32_t session_id,
+ uint32_t resource_id,
+ ConstByteSpan raw_chunk,
+ stream::Stream* stream,
+ const TransferParameters& max_parameters,
+ Function<void(Status)>&& on_completion,
+ chrono::SystemClock::duration timeout,
+ chrono::SystemClock::duration initial_timeout,
+ uint8_t max_retries,
+ uint32_t max_lifetime_retries) {
// Block until the last event has been processed.
next_event_ownership_.acquire();
bool is_client_transfer = stream != nullptr;
+ if (is_client_transfer) {
+ if (version == ProtocolVersion::kLegacy) {
+ session_id = resource_id;
+ } else if (session_id == Context::kUnassignedSessionId) {
+ session_id = AssignSessionId();
+ }
+ }
+
next_event_.type = is_client_transfer ? EventType::kNewClientTransfer
: EventType::kNewServerTransfer;
@@ -128,6 +138,7 @@ void TransferThread::StartTransfer(TransferType type,
.resource_id = resource_id,
.max_parameters = &max_parameters,
.timeout = timeout,
+ .initial_timeout = initial_timeout,
.max_retries = max_retries,
.max_lifetime_retries = max_lifetime_retries,
.transfer_thread = this,
@@ -158,11 +169,7 @@ void TransferThread::StartTransfer(TransferType type,
// No handler exists for the transfer: return a NOT_FOUND.
next_event_.type = EventType::kSendStatusChunk;
next_event_.send_status_chunk = {
- // Identify the status chunk using the requested resource ID rather
- // than the session ID. In legacy, the two are the same, whereas in
- // v2+ the client has not yet been assigned a session.
- .session_id = resource_id,
- .set_resource_id = version == ProtocolVersion::kVersionTwo,
+ .session_id = session_id,
.protocol_version = version,
.status = Status::NotFound().code(),
.stream = type == TransferType::kTransmit
@@ -196,7 +203,7 @@ void TransferThread::ProcessChunk(EventType type, ConstByteSpan chunk) {
next_event_.type = type;
next_event_.chunk = {
.context_identifier = identifier->value(),
- .match_resource_id = identifier->is_resource(),
+ .match_resource_id = identifier->is_legacy(),
.data = chunk_buffer_.data(),
.size = chunk.size(),
};
@@ -204,6 +211,24 @@ void TransferThread::ProcessChunk(EventType type, ConstByteSpan chunk) {
event_notification_.release();
}
+void TransferThread::SendStatus(TransferStream stream,
+ uint32_t session_id,
+ ProtocolVersion version,
+ Status status) {
+ // Block until the last event has been processed.
+ next_event_ownership_.acquire();
+
+ next_event_.type = EventType::kSendStatusChunk;
+ next_event_.send_status_chunk = {
+ .session_id = session_id,
+ .protocol_version = version,
+ .status = status.code(),
+ .stream = stream,
+ };
+
+ event_notification_.release();
+}
+
void TransferThread::EndTransfer(EventType type,
uint32_t session_id,
Status status,
@@ -319,9 +344,7 @@ void TransferThread::HandleEvent(const internal::Event& event) {
} else if (event.type == EventType::kNewServerTransfer) {
// On the server, send a status chunk back to the client.
SendStatusChunk(
- {.session_id = event.new_transfer.resource_id,
- .set_resource_id = event.new_transfer.protocol_version ==
- ProtocolVersion::kVersionTwo,
+ {.session_id = event.new_transfer.session_id,
.protocol_version = event.new_transfer.protocol_version,
.status = Status::ResourceExhausted().code(),
.stream = event.new_transfer.type == TransferType::kTransmit
@@ -394,10 +417,6 @@ void TransferThread::SendStatusChunk(
Chunk chunk =
Chunk::Final(event.protocol_version, event.session_id, event.status);
- if (event.set_resource_id) {
- chunk.set_resource_id(event.session_id);
- }
-
Result<ConstByteSpan> result = chunk.Encode(chunk_buffer_);
if (!result.ok()) {
PW_LOG_ERROR("Failed to encode final chunk for transfer %u",
@@ -412,6 +431,15 @@ void TransferThread::SendStatusChunk(
}
}
+// Should only be called with the `next_event_ownership_` lock held.
+uint32_t TransferThread::AssignSessionId() {
+ uint32_t session_id = next_session_id_++;
+ if (session_id == 0) {
+ session_id = next_session_id_++;
+ }
+ return session_id;
+}
+
} // namespace pw::transfer::internal
PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_transfer/transfer_thread_test.cc b/pw_transfer/transfer_thread_test.cc
index d83f8298d..0df41ec6f 100644
--- a/pw_transfer/transfer_thread_test.cc
+++ b/pw_transfer/transfer_thread_test.cc
@@ -32,6 +32,10 @@ namespace {
using internal::Chunk;
+// Effectively unlimited timeout as these tests should never hit it.
+constexpr chrono::SystemClock::duration kNeverTimeout =
+ std::chrono::seconds(60);
+
// TODO(frolv): Have a generic way to obtain a thread for testing on any system.
thread::Options& TransferThreadOptions() {
static thread::stl::Options options;
@@ -111,7 +115,7 @@ TEST_F(TransferThreadTest, AddTransferHandler) {
3,
{},
max_parameters_,
- std::chrono::seconds(2),
+ kNeverTimeout,
3,
10);
@@ -136,7 +140,7 @@ TEST_F(TransferThreadTest, RemoveTransferHandler) {
3,
{},
max_parameters_,
- std::chrono::seconds(2),
+ kNeverTimeout,
3,
10);
@@ -173,7 +177,7 @@ TEST_F(TransferThreadTest, ProcessChunk_SendsWindow) {
.set_max_chunk_size_bytes(8)
.set_offset(0)),
max_parameters_,
- std::chrono::seconds(2),
+ kNeverTimeout,
3,
10);
});
@@ -220,7 +224,7 @@ TEST_F(TransferThreadTest, StartTransferExhausted_Server) {
.set_max_chunk_size_bytes(8)
.set_offset(0)),
max_parameters_,
- std::chrono::seconds(2),
+ kNeverTimeout,
3,
10);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -244,7 +248,7 @@ TEST_F(TransferThreadTest, StartTransferExhausted_Server) {
.set_max_chunk_size_bytes(8)
.set_offset(0)),
max_parameters_,
- std::chrono::seconds(2),
+ kNeverTimeout,
3,
10);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -279,7 +283,8 @@ TEST_F(TransferThreadTest, StartTransferExhausted_Client) {
&buffer3,
max_parameters_,
[&status3](Status status) { status3 = status; },
- std::chrono::seconds(2),
+ kNeverTimeout,
+ kNeverTimeout,
3,
10);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -296,7 +301,8 @@ TEST_F(TransferThreadTest, StartTransferExhausted_Client) {
&buffer4,
max_parameters_,
[&status4](Status status) { status4 = status; },
- std::chrono::seconds(2),
+ kNeverTimeout,
+ kNeverTimeout,
3,
10);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -322,7 +328,7 @@ TEST_F(TransferThreadTest, VersionTwo_NoHandler) {
/*resource_id=*/7,
{},
max_parameters_,
- std::chrono::seconds(2),
+ kNeverTimeout,
3,
10);
@@ -333,10 +339,10 @@ TEST_F(TransferThreadTest, VersionTwo_NoHandler) {
ASSERT_EQ(ctx_.total_responses(), 1u);
Result<Chunk::Identifier> id = Chunk::ExtractIdentifier(ctx_.response());
ASSERT_TRUE(id.ok());
- EXPECT_EQ(id->value(), 7u);
+ EXPECT_EQ(id->value(), 421u);
auto chunk = DecodeChunk(ctx_.response());
- EXPECT_EQ(chunk.session_id(), 7u);
- EXPECT_EQ(chunk.resource_id(), 7u);
+ EXPECT_EQ(chunk.session_id(), 421u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
ASSERT_TRUE(chunk.status().has_value());
EXPECT_EQ(chunk.status().value(), Status::NotFound());
diff --git a/pw_transfer/ts/client.ts b/pw_transfer/ts/client.ts
index c39e37775..79f397df2 100644
--- a/pw_transfer/ts/client.ts
+++ b/pw_transfer/ts/client.ts
@@ -19,8 +19,8 @@ import {
BidirectionalStreamingMethodStub,
ServiceClient,
} from 'pigweedjs/pw_rpc';
-import {Status} from 'pigweedjs/pw_status';
-import {Chunk} from 'pigweedjs/protos/pw_transfer/transfer_pb';
+import { Status } from 'pigweedjs/pw_status';
+import { Chunk } from 'pigweedjs/protos/pw_transfer/transfer_pb';
import {
ReadTransfer,
@@ -49,8 +49,8 @@ const DEFAULT_INITIAL_RESPONSE_TIMEOUT = 4;
*/
export class Manager {
// Ongoing transfers in the service by ID
- private readTransfers: TransferDict = {};
- private writeTransfers: TransferDict = {};
+ readTransfers: TransferDict = {};
+ writeTransfers: TransferDict = {};
// RPC streams for read and write transfers. These are shareable by
// multiple transfers of the same type.
@@ -73,7 +73,7 @@ export class Manager {
private service: ServiceClient,
private defaultResponseTimeoutS = DEFAULT_RESPONSE_TIMEOUT_S,
private initialResponseTimeoutS = DEFAULT_INITIAL_RESPONSE_TIMEOUT,
- private maxRetries = DEFAULT_MAX_RETRIES
+ private maxRetries = DEFAULT_MAX_RETRIES,
) {}
/**
@@ -83,11 +83,11 @@ export class Manager {
*/
async read(
resourceId: number,
- progressCallback?: ProgressCallback
+ progressCallback?: ProgressCallback,
): Promise<Uint8Array> {
if (resourceId in this.readTransfers) {
throw new Error(
- `Read transfer for resource ${resourceId} already exists`
+ `Read transfer for resource ${resourceId} already exists`,
);
}
const transfer = new ReadTransfer(
@@ -95,7 +95,7 @@ export class Manager {
this.sendReadChunkCallback,
this.defaultResponseTimeoutS,
this.maxRetries,
- progressCallback
+ progressCallback,
);
this.startReadTransfer(transfer);
@@ -129,7 +129,7 @@ export class Manager {
async write(
resourceId: number,
data: Uint8Array,
- progressCallback?: ProgressCallback
+ progressCallback?: ProgressCallback,
): Promise<void> {
const transfer = new WriteTransfer(
resourceId,
@@ -138,7 +138,7 @@ export class Manager {
this.defaultResponseTimeoutS,
this.initialResponseTimeoutS,
this.maxRetries,
- progressCallback
+ progressCallback,
);
this.startWriteTransfer(transfer);
@@ -172,27 +172,31 @@ export class Manager {
private openReadStream(): void {
const readRpc = this.service.method(
- 'Read'
+ 'Read',
)! as BidirectionalStreamingMethodStub;
this.readStream = readRpc.invoke(
(chunk: Chunk) => {
this.handleChunk(this.readTransfers, chunk);
},
- () => {},
- this.onReadError
+ () => {
+ // Do nothing.
+ },
+ this.onReadError,
);
}
private openWriteStream(): void {
const writeRpc = this.service.method(
- 'Write'
+ 'Write',
)! as BidirectionalStreamingMethodStub;
this.writeStream = writeRpc.invoke(
(chunk: Chunk) => {
this.handleChunk(this.writeTransfers, chunk);
},
- () => {},
- this.onWriteError
+ () => {
+ // Do nothing.
+ },
+ this.onWriteError,
);
}
@@ -255,7 +259,7 @@ export class Manager {
const transfer = transfers[chunk.getTransferId()];
if (transfer === undefined) {
console.error(
- `TransferManager received chunk for unknown transfer ${chunk.getTransferId()}`
+ `TransferManager received chunk for unknown transfer ${chunk.getTransferId()}`,
);
return;
}
diff --git a/pw_transfer/ts/index.ts b/pw_transfer/ts/index.ts
index 5ba7e8382..7a193cfa7 100644
--- a/pw_transfer/ts/index.ts
+++ b/pw_transfer/ts/index.ts
@@ -12,4 +12,5 @@
// License for the specific language governing permissions and limitations under
// the License.
-export {Manager} from './client';
+export { Manager } from './client';
+export { ProgressStats, ProgressCallback } from './transfer';
diff --git a/pw_transfer/ts/transfer.ts b/pw_transfer/ts/transfer.ts
index c098a8232..c4eef6b2d 100644
--- a/pw_transfer/ts/transfer.ts
+++ b/pw_transfer/ts/transfer.ts
@@ -12,19 +12,14 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {
- BidirectionalStreamingCall,
- BidirectionalStreamingMethodStub,
- ServiceClient,
-} from 'pigweedjs/pw_rpc';
-import {Status} from 'pigweedjs/pw_status';
-import {Chunk} from 'pigweedjs/protos/pw_transfer/transfer_pb';
+import { Status } from 'pigweedjs/pw_status';
+import { Chunk } from 'pigweedjs/protos/pw_transfer/transfer_pb';
export class ProgressStats {
constructor(
readonly bytesSent: number,
readonly bytesConfirmedReceived: number,
- readonly totalSizeBytes?: number
+ readonly totalSizeBytes?: number,
) {}
get percentReceived(): number {
@@ -55,7 +50,7 @@ class Timer {
constructor(
readonly timeoutS: number,
- private readonly callback: () => any
+ private readonly callback: () => any,
) {}
/**
@@ -89,7 +84,7 @@ class Timer {
export abstract class Transfer {
status: Status = Status.OK;
done: Promise<Status>;
- protected data = new Uint8Array();
+ data = new Uint8Array(0);
private retries = 0;
private responseTimer?: Timer;
@@ -100,10 +95,10 @@ export abstract class Transfer {
protected sendChunk: (chunk: Chunk) => void,
responseTimeoutS: number,
private maxRetries: number,
- private progressCallback?: ProgressCallback
+ private progressCallback?: ProgressCallback,
) {
this.responseTimer = new Timer(responseTimeoutS, this.onTimeout);
- this.done = new Promise<Status>(resolve => {
+ this.done = new Promise<Status>((resolve) => {
this.resolve = resolve!;
});
}
@@ -126,7 +121,7 @@ export abstract class Transfer {
}
console.debug(
- `Received no responses for ${this.responseTimer?.timeoutS}; retrying ${this.retries}/${this.maxRetries}`
+ `Received no responses for ${this.responseTimer?.timeoutS}; retrying ${this.retries}/${this.maxRetries}`,
);
this.retryAfterTimeout();
@@ -145,7 +140,7 @@ export abstract class Transfer {
/** Sends the initial chunk of the transfer. */
begin(): void {
- this.sendChunk(this.initialChunk);
+ this.sendChunk(this.initialChunk as any);
this.responseTimer?.start();
}
@@ -167,12 +162,12 @@ export abstract class Transfer {
updateProgress(
bytesSent: number,
bytesConfirmedReceived: number,
- totalSizeBytes?: number
+ totalSizeBytes?: number,
): void {
const stats = new ProgressStats(
bytesSent,
bytesConfirmedReceived,
- totalSizeBytes
+ totalSizeBytes,
);
console.debug(`Transfer ${this.id} progress: ${stats}`);
@@ -232,8 +227,6 @@ export class ReadTransfer extends Transfer {
// of the window, and so on.
private static EXTEND_WINDOW_DIVISOR = 2;
- data = new Uint8Array();
-
constructor(
id: number,
sendChunk: (chunk: Chunk) => void,
@@ -242,7 +235,7 @@ export class ReadTransfer extends Transfer {
progressCallback?: ProgressCallback,
maxBytesToReceive = 8192,
maxChunkSize = 1024,
- chunkDelayMicroS?: number
+ chunkDelayMicroS?: number,
) {
super(id, sendChunk, responseTimeoutS, maxRetries, progressCallback);
this.maxBytesToReceive = maxBytesToReceive;
@@ -252,12 +245,12 @@ export class ReadTransfer extends Transfer {
this.windowEndOffset = maxBytesToReceive;
}
- protected get initialChunk(): Chunk {
+ protected get initialChunk(): any {
return this.transferParameters(Chunk.Type.START);
}
/** Builds an updated transfer parameters chunk to send the server. */
- private transferParameters(type: Chunk.TypeMap[keyof Chunk.TypeMap]): Chunk {
+ private transferParameters(type: any): Chunk {
this.pendingBytes = this.maxBytesToReceive;
this.windowEndOffset = this.offset + this.maxBytesToReceive;
@@ -328,7 +321,7 @@ export class ReadTransfer extends Transfer {
this.id
}: transmitter sent invalid earlier end offset ${chunk.getWindowEndOffset()} (receiver offset ${
this.offset
- })`
+ })`,
);
this.sendError(Status.INTERNAL);
return;
@@ -340,7 +333,7 @@ export class ReadTransfer extends Transfer {
this.id
}: transmitter sent invalid later end offset ${chunk.getWindowEndOffset()} (receiver end offset ${
this.windowEndOffset
- })`
+ })`,
);
this.sendError(Status.INTERNAL);
return;
@@ -379,7 +372,6 @@ export class ReadTransfer extends Transfer {
* A client => server write transfer.
*/
export class WriteTransfer extends Transfer {
- readonly data: Uint8Array;
private windowId = 0;
offset = 0;
maxChunkSize = 0;
@@ -394,14 +386,14 @@ export class WriteTransfer extends Transfer {
responseTimeoutS: number,
initialResponseTimeoutS: number,
maxRetries: number,
- progressCallback?: ProgressCallback
+ progressCallback?: ProgressCallback,
) {
super(id, sendChunk, responseTimeoutS, maxRetries, progressCallback);
this.data = data;
this.lastChunk = this.initialChunk;
}
- protected get initialChunk(): Chunk {
+ protected get initialChunk(): any {
// TODO(frolv): The session ID should not be set here but assigned by the
// server during an initial handshake.
const chunk = new Chunk();
@@ -429,6 +421,7 @@ export class WriteTransfer extends Transfer {
const bytesAknowledged = chunk.getOffset();
let writeChunk: Chunk;
+ // eslint-disable-next-line no-constant-condition
while (true) {
writeChunk = this.nextChunk();
this.offset += writeChunk.getData().length;
@@ -459,7 +452,7 @@ export class WriteTransfer extends Transfer {
this.id
}: server requested invalid offset ${chunk.getOffset()} (size ${
this.data.length
- })`
+ })`,
);
this.sendError(Status.OUT_OF_RANGE);
@@ -468,7 +461,7 @@ export class WriteTransfer extends Transfer {
if (chunk.getPendingBytes() === 0) {
console.error(
- `Transfer ${this.id}: service requested 0 bytes (invalid); aborting`
+ `Transfer ${this.id}: service requested 0 bytes (invalid); aborting`,
);
this.sendError(Status.INTERNAL);
return false;
@@ -481,7 +474,7 @@ export class WriteTransfer extends Transfer {
console.debug(
`Write transfer ${
this.id
- } rolling back to offset ${chunk.getOffset()} from ${this.offset}`
+ } rolling back to offset ${chunk.getOffset()} from ${this.offset}`,
);
}
@@ -492,14 +485,14 @@ export class WriteTransfer extends Transfer {
// to be set in these version, so it must be calculated.
const maxBytesToSend = Math.min(
chunk.getPendingBytes(),
- this.data.length - this.offset
+ this.data.length - this.offset,
);
this.windowEndOffset = this.offset + maxBytesToSend;
} else {
// Extend the window to the new end offset specified by the server.
this.windowEndOffset = Math.min(
chunk.getWindowEndOffset(),
- this.data.length
+ this.data.length,
);
}
@@ -522,7 +515,7 @@ export class WriteTransfer extends Transfer {
const maxBytesInChunk = Math.min(
this.maxChunkSize,
- this.windowEndOffset - this.offset
+ this.windowEndOffset - this.offset,
);
chunk.setData(this.data.slice(this.offset, this.offset + maxBytesInChunk));
diff --git a/pw_transfer/ts/transfer_test.ts b/pw_transfer/ts/transfer_test.ts
index 148ad44ac..723539dff 100644
--- a/pw_transfer/ts/transfer_test.ts
+++ b/pw_transfer/ts/transfer_test.ts
@@ -21,16 +21,16 @@ import {
MethodStub,
ServiceClient,
} from 'pigweedjs/pw_rpc';
-import {Status} from 'pigweedjs/pw_status';
+import { Status } from 'pigweedjs/pw_status';
import {
PacketType,
RpcPacket,
} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {ProtoCollection} from 'pigweedjs/protos/collection';
-import {Chunk} from 'pigweedjs/protos/pw_transfer/transfer_pb';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
+import { Chunk } from 'pigweedjs/protos/pw_transfer/transfer_pb';
-import {Manager} from './client';
-import {ProgressStats} from './transfer';
+import { Manager } from './client';
+import { ProgressStats } from './transfer';
const DEFAULT_TIMEOUT_S = 0.3;
@@ -114,7 +114,7 @@ describe('Transfer client', () => {
sessionId: number,
offset: number,
data: string,
- remainingBytes: number
+ remainingBytes: number,
): Chunk {
const chunk = new Chunk();
chunk.setTransferId(sessionId);
@@ -217,7 +217,7 @@ describe('Transfer client', () => {
.then(() => {
fail('Unexpected completed promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(27);
expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]);
expect(sentChunks).toHaveLength(4);
@@ -237,7 +237,7 @@ describe('Transfer client', () => {
.then(() => {
fail('Unexpected completed promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(31);
expect(Status[error.status]).toEqual(Status[Status.NOT_FOUND]);
});
@@ -249,10 +249,10 @@ describe('Transfer client', () => {
enqueueServerError(service.method('Read')!, Status.NOT_FOUND);
await manager
.read(31)
- .then(data => {
+ .then((data) => {
fail('Unexpected completed promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(31);
expect(Status[error.status]).toEqual(Status[Status.INTERNAL]);
});
@@ -400,7 +400,7 @@ describe('Transfer client', () => {
await manager.write(4, textEncoder.encode('hello this is a message'));
expect(receivedData()).toEqual(
- textEncoder.encode('hello this is a message')
+ textEncoder.encode('hello this is a message'),
);
expect(sentChunks[1].getData()).toEqual(textEncoder.encode('hell'));
expect(sentChunks[2].getData()).toEqual(textEncoder.encode('o th'));
@@ -441,7 +441,7 @@ describe('Transfer client', () => {
textEncoder.encode('data to write'),
(stats: ProgressStats) => {
progress.push(stats);
- }
+ },
);
expect(sentChunks).toHaveLength(3);
expect(receivedData()).toEqual(textEncoder.encode('data to write'));
@@ -533,7 +533,7 @@ describe('Transfer client', () => {
.then(() => {
fail('Unexpected succesful promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(4);
expect(Status[error.status]).toEqual(Status[Status.OUT_OF_RANGE]);
});
@@ -553,7 +553,7 @@ describe('Transfer client', () => {
.then(() => {
fail('Unexpected succesful promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(21);
expect(Status[error.status]).toEqual(Status[Status.UNAVAILABLE]);
});
@@ -573,7 +573,7 @@ describe('Transfer client', () => {
.then(() => {
fail('Unexpected succesful promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(21);
expect(Status[error.status]).toEqual(Status[Status.INTERNAL]);
});
@@ -587,7 +587,7 @@ describe('Transfer client', () => {
.then(() => {
fail('unexpected succesful write');
})
- .catch(error => {
+ .catch((error) => {
expect(sentChunks).toHaveLength(3); // Initial chunk + two retries.
expect(error.id).toEqual(22);
expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]);
@@ -609,7 +609,7 @@ describe('Transfer client', () => {
.then(() => {
fail('unexpected succesful write');
})
- .catch(error => {
+ .catch((error) => {
const expectedChunk1 = new Chunk();
expectedChunk1.setTransferId(22);
expectedChunk1.setResourceId(22);
@@ -654,7 +654,7 @@ describe('Transfer client', () => {
.then(() => {
fail('Unexpected succesful promise');
})
- .catch(error => {
+ .catch((error) => {
expect(error.id).toEqual(23);
expect(Status[error.status]).toEqual(Status[Status.INTERNAL]);
});
diff --git a/pw_unit_test/BUILD.bazel b/pw_unit_test/BUILD.bazel
index 64749537c..3077e5546 100644
--- a/pw_unit_test/BUILD.bazel
+++ b/pw_unit_test/BUILD.bazel
@@ -19,7 +19,7 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -34,9 +34,9 @@ pw_cc_library(
],
)
-pw_cc_library(
+alias(
name = "pw_unit_test",
- deps = ["@pigweed_config//:pw_unit_test_googletest_backend"],
+ actual = "@pigweed//targets:pw_unit_test_googletest_backend",
)
pw_cc_library(
@@ -61,6 +61,21 @@ pw_cc_library(
],
)
+# Identifies when the light framework is being used.
+config_setting(
+ name = "light_setting",
+ flag_values = {
+ "@pigweed//targets:pw_unit_test_googletest_backend": "@pigweed//pw_unit_test:light",
+ },
+)
+
+config_setting(
+ name = "gtest_setting",
+ flag_values = {
+ "@pigweed//targets:pw_unit_test_googletest_backend": "@com_google_googletest//:gtest",
+ },
+)
+
pw_cc_library(
name = "event_handler",
hdrs = ["public/pw_unit_test/event_handler.h"],
@@ -78,6 +93,39 @@ pw_cc_library(
)
pw_cc_library(
+ name = "googletest_handler_adapter",
+ testonly = True,
+ srcs = ["googletest_handler_adapter.cc"],
+ hdrs = ["public/pw_unit_test/googletest_handler_adapter.h"],
+ includes = ["public"],
+ deps = [
+ ":event_handler",
+ "//pw_preprocessor",
+ "@com_google_googletest//:gtest",
+ ],
+)
+
+pw_cc_library(
+ name = "googletest_test_matchers",
+ testonly = True,
+ hdrs = ["public/pw_unit_test/googletest_test_matchers.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_result",
+ "//pw_status",
+ "@com_google_googletest//:gtest",
+ ],
+)
+
+pw_cc_test(
+ name = "googletest_test_matchers_test",
+ srcs = ["googletest_test_matchers_test.cc"],
+ deps = [
+ ":googletest_test_matchers",
+ ],
+)
+
+pw_cc_library(
name = "simple_printing_event_handler",
srcs = ["simple_printing_event_handler.cc"],
hdrs = [
@@ -140,13 +188,22 @@ pw_cc_library(
],
)
+# Provides logging to either the light framework or an external GoogleTest.
+pw_cc_library(
+ name = "logging",
+ deps = [":logging_event_handler"] + select({
+ ":gtest_setting": [":googletest_handler_adapter"],
+ "//conditions:default": [],
+ }),
+)
+
pw_cc_binary(
name = "logging_main",
srcs = [
"logging_main.cc",
],
deps = [
- ":logging_event_handler",
+ ":logging",
"//pw_unit_test",
],
)
@@ -169,16 +226,27 @@ pw_proto_library(
pw_cc_library(
name = "rpc_service",
- srcs = [
- "rpc_light_event_handler.cc",
- "unit_test_service.cc",
- ],
+ srcs = ["unit_test_service.cc"] + select({
+ ":light_setting": ["rpc_light_event_handler.cc"],
+ "//conditions:default": [":rpc_gtest_event_handler.cc"],
+ }),
hdrs = [
+ "public/pw_unit_test/config.h",
"public/pw_unit_test/unit_test_service.h",
- "rpc_light_public/pw_unit_test/internal/rpc_event_handler.h",
- ],
- includes = ["rpc_light_public"],
+ ] + select({
+ ":light_setting": [
+ "rpc_light_public/pw_unit_test/internal/rpc_event_handler.h",
+ ],
+ "//conditions:default": [
+ "rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h",
+ ],
+ }),
+ includes = ["public"] + select({
+ ":light_setting": ["rpc_light_public"],
+ "//conditions:default": ["rpc_gtest_public"],
+ }),
deps = [
+ ":event_handler",
":pw_unit_test",
":unit_test_cc.pwpb",
":unit_test_cc.raw_rpc",
@@ -223,6 +291,10 @@ pw_cc_library(
pw_cc_test(
name = "static_library_support_test",
srcs = ["static_library_support_test.cc"],
+ target_compatible_with = select({
+ "//pw_unit_test:light_setting": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+ }),
deps = [
":static_library_support",
":tests_in_archive",
@@ -233,6 +305,10 @@ pw_cc_test(
pw_cc_test(
name = "framework_test",
srcs = ["framework_test.cc"],
+ target_compatible_with = select({
+ "//pw_unit_test:light_setting": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+ }),
deps = [
":pw_unit_test",
"//pw_assert",
@@ -250,15 +326,3 @@ filegroup(
# "//pw_rpc/system_server",
# ],
)
-
-# GTest is not yet supported in the Bazel build. This filegroup silences
-# warnings about these files not being included in the Bazel build.
-filegroup(
- name = "gtest_support",
- srcs = [
- "googletest_handler_adapter.cc",
- "public/pw_unit_test/googletest_handler_adapter.h",
- "rpc_gtest_event_handler.cc",
- "rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h",
- ],
-)
diff --git a/pw_unit_test/BUILD.gn b/pw_unit_test/BUILD.gn
index 9ad2f5b8d..714acec38 100644
--- a/pw_unit_test/BUILD.gn
+++ b/pw_unit_test/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/module_config.gni")
+import("$dir_pw_build/python_action_test.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
@@ -57,6 +58,7 @@ pw_source_set("config") {
# pw_unit_test facade. This provides a GoogleTest-compatible test framework.
pw_source_set("pw_unit_test") {
+ testonly = pw_unit_test_TESTONLY
public_deps = [ pw_unit_test_GOOGLETEST_BACKEND ]
}
@@ -106,7 +108,7 @@ pw_source_set("googletest_style_event_handler") {
pw_source_set("googletest_handler_adapter") {
public_configs = [ ":public_include_path" ]
public_deps = [
- ":logging_event_handler",
+ ":event_handler",
"$dir_pw_third_party/googletest",
dir_pw_preprocessor,
]
@@ -114,6 +116,22 @@ pw_source_set("googletest_handler_adapter") {
sources = [ "googletest_handler_adapter.cc" ]
}
+pw_source_set("googletest_test_matchers") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_unit_test/googletest_test_matchers.h" ]
+ public_deps = [
+ "$dir_pw_third_party/googletest",
+ dir_pw_result,
+ dir_pw_status,
+ ]
+}
+
+pw_test("googletest_test_matchers_test") {
+ enable_if = pw_unit_test_GOOGLETEST_BACKEND != ""
+ sources = [ "googletest_test_matchers_test.cc" ]
+ deps = [ ":googletest_test_matchers" ]
+}
+
# Library providing an event handler which outputs human-readable text.
pw_source_set("simple_printing_event_handler") {
public_deps = [
@@ -128,6 +146,7 @@ pw_source_set("simple_printing_event_handler") {
# framework. Unit test files can link against this library to build runnable
# unit test executables.
pw_source_set("simple_printing_main") {
+ testonly = pw_unit_test_TESTONLY
deps = [
":pw_unit_test",
":simple_printing_event_handler",
@@ -146,6 +165,7 @@ pw_source_set("printf_event_handler") {
}
pw_source_set("printf_main") {
+ testonly = pw_unit_test_TESTONLY
deps = [
":printf_event_handler",
":pw_unit_test",
@@ -163,9 +183,19 @@ pw_source_set("logging_event_handler") {
sources = [ "logging_event_handler.cc" ]
}
+# Provides logging to either the light framework or an external GoogleTest.
+group("logging") {
+ public_deps = [ ":logging_event_handler" ]
+ deps = []
+ if (pw_unit_test_GOOGLETEST_BACKEND != "$dir_pw_unit_test:light") {
+ deps += [ ":googletest_handler_adapter" ]
+ }
+}
+
pw_source_set("logging_main") {
+ testonly = pw_unit_test_TESTONLY
deps = [
- ":logging_event_handler",
+ ":logging",
":pw_unit_test",
]
sources = [ "logging_main.cc" ]
@@ -180,6 +210,7 @@ config("rpc_service_backend_gtest") {
}
pw_source_set("rpc_service") {
+ testonly = pw_unit_test_TESTONLY
public_configs = [ ":public_include_path" ]
public_deps = [
":event_handler",
@@ -205,6 +236,7 @@ pw_source_set("rpc_service") {
}
pw_source_set("rpc_main") {
+ testonly = pw_unit_test_TESTONLY
public_deps = [ ":pw_unit_test" ]
deps = [
":rpc_service",
@@ -222,6 +254,7 @@ pw_source_set("static_library_support") {
}
pw_executable("test_rpc_server") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "test_rpc_server.cc" ]
deps = [
":pw_unit_test",
@@ -238,6 +271,7 @@ pw_proto_library("unit_test_proto") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
+ other_deps = [ "py" ]
}
pw_test("metadata_only_test") {
@@ -249,26 +283,27 @@ pw_test("metadata_only_test") {
# pw_test_group produces the metadata file for its tests.
pw_test_group("metadata_only_group") {
tests = [ ":metadata_only_test" ]
+ output_metadata = true
}
-pw_python_script("test_group_metadata_test") {
+pw_python_action_test("test_group_metadata_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "py/test_group_metadata_test.py" ]
- action = {
- args = [
- "--stamp-path",
- "<TARGET_FILE(:metadata_only_group)>",
- ]
- deps = [ ":metadata_only_group" ]
- stamp = true
- }
+ args = [
+ "--stamp-path",
+ "<TARGET_FILE(:metadata_only_group)>",
+ ]
+ deps = [ ":metadata_only_group" ]
}
pw_test("framework_test") {
+ enable_if = pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_unit_test:light"
sources = [ "framework_test.cc" ]
deps = [ dir_pw_assert ]
}
pw_static_library("tests_in_archive") {
+ testonly = pw_unit_test_TESTONLY
sources = [
"static_library_archived_tests.cc",
"static_library_missing_archived_tests.cc",
@@ -278,6 +313,7 @@ pw_static_library("tests_in_archive") {
}
pw_test("static_library_support_test") {
+ enable_if = pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_unit_test:light"
sources = [ "static_library_support_test.cc" ]
deps = [
":static_library_support",
@@ -291,4 +327,7 @@ pw_test_group("tests") {
":framework_test",
":static_library_support_test",
]
+ if (dir_pw_third_party_googletest != "") {
+ tests += [ ":googletest_test_matchers_test" ]
+ }
}
diff --git a/pw_unit_test/CMakeLists.txt b/pw_unit_test/CMakeLists.txt
index f66f9dfdc..6892f46d0 100644
--- a/pw_unit_test/CMakeLists.txt
+++ b/pw_unit_test/CMakeLists.txt
@@ -74,6 +74,27 @@ pw_add_library(pw_unit_test.googletest_style_event_handler STATIC
googletest_style_event_handler.cc
)
+if(NOT ${pw_unit_test_GOOGLETEST_BACKEND} STREQUAL "")
+ pw_add_library(pw_unit_test.googletest_test_matchers INTERFACE
+ HEADERS
+ public/pw_unit_test/googletest_test_matchers.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_result
+ pw_status
+ ${pw_unit_test_GOOGLETEST_BACKEND}
+ )
+ pw_add_test(pw_unit_test.googletest_test_matchers_test
+ SOURCES
+ googletest_test_matchers_test.cc
+ PRIVATE_DEPS
+ pw_unit_test.googletest_test_matchers
+ GROUPS
+ pw_unit_test
+ )
+endif()
+
pw_add_library(pw_unit_test.simple_printing_main STATIC
SOURCES
simple_printing_main.cc
diff --git a/pw_unit_test/docs.rst b/pw_unit_test/docs.rst
index 12d1e9649..85b54820a 100644
--- a/pw_unit_test/docs.rst
+++ b/pw_unit_test/docs.rst
@@ -12,7 +12,7 @@ Pigweed. The default implementation is the embedded-friendly
.. note::
- This documentation is currently incomplete.
+ This documentation is currently incomplete.
-------------------------------------------
pw_unit_test:light: GoogleTest for Embedded
@@ -33,22 +33,78 @@ for examples of how to define unit test cases.
expected in a complete testing framework; nevertheless, it is already used
heavily within Pigweed.
-.. note::
-
- Many of GoogleTest's more advanced features are not yet implemented. Missing
- features include:
-
- * Any GoogleMock features (e.g. :c:macro:`EXPECT_THAT`)
- * Floating point comparison macros (e.g. :c:macro:`EXPECT_FLOAT_EQ`)
- * Death tests (e.g. :c:macro:`EXPECT_DEATH`); ``EXPECT_DEATH_IF_SUPPORTED``
- does nothing but silently passes
- * Value-parameterized tests
-
- To request a feature addition, please
- `let us know <mailto:pigweed@googlegroups.com>`_.
+GoogleTest compatibility
+========================
+pw_unit_test implements a subset of GoogleTest. Supported features include:
+
+* Test and test suite declarations.
+* Most ``EXPECT`` and ``ASSERT`` macros, including ``EXPECT_OK`` and
+ ``ASSERT_OK`` for functions returning a status.
+* ``ASSERT_OK_AND_ASSIGN`` to test assigning a value when status is ``OK`` or
+ fail the test.
+* ``StatusIs`` matcher to expect a specific ``pw::Status`` other that ``OK``.
+* ``IsOkAndHolds`` matcher to expect an object's status is ``OK`` and the value
+ matches an expected value.
+* Stream-style expectation messages, such as
+ ``EXPECT_EQ(val, 5) << "Inputs: " << input``. Messages are currently ignored.
+
+Many of GoogleTest's advanced features are not yet implemented. Missing features
+include:
+
+* Any GoogleMock features (e.g. :c:macro:`EXPECT_THAT`)
+* Floating point comparison macros (e.g. :c:macro:`EXPECT_FLOAT_EQ`)
+* Death tests (e.g. :c:macro:`EXPECT_DEATH`); ``EXPECT_DEATH_IF_SUPPORTED``
+ does nothing but silently passes
+* Value-parameterized tests
+
+To request a feature addition, please `let us know
+<mailto:pigweed@googlegroups.com>`_.
+
+See `Using upstream GoogleTest`_ below for information
+about using upstream GoogleTest instead.
+
+API Reference
+-------------
- See `Using upstream GoogleTest`_ below for information
- about using upstream GoogleTest instead.
+Expectations
+````````````
+Expectations perform a check that when fails continues the test's execution
+while still marking the test as a failure. They're particularly handy when
+verifying multiple dimensions of the same feature so we can see all the errors
+at the same time.
+
+.. doxygendefine:: EXPECT_TRUE
+.. doxygendefine:: EXPECT_FALSE
+.. doxygendefine:: EXPECT_EQ
+.. doxygendefine:: EXPECT_NE
+.. doxygendefine:: EXPECT_GT
+.. doxygendefine:: EXPECT_GE
+.. doxygendefine:: EXPECT_LT
+.. doxygendefine:: EXPECT_LE
+.. doxygendefine:: EXPECT_NEAR
+.. doxygendefine:: EXPECT_FLOAT_EQ
+.. doxygendefine:: EXPECT_DOUBLE_EQ
+.. doxygendefine:: EXPECT_STREQ
+.. doxygendefine:: EXPECT_STRNE
+
+Assertions
+``````````
+Assertions work exactly the same as expectations, but stop the execution of the
+test as soon as a failed condition is met.
+
+.. doxygendefine:: ASSERT_TRUE
+.. doxygendefine:: ASSERT_FALSE
+.. doxygendefine:: ASSERT_EQ
+.. doxygendefine:: ASSERT_NE
+.. doxygendefine:: ASSERT_GT
+.. doxygendefine:: ASSERT_GE
+.. doxygendefine:: ASSERT_LT
+.. doxygendefine:: ASSERT_LE
+.. doxygendefine:: ASSERT_NEAR
+.. doxygendefine:: ASSERT_FLOAT_EQ
+.. doxygendefine:: ASSERT_DOUBLE_EQ
+.. doxygendefine:: ASSERT_STREQ
+.. doxygendefine:: ASSERT_STRNE
The EventHandler interface
==========================
@@ -109,8 +165,7 @@ GoogleTest-style output using the shared
.. cpp:class:: PrintfEventHandler : public GoogleTestStyleEventHandler
- A C++14-compatible event handler that uses ``std::printf`` to output test
- results.
+ Event handler that uses ``std::printf`` to output test results.
.. cpp:namespace-pop::
@@ -175,6 +230,10 @@ The following example shows how to write a main function that runs
}
int main() {
+ // The following line has no effect with pw_unit_test_light, but makes this
+ // test compatible with upstream GoogleTest.
+ testing::InitGoogleTest();
+
// Since we are using pw_unit_test:light, set up an event handler.
pw::unit_test::SimplePrintingEventHandler handler(WriteString);
pw::unit_test::RegisterEventHandler(&handler);
@@ -212,6 +271,8 @@ test code.
sources = [ "foo_test.cc" ]
}
+.. _module-pw_unit_test-pw_test:
+
pw_test template
````````````````
``pw_test`` defines a single unit test suite. It creates several sub-targets.
@@ -228,15 +289,22 @@ pw_test template
``pw_executable``.
* ``enable_if``: Boolean indicating whether the test should be built. If false,
replaces the test with an empty target. Default true.
+* ``source_gen_deps``: List of target labels that generate source files used by
+ this test. The labels must meet the constraints of GN's `get_target_outputs`,
+ namely they must have been previously defined in the current file. This
+ argument is required if a test uses generated source files and `enable_if` can
+ evaluate to false.
* ``test_main``: Target label to add to the tests's dependencies to provide the
``main()`` function. Defaults to ``pw_unit_test_MAIN``. Set to ``""`` if
``main()`` is implemented in the test's ``sources``.
* ``test_automatic_runner_args``: Array of args to pass to automatic test
runner. Defaults to ``pw_unit_test_AUTOMATIC_RUNNER_ARGS``.
+* ``envvars``: Array of ``"key=value"`` strings representing environment
+ variables to set when invoking the automatic test runner.
**Example**
-.. code::
+.. code-block::
import("$dir_pw_unit_test/test.gni")
@@ -245,6 +313,8 @@ pw_test template
enable_if = device_has_1m_flash
}
+.. _module-pw_unit_test-pw_test_group:
+
pw_test_group template
``````````````````````
``pw_test_group`` defines a collection of tests or other test groups. It creates
@@ -270,7 +340,7 @@ several sub-targets:
**Example**
-.. code::
+.. code-block::
import("$dir_pw_unit_test/test.gni")
@@ -407,6 +477,13 @@ Build arguments
Controls whether to build and run facade tests. Facade tests add considerably
to build time, so they are disabled by default.
+.. option:: pw_unit_test_TESTONLY <boolean>
+
+ Controls the `testonly` variable in pw_test, pw_test_group, and
+ miscellaneous testing targets. This is useful if your test libraries (e.g.
+ GoogleTest) used by pw_unit_test have the `testonly` flag set. False by
+ default for backwards compatibility.
+
CMake
-----
pw_add_test function
@@ -439,7 +516,7 @@ sub-targets.
**Example**
-.. code::
+.. code-block::
include($ENV{PW_ROOT}/pw_unit_test/test.cmake)
@@ -479,7 +556,7 @@ creates several sub-targets:
**Example**
-.. code::
+.. code-block::
include($ENV{PW_ROOT}/pw_unit_test/test.cmake)
@@ -553,6 +630,86 @@ Build arguments
Type: string (path to a .cmake file)
Usage: toolchain-controlled only
+Bazel
+-----
+To define simple unit tests, set the ``pw_unit_test_MAIN`` build variable to a
+target which configures the test framework as described in the
+:ref:`running-tests` section, and use the ``pw_cc_test`` rule to register your
+test code.
+
+.. code-block::
+
+ load("//pw_build:pigweed.bzl", "pw_cc_test")
+
+ pw_cc_test(
+ name = "foo_test",
+ srcs = ["foo_test.cc"],
+ }
+
+.. _module-pw_unit_test-pw_cc_test:
+
+
+pw_cc_test rule
+```````````````
+``pw_cc_test`` is a wrapper for `cc_test`_ that provides some defaults,
+such as a dep on ``@pigweed//targets:pw_unit_test_main``. It supports and passes
+through all the arguments recognized by ``cc_test``. Notably, tests can be
+enabled or disabled using ``target_compatible_with``. For example, the following
+test is skipped when `using upstream GoogleTest`_:
+
+.. code-block::
+
+ load("//pw_build:pigweed.bzl", "pw_cc_test")
+
+ pw_cc_test(
+ name = "no_upstream_test",
+ srcs = ["no_upstream_test.cc"],
+ target_compatible_with = select({
+ "//pw_unit_test:light_setting": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+ }),
+ }
+
+.. _cc_test: https://bazel.build/reference/be/c-cpp#cc_test
+
+
+Build arguments
+```````````````
+.. option:: pw_unit_test_googletest_backend <target>
+
+ The GoogleTest implementation to use for Pigweed unit tests. This library
+ provides "gtest/gtest.h" and related headers. Defaults to
+ ``"@pigweed//pw_unit_test:light"``, which implements a subset of GoogleTest.
+
+ Type: string (Bazel target label)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_main <target>
+
+ Implementation of a main function for ``pw_cc_test`` unit test binaries.
+
+ Type: string (Bazel target label)
+ Usage: toolchain-controlled only
+
+Serial test runner
+==================
+To accelerate automated unit test bringup for devices with plain-text logging,
+this module provides a serial-based test runner that triggers a device flash
+and evaluates whether the test passed or failed based on the produced output.
+
+pw_unit_test.serial_test_runner
+-------------------------------
+.. automodule:: pw_unit_test.serial_test_runner
+ :members:
+ DEFAULT_TEST_START_CHARACTER,
+ SerialTestingDevice,
+ run_device_test,
+
+Setup
+-----
+To add support for a new device, implement a ``SerialTestingDevice`` class for
+your device, and then configure your on-device firmware to wait to run unit
+tests until ``DEFAULT_TEST_START_CHARACTER`` is sent over the serial connection.
RPC service
===========
@@ -568,7 +725,7 @@ however some features (such as test suite filtering) are missing.
To set up RPC-based unit tests in your application, instantiate a
``pw::unit_test::UnitTestService`` and register it with your RPC server.
-.. code:: c++
+.. code-block:: c++
#include "pw_rpc/server.h"
#include "pw_unit_test/unit_test_service.h"
@@ -588,26 +745,35 @@ To set up RPC-based unit tests in your application, instantiate a
All tests flashed to an attached device can be run via python by calling
``pw_unit_test.rpc.run_tests()`` with a RPC client services object that has
the unit testing RPC service enabled. By default, the results will output via
-logging.
+logging. This method returns a ``TestRecord`` dataclass instance, containing
+the results of the test run.
-.. code:: python
+.. code-block:: python
- from pw_hdlc.rpc import HdlcRpcClient
- from pw_unit_test.rpc import run_tests
+ import serial
- PROTO = Path(os.environ['PW_ROOT'],
- 'pw_unit_test/pw_unit_test_proto/unit_test.proto')
+ from pw_hdlc import rpc
+ from pw_unit_test.rpc import run_tests
- client = HdlcRpcClient(serial.Serial(device, baud), PROTO)
- run_tests(client.rpcs())
+ PROTO = Path(
+ os.environ['PW_ROOT'],
+ 'pw_unit_test/pw_unit_test_proto/unit_test.proto'
+ )
+ serial_device = serial.Serial(device, baud)
+ with rpc.SerialReader(serial_device) as reader:
+ with rpc.HdlcRpcClient(
+ reader, PROTO, rpc.default_channels(serial_device.write)
+ ) as client:
+ run_tests(client.rpcs())
pw_unit_test.rpc
----------------
.. automodule:: pw_unit_test.rpc
- :members: EventHandler, run_tests
+ :members: EventHandler, run_tests, TestRecord
+----------------------------
Module Configuration Options
-============================
+----------------------------
The following configurations can be adjusted via compile-time configuration of
this module.
@@ -622,8 +788,9 @@ this module.
The size of the memory pool to use for test fixture instances. By default this
is set to 16K.
+-------------------------
Using upstream GoogleTest
-=========================
+-------------------------
Upstream `GoogleTest`_ may be used as the backend for ``pw_unit_test``. A clone
of the GoogleTest repository is required. See the
:ref:`third_party/googletest documentation <module-pw_third_party_googletest>`
@@ -632,25 +799,28 @@ for details.
When using upstream `GoogleTest`_ as the backend, the
:cpp:class:`pw::unit_test::GoogleTestHandlerAdapter` can be used in conjunction
with the above mentioned `EventHandler Interface <#the-eventhandler-interface>`_
-and `Predefined event handlers`_. An example of how you can use the adapter in
-conjunction with an ``EventHandler`` is shown below.
+and `Predefined event handlers`_. Included with this class is an implementation
+of `RegisterEventHandler` that wraps event handlers in an adapter. This allows
+the `main` functions written for `pw_unit_test:light` to work with upstream
+GoogleTest without modification, as shown below.
- .. code-block:: c++
+.. code-block:: c++
- testing::InitGoogleTest();
- auto* unit_test = testing::UnitTest::GetInstance();
-
- pw::unit_test::LoggingEventHandler logger;
- pw::unit_test::GoogleTestHandlerAdapter listener_adapter(logger);
- unit_test->listeners().Append(&listener_adapter);
+ #include "gtest/gtest.h"
+ #include "pw_unit_test/logging_event_handler.h"
- const auto status = RUN_ALL_TESTS();
+ int main() {
+ testing::InitGoogleTest();
+ pw::unit_test::LoggingEventHandler logger;
+ pw::unit_test::RegisterEventHandler(&logger);
+ return RUN_ALL_TESTS();
+ }
.. cpp:namespace-push:: pw::unit_test
.. cpp:class:: GoogleTestHandlerAdapter
- A GoogleTest Event Listener that fires GoogleTest emitted events to an
- appropriate ``EventHandler``.
+ A GoogleTest Event Listener that fires GoogleTest emitted events to an
+ appropriate ``EventHandler``.
.. cpp::namespace-pop::
diff --git a/pw_unit_test/framework.cc b/pw_unit_test/framework.cc
index 945e2f4fb..e58cee0bf 100644
--- a/pw_unit_test/framework.cc
+++ b/pw_unit_test/framework.cc
@@ -78,6 +78,36 @@ int Framework::RunAllTests() {
return exit_status_;
}
+void Framework::SetUpTestSuiteIfNeeded(SetUpTestSuiteFunc set_up_ts) const {
+ if (set_up_ts == Test::SetUpTestSuite) {
+ return;
+ }
+
+ for (TestInfo* info = tests_; info != current_test_; info = info->next()) {
+ if (info->test_case().suite_name == current_test_->test_case().suite_name) {
+ return;
+ }
+ }
+
+ set_up_ts();
+}
+
+void Framework::TearDownTestSuiteIfNeeded(
+ TearDownTestSuiteFunc tear_down_ts) const {
+ if (tear_down_ts == Test::TearDownTestSuite) {
+ return;
+ }
+
+ for (TestInfo* info = current_test_->next(); info != nullptr;
+ info = info->next()) {
+ if (info->test_case().suite_name == current_test_->test_case().suite_name) {
+ return;
+ }
+ }
+
+ tear_down_ts();
+}
+
void Framework::StartTest(const TestInfo& test) {
current_test_ = &test;
current_result_ = TestResult::kSuccess;
diff --git a/pw_unit_test/framework_test.cc b/pw_unit_test/framework_test.cc
index 939c818e0..211c665ce 100644
--- a/pw_unit_test/framework_test.cc
+++ b/pw_unit_test/framework_test.cc
@@ -57,6 +57,46 @@ TEST(PigweedTest, ExpectBasicComparisons) {
ASSERT_LE(-2, -2);
}
+TEST(PigweedTest, ExpectNearComparisons) {
+ EXPECT_NEAR(1, 2, 1);
+ ASSERT_NEAR(1, 2, 1);
+
+ EXPECT_NEAR(-5, 5, 10);
+ ASSERT_NEAR(-5, 5, 10);
+
+ int x = 17;
+ int epsilon = 5;
+
+ EXPECT_NEAR(x, 15, epsilon);
+ ASSERT_NEAR(x, 15, epsilon);
+}
+
+TEST(PigweedTest, ExpectFloatComparisons) {
+ EXPECT_FLOAT_EQ(5.0f, 10.0f / 2);
+ ASSERT_FLOAT_EQ(5.0f, 10.0f / 2);
+
+ EXPECT_FLOAT_EQ(-0.5f, -5.0f / 10);
+ ASSERT_FLOAT_EQ(-0.5f, -5.0f / 10);
+
+ float x = 17.0f / 20.0f;
+
+ EXPECT_FLOAT_EQ(x, 17.0f / 20.0f);
+ ASSERT_FLOAT_EQ(x, 17.0f / 20.0f);
+}
+
+TEST(PigweedTest, ExpectDoubleComparisons) {
+ EXPECT_DOUBLE_EQ(5.0, 10.0 / 2);
+ ASSERT_DOUBLE_EQ(5.0, 10.0 / 2);
+
+ EXPECT_DOUBLE_EQ(-0.5, -5.0 / 10);
+ ASSERT_DOUBLE_EQ(-0.5, -5.0 / 10);
+
+ double x = 17.0 / 20.0;
+
+ EXPECT_DOUBLE_EQ(x, 17.0 / 20.0);
+ ASSERT_DOUBLE_EQ(x, 17.0 / 20.0);
+}
+
TEST(PigweedTest, ExpectStringEquality) {
EXPECT_STREQ("", "");
EXPECT_STREQ("Yes", "Yes");
@@ -66,6 +106,9 @@ TEST(PigweedTest, ExpectStringEquality) {
EXPECT_STRNE("NO", "no");
ASSERT_STRNE("yes", no);
+
+ EXPECT_STREQ(nullptr, nullptr);
+ EXPECT_STRNE("abc", nullptr);
}
TEST(PigweedTest, SucceedAndFailMacros) {
@@ -85,6 +128,37 @@ TEST(PigweedTest, SkipMacro) {
EXPECT_TRUE(false);
}
+TEST(PigweedTest, Logs) {
+ EXPECT_TRUE(true) << "This message is ignored";
+ EXPECT_FALSE(false) << "This message is ignored";
+ EXPECT_EQ(0, 0) << "This message is ignored";
+ EXPECT_NE(0, 1) << "This message is ignored";
+ EXPECT_GT(1, 0) << "This message is ignored";
+ EXPECT_GE(0, 0) << "This message is ignored";
+ EXPECT_LT(0, 1) << "This message is ignored";
+ EXPECT_LE(0, 0) << "This message is ignored";
+ EXPECT_STREQ("", "") << "This message is ignored";
+ EXPECT_STRNE("", "?") << "This message is ignored";
+
+ ASSERT_TRUE(true) << "This message is ignored";
+ ASSERT_FALSE(false) << "This message is ignored";
+ ASSERT_EQ(0, 0) << "This message is ignored";
+ ASSERT_NE(0, 1) << "This message is ignored";
+ ASSERT_GT(1, 0) << "This message is ignored";
+ ASSERT_GE(0, 0) << "This message is ignored";
+ ASSERT_LT(0, 1) << "This message is ignored";
+ ASSERT_LE(0, 0) << "This message is ignored";
+ ASSERT_STREQ("", "") << "This message is ignored";
+ ASSERT_STRNE("", "?") << "This message is ignored";
+
+ if (false) {
+ ADD_FAILURE() << "This failed!" << 123;
+ GTEST_FAIL() << "This failed!" << 123 << '?';
+ GTEST_SKIP() << 1.0f << " skips!";
+ }
+ GTEST_SUCCEED() << "This message is ignored";
+}
+
class SkipOnSetUpTest : public ::testing::Test {
public:
void SetUp() override { GTEST_SKIP(); }
@@ -144,6 +218,13 @@ TEST(PigweedTest, MacroArgumentsOnlyAreEvaluatedOnce) {
EXPECT_EQ(i, 4);
}
+class ClassWithPrivateMethod {
+ FRIEND_TEST(FixtureTest, FriendClass);
+
+ private:
+ int Return314() { return 314; }
+};
+
class FixtureTest : public ::testing::Test {
public:
FixtureTest() : string_("hello world") {}
@@ -160,6 +241,10 @@ TEST_F(FixtureTest, CustomFixture) {
EXPECT_EQ(StringLength(), 11);
}
+TEST_F(FixtureTest, FriendClass) {
+ EXPECT_EQ(ClassWithPrivateMethod().Return314(), 314);
+}
+
class PigweedTestFixture : public ::testing::Test {
protected:
PigweedTestFixture() : cool_number_(35) {}
@@ -191,21 +276,96 @@ class Expectations : public ::testing::Test {
TEST_F(Expectations, SetCoolNumber) { cool_number_ = 14159; }
class SetUpAndTearDown : public ::testing::Test {
- protected:
- SetUpAndTearDown() : value_(0) { EXPECT_EQ(value_, 0); }
+ public:
+ static int value;
- ~SetUpAndTearDown() override { EXPECT_EQ(value_, 1); }
+ static void SetUpTestSuite() {
+ value = 1;
+ EXPECT_EQ(value, 1);
+ value++;
+ }
- void SetUp() override { value_ = 1337; }
+ static void TearDownTestSuite() {
+ EXPECT_EQ(value, 7);
+ value++;
+ }
- void TearDown() override { value_ = 1; }
+ protected:
+ SetUpAndTearDown() {
+ EXPECT_EQ(value, 2);
+ value++;
+ }
- int value_;
+ ~SetUpAndTearDown() override {
+ EXPECT_EQ(value, 6);
+ value++;
+ }
+
+ void SetUp() override {
+ EXPECT_EQ(value, 3);
+ value++;
+ }
+
+ void TearDown() override {
+ EXPECT_EQ(value, 5);
+ value++;
+ }
};
+int SetUpAndTearDown::value = 1;
+
TEST_F(SetUpAndTearDown, MakeSureItIsSet) {
- EXPECT_EQ(value_, 1337);
- value_ = 3210;
+ EXPECT_EQ(value, 4);
+ value++;
+}
+
+TEST(TestSuiteTearDown, MakeSureItRan) {
+ EXPECT_EQ(SetUpAndTearDown::value, 8);
+}
+
+TEST(UnknownTypeToString, SmallObject) {
+ struct {
+ char a = 0xa1;
+ } object;
+
+ StringBuffer<64> expected;
+ expected << "<1-byte object at 0x" << &object << '>';
+ ASSERT_EQ(OkStatus(), expected.status());
+
+ StringBuffer<64> actual;
+ actual << object;
+ ASSERT_EQ(OkStatus(), actual.status());
+ EXPECT_STREQ(expected.c_str(), actual.c_str());
+}
+
+TEST(UnknownTypeToString, NineByteObject) {
+ struct {
+ char a[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
+ } object;
+
+ StringBuffer<64> expected;
+ expected << "<9-byte object at 0x" << &object << '>';
+ ASSERT_EQ(OkStatus(), expected.status());
+
+ StringBuffer<64> actual;
+ actual << object;
+ ASSERT_EQ(OkStatus(), actual.status());
+ EXPECT_STREQ(expected.c_str(), actual.c_str());
+}
+
+TEST(UnknownTypeToString, TenByteObject) {
+ struct {
+ char a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+ } object;
+
+ StringBuffer<72> expected;
+ expected << "<10-byte object at 0x" << &object << '>';
+ ASSERT_EQ(OkStatus(), expected.status());
+
+ StringBuffer<72> actual;
+ actual << object;
+ ASSERT_EQ(OkStatus(), actual.status());
+ EXPECT_STREQ(expected.c_str(), actual.c_str());
}
} // namespace
diff --git a/pw_unit_test/googletest_handler_adapter.cc b/pw_unit_test/googletest_handler_adapter.cc
index 9d29d1858..a48c9c667 100644
--- a/pw_unit_test/googletest_handler_adapter.cc
+++ b/pw_unit_test/googletest_handler_adapter.cc
@@ -18,6 +18,22 @@
namespace pw::unit_test {
+void RegisterEventHandler(EventHandler* event_handler) {
+ static testing::TestEventListener* gTestListener = nullptr;
+ auto& listeners = testing::UnitTest::GetInstance()->listeners();
+ if (!gTestListener) {
+ gTestListener = listeners.default_result_printer();
+ }
+ if (gTestListener) {
+ listeners.Release(gTestListener);
+ delete gTestListener;
+ }
+ if (event_handler) {
+ gTestListener = new pw::unit_test::GoogleTestHandlerAdapter(*event_handler);
+ listeners.Append(gTestListener);
+ }
+}
+
void GoogleTestHandlerAdapter::OnTestProgramStart(
const testing::UnitTest& unit_test) {
handler_.TestProgramStart(
diff --git a/pw_unit_test/googletest_test_matchers_test.cc b/pw_unit_test/googletest_test_matchers_test.cc
new file mode 100644
index 000000000..58d7a5bdf
--- /dev/null
+++ b/pw_unit_test/googletest_test_matchers_test.cc
@@ -0,0 +1,193 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#include "pw_unit_test/googletest_test_matchers.h"
+
+#include <cstdlib>
+
+#include "pw_status/status.h"
+
+namespace pw::unit_test {
+namespace {
+
+using ::testing::Eq;
+using ::testing::Not;
+
+TEST(TestMatchers, AssertOk) { ASSERT_OK(OkStatus()); }
+TEST(TestMatchers, AssertOkStatusWithSize) { ASSERT_OK(StatusWithSize(123)); }
+TEST(TestMatchers, AssertOkResult) { ASSERT_OK(Result<int>(123)); }
+
+TEST(TestMatchers, ExpectOk) { EXPECT_OK(OkStatus()); }
+TEST(TestMatchers, ExpectOkStatusWithSize) { EXPECT_OK(StatusWithSize(123)); }
+TEST(TestMatchers, ExpectOkResult) { EXPECT_OK(Result<int>(123)); }
+
+TEST(TestMatchers, StatusIsSuccess) {
+ EXPECT_THAT(OkStatus(), StatusIs(OkStatus()));
+ EXPECT_THAT(Status::Cancelled(), StatusIs(Status::Cancelled()));
+ EXPECT_THAT(Status::Unknown(), StatusIs(Status::Unknown()));
+ EXPECT_THAT(Status::InvalidArgument(), StatusIs(Status::InvalidArgument()));
+ EXPECT_THAT(Status::DeadlineExceeded(), StatusIs(Status::DeadlineExceeded()));
+ EXPECT_THAT(Status::NotFound(), StatusIs(Status::NotFound()));
+ EXPECT_THAT(Status::AlreadyExists(), StatusIs(Status::AlreadyExists()));
+ EXPECT_THAT(Status::PermissionDenied(), StatusIs(Status::PermissionDenied()));
+ EXPECT_THAT(Status::ResourceExhausted(),
+ StatusIs(Status::ResourceExhausted()));
+ EXPECT_THAT(Status::FailedPrecondition(),
+ StatusIs(Status::FailedPrecondition()));
+ EXPECT_THAT(Status::Aborted(), StatusIs(Status::Aborted()));
+ EXPECT_THAT(Status::OutOfRange(), StatusIs(Status::OutOfRange()));
+ EXPECT_THAT(Status::Unimplemented(), StatusIs(Status::Unimplemented()));
+ EXPECT_THAT(Status::Internal(), StatusIs(Status::Internal()));
+ EXPECT_THAT(Status::Unavailable(), StatusIs(Status::Unavailable()));
+ EXPECT_THAT(Status::DataLoss(), StatusIs(Status::DataLoss()));
+ EXPECT_THAT(Status::Unauthenticated(), StatusIs(Status::Unauthenticated()));
+}
+
+TEST(TestMatchers, StatusIsSuccessStatusWithSize) {
+ EXPECT_THAT(StatusWithSize(), StatusIs(OkStatus()));
+ EXPECT_THAT(StatusWithSize::Cancelled(), StatusIs(Status::Cancelled()));
+ EXPECT_THAT(StatusWithSize::Unknown(), StatusIs(Status::Unknown()));
+ EXPECT_THAT(StatusWithSize::InvalidArgument(),
+ StatusIs(Status::InvalidArgument()));
+ EXPECT_THAT(StatusWithSize::DeadlineExceeded(),
+ StatusIs(Status::DeadlineExceeded()));
+ EXPECT_THAT(StatusWithSize::NotFound(), StatusIs(Status::NotFound()));
+ EXPECT_THAT(StatusWithSize::AlreadyExists(),
+ StatusIs(Status::AlreadyExists()));
+ EXPECT_THAT(StatusWithSize::PermissionDenied(),
+ StatusIs(Status::PermissionDenied()));
+ EXPECT_THAT(StatusWithSize::ResourceExhausted(),
+ StatusIs(Status::ResourceExhausted()));
+ EXPECT_THAT(StatusWithSize::FailedPrecondition(),
+ StatusIs(Status::FailedPrecondition()));
+ EXPECT_THAT(StatusWithSize::Aborted(), StatusIs(Status::Aborted()));
+ EXPECT_THAT(StatusWithSize::OutOfRange(), StatusIs(Status::OutOfRange()));
+ EXPECT_THAT(StatusWithSize::Unimplemented(),
+ StatusIs(Status::Unimplemented()));
+ EXPECT_THAT(StatusWithSize::Internal(), StatusIs(Status::Internal()));
+ EXPECT_THAT(StatusWithSize::Unavailable(), StatusIs(Status::Unavailable()));
+ EXPECT_THAT(StatusWithSize::DataLoss(), StatusIs(Status::DataLoss()));
+ EXPECT_THAT(StatusWithSize::Unauthenticated(),
+ StatusIs(Status::Unauthenticated()));
+}
+
+TEST(TestMatchers, StatusIsSuccessOkResult) {
+ const Result<int> result = 46;
+ EXPECT_THAT(result, StatusIs(OkStatus()));
+}
+
+TEST(TestMatchers, StatusIsSuccessResult) {
+ EXPECT_THAT(Result<int>(Status::Cancelled()), StatusIs(Status::Cancelled()));
+ EXPECT_THAT(Result<int>(Status::Unknown()), StatusIs(Status::Unknown()));
+ EXPECT_THAT(Result<int>(Status::InvalidArgument()),
+ StatusIs(Status::InvalidArgument()));
+ EXPECT_THAT(Result<int>(Status::DeadlineExceeded()),
+ StatusIs(Status::DeadlineExceeded()));
+ EXPECT_THAT(Result<int>(Status::NotFound()), StatusIs(Status::NotFound()));
+ EXPECT_THAT(Result<int>(Status::AlreadyExists()),
+ StatusIs(Status::AlreadyExists()));
+ EXPECT_THAT(Result<int>(Status::PermissionDenied()),
+ StatusIs(Status::PermissionDenied()));
+ EXPECT_THAT(Result<int>(Status::ResourceExhausted()),
+ StatusIs(Status::ResourceExhausted()));
+ EXPECT_THAT(Result<int>(Status::FailedPrecondition()),
+ StatusIs(Status::FailedPrecondition()));
+ EXPECT_THAT(Result<int>(Status::Aborted()), StatusIs(Status::Aborted()));
+ EXPECT_THAT(Result<int>(Status::OutOfRange()),
+ StatusIs(Status::OutOfRange()));
+ EXPECT_THAT(Result<int>(Status::Unimplemented()),
+ StatusIs(Status::Unimplemented()));
+ EXPECT_THAT(Result<int>(Status::Internal()), StatusIs(Status::Internal()));
+ EXPECT_THAT(Result<int>(Status::Unavailable()),
+ StatusIs(Status::Unavailable()));
+ EXPECT_THAT(Result<int>(Status::DataLoss()), StatusIs(Status::DataLoss()));
+ EXPECT_THAT(Result<int>(Status::Unauthenticated()),
+ StatusIs(Status::Unauthenticated()));
+}
+
+TEST(IsOkAndHoldsTest, StatusWithSize) {
+ const auto status_with_size = StatusWithSize{OkStatus(), 42};
+ EXPECT_THAT(status_with_size, IsOkAndHolds(Eq(42u)));
+}
+
+TEST(IsOkAndHoldsTest, Result) {
+ auto value = Result<int>{42};
+ EXPECT_THAT(value, IsOkAndHolds(Eq(42)));
+}
+
+TEST(IsOkAndHoldsTest, BadStatusWithSize) {
+ const auto status_with_size = StatusWithSize{Status::InvalidArgument(), 0};
+ EXPECT_THAT(status_with_size, Not(IsOkAndHolds(Eq(42u))));
+}
+
+TEST(IsOkAndHoldsTest, WrongStatusWithSize) {
+ const auto status_with_size = StatusWithSize{OkStatus(), 100};
+ EXPECT_THAT(status_with_size, IsOkAndHolds(Not(Eq(42u))));
+ EXPECT_THAT(status_with_size, Not(IsOkAndHolds(Eq(42u))));
+}
+
+TEST(IsOkAndHoldsTest, BadResult) {
+ const auto value = Result<int>{Status::InvalidArgument()};
+ EXPECT_THAT(value, Not(IsOkAndHolds(Eq(42))));
+}
+
+TEST(IsOkAndHoldsTest, WrongResult) {
+ const auto value = Result<int>{100};
+ EXPECT_THAT(value, IsOkAndHolds(Not(Eq(42))));
+ EXPECT_THAT(value, Not(IsOkAndHolds(Eq(42))));
+}
+
+TEST(AssertOkAndAssignTest, OkResult) {
+ const auto value = Result<int>(5);
+
+ int existing_value = 0;
+ ASSERT_OK_AND_ASSIGN(existing_value, value);
+ EXPECT_EQ(5, existing_value);
+
+ ASSERT_OK_AND_ASSIGN(int declare_and_assign, value);
+ EXPECT_EQ(5, declare_and_assign);
+
+ ASSERT_OK_AND_ASSIGN(auto& declare_auto_ref_and_assign, value);
+ EXPECT_EQ(5, declare_auto_ref_and_assign);
+}
+
+// The following test is commented out and is only for checking what
+// failure cases would look like. For example, when uncommenting the test,
+// the output is:
+//
+// ERR pw_unit_test/googletest_test_matchers_test.cc:50: Failure
+// ERR Expected:
+// ERR Actual: Value of: OkStatus()
+// Expected: has status UNKNOWN
+// Actual: 4-byte object <00-00 00-00>, which has status OK
+
+// ERR pw_unit_test/googletest_test_matchers_test.cc:51: Failure
+// ERR Expected:
+// ERR Actual: Value of: Status::Unknown()
+// Expected: is OK
+// Actual: 4-byte object <02-00 00-00>, which has status UNKNOWN
+
+// ERR pw_unit_test/googletest_test_matchers_test.cc:52: Failure
+// ERR Expected:
+// ERR Actual: Value of: Status::Unknown()
+// Expected: is OK
+// Actual: 4-byte object <02-00 00-00>, which has status UNKNOWN
+//
+// TEST(TestMatchers, SampleFailures) {
+// EXPECT_THAT(OkStatus(), StatusIs(Status::Unknown()));
+// EXPECT_OK(Status::Unknown());
+// ASSERT_OK(Status::Unknown());
+// }
+
+} // namespace
+} // namespace pw::unit_test \ No newline at end of file
diff --git a/pw_unit_test/logging_main.cc b/pw_unit_test/logging_main.cc
index b780e2ed3..a6490f894 100644
--- a/pw_unit_test/logging_main.cc
+++ b/pw_unit_test/logging_main.cc
@@ -15,7 +15,8 @@
#include "gtest/gtest.h"
#include "pw_unit_test/logging_event_handler.h"
-int main() {
+int main(int argc, char** argv) {
+ testing::InitGoogleTest(&argc, argv);
pw::unit_test::LoggingEventHandler handler;
pw::unit_test::RegisterEventHandler(&handler);
return RUN_ALL_TESTS();
diff --git a/pw_unit_test/public/pw_unit_test/event_handler.h b/pw_unit_test/public/pw_unit_test/event_handler.h
index d59004bee..14c2a0517 100644
--- a/pw_unit_test/public/pw_unit_test/event_handler.h
+++ b/pw_unit_test/public/pw_unit_test/event_handler.h
@@ -160,5 +160,10 @@ class EventHandler {
const TestExpectation& expectation) = 0;
};
+// Sets the event handler for a test run. Must be called before RUN_ALL_TESTS()
+// to receive test output. If `event_handler` is null, this will disable event
+// handling. This method is not thread-safe.
+void RegisterEventHandler(EventHandler* event_handler);
+
} // namespace unit_test
} // namespace pw
diff --git a/pw_unit_test/public/pw_unit_test/googletest_test_matchers.h b/pw_unit_test/public/pw_unit_test/googletest_test_matchers.h
new file mode 100644
index 000000000..e7965aaa5
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/googletest_test_matchers.h
@@ -0,0 +1,256 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <type_traits>
+#include <utility>
+
+#include "gmock/gmock-matchers.h"
+#include "gtest/gtest.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::unit_test {
+namespace internal {
+// Gets the pw::Status of different types of objects with a pw::Status for
+// Matchers that check the status.
+inline constexpr Status GetStatus(Status status) { return status; }
+
+inline constexpr Status GetStatus(StatusWithSize status_with_size) {
+ return status_with_size.status();
+}
+
+template <typename T>
+inline constexpr Status GetStatus(const Result<T>& result) {
+ return result.status();
+}
+
+// Gets the value of an object whose value is guarded by a ``pw::OkStatus()``.
+// Used by Matchers.
+constexpr size_t GetValue(StatusWithSize status_with_size) {
+ return status_with_size.size();
+}
+
+template <typename V>
+constexpr const V& GetValue(const Result<V>& result) {
+ return result.value();
+}
+
+template <typename V>
+constexpr V GetValue(Result<V>&& result) {
+ return std::move(result).value();
+}
+
+// Implements IsOk().
+class IsOkMatcher {
+ public:
+ using is_gtest_matcher = void;
+
+ void DescribeTo(std::ostream* os) const { *os << "is OK"; }
+
+ void DescribeNegationTo(std::ostream* os) const { *os << "isn't OK"; }
+
+ template <typename T>
+ bool MatchAndExplain(T&& actual_value,
+ ::testing::MatchResultListener* listener) const {
+ const auto status = GetStatus(actual_value);
+ if (!status.ok()) {
+ *listener << "which has status " << pw_StatusString(status);
+ return false;
+ }
+ return true;
+ }
+};
+
+// Implements IsOkAndHolds(m) as a monomorphic matcher.
+template <typename StatusType>
+class IsOkAndHoldsMatcherImpl {
+ public:
+ using is_gtest_matcher = void;
+ using ValueType = decltype(GetValue(std::declval<StatusType>()));
+
+ // NOLINTBEGIN(bugprone-forwarding-reference-overload)
+ template <typename InnerMatcher>
+ explicit IsOkAndHoldsMatcherImpl(InnerMatcher&& inner_matcher)
+ : inner_matcher_(::testing::SafeMatcherCast<const ValueType&>(
+ std::forward<InnerMatcher>(inner_matcher))) {}
+ // NOLINTEND(bugprone-forwarding-reference-overload)
+
+ void DescribeTo(std::ostream* os) const {
+ *os << "is OK and has a value that ";
+ inner_matcher_.DescribeTo(os);
+ }
+
+ void DescribeNegationTo(std::ostream* os) const {
+ *os << "isn't OK or has a value that ";
+ inner_matcher_.DescribeNegationTo(os);
+ }
+
+ bool MatchAndExplain(const StatusType& actual_value,
+ ::testing::MatchResultListener* listener) const {
+ const auto& status = GetStatus(actual_value);
+ if (!status.ok()) {
+ *listener << "which has status " << pw_StatusString(status);
+ return false;
+ }
+
+ const auto& value = GetValue(actual_value);
+ *listener << "which contains value " << ::testing::PrintToString(value);
+
+ ::testing::StringMatchResultListener inner_listener;
+ const bool matches = inner_matcher_.MatchAndExplain(value, &inner_listener);
+ const std::string inner_explanation = inner_listener.str();
+ if (!inner_explanation.empty()) {
+ *listener << ", " << inner_explanation;
+ }
+
+ return matches;
+ }
+
+ private:
+ const ::testing::Matcher<const ValueType&> inner_matcher_;
+};
+
+// Implements IsOkAndHolds(m) as a polymorphic matcher.
+//
+// We have to manually create it as a class instead of using the
+// `::testing::MakePolymorphicMatcher()` helper because of the custom conversion
+// to Matcher<T>.
+template <typename InnerMatcher>
+class IsOkAndHoldsMatcher {
+ public:
+ explicit IsOkAndHoldsMatcher(InnerMatcher inner_matcher)
+ : inner_matcher_(std::move(inner_matcher)) {}
+
+ // NOLINTBEGIN(google-explicit-constructor)
+ template <typename StatusType>
+ operator ::testing::Matcher<StatusType>() const {
+ return ::testing::Matcher<StatusType>(
+ internal::IsOkAndHoldsMatcherImpl<const StatusType&>(inner_matcher_));
+ }
+ // NOLINTEND(google-explicit-constructor)
+
+ private:
+ const InnerMatcher inner_matcher_;
+};
+
+// Implements StatusIs().
+class StatusIsMatcher {
+ public:
+ explicit StatusIsMatcher(Status expected_status)
+ : expected_status_(expected_status) {}
+
+ void DescribeTo(std::ostream* os) const {
+ *os << "has status " << pw_StatusString(expected_status_);
+ }
+
+ void DescribeNegationTo(std::ostream* os) const {
+ *os << "does not have status " << pw_StatusString(expected_status_);
+ }
+
+ template <typename T>
+ bool MatchAndExplain(T&& actual_value,
+ ::testing::MatchResultListener* listener) const {
+ const auto status = GetStatus(actual_value);
+ if (status != expected_status_) {
+ *listener << "which has status " << pw_StatusString(status);
+ return false;
+ }
+ return true;
+ }
+
+ private:
+ const Status expected_status_;
+};
+
+} // namespace internal
+
+/// Macros for testing the results of functions that return ``pw::Status``,
+/// ``pw::StatusWithSize``, or ``pw::Result<T>`` (for any T).
+#define EXPECT_OK(expression) EXPECT_THAT(expression, ::pw::unit_test::IsOk())
+#define ASSERT_OK(expression) ASSERT_THAT(expression, ::pw::unit_test::IsOk())
+
+/// Returns a gMock matcher that matches a `pw::Status`, `pw::StatusWithSize`,
+/// or `pw::Result<T>` (for any T) which is OK.
+inline internal::IsOkMatcher IsOk() { return {}; }
+
+/// Returns a gMock matcher that matches a `pw::Status`, `pw::StatusWithSize`,
+/// or `pw::Result<T>` (for any T) which has the given status.
+inline auto StatusIs(Status expected_status) {
+ return ::testing::MakePolymorphicMatcher(
+ internal::StatusIsMatcher(expected_status));
+}
+
+/// Returns a gMock matcher that matches a `pw::StatusWithSize` or
+/// `pw::Result<T>` (for any T) which is OK and holds a value matching the inner
+/// matcher.
+template <typename InnerMatcher>
+inline internal::IsOkAndHoldsMatcher<InnerMatcher> IsOkAndHolds(
+ InnerMatcher&& inner_matcher) {
+ return internal::IsOkAndHoldsMatcher<InnerMatcher>(
+ std::forward<InnerMatcher>(inner_matcher));
+}
+
+/// Executes an expression that returns a `pw::Result` or `pw::StatusWithSize`
+/// and assigns or moves that value to lhs if the error code is OK. If the
+// status is non-OK, generates a test failure and returns from the current
+/// function, which must have a void return type.
+///
+/// The MOVE variant moves the content out of the `pw::Result` and into lhs.
+/// This variant is required for move-only types.
+//
+/// Example: Declaring and initializing a new value. E.g.:
+/// ASSERT_OK_AND_ASSIGN(auto value, MaybeGetValue(arg));
+/// ASSERT_OK_AND_ASSIGN(const ValueType& value, MaybeGetValue(arg));
+/// ASSERT_OK_AND_MOVE(auto ptr, MaybeGetUniquePtr(arg))
+///
+/// Example: Assigning to an existing value
+/// ValueType value;
+/// ASSERT_OK_AND_ASSIGN(value, MaybeGetValue(arg));
+///
+/// The value assignment example would expand into something like:
+/// auto status_or_value = MaybeGetValue(arg);
+/// ASSERT_OK(status_or_value.status());
+/// value = status_or_value.ValueOrDie();
+///
+/// WARNING: ASSERT_OK_AND_ASSIGN (and the move variant) expand into multiple
+/// statements; it cannot be used in a single statement (e.g. as the body of
+/// an if statement without {})!
+#define ASSERT_OK_AND_ASSIGN(lhs, rexpr) \
+ ASSERT_OK_AND_ASSIGN_DETAIL(UNIQUE_IDENTIFIER_DETAIL(__LINE__), lhs, rexpr)
+#define ASSERT_OK_AND_MOVE(lhs, rexpr) \
+ ASSERT_OK_AND_MOVE_DETAIL(UNIQUE_IDENTIFIER_DETAIL(__LINE__), lhs, rexpr)
+
+// NOLINTBEGIN(bugprone-macro-parentheses)
+// The suggestion would produce bad code.
+#define ASSERT_OK_AND_ASSIGN_DETAIL(result, lhs, rexpr) \
+ const auto& result = (rexpr); \
+ if (!result.ok()) { \
+ FAIL() << #rexpr << " is not OK."; \
+ } \
+ lhs = ::pw::unit_test::internal::GetValue(result);
+#define ASSERT_OK_AND_MOVE_DETAIL(result, lhs, rexpr) \
+ auto&& result = (rexpr); \
+ if (!result.ok()) { \
+ FAIL() << #rexpr << " is not OK."; \
+ } \
+ lhs = ::pw::unit_test::internal::GetValue(std::move(result));
+// NOLINTEND(bugprone-macro-parentheses)
+
+#define UNIQUE_IDENTIFIER_DETAIL(line) UNIQUE_IDENTIFIER_EXPANDED_DETAIL(line)
+#define UNIQUE_IDENTIFIER_EXPANDED_DETAIL(line) \
+ _assert_ok_and_assign_unique_name_##line
+
+} // namespace pw::unit_test
diff --git a/pw_unit_test/public/pw_unit_test/internal/framework.h b/pw_unit_test/public/pw_unit_test/internal/framework.h
index 73af3928e..be93c64f8 100644
--- a/pw_unit_test/public/pw_unit_test/internal/framework.h
+++ b/pw_unit_test/public/pw_unit_test/internal/framework.h
@@ -47,40 +47,184 @@
_PW_TEST_SUITE_NAMES_MUST_BE_UNIQUE(int /* TEST_F */, test_fixture); \
_PW_TEST(test_fixture, test_name, test_fixture)
-#define EXPECT_TRUE(expr) static_cast<void>(_PW_TEST_BOOL(expr, true))
-#define EXPECT_FALSE(expr) static_cast<void>(_PW_TEST_BOOL(expr, false))
-#define EXPECT_EQ(lhs, rhs) static_cast<void>(_PW_TEST_OP(lhs, rhs, ==))
-#define EXPECT_NE(lhs, rhs) static_cast<void>(_PW_TEST_OP(lhs, rhs, !=))
-#define EXPECT_GT(lhs, rhs) static_cast<void>(_PW_TEST_OP(lhs, rhs, >))
-#define EXPECT_GE(lhs, rhs) static_cast<void>(_PW_TEST_OP(lhs, rhs, >=))
-#define EXPECT_LT(lhs, rhs) static_cast<void>(_PW_TEST_OP(lhs, rhs, <))
-#define EXPECT_LE(lhs, rhs) static_cast<void>(_PW_TEST_OP(lhs, rhs, <=))
-#define EXPECT_STREQ(lhs, rhs) static_cast<void>(_PW_TEST_C_STR(lhs, rhs, ==))
-#define EXPECT_STRNE(lhs, rhs) static_cast<void>(_PW_TEST_C_STR(lhs, rhs, !=))
-
+// Use of the FRIEND_TEST() macro is discouraged, because it induces coupling
+// between testing and implementation code. Consider this a last resort only.
+#define FRIEND_TEST(test_suite_name, test_name) \
+ friend class test_suite_name##_##test_name##_Test
+
+/// @def EXPECT_TRUE
+/// Verifies that @p expr evaluates to true.
+/// @param expr Condition to evaluate
+#define EXPECT_TRUE(expr) _PW_TEST_EXPECT(_PW_TEST_BOOL(expr, true))
+
+/// @def EXPECT_FALSE
+/// Verifies that @p expr evaluates to false.
+/// @param expr Condition to evaluate
+#define EXPECT_FALSE(expr) _PW_TEST_EXPECT(_PW_TEST_BOOL(expr, false))
+
+/// @def EXPECT_EQ
+/// Verifies that @p lhs == @p rhs
+/// <p>
+/// Does pointer equality on pointers. If used on two C strings, it tests if
+/// they are in the same memory location, not if they have the same value. Use
+/// #EXPECT_STREQ to compare C strings (e.g. <code>const char*</code>) by value.
+/// <p>
+/// When comparing a pointer to <code>NULL</code> use
+/// <code>EXPECT_EQ(ptr, nullptr)</code> instead of
+/// <code>EXPECT_EQ(ptr, NULL)</code>.
+/// @param lhs The left side of the equality comparison
+/// @param rhs The right side of the equality comparison
+#define EXPECT_EQ(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_OP(lhs, rhs, ==))
+
+/// @def EXPECT_NE
+/// Verifies that @p lhs != @p rhs
+/// <p>
+/// Does pointer equality on pointers. If used on two C strings, it tests if
+/// they are in different memory locations, not if they have different values.
+/// Use #EXPECT_STRNE to compare C strings (e.g. <code>const char*</code>) by
+/// value.
+/// <p>
+/// When comparing a pointer to <code>NULL</code>, use
+/// <code>EXPECT_NE(ptr, nullptr)</code> instead of
+/// <code>EXPECT_NE(ptr, NULL)</code>.
+/// @param lhs The left side of the inequality comparison
+/// @param rhs The right side of the inequality comparison
+#define EXPECT_NE(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_OP(lhs, rhs, !=))
+
+/// @def EXPECT_GT
+/// Verifies that @p lhs > @p rhs
+/// @param lhs The left side of the comparison
+/// @param rhs The right side of the comparison
+#define EXPECT_GT(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_OP(lhs, rhs, >))
+
+/// @def EXPECT_GE
+/// Verifies that @p lhs >= @p rhs
+/// @param lhs The left side of the comparison
+/// @param rhs The right side of the comparison
+#define EXPECT_GE(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_OP(lhs, rhs, >=))
+
+/// @def EXPECT_LT
+/// Verifies that @p lhs < @p rhs
+/// @param lhs The left side of the comparison
+/// @param rhs The right side of the comparison
+#define EXPECT_LT(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_OP(lhs, rhs, <))
+
+/// @def EXPECT_LE
+/// Verifies that @p lhs <= @p rhs
+/// @param lhs The left side of the comparison
+/// @param rhs The right side of the comparison
+#define EXPECT_LE(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_OP(lhs, rhs, <=))
+
+/// @def EXPECT_NEAR
+/// Verifies that the difference between @p lhs and @p rhs does not exceed the
+/// absolute error bound @p epsilon.
+/// @param lhs The left side of the comparison
+/// @param rhs The right side of the comparison
+/// @param epsilon The maximum difference between @p lhs and @p rhs
+#define EXPECT_NEAR(lhs, rhs, epsilon) \
+ _PW_TEST_EXPECT(_PW_TEST_NEAR(lhs, rhs, epsilon))
+
+/// @def EXPECT_FLOAT_EQ
+/// Verifies that the two float values @p rhs and @p lhs are approximately
+/// equal, to within 4 ULPs from each other.
+/// @param lhs The left side of the equality comparison
+/// @param rhs The right side of the equality comparison
+#define EXPECT_FLOAT_EQ(lhs, rhs) \
+ _PW_TEST_EXPECT( \
+ _PW_TEST_NEAR(lhs, rhs, 4 * std::numeric_limits<float>::epsilon()))
+
+/// @def EXPECT_DOUBLE_EQ
+/// Verifies that the two double values @p rhs and @p lhs are approximately
+/// equal, to within 4 ULPs from each other.
+/// @param lhs The left side of the equality comparison
+/// @param rhs The right side of the equality comparison
+#define EXPECT_DOUBLE_EQ(lhs, rhs) \
+ _PW_TEST_EXPECT( \
+ _PW_TEST_NEAR(lhs, rhs, 4 * std::numeric_limits<double>::epsilon()))
+
+/// @def EXPECT_STREQ
+/// Verifies that the two C strings @p lhs and @p rhs have the same contents.
+/// @param lhs The left side of the equality comparison
+/// @param rhs The right side of the equality comparison
+#define EXPECT_STREQ(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_C_STR(lhs, rhs, ==))
+
+/// @def EXPECT_STRNE
+/// Verifies that the two C strings @p lhs and @p rhs have different content
+/// @param lhs The left side of the inequality comparison
+/// @param rhs The right side of the inequality comparison
+#define EXPECT_STRNE(lhs, rhs) _PW_TEST_EXPECT(_PW_TEST_C_STR(lhs, rhs, !=))
+
+/// @def ASSERT_TRUE
+/// @see EXPECT_TRUE
#define ASSERT_TRUE(expr) _PW_TEST_ASSERT(_PW_TEST_BOOL(expr, true))
+
+/// @def ASSERT_FALSE
+/// @see EXPECT_FALSE
#define ASSERT_FALSE(expr) _PW_TEST_ASSERT(_PW_TEST_BOOL(expr, false))
+
+/// @def ASSERT_EQ
+/// @see EXPECT_EQ
#define ASSERT_EQ(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_OP(lhs, rhs, ==))
+
+/// @def ASSERT_NE
+/// @see EXPECT_NE
#define ASSERT_NE(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_OP(lhs, rhs, !=))
+
+/// @def ASSERT_GT
+/// @see EXPECT_GT
#define ASSERT_GT(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_OP(lhs, rhs, >))
+
+/// @def ASSERT_GE
+/// @see EXPECT_GE
#define ASSERT_GE(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_OP(lhs, rhs, >=))
+
+/// @def ASSERT_LT
+/// @see EXPECT_LT
#define ASSERT_LT(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_OP(lhs, rhs, <))
+
+/// @def ASSERT_LE
+/// @see EXPECT_LE
#define ASSERT_LE(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_OP(lhs, rhs, <=))
+
+/// @def ASSERT_NEAR
+/// @see EXPECT_NEAR
+#define ASSERT_NEAR(lhs, rhs, epsilon) \
+ _PW_TEST_ASSERT(_PW_TEST_NEAR(lhs, rhs, epsilon))
+
+/// @def ASSERT_FLOAT_EQ
+/// @see EXPECT_FLOAT_EQ
+#define ASSERT_FLOAT_EQ(lhs, rhs) \
+ _PW_TEST_ASSERT( \
+ _PW_TEST_NEAR(lhs, rhs, 4 * std::numeric_limits<float>::epsilon()))
+
+/// @def ASSERT_DOUBLE_EQ
+/// @see EXPECT_DOUBLE_EQ
+#define ASSERT_DOUBLE_EQ(lhs, rhs) \
+ _PW_TEST_ASSERT( \
+ _PW_TEST_NEAR(lhs, rhs, 4 * std::numeric_limits<double>::epsilon()))
+
+/// @def ASSERT_STREQ
+/// @see EXPECT_STREQ
#define ASSERT_STREQ(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_C_STR(lhs, rhs, ==))
+
+/// @def ASSERT_STRNE
+/// @see EXPECT_STRNE
#define ASSERT_STRNE(lhs, rhs) _PW_TEST_ASSERT(_PW_TEST_C_STR(lhs, rhs, !=))
// Generates a non-fatal failure with a generic message.
-#define ADD_FAILURE() \
- ::pw::unit_test::internal::Framework::Get().CurrentTestExpectSimple( \
- "(line is not executed)", "(line was executed)", __LINE__, false)
+#define ADD_FAILURE() \
+ ::pw::unit_test::internal::Framework::Get().CurrentTestExpectSimple( \
+ "(line is not executed)", "(line was executed)", __LINE__, false); \
+ _PW_UNIT_TEST_LOG
// Generates a fatal failure with a generic message.
#define GTEST_FAIL() return ADD_FAILURE()
// Skips test at runtime, which is neither successful nor failed. Skip aborts
// current function.
-#define GTEST_SKIP() \
- return ::pw::unit_test::internal::Framework::Get().CurrentTestSkip(__LINE__)
+#define GTEST_SKIP() \
+ ::pw::unit_test::internal::Framework::Get().CurrentTestSkip(__LINE__); \
+ return _PW_UNIT_TEST_LOG
// Define either macro to 1 to omit the definition of FAIL(), which is a
// generic name and clashes with some other libraries.
@@ -91,7 +235,8 @@
// Generates a success with a generic message.
#define GTEST_SUCCEED() \
::pw::unit_test::internal::Framework::Get().CurrentTestExpectSimple( \
- "(success)", "(success)", __LINE__, true)
+ "(success)", "(success)", __LINE__, true); \
+ _PW_UNIT_TEST_LOG
// Define either macro to 1 to omit the definition of SUCCEED(), which
// is a generic name and clashes with some other libraries.
@@ -107,7 +252,8 @@
// In order to receive test output, an event handler must be registered before
// this is called:
//
-// int main() {
+// int main(int argc, char** argv) {
+// testing::InitGoogleTest(&argc, argv);
// MyEventHandler handler;
// pw::unit_test::RegisterEventHandler(&handler);
// return RUN_ALL_TESTS();
@@ -155,7 +301,39 @@ namespace string {
template <typename T>
StatusWithSize UnknownTypeToString(const T& value, span<char> buffer) {
StringBuilder sb(buffer);
- sb << '<' << sizeof(value) << "-byte object at 0x" << &value << '>';
+ sb << '<' << sizeof(value) << "-byte object at 0x" << &value;
+
+ // How many bytes of the object to print.
+ //
+ // WARNING: Printing the contents of an object may be undefined behavior!
+ // Accessing unintialized memory is undefined behavior, and objects sometimes
+ // contiain uninitialized regions, such as padding bytes or unalloacted
+ // storage (e.g. std::optional). kPrintMaybeUnintializedBytes MUST stay at 0,
+ // except when changed locally to help with debugging.
+ constexpr size_t kPrintMaybeUnintializedBytes = 0;
+
+ constexpr size_t kBytesToPrint =
+ std::min(sizeof(value), kPrintMaybeUnintializedBytes);
+
+ if (kBytesToPrint != 0u) {
+ sb << " |";
+
+ // reinterpret_cast to std::byte is permitted by C++'s type aliasing rules.
+ const std::byte* bytes = reinterpret_cast<const std::byte*>(&value);
+
+ for (size_t i = 0; i < kBytesToPrint; ++i) {
+ sb << ' ' << bytes[i];
+ }
+
+ // If there's just one more byte, output it. Otherwise, output ellipsis.
+ if (sizeof(value) == kBytesToPrint + 1) {
+ sb << ' ' << bytes[sizeof(value) - 1];
+ } else if (sizeof(value) > kBytesToPrint) {
+ sb << " …";
+ }
+ }
+
+ sb << '>';
return sb.status_with_size();
}
@@ -164,16 +342,15 @@ StatusWithSize UnknownTypeToString(const T& value, span<char> buffer) {
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
namespace unit_test {
-
-// Sets the event handler for a test run. Must be called before RUN_ALL_TESTS()
-// to receive test output.
-void RegisterEventHandler(EventHandler* event_handler);
-
namespace internal {
class Test;
class TestInfo;
+// Types of SetUpTestSuite() and TearDownTestSuite() functions.
+using SetUpTestSuiteFunc = void (*)();
+using TearDownTestSuiteFunc = void (*)();
+
// Used to tag arguments to EXPECT_STREQ/EXPECT_STRNE so they are treated like C
// strings rather than pointers.
struct CStringArg {
@@ -204,7 +381,7 @@ class Framework {
// Sets the handler to which the framework dispatches test events. During a
// test run, the framework owns the event handler.
- void RegisterEventHandler(EventHandler* event_handler) {
+ inline void RegisterEventHandler(EventHandler* event_handler) {
event_handler_ = event_handler;
}
@@ -227,6 +404,9 @@ class Framework {
// Whether the current test is skipped.
bool IsSkipped() const { return current_result_ == TestResult::kSkipped; }
+ // Whether the current test has failed.
+ bool HasFailure() const { return current_result_ == TestResult::kFailure; }
+
// Constructs an instance of a unit test class and runs the test.
//
// Tests are constructed within a static memory pool at run time instead of
@@ -250,6 +430,8 @@ class Framework {
// uninitialized memory.
std::memset(&framework.memory_pool_, 0xa5, sizeof(framework.memory_pool_));
+ framework.SetUpTestSuiteIfNeeded(TestInstance::SetUpTestSuite);
+
// Construct the test object within the static memory pool. The StartTest
// function has already been called by the TestInfo at this point.
TestInstance* test_instance = new (&framework.memory_pool_) TestInstance;
@@ -259,9 +441,42 @@ class Framework {
// objects constructed using placement new.
test_instance->~TestInstance();
+ framework.TearDownTestSuiteIfNeeded(TestInstance::TearDownTestSuite);
+
framework.EndCurrentTest();
}
+ template <typename Expectation, typename Lhs, typename Rhs, typename Epsilon>
+ bool CurrentTestExpect(Expectation expectation,
+ const Lhs& lhs,
+ const Rhs& rhs,
+ const Epsilon& epsilon,
+ const char* expression,
+ int line) {
+ // Size of the buffer into which to write the string with the evaluated
+ // version of the arguments. This buffer is allocated on the unit test's
+ // stack, so it shouldn't be too large.
+ // TODO(hepler): Make this configurable.
+ [[maybe_unused]] constexpr size_t kExpectationBufferSizeBytes = 192;
+
+ const bool success = expectation(lhs, rhs, epsilon);
+ CurrentTestExpectSimple(
+ expression,
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ MakeString<kExpectationBufferSizeBytes>(ConvertForPrint(lhs),
+ " within ",
+ ConvertForPrint(epsilon),
+ " of ",
+ ConvertForPrint(rhs))
+ .c_str(),
+#else
+ "(evaluation requires C++17)",
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+ line,
+ success);
+ return success;
+ }
+
// Runs an expectation function for the currently active test case.
template <typename Expectation, typename Lhs, typename Rhs>
bool CurrentTestExpect(Expectation expectation,
@@ -274,7 +489,7 @@ class Framework {
// version of the arguments. This buffer is allocated on the unit test's
// stack, so it shouldn't be too large.
// TODO(hepler): Make this configurable.
- [[maybe_unused]] constexpr size_t kExpectationBufferSizeBytes = 128;
+ [[maybe_unused]] constexpr size_t kExpectationBufferSizeBytes = 192;
const bool success = expectation(lhs, rhs);
CurrentTestExpectSimple(
@@ -320,6 +535,12 @@ class Framework {
return std::forward<T>(value);
}
+ // If current_test_ will be first of its suite, call set_up_ts
+ void SetUpTestSuiteIfNeeded(SetUpTestSuiteFunc set_up_ts) const;
+
+ // If current_test_ was the last of its suite, call tear_down_ts
+ void TearDownTestSuiteIfNeeded(TearDownTestSuiteFunc tear_down_ts) const;
+
// Sets current_test_ and dispatches an event indicating that a test started.
void StartTest(const TestInfo& test);
@@ -418,6 +639,11 @@ class Test {
virtual ~Test() = default;
+ static void SetUpTestSuite() {}
+ static void TearDownTestSuite() {}
+
+ static bool HasFailure() { return Framework::Get().HasFailure(); }
+
// Runs the unit test.
void PigweedTestRun() {
SetUp();
@@ -465,6 +691,35 @@ constexpr bool HasNoUnderscores(const char* suite) {
return true;
}
+// GoogleTest supports stream-style messages, but pw_unit_test does not. This
+// class accepts and ignores C++ <<-style logs. This could be replaced with
+// pw_log/glog_adapter.h.
+class IgnoreLogs {
+ public:
+ constexpr IgnoreLogs() = default;
+
+ template <typename T>
+ constexpr const IgnoreLogs& operator<<(const T&) const {
+ return *this;
+ }
+};
+
+// Used to ignore a stream-style message in an assert, which returns. This uses
+// a similar approach as upstream GoogleTest, but drops any messages.
+class ReturnHelper {
+ public:
+ constexpr ReturnHelper() = default;
+
+ // Return void so that assigning to ReturnHelper converts the log expression
+ // to void without blocking the stream-style log with a closing parenthesis.
+ // NOLINTNEXTLINE(misc-unconventional-assign-operator)
+ constexpr void operator=(const IgnoreLogs&) const {}
+};
+
+#define _PW_UNIT_TEST_LOG \
+ ::pw::unit_test::internal::ReturnHelper() = \
+ ::pw::unit_test::internal::IgnoreLogs()
+
} // namespace internal
#if PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -510,12 +765,13 @@ inline void SetTestSuitesToRun(span<std::string_view> test_suites) {
\
void class_name::PigweedTestBody()
-#define _PW_TEST_ASSERT(expectation) \
- do { \
- if (!(expectation)) { \
- return static_cast<void>(0); /* Prevent using ASSERT in constructors. */ \
- } \
- } while (0)
+#define _PW_TEST_ASSERT(expectation) \
+ if (!(expectation)) \
+ return _PW_UNIT_TEST_LOG
+
+#define _PW_TEST_EXPECT(expectation) \
+ if (!(expectation)) \
+ _PW_UNIT_TEST_LOG
#define _PW_TEST_BOOL(expr, value) \
::pw::unit_test::internal::Framework::Get().CurrentTestExpect( \
@@ -537,10 +793,27 @@ inline void SetTestSuitesToRun(span<std::string_view> test_suites) {
#lhs " " #op " " #rhs, \
__LINE__)
+#define _PW_TEST_NEAR(lhs, rhs, epsilon) \
+ ::pw::unit_test::internal::Framework::Get().CurrentTestExpect( \
+ [](const auto& _pw_lhs, const auto& _pw_rhs, const auto& _pw_epsilon) { \
+ return std::abs(_pw_lhs - _pw_rhs) <= _pw_epsilon; \
+ }, \
+ (lhs), \
+ (rhs), \
+ (epsilon), \
+ #lhs " within " #epsilon " of " #rhs, \
+ __LINE__)
+
#define _PW_TEST_C_STR(lhs, rhs, op) \
::pw::unit_test::internal::Framework::Get().CurrentTestExpect( \
[](const auto& _pw_lhs, const auto& _pw_rhs) { \
- return std::strcmp(_pw_lhs.c_str, _pw_rhs.c_str) op 0; \
+ auto cmp = [](const char* l, const char* r) -> int { \
+ if (!l || !r) { \
+ return l != r; \
+ } \
+ return std::strcmp(l, r); \
+ }; \
+ return cmp(_pw_lhs.c_str, _pw_rhs.c_str) op 0; \
}, \
::pw::unit_test::internal::CStringArg{lhs}, \
::pw::unit_test::internal::CStringArg{rhs}, \
@@ -568,9 +841,12 @@ inline void SetTestSuitesToRun(span<std::string_view> test_suites) {
PW_MODIFY_DIAGNOSTICS_POP()
#endif // GCC8 or older.
-// Alias Test as ::testing::Test for GoogleTest compatibility.
namespace testing {
+// Alias Test as ::testing::Test for GoogleTest compatibility.
using Test = ::pw::unit_test::internal::Test;
+// Provide a no-op init routine for GoogleTest compatibility.
+inline void InitGoogleTest(int*, char**) {}
+
} // namespace testing
diff --git a/pw_unit_test/public/pw_unit_test/printf_event_handler.h b/pw_unit_test/public/pw_unit_test/printf_event_handler.h
index 41dc6b28d..796b9fc21 100644
--- a/pw_unit_test/public/pw_unit_test/printf_event_handler.h
+++ b/pw_unit_test/public/pw_unit_test/printf_event_handler.h
@@ -13,6 +13,7 @@
// the License.
#pragma once
+#include <cstdarg>
#include <cstdio>
#include "pw_unit_test/googletest_style_event_handler.h"
@@ -31,7 +32,7 @@ class PrintfEventHandler final : public GoogleTestStyleEventHandler {
void Write(const char* content) override { std::printf("%s", content); }
void WriteLine(const char* format, ...) override {
- va_list args;
+ std::va_list args;
va_start(args, format);
std::vprintf(format, args);
diff --git a/pw_unit_test/public/pw_unit_test/unit_test_service.h b/pw_unit_test/public/pw_unit_test/unit_test_service.h
index ca32ef5d8..675b7191f 100644
--- a/pw_unit_test/public/pw_unit_test/unit_test_service.h
+++ b/pw_unit_test/public/pw_unit_test/unit_test_service.h
@@ -41,7 +41,7 @@ class UnitTestService final
event_writer(event);
if (event.status().ok()) {
writer_.Write(event)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
}
diff --git a/pw_unit_test/py/BUILD.gn b/pw_unit_test/py/BUILD.gn
index d501fafbf..0864be24c 100644
--- a/pw_unit_test/py/BUILD.gn
+++ b/pw_unit_test/py/BUILD.gn
@@ -15,17 +15,19 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action_test.gni")
import("$dir_pw_rpc/internal/integration_test_ports.gni")
+import("$dir_pw_unit_test/test.gni")
pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_unit_test/__init__.py",
"pw_unit_test/rpc.py",
+ "pw_unit_test/serial_test_runner.py",
"pw_unit_test/test_runner.py",
]
python_deps = [
@@ -37,7 +39,8 @@ pw_python_package("py") {
mypy_ini = "$dir_pigweed/.mypy.ini"
}
-pw_python_script("rpc_service_test") {
+pw_python_action_test("rpc_service_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "rpc_service_test.py" ]
python_deps = [
":py",
@@ -45,16 +48,11 @@ pw_python_script("rpc_service_test") {
"$dir_pw_rpc/py",
"$dir_pw_status/py",
]
- pylintrc = "$dir_pigweed/.pylintrc"
- mypy_ini = "$dir_pigweed/.mypy.ini"
-
- action = {
- args = [
- "--port=$pw_unit_test_RPC_SERVICE_TEST_PORT",
- "--test-server-command",
- "<TARGET_FILE(..:test_rpc_server)>",
- ]
- deps = [ "..:test_rpc_server" ]
- stamp = true
- }
+ args = [
+ "--port=$pw_unit_test_RPC_SERVICE_TEST_PORT",
+ "--test-server-command",
+ "<TARGET_FILE(..:test_rpc_server)>",
+ ]
+ deps = [ "..:test_rpc_server" ]
+ tags = [ "integration" ]
}
diff --git a/pw_unit_test/py/pw_unit_test/rpc.py b/pw_unit_test/py/pw_unit_test/rpc.py
index ac1c56a2f..459071ec0 100644
--- a/pw_unit_test/py/pw_unit_test/rpc.py
+++ b/pw_unit_test/py/pw_unit_test/rpc.py
@@ -17,7 +17,7 @@ import enum
import abc
from dataclasses import dataclass
import logging
-from typing import Iterable
+from typing import Iterable, List, Tuple
from pw_rpc.client import Services
from pw_rpc.callback_client import OptionalTimeout, UseDefault
@@ -133,13 +133,28 @@ class LoggingEventHandler(EventHandler):
log(' Actual: %s', expectation.evaluated_expression)
+@dataclass(frozen=True)
+class TestRecord:
+ """Class for recording test results."""
+
+ passing_tests: Tuple[TestCase, ...]
+ failing_tests: Tuple[TestCase, ...]
+ disabled_tests: Tuple[TestCase, ...]
+
+ def all_tests_passed(self) -> bool:
+ return not self.failing_tests
+
+ def __bool__(self) -> bool:
+ return self.all_tests_passed()
+
+
def run_tests(
rpcs: Services,
report_passed_expectations: bool = False,
test_suites: Iterable[str] = (),
event_handlers: Iterable[EventHandler] = (LoggingEventHandler(),),
timeout_s: OptionalTimeout = UseDefault.VALUE,
-) -> bool:
+) -> TestRecord:
"""Runs unit tests on a device over Pigweed RPC.
Calls each of the provided event handlers as test events occur, and returns
@@ -174,39 +189,53 @@ def run_tests(
for event_handler in event_handlers:
event_handler.run_all_tests_start()
- all_tests_passed = False
+ passing_tests: List[TestCase] = []
+ failing_tests: List[TestCase] = []
+ disabled_tests: List[TestCase] = []
for response in test_responses:
- if response.HasField('test_case_start'):
- raw_test_case = response.test_case_start
- current_test_case = _test_case(raw_test_case)
-
- for event_handler in event_handlers:
- if response.HasField('test_run_start'):
+ if response.HasField('test_run_start'):
+ for event_handler in event_handlers:
event_handler.run_all_tests_start()
- elif response.HasField('test_run_end'):
+ elif response.HasField('test_run_end'):
+ for event_handler in event_handlers:
event_handler.run_all_tests_end(
response.test_run_end.passed, response.test_run_end.failed
)
- if response.test_run_end.failed == 0:
- all_tests_passed = True
- elif response.HasField('test_case_start'):
+ assert len(passing_tests) == response.test_run_end.passed
+ assert len(failing_tests) == response.test_run_end.failed
+ test_record = TestRecord(
+ passing_tests=tuple(passing_tests),
+ failing_tests=tuple(failing_tests),
+ disabled_tests=tuple(disabled_tests),
+ )
+ elif response.HasField('test_case_start'):
+ raw_test_case = response.test_case_start
+ current_test_case = _test_case(raw_test_case)
+ for event_handler in event_handlers:
event_handler.test_case_start(current_test_case)
- elif response.HasField('test_case_end'):
- result = TestCaseResult(response.test_case_end)
+ elif response.HasField('test_case_end'):
+ result = TestCaseResult(response.test_case_end)
+ for event_handler in event_handlers:
event_handler.test_case_end(current_test_case, result)
- elif response.HasField('test_case_disabled'):
- event_handler.test_case_disabled(
- _test_case(response.test_case_disabled)
- )
- elif response.HasField('test_case_expectation'):
- raw_expectation = response.test_case_expectation
- expectation = TestExpectation(
- raw_expectation.expression,
- raw_expectation.evaluated_expression,
- raw_expectation.line_number,
- raw_expectation.success,
- )
+ if result == TestCaseResult.SUCCESS:
+ passing_tests.append(current_test_case)
+ else:
+ failing_tests.append(current_test_case)
+ elif response.HasField('test_case_disabled'):
+ raw_test_case = response.test_case_disabled
+ current_test_case = _test_case(raw_test_case)
+ for event_handler in event_handlers:
+ event_handler.test_case_disabled(current_test_case)
+ disabled_tests.append(current_test_case)
+ elif response.HasField('test_case_expectation'):
+ raw_expectation = response.test_case_expectation
+ expectation = TestExpectation(
+ raw_expectation.expression,
+ raw_expectation.evaluated_expression,
+ raw_expectation.line_number,
+ raw_expectation.success,
+ )
+ for event_handler in event_handlers:
event_handler.test_case_expect(current_test_case, expectation)
-
- return all_tests_passed
+ return test_record
diff --git a/pw_unit_test/py/pw_unit_test/serial_test_runner.py b/pw_unit_test/py/pw_unit_test/serial_test_runner.py
new file mode 100644
index 000000000..cdb879ba7
--- /dev/null
+++ b/pw_unit_test/py/pw_unit_test/serial_test_runner.py
@@ -0,0 +1,196 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""This library facilitates automating unit tests on devices with serial ports.
+
+This library assumes that the on-device test runner will emit the test results
+as plain-text over a serial port, and tests will be triggered by a pre-defined
+input (DEFAULT_TEST_START_CHARACTER) over the same serial port that results
+are emitted from.
+"""
+
+import abc
+import logging
+from pathlib import Path
+
+import serial # type: ignore
+
+
+_LOG = logging.getLogger("serial_test_runner")
+
+# Verification of test pass/failure depends on these strings. If the formatting
+# or output of the simple_printing_event_handler changes, this may need to be
+# updated.
+_TESTS_STARTING_STRING = b'[==========] Running all tests.'
+_TESTS_DONE_STRING = b'[==========] Done running all tests.'
+_TEST_FAILURE_STRING = b'[ FAILED ]'
+
+# Character used to trigger test start.
+DEFAULT_TEST_START_CHARACTER = ' '.encode('utf-8')
+
+
+class FlashingFailure(Exception):
+ """A simple exception to be raised when flashing fails."""
+
+
+class TestingFailure(Exception):
+ """A simple exception to be raised when a testing step fails."""
+
+
+class DeviceNotFound(Exception):
+ """A simple exception to be raised when unable to connect to a device."""
+
+
+class SerialTestingDevice(abc.ABC):
+ """A device that supports automated testing via parsing serial output."""
+
+ @abc.abstractmethod
+ def load_binary(self, binary: Path) -> None:
+ """Flashes the specified binary to this device.
+
+ Raises:
+ DeviceNotFound: This device is no longer available.
+ FlashingFailure: The binary could not be flashed.
+ """
+
+ @abc.abstractmethod
+ def serial_port(self) -> str:
+ """Returns the name of the com port this device is enumerated on.
+
+ Raises:
+ DeviceNotFound: This device is no longer available.
+ """
+
+ @abc.abstractmethod
+ def baud_rate(self) -> int:
+ """Returns the baud rate to use when connecting to this device.
+
+ Raises:
+ DeviceNotFound: This device is no longer available.
+ """
+
+
+def _log_subprocess_output(level, output: bytes, logger: logging.Logger):
+ """Logs subprocess output line-by-line."""
+
+ lines = output.decode('utf-8', errors='replace').splitlines()
+ for line in lines:
+ logger.log(level, line)
+
+
+def trigger_test_run(
+ port: str,
+ baud_rate: int,
+ test_timeout: float,
+ trigger_data: bytes = DEFAULT_TEST_START_CHARACTER,
+) -> bytes:
+ """Triggers a test run, and returns captured test results."""
+
+ serial_data = bytearray()
+ device = serial.Serial(baudrate=baud_rate, port=port, timeout=test_timeout)
+ if not device.is_open:
+ raise TestingFailure('Failed to open device')
+
+ # Flush input buffer and trigger the test start.
+ device.reset_input_buffer()
+ device.write(trigger_data)
+
+ # Block and wait for the first byte.
+ serial_data += device.read()
+ if not serial_data:
+ raise TestingFailure('Device not producing output')
+
+ # Read with a reasonable timeout until we stop getting characters.
+ while True:
+ bytes_read = device.readline()
+ if not bytes_read:
+ break
+ serial_data += bytes_read
+ if serial_data.rfind(_TESTS_DONE_STRING) != -1:
+ # Set to much more aggressive timeout since the last one or two
+ # lines should print out immediately. (one line if all fails or all
+ # passes, two lines if mixed.)
+ device.timeout = 0.01
+
+ # Remove carriage returns.
+ serial_data = serial_data.replace(b"\r", b"")
+
+ # Try to trim captured results to only contain most recent test run.
+ test_start_index = serial_data.rfind(_TESTS_STARTING_STRING)
+ return (
+ serial_data
+ if test_start_index == -1
+ else serial_data[test_start_index:]
+ )
+
+
+def handle_test_results(
+ test_output: bytes, logger: logging.Logger = _LOG
+) -> None:
+ """Parses test output to determine whether tests passed or failed.
+
+ Raises:
+ TestingFailure if any tests fail or if test results are incomplete.
+ """
+
+ if test_output.find(_TESTS_STARTING_STRING) == -1:
+ raise TestingFailure('Failed to find test start')
+
+ if test_output.rfind(_TESTS_DONE_STRING) == -1:
+ _log_subprocess_output(logging.INFO, test_output, logger)
+ raise TestingFailure('Tests did not complete')
+
+ if test_output.rfind(_TEST_FAILURE_STRING) != -1:
+ _log_subprocess_output(logging.INFO, test_output, logger)
+ raise TestingFailure('Test suite had one or more failures')
+
+ _log_subprocess_output(logging.DEBUG, test_output, logger)
+
+ logger.info('Test passed!')
+
+
+def run_device_test(
+ device: SerialTestingDevice,
+ binary: Path,
+ test_timeout: float,
+ logger: logging.Logger = _LOG,
+) -> bool:
+ """Runs tests on a device.
+
+ When a unit test run fails, results will be logged as an error.
+
+ Args:
+ device: The device to run tests on.
+ binary: The binary containing tests that will be flashed to the device.
+ test_timeout: If the device stops producing output longer than this
+ timeout, the test will be considered stuck and the test will be aborted.
+
+ Returns:
+ True if all tests passed.
+ """
+
+ logger.info('Flashing binary to device')
+ device.load_binary(binary)
+ try:
+ logger.info('Running test')
+ test_output = trigger_test_run(
+ device.serial_port(), device.baud_rate(), test_timeout
+ )
+ if test_output:
+ handle_test_results(test_output, logger)
+ except TestingFailure as err:
+ logger.error(err)
+ return False
+
+ return True
diff --git a/pw_unit_test/py/pw_unit_test/test_runner.py b/pw_unit_test/py/pw_unit_test/test_runner.py
index a7b06ac5e..1869ce026 100644
--- a/pw_unit_test/py/pw_unit_test/test_runner.py
+++ b/pw_unit_test/py/pw_unit_test/test_runner.py
@@ -65,12 +65,13 @@ def register_arguments(parser: argparse.ArgumentParser) -> None:
'-m', '--timeout', type=float, help='Timeout for test runner in seconds'
)
parser.add_argument(
- '--coverage-profraw',
- type=str,
- help='The name of the coverage profraw file to produce with the'
- ' coverage information from this test. Only provide this if the test'
- ' should be run for coverage and is properly instrumented.',
+ '-e',
+ '--env',
+ nargs='*',
+ help='Environment variables to set for the test. These should be of the'
+ ' form `var_name=value`.',
)
+
parser.add_argument(
'runner_args', nargs="*", help='Arguments to forward to the test runner'
)
@@ -160,15 +161,17 @@ class TestRunner:
executable: str,
args: Sequence[str],
tests: Iterable[Test],
- coverage_profraw: Optional[str] = None,
+ env: Optional[Dict[str, str]] = None,
timeout: Optional[float] = None,
+ verbose: bool = False,
) -> None:
self._executable: str = executable
self._args: Sequence[str] = args
self._tests: List[Test] = list(tests)
- self._coverage_profraw = coverage_profraw
+ self._env: Dict[str, str] = env or {}
self._timeout = timeout
self._result_sink: Optional[Dict[str, str]] = None
+ self.verbose = verbose
# Access go/result-sink, if available.
ctx_path = Path(os.environ.get("LUCI_CONTEXT", ''))
@@ -201,11 +204,11 @@ class TestRunner:
test.start_time = datetime.datetime.now(datetime.timezone.utc)
start_time = time.monotonic()
try:
- env = {}
- if self._coverage_profraw is not None:
- env['LLVM_PROFILE_FILE'] = str(Path(self._coverage_profraw))
process = await pw_cli.process.run_async(
- *command, env=env, timeout=self._timeout
+ *command,
+ env=self._env,
+ timeout=self._timeout,
+ log_output=self.verbose,
)
except subprocess.CalledProcessError as err:
_LOG.error(err)
@@ -287,7 +290,7 @@ class TestRunner:
# Need to decode the bytes back to ASCII or they will not be
# encodable by json.dumps.
#
- # TODO(b/248349219): Instead of stripping the ANSI color
+ # TODO: b/248349219 - Instead of stripping the ANSI color
# codes, convert them to HTML.
"contents": base64.b64encode(
_strip_ansi(process.output)
@@ -462,14 +465,34 @@ def tests_from_paths(paths: Sequence[str]) -> List[Test]:
return tests
+def parse_env(env: Sequence[str]) -> Dict[str, str]:
+ """Returns a dictionary of environment names and values.
+
+ Args:
+ env: List of strings of the form "key=val".
+
+ Raises:
+ ValueError if `env` is malformed.
+ """
+ envvars = {}
+ if env:
+ for envvar in env:
+ parts = envvar.split('=')
+ if len(parts) != 2:
+ raise ValueError(f'malformed environment variable: {envvar}')
+ envvars[parts[0]] = parts[1]
+ return envvars
+
+
async def find_and_run_tests(
root: str,
runner: str,
runner_args: Sequence[str] = (),
- coverage_profraw: Optional[str] = None,
+ env: Sequence[str] = (),
timeout: Optional[float] = None,
group: Optional[Sequence[str]] = None,
test: Optional[Sequence[str]] = None,
+ verbose: bool = False,
) -> int:
"""Runs some unit tests."""
@@ -478,8 +501,10 @@ async def find_and_run_tests(
else:
tests = tests_from_groups(group, root)
+ envvars = parse_env(env)
+
test_runner = TestRunner(
- runner, runner_args, tests, coverage_profraw, timeout
+ runner, runner_args, tests, envvars, timeout, verbose
)
await test_runner.run_tests()
@@ -499,7 +524,6 @@ def main() -> int:
)
args_as_dict = dict(vars(parser.parse_args()))
- del args_as_dict['verbose']
return asyncio.run(find_and_run_tests(**args_as_dict))
diff --git a/pw_unit_test/py/setup.cfg b/pw_unit_test/py/setup.cfg
index d0f7b5d9d..98384fa27 100644
--- a/pw_unit_test/py/setup.cfg
+++ b/pw_unit_test/py/setup.cfg
@@ -22,6 +22,7 @@ description = Unit tests for Pigweed projects
packages = find:
zip_safe = False
install_requires =
+ pyserial~=3.5.0
requests
types-requests
diff --git a/pw_unit_test/py/setup.py b/pw_unit_test/py/setup.py
deleted file mode 100644
index a3101bfd6..000000000
--- a/pw_unit_test/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_unit_test"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_unit_test/py/test_group_metadata_test.py b/pw_unit_test/py/test_group_metadata_test.py
index d54e86fbc..1ab32ea60 100644
--- a/pw_unit_test/py/test_group_metadata_test.py
+++ b/pw_unit_test/py/test_group_metadata_test.py
@@ -40,13 +40,14 @@ class TestGroupMetadataTest(unittest.TestCase):
meta = json.loads(meta_text)
except json.decoder.JSONDecodeError as jde:
raise ValueError(
- f'Failed to decode file {self.metadata_path} as JSON: {meta_text}'
+ f'Failed to decode file {self.metadata_path} '
+ f'as JSON: {meta_text}'
) from jde
self.assertIsInstance(meta, list)
found = False
for test_entry in meta:
- self.assertIn('type', test_entry)
- if test_entry['type'] != 'test':
+ self.assertIn('test_type', test_entry)
+ if test_entry['test_type'] != 'unit_test':
continue
self.assertIn('test_name', test_entry)
self.assertIn('test_directory', test_entry)
diff --git a/pw_unit_test/test.cmake b/pw_unit_test/test.cmake
index 08e268eb3..37d060dae 100644
--- a/pw_unit_test/test.cmake
+++ b/pw_unit_test/test.cmake
@@ -97,7 +97,7 @@ function(pw_add_test NAME)
HEADERS
${arg_HEADERS}
PRIVATE_DEPS
- # TODO(b/232141950): Apply compilation options that affect ABI
+ # TODO: b/232141950 - Apply compilation options that affect ABI
# globally in the CMake build instead of injecting them into libraries.
pw_build
${arg_PRIVATE_DEPS}
diff --git a/pw_unit_test/test.gni b/pw_unit_test/test.gni
index 86399f7e0..ead5044e0 100644
--- a/pw_unit_test/test.gni
+++ b/pw_unit_test/test.gni
@@ -16,7 +16,9 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python_action.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_build/test_info.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
declare_args() {
@@ -98,6 +100,11 @@ declare_args() {
# Type: string (path to a .gni file)
# Usage: toolchain-controlled only
pw_unit_test_EXECUTABLE_TARGET_TYPE_FILE = ""
+
+ # If true, the pw_unit_test target, pw_test targets, and pw_test_group targets
+ # will define `testonly = true`. This is false by default for backwards
+ # compatibility.
+ pw_unit_test_TESTONLY = false
}
if (pw_unit_test_EXECUTABLE_TARGET_TYPE != "pw_executable" &&
@@ -134,28 +141,38 @@ template("pw_internal_disableable_target") {
if (defined(invoker.public)) {
inputs += invoker.public
}
+
+ if (defined(invoker.source_gen_deps)) {
+ deps = invoker.source_gen_deps
+ }
}
}
target(invoker.target_type, _actual_target_name) {
+ sources = []
+ public_deps = []
+ deps = []
forward_variables_from(invoker,
"*",
[
- "negative_compilation_tests",
"enable_if",
+ "negative_compilation_tests",
+ "source_gen_deps",
"target_type",
"test_automatic_runner_args",
])
# Remove "" from dependencies. This allows disabling targets if a variable
# (e.g. a backend) is empty.
- if (defined(public_deps)) {
- public_deps += [ "" ]
- public_deps -= [ "" ]
- }
- if (defined(deps)) {
- deps += [ "" ]
- deps -= [ "" ]
+ public_deps += [ "" ]
+ public_deps -= [ "" ]
+ deps += [ "" ]
+ deps -= [ "" ]
+ if (defined(invoker.source_gen_deps)) {
+ deps += invoker.source_gen_deps
+ foreach(source_gen_dep, invoker.source_gen_deps) {
+ sources += get_target_outputs(source_gen_dep)
+ }
}
}
}
@@ -171,15 +188,30 @@ template("pw_internal_disableable_target") {
# creates a "${test_name}.run" target which runs the unit test executable after
# building it.
#
+# Targets defined using this template will produce test metadata with a
+# `test_type` of "unit_test" and an additional `test_directory` value describing
+# the location of the test binary within the build output.
+#
# Args:
# - enable_if: (optional) Conditionally enables or disables this test. The
# test target and *.run target do nothing when the test is disabled. The
# disabled test can still be built and run with the
# <target_name>.DISABLED and <target_name>.DISABLED.run targets.
# Defaults to true (enable_if).
+# - envvars: (optional) A list of `var_name=value` strings to set as
+# environment variables when running the target exectuable.
+# - tags: (optional) List of strings to include in the test metadata. These
+# have no effect on the build, but may be used by external tools to
+# distinguish between tests. For example, a tool may want to skip tests
+# tagged as "slow".
# - extra_metadata: (optional) Extra metadata to include in test group
# metadata output. This can be used to pass information about this test
# to later build tasks.
+# - source_gen_deps: (optional) List of targets which generate `sources` for
+# this test. These `deps` will be included even if the test is disabled.
+# `get_target_outputs` is used to add the generated `sources` to the
+# test's `sources` list, so these targets must appear earlier in the
+# same build file.
# - All of the regular "executable" target args are accepted.
#
template("pw_test") {
@@ -190,10 +222,8 @@ template("pw_test") {
_test_target_name = target_name
_test_is_enabled = !defined(invoker.enable_if) || invoker.enable_if
- _is_coverage_run =
- pw_toolchain_COVERAGE_ENABLED &&
- filter_include(pw_toolchain_SANITIZERS, [ "coverage" ]) == [ "coverage" ]
- if (_is_coverage_run) {
+
+ if (pw_toolchain_COVERAGE_ENABLED) {
_profraw_path = "$target_out_dir/test/$_test_target_name.profraw"
}
@@ -204,12 +234,6 @@ template("pw_test") {
_test_output_dir = invoker.output_dir
}
- _extra_metadata = {
- }
- if (defined(invoker.extra_metadata)) {
- _extra_metadata = invoker.extra_metadata
- }
-
_test_main = pw_unit_test_MAIN
if (defined(invoker.test_main)) {
_test_main = invoker.test_main
@@ -219,6 +243,7 @@ template("pw_test") {
pw_internal_disableable_target("$target_name.lib") {
target_type = "pw_source_set"
enable_if = _test_is_enabled
+ testonly = pw_unit_test_TESTONLY
# It is possible that the executable target type has been overriden by
# pw_unit_test_EXECUTABLE_TARGET_TYPE, which may allow for additional
@@ -264,7 +289,7 @@ template("pw_test") {
"output_name",
"public",
"sources",
- "testonly",
+ "source_gen_deps",
"visibility",
# pw_source_set variables
@@ -288,36 +313,50 @@ template("pw_test") {
}
}
+ # Metadata for this test when used as part of a pw_test_group target.
+ _test_metadata = "${target_name}.metadata"
+ _extra_metadata = {
+ forward_variables_from(invoker, [ "extra_metadata" ])
+ test_directory = rebase_path(_test_output_dir, root_build_dir)
+ }
+ pw_test_info(_test_metadata) {
+ test_type = "unit_test"
+ test_name = _test_target_name
+ forward_variables_from(invoker, [ "tags" ])
+ extra_metadata = _extra_metadata
+ }
+
pw_internal_disableable_target(_test_target_name) {
target_type = pw_unit_test_EXECUTABLE_TARGET_TYPE
enable_if = _test_is_enabled
+ testonly = pw_unit_test_TESTONLY
# Include configs, deps, etc. from the pw_test in the executable as well as
# the library to ensure that linker flags propagate to the executable.
+ deps = []
forward_variables_from(invoker,
"*",
[
"extra_metadata",
"metadata",
"sources",
+ "source_gen_deps",
"public",
])
+ deps += [
+ ":$_test_metadata",
+ ":$_test_target_name.lib",
+ ]
+ if (_test_main != "") {
+ deps += [ _test_main ]
+ }
+ output_dir = _test_output_dir
- # Metadata for this test when used as part of a pw_test_group target.
metadata = {
- tests = [
- {
- type = "test"
- test_name = _test_target_name
- test_directory = rebase_path(_test_output_dir, root_build_dir)
- extra_metadata = _extra_metadata
- },
- ]
-
# N.B.: This is placed here instead of in $_test_target_name._run because
# pw_test_group only forwards the metadata from _test_target_name and not
# _test_target_name._run or _test_target_name.run.
- if (_is_coverage_run) {
+ if (pw_toolchain_COVERAGE_ENABLED) {
profraws = [
{
type = "profraw"
@@ -325,23 +364,17 @@ template("pw_test") {
},
]
}
- }
- if (!defined(deps)) {
- deps = []
- }
- deps += [ ":$_test_target_name.lib" ]
- if (_test_main != "") {
- deps += [ _test_main ]
+ # Only collect test metadata for the test itself.
+ test_barrier = [ ":$_test_metadata" ]
}
-
- output_dir = _test_output_dir
}
if (defined(invoker.negative_compilation_tests) &&
invoker.negative_compilation_tests) {
pw_cc_negative_compilation_test("$target_name.nc_test") {
forward_variables_from(invoker, "*")
+ testonly = pw_unit_test_TESTONLY
# Add a dependency on pw_unit_test since it is implied for pw_unit_test
# targets.
@@ -363,6 +396,7 @@ template("pw_test") {
# Create a placeholder .run target for the regular version of the test.
group(_test_target_name + ".run") {
+ testonly = pw_unit_test_TESTONLY
deps = [ ":$_test_target_name" ]
}
}
@@ -396,16 +430,25 @@ template("pw_test") {
"--test",
"<TARGET_FILE(:$_test_to_run)>",
]
+ if (defined(invoker.envvars)) {
+ foreach(envvars, envvars) {
+ args += [
+ "--env",
+ envvar,
+ ]
+ }
+ }
if (pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT != "") {
args += [
"--timeout",
pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT,
]
}
- if (_is_coverage_run) {
+ if (pw_toolchain_COVERAGE_ENABLED) {
+ _llvm_profile_file = rebase_path(_profraw_path, root_build_dir)
args += [
- "--coverage-profraw",
- rebase_path(_profraw_path, root_build_dir),
+ "--env",
+ "LLVM_PROFILE_FILE=" + _llvm_profile_file,
]
}
@@ -414,17 +457,19 @@ template("pw_test") {
}
outputs = []
- if (_is_coverage_run) {
+ if (pw_toolchain_COVERAGE_ENABLED) {
outputs += [ _profraw_path ]
}
stamp = true
}
group(_test_to_run + ".run") {
+ testonly = pw_unit_test_TESTONLY
public_deps = [ ":$_test_to_run._run" ]
}
} else {
group(_test_target_name + ".run") {
+ testonly = pw_unit_test_TESTONLY
public_deps = [ ":$_test_target_name" ]
}
}
@@ -432,16 +477,21 @@ template("pw_test") {
# Defines a related collection of unit tests.
#
-# pw_test_group targets output a JSON metadata file for the Pigweed test runner.
+# Targets defined using this template will produce test metadata with a
+# `test_type` of "test_group" and an additional `deps` list describing the tests
+# collected by this target.
#
# Args:
# - tests: List of pw_test targets for each of the tests in the group.
# - group_deps: (optional) pw_test_group targets on which this group depends.
# - enable_if: (optional) Conditionally enables or disables this test group.
# If false, an empty group is created. Defaults to true.
+# - output_metadata: (optional) If true, generates a JSON file containing the
+# test metadata for this group and all of its dependencies. Defaults to
+# false.
+#
template("pw_test_group") {
_group_target = target_name
- _group_deps_metadata = []
if (defined(invoker.tests)) {
_deps = invoker.tests
} else {
@@ -457,22 +507,11 @@ template("pw_test_group") {
if (_group_is_enabled) {
if (defined(invoker.group_deps)) {
- # If the group specified any other group dependencies, create a metadata
- # entry for each of them indicating that they are another group and a
- # group target to collect that metadata.
- foreach(dep, invoker.group_deps) {
- _group_deps_metadata += [
- {
- type = "dep"
- group = get_label_info(dep, "label_no_toolchain")
- },
- ]
- }
-
_deps += invoker.group_deps
}
group(_group_target + ".lib") {
+ testonly = pw_unit_test_TESTONLY
deps = []
foreach(_target, _deps) {
_dep_target = get_label_info(_target, "label_no_toolchain")
@@ -481,46 +520,53 @@ template("pw_test_group") {
}
}
- _metadata_group_target = "${target_name}_pw_test_group_metadata"
- group(_metadata_group_target) {
- metadata = {
- group_deps = _group_deps_metadata
- self = [
- {
- type = "self"
- name = get_label_info(":$_group_target", "label_no_toolchain")
- },
- ]
-
- # Metadata from the group's own unit test targets is forwarded through
- # the group dependencies group. This entry is listed as a "walk_key" in
- # the generated file so that only test targets' metadata (not group
- # targets) appear in the output.
- if (defined(invoker.tests)) {
- propagate_metadata_from = invoker.tests
+ # Create a manifest entry to indicate which tests are a part of this group.
+ _test_group_metadata = "${target_name}_pw_test_group_metadata"
+ _extra_metadata = {
+ forward_variables_from(invoker, [ "extra_metadata" ])
+ if (_deps != []) {
+ deps = []
+ foreach(dep, _deps) {
+ deps += [ get_label_info(dep, "label_no_toolchain") ]
}
}
+ }
+ pw_test_info(_test_group_metadata) {
+ testonly = pw_unit_test_TESTONLY
+ build_label = _group_target
+ test_type = "test_group"
+ test_name = rebase_path(get_label_info(_group_target, "dir"), "//")
+ extra_metadata = _extra_metadata
deps = _deps
}
- _test_group_deps = [ ":$_metadata_group_target" ]
-
- generated_file(_group_target) {
- outputs = [ "$target_out_dir/$target_name.testinfo.json" ]
- data_keys = [
- "group_deps",
- "self",
- "tests",
- ]
- walk_keys = [ "propagate_metadata_from" ]
- output_conversion = "json"
- deps = _test_group_deps
+ if (defined(invoker.output_metadata) && invoker.output_metadata) {
+ generated_file(_group_target) {
+ testonly = pw_unit_test_TESTONLY
+ outputs = [ "$target_out_dir/$target_name.testinfo.json" ]
+ data_keys = [
+ "test_groups",
+ "unit_tests",
+ "action_tests",
+ "perf_tests",
+ "fuzz_tests",
+ ]
+ walk_keys = [ "test_barrier" ]
+ output_conversion = "json"
+ deps = [ ":$_test_group_metadata" ]
+ }
+ } else {
+ group(_group_target) {
+ testonly = pw_unit_test_TESTONLY
+ deps = [ ":$_test_group_metadata" ]
+ }
}
# If automatic test running is enabled, create a *.run group that collects
# all of the individual *.run targets and groups.
if (pw_unit_test_AUTOMATIC_RUNNER != "") {
group(_group_target + ".run") {
+ testonly = pw_unit_test_TESTONLY
deps = [ ":$_group_target" ]
foreach(_target, _deps) {
_dep_target = get_label_info(_target, "label_no_toolchain")
diff --git a/pw_unit_test/unit_test_service.cc b/pw_unit_test/unit_test_service.cc
index b3b9530ba..dc823b0f6 100644
--- a/pw_unit_test/unit_test_service.cc
+++ b/pw_unit_test/unit_test_service.cc
@@ -34,13 +34,13 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
Status status;
while ((status = decoder.Next()).ok()) {
- switch (static_cast<TestRunRequest::Fields>(decoder.FieldNumber())) {
- case TestRunRequest::Fields::kReportPassedExpectations:
+ switch (static_cast<pwpb::TestRunRequest::Fields>(decoder.FieldNumber())) {
+ case pwpb::TestRunRequest::Fields::kReportPassedExpectations:
decoder.ReadBool(&verbose_)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
break;
- case TestRunRequest::Fields::kTestSuite: {
+ case pwpb::TestRunRequest::Fields::kTestSuite: {
std::string_view suite_name;
if (!decoder.ReadString(&suite_name).ok()) {
break;
@@ -52,7 +52,7 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
PW_LOG_ERROR("Maximum of %u test suite filters supported",
static_cast<unsigned>(suites_to_run.max_size()));
writer_.Finish(Status::InvalidArgument())
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
@@ -63,7 +63,7 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
if (status != Status::OutOfRange()) {
writer_.Finish(status)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
return;
}
@@ -71,59 +71,60 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
handler_.ExecuteTests(suites_to_run);
PW_LOG_INFO("Unit test run complete");
- writer_.Finish().IgnoreError(); // TODO(b/242598609): Handle Status properly
+ writer_.Finish().IgnoreError(); // TODO: b/242598609 - Handle Status properly
}
void UnitTestService::WriteTestRunStart() {
// Write out the key for the start field (even though the message is empty).
- WriteEvent(
- [&](Event::StreamEncoder& event) { event.GetTestRunStartEncoder(); });
+ WriteEvent([&](pwpb::Event::StreamEncoder& event) {
+ event.GetTestRunStartEncoder();
+ });
}
void UnitTestService::WriteTestRunEnd(const RunTestsSummary& summary) {
- WriteEvent([&](Event::StreamEncoder& event) {
- TestRunEnd::StreamEncoder test_run_end = event.GetTestRunEndEncoder();
+ WriteEvent([&](pwpb::Event::StreamEncoder& event) {
+ pwpb::TestRunEnd::StreamEncoder test_run_end = event.GetTestRunEndEncoder();
test_run_end.WritePassed(summary.passed_tests)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
test_run_end.WriteFailed(summary.failed_tests)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
test_run_end.WriteSkipped(summary.skipped_tests)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
test_run_end.WriteDisabled(summary.disabled_tests)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
});
}
void UnitTestService::WriteTestCaseStart(const TestCase& test_case) {
- WriteEvent([&](Event::StreamEncoder& event) {
- TestCaseDescriptor::StreamEncoder descriptor =
+ WriteEvent([&](pwpb::Event::StreamEncoder& event) {
+ pwpb::TestCaseDescriptor::StreamEncoder descriptor =
event.GetTestCaseStartEncoder();
descriptor.WriteSuiteName(test_case.suite_name)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
descriptor.WriteTestName(test_case.test_name)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
descriptor.WriteFileName(test_case.file_name)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
});
}
void UnitTestService::WriteTestCaseEnd(TestResult result) {
- WriteEvent([&](Event::StreamEncoder& event) {
- event.WriteTestCaseEnd(static_cast<TestCaseResult>(result))
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ WriteEvent([&](pwpb::Event::StreamEncoder& event) {
+ event.WriteTestCaseEnd(static_cast<pwpb::TestCaseResult>(result))
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
});
}
void UnitTestService::WriteTestCaseDisabled(const TestCase& test_case) {
- WriteEvent([&](Event::StreamEncoder& event) {
- TestCaseDescriptor::StreamEncoder descriptor =
+ WriteEvent([&](pwpb::Event::StreamEncoder& event) {
+ pwpb::TestCaseDescriptor::StreamEncoder descriptor =
event.GetTestCaseDisabledEncoder();
descriptor.WriteSuiteName(test_case.suite_name)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
descriptor.WriteTestName(test_case.test_name)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
descriptor.WriteFileName(test_case.file_name)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
});
}
@@ -133,18 +134,18 @@ void UnitTestService::WriteTestCaseExpectation(
return;
}
- WriteEvent([&](Event::StreamEncoder& event) {
- TestCaseExpectation::StreamEncoder test_case_expectation =
+ WriteEvent([&](pwpb::Event::StreamEncoder& event) {
+ pwpb::TestCaseExpectation::StreamEncoder test_case_expectation =
event.GetTestCaseExpectationEncoder();
test_case_expectation.WriteExpression(expectation.expression)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
test_case_expectation
.WriteEvaluatedExpression(expectation.evaluated_expression)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
test_case_expectation.WriteLineNumber(expectation.line_number)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
test_case_expectation.WriteSuccess(expectation.success)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
});
}
diff --git a/pw_unit_test_zephyr/BUILD.gn b/pw_unit_test_zephyr/BUILD.gn
new file mode 100644
index 000000000..b81bcd0fc
--- /dev/null
+++ b/pw_unit_test_zephyr/BUILD.gn
@@ -0,0 +1,26 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = []
+}
diff --git a/pw_unit_test_zephyr/CMakeLists.txt b/pw_unit_test_zephyr/CMakeLists.txt
new file mode 100644
index 000000000..06909213d
--- /dev/null
+++ b/pw_unit_test_zephyr/CMakeLists.txt
@@ -0,0 +1,28 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+cmake_minimum_required(VERSION 3.20)
+project(PigweedZephyrTest)
+
+set(pw_third_party_nanopb_ADD_SUBDIRECTORY ON CACHE BOOL "" FORCE)
+
+find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
+enable_language(C CXX)
+
+target_sources(app PRIVATE main.cc)
+target_link_libraries(app
+ PRIVATE
+ pw_unit_test.light
+ pw_unit_test.logging_event_handler
+)
diff --git a/pw_unit_test_zephyr/docs.rst b/pw_unit_test_zephyr/docs.rst
new file mode 100644
index 000000000..38215702b
--- /dev/null
+++ b/pw_unit_test_zephyr/docs.rst
@@ -0,0 +1,10 @@
+.. _module-pw_unit_test_zephyr:
+
+===================
+pw_unit_test_zephyr
+===================
+``pw_unit_test_zephyr`` provides a shim to run Pigweed's
+:ref:`module-pw_unit_test` on Zephyr. To add new tests, the ``testcase.yaml``
+file must be modified to include the new test's name and Kconfigs. See
+:ref:`docs-os-zephyr` for more information on getting started with Pigweed
+and Zephyr.
diff --git a/pw_unit_test_zephyr/main.cc b/pw_unit_test_zephyr/main.cc
new file mode 100644
index 000000000..d86445587
--- /dev/null
+++ b/pw_unit_test_zephyr/main.cc
@@ -0,0 +1,24 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <gtest/gtest.h>
+#include <pw_unit_test/logging_event_handler.h>
+#include <zephyr/kernel.h>
+
+int main() {
+ testing::InitGoogleTest(nullptr, nullptr);
+ pw::unit_test::LoggingEventHandler handler;
+ pw::unit_test::RegisterEventHandler(&handler);
+ return RUN_ALL_TESTS();
+}
diff --git a/pw_unit_test_zephyr/prj.conf b/pw_unit_test_zephyr/prj.conf
new file mode 100644
index 000000000..fb2009102
--- /dev/null
+++ b/pw_unit_test_zephyr/prj.conf
@@ -0,0 +1,32 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# Do not rely on the PW_ROOT environment variable being set through bootstrap.
+# Regardless of whether it's set or not the following include will ensure it is.
+
+# Test configs:
+CONFIG_TEST=y
+
+# C++ configs:
+CONFIG_CPP=y
+CONFIG_CPP_MAIN=y
+CONFIG_STD_CPP20=y
+
+# Enable asserts
+CONFIG_PIGWEED_ASSERT=y
+
+# Enable logging (needed to capture gTest output)
+CONFIG_LOG=y
+CONFIG_PIGWEED_LOG_ZEPHYR=y
+CONFIG_PIGWEED_SYS_IO=y
diff --git a/pw_build/py/setup.py b/pw_unit_test_zephyr/testcase.yaml
index a8fc27435..f786f0de2 100644
--- a/pw_build/py/setup.py
+++ b/pw_unit_test_zephyr/testcase.yaml
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,8 +11,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""pw_build"""
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
+common:
+ harness: gtest
+ timeout: 10
+ integration_platforms:
+ - native_posix
+tests:
+ pw.base64:
+ extra_configs:
+ - CONFIG_PIGWEED_BASE64=y
diff --git a/pw_varint/Android.bp b/pw_varint/Android.bp
index 03cbd4623..3d65acd69 100644
--- a/pw_varint/Android.bp
+++ b/pw_varint/Android.bp
@@ -21,11 +21,32 @@ cc_library_static {
cpp_std: "c++20",
vendor_available: true,
export_include_dirs: ["public"],
+ defaults: [
+ "pw_assert_log_defaults",
+ ],
header_libs: [
- "pw_preprocessor_headers",
+ "fuchsia_sdk_lib_stdcompat",
+ "pw_assert_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
"pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
"pw_span_headers",
],
- srcs: ["varint.cc"],
+ export_header_lib_headers: [
+ "fuchsia_sdk_lib_stdcompat",
+ "pw_polyfill_headers",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_status",
+ "pw_stream",
+ ],
+ srcs: [
+ "stream.cc",
+ "varint_c.c",
+ "varint.cc",
+ ],
host_supported: true,
}
diff --git a/pw_varint/BUILD.bazel b/pw_varint/BUILD.bazel
index 5b516294e..4688787d6 100644
--- a/pw_varint/BUILD.bazel
+++ b/pw_varint/BUILD.bazel
@@ -26,6 +26,7 @@ pw_cc_library(
name = "pw_varint",
srcs = [
"varint.cc",
+ "varint_c.c",
],
hdrs = [
"public/pw_varint/varint.h",
@@ -35,6 +36,7 @@ pw_cc_library(
"//pw_polyfill",
"//pw_preprocessor",
"//pw_span",
+ "//third_party/fuchsia:stdcompat",
],
)
@@ -63,7 +65,7 @@ pw_cc_test(
],
deps = [
":pw_varint",
- "//pw_unit_test",
+ "//pw_fuzzer:fuzztest",
],
)
diff --git a/pw_varint/BUILD.gn b/pw_varint/BUILD.gn
index ae545d651..372abda93 100644
--- a/pw_varint/BUILD.gn
+++ b/pw_varint/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_fuzzer/fuzz_test.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -25,12 +26,19 @@ config("default_config") {
pw_source_set("pw_varint") {
public_configs = [ ":default_config" ]
public_deps = [
+ "$dir_pw_third_party/fuchsia:stdcompat",
dir_pw_polyfill,
dir_pw_preprocessor,
dir_pw_span,
]
- sources = [ "varint.cc" ]
+ sources = [
+ "varint.cc",
+ "varint_c.c",
+ ]
public = [ "public/pw_varint/varint.h" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_source_set("stream") {
@@ -54,12 +62,15 @@ pw_test_group("tests") {
]
}
-pw_test("varint_test") {
+pw_fuzz_test("varint_test") {
deps = [ ":pw_varint" ]
sources = [
"varint_test.cc",
"varint_test_c.c",
]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("stream_test") {
@@ -69,6 +80,9 @@ pw_test("stream_test") {
dir_pw_stream,
]
sources = [ "stream_test.cc" ]
+
+ # TODO: b/259746255 - Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_doc_group("docs") {
diff --git a/pw_varint/CMakeLists.txt b/pw_varint/CMakeLists.txt
index 74b495d12..55cf7422b 100644
--- a/pw_varint/CMakeLists.txt
+++ b/pw_varint/CMakeLists.txt
@@ -13,6 +13,7 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_unit_test/test.cmake)
pw_add_library(pw_varint STATIC
HEADERS
@@ -23,12 +24,11 @@ pw_add_library(pw_varint STATIC
pw_polyfill
pw_preprocessor
pw_span
+ pw_third_party.fuchsia.stdcompat
SOURCES
varint.cc
+ varint_c.c
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_VARINT)
- zephyr_link_libraries(pw_varint)
-endif()
pw_add_library(pw_varint.stream STATIC
HEADERS
@@ -50,6 +50,7 @@ pw_add_test(pw_varint.varint_test
varint_test_c.c
PRIVATE_DEPS
pw_varint
+ pw_fuzzer.fuzztest
GROUPS
modules
pw_varint
diff --git a/pw_varint/Kconfig b/pw_varint/Kconfig
index b43114dd1..9ba55faaa 100644
--- a/pw_varint/Kconfig
+++ b/pw_varint/Kconfig
@@ -12,7 +12,13 @@
# License for the specific language governing permissions and limitations under
# the License.
+menu "pw_varint"
+
config PIGWEED_VARINT
- bool "Enable Pigweed variable length int library (pw_varint)"
+ bool "Link pw_varint library"
select PIGWEED_PREPROCESSOR
select PIGWEED_SPAN
+ help
+ See :ref:`module-pw_varint` for module details.
+
+endmenu
diff --git a/pw_varint/docs.rst b/pw_varint/docs.rst
index 1d87fe61b..5d8fc5da5 100644
--- a/pw_varint/docs.rst
+++ b/pw_varint/docs.rst
@@ -1,59 +1,70 @@
.. _module-pw_varint:
----------
+=========
pw_varint
----------
-The ``pw_varint`` module provides functions for encoding and decoding variable
-length integers, or varints. For smaller values, varints require less memory
-than a fixed-size encoding. For example, a 32-bit (4-byte) integer requires 1--5
-bytes when varint-encoded.
-
-`Protocol Buffers <https://developers.google.com/protocol-buffers/docs/encoding#varints>`_
-use a variable-length encoding for integers.
+=========
+.. doxygenfile:: pw_varint/varint.h
+ :sections: detaileddescription
+-------------
Compatibility
-=============
+-------------
* C
-* C++14 (with :doc:`../pw_polyfill/docs`)
-
-API
+* C++
+* `Rust </rustdoc/pw_varint>`_
+
+-------------
+API Reference
+-------------
+
+.. _module-pw_varint-api-c:
+
+C
+=
+.. doxygendefine:: PW_VARINT_MAX_INT32_SIZE_BYTES
+.. doxygendefine:: PW_VARINT_MAX_INT64_SIZE_BYTES
+.. doxygenfunction:: pw_varint_Encode32
+.. doxygenfunction:: pw_varint_Encode64
+.. doxygenfunction:: pw_varint_Decode32
+.. doxygenfunction:: pw_varint_Decode64
+.. doxygenfunction:: pw_varint_ZigZagEncode32
+.. doxygenfunction:: pw_varint_ZigZagEncode64
+.. doxygenfunction:: pw_varint_ZigZagDecode32
+.. doxygenfunction:: pw_varint_ZigZagDecode64
+.. doxygendefine:: PW_VARINT_ENCODED_SIZE_BYTES
+.. doxygenfunction:: pw_varint_EncodedSizeBytes
+.. doxygenenum:: pw_varint_Format
+.. doxygenfunction:: pw_varint_EncodeCustom
+.. doxygenfunction:: pw_varint_DecodeCustom
+
+C++
===
-
-.. cpp:function:: size_t EncodedSize(uint64_t integer)
-
-Returns the size of an integer when encoded as a varint. Works on both signed
-and unsigned integers.
-
-.. cpp:function:: size_t ZigZagEncodedSize(int64_t integer)
-
-Returns the size of a signed integer when ZigZag encoded as a varint.
-
-.. cpp:function:: uint64_t MaxValueInBytes(size_t bytes)
-
-Returns the maximum integer value that can be encoded as a varint into the
-specified number of bytes.
-
+.. doxygenvariable:: pw::varint::kMaxVarint32SizeBytes
+.. doxygenvariable:: pw::varint::kMaxVarint64SizeBytes
+.. doxygenfunction:: pw::varint::ZigZagEncode
+.. doxygenfunction:: pw::varint::ZigZagDecode
+.. doxygenfunction:: pw::varint::EncodedSize
+.. doxygenfunction:: pw::varint::EncodeLittleEndianBase128
+.. doxygenfunction:: pw::varint::Encode(T integer, const span<std::byte> &output)
+.. doxygenfunction:: pw::varint::Decode(const span<const std::byte>& input, int64_t* output)
+.. doxygenfunction:: pw::varint::Decode(const span<const std::byte>& input, uint64_t* output)
+.. doxygenfunction:: pw::varint::MaxValueInBytes(size_t bytes)
+.. doxygenenum:: pw::varint::Format
+.. doxygenfunction:: pw::varint::Encode(uint64_t value, span<std::byte> output, Format format)
+.. doxygenfunction:: pw::varint::Decode(span<const std::byte> input, uint64_t* value, Format format)
Stream API
----------
+.. doxygenfunction:: pw::varint::Read(stream::Reader& reader, uint64_t* output, size_t max_size)
+.. doxygenfunction:: pw::varint::Read(stream::Reader& reader, int64_t* output, size_t max_size)
-.. cpp:function:: StatusWithSize Read(stream::Reader& reader, int64_t* output)
-.. cpp:function:: StatusWithSize Read(stream::Reader& reader, uint64_t* output)
-
-Decoders a varint from the current position of a stream. If reading into a
-signed integer, the value is ZigZag decoded.
-
-Returns the number of bytes read from the stream, places the value in `output`,
-if successful. Returns `OutOfRange` if the varint does not fit in to the type,
-or if the input is exhausted before the number terminates.
-
-Reads a maximum of 10 bytes.
-
-Dependencies
-============
-* ``pw_span``
+Rust
+====
+``pw_varint``'s Rust API is documented in our
+`rustdoc API docs </rustdoc/pw_varint>`_.
+------
Zephyr
-======
+------
To enable ``pw_varint`` for Zephyr add ``CONFIG_PIGWEED_VARINT=y`` to the
project's configuration.
diff --git a/pw_varint/public/pw_varint/stream.h b/pw_varint/public/pw_varint/stream.h
index c055bc52b..effac3df5 100644
--- a/pw_varint/public/pw_varint/stream.h
+++ b/pw_varint/public/pw_varint/stream.h
@@ -22,13 +22,23 @@
namespace pw {
namespace varint {
-// Reads a varint-encoded value from a pw::stream. If reading into a signed
-// integer, the value is ZigZag decoded.
-//
-// Returns the number of bytes read from the stream if successful, OutOfRange
-// if the varint does not fit in a int64_t / uint64_t or if the input is
-// exhausted before the number terminates. Reads a maximum of 10 bytes or
-// max_size, whichever is smaller.
+/// @brief Decodes a variable-length integer (varint) from the current position
+/// of a `pw::stream`. Reads a maximum of 10 bytes or `max_size`, whichever is
+/// smaller.
+///
+/// @param reader The `pw::stream` to read from.
+///
+/// @param output The integer to read into. If reading into a signed integer,
+/// the value is
+/// [ZigZag](https://protobuf.dev/programming-guides/encoding/#signed-ints)-decoded.
+///
+/// @param max_size The maximum number of bytes to read. The upper limit is 10
+/// bytes.
+///
+/// @returns The number of bytes read from the stream if successful. The value
+/// is placed in `output`. Returns `OutOfRange` if the varint does not fit into
+/// `output`. Also returns `OutOfRange` if the input is exhausted before the
+/// number terminates.
StatusWithSize Read(stream::Reader& reader,
int64_t* output,
size_t max_size = std::numeric_limits<size_t>::max());
diff --git a/pw_varint/public/pw_varint/varint.h b/pw_varint/public/pw_varint/varint.h
index d53705be0..9eac76749 100644
--- a/pw_varint/public/pw_varint/varint.h
+++ b/pw_varint/public/pw_varint/varint.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -13,6 +13,25 @@
// the License.
#pragma once
+/// @file pw_varint/varint.h
+///
+/// The `pw_varint` module provides functions for encoding and decoding variable
+/// length integers or varints. For smaller values, varints require less memory
+/// than a fixed-size encoding. For example, a 32-bit (4-byte) integer requires
+/// 1–5 bytes when varint-encoded.
+///
+/// `pw_varint` supports custom variable-length encodings with different
+/// terminator bit values and positions (@cpp_enum{pw::varint::Format}).
+/// The basic encoding for unsigned integers is Little Endian Base 128 (LEB128).
+/// ZigZag encoding is also supported, which maps negative integers to positive
+/// integers to improve encoding density for LEB128.
+///
+/// <a
+/// href=https://developers.google.com/protocol-buffers/docs/encoding#varints>
+/// Protocol Buffers</a> and @rstref{HDLC <module-pw_hdlc>} use variable-length
+/// integer encodings for integers.
+
+#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
@@ -22,8 +41,112 @@
extern "C" {
#endif
-// Expose a subset of the varint API for use in C code.
+/// Maximum size of an LEB128-encoded `uint32_t`.
+#define PW_VARINT_MAX_INT32_SIZE_BYTES 5
+
+/// Maximum size of an LEB128-encoded `uint64_t`.
+#define PW_VARINT_MAX_INT64_SIZE_BYTES 10
+
+/// Encodes a 32-bit integer as LEB128.
+///
+/// @returns the number of bytes written
+size_t pw_varint_Encode32(uint32_t integer,
+ void* output,
+ size_t output_size_bytes);
+
+/// Encodes a 64-bit integer as LEB128.
+///
+/// @returns the number of bytes written
+size_t pw_varint_Encode64(uint64_t integer,
+ void* output,
+ size_t output_size_bytes);
+
+/// Zig-zag encodes an `int32_t`, returning it as a `uint32_t`.
+static inline uint32_t pw_varint_ZigZagEncode32(int32_t n) {
+ return (uint32_t)((uint32_t)n << 1) ^ (uint32_t)(n >> (sizeof(n) * 8 - 1));
+}
+
+/// Zig-zag encodes an `int64_t`, returning it as a `uint64_t`.
+static inline uint64_t pw_varint_ZigZagEncode64(int64_t n) {
+ return (uint64_t)((uint64_t)n << 1) ^ (uint64_t)(n >> (sizeof(n) * 8 - 1));
+}
+
+/// Extracts and encodes 7 bits from the integer. Sets the top bit to indicate
+/// more data is coming, which must be cleared if this was the last byte.
+static inline uint8_t pw_varint_EncodeOneByte32(uint32_t* integer) {
+ const uint8_t bits = (uint8_t)((*integer & 0x7Fu) | 0x80u);
+ *integer >>= 7;
+ return bits;
+}
+
+/// @copydoc pw_varint_EncodeOneByte32
+static inline uint8_t pw_varint_EncodeOneByte64(uint64_t* integer) {
+ const uint8_t bits = (uint8_t)((*integer & 0x7Fu) | 0x80u);
+ *integer >>= 7;
+ return bits;
+}
+
+/// Zig-zag decodes a `uint64_t`, returning it as an `int64_t`.
+static inline int32_t pw_varint_ZigZagDecode32(uint32_t n)
+ PW_NO_SANITIZE("unsigned-integer-overflow") {
+ return (int32_t)((n >> 1) ^ (~(n & 1) + 1));
+}
+/// Zig-zag decodes a `uint64_t`, returning it as an `int64_t`.
+static inline int64_t pw_varint_ZigZagDecode64(uint64_t n)
+ PW_NO_SANITIZE("unsigned-integer-overflow") {
+ return (int64_t)((n >> 1) ^ (~(n & 1) + 1));
+}
+
+/// Decodes an LEB128-encoded integer to a `uint32_t`.
+/// @returns the number of bytes read; 0 if decoding failed
+size_t pw_varint_Decode32(const void* input,
+ size_t input_size_bytes,
+ uint32_t* output);
+
+/// Decodes an LEB128-encoded integer to a `uint64_t`.
+/// @returns the number of bytes read; 0 if decoding failed
+size_t pw_varint_Decode64(const void* input,
+ size_t input_size_bytes,
+ uint64_t* output);
+
+/// Decodes one byte of an LEB128-encoded integer to a `uint32_t`.
+/// @returns true if there is more data to decode (top bit is set).
+static inline bool pw_varint_DecodeOneByte32(uint8_t byte,
+ size_t count,
+ uint32_t* value) {
+ *value |= (uint32_t)(byte & 0x7fu) << (count * 7);
+ return (byte & 0x80u) != 0u;
+}
+
+/// Decodes one byte of an LEB128-encoded integer to a `uint64_t`.
+/// @returns true if there is more data to decode (top bit is set).
+static inline bool pw_varint_DecodeOneByte64(uint8_t byte,
+ size_t count,
+ uint64_t* value) {
+ *value |= (uint64_t)(byte & 0x7fu) << (count * 7);
+ return (byte & 0x80u) != 0u;
+}
+
+/// Macro that returns the encoded size of up to a 64-bit integer. This is
+/// inefficient, but is a constant expression if the input is a constant. Use
+/// `pw_varint_EncodedSizeBytes` for runtime encoded size calculation.
+#define PW_VARINT_ENCODED_SIZE_BYTES(value) \
+ ((unsigned long long)value < (1u << 7) ? 1u \
+ : (unsigned long long)value < (1u << 14) ? 2u \
+ : (unsigned long long)value < (1u << 21) ? 3u \
+ : (unsigned long long)value < (1u << 28) ? 4u \
+ : (unsigned long long)value < (1llu << 35) ? 5u \
+ : (unsigned long long)value < (1llu << 42) ? 6u \
+ : (unsigned long long)value < (1llu << 49) ? 7u \
+ : (unsigned long long)value < (1llu << 56) ? 8u \
+ : (unsigned long long)value < (1llu << 63) ? 9u \
+ : 10u)
+
+/// Returns the size of a `uint64_t` when encoded as a varint (LEB128).
+size_t pw_varint_EncodedSizeBytes(uint64_t integer);
+
+/// Describes a custom varint format.
typedef enum {
PW_VARINT_ZERO_TERMINATED_LEAST_SIGNIFICANT = 0,
PW_VARINT_ZERO_TERMINATED_MOST_SIGNIFICANT = 1,
@@ -31,41 +154,18 @@ typedef enum {
PW_VARINT_ONE_TERMINATED_MOST_SIGNIFICANT = 3,
} pw_varint_Format;
+/// Encodes a `uint64_t` using a custom varint format.
size_t pw_varint_EncodeCustom(uint64_t integer,
void* output,
size_t output_size,
pw_varint_Format format);
+
+/// Decodes a `uint64_t` using a custom varint format.
size_t pw_varint_DecodeCustom(const void* input,
size_t input_size,
uint64_t* output,
pw_varint_Format format);
-static inline size_t pw_varint_Encode(uint64_t integer,
- void* output,
- size_t output_size) {
- return pw_varint_EncodeCustom(
- integer, output, output_size, PW_VARINT_ZERO_TERMINATED_MOST_SIGNIFICANT);
-}
-
-size_t pw_varint_ZigZagEncode(int64_t integer,
- void* output,
- size_t output_size);
-
-static inline size_t pw_varint_Decode(const void* input,
- size_t input_size,
- uint64_t* output) {
- return pw_varint_DecodeCustom(
- input, input_size, output, PW_VARINT_ZERO_TERMINATED_MOST_SIGNIFICANT);
-}
-
-size_t pw_varint_ZigZagDecode(const void* input,
- size_t input_size,
- int64_t* output);
-
-// Returns the size of an when encoded as a varint.
-size_t pw_varint_EncodedSize(uint64_t integer);
-size_t pw_varint_ZigZagEncodedSize(int64_t integer);
-
#ifdef __cplusplus
} // extern "C"
@@ -73,36 +173,46 @@ size_t pw_varint_ZigZagEncodedSize(int64_t integer);
#include <limits>
#include <type_traits>
+#include "lib/stdcompat/bit.h"
#include "pw_polyfill/language_feature_macros.h"
#include "pw_span/span.h"
namespace pw {
namespace varint {
-// The maximum number of bytes occupied by an encoded varint.
-PW_INLINE_VARIABLE constexpr size_t kMaxVarint32SizeBytes = 5;
-PW_INLINE_VARIABLE constexpr size_t kMaxVarint64SizeBytes = 10;
+/// Maximum size of a varint (LEB128) encoded `uint32_t`.
+PW_INLINE_VARIABLE constexpr size_t kMaxVarint32SizeBytes =
+ PW_VARINT_MAX_INT32_SIZE_BYTES;
-// ZigZag encodes a signed integer. This maps small negative numbers to small,
-// unsigned positive numbers, which improves their density for LEB128 encoding.
-//
-// ZigZag encoding works by moving the sign bit from the most-significant bit to
-// the least-significant bit. For the signed k-bit integer n, the formula is
-//
-// (n << 1) ^ (n >> (k - 1))
-//
-// See the following for a description of ZigZag encoding:
-// https://developers.google.com/protocol-buffers/docs/encoding#types
+/// Maximum size of a varint (LEB128) encoded `uint64_t`.
+PW_INLINE_VARIABLE constexpr size_t kMaxVarint64SizeBytes =
+ PW_VARINT_MAX_INT64_SIZE_BYTES;
+
+/// ZigZag encodes a signed integer. This maps small negative numbers to small,
+/// unsigned positive numbers, which improves their density for LEB128 encoding.
+///
+/// ZigZag encoding works by moving the sign bit from the most-significant bit
+/// to the least-significant bit. For the signed `k`-bit integer `n`, the
+/// formula is:
+///
+/// @code{.cpp}
+/// (n << 1) ^ (n >> (k - 1))
+/// @endcode
+///
+/// See the following for a description of ZigZag encoding:
+/// https://developers.google.com/protocol-buffers/docs/encoding#types
template <typename T>
constexpr std::make_unsigned_t<T> ZigZagEncode(T n) {
static_assert(std::is_signed<T>(), "Zig-zag encoding is for signed integers");
using U = std::make_unsigned_t<T>;
- return (static_cast<U>(n) << 1) ^ static_cast<U>(n >> (sizeof(T) * 8 - 1));
+ return static_cast<U>(static_cast<U>(n) << 1) ^
+ static_cast<U>(n >> (sizeof(T) * 8 - 1));
}
-// ZigZag decodes a signed integer.
-// The calculation is done modulo std::numeric_limits<T>::max()+1, so the
-// unsigned integer overflows are intentional.
+/// ZigZag decodes a signed integer.
+///
+/// The calculation is done modulo `std::numeric_limits<T>::max()+1`, so the
+/// unsigned integer overflows are intentional.
template <typename T>
constexpr std::make_signed_t<T> ZigZagDecode(T n)
PW_NO_SANITIZE("unsigned-integer-overflow") {
@@ -111,59 +221,90 @@ constexpr std::make_signed_t<T> ZigZagDecode(T n)
return static_cast<std::make_signed_t<T>>((n >> 1) ^ (~(n & 1) + 1));
}
-// Encodes a uint64_t with Little-Endian Base 128 (LEB128) encoding.
+/// @brief Computes the size of an integer when encoded as a varint.
+///
+/// @param integer The integer whose encoded size is to be computed. `integer`
+/// can be signed or unsigned.
+///
+/// @returns The size of `integer` when encoded as a varint.
+template <typename T,
+ typename = std::enable_if_t<std::is_integral<T>::value ||
+ std::is_convertible<T, uint64_t>::value>>
+constexpr size_t EncodedSize(T integer) {
+ if (integer == 0) {
+ return 1;
+ }
+ return static_cast<size_t>(
+ (64 - cpp20::countl_zero(static_cast<uint64_t>(integer)) + 6) / 7);
+}
+
+/// Encodes a `uint64_t` with Little-Endian Base 128 (LEB128) encoding.
+/// @returns the number of bytes written; 0 if the buffer is too small
inline size_t EncodeLittleEndianBase128(uint64_t integer,
const span<std::byte>& output) {
- return pw_varint_Encode(integer, output.data(), output.size());
+ return pw_varint_Encode64(integer, output.data(), output.size());
}
-// Encodes the provided integer using a variable-length encoding and returns the
-// number of bytes written.
-//
-// The encoding is the same as used in protocol buffers. Signed integers are
-// ZigZag encoded to remove leading 1s from small negative numbers, then the
-// resulting number is encoded as Little Endian Base 128 (LEB128). Unsigned
-// integers are encoded directly as LEB128.
-//
-// Returns the number of bytes written or 0 if the result didn't fit in the
-// encoding buffer.
+/// Encodes the provided integer using a variable-length encoding and returns
+/// the number of bytes written.
+///
+/// The encoding is the same as used in protocol buffers. Signed integers are
+/// ZigZag encoded to remove leading 1s from small negative numbers, then the
+/// resulting number is encoded as Little Endian Base 128 (LEB128). Unsigned
+/// integers are encoded directly as LEB128.
+///
+/// Returns the number of bytes written or 0 if the result didn't fit in the
+/// encoding buffer.
template <typename T>
size_t Encode(T integer, const span<std::byte>& output) {
if (std::is_signed<T>()) {
- return pw_varint_ZigZagEncode(integer, output.data(), output.size());
+ using Signed =
+ std::conditional_t<std::is_signed<T>::value, T, std::make_signed_t<T>>;
+ return EncodeLittleEndianBase128(ZigZagEncode(static_cast<Signed>(integer)),
+ output);
} else {
- return pw_varint_Encode(integer, output.data(), output.size());
+ using Unsigned = std::
+ conditional_t<std::is_signed<T>::value, std::make_unsigned_t<T>, T>;
+ return EncodeLittleEndianBase128(static_cast<Unsigned>(integer), output);
}
}
-// Decodes a varint-encoded value. If reading into a signed integer, the value
-// is ZigZag decoded.
-//
-// Returns the number of bytes read from the input if successful. Returns zero
-// if the result does not fit in a int64_t / uint64_t or if the input is
-// exhausted before the number terminates. Reads a maximum of 10 bytes.
-//
-// The following example decodes multiple varints from a buffer:
-//
-// while (!data.empty()) {
-// int64_t value;
-// size_t bytes = Decode(data, &value);
-//
-// if (bytes == 0u) {
-// return Status::DataLoss();
-// }
-// results.push_back(value);
-// data = data.subspan(bytes)
-// }
-//
-inline size_t Decode(const span<const std::byte>& input, int64_t* value) {
- return pw_varint_ZigZagDecode(input.data(), input.size(), value);
+/// Decodes a varint-encoded value. If reading into a signed integer, the value
+/// is ZigZag decoded.
+///
+/// Returns the number of bytes read from the input if successful. Returns zero
+/// if the result does not fit in a `int64_t`/ `uint64_t` or if the input is
+/// exhausted before the number terminates. Reads a maximum of 10 bytes.
+///
+/// The following example decodes multiple varints from a buffer:
+///
+/// @code{.cpp}
+///
+/// while (!data.empty()) {
+/// int64_t value;
+/// size_t bytes = Decode(data, &value);
+///
+/// if (bytes == 0u) {
+/// return Status::DataLoss();
+/// }
+/// results.push_back(value);
+/// data = data.subspan(bytes)
+/// }
+///
+/// @endcode
+inline size_t Decode(const span<const std::byte>& input, int64_t* output) {
+ uint64_t value = 0;
+ size_t bytes_read = pw_varint_Decode64(input.data(), input.size(), &value);
+ *output = pw_varint_ZigZagDecode64(value);
+ return bytes_read;
}
+/// @overload
inline size_t Decode(const span<const std::byte>& input, uint64_t* value) {
- return pw_varint_Decode(input.data(), input.size(), value);
+ return pw_varint_Decode64(input.data(), input.size(), value);
}
+/// Describes a custom varint format.
enum class Format {
kZeroTerminatedLeastSignificant = PW_VARINT_ZERO_TERMINATED_LEAST_SIGNIFICANT,
kZeroTerminatedMostSignificant = PW_VARINT_ZERO_TERMINATED_MOST_SIGNIFICANT,
@@ -171,7 +312,7 @@ enum class Format {
kOneTerminatedMostSignificant = PW_VARINT_ONE_TERMINATED_MOST_SIGNIFICANT,
};
-// Encodes a varint in a custom format.
+/// Encodes a varint in a custom format.
inline size_t Encode(uint64_t value, span<std::byte> output, Format format) {
return pw_varint_EncodeCustom(value,
output.data(),
@@ -179,7 +320,7 @@ inline size_t Encode(uint64_t value, span<std::byte> output, Format format) {
static_cast<pw_varint_Format>(format));
}
-// Decodes a varint from a custom format.
+/// Decodes a varint from a custom format.
inline size_t Decode(span<const std::byte> input,
uint64_t* value,
Format format) {
@@ -187,34 +328,28 @@ inline size_t Decode(span<const std::byte> input,
input.data(), input.size(), value, static_cast<pw_varint_Format>(format));
}
-// Returns a size of an integer when encoded as a varint.
-constexpr size_t EncodedSize(uint64_t integer) {
- return integer == 0 ? 1 : (64 - __builtin_clzll(integer) + 6) / 7;
-}
-
-// Returns a size of an signed integer when ZigZag encoded as a varint.
-constexpr size_t ZigZagEncodedSize(int64_t integer) {
- return EncodedSize(ZigZagEncode(integer));
-}
-
-// Returns the maximum integer value that can be encoded in a varint of the
-// specified number of bytes.
-//
-// These values are also listed in the table below. Zigzag encoding cuts these
-// in half, as positive and negative integers are alternated.
-//
-// Bytes Max value
-// 1 127
-// 2 16,383
-// 3 2,097,151
-// 4 268,435,455
-// 5 34,359,738,367 -- needed for max uint32 value
-// 6 4,398,046,511,103
-// 7 562,949,953,421,311
-// 8 72,057,594,037,927,935
-// 9 9,223,372,036,854,775,807
-// 10 uint64 max value
-//
+/// @brief Returns the maximum (max) integer value that can be encoded as a
+/// varint into the specified number of bytes.
+///
+/// The following table lists the max value for each byte size:
+///
+/// | Bytes | Max value |
+/// | ----- | ------------------------- |
+/// | 1 | 127 |
+/// | 2 | 16,383 |
+/// | 3 | 2,097,151 |
+/// | 4 | 268,435,455 |
+/// | 5 | 34,359,738,367 |
+/// | 6 | 4,398,046,511,103 |
+/// | 7 | 562,949,953,421,311 |
+/// | 8 | 72,057,594,037,927,935 |
+/// | 9 | 9,223,372,036,854,775,807 |
+/// | 10 | (uint64 max value) |
+///
+/// @param bytes The size of the varint, in bytes. 5 bytes are needed for the
+/// max `uint32` value. 10 bytes are needed for the max `uint64` value.
+///
+/// @return The max integer value for a varint of size `bytes`.
constexpr uint64_t MaxValueInBytes(size_t bytes) {
return bytes >= kMaxVarint64SizeBytes ? std::numeric_limits<uint64_t>::max()
: (uint64_t(1) << (7 * bytes)) - 1;
diff --git a/pw_varint/rust/BUILD.bazel b/pw_varint/rust/BUILD.bazel
new file mode 100644
index 000000000..2bccb4751
--- /dev/null
+++ b/pw_varint/rust/BUILD.bazel
@@ -0,0 +1,45 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_doc_test", "rust_library", "rust_test")
+
+rust_library(
+ name = "pw_varint",
+ srcs = ["pw_varint.rs"],
+ crate_features = select({
+ "@rust_crates//:std": ["std"],
+ "//conditions:default": [""],
+ }),
+ visibility = ["//visibility:public"],
+ deps = ["//pw_status/rust:pw_status"],
+)
+
+rust_test(
+ name = "pw_varint_test",
+ crate = ":pw_varint",
+ crate_features = select({
+ "@rust_crates//:std": ["std"],
+ "//conditions:default": [""],
+ }),
+)
+
+rust_doc_test(
+ name = "pw_varint_doc_test",
+ crate = ":pw_varint",
+)
+
+rust_doc(
+ name = "pw_varint_doc",
+ crate = ":pw_varint",
+)
diff --git a/pw_varint/rust/pw_varint.rs b/pw_varint/rust/pw_varint.rs
new file mode 100644
index 000000000..cd6ff62bb
--- /dev/null
+++ b/pw_varint/rust/pw_varint.rs
@@ -0,0 +1,367 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+//! # pw_varint
+//!
+//! `pw_varint` provides support for encoding and decoding variable length
+//! integers. Small values require less memory than they do with fixed
+//! encoding. Signed integers are first zig-zag encoded to allow small
+//! negative numbers to realize the memory savings. For more information see
+//! [Pigweed's pw_varint documentation](https://pigweed.dev/pw_varint).
+//!
+//! The encoding format used is compatible with
+//! [Protocol Buffers](https://developers.google.com/protocol-buffers/docs/encoding#varints).
+//!
+//! Encoding and decoding is provided through the [VarintEncode] and
+//! [VarintDecode] traits.
+//!
+//! # Example
+//!
+//! ```
+//! use pw_varint::{VarintEncode, VarintDecode};
+//!
+//! let mut buffer = [0u8; 64];
+//!
+//! let encoded_len = (-1i64).varint_encode(&mut buffer).unwrap();
+//!
+//! let (decoded_len, val) = i64::varint_decode(&buffer ).unwrap();
+//! ```
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+use core::num::Wrapping;
+
+use pw_status::{Error, Result};
+
+/// A trait for objects than can be decoded from a varint.
+///
+/// `pw_varint` provides implementations for [i16], [u16], [i32], [u32],
+/// [i64], and [u64].
+pub trait VarintDecode: Sized {
+ /// Decode a type from a varint encoded series of bytes.
+ ///
+ /// Signed values will be implicitly zig-zag decoded.
+ fn varint_decode(data: &[u8]) -> Result<(usize, Self)>;
+}
+
+/// A trait for objects than can be encoded into a varint.
+///
+/// `pw_varint` provides implementations for [i16], [u16], [i32], [u32],
+/// [i64], and [u64].
+pub trait VarintEncode: Sized {
+ /// Encode a type into a varint encoded series of bytes.
+ ///
+ /// Signed values will be implicitly zig-zag encoded.
+ fn varint_encode(self, data: &mut [u8]) -> Result<usize>;
+}
+
+macro_rules! unsigned_varint_impl {
+ ($t:ty) => {
+ impl VarintDecode for $t {
+ fn varint_decode(data: &[u8]) -> Result<(usize, Self)> {
+ let (data, val) = decode_u64(data)?;
+ Ok((data, val as Self))
+ }
+ }
+
+ impl VarintEncode for $t {
+ fn varint_encode(self, data: &mut [u8]) -> Result<usize> {
+ encode_u64(data, self as u64)
+ }
+ }
+ };
+}
+
+macro_rules! signed_varint_impl {
+ ($t:ty) => {
+ impl VarintDecode for $t {
+ fn varint_decode(data: &[u8]) -> Result<(usize, Self)> {
+ let (data, val) = decode_u64(data)?;
+ Ok((data, zig_zag_decode(val) as Self))
+ }
+ }
+
+ impl VarintEncode for $t {
+ fn varint_encode(self, data: &mut [u8]) -> Result<usize> {
+ encode_u64(data, zig_zag_encode(self as i64))
+ }
+ }
+ };
+}
+
+unsigned_varint_impl!(u8);
+unsigned_varint_impl!(u16);
+unsigned_varint_impl!(u32);
+unsigned_varint_impl!(u64);
+
+signed_varint_impl!(i8);
+signed_varint_impl!(i16);
+signed_varint_impl!(i32);
+signed_varint_impl!(i64);
+
+fn decode_u64(data: &[u8]) -> Result<(usize, u64)> {
+ let mut value: u64 = 0;
+ for (i, d) in data.iter().enumerate() {
+ value |= (*d as u64 & 0x7f) << (i * 7);
+
+ if (*d & 0x80) == 0 {
+ return Ok((i + 1, value));
+ }
+ }
+ Err(Error::OutOfRange)
+}
+
+fn encode_u64(data: &mut [u8], value: u64) -> Result<usize> {
+ let mut value = value;
+ for (i, d) in data.iter_mut().enumerate() {
+ let mut byte: u8 = (value & 0x7f) as u8;
+ value >>= 7;
+ if value > 0 {
+ byte |= 0x80;
+ }
+ *d = byte;
+ if value == 0 {
+ return Ok(i + 1);
+ }
+ }
+ Err(Error::OutOfRange)
+}
+
+// ZigZag encodes a signed integer. This maps small negative numbers to small,
+// unsigned positive numbers, which improves their density for LEB128 encoding.
+//
+// ZigZag encoding works by moving the sign bit from the most-significant bit to
+// the least-significant bit. For the signed k-bit integer n, the formula is
+//
+// (n << 1) ^ (n >> (k - 1))
+//
+// See the following for a description of ZigZag encoding:
+// https://developers.google.com/protocol-buffers/docs/encoding#types
+fn zig_zag_encode(value: i64) -> u64 {
+ ((value as u64) << 1) ^ ((value >> (i64::BITS - 1)) as u64)
+}
+
+fn zig_zag_decode(value: u64) -> i64 {
+ let value = Wrapping(value);
+ ((value >> 1) ^ (!(value & Wrapping(1)) + Wrapping(1))).0 as i64
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ fn success_cases_u8<T>() -> Vec<(Vec<u8>, T)>
+ where
+ T: From<u8>,
+ {
+ vec![
+ // From varint_test.cc EncodeSizeUnsigned32_SmallSingleByte.
+ (vec![0x00], 0x00.into()),
+ (vec![0x01], 0x01.into()),
+ (vec![0x02], 0x02.into()),
+ // From varint_test.cc EncodeSizeUnsigned32_LargeSingleByte.
+ (vec![0x3f], 0x3f.into()),
+ (vec![0x40], 0x40.into()),
+ (vec![0x7e], 0x7e.into()),
+ (vec![0x7f], 0x7f.into()),
+ // From varint_test.cc EncodeSizeUnsigned32_MultiByte.
+ (vec![0x80, 0x01], 128.into()),
+ (vec![0x81, 0x01], 129.into()),
+ // From https://protobuf.dev/programming-guides/encoding/.
+ (vec![0x96, 0x01], 150.into()),
+ ]
+ }
+
+ fn success_cases_i8<T>() -> Vec<(Vec<u8>, T)>
+ where
+ T: From<i8>,
+ {
+ vec![
+ // From varint_test.cc EncodeSizeSigned32_SmallSingleByte.
+ (vec![0x00], 0i8.into()),
+ (vec![0x01], (-1i8).into()),
+ (vec![0x02], 1i8.into()),
+ (vec![0x03], (-2i8).into()),
+ (vec![0x04], 2i8.into()),
+ // From varint_test.cc EncodeSizeSigned32_LargeSingleByte.
+ (vec![125], (-63i8).into()),
+ (vec![126], (63i8).into()),
+ (vec![127], (-64i8).into()),
+ // From varint_test.cc EncodeSizeSigned32_MultiByte.
+ (vec![0x80, 0x1], 64i8.into()),
+ (vec![0x81, 0x1], (-65i8).into()),
+ (vec![0x82, 0x1], 65i8.into()),
+ ]
+ }
+
+ fn success_cases_u32<T>() -> Vec<(Vec<u8>, T)>
+ where
+ T: From<u32>,
+ {
+ vec![
+ // From varint_test.cc EncodeSizeUnsigned32_MultiByte.
+ (vec![0xfe, 0xff, 0xff, 0xff, 0x0f], 0xffff_fffe.into()),
+ (vec![0xff, 0xff, 0xff, 0xff, 0x0f], 0xffff_ffff.into()),
+ ]
+ }
+
+ fn success_cases_i32<T>() -> Vec<(Vec<u8>, T)>
+ where
+ T: From<i32>,
+ {
+ vec![
+ // From varint_test.cc EncodeSizeSigned32_MultiByte.
+ (vec![0xff, 0xff, 0xff, 0xff, 0x0f], i32::MIN.into()),
+ (vec![0xfe, 0xff, 0xff, 0xff, 0x0f], i32::MAX.into()),
+ ]
+ }
+
+ #[test]
+ fn decode_test_u16() {
+ for case in success_cases_u8::<u16>() {
+ assert_eq!(u16::varint_decode(&case.0), Ok((case.0.len(), case.1)));
+ }
+
+ assert_eq!(u16::varint_decode(&[0x96]), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn decode_test_i16() {
+ for case in success_cases_i8::<i16>() {
+ assert_eq!(i16::varint_decode(&case.0), Ok((case.0.len(), case.1)));
+ }
+
+ assert_eq!(i16::varint_decode(&[0x96]), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn decode_test_u32() {
+ for case in success_cases_u8::<u32>()
+ .into_iter()
+ .chain(success_cases_u32::<u32>())
+ {
+ assert_eq!(u32::varint_decode(&case.0), Ok((case.0.len(), case.1)));
+ }
+
+ assert_eq!(u32::varint_decode(&[0x96]), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn decode_test_i32() {
+ for case in success_cases_i8::<i32>()
+ .into_iter()
+ .chain(success_cases_i32::<i32>())
+ {
+ assert_eq!(i32::varint_decode(&case.0), Ok((case.0.len(), case.1)));
+ }
+
+ assert_eq!(i32::varint_decode(&[0x96]), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn decode_test_u64() {
+ for case in success_cases_u8::<u64>()
+ .into_iter()
+ .chain(success_cases_u32::<u64>())
+ {
+ assert_eq!(u64::varint_decode(&case.0), Ok((case.0.len(), case.1)));
+ }
+
+ assert_eq!(u64::varint_decode(&[0x96]), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn decode_test_i64() {
+ for case in success_cases_i8::<i64>()
+ .into_iter()
+ .chain(success_cases_i32::<i64>())
+ {
+ assert_eq!(i64::varint_decode(&case.0), Ok((case.0.len(), case.1)));
+ }
+
+ assert_eq!(i64::varint_decode(&[0x96]), Err(Error::OutOfRange));
+ }
+
+ #[test]
+ fn encode_test_u16() {
+ for case in success_cases_u8::<u16>() {
+ let mut buffer = [0u8; 64];
+ let len = case.1.varint_encode(&mut buffer).unwrap();
+ assert_eq!(len, case.0.len());
+ assert_eq!(&buffer[0..len], case.0);
+ }
+ }
+
+ #[test]
+ fn encode_test_i16() {
+ for case in success_cases_i8::<i16>() {
+ let mut buffer = [0u8; 64];
+ let len = case.1.varint_encode(&mut buffer).unwrap();
+ assert_eq!(len, case.0.len());
+ assert_eq!(&buffer[0..len], case.0);
+ }
+ }
+
+ #[test]
+ fn encode_test_u32() {
+ for case in success_cases_u8::<u32>()
+ .into_iter()
+ .chain(success_cases_u32::<u32>())
+ {
+ let mut buffer = [0u8; 64];
+ let len = case.1.varint_encode(&mut buffer).unwrap();
+ assert_eq!(len, case.0.len());
+ assert_eq!(&buffer[0..len], case.0);
+ }
+ }
+
+ #[test]
+ fn encode_test_i32() {
+ for case in success_cases_i8::<i32>()
+ .into_iter()
+ .chain(success_cases_i32::<i32>())
+ {
+ let mut buffer = [0u8; 64];
+ let len = case.1.varint_encode(&mut buffer).unwrap();
+ assert_eq!(len, len);
+ assert_eq!(&buffer[0..len], case.0);
+ }
+ }
+
+ #[test]
+ fn encode_test_u64() {
+ for case in success_cases_u8::<u64>()
+ .into_iter()
+ .chain(success_cases_u32::<u64>())
+ {
+ let mut buffer = [0u8; 64];
+ let len = case.1.varint_encode(&mut buffer).unwrap();
+ assert_eq!(len, case.0.len());
+ assert_eq!(&buffer[0..len], case.0);
+ }
+ }
+
+ #[test]
+ fn encode_test_i64() {
+ for case in success_cases_i8::<i64>()
+ .into_iter()
+ .chain(success_cases_i32::<i64>())
+ {
+ let mut buffer = [0u8; 64];
+ let len = case.1.varint_encode(&mut buffer).unwrap();
+ assert_eq!(len, case.0.len());
+ assert_eq!(&buffer[0..len], case.0);
+ }
+ }
+}
diff --git a/pw_varint/stream_test.cc b/pw_varint/stream_test.cc
index bd7069400..b2b1214c7 100644
--- a/pw_varint/stream_test.cc
+++ b/pw_varint/stream_test.cc
@@ -185,7 +185,7 @@ TEST(VarintRead, Unsigned64_SingleByte) {
}
TEST(VarintRead, Unsigned64_MultiByte) {
- uint64_t value = -1234;
+ uint64_t value;
{
const auto buffer = MakeBuffer("\x80\x01");
@@ -243,7 +243,7 @@ TEST(VarintRead, Unsigned64_MultiByte) {
}
TEST(VarintRead, Errors) {
- uint64_t value = -1234;
+ uint64_t value;
{
std::array<std::byte, 0> buffer{};
@@ -275,7 +275,7 @@ TEST(VarintRead, Errors) {
}
TEST(VarintRead, SizeLimit) {
- uint64_t value = -1234;
+ uint64_t value;
{
// buffer contains a valid varint, but we limit the read length to ensure
diff --git a/pw_varint/varint.cc b/pw_varint/varint.cc
index 4ae65cdcc..e10bb7306 100644
--- a/pw_varint/varint.cc
+++ b/pw_varint/varint.cc
@@ -122,42 +122,9 @@ extern "C" size_t pw_varint_DecodeCustom(const void* input,
return count;
}
-// TODO(frolv): Remove this deprecated alias.
-extern "C" size_t pw_VarintEncode(uint64_t integer,
- void* output,
- size_t output_size) {
- return pw_varint_Encode(integer, output, output_size);
-}
-
-extern "C" size_t pw_varint_ZigZagEncode(int64_t integer,
- void* output,
- size_t output_size) {
- return pw_varint_Encode(ZigZagEncode(integer), output, output_size);
-}
-
-// TODO(frolv): Remove this deprecated alias.
-extern "C" size_t pw_VarintDecode(const void* input,
- size_t input_size,
- uint64_t* output) {
- return pw_varint_Decode(input, input_size, output);
-}
-
-extern "C" size_t pw_varint_ZigZagDecode(const void* input,
- size_t input_size,
- int64_t* output) {
- uint64_t value = 0;
- size_t bytes = pw_varint_Decode(input, input_size, &value);
- *output = ZigZagDecode(value);
- return bytes;
-}
-
-extern "C" size_t pw_varint_EncodedSize(uint64_t integer) {
+extern "C" size_t pw_varint_EncodedSizeBytes(uint64_t integer) {
return EncodedSize(integer);
}
-extern "C" size_t pw_varint_ZigZagEncodedSize(int64_t integer) {
- return ZigZagEncodedSize(integer);
-}
-
} // namespace varint
} // namespace pw
diff --git a/pw_varint/varint_c.c b/pw_varint/varint_c.c
new file mode 100644
index 000000000..9194b4e34
--- /dev/null
+++ b/pw_varint/varint_c.c
@@ -0,0 +1,77 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_varint/varint.h"
+
+#define VARINT_ENCODE_FUNCTION_BODY(bits) \
+ size_t written = 0; \
+ uint8_t* buffer = (uint8_t*)output; \
+ \
+ do { \
+ if (written >= output_size_bytes) { \
+ return 0u; \
+ } \
+ buffer[written++] = pw_varint_EncodeOneByte##bits(&integer); \
+ } while (integer != 0u); \
+ \
+ buffer[written - 1] &= 0x7f; \
+ return written
+
+size_t pw_varint_Encode32(uint32_t integer,
+ void* output,
+ size_t output_size_bytes) {
+ VARINT_ENCODE_FUNCTION_BODY(32);
+}
+
+size_t pw_varint_Encode64(uint64_t integer,
+ void* output,
+ size_t output_size_bytes) {
+ VARINT_ENCODE_FUNCTION_BODY(64);
+}
+
+#define VARINT_DECODE_FUNCTION_BODY(bits) \
+ uint##bits##_t value = 0; \
+ size_t count = 0; \
+ const uint8_t* buffer = (const uint8_t*)(input); \
+ \
+ /* Only read to the end of the buffer or largest possible encoded size. */ \
+ const size_t max_count = \
+ input_size_bytes < PW_VARINT_MAX_INT##bits##_SIZE_BYTES \
+ ? input_size_bytes \
+ : PW_VARINT_MAX_INT##bits##_SIZE_BYTES; \
+ \
+ bool keep_going; \
+ do { \
+ if (count >= max_count) { \
+ return 0; \
+ } \
+ \
+ keep_going = pw_varint_DecodeOneByte##bits(buffer[count], count, &value); \
+ count += 1; \
+ } while (keep_going); \
+ \
+ *output = value; \
+ return count
+
+size_t pw_varint_Decode32(const void* input,
+ size_t input_size_bytes,
+ uint32_t* output) {
+ VARINT_DECODE_FUNCTION_BODY(32);
+}
+
+size_t pw_varint_Decode64(const void* input,
+ size_t input_size_bytes,
+ uint64_t* output) {
+ VARINT_DECODE_FUNCTION_BODY(64);
+}
diff --git a/pw_varint/varint_test.cc b/pw_varint/varint_test.cc
index dfae0f7a6..02bfd0042 100644
--- a/pw_varint/varint_test.cc
+++ b/pw_varint/varint_test.cc
@@ -20,6 +20,7 @@
#include <limits>
#include "gtest/gtest.h"
+#include "pw_fuzzer/fuzztest.h"
namespace pw::varint {
namespace {
@@ -27,14 +28,20 @@ namespace {
extern "C" {
// Functions defined in varint_test.c which call the varint API from C.
-size_t pw_varint_CallEncode(uint64_t integer, void* output, size_t output_size);
-size_t pw_varint_CallZigZagEncode(int64_t integer,
- void* output,
- size_t output_size);
-size_t pw_varint_CallDecode(void* input, size_t input_size, uint64_t* output);
-size_t pw_varint_CallZigZagDecode(void* input,
- size_t input_size,
- int64_t* output);
+size_t pw_varint_CallEncode32(uint32_t integer,
+ void* output,
+ size_t output_size_bytes);
+size_t pw_varint_CallEncode64(uint64_t integer,
+ void* output,
+ size_t output_size_bytes);
+size_t pw_varint_CallZigZagAndVarintEncode64(int64_t integer,
+ void* output,
+ size_t output_size_bytes);
+size_t pw_varint_CallDecode32(void* input, size_t input_size, uint32_t* output);
+size_t pw_varint_CallDecode64(void* input, size_t input_size, uint64_t* output);
+size_t pw_varint_CallZigZagAndVarintDecode64(void* input,
+ size_t input_size,
+ int64_t* output);
} // extern "C"
@@ -64,11 +71,11 @@ TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_SmallSingleByte) {
}
TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_SmallSingleByte_C) {
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(0), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT32_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(1), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT32_C(1), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{1}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(2), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT32_C(2), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{2}, buffer_[0]);
}
@@ -84,13 +91,15 @@ TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_LargeSingleByte) {
}
TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_LargeSingleByte_C) {
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(63), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT32_C(63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{63}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(64), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT32_C(64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{64}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(126), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u,
+ pw_varint_CallEncode64(UINT32_C(126), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{126}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(127), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u,
+ pw_varint_CallEncode64(UINT32_C(127), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
@@ -108,20 +117,42 @@ TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_MultiByte) {
}
TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_MultiByte_C) {
- ASSERT_EQ(2u, pw_varint_CallEncode(UINT32_C(128), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(2u,
+ pw_varint_CallEncode64(UINT32_C(128), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
- ASSERT_EQ(2u, pw_varint_CallEncode(UINT32_C(129), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(2u,
+ pw_varint_CallEncode64(UINT32_C(129), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x81\x01", buffer_, 2), 0);
ASSERT_EQ(
5u,
- pw_varint_CallEncode(
+ pw_varint_CallEncode32(
std::numeric_limits<uint32_t>::max() - 1, buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
ASSERT_EQ(
5u,
- pw_varint_CallEncode(
+ pw_varint_CallEncode32(
+ std::numeric_limits<uint32_t>::max(), buffer_, sizeof(buffer_)));
+ EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
+
+ // Call with 64-bit function as well
+ ASSERT_EQ(2u,
+ pw_varint_CallEncode64(UINT32_C(128), buffer_, sizeof(buffer_)));
+ EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
+ ASSERT_EQ(2u,
+ pw_varint_CallEncode64(UINT32_C(129), buffer_, sizeof(buffer_)));
+ EXPECT_EQ(std::memcmp("\x81\x01", buffer_, 2), 0);
+
+ ASSERT_EQ(
+ 5u,
+ pw_varint_CallEncode64(
+ std::numeric_limits<uint32_t>::max() - 1, buffer_, sizeof(buffer_)));
+ EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
+
+ ASSERT_EQ(
+ 5u,
+ pw_varint_CallEncode64(
std::numeric_limits<uint32_t>::max(), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
}
@@ -141,19 +172,24 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned32_SmallSingleByte) {
TEST_F(VarintWithBuffer, EncodeSizeSigned32_SmallSingleByte_C) {
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(0), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(-1), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(-1), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{1}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(1), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(1), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{2}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(-2), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(-2), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{3}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(2), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(2), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{4}, buffer_[0]);
}
@@ -168,13 +204,16 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned32_LargeSingleByte) {
TEST_F(VarintWithBuffer, EncodeSizeSigned32_LargeSingleByte_C) {
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(-63), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(-63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{125}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(63), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{126}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT32_C(-64), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(-64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
@@ -195,22 +234,25 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned32_MultiByte) {
TEST_F(VarintWithBuffer, EncodeSizeSigned32_MultiByte_C) {
ASSERT_EQ(2u,
- pw_varint_CallZigZagEncode(INT32_C(64), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u,
- pw_varint_CallZigZagEncode(INT32_C(-65), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(-65), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x81\x01", buffer_, 2), 0);
ASSERT_EQ(2u,
- pw_varint_CallZigZagEncode(INT32_C(65), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT32_C(65), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x82\x01", buffer_, 2), 0);
ASSERT_EQ(5u,
- pw_varint_CallZigZagEncode(
+ pw_varint_CallZigZagAndVarintEncode64(
std::numeric_limits<int32_t>::min(), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
ASSERT_EQ(5u,
- pw_varint_CallZigZagEncode(
+ pw_varint_CallZigZagAndVarintEncode64(
std::numeric_limits<int32_t>::max(), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
}
@@ -225,11 +267,11 @@ TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_SmallSingleByte) {
}
TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_SmallSingleByte_C) {
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(0), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT64_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(1), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT64_C(1), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{1}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(2), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT64_C(2), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{2}, buffer_[0]);
}
@@ -245,13 +287,15 @@ TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_LargeSingleByte) {
}
TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_LargeSingleByte_C) {
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(63), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT64_C(63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{63}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(64), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u, pw_varint_CallEncode64(UINT64_C(64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{64}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(126), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u,
+ pw_varint_CallEncode64(UINT64_C(126), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{126}, buffer_[0]);
- ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(127), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(1u,
+ pw_varint_CallEncode64(UINT64_C(127), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
@@ -277,33 +321,35 @@ TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_MultiByte) {
}
TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_MultiByte_C) {
- ASSERT_EQ(2u, pw_varint_CallEncode(UINT64_C(128), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(2u,
+ pw_varint_CallEncode64(UINT64_C(128), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
- ASSERT_EQ(2u, pw_varint_CallEncode(UINT64_C(129), buffer_, sizeof(buffer_)));
+ ASSERT_EQ(2u,
+ pw_varint_CallEncode64(UINT64_C(129), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x81\x01", buffer_, 2), 0);
ASSERT_EQ(
5u,
- pw_varint_CallEncode(
+ pw_varint_CallEncode64(
std::numeric_limits<uint32_t>::max() - 1, buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
ASSERT_EQ(
5u,
- pw_varint_CallEncode(
+ pw_varint_CallEncode64(
std::numeric_limits<uint32_t>::max(), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
ASSERT_EQ(
10u,
- pw_varint_CallEncode(
+ pw_varint_CallEncode64(
std::numeric_limits<uint64_t>::max() - 1, buffer_, sizeof(buffer_)));
EXPECT_EQ(
std::memcmp("\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
ASSERT_EQ(
10u,
- pw_varint_CallEncode(
+ pw_varint_CallEncode64(
std::numeric_limits<uint64_t>::max(), buffer_, sizeof(buffer_)));
EXPECT_EQ(
std::memcmp("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
@@ -324,19 +370,24 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned64_SmallSingleByte) {
TEST_F(VarintWithBuffer, EncodeSizeSigned64_SmallSingleByte_C) {
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(0), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(-1), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(-1), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{1}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(1), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(1), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{2}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(-2), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(-2), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{3}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(2), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(2), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{4}, buffer_[0]);
}
@@ -351,13 +402,16 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned64_LargeSingleByte) {
TEST_F(VarintWithBuffer, EncodeSizeSigned64_LargeSingleByte_C) {
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(-63), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(-63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{125}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(63), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{126}, buffer_[0]);
ASSERT_EQ(1u,
- pw_varint_CallZigZagEncode(INT64_C(-64), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(-64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
@@ -390,37 +444,40 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned64_MultiByte) {
TEST_F(VarintWithBuffer, EncodeSizeSigned64_MultiByte_C) {
ASSERT_EQ(2u,
- pw_varint_CallZigZagEncode(INT64_C(64), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u,
- pw_varint_CallZigZagEncode(INT64_C(-65), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(-65), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x81\x01", buffer_, 2), 0);
ASSERT_EQ(2u,
- pw_varint_CallZigZagEncode(INT64_C(65), buffer_, sizeof(buffer_)));
+ pw_varint_CallZigZagAndVarintEncode64(
+ INT64_C(65), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x82\x01", buffer_, 2), 0);
ASSERT_EQ(5u,
- pw_varint_CallZigZagEncode(
+ pw_varint_CallZigZagAndVarintEncode64(
static_cast<int64_t>(std::numeric_limits<int32_t>::min()),
buffer_,
sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
ASSERT_EQ(5u,
- pw_varint_CallZigZagEncode(
+ pw_varint_CallZigZagAndVarintEncode64(
static_cast<int64_t>(std::numeric_limits<int32_t>::max()),
buffer_,
sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
ASSERT_EQ(10u,
- pw_varint_CallZigZagEncode(
+ pw_varint_CallZigZagAndVarintEncode64(
std::numeric_limits<int64_t>::min(), buffer_, sizeof(buffer_)));
EXPECT_EQ(
std::memcmp("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
ASSERT_EQ(10u,
- pw_varint_CallZigZagEncode(
+ pw_varint_CallZigZagAndVarintEncode64(
std::numeric_limits<int64_t>::max(), buffer_, sizeof(buffer_)));
EXPECT_EQ(
std::memcmp("\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
@@ -430,36 +487,26 @@ TEST_F(VarintWithBuffer, EncodeSizeSigned64_MultiByte_C) {
// tests. Set the increment to 1 to test every number (this is slow).
constexpr int kIncrement = 100'000'009;
-TEST_F(VarintWithBuffer, EncodeDecodeSigned32) {
- int32_t i = std::numeric_limits<int32_t>::min();
- while (true) {
- size_t encoded = Encode(i, buffer_);
-
- int64_t result;
- size_t decoded = Decode(buffer_, &result);
+template <typename T, typename U = T>
+void EncodeDecode(T value) {
+ std::byte buffer[10];
+ size_t encoded = Encode(value, buffer);
- EXPECT_EQ(encoded, decoded);
- ASSERT_EQ(i, result);
+ U result;
+ size_t decoded = Decode(buffer, &result);
- if (i > std::numeric_limits<int32_t>::max() - kIncrement) {
- break;
- }
+ EXPECT_EQ(encoded, decoded);
+ ASSERT_EQ(value, result);
+}
- i += kIncrement;
- }
+void EncodeDecodeSigned32(int32_t value) {
+ EncodeDecode<int32_t, int64_t>(value);
}
-TEST_F(VarintWithBuffer, EncodeDecodeSigned32_C) {
+TEST(Varint, EncodeDecodeSigned32Incremental) {
int32_t i = std::numeric_limits<int32_t>::min();
while (true) {
- size_t encoded = pw_varint_CallZigZagEncode(i, buffer_, sizeof(buffer_));
-
- int64_t result;
- size_t decoded =
- pw_varint_CallZigZagDecode(buffer_, sizeof(buffer_), &result);
-
- EXPECT_EQ(encoded, decoded);
- ASSERT_EQ(i, result);
+ EncodeDecodeSigned32(i);
if (i > std::numeric_limits<int32_t>::max() - kIncrement) {
break;
@@ -469,35 +516,16 @@ TEST_F(VarintWithBuffer, EncodeDecodeSigned32_C) {
}
}
-TEST_F(VarintWithBuffer, EncodeDecodeUnsigned32) {
- uint32_t i = 0;
- while (true) {
- size_t encoded = Encode(i, buffer_);
-
- uint64_t result;
- size_t decoded = Decode(buffer_, &result);
-
- EXPECT_EQ(encoded, decoded);
- ASSERT_EQ(i, result);
+FUZZ_TEST(Varint, EncodeDecodeSigned32);
- if (i > std::numeric_limits<uint32_t>::max() - kIncrement) {
- break;
- }
-
- i += kIncrement;
- }
+void EncodeDecodeUnsigned32(uint32_t value) {
+ EncodeDecode<uint32_t, uint64_t>(value);
}
-TEST_F(VarintWithBuffer, EncodeDecodeUnsigned32_C) {
+TEST(Varint, EncodeDecodeUnsigned32Incremental) {
uint32_t i = 0;
while (true) {
- size_t encoded = pw_varint_CallEncode(i, buffer_, sizeof(buffer_));
-
- uint64_t result;
- size_t decoded = pw_varint_CallDecode(buffer_, sizeof(buffer_), &result);
-
- EXPECT_EQ(encoded, decoded);
- ASSERT_EQ(i, result);
+ EncodeDecodeUnsigned32(i);
if (i > std::numeric_limits<uint32_t>::max() - kIncrement) {
break;
@@ -507,6 +535,53 @@ TEST_F(VarintWithBuffer, EncodeDecodeUnsigned32_C) {
}
}
+FUZZ_TEST(Varint, EncodeDecodeUnsigned32);
+
+#define ENCODE_DECODE_C_TEST(bits) \
+ void EncodeDecode##bits##_C(uint##bits##_t value) { \
+ std::byte buffer[kMaxVarint##bits##SizeBytes]; \
+ size_t encoded = \
+ pw_varint_CallEncode##bits(value, buffer, sizeof(buffer)); \
+ \
+ uint##bits##_t result; \
+ size_t decoded = \
+ pw_varint_CallDecode##bits(buffer, sizeof(buffer), &result); \
+ \
+ EXPECT_EQ(encoded, decoded); \
+ ASSERT_EQ(value, result); \
+ } \
+ \
+ TEST(Varint, EncodeDecode##bits##Signed32Incremental_C) { \
+ int32_t i = std::numeric_limits<int32_t>::min(); \
+ while (true) { \
+ EncodeDecode##bits##_C(static_cast<uint##bits##_t>(i)); \
+ \
+ if (i > std::numeric_limits<int32_t>::max() - kIncrement) { \
+ break; \
+ } \
+ \
+ i += kIncrement; \
+ } \
+ } \
+ \
+ TEST(Varint, EncodeDecode##bits##Unsigned32Incremental_C) { \
+ uint32_t i = 0; \
+ while (true) { \
+ EncodeDecode##bits##_C(static_cast<uint##bits##_t>(i)); \
+ \
+ if (i > std::numeric_limits<uint32_t>::max() - kIncrement) { \
+ break; \
+ } \
+ \
+ i += kIncrement; \
+ } \
+ } \
+ \
+ FUZZ_TEST(Varint, EncodeDecode##bits##_C)
+
+ENCODE_DECODE_C_TEST(32);
+ENCODE_DECODE_C_TEST(64);
+
template <size_t kStringSize>
auto MakeBuffer(const char (&data)[kStringSize]) {
constexpr size_t kSizeBytes = kStringSize - 1;
@@ -543,32 +618,38 @@ TEST(VarintDecode, DecodeSigned64_SingleByte_C) {
int64_t value = -1234;
auto buffer = MakeBuffer("\x00");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer.data(), buffer.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer.data(), buffer.size(), &value),
1u);
EXPECT_EQ(value, 0);
buffer = MakeBuffer("\x01");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer.data(), buffer.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer.data(), buffer.size(), &value),
1u);
EXPECT_EQ(value, -1);
buffer = MakeBuffer("\x02");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer.data(), buffer.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer.data(), buffer.size(), &value),
1u);
EXPECT_EQ(value, 1);
buffer = MakeBuffer("\x03");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer.data(), buffer.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer.data(), buffer.size(), &value),
1u);
EXPECT_EQ(value, -2);
buffer = MakeBuffer("\x04");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer.data(), buffer.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer.data(), buffer.size(), &value),
1u);
EXPECT_EQ(value, 2);
buffer = MakeBuffer("\x04");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer.data(), buffer.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer.data(), buffer.size(), &value),
1u);
EXPECT_EQ(value, 2);
}
@@ -606,37 +687,44 @@ TEST(VarintDecode, DecodeSigned64_MultiByte_C) {
int64_t value = -1234;
auto buffer2 = MakeBuffer("\x80\x01");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer2.data(), buffer2.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer2.data(), buffer2.size(), &value),
2u);
EXPECT_EQ(value, 64);
buffer2 = MakeBuffer("\x81\x01");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer2.data(), buffer2.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer2.data(), buffer2.size(), &value),
2u);
EXPECT_EQ(value, -65);
buffer2 = MakeBuffer("\x82\x01");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer2.data(), buffer2.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer2.data(), buffer2.size(), &value),
2u);
EXPECT_EQ(value, 65);
auto buffer4 = MakeBuffer("\xff\xff\xff\xff\x0f");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer4.data(), buffer4.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer4.data(), buffer4.size(), &value),
5u);
EXPECT_EQ(value, std::numeric_limits<int32_t>::min());
buffer4 = MakeBuffer("\xfe\xff\xff\xff\x0f");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer4.data(), buffer4.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer4.data(), buffer4.size(), &value),
5u);
EXPECT_EQ(value, std::numeric_limits<int32_t>::max());
auto buffer8 = MakeBuffer("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer8.data(), buffer8.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer8.data(), buffer8.size(), &value),
10u);
EXPECT_EQ(value, std::numeric_limits<int64_t>::min());
buffer8 = MakeBuffer("\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01");
- EXPECT_EQ(pw_varint_CallZigZagDecode(buffer8.data(), buffer8.size(), &value),
+ EXPECT_EQ(pw_varint_CallZigZagAndVarintDecode64(
+ buffer8.data(), buffer8.size(), &value),
10u);
EXPECT_EQ(value, std::numeric_limits<int64_t>::max());
}
@@ -849,7 +937,7 @@ TEST_F(VarintWithBuffer, EncodeWithOptions_MultiByte) {
}
TEST(Varint, DecodeWithOptions_SingleByte) {
- uint64_t value = -1234;
+ uint64_t value;
EXPECT_EQ(
Decode(
@@ -934,7 +1022,7 @@ TEST(Varint, DecodeWithOptions_SingleByte) {
}
TEST(Varint, DecodeWithOptions_MultiByte) {
- uint64_t value = -1234;
+ uint64_t value;
EXPECT_EQ(Decode(MakeBuffer("\x01\x10"),
&value,
@@ -1009,50 +1097,42 @@ TEST(Varint, DecodeWithOptions_MultiByte) {
EXPECT_EQ(value, 0u);
}
-TEST(Varint, EncodedSize) {
- EXPECT_EQ(EncodedSize(uint64_t(0u)), 1u);
- EXPECT_EQ(EncodedSize(uint64_t(1u)), 1u);
- EXPECT_EQ(EncodedSize(uint64_t(127u)), 1u);
- EXPECT_EQ(EncodedSize(uint64_t(128u)), 2u);
- EXPECT_EQ(EncodedSize(uint64_t(16383u)), 2u);
- EXPECT_EQ(EncodedSize(uint64_t(16384u)), 3u);
- EXPECT_EQ(EncodedSize(uint64_t(2097151u)), 3u);
- EXPECT_EQ(EncodedSize(uint64_t(2097152u)), 4u);
- EXPECT_EQ(EncodedSize(uint64_t(268435455u)), 4u);
- EXPECT_EQ(EncodedSize(uint64_t(268435456u)), 5u);
- EXPECT_EQ(EncodedSize(uint64_t(34359738367u)), 5u);
- EXPECT_EQ(EncodedSize(uint64_t(34359738368u)), 6u);
- EXPECT_EQ(EncodedSize(uint64_t(4398046511103u)), 6u);
- EXPECT_EQ(EncodedSize(uint64_t(4398046511104u)), 7u);
- EXPECT_EQ(EncodedSize(uint64_t(562949953421311u)), 7u);
- EXPECT_EQ(EncodedSize(uint64_t(562949953421312u)), 8u);
- EXPECT_EQ(EncodedSize(uint64_t(72057594037927935u)), 8u);
- EXPECT_EQ(EncodedSize(uint64_t(72057594037927936u)), 9u);
- EXPECT_EQ(EncodedSize(uint64_t(9223372036854775807u)), 9u);
- EXPECT_EQ(EncodedSize(uint64_t(9223372036854775808u)), 10u);
- EXPECT_EQ(EncodedSize(std::numeric_limits<uint64_t>::max()), 10u);
- EXPECT_EQ(EncodedSize(std::numeric_limits<int64_t>::max()), 9u);
- EXPECT_EQ(EncodedSize(int64_t(-1)), 10u);
- EXPECT_EQ(EncodedSize(std::numeric_limits<int64_t>::min()), 10u);
-}
-
-TEST(Varint, ZigZagEncodedSize) {
- EXPECT_EQ(ZigZagEncodedSize(int64_t(0)), 1u);
- EXPECT_EQ(ZigZagEncodedSize(int64_t(-1)), 1u);
- EXPECT_EQ(ZigZagEncodedSize(int64_t(1)), 1u);
- EXPECT_EQ(ZigZagEncodedSize(int64_t(-64)), 1u);
- EXPECT_EQ(ZigZagEncodedSize(int64_t(-65)), 2u);
- EXPECT_EQ(ZigZagEncodedSize(int64_t(63)), 1u);
- EXPECT_EQ(ZigZagEncodedSize(int64_t(64)), 2u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int8_t>::min()), 2u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int8_t>::max()), 2u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int16_t>::min()), 3u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int16_t>::max()), 3u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int32_t>::min()), 5u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int32_t>::max()), 5u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int64_t>::min()), 10u);
- EXPECT_EQ(ZigZagEncodedSize(std::numeric_limits<int64_t>::max()), 10u);
-}
+#define ENCODED_SIZE_TEST(function) \
+ TEST(Varint, function) { \
+ EXPECT_EQ(function(uint64_t(0u)), 1u); \
+ EXPECT_EQ(function(uint64_t(1u)), 1u); \
+ EXPECT_EQ(function(uint64_t(127u)), 1u); \
+ EXPECT_EQ(function(uint64_t(128u)), 2u); \
+ EXPECT_EQ(function(uint64_t(16383u)), 2u); \
+ EXPECT_EQ(function(uint64_t(16384u)), 3u); \
+ EXPECT_EQ(function(uint64_t(2097151u)), 3u); \
+ EXPECT_EQ(function(uint64_t(2097152u)), 4u); \
+ EXPECT_EQ(function(uint64_t(268435455u)), 4u); \
+ EXPECT_EQ(function(uint64_t(268435456u)), 5u); \
+ EXPECT_EQ(function(uint64_t(34359738367u)), 5u); \
+ EXPECT_EQ(function(uint64_t(34359738368u)), 6u); \
+ EXPECT_EQ(function(uint64_t(4398046511103u)), 6u); \
+ EXPECT_EQ(function(uint64_t(4398046511104u)), 7u); \
+ EXPECT_EQ(function(uint64_t(562949953421311u)), 7u); \
+ EXPECT_EQ(function(uint64_t(562949953421312u)), 8u); \
+ EXPECT_EQ(function(uint64_t(72057594037927935u)), 8u); \
+ EXPECT_EQ(function(uint64_t(72057594037927936u)), 9u); \
+ EXPECT_EQ(function(uint64_t(9223372036854775807u)), 9u); \
+ EXPECT_EQ(function(uint64_t(9223372036854775808u)), 10u); \
+ EXPECT_EQ(function(std::numeric_limits<uint64_t>::max()), 10u); \
+ EXPECT_EQ( \
+ static_cast<uint64_t>(function(std::numeric_limits<int64_t>::max())), \
+ 9u); \
+ EXPECT_EQ(function(uint64_t(-1)), 10u); \
+ EXPECT_EQ( \
+ function(static_cast<uint64_t>(std::numeric_limits<int64_t>::min())), \
+ 10u); \
+ } \
+ static_assert(true)
+
+ENCODED_SIZE_TEST(EncodedSize);
+ENCODED_SIZE_TEST(pw_varint_EncodedSizeBytes);
+ENCODED_SIZE_TEST(PW_VARINT_ENCODED_SIZE_BYTES);
constexpr uint64_t CalculateMaxValueInBytes(size_t bytes) {
uint64_t value = 0;
diff --git a/pw_varint/varint_test_c.c b/pw_varint/varint_test_c.c
index 0e66ac90f..355db97ab 100644
--- a/pw_varint/varint_test_c.c
+++ b/pw_varint/varint_test_c.c
@@ -19,24 +19,42 @@
#include "pw_varint/varint.h"
-size_t pw_varint_CallEncode(uint64_t integer,
- void* output,
- size_t output_size) {
- return pw_varint_Encode(integer, output, output_size);
+size_t pw_varint_CallEncode32(uint32_t integer,
+ void* output,
+ size_t output_size_bytes) {
+ return pw_varint_Encode32(integer, output, output_size_bytes);
}
-size_t pw_varint_CallZigZagEncode(int64_t integer,
- void* output,
- size_t output_size) {
- return pw_varint_ZigZagEncode(integer, output, output_size);
+size_t pw_varint_CallEncode64(uint64_t integer,
+ void* output,
+ size_t output_size_bytes) {
+ return pw_varint_Encode64(integer, output, output_size_bytes);
}
-size_t pw_varint_CallDecode(void* input, size_t input_size, uint64_t* output) {
- return pw_varint_Decode(input, input_size, output);
+size_t pw_varint_CallZigZagAndVarintEncode64(int64_t integer,
+ void* output,
+ size_t output_size_bytes) {
+ return pw_varint_Encode64(
+ pw_varint_ZigZagEncode64(integer), output, output_size_bytes);
}
-size_t pw_varint_CallZigZagDecode(void* input,
- size_t input_size,
- int64_t* output) {
- return pw_varint_ZigZagDecode(input, input_size, output);
+size_t pw_varint_CallDecode32(void* input,
+ size_t input_size,
+ uint32_t* output) {
+ return pw_varint_Decode32(input, input_size, output);
+}
+
+size_t pw_varint_CallDecode64(void* input,
+ size_t input_size,
+ uint64_t* output) {
+ return pw_varint_Decode64(input, input_size, output);
+}
+
+size_t pw_varint_CallZigZagAndVarintDecode64(void* input,
+ size_t input_size,
+ int64_t* output) {
+ uint64_t value = 0;
+ const size_t bytes_read = pw_varint_Decode64(input, input_size, &value);
+ *output = pw_varint_ZigZagDecode64(value);
+ return bytes_read;
}
diff --git a/pw_watch/BUILD.gn b/pw_watch/BUILD.gn
index 2405d1556..f8d02ee0c 100644
--- a/pw_watch/BUILD.gn
+++ b/pw_watch/BUILD.gn
@@ -18,11 +18,11 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
- inputs = [
- "doc_resources/pw_watch_on_device_demo.gif",
- "doc_resources/pw_watch_test_demo2.gif",
+ sources = [
+ "cli.rst",
+ "docs.rst",
+ "guide.rst",
]
- sources = [ "docs.rst" ]
}
pw_test_group("tests") {
diff --git a/pw_watch/cli.rst b/pw_watch/cli.rst
new file mode 100644
index 000000000..0360c85d5
--- /dev/null
+++ b/pw_watch/cli.rst
@@ -0,0 +1,16 @@
+.. _module-pw_watch-cli:
+
+=======================
+pw_watch CLI reference
+=======================
+.. pigweed-module-subpage::
+ :name: pw_watch
+ :tagline: Embedded development file system watcher
+
+.. argparse::
+ :module: pw_watch.watch
+ :func: get_parser
+ :prog: pw watch
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
diff --git a/pw_watch/doc_resources/pw_watch_on_device_demo.gif b/pw_watch/doc_resources/pw_watch_on_device_demo.gif
deleted file mode 120000
index dd323496d..000000000
--- a/pw_watch/doc_resources/pw_watch_on_device_demo.gif
+++ /dev/null
@@ -1 +0,0 @@
-../../docs/images/pw_watch_on_device_demo.gif \ No newline at end of file
diff --git a/pw_watch/doc_resources/pw_watch_test_demo2.gif b/pw_watch/doc_resources/pw_watch_test_demo2.gif
deleted file mode 120000
index a3c62134e..000000000
--- a/pw_watch/doc_resources/pw_watch_test_demo2.gif
+++ /dev/null
@@ -1 +0,0 @@
-../../docs/images/pw_watch_test_demo2.gif \ No newline at end of file
diff --git a/pw_watch/docs.rst b/pw_watch/docs.rst
index 8998b3f2b..62e4503d5 100644
--- a/pw_watch/docs.rst
+++ b/pw_watch/docs.rst
@@ -1,139 +1,77 @@
.. _module-pw_watch:
+.. rst-class:: with-subtitle
+
========
pw_watch
========
+.. pigweed-module::
+ :name: pw_watch
+ :tagline: Embedded development file system watcher
+ :status: stable
+
+ * **Automatically trigger build actions when source files change**
+
+----------
+Background
+----------
+In the web development space, file system watchers like `nodemon
+<https://www.npmjs.com/package/nodemon>`_ and `watchman
+<https://facebook.github.io/watchman/>`_ are prevalent. These watchers trigger
+actions when files change (such as reloading a web server), making development
+much faster. In the embedded space, file system watchers are less prevalent but
+no less useful!
+
+.. _module-pw_watch-design:
+
+------------
+Our solution
+------------
``pw_watch`` is similar to file system watchers found in web development
-tooling. These watchers trigger a web server reload on source file change,
-increasing iteration. In the embedded space, file system watchers are less
-prevalent but no less useful! The Pigweed watcher module makes it easy to
-instantly compile, flash, and run tests upon save.
+tooling but is focused around embedded development use cases. After changing
+source code, ``pw_watch`` can instantly compile, flash, and run tests.
-.. figure:: doc_resources/pw_watch_test_demo2.gif
+.. figure:: https://storage.googleapis.com/pigweed-media/pw_watch_test_demo2.gif
:width: 1420
- :alt: pw watch running in fullscreen mode and displaying errors
+ :alt: ``pw watch`` running in fullscreen mode and displaying errors
``pw watch`` running in fullscreen mode and displaying errors.
---------------------------
-``pw watch`` Command Usage
---------------------------
-The simplest way to get started with ``pw_watch`` is to launch it from a shell
-using the Pigweed environment as ``pw watch``. By default, ``pw_watch`` watches
-for repository changes and triggers the default Ninja build target at out/. To
-override this behavior, provide the ``-C`` argument to ``pw watch``.
-
-.. argparse::
- :module: pw_watch.watch
- :func: get_parser
- :prog: pw watch
- :nodefaultconst:
- :nodescription:
- :noepilog:
-
---------
-Examples
---------
-
-Ninja
-=====
-Build the default target in ``out/`` using ``ninja``.
-
-.. code-block:: sh
-
- pw watch -C out
-
-Build ``python.lint`` and ``stm32f429i`` targets in ``out/`` using ``ninja``.
-
-.. code-block:: sh
-
- pw watch python.lint stm32f429i
-
-Build the ``pw_run_tests.modules`` target in the ``out/cmake/`` directory
-
-.. code-block:: sh
+Combined with the GN-based build which expresses the full dependency tree,
+only the exact tests affected by source changes are run.
- pw watch -C out/cmake pw_run_tests.modules
+The demo below shows ``pw_watch`` building for a STMicroelectronics
+STM32F429I-DISC1 development board, flashing the board with the affected test,
+and verifying the test runs as expected. Once this is set up, you can attach
+multiple devices to run tests in a distributed manner to reduce the time it
+takes to run tests.
-Build the default target in ``out/`` and ``pw_apps`` in ``out/cmake/``
+.. figure:: https://storage.googleapis.com/pigweed-media/pw_watch_on_device_demo.gif
+ :width: 800
+ :alt: pw_watch running on-device tests
-.. code-block:: sh
+.. _module-pw_watch-get-started:
- pw watch -C out -C out/cmake pw_apps
+-----------
+Get started
+-----------
+.. code-block:: bash
-Build ``python.tests`` in ``out/`` and ``pw_apps`` in ``out/cmake/``
+ cd ~/pigweed
+ source activate.sh
+ pw watch
-.. code-block:: sh
-
- pw watch python.tests -C out/cmake pw_apps
-
-Bazel
-=====
-Run ``bazel build`` followed by ``bazel test`` on the target ``//...`` using the
-``out-bazel/`` build directory.
-
-.. code-block:: sh
-
- pw watch --run-command 'mkdir -p out-bazel' \
- -C out-bazel '//...' \
- --build-system-command out-bazel 'bazel build' \
- --build-system-command out-bazel 'bazel test'
-
-Log Files
-=========
-Run two separate builds simultaneously and stream the output to separate build
-log files. These log files are created:
-
-- ``out/build.txt``: This will contain overall status messages and any sub build
- errors.
-- ``out/build_out.txt``: Sub-build log only output for the ``out`` build
- directory:
-- ``out/build_outbazel.txt``: Sub-build log only output for the ``outbazel``
- build directory.
-
-.. code-block:: sh
-
- pw watch \
- --parallel \
- --logfile out/build.txt \
- --separate-logfiles \
- -C out default \
- -C outbazel '//...:all' \
- --build-system-command outbazel 'bazel build' \
- --build-system-command outbazel 'bazel test'
-
-Including and Ignoring Files
-============================
-``pw watch`` only rebuilds when a file that is not ignored by Git changes.
-Adding exclusions to a ``.gitignore`` causes watch to ignore them, even if the
-files were forcibly added to a repo. By default, only files matching certain
-extensions are applied, even if they're tracked by Git. The ``--patterns`` and
-``--ignore-patterns`` arguments can be used to include or exclude specific
-patterns. These patterns do not override Git's ignoring logic.
-
-The ``--exclude-list`` argument can be used to exclude directories from being
-watched. This decreases the number of files monitored with inotify in Linux.
-
-Documentation Output
-====================
-When using ``--serve-docs``, by default the docs will be rebuilt when changed,
-just like code files. However, you will need to manually reload the page in
-your browser to see changes. If you install the ``httpwatcher`` Python package
-into your Pigweed environment (``pip install httpwatcher``), docs pages will
-automatically reload when changed.
+The simplest way to get started with ``pw_watch`` is to launch it from a shell
+using the Pigweed environment as ``pw watch``. By default, ``pw_watch`` watches
+for repository changes and triggers the default Ninja build target at ``//out``.
+To override this behavior, provide the ``-C`` argument to ``pw watch``.
-Disable Automatic Rebuilds
-==========================
-``pw watch`` automatically restarts an ongoing build when files
-change. This can be disabled with the ``--no-restart`` option. While running
-``pw watch``, you may also press enter to immediately restart a build.
+See :ref:`module-pw_watch-guide` for more examples and
+:ref:`module-pw_watch-cli` for detailed CLI usage information.
----------------------
-Unit Test Integration
----------------------
-Thanks to GN's understanding of the full dependency tree, only the tests
-affected by a file change are run when ``pw_watch`` triggers a build. By
-default, host builds using ``pw_watch`` will run unit tests. To run unit tests
-on a device as part of ``pw_watch``, refer to your device's
-:ref:`target documentation<docs-targets>`.
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+ guide
+ cli
diff --git a/pw_watch/guide.rst b/pw_watch/guide.rst
new file mode 100644
index 000000000..a6845de19
--- /dev/null
+++ b/pw_watch/guide.rst
@@ -0,0 +1,140 @@
+.. _module-pw_watch-guide:
+
+=====================
+pw_watch how-to guide
+=====================
+.. pigweed-module-subpage::
+ :name: pw_watch
+ :tagline: Embedded development file system watcher
+
+This guide shows you how to do common ``pw_watch`` tasks.
+
+See :ref:`docs-build-system` for an overview of Pigweed's approach to build
+systems.
+
+-------------------------------
+Set up your Pigweed environment
+-------------------------------
+See :ref:`activate-pigweed-environment` if you see an error like this:
+
+.. code-block:: sh
+
+ pw watch
+ bash: pw: command not found
+
+-----
+Ninja
+-----
+This section contains common use cases for :ref:`docs-build-system-gn`
+users.
+
+.. _module-pw_watch-guide-ninja-custom-dirs:
+
+Set up a custom build directory
+-------------------------------
+
+Before running any command that uses a custom build directory, you need to
+run ``gn gen <dir>``, where ``<dir>`` is a placeholder for the name of your
+custom build directory.
+
+For example, before running this command:
+
+.. code-block:: sh
+
+ pw watch -C out2
+
+You need to run this command:
+
+.. code-block:: sh
+
+ gn gen out2
+
+Build the default target and use the default build directory
+------------------------------------------------------------
+.. code-block:: sh
+
+ pw watch
+
+The default build directory is ``out``.
+
+Customize the build directory
+-----------------------------
+This section assumes you have completed
+:ref:`module-pw_watch-guide-ninja-custom-dirs`.
+
+.. code-block:: sh
+
+ pw watch -C out2
+
+This builds the default target in ``out2``.
+
+Build two targets
+-----------------
+.. code-block:: sh
+
+ pw watch stm32f429i python.lint
+
+The ``stm32f429i`` and ``python.lint`` targets are both built in the default
+build directory (``out``).
+
+Build the same target in different build directories
+----------------------------------------------------
+This section assumes you have completed
+:ref:`module-pw_watch-guide-ninja-custom-dirs`.
+
+.. code-block:: sh
+
+ pw watch -C out1 -C out2
+
+This example builds the default target in both ``out1`` and ``out2``.
+
+Build different targets in different build directories
+------------------------------------------------------
+This section assumes you have completed
+:ref:`module-pw_watch-guide-ninja-custom-dirs`.
+
+.. code-block:: sh
+
+ pw watch stm32f429i -C out2 python.lint
+
+The ``stm32f429i`` target is built in the default build directory (``out``).
+The ``python.lint`` target is built in the custom build directory (``out2``).
+
+Unit test integration
+---------------------
+Thanks to GN's understanding of the full dependency tree, only the tests
+affected by a file change are run when ``pw_watch`` triggers a build. By
+default, host builds using ``pw_watch`` will run unit tests. To run unit tests
+on a device as part of ``pw_watch``, refer to your device's
+:ref:`target documentation<docs-targets>`.
+
+----------------------------
+Build-system-agnostic guides
+----------------------------
+This section discusses general use cases that all apply to all ``pw watch``
+usage. In other words, these use cases are not affected by whether you're
+using GN, Bazel, and so on.
+
+Ignore files
+------------
+``pw watch`` only rebuilds when a file that is not ignored by Git changes.
+Adding exclusions to a ``.gitignore`` causes ``pw watch`` to ignore them, even
+if the files were forcibly added to a repo. By default, only files matching
+certain extensions are applied, even if they're tracked by Git. The
+``--patterns`` and ``--ignore-patterns`` arguments can be used to include or
+exclude specific patterns. These patterns do not override Git's ignoring logic.
+
+The ``--exclude-list`` argument can be used to exclude directories from being
+watched. This decreases the number of files monitored with ``inotify`` in Linux.
+
+Automatically reload docs
+-------------------------
+When using ``--serve-docs``, by default the docs will be rebuilt when changed,
+just like code files. However, you will need to manually reload the page in
+your browser to see changes.
+
+Disable automatic rebuilds
+--------------------------
+``pw watch`` automatically restarts an ongoing build when files change. This
+can be disabled with the ``--no-restart`` option. While running ``pw watch``,
+you may also press :kbd:`Enter` to immediately restart a build.
diff --git a/pw_watch/py/BUILD.gn b/pw_watch/py/BUILD.gn
index 31576d090..fa3cc8329 100644
--- a/pw_watch/py/BUILD.gn
+++ b/pw_watch/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"pw_watch/__init__.py",
diff --git a/pw_watch/py/pw_watch/argparser.py b/pw_watch/py/pw_watch/argparser.py
index 77974ece4..d37e8dcdd 100644
--- a/pw_watch/py/pw_watch/argparser.py
+++ b/pw_watch/py/pw_watch/argparser.py
@@ -36,6 +36,8 @@ WATCH_PATTERNS = (
'*.go',
'*.h',
'*.hpp',
+ '*.html',
+ '*.js',
'*.ld',
'*.md',
'*.options',
@@ -46,6 +48,7 @@ WATCH_PATTERNS = (
'*.s',
'*.S',
'*.toml',
+ '*.ts',
)
@@ -84,7 +87,7 @@ def add_parser_arguments(
'--no-restart',
dest='restart',
action='store_false',
- help='do not restart ongoing builds if files change',
+ help='Do not restart ongoing builds if files change.',
)
watch_group.add_argument(
@@ -110,8 +113,10 @@ def add_parser_arguments(
dest='serve_docs_path',
type=Path,
default='docs/gen/docs',
- help='Set the path for the docs to serve. Default: docs/gen/docs'
- ' in the build directory.',
+ help=(
+ 'Set the path for the docs to serve. Default: docs/gen/docs '
+ 'in the build directory.'
+ ),
)
watch_group.add_argument(
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py
index d956df3ec..cd75ab596 100755
--- a/pw_watch/py/pw_watch/watch.py
+++ b/pw_watch/py/pw_watch/watch.py
@@ -64,11 +64,6 @@ from typing import (
Tuple,
)
-try:
- import httpwatcher # type: ignore[import]
-except ImportError:
- httpwatcher = None
-
from watchdog.events import FileSystemEventHandler # type: ignore[import]
from watchdog.observers import Observer # type: ignore[import]
@@ -106,7 +101,7 @@ _ERRNO_INOTIFY_LIMIT_REACHED = 28
# Suppress events under 'fsevents', generated by watchdog on every file
# event on MacOS.
-# TODO(b/182281481): Fix file ignoring, rather than just suppressing logs
+# TODO: b/182281481 - Fix file ignoring, rather than just suppressing logs
_FSEVENTS_LOG = logging.getLogger('fsevents')
_FSEVENTS_LOG.setLevel(logging.WARNING)
@@ -151,6 +146,12 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
NINJA_BUILD_STEP = re.compile(
r'^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$'
)
+ _FILESYSTEM_EVENTS_THAT_TRIGGER_BUILDS = [
+ 'created',
+ 'modified',
+ 'deleted',
+ 'moved',
+ ]
def __init__( # pylint: disable=too-many-arguments
self,
@@ -230,6 +231,9 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
if event.is_directory:
return
+ if event.event_type not in self._FILESYSTEM_EVENTS_THAT_TRIGGER_BUILDS:
+ return
+
# Collect paths of interest from the event.
paths: List[str] = []
if hasattr(event, 'dest_path'):
@@ -409,7 +413,7 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
self.current_build_errors = recipe.status.error_count
if self.watch_app:
- self.watch_app.redraw_ui()
+ self.watch_app.logs_redraw()
desired_logger = _LOG
if self.separate_logfiles:
@@ -667,7 +671,7 @@ def _simple_docs_server(
) -> Callable[[], None]:
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
- super().__init__(*args, directory=path, **kwargs)
+ super().__init__(*args, directory=str(path), **kwargs)
# Disable logs to stdout
def log_message(
@@ -682,19 +686,6 @@ def _simple_docs_server(
return simple_http_server_thread
-def _httpwatcher_docs_server(
- address: str, port: int, path: Path
-) -> Callable[[], None]:
- def httpwatcher_thread():
- # Disable logs from httpwatcher and deps
- logging.getLogger('httpwatcher').setLevel(logging.CRITICAL)
- logging.getLogger('tornado').setLevel(logging.CRITICAL)
-
- httpwatcher.watch(path, host=address, port=port)
-
- return httpwatcher_thread
-
-
def _serve_docs(
build_dir: Path,
docs_path: Path,
@@ -703,16 +694,8 @@ def _serve_docs(
) -> None:
address = '127.0.0.1'
docs_path = build_dir.joinpath(docs_path.joinpath('html'))
-
- if httpwatcher is not None:
- _LOG.info('Using httpwatcher. Docs will reload when changed.')
- server_thread = _httpwatcher_docs_server(address, port, docs_path)
- else:
- _LOG.info(
- 'Using simple HTTP server. Docs will not reload when changed.'
- )
- _LOG.info('Install httpwatcher and restart for automatic docs reload.')
- server_thread = _simple_docs_server(address, port, docs_path)
+ server_thread = _simple_docs_server(address, port, docs_path)
+ _LOG.info('Serving docs at http://%s:%d', address, port)
# Spin up server in a new thread since it blocks
threading.Thread(None, server_thread, 'pw_docs_server').start()
diff --git a/pw_watch/py/pw_watch/watch_app.py b/pw_watch/py/pw_watch/watch_app.py
index 38514533d..4546d2d53 100644
--- a/pw_watch/py/pw_watch/watch_app.py
+++ b/pw_watch/py/pw_watch/watch_app.py
@@ -57,7 +57,7 @@ from prompt_toolkit.formatted_text import StyleAndTextTuples
from prompt_toolkit.lexers import PygmentsLexer
from pygments.lexers.markup import MarkdownLexer # type: ignore
-from pw_console.console_app import get_default_colordepth
+from pw_console.console_app import get_default_colordepth, MIN_REDRAW_INTERVAL
from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR
from pw_console.help_window import HelpWindow
from pw_console.key_bindings import DEFAULT_KEY_BINDINGS
@@ -364,6 +364,9 @@ class WatchApp(PluginMixin):
],
level_name=level_name,
)
+ # Repeat the Attaching filesystem watcher message for the full screen
+ # interface. The original log in watch.py will be hidden from view.
+ _LOG.info('Attaching filesystem watcher...')
self.window_manager.window_lists[0].display_mode = DisplayMode.TABBED
@@ -535,6 +538,7 @@ class WatchApp(PluginMixin):
),
style_transformation=self.style_transformation,
full_screen=True,
+ min_redraw_interval=MIN_REDRAW_INTERVAL,
)
self.plugin_init(
diff --git a/pw_watch/py/setup.py b/pw_watch/py/setup.py
deleted file mode 100644
index e73a002da..000000000
--- a/pw_watch/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""pw_watch"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_web/BUILD.gn b/pw_web/BUILD.gn
index a4d5ebe10..5069f943c 100644
--- a/pw_web/BUILD.gn
+++ b/pw_web/BUILD.gn
@@ -18,7 +18,10 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "docs.rst",
+ "testing.rst",
+ ]
}
pw_test_group("tests") {
diff --git a/pw_web/docs.rst b/pw_web/docs.rst
index a6eddf98e..d90c22a6a 100644
--- a/pw_web/docs.rst
+++ b/pw_web/docs.rst
@@ -3,7 +3,6 @@
---------
pw_web
---------
-
Pigweed provides an NPM package with modules to build web apps for Pigweed
devices.
@@ -16,26 +15,25 @@ Installation
-------------
If you have a bundler set up, you can install ``pigweedjs`` in your web application by:
-.. code:: bash
+.. code-block:: bash
$ npm install --save pigweedjs
After installing, you can import modules from ``pigweedjs`` in this way:
-.. code:: javascript
+.. code-block:: javascript
import { pw_rpc, pw_tokenizer, Device, WebSerial } from 'pigweedjs';
Import Directly in HTML
^^^^^^^^^^^^^^^^^^^^^^^
-
If you don't want to set up a bundler, you can also load Pigweed directly in
your HTML page by:
-.. code:: html
+.. code-block:: html
- <script src="https://unpkg.com/pigweedjs@0.0.5/dist/index.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
<script>
const { pw_rpc, pw_hdlc, Device, WebSerial } from Pigweed;
</script>
@@ -50,15 +48,15 @@ instructions on how to build the demo and try things out.
``pigweedjs`` provides a ``Device`` API which simplifies common tasks. Here is
an example to connect to device and call ``EchoService.Echo`` RPC service.
-.. code:: html
+.. code-block:: html
<h1>Hello Pigweed</h1>
<button onclick="connect()">Connect</button>
<button onclick="echo()">Echo RPC</button>
<br /><br />
<code></code>
- <script src="https://unpkg.com/pigweedjs@0.0.5/dist/index.umd.js"></script>
- <script src="https://unpkg.com/pigweedjs@0.0.5/dist/protos/collection.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/protos/collection.umd.js"></script>
<script>
const { Device } = Pigweed;
const { ProtoCollection } = PigweedProtoCollection;
@@ -79,14 +77,14 @@ pw_system demo uses ``pw_log_rpc``; an RPC-based logging solution. pw_system
also uses pw_tokenizer to tokenize strings and save device space. Below is an
example that streams logs using the ``Device`` API.
-.. code:: html
+.. code-block:: html
<h1>Hello Pigweed</h1>
<button onclick="connect()">Connect</button>
<br /><br />
<code></code>
- <script src="https://unpkg.com/pigweedjs@0.0.5/dist/index.umd.js"></script>
- <script src="https://unpkg.com/pigweedjs@0.0.5/dist/protos/collection.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/protos/collection.umd.js"></script>
<script>
const { Device, pw_tokenizer } = Pigweed;
const { ProtoCollection } = PigweedProtoCollection;
@@ -110,7 +108,7 @@ example that streams logs using the ``Device`` API.
The above example requires a token database in CSV format. You can generate one
from the pw_system's ``.elf`` file by running:
-.. code:: bash
+.. code-block:: bash
$ pw_tokenizer/py/pw_tokenizer/database.py create \
--database db.csv out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf
@@ -149,12 +147,11 @@ Device has following public API:
WebSerialTransport
------------------
-
To help with connecting to WebSerial and listening for serial data, a helper
class is also included under ``WebSerial.WebSerialTransport``. Here is an
example usage:
-.. code:: javascript
+.. code-block:: javascript
import { WebSerial, pw_hdlc } from 'pigweedjs';
@@ -164,6 +161,9 @@ example usage:
// Present device selection prompt to user
await transport.connect();
+ // Or connect to an existing `SerialPort`
+ // await transport.connectPort(port);
+
// Listen and decode HDLC frames
transport.chunks.subscribe((item) => {
const decoded = decoder.process(item);
@@ -175,6 +175,8 @@ example usage:
}
});
+ // Later, close all streams and close the port.
+ transport.disconnect();
Individual Modules
==================
@@ -187,13 +189,133 @@ Following Pigweed modules are included in the NPM package:
Web Console
===========
-
Pigweed includes a web console that demonstrates `pigweedjs` usage in a
React-based web app. Web console includes a log viewer and a REPL that supports
autocomplete. Here's how to run the web console locally:
-.. code:: bash
+.. code-block:: bash
$ cd pw_web/webconsole
$ npm install
$ npm run dev
+
+Log viewer component
+====================
+The NPM package also includes a log viewer component that can be embedded in any
+webapp. The component works with Pigweed's RPC stack out-of-the-box but also
+supports defining your own log source.
+
+The component is composed of the component itself and a log source. Here is a
+simple example app that uses a mock log source:
+
+.. code-block:: html
+
+ <div id="log-viewer-container"></div>
+ <script src="https://unpkg.com/pigweedjs/dist/logging.umd.js"></script>
+ <script>
+
+ const { createLogViewer, MockLogSource } = PigweedLogging;
+ const logSource = new MockLogSource();
+ const containerEl = document.querySelector(
+ '#log-viewer-container'
+ );
+
+ let unsubscribe = createLogViewer(logSource, containerEl);
+ logSource.start(); // Start producing mock logs
+
+ </script>
+
+The code above will render a working log viewer that just streams mock
+log entries.
+
+It also comes with an RPC log source with support for detokenization. Here is an
+example app using that:
+
+.. code-block:: html
+
+ <div id="log-viewer-container"></div>
+ <script src="https://unpkg.com/pigweedjs/dist/index.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/protos/collection.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs/dist/logging.umd.js"></script>
+ <script>
+
+ const { Device, pw_tokenizer } = Pigweed;
+ const { ProtoCollection } = PigweedProtoCollection;
+ const { createLogViewer, PigweedRPCLogSource } = PigweedLogging;
+
+ const device = new Device(new ProtoCollection());
+ const logSource = new PigweedRPCLogSource(device, "CSV TOKEN DB HERE");
+ const containerEl = document.querySelector(
+ '#log-viewer-container'
+ );
+
+ let unsubscribe = createLogViewer(logSource, containerEl);
+
+ </script>
+
+Custom Log Source
+-----------------
+You can define a custom log source that works with the log viewer component by
+just extending the abstract `LogSource` class and emitting the `logEntry` events
+like this:
+
+.. code-block:: typescript
+
+ import { LogSource, LogEntry, Severity } from 'pigweedjs/logging';
+
+ export class MockLogSource extends LogSource {
+ constructor(){
+ super();
+ // Do any initializations here
+ // ...
+ // Then emit logs
+ const log1: LogEntry = {
+
+ }
+ this.emitEvent('logEntry', {
+ severity: Severity.INFO,
+ timestamp: new Date(),
+ fields: [
+ { key: 'severity', value: severity }
+ { key: 'timestamp', value: new Date().toISOString() },
+ { key: 'source', value: "LEFT SHOE" },
+ { key: 'message', value: "Running mode activated." }
+ ]
+ });
+ }
+ }
+
+After this, you just need to pass your custom log source object
+to `createLogViewer()`. See implementation of
+`PigweedRPCLogSource <https://cs.opensource.google/pigweed/pigweed/+/main:ts/logging_source_rpc.ts>`_
+for reference.
+
+Color Scheme
+------------
+The log viewer web component provides the ability to set the color scheme manually, overriding any default or system preferences.
+
+To set the color scheme, first obtain a reference to the ``log-viewer`` element in the DOM. A common way to do this is by using ``querySelector()``:
+
+.. code-block:: javascript
+
+ const logViewer = document.querySelector('log-viewer');
+
+You can then set the color scheme dynamically by updating the component's `colorScheme` property or by setting a value for the `colorscheme` HTML attribute.
+
+.. code-block:: javascript
+
+ logViewer.colorScheme = 'dark';
+
+.. code-block:: javascript
+
+ logViewer.setAttribute('colorscheme', 'dark');
+
+The color scheme can be set to ``'dark'``, ``'light'``, or the default ``'auto'`` which allows the component to adapt to the preferences in the operating system settings.
+
+Guides
+======
+
+.. toctree::
+ :maxdepth: 1
+
+ testing
diff --git a/pw_web/log-viewer/.gitignore b/pw_web/log-viewer/.gitignore
new file mode 100644
index 000000000..b8214fe6d
--- /dev/null
+++ b/pw_web/log-viewer/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+*.tsbuildinfo
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Generated files
+src/assets/filters.js \ No newline at end of file
diff --git a/pw_web/log-viewer/README.md b/pw_web/log-viewer/README.md
new file mode 100644
index 000000000..10862ee77
--- /dev/null
+++ b/pw_web/log-viewer/README.md
@@ -0,0 +1,52 @@
+# Log Viewer
+
+An embeddable log-viewing web component that is customizable and extensible for use in various developer contexts.
+
+## Installation
+
+1. Clone the main Pigweed repository:
+
+```
+git clone https://pigweed.googlesource.com/pigweed/pigweed
+```
+
+2. Navigate to the project directory:
+
+```
+cd pigweed
+```
+
+3. Install the necessary dependencies:
+
+```
+source bootstrap.sh
+```
+
+## Build
+
+1. Navigate to the project directory:
+
+```
+cd pigweed/pigweed_web/log-viewer
+```
+
+
+2. run the following command at the root of the project:
+
+```
+npm run build
+```
+
+This will generate the compiled files used in the application.
+
+## Development Mode
+
+To run the application in development mode, use the following command:
+
+```
+npm run dev -- --host
+```
+
+This will start the development server and launch the application. The `--host` flag is optional and can be used to specify the host address.
+
+## Usage \ No newline at end of file
diff --git a/pw_web/log-viewer/index.html b/pw_web/log-viewer/index.html
new file mode 100644
index 000000000..b580bd285
--- /dev/null
+++ b/pw_web/log-viewer/index.html
@@ -0,0 +1,36 @@
+<!--
+Copyright 2023 The Pigweed Authors
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not
+use this file except in compliance with the License. You may obtain a copy of
+the License at
+
+ https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations under
+the License.
+-->
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Log Viewer</title>
+ <link rel="stylesheet" href="./src/index.css" />
+ <link rel="icon" href="src/assets/favicon.svg" />
+ <script type="module" src="./src/index.ts"></script>
+ <script
+ type="module"
+ src="/src/components/log-viewer.ts"
+ ></script>
+ </head>
+
+ <body>
+ <main id="log-viewer-container">
+ </main>
+ </body>
+</html> \ No newline at end of file
diff --git a/pw_web/log-viewer/package-lock.json b/pw_web/log-viewer/package-lock.json
new file mode 100644
index 000000000..cc4b79624
--- /dev/null
+++ b/pw_web/log-viewer/package-lock.json
@@ -0,0 +1,4892 @@
+{
+ "name": "log-viewer",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "log-viewer",
+ "version": "0.0.0",
+ "dependencies": {
+ "@lit-labs/virtualizer": "^2.0.7",
+ "@material/web": "^1.0.0-pre.16",
+ "lit": "^3.0.0-pre.0",
+ "prettier-plugin-jsdoc": "^0.4.2"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.59.7",
+ "eslint": "^8.41.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-config-standard-with-typescript": "^34.0.1",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-lit": "^1.8.3",
+ "eslint-plugin-lit-a11y": "^1.1.0-next.1",
+ "eslint-plugin-n": "^15.7.0",
+ "eslint-plugin-promise": "^6.1.1",
+ "prettier": "^2.8.8",
+ "typescript": "^5.0.4",
+ "vite": "^4.3.2"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.12.11",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz",
+ "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz",
+ "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz",
+ "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.5",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.21.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz",
+ "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==",
+ "dev": true,
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/runtime-corejs3": {
+ "version": "7.21.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.21.5.tgz",
+ "integrity": "sha512-FRqFlFKNazWYykft5zvzuEl1YyTDGsIRrjV9rvxvYkUC7W/ueBng1X68Xd6uRMzAaJ0xMKn08/wem5YS1lpX8w==",
+ "dev": true,
+ "dependencies": {
+ "core-js-pure": "^3.25.1",
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz",
+ "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz",
+ "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz",
+ "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz",
+ "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz",
+ "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz",
+ "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz",
+ "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz",
+ "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz",
+ "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz",
+ "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz",
+ "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz",
+ "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz",
+ "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz",
+ "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz",
+ "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz",
+ "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz",
+ "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz",
+ "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz",
+ "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz",
+ "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz",
+ "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz",
+ "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
+ "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
+ "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.5.2",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.42.0.tgz",
+ "integrity": "sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+ "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
+ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
+ "dev": true
+ },
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.1.2-pre.0",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2-pre.0.tgz",
+ "integrity": "sha512-3FSKQV90k20guBluMFzd9paVuzZTxeL1vDuqNc8SbwpiCmZkY7CJH7HH4HVD35D4hU8d8JTKKL51eXRWzXBZXQ=="
+ },
+ "node_modules/@lit-labs/virtualizer": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@lit-labs/virtualizer/-/virtualizer-2.0.7.tgz",
+ "integrity": "sha512-WWkI31QsSVoJZRuFo3sMn28LgSf2MYL7gp6OG2ZNksr2EoA3D8igNGp49cajI8HGHurSK4jGpMN2LHZrmy+Cxw==",
+ "dependencies": {
+ "lit": "^2.8.0",
+ "tslib": "^2.0.3"
+ }
+ },
+ "node_modules/@lit-labs/virtualizer/node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz",
+ "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ=="
+ },
+ "node_modules/@lit-labs/virtualizer/node_modules/@lit/reactive-element": {
+ "version": "1.6.3",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
+ "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.0.0"
+ }
+ },
+ "node_modules/@lit-labs/virtualizer/node_modules/lit": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
+ "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.6.0",
+ "lit-element": "^3.3.0",
+ "lit-html": "^2.8.0"
+ }
+ },
+ "node_modules/@lit-labs/virtualizer/node_modules/lit-element": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
+ "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.1.0",
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.8.0"
+ }
+ },
+ "node_modules/@lit-labs/virtualizer/node_modules/lit-html": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
+ "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "2.0.0-pre.0",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.0-pre.0.tgz",
+ "integrity": "sha512-Buhl2svhltEDA+4hkVaABgmpCWpBcMiU1emOisA/izBXkAxh69Qgqrb7aZajBijex9pvFI7HK4SrM+/IDT0YFQ==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.1.2-pre.0"
+ }
+ },
+ "node_modules/@material/web": {
+ "version": "1.0.0-pre.16",
+ "resolved": "https://registry.npmjs.org/@material/web/-/web-1.0.0-pre.16.tgz",
+ "integrity": "sha512-BDrwtY7WkbGhh2jxupbRdh1/wEiy69h6XzjgCxISJNNQVyn6rbOv1U57+d9J24Z5DvSTKv8NHrNyXT6iwmG2KQ==",
+ "dependencies": {
+ "lit": "^2.7.4",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@material/web/node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.1.tgz",
+ "integrity": "sha512-kXOeFbfCm4fFf2A3WwVEeQj55tMZa8c8/f9AKHMobQMkzNUfUj+antR3fRPaZJawsa1aZiP/Da3ndpZrwEe4rQ=="
+ },
+ "node_modules/@material/web/node_modules/@lit/reactive-element": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.2.tgz",
+ "integrity": "sha512-rDfl+QnCYjuIGf5xI2sVJWdYIi56CTCwWa+nidKYX6oIuBYwUbT/vX4qbUDlHiZKJ/3FRNQ/tWJui44p6/stSA==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.0.0"
+ }
+ },
+ "node_modules/@material/web/node_modules/lit": {
+ "version": "2.7.5",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-2.7.5.tgz",
+ "integrity": "sha512-i/cH7Ye6nBDUASMnfwcictBnsTN91+aBjXoTHF2xARghXScKxpD4F4WYI+VLXg9lqbMinDfvoI7VnZXjyHgdfQ==",
+ "dependencies": {
+ "@lit/reactive-element": "^1.6.0",
+ "lit-element": "^3.3.0",
+ "lit-html": "^2.7.0"
+ }
+ },
+ "node_modules/@material/web/node_modules/lit-element": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.2.tgz",
+ "integrity": "sha512-xXAeVWKGr4/njq0rGC9dethMnYCq5hpKYrgQZYTzawt9YQhMiXfD+T1RgrdY3NamOxwq2aXlb0vOI6e29CKgVQ==",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.1.0",
+ "@lit/reactive-element": "^1.3.0",
+ "lit-html": "^2.7.0"
+ }
+ },
+ "node_modules/@material/web/node_modules/lit-html": {
+ "version": "2.7.4",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.7.4.tgz",
+ "integrity": "sha512-/Jw+FBpeEN+z8X6PJva5n7+0MzCVAH2yypN99qHYYkq8bI+j7I39GH+68Z/MZD6rGKDK9RpzBw7CocfmHfq6+g==",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.8",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz",
+ "integrity": "sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.11",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
+ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
+ "dev": true
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@types/mdast": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.11.tgz",
+ "integrity": "sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==",
+ "dependencies": {
+ "@types/unist": "*"
+ }
+ },
+ "node_modules/@types/ms": {
+ "version": "0.7.31",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
+ "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
+ },
+ "node_modules/@types/node": {
+ "version": "20.2.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.1.tgz",
+ "integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==",
+ "dev": true
+ },
+ "node_modules/@types/parse5": {
+ "version": "2.2.34",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-2.2.34.tgz",
+ "integrity": "sha512-p3qOvaRsRpFyEmaS36RtLzpdxZZnmxGuT1GMgzkTtTJVFuEw7KFjGK83MFODpJExgX1bEzy9r0NYjMC3IMfi7w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/semver": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz",
+ "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
+ "dev": true
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
+ "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
+ },
+ "node_modules/@types/unist": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
+ "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.7.tgz",
+ "integrity": "sha512-BL+jYxUFIbuYwy+4fF86k5vdT9lT0CNJ6HtwrIvGh0PhH8s0yy5rjaKH2fDCrz5ITHy07WCzVGNvAmjJh4IJFA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.4.0",
+ "@typescript-eslint/scope-manager": "5.59.7",
+ "@typescript-eslint/type-utils": "5.59.7",
+ "@typescript-eslint/utils": "5.59.7",
+ "debug": "^4.3.4",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "natural-compare-lite": "^1.4.0",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^5.0.0",
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz",
+ "integrity": "sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "@typescript-eslint/visitor-keys": "5.59.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz",
+ "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz",
+ "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.59.6",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.6.tgz",
+ "integrity": "sha512-7pCa6al03Pv1yf/dUg/s1pXz/yGMUBAw5EeWqNTFiSueKvRNonze3hma3lhdsOrQcaOXhbk5gKu2Fludiho9VA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.59.6",
+ "@typescript-eslint/types": "5.59.6",
+ "@typescript-eslint/typescript-estree": "5.59.6",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.59.6",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.6.tgz",
+ "integrity": "sha512-gLbY3Le9Dxcb8KdpF0+SJr6EQ+hFGYFl6tVY8VxLPFDfUZC7BHFw+Vq7bM5lE9DwWPfx4vMWWTLGXgpc0mAYyQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.6",
+ "@typescript-eslint/visitor-keys": "5.59.6"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.7.tgz",
+ "integrity": "sha512-ozuz/GILuYG7osdY5O5yg0QxXUAEoI4Go3Do5xeu+ERH9PorHBPSdvD3Tjp2NN2bNLh1NJQSsQu2TPu/Ly+HaQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/typescript-estree": "5.59.7",
+ "@typescript-eslint/utils": "5.59.7",
+ "debug": "^4.3.4",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz",
+ "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz",
+ "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "@typescript-eslint/visitor-keys": "5.59.7",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz",
+ "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.59.6",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.6.tgz",
+ "integrity": "sha512-tH5lBXZI7T2MOUgOWFdVNUILsI02shyQvfzG9EJkoONWugCG77NDDa1EeDGw7oJ5IvsTAAGVV8I3Tk2PNu9QfA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.59.6",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.6.tgz",
+ "integrity": "sha512-vW6JP3lMAs/Tq4KjdI/RiHaaJSO7IUsbkz17it/Rl9Q+WkQ77EOuOnlbaU8kKfVIOJxMhnRiBG+olE7f3M16DA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.6",
+ "@typescript-eslint/visitor-keys": "5.59.6",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.7.tgz",
+ "integrity": "sha512-yCX9WpdQKaLufz5luG4aJbOpdXf/fjwGMcLFXZVPUz3QqLirG5QcwwnIHNf8cjLjxK4qtzTO8udUtMQSAToQnQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.59.7",
+ "@typescript-eslint/types": "5.59.7",
+ "@typescript-eslint/typescript-estree": "5.59.7",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.7.tgz",
+ "integrity": "sha512-FL6hkYWK9zBGdxT2wWEd2W8ocXMu3K94i3gvMrjXpx+koFYdYV7KprKfirpgY34vTGzEPPuKoERpP8kD5h7vZQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "@typescript-eslint/visitor-keys": "5.59.7"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.7.tgz",
+ "integrity": "sha512-UnVS2MRRg6p7xOSATscWkKjlf/NDKuqo5TdbWck6rIRZbmKpVNTLALzNvcjIfHBE7736kZOFc/4Z3VcZwuOM/A==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.7.tgz",
+ "integrity": "sha512-4A1NtZ1I3wMN2UGDkU9HMBL+TIQfbrh4uS0WDMMpf3xMRursDbqEf1ahh6vAAe3mObt8k3ZATnezwG4pdtWuUQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "@typescript-eslint/visitor-keys": "5.59.7",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.59.7",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.7.tgz",
+ "integrity": "sha512-tyN+X2jvMslUszIiYbF0ZleP+RqQsFVpGrKI6e0Eet1w8WmhsAtmzaqm8oM8WJQ1ysLwhnsK/4hYHJjOgJVfQQ==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.7",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.59.6",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.6.tgz",
+ "integrity": "sha512-zEfbFLzB9ETcEJ4HZEEsCR9HHeNku5/Qw1jSS5McYJv5BR+ftYXwFFAH5Al+xkGaZEqowMwl7uoJjQb1YSPF8Q==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.59.6",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.8.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
+ "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true
+ },
+ "node_modules/aria-query": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz",
+ "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.10.2",
+ "@babel/runtime-corejs3": "^7.10.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz",
+ "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz",
+ "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
+ "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz",
+ "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.1.tgz",
+ "integrity": "sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
+ "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/binary-searching": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/binary-searching/-/binary-searching-2.0.5.tgz",
+ "integrity": "sha512-v4N2l3RxL+m4zDxyxz3Ne2aTmiPn8ZUpKFpdPtO+ItW1NcTCXA7JeHG5GMBSvoKSkQZ9ycS+EouDVxYB9ufKWA=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/builtins": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
+ "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.0.0"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/character-entities": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/comment-parser": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz",
+ "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==",
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/core-js-pure": {
+ "version": "3.30.2",
+ "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.30.2.tgz",
+ "integrity": "sha512-p/npFUJXXBkCCTIlEGBdghofn00jWG6ZOtdoIXSJmAu2QBvN0IqpZXWweOytcwE6cfx8ZvVUy1vw8zxhe4Y2vg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decode-named-character-reference": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
+ "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==",
+ "dependencies": {
+ "character-entities": "^2.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
+ "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
+ "dev": true,
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom5": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dom5/-/dom5-3.0.1.tgz",
+ "integrity": "sha512-JPFiouQIr16VQ4dX6i0+Hpbg3H2bMKPmZ+WZgBOSSvOPx9QHwwY8sPzeM2baUtViESYto6wC2nuZOMC/6gulcA==",
+ "dev": true,
+ "dependencies": {
+ "@types/parse5": "^2.2.34",
+ "clone": "^2.1.0",
+ "parse5": "^4.0.0"
+ }
+ },
+ "node_modules/dom5/node_modules/parse5": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
+ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==",
+ "dev": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true
+ },
+ "node_modules/enquirer": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
+ "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
+ "dev": true,
+ "dependencies": {
+ "ansi-colors": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.21.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz",
+ "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.0",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz",
+ "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz",
+ "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.18",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.18.tgz",
+ "integrity": "sha512-z1lix43jBs6UKjcZVKOw2xx69ffE2aG0PygLL5qJ9OS/gy0Ewd1gW/PUQIOIQGXBHWNywSc0floSKoMFF8aK2w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.18",
+ "@esbuild/android-arm64": "0.17.18",
+ "@esbuild/android-x64": "0.17.18",
+ "@esbuild/darwin-arm64": "0.17.18",
+ "@esbuild/darwin-x64": "0.17.18",
+ "@esbuild/freebsd-arm64": "0.17.18",
+ "@esbuild/freebsd-x64": "0.17.18",
+ "@esbuild/linux-arm": "0.17.18",
+ "@esbuild/linux-arm64": "0.17.18",
+ "@esbuild/linux-ia32": "0.17.18",
+ "@esbuild/linux-loong64": "0.17.18",
+ "@esbuild/linux-mips64el": "0.17.18",
+ "@esbuild/linux-ppc64": "0.17.18",
+ "@esbuild/linux-riscv64": "0.17.18",
+ "@esbuild/linux-s390x": "0.17.18",
+ "@esbuild/linux-x64": "0.17.18",
+ "@esbuild/netbsd-x64": "0.17.18",
+ "@esbuild/openbsd-x64": "0.17.18",
+ "@esbuild/sunos-x64": "0.17.18",
+ "@esbuild/win32-arm64": "0.17.18",
+ "@esbuild/win32-ia32": "0.17.18",
+ "@esbuild/win32-x64": "0.17.18"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.42.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.42.0.tgz",
+ "integrity": "sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.4.0",
+ "@eslint/eslintrc": "^2.0.3",
+ "@eslint/js": "8.42.0",
+ "@humanwhocodes/config-array": "^0.11.10",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.0",
+ "eslint-visitor-keys": "^3.4.1",
+ "espree": "^9.5.2",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz",
+ "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-config-standard": {
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz",
+ "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "peerDependencies": {
+ "eslint": "^8.0.1",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.0.0",
+ "eslint-plugin-promise": "^6.0.0"
+ }
+ },
+ "node_modules/eslint-config-standard-with-typescript": {
+ "version": "34.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-34.0.1.tgz",
+ "integrity": "sha512-J7WvZeLtd0Vr9F+v4dZbqJCLD16cbIy4U+alJMq4MiXdpipdBM3U5NkXaGUjePc4sb1ZE01U9g6VuTBpHHz1fg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/parser": "^5.43.0",
+ "eslint-config-standard": "17.0.0"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.43.0",
+ "eslint": "^8.0.1",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.0.0",
+ "eslint-plugin-promise": "^6.0.0",
+ "typescript": "*"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz",
+ "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
+ "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-es": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz",
+ "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==",
+ "dev": true,
+ "dependencies": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.26.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
+ "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.4",
+ "array.prototype.flat": "^1.2.5",
+ "debug": "^2.6.9",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-module-utils": "^2.7.3",
+ "has": "^1.0.3",
+ "is-core-module": "^2.8.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.5",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "dev": true
+ },
+ "node_modules/eslint-plugin-lit": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lit/-/eslint-plugin-lit-1.8.3.tgz",
+ "integrity": "sha512-wmeYfBnWPUChbdZagOhG519gaWz9Q7OGT/nCx3YVHuCCrW9q9u0p/IQueQeoaMojUqOSgM/22oSDOaBruYGqag==",
+ "dev": true,
+ "dependencies": {
+ "parse5": "^6.0.1",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "requireindex": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ },
+ "peerDependencies": {
+ "eslint": ">= 5"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y": {
+ "version": "1.1.0-next.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-lit-a11y/-/eslint-plugin-lit-a11y-1.1.0-next.1.tgz",
+ "integrity": "sha512-OAv0W84maDyHs22/aXIFcc94SnoN0jnfUqNgxyJ1LMrdiMNQtkjTQa4Yq7g/IA/0Nf/TTGBRRjMP7YOIXBOGnA==",
+ "dev": true,
+ "dependencies": {
+ "aria-query": "^4.2.2",
+ "axe-core": "^4.3.3",
+ "axobject-query": "^2.2.0",
+ "dom5": "^3.0.1",
+ "emoji-regex": "^9.2.0",
+ "eslint": "^7.6.0",
+ "eslint-rule-extender": "0.0.1",
+ "intl-list-format": "^1.0.3",
+ "parse5": "^5.1.1",
+ "parse5-htmlparser2-tree-adapter": "^6.0.1",
+ "requireindex": "~1.2.0"
+ },
+ "peerDependencies": {
+ "eslint": ">= 5"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/@eslint/eslintrc": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+ "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+ "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/acorn": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
+ "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/eslint": {
+ "version": "7.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/espree": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
+ "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/ignore": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/eslint-plugin-lit-a11y/node_modules/parse5": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+ "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+ "dev": true
+ },
+ "node_modules/eslint-plugin-n": {
+ "version": "15.7.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.7.0.tgz",
+ "integrity": "sha512-jDex9s7D/Qial8AGVIHq4W7NswpUD5DPDL2RH8Lzd9EloWUuvUkHfv4FRLMipH5q2UtyurorBkPeNi1wVWNh3Q==",
+ "dev": true,
+ "dependencies": {
+ "builtins": "^5.0.1",
+ "eslint-plugin-es": "^4.1.0",
+ "eslint-utils": "^3.0.0",
+ "ignore": "^5.1.1",
+ "is-core-module": "^2.11.0",
+ "minimatch": "^3.1.2",
+ "resolve": "^1.22.1",
+ "semver": "^7.3.8"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-n/node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-plugin-n/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-plugin-promise": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/eslint-rule-extender": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-rule-extender/-/eslint-rule-extender-0.0.1.tgz",
+ "integrity": "sha512-F0j1Twve3lamL3J0rRSVAynlp58sDPG39JFcQrM+u9Na7PmCgiPHNODh6YE9mduaGcsn3NBqbf6LZRj0cLr8Ng==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kaicataldo"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
+ "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
+ "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
+ "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
+ "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.5.2",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
+ "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.12",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
+ "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==",
+ "dev": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
+ "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
+ "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
+ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
+ "dev": true
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
+ "dev": true
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+ "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+ "dev": true
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz",
+ "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.20.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
+ "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
+ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+ "dev": true
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "dev": true,
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+ "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz",
+ "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
+ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz",
+ "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/intl-list-format": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/intl-list-format/-/intl-list-format-1.0.3.tgz",
+ "integrity": "sha512-VNF1Mh0K1xALXkz/5QsK1gfKRvEQO/jWaniTGAzQvbzGr5uyGDskQrRjnf6Qnbc9/JRbNE8BQtTg6iWuFrZorw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz",
+ "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.12.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz",
+ "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==",
+ "dev": true,
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+ "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz",
+ "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lit": {
+ "version": "3.0.0-pre.0",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.0.0-pre.0.tgz",
+ "integrity": "sha512-dW3TR4LhMcc7bE4QYfzC2AWGMzpmUmYLTok537nFcmHHoNO9eARbA7bi5eQmvEtWhySbRQ1SFHSPvp7bTFpcHw==",
+ "dependencies": {
+ "@lit/reactive-element": "^2.0.0-pre.0",
+ "lit-element": "^4.0.0-pre.0",
+ "lit-html": "^3.0.0-pre.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.0.0-pre.0",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.0-pre.0.tgz",
+ "integrity": "sha512-+FoW5lK5EBvEqJIK5XWYgl5uW5LpqzxCymLIS1ra9XnDEqIIA4kDlkkhTg2VBBVHAHkCFxCn6KVPr/miFQreMQ==",
+ "dependencies": {
+ "@lit/reactive-element": "^2.0.0-pre.0",
+ "lit-html": "^3.0.0-pre.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.0.0-pre.0",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.0.0-pre.0.tgz",
+ "integrity": "sha512-9rpjrE/l0WaJznK2A/IeR7lV0doCeC8HABfK46iE90O/9awaNKsFYB7Rhj3WSe94nJ7WBNOYT3TBPINgUMx3lw==",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/mdast-util-from-markdown": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz",
+ "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0",
+ "@types/unist": "^2.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "mdast-util-to-string": "^3.1.0",
+ "micromark": "^3.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-decode-string": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "unist-util-stringify-position": "^3.0.0",
+ "uvu": "^0.5.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/mdast-util-to-string": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz",
+ "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==",
+ "dependencies": {
+ "@types/mdast": "^3.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromark": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz",
+ "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "@types/debug": "^4.0.0",
+ "debug": "^4.0.0",
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-core-commonmark": "^1.0.1",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-combine-extensions": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-sanitize-uri": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-core-commonmark": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz",
+ "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-factory-destination": "^1.0.0",
+ "micromark-factory-label": "^1.0.0",
+ "micromark-factory-space": "^1.0.0",
+ "micromark-factory-title": "^1.0.0",
+ "micromark-factory-whitespace": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-classify-character": "^1.0.0",
+ "micromark-util-html-tag-name": "^1.0.0",
+ "micromark-util-normalize-identifier": "^1.0.0",
+ "micromark-util-resolve-all": "^1.0.0",
+ "micromark-util-subtokenize": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.1",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-factory-destination": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz",
+ "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-factory-label": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz",
+ "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-factory-space": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz",
+ "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-factory-title": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz",
+ "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-factory-whitespace": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz",
+ "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-factory-space": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-character": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz",
+ "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-chunked": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz",
+ "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-classify-character": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz",
+ "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-combine-extensions": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz",
+ "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-numeric-character-reference": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz",
+ "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-decode-string": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz",
+ "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "decode-named-character-reference": "^1.0.0",
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-decode-numeric-character-reference": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-encode": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz",
+ "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-html-tag-name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz",
+ "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-normalize-identifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz",
+ "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-resolve-all": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz",
+ "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-types": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-sanitize-uri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz",
+ "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-character": "^1.0.0",
+ "micromark-util-encode": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0"
+ }
+ },
+ "node_modules/micromark-util-subtokenize": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz",
+ "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ],
+ "dependencies": {
+ "micromark-util-chunked": "^1.0.0",
+ "micromark-util-symbol": "^1.0.0",
+ "micromark-util-types": "^1.0.0",
+ "uvu": "^0.5.0"
+ }
+ },
+ "node_modules/micromark-util-symbol": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz",
+ "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromark-util-types": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz",
+ "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==",
+ "funding": [
+ {
+ "type": "GitHub Sponsors",
+ "url": "https://github.com/sponsors/unifiedjs"
+ },
+ {
+ "type": "OpenCollective",
+ "url": "https://opencollective.com/unified"
+ }
+ ]
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mri": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/natural-compare-lite": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz",
+ "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==",
+ "dev": true
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+ "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
+ "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+ "dev": true
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
+ "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
+ "dev": true,
+ "dependencies": {
+ "parse5": "^6.0.1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.23.tgz",
+ "integrity": "sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.8.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
+ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/prettier-plugin-jsdoc": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-jsdoc/-/prettier-plugin-jsdoc-0.4.2.tgz",
+ "integrity": "sha512-w2jnAQm3z0GAG0bhzVJeehzDtrhGMSxJjit5ApCc2oxWfc7+jmLAkbtdOXaSpfwZz3IWkk+PiQPeRrLNpbM+Mw==",
+ "dependencies": {
+ "binary-searching": "^2.0.5",
+ "comment-parser": "^1.3.1",
+ "mdast-util-from-markdown": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "prettier": ">=2.1.2"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
+ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "dev": true
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
+ "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
+ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requireindex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz",
+ "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.5"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.2",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz",
+ "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==",
+ "dev": true,
+ "dependencies": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
+ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
+ "dev": true,
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "3.21.5",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.21.5.tgz",
+ "integrity": "sha512-a4NTKS4u9PusbUJcfF4IMxuqjFzjm6ifj76P54a7cKnvVzJaG12BLVR+hgU2YDGHzyMMQNxLAZWuALsn8q2oQg==",
+ "dev": true,
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/sade": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
+ "dependencies": {
+ "mri": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+ "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.5.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz",
+ "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
+ "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz",
+ "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz",
+ "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/table": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.2",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
+ "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz",
+ "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w=="
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "dev": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
+ "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
+ "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=12.20"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unist-util-stringify-position": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz",
+ "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==",
+ "dependencies": {
+ "@types/unist": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/uvu": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz",
+ "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==",
+ "dependencies": {
+ "dequal": "^2.0.0",
+ "diff": "^5.0.0",
+ "kleur": "^4.0.3",
+ "sade": "^1.7.3"
+ },
+ "bin": {
+ "uvu": "bin.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
+ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
+ "dev": true
+ },
+ "node_modules/vite": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
+ "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
+ "dev": true,
+ "dependencies": {
+ "esbuild": "^0.17.5",
+ "postcss": "^8.4.23",
+ "rollup": "^3.21.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz",
+ "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/pw_web/log-viewer/package.json b/pw_web/log-viewer/package.json
new file mode 100644
index 000000000..82627ac52
--- /dev/null
+++ b/pw_web/log-viewer/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "log-viewer",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build --base=/asset/5b095937-1e59-52cf-9255-eb8577d1ff83/",
+ "preview": "vite preview",
+ "lint": "eslint --max-warnings=0 src"
+ },
+ "dependencies": {
+ "@lit-labs/virtualizer": "^2.0.7",
+ "@material/web": "^1.0.0-pre.16",
+ "lit": "^3.0.0-pre.0",
+ "prettier-plugin-jsdoc": "^0.4.2"
+ },
+ "devDependencies": {
+ "@typescript-eslint/eslint-plugin": "^5.59.7",
+ "eslint": "^8.41.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-config-standard-with-typescript": "^34.0.1",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-lit": "^1.8.3",
+ "eslint-plugin-lit-a11y": "^1.1.0-next.1",
+ "eslint-plugin-n": "^15.7.0",
+ "eslint-plugin-promise": "^6.1.1",
+ "prettier": "^2.8.8",
+ "typescript": "^5.0.4",
+ "vite": "^4.3.2"
+ }
+}
diff --git a/pw_web/log-viewer/src/assets/favicon.svg b/pw_web/log-viewer/src/assets/favicon.svg
new file mode 100644
index 000000000..4ebaf2a48
--- /dev/null
+++ b/pw_web/log-viewer/src/assets/favicon.svg
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="82.519623"
+ height="96"
+ viewBox="0 0 21.833318 25.4"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+ sodipodi:docname="pw-logo.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ showgrid="true"
+ inkscape:zoom="5.1754899"
+ inkscape:cx="34.29627"
+ inkscape:cy="64.824781"
+ inkscape:window-width="2131"
+ inkscape:window-height="1334"
+ inkscape:window-x="1307"
+ inkscape:window-y="72"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer1"
+ inkscape:pageshadow="2"
+ fit-margin-top="0"
+ fit-margin-left="0.25981"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ width="96px">
+ <inkscape:grid
+ type="xygrid"
+ id="grid899"
+ originx="-0.29938562"
+ originy="0.007612793" />
+ </sodipodi:namedview>
+ <defs
+ id="defs2" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-0.29938553,0.00761281)">
+ <g
+ id="g934"
+ transform="matrix(6.0036869,0,0,6.0036869,-1.8419922,0.03809209)">
+ <path
+ style="fill:#f100f7;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.1616449,3.4336061 C 1.9091824,3.5326629 1.5875433,1.5193339 1.8309272,0.85077532 1.9385896,0.55503414 2.031071,0.2463175 2.4114224,-0.00401171 2.8544419,-0.05203268 2.6565454,0.38084903 2.5067113,1.256888 2.3568774,2.1329272 2.1616453,3.4336062 2.1616449,3.4336061 Z"
+ id="path2515"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#b700d7;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.2191106,3.4109307 C 2.0678012,3.6359974 2.0167095,3.0174321 1.7528477,2.3566876 1.4889859,1.6959431 0.94922328,2.1927191 1.0001916,1.7425692 1.1506181,1.5574268 1.8838262,1.5874003 2.2588512,2.3931613 2.6338765,3.1989223 2.219111,3.4109305 2.2191106,3.4109307 Z"
+ id="path2413"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#951798;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.1237722,3.5148295 C 2.3150351,3.7071003 2.3599863,3.0564818 2.6763817,2.4192218 2.9059426,1.9568575 3.3825927,1.9923461 3.1850672,1.6011417 2.9975756,1.4621979 2.4269145,1.8007494 2.2115779,2.6630282 1.9962408,3.525307 2.1237717,3.5148295 2.1237722,3.5148295 Z"
+ id="path2513"
+ sodipodi:nodetypes="cscsc"
+ inkscape:transform-center-x="-0.045186222"
+ inkscape:transform-center-y="-0.11748418" />
+ <path
+ style="fill:#b700d7;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.16657,3.1630056 C 1.935386,3.3047925 2.3927779,2.3796051 2.1943537,1.717073 2.0826164,1.3439858 1.8924319,1.040618 2.0299315,0.81735074 2.252724,0.72377787 2.5506558,1.2084909 2.5567396,2.0972302 2.5628236,2.9859696 2.1665704,3.1630056 2.16657,3.1630056 Z"
+ id="path2517"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#fb71fe;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.1694107,3.3555634 C 1.972559,3.5421082 1.8165702,2.9074538 1.703537,2.2050082 1.5905038,1.5025626 1.0219277,1.2858513 1.2514214,1.001359 1.637517,0.63319073 1.9169663,1.6332865 2.1067716,2.5015425 c 0.1898056,0.8682561 0.062639,0.8540209 0.062639,0.8540209 z"
+ id="path2519"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#00a100;fill-opacity:1;stroke-width:0.264583"
+ d="M 2.1055809,4.223121 C 2.0551089,3.1942573 2.0383098,2.9291347 2.6324934,2.5000643 3.2266767,2.070994 3.6923741,2.4674508 3.9933288,2.5699226 3.718952,2.7370717 3.4647904,2.6395555 3.1496058,2.781203 2.7064121,2.9803792 2.738338,3.0037867 2.6187891,3.2423382 2.2503826,3.9774682 2.4762183,4.1592185 2.1055809,4.223121 Z"
+ id="path2415"
+ sodipodi:nodetypes="cscssc" />
+ <path
+ style="fill:#00cf00;fill-opacity:1;stroke-width:0.272503"
+ d="M 2.0975886,4.2112405 C 1.7892123,4.1687886 2.0455122,3.5896312 1.4981867,2.9104616 1.0529822,2.3580126 0.74373118,3.1368263 0.36812698,2.8108117 0.69766349,1.9826211 1.4696657,1.6318979 1.8391765,2.643645 c 0.097896,0.2680453 0.1875581,0.3804525 0.2399067,0.5618191 0.1452432,0.5032081 0.2932885,1.0610147 0.018505,1.0057764 z"
+ id="path2411"
+ sodipodi:nodetypes="cscssc" />
+ </g>
+ </g>
+</svg>
diff --git a/pw_web/log-viewer/src/components/log-list/log-list.styles.ts b/pw_web/log-viewer/src/components/log-list/log-list.styles.ts
new file mode 100644
index 000000000..a10b36360
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-list/log-list.styles.ts
@@ -0,0 +1,327 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { css } from 'lit';
+
+export const styles = css`
+ * {
+ box-sizing: border-box;
+ }
+
+ :host {
+ background-color: var(--sys-log-viewer-color-table-bg);
+ color: var(--sys-log-viewer-color-table-text);
+ display: block;
+ font-family: 'Roboto Mono', monospace;
+ font-size: 1rem;
+ height: 100%;
+ overflow: hidden;
+ position: relative;
+ }
+
+ .table-container {
+ display: grid;
+ height: 100%;
+ overflow: scroll;
+ scroll-behavior: auto;
+ width: 100%;
+ }
+
+ table {
+ border-collapse: collapse;
+ display: block;
+ height: 100%;
+ table-layout: fixed;
+ width: 100%;
+ }
+
+ thead,
+ th {
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ }
+
+ thead {
+ background-color: var(--sys-log-viewer-color-table-header-bg);
+ color: var(--sys-log-viewer-color-table-header-text);
+ display: block;
+ width: 100%;
+ }
+
+ tr {
+ border-bottom: 1px solid var(--sys-log-viewer-color-table-cell-outline);
+ display: grid;
+ grid-template-columns: var(--column-widths);
+ justify-content: flex-start;
+ width: 100%;
+ will-change: transform;
+ }
+
+ .log-row--warning {
+ --bg-color: var(--sys-log-viewer-color-surface-yellow);
+ --text-color: var(--sys-log-viewer-color-on-surface-yellow);
+ --icon-color: var(--sys-log-viewer-color-orange-bright);
+ }
+
+ .log-row--error,
+ .log-row--critical {
+ --bg-color: var(--sys-log-viewer-color-surface-error);
+ --text-color: var(--sys-log-viewer-color-on-surface-error);
+ --icon-color: var(--sys-log-viewer-color-error-bright);
+ }
+
+ .log-row--debug {
+ --bg-color: initial;
+ --text-color: var(--sys-log-viewer-color-debug);
+ --icon-color: var(--sys-log-viewer-color-debug);
+ }
+
+ .log-row--warning .cell-icon,
+ .log-row--error .cell-icon,
+ .log-row--critical .cell-icon {
+ color: var(--icon-color);
+ }
+
+ .log-row--warning,
+ .log-row--error,
+ .log-row--critical,
+ .log-row--debug {
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ }
+
+ .log-row .cell-content {
+ display: inline-flex;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre-wrap;
+ }
+
+ .log-row--nowrap .cell-content {
+ white-space: pre;
+ }
+
+ tbody tr::before {
+ background-color: transparent;
+ bottom: 0;
+ content: '';
+ display: block;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 100%;
+ z-index: -1;
+ }
+
+ tbody tr:hover::before {
+ background-color: rgba(
+ var(--sys-log-viewer-color-table-row-highlight),
+ 0.05
+ );
+ }
+
+ th,
+ td {
+ display: block;
+ grid-row: 1;
+ overflow: hidden;
+ padding: var(--sys-log-viewer-table-cell-padding);
+ text-align: left;
+ text-overflow: ellipsis;
+ }
+
+ th[hidden],
+ td[hidden] {
+ display: none;
+ }
+
+ th {
+ grid-row: 1;
+ white-space: nowrap;
+ }
+
+ th[title='severity'] {
+ visibility: hidden;
+ }
+
+ td {
+ display: inline-flex;
+ position: relative;
+ vertical-align: top;
+ align-items: flex-start;
+ }
+
+ .cell-text {
+ line-height: normal;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .jump-to-bottom-btn {
+ bottom: 2.25rem;
+ font-family: 'Roboto Flex', sans-serif;
+ position: absolute;
+ place-self: center;
+ transform: translateY(15%) scale(0.9);
+ }
+
+ .resize-handle {
+ background-color: var(--sys-log-viewer-color-table-cell-outline);
+ bottom: 0;
+ content: '';
+ cursor: col-resize;
+ height: 100%;
+ left: 0;
+ mix-blend-mode: luminosity;
+ opacity: 1;
+ pointer-events: auto;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: opacity 300ms ease;
+ width: 1px;
+ z-index: 1;
+ }
+
+ .resize-handle:hover {
+ background-color: var(--sys-log-viewer-color-primary);
+ mix-blend-mode: unset;
+ outline: 1px solid var(--sys-log-viewer-color-primary);
+ }
+
+ .resize-handle::before {
+ bottom: 0;
+ content: '';
+ display: block;
+ position: absolute;
+ right: -0.5rem;
+ top: 0;
+ width: 1rem;
+ }
+
+ .cell-icon {
+ display: block;
+ font-variation-settings:
+ 'FILL' 1,
+ 'wght' 400,
+ 'GRAD' 200,
+ 'opsz' 58;
+ font-size: var(--sys-log-viewer-table-cell-icon-size);
+ user-select: none;
+ display: grid;
+ place-content: center;
+ place-items: center;
+ }
+
+ .overflow-indicator {
+ pointer-events: none;
+ position: absolute;
+ width: 8rem;
+ }
+
+ .bottom-indicator {
+ align-self: flex-end;
+ background: linear-gradient(
+ to bottom,
+ transparent,
+ var(--sys-log-viewer-color-overflow-indicator)
+ );
+ height: 8rem;
+ pointer-events: none;
+ position: absolute;
+ width: calc(100% - 1rem);
+ }
+
+ .left-indicator {
+ background: linear-gradient(
+ to left,
+ transparent,
+ var(--sys-log-viewer-color-overflow-indicator)
+ );
+ height: calc(100% - 1rem);
+ justify-self: flex-start;
+ }
+
+ .right-indicator {
+ background: linear-gradient(
+ to right,
+ transparent,
+ var(--sys-log-viewer-color-overflow-indicator)
+ );
+ height: calc(100% - 1rem);
+ justify-self: flex-end;
+ }
+
+ mark {
+ background-color: var(--sys-log-viewer-color-table-mark);
+ border-radius: 4px;
+ color: var(--sys-log-viewer-color-table-mark-text);
+ outline: 1px solid var(--sys-log-viewer-color-table-mark);
+ }
+
+ .jump-to-bottom-btn,
+ .bottom-indicator {
+ opacity: 0;
+ transition:
+ opacity 100ms ease,
+ transform 100ms ease,
+ visibility 100ms ease;
+ visibility: hidden;
+ }
+
+ .jump-to-bottom-btn[data-visible='true'],
+ .bottom-indicator[data-visible='true'] {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ transition:
+ opacity 250ms ease,
+ transform 250ms ease,
+ 250ms ease;
+ visibility: visible;
+ }
+
+ ::-webkit-scrollbar {
+ box-shadow: inset 0 0 2rem 2rem var(--md-sys-color-surface-container-low);
+ -webkit-appearance: auto;
+ }
+
+ ::-webkit-scrollbar-corner {
+ background: var(--md-sys-color-surface-container-low);
+ }
+
+ ::-webkit-scrollbar-thumb {
+ border-radius: 20px;
+ box-shadow: inset 0 0 2rem 2rem var(--md-sys-color-outline-variant);
+ border: inset 3px transparent;
+ }
+
+ ::-webkit-scrollbar-thumb:horizontal {
+ border-top: inset 4px transparent;
+ }
+
+ ::-webkit-scrollbar-thumb:vertical {
+ border-left: inset 4px transparent;
+ height: calc(100% / 3);
+ }
+
+ ::-webkit-scrollbar-track:horizontal {
+ border-top: solid 1px var(--md-sys-color-outline-variant);
+ }
+
+ ::-webkit-scrollbar-track:vertical {
+ border-left: solid 1px var(--md-sys-color-outline-variant);
+ }
+`;
diff --git a/pw_web/log-viewer/src/components/log-list/log-list.ts b/pw_web/log-viewer/src/components/log-list/log-list.ts
new file mode 100644
index 000000000..58d635ce7
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-list/log-list.ts
@@ -0,0 +1,588 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LitElement, html, PropertyValues, TemplateResult } from 'lit';
+import {
+ customElement,
+ property,
+ query,
+ queryAll,
+ state,
+} from 'lit/decorators.js';
+import { classMap } from 'lit/directives/class-map.js';
+import { styles } from './log-list.styles';
+import { LogEntry, Severity, TableColumn } from '../../shared/interfaces';
+import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
+import '@lit-labs/virtualizer';
+
+/**
+ * A sub-component of the log view which takes filtered logs and renders them in
+ * a virtualized HTML table.
+ *
+ * @element log-list
+ */
+@customElement('log-list')
+export class LogList extends LitElement {
+ static styles = styles;
+
+ /** The `id` of the parent view containing this log list. */
+ @property()
+ viewId = '';
+
+ /** An array of log entries to be displayed. */
+ @property({ type: Array })
+ logs: LogEntry[] = [];
+
+ /** A string representing the value contained in the search field. */
+ @property({ type: String })
+ searchText = '';
+
+ /** Whether line wrapping in table cells should be used. */
+ @property({ type: Boolean })
+ lineWrap = false;
+
+ @property({ type: Array })
+ columnData: TableColumn[] = [];
+
+ /** Indicates whether the table content is overflowing to the right. */
+ @state()
+ private _isOverflowingToRight = false;
+
+ /**
+ * Indicates whether to automatically scroll the table container to the bottom
+ * when new log entries are added.
+ */
+ @state()
+ private _autoscrollIsEnabled = true;
+
+ /** A number representing the scroll percentage in the horizontal direction. */
+ @state()
+ private _scrollPercentageLeft = 0;
+
+ @query('.table-container') private _tableContainer!: HTMLDivElement;
+ @query('table') private _table!: HTMLTableElement;
+ @query('tbody') private _tableBody!: HTMLTableSectionElement;
+ @queryAll('tr') private _tableRows!: HTMLTableRowElement[];
+
+ /** Indicates whether to enable autosizing of incoming log entries. */
+ private _autosizeLocked = false;
+
+ /** The number of times the `logs` array has been updated. */
+ private logUpdateCount: number = 0;
+
+ /** The last known vertical scroll position of the table container. */
+ private lastScrollTop: number = 0;
+
+ /** The maximum number of log entries to render in the list. */
+ private readonly MAX_ENTRIES = 100_000;
+
+ /** The maximum number of log updates until autosize is disabled. */
+ private readonly AUTOSIZE_LIMIT: number = 8;
+
+ /** The minimum width (in px) for table columns. */
+ private readonly MIN_COL_WIDTH: number = 52;
+
+ /**
+ * Data used for column resizing including the column index, the starting
+ * mouse position (X-coordinate), and the initial width of the column.
+ */
+ private columnResizeData: {
+ columnIndex: number;
+ startX: number;
+ startWidth: number;
+ } | null = null;
+
+ firstUpdated() {
+ setInterval(() => this.updateHorizontalOverflowState(), 1000);
+
+ this._tableContainer.addEventListener('scroll', this.handleTableScroll);
+ this._tableBody.addEventListener('rangeChanged', this.onRangeChanged);
+
+ const newRowObserver = new MutationObserver(this.onTableRowAdded);
+ newRowObserver.observe(this._table, {
+ childList: true,
+ subtree: true,
+ });
+ }
+
+ updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+
+ if (
+ changedProperties.has('offsetWidth') ||
+ changedProperties.has('scrollWidth')
+ ) {
+ this.updateHorizontalOverflowState();
+ }
+
+ if (changedProperties.has('logs')) {
+ this.logUpdateCount++;
+ this.handleTableScroll();
+ }
+
+ if (changedProperties.has('columnData')) {
+ this.updateColumnWidths(this.generateGridTemplateColumns());
+ }
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this._tableContainer.removeEventListener('scroll', this.handleTableScroll);
+ this._tableBody.removeEventListener('rangeChanged', this.onRangeChanged);
+ }
+
+ private onTableRowAdded = () => {
+ if (!this._autosizeLocked) {
+ this.autosizeColumns();
+ }
+
+ // Disable auto-sizing once a certain number of updates to the logs array have been made
+ if (this.logUpdateCount >= this.AUTOSIZE_LIMIT) {
+ this._autosizeLocked = true;
+ }
+ };
+
+ /** Called when the Lit virtualizer updates its range of entries. */
+ private onRangeChanged = () => {
+ if (this._autoscrollIsEnabled) {
+ this.scrollTableToBottom();
+ }
+ };
+
+ /** Scrolls to the bottom of the table container. */
+ private scrollTableToBottom() {
+ const container = this._tableContainer;
+
+ // TODO: b/298097109 - Refactor `setTimeout` usage
+ setTimeout(() => {
+ container.scrollTop = container.scrollHeight;
+ }, 0); // Complete any rendering tasks before scrolling
+ }
+
+ private onJumpToBottomButtonClick() {
+ this._autoscrollIsEnabled = true;
+ this.scrollTableToBottom();
+ }
+
+ /**
+ * Calculates the maximum column widths for the table and updates the table
+ * rows.
+ */
+ private autosizeColumns = (rows = this._tableRows) => {
+ // Iterate through each row to find the maximum width in each column
+ rows.forEach((row) => {
+ const cells = Array.from(row.children).filter(
+ (cell) => !cell.hasAttribute('hidden'),
+ ) as HTMLTableCellElement[];
+
+ cells.forEach((cell, columnIndex) => {
+ if (columnIndex === 0) return;
+
+ const textLength = cell.textContent?.trim().length || 0;
+
+ if (!this._autosizeLocked) {
+ // Update the preferred width if it's smaller than the new one
+ if (this.columnData[columnIndex]) {
+ this.columnData[columnIndex].characterLength = Math.max(
+ this.columnData[columnIndex].characterLength,
+ textLength,
+ );
+ } else {
+ // Initialize if the column data for this index does not exist
+ this.columnData[columnIndex] = {
+ fieldName: '',
+ characterLength: textLength,
+ manualWidth: null,
+ isVisible: true,
+ };
+ }
+ }
+ });
+ });
+ };
+
+ private generateGridTemplateColumns(
+ newWidth?: number,
+ resizingIndex?: number,
+ ): string {
+ let gridTemplateColumns = '';
+
+ this.columnData.forEach((col, i) => {
+ let columnValue = '';
+
+ if (col.isVisible) {
+ if (i === resizingIndex) {
+ columnValue = `${newWidth}px`;
+ } else if (col.manualWidth !== null) {
+ columnValue = `${col.manualWidth}px`;
+ } else {
+ if (i === 0) {
+ columnValue = '3rem';
+ } else {
+ const chWidth = col.characterLength;
+ const padding = 34;
+ columnValue = `clamp(${this.MIN_COL_WIDTH}px, ${chWidth}ch + ${padding}px, 80ch)`;
+ }
+ }
+
+ gridTemplateColumns += columnValue + ' ';
+ }
+ });
+
+ return gridTemplateColumns.trim();
+ }
+
+ private updateColumnWidths(gridTemplateColumns: string) {
+ this.style.setProperty('--column-widths', gridTemplateColumns);
+ }
+
+ /**
+ * Highlights text content within the table cell based on the current filter
+ * value.
+ *
+ * @param {string} text - The table cell text to be processed.
+ */
+ private highlightMatchedText(text: string): TemplateResult[] {
+ if (!this.searchText) {
+ return [html`${text}`];
+ }
+
+ const searchPhrase = this.searchText?.replace(/(^"|')|("|'$)/g, '');
+ const escapedsearchText = searchPhrase.replace(
+ /[.*+?^${}()|[\]\\]/g,
+ '\\$&',
+ );
+ const regex = new RegExp(`(${escapedsearchText})`, 'gi');
+ const parts = text.split(regex);
+ return parts.map((part) =>
+ regex.test(part) ? html`<mark>${part}</mark>` : html`${part}`,
+ );
+ }
+
+ /** Updates horizontal overflow state. */
+ private updateHorizontalOverflowState() {
+ const containerWidth = this.offsetWidth;
+ const tableWidth = this._tableContainer.scrollWidth;
+
+ this._isOverflowingToRight = tableWidth > containerWidth;
+ }
+
+ /**
+ * Calculates scroll-related properties and updates the component's state when
+ * the user scrolls the table.
+ */
+ private handleTableScroll = () => {
+ const container = this._tableContainer;
+ const currentScrollTop = container.scrollTop;
+ const containerWidth = container.offsetWidth;
+ const scrollLeft = container.scrollLeft;
+ const scrollY =
+ container.scrollHeight - currentScrollTop - container.clientHeight;
+ const maxScrollLeft = container.scrollWidth - containerWidth;
+
+ // Determine scroll direction and update the last known scroll position
+ const isScrollingVertically = currentScrollTop !== this.lastScrollTop;
+ const isScrollingUp = currentScrollTop < this.lastScrollTop;
+ this.lastScrollTop = currentScrollTop;
+
+ const logsAreCleared = this.logs.length == 0;
+
+ if (logsAreCleared) {
+ this._autoscrollIsEnabled = true;
+ return;
+ }
+
+ // Run autoscroll logic if scrolling vertically
+ if (!isScrollingVertically) {
+ this._scrollPercentageLeft = scrollLeft / maxScrollLeft || 0;
+ return;
+ }
+
+ // Scroll direction up, disable autoscroll
+ if (isScrollingUp) {
+ this._autoscrollIsEnabled = false;
+ return;
+ }
+
+ // Scroll direction down, enable autoscroll if near the bottom
+ if (Math.abs(scrollY) <= 1) {
+ this._autoscrollIsEnabled = true;
+ return;
+ }
+ };
+
+ /**
+ * Handles column resizing.
+ *
+ * @param {MouseEvent} event - The mouse event triggered during column
+ * resizing.
+ * @param {number} columnIndex - An index specifying the column being resized.
+ */
+ private handleColumnResizeStart(event: MouseEvent, columnIndex: number) {
+ event.preventDefault();
+
+ // Check if the corresponding index in columnData is not visible. If not,
+ // check the columnIndex - 1th element until one isn't hidden.
+ while (
+ this.columnData[columnIndex] &&
+ !this.columnData[columnIndex].isVisible
+ ) {
+ columnIndex--;
+ if (columnIndex < 0) {
+ // Exit the loop if we've checked all possible columns
+ return;
+ }
+ }
+
+ // If no visible columns are found, return early
+ if (columnIndex < 0) return;
+
+ const startX = event.clientX;
+ const columnHeader = this._table.querySelector(
+ `th:nth-child(${columnIndex + 1})`,
+ ) as HTMLTableCellElement;
+
+ if (!columnHeader) return;
+
+ const startWidth = columnHeader.offsetWidth;
+
+ this.columnResizeData = {
+ columnIndex: columnIndex,
+ startX,
+ startWidth,
+ };
+
+ const handleColumnResize = (event: MouseEvent) => {
+ this.handleColumnResize(event);
+ };
+
+ const handleColumnResizeEnd = () => {
+ this.columnResizeData = null;
+ document.removeEventListener('mousemove', handleColumnResize);
+ document.removeEventListener('mouseup', handleColumnResizeEnd);
+
+ // Communicate column data changes back to parent Log View
+ const updateColumnData = new CustomEvent('update-column-data', {
+ detail: this.columnData,
+ });
+
+ this.dispatchEvent(updateColumnData);
+ };
+
+ document.addEventListener('mousemove', handleColumnResize);
+ document.addEventListener('mouseup', handleColumnResizeEnd);
+ }
+
+ /**
+ * Adjusts the column width during a column resize.
+ *
+ * @param {MouseEvent} event - The mouse event object.
+ */
+ private handleColumnResize(event: MouseEvent) {
+ if (!this.columnResizeData) return;
+
+ const { columnIndex, startX, startWidth } = this.columnResizeData;
+ const offsetX = event.clientX - startX;
+ const newWidth = Math.max(startWidth + offsetX, this.MIN_COL_WIDTH);
+
+ // Ensure the column index exists in columnData
+ if (this.columnData[columnIndex]) {
+ this.columnData[columnIndex].manualWidth = newWidth;
+ }
+
+ const gridTemplateColumns = this.generateGridTemplateColumns(
+ newWidth,
+ columnIndex,
+ );
+
+ this.updateColumnWidths(gridTemplateColumns);
+ }
+
+ render() {
+ const logsDisplayed: LogEntry[] = this.logs.slice(0, this.MAX_ENTRIES);
+
+ return html`
+ <div
+ class="table-container"
+ role="log"
+ @scroll="${this.handleTableScroll}"
+ >
+ <table>
+ <thead>
+ ${this.tableHeaderRow()}
+ </thead>
+
+ <tbody>
+ ${virtualize({
+ items: logsDisplayed,
+ renderItem: (log) => html`${this.tableDataRow(log)}`,
+ })}
+ </tbody>
+ </table>
+ ${this.overflowIndicators()} ${this.jumpToBottomButton()}
+ </div>
+ `;
+ }
+
+ private tableHeaderRow() {
+ return html`
+ <tr>
+ ${this.columnData.map((columnData, columnIndex) =>
+ this.tableHeaderCell(
+ columnData.fieldName,
+ columnIndex,
+ columnData.isVisible,
+ ),
+ )}
+ </tr>
+ `;
+ }
+
+ private tableHeaderCell(
+ fieldKey: string,
+ columnIndex: number,
+ isVisible: boolean,
+ ) {
+ return html`
+ <th title="${fieldKey}" ?hidden=${!isVisible}>
+ ${fieldKey}
+ ${columnIndex > 0 ? this.resizeHandle(columnIndex - 1) : html``}
+ </th>
+ `;
+ }
+
+ private resizeHandle(columnIndex: number) {
+ if (columnIndex === 0) {
+ return html`
+ <span class="resize-handle" style="pointer-events: none"></span>
+ `;
+ }
+
+ return html`
+ <span
+ class="resize-handle"
+ @mousedown="${(event: MouseEvent) =>
+ this.handleColumnResizeStart(event, columnIndex)}"
+ ></span>
+ `;
+ }
+
+ private tableDataRow(log: LogEntry) {
+ const classes = {
+ 'log-row': true,
+ 'log-row--nowrap': !this.lineWrap,
+ };
+ const logSeverityClass = ('log-row--' +
+ (log.severity || Severity.INFO).toLowerCase()) as keyof typeof classes;
+ classes[logSeverityClass] = true;
+
+ return html`
+ <tr class="${classMap(classes)}">
+ ${this.columnData.map((columnData, columnIndex) =>
+ this.tableDataCell(
+ log,
+ columnData.fieldName,
+ columnIndex,
+ columnData.isVisible,
+ ),
+ )}
+ </tr>
+ `;
+ }
+
+ private tableDataCell(
+ log: LogEntry,
+ fieldKey: string,
+ columnIndex: number,
+ isVisible: boolean,
+ ) {
+ const field = log.fields.find((f) => f.key === fieldKey) || {
+ key: fieldKey,
+ value: '',
+ };
+
+ if (field.key == 'severity') {
+ const severityIcons = new Map<Severity, string>([
+ [Severity.WARNING, 'warning'],
+ [Severity.ERROR, 'cancel'],
+ [Severity.CRITICAL, 'brightness_alert'],
+ [Severity.DEBUG, 'bug_report'],
+ ]);
+
+ const severityValue = field.value as Severity;
+ const iconId = severityIcons.get(severityValue) || '';
+ const toTitleCase = (input: string): string => {
+ return input.replace(/\b\w+/g, (match) => {
+ return match.charAt(0).toUpperCase() + match.slice(1).toLowerCase();
+ });
+ };
+
+ return html`
+ <td ?hidden=${!isVisible}>
+ <div class="cell-content">
+ <md-icon
+ class="cell-icon"
+ title="${toTitleCase(field.value.toString())}"
+ >
+ ${iconId}
+ </md-icon>
+ </div>
+ </td>
+ `;
+ }
+
+ return html`
+ <td ?hidden=${!isVisible}>
+ <div class="cell-content">
+ <span class="cell-text"
+ >${this.highlightMatchedText(field.value.toString())}</span
+ >
+ </div>
+ ${columnIndex > 0 ? this.resizeHandle(columnIndex - 1) : html``}
+ </td>
+ `;
+ }
+
+ private overflowIndicators = () => html`
+ <div
+ class="bottom-indicator"
+ data-visible="${this._autoscrollIsEnabled ? 'false' : 'true'}"
+ ></div>
+
+ <div
+ class="overflow-indicator left-indicator"
+ style="opacity: ${this._scrollPercentageLeft}"
+ ?hidden="${!this._isOverflowingToRight}"
+ ></div>
+
+ <div
+ class="overflow-indicator right-indicator"
+ style="opacity: ${1 - this._scrollPercentageLeft}"
+ ?hidden="${!this._isOverflowingToRight}"
+ ></div>
+ `;
+
+ private jumpToBottomButton = () => html`
+ <md-filled-button
+ class="jump-to-bottom-btn"
+ title="Jump to Bottom"
+ @click="${this.onJumpToBottomButtonClick}"
+ leading-icon
+ data-visible="${this._autoscrollIsEnabled ? 'false' : 'true'}"
+ >
+ <md-icon slot="icon" aria-hidden="true">arrow_downward</md-icon>
+ Jump to Bottom
+ </md-filled-button>
+ `;
+}
diff --git a/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts
new file mode 100644
index 000000000..0277857eb
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.styles.ts
@@ -0,0 +1,114 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { css } from 'lit';
+
+export const styles = css`
+ :host {
+ align-items: center;
+ background-color: var(--sys-log-viewer-color-controls-bg);
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
+ box-sizing: border-box;
+ color: var(--sys-log-viewer-color-controls-text);
+ display: flex;
+ flex-shrink: 0;
+ gap: 1rem;
+ height: 3rem;
+ justify-content: space-between;
+ padding: 0 1rem;
+ --md-list-item-leading-icon-size: 1.5rem;
+ }
+
+ :host > * {
+ display: flex;
+ }
+
+ .host-name {
+ font-size: 1.125rem;
+ font-weight: 300;
+ margin: 0;
+ white-space: nowrap;
+ }
+
+ .field-menu {
+ background-color: var(--md-sys-color-surface-container);
+ border-radius: 4px;
+ margin: 0;
+ padding: 0.5rem 0.75rem;
+ position: absolute;
+ right: 0;
+ z-index: 2;
+ }
+
+ md-standard-icon-button[selected] {
+ background-color: var(--sys-log-viewer-color-controls-button-enabled);
+ border-radius: 100%;
+ }
+
+ .field-menu-item {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ width: max-content;
+ }
+
+ .field-toggle {
+ border-radius: 1.5rem;
+ position: relative;
+ }
+
+ .input-container {
+ justify-content: flex-end;
+ width: 100%;
+ }
+
+ .input-facade {
+ align-items: center;
+ background-color: var(--sys-log-viewer-color-controls-input-bg);
+ border: 1px solid var(--sys-log-viewer-color-controls-input-outline);
+ border-radius: 1.5rem;
+ cursor: text;
+ display: inline-flex;
+ font-size: 1rem;
+ height: 0.75rem;
+ line-height: 0.75;
+ max-width: 30rem;
+ overflow: hidden;
+ padding: 0.5rem 1rem;
+ width: 100%;
+ }
+
+ input[type='text'] {
+ display: none;
+ }
+
+ input::placeholder {
+ color: var(--md-sys-color-on-surface-variant);
+ }
+
+ input[type='checkbox'] {
+ accent-color: var(--md-sys-color-primary);
+ height: 1.125rem;
+ width: 1.125rem;
+ }
+
+ label {
+ padding-left: 0.75rem;
+ }
+
+ p {
+ flex: 1 0;
+ white-space: nowrap;
+ }
+`;
diff --git a/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts
new file mode 100644
index 000000000..8c0226f7b
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-view-controls/log-view-controls.ts
@@ -0,0 +1,354 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LitElement, html } from 'lit';
+import {
+ customElement,
+ property,
+ query,
+ queryAll,
+ state,
+} from 'lit/decorators.js';
+import { styles } from './log-view-controls.styles';
+import { State, TableColumn } from '../../shared/interfaces';
+import { StateStore, LocalStorageState } from '../../shared/state';
+
+/**
+ * A sub-component of the log view with user inputs for managing and customizing
+ * log entry display and interaction.
+ *
+ * @element log-view-controls
+ */
+@customElement('log-view-controls')
+export class LogViewControls extends LitElement {
+ static styles = styles;
+
+ /** The `id` of the parent view containing this log list. */
+ @property({ type: String })
+ viewId = '';
+
+ @property({ type: Array })
+ columnData: TableColumn[] = [];
+
+ /** Indicates whether to enable the button for closing the current log view. */
+ @property({ type: Boolean })
+ hideCloseButton = false;
+
+ /** A StateStore object that stores state of views */
+ @state()
+ _stateStore: StateStore = new LocalStorageState();
+
+ @state()
+ _viewTitle = 'Log View';
+
+ @state()
+ _moreActionsMenuOpen = false;
+
+ @query('.field-menu') _fieldMenu!: HTMLMenuElement;
+
+ @query('#search-field') _searchField!: HTMLInputElement;
+
+ @query('.input-facade') _inputFacade!: HTMLDivElement;
+
+ @queryAll('.item-checkboxes') _itemCheckboxes!: HTMLCollection[];
+
+ private _state: State;
+
+ /** The timer identifier for debouncing search input. */
+ private _inputDebounceTimer: number | null = null;
+
+ /** The delay (in ms) used for debouncing search input. */
+ private readonly INPUT_DEBOUNCE_DELAY = 50;
+
+ @query('.more-actions-button') moreActionsButtonEl!: HTMLElement;
+
+ constructor() {
+ super();
+ this._state = this._stateStore.getState();
+ }
+
+ protected firstUpdated(): void {
+ let searchText = '';
+ if (this._state !== null) {
+ const viewConfigArr = this._state.logViewConfig;
+ for (const i in viewConfigArr) {
+ if (viewConfigArr[i].viewID === this.viewId) {
+ searchText = viewConfigArr[i].search as string;
+ this._viewTitle = viewConfigArr[i].viewTitle
+ ? viewConfigArr[i].viewTitle
+ : this._viewTitle;
+ }
+ }
+ }
+
+ this._inputFacade.textContent = searchText;
+ this._inputFacade.dispatchEvent(new CustomEvent('input'));
+ }
+
+ /**
+ * Called whenever the search field value is changed. Debounces the input
+ * event and dispatches an event with the input value after a specified
+ * delay.
+ *
+ * @param {Event} event - The input event object.
+ */
+ private handleInput = (event: Event) => {
+ if (this._inputDebounceTimer) {
+ clearTimeout(this._inputDebounceTimer);
+ }
+
+ const inputFacade = event.target as HTMLDivElement;
+ this.markKeysInText(inputFacade);
+ this._searchField.value = inputFacade.textContent || '';
+ const inputValue = this._searchField.value;
+
+ this._inputDebounceTimer = window.setTimeout(() => {
+ const customEvent = new CustomEvent('input-change', {
+ detail: { inputValue },
+ bubbles: true,
+ composed: true,
+ });
+
+ this.dispatchEvent(customEvent);
+ }, this.INPUT_DEBOUNCE_DELAY);
+ };
+
+ private markKeysInText(target: HTMLElement) {
+ const pattern = /\b(\w+):(?=\w)/;
+ const textContent = target.textContent || '';
+ const conditions = textContent.split(/\s+/);
+ const wordsBeforeColons: string[] = [];
+
+ for (const condition of conditions) {
+ const match = condition.match(pattern);
+ if (match) {
+ wordsBeforeColons.push(match[0]);
+ }
+ }
+ }
+
+ private handleKeydown = (event: KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === 'Cmd') {
+ event.preventDefault();
+ }
+ };
+
+ /**
+ * Dispatches a custom event for clearing logs. This event includes a
+ * `timestamp` object indicating the date/time in which the 'clear-logs' event
+ * was dispatched.
+ */
+ private handleClearLogsClick() {
+ const timestamp = new Date();
+
+ const clearLogs = new CustomEvent('clear-logs', {
+ detail: { timestamp },
+ bubbles: true,
+ composed: true,
+ });
+
+ this.dispatchEvent(clearLogs);
+ }
+
+ /** Dispatches a custom event for toggling wrapping. */
+ private handleWrapToggle() {
+ const wrapToggle = new CustomEvent('wrap-toggle', {
+ bubbles: true,
+ composed: true,
+ });
+
+ this.dispatchEvent(wrapToggle);
+ }
+
+ /**
+ * Dispatches a custom event for closing the parent view. This event includes
+ * a `viewId` object indicating the `id` of the parent log view.
+ */
+ private handleCloseViewClick() {
+ const closeView = new CustomEvent('close-view', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ viewId: this.viewId,
+ },
+ });
+
+ this.dispatchEvent(closeView);
+ }
+
+ /**
+ * Dispatches a custom event for showing or hiding a column in the table. This
+ * event includes a `field` string indicating the affected column's field name
+ * and an `isChecked` boolean indicating whether to show or hide the column.
+ *
+ * @param {Event} event - The click event object.
+ */
+ private handleColumnToggle(event: Event) {
+ const inputEl = event.target as HTMLInputElement;
+ const columnToggle = new CustomEvent('column-toggle', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ field: inputEl.value,
+ isChecked: inputEl.checked,
+ },
+ });
+
+ this.dispatchEvent(columnToggle);
+ }
+
+ private handleAddView() {
+ const addView = new CustomEvent('add-view', {
+ bubbles: true,
+ composed: true,
+ });
+
+ this.dispatchEvent(addView);
+ }
+
+ /**
+ * Dispatches a custom event for downloading a logs file. This event includes
+ * a `format` string indicating the format of the file to be downloaded and a
+ * `viewTitle` string which passes the title of the current view for naming
+ * the file.
+ *
+ * @param {Event} event - The click event object.
+ */
+ private handleDownloadLogs() {
+ const downloadLogs = new CustomEvent('download-logs', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ format: 'plaintext',
+ viewTitle: this._viewTitle,
+ },
+ });
+
+ this.dispatchEvent(downloadLogs);
+ }
+
+ /** Opens and closes the column visibility dropdown menu. */
+ private toggleColumnVisibilityMenu() {
+ this._fieldMenu.hidden = !this._fieldMenu.hidden;
+ }
+
+ /** Opens and closes the More Actions menu. */
+ private toggleMoreActionsMenu() {
+ this._moreActionsMenuOpen = !this._moreActionsMenuOpen;
+ }
+
+ render() {
+ return html`
+ <p class="host-name">${this._viewTitle}</p>
+
+ <div class="input-container">
+ <div class="input-facade" contenteditable="plaintext-only" @input="${
+ this.handleInput
+ }" @keydown="${this.handleKeydown}"></div>
+ <input id="search-field" type="text"></input>
+ </div>
+
+ <div class="actions-container">
+ <span class="action-button" hidden>
+ <md-icon-button>
+ <md-icon>pause_circle</md-icon>
+ </md-icon-button>
+ </span>
+
+ <span class="action-button" hidden>
+ <md-icon-button>
+ <md-icon>wrap_text</md-icon>
+ </md-icon-button>
+ </span>
+
+ <span class="action-button" title="Clear logs">
+ <md-icon-button @click=${this.handleClearLogsClick}>
+ <md-icon>delete_sweep</md-icon>
+ </md-icon-button>
+ </span>
+
+ <span class="action-button" title="Toggle Line Wrapping">
+ <md-icon-button @click=${this.handleWrapToggle} toggle>
+ <md-icon>wrap_text</md-icon>
+ </md-icon-button>
+ </span>
+
+ <span class='action-button field-toggle' title="Toggle fields">
+ <md-icon-button @click=${this.toggleColumnVisibilityMenu} toggle>
+ <md-icon>view_column</md-icon>
+ </md-icon-button>
+ <menu class='field-menu' hidden>
+ ${this.columnData.map(
+ (column) => html`
+ <li class="field-menu-item">
+ <input
+ class="item-checkboxes"
+ @click=${this.handleColumnToggle}
+ ?checked=${column.isVisible}
+ type="checkbox"
+ value=${column.fieldName}
+ id=${column.fieldName}
+ />
+ <label for=${column.fieldName}>${column.fieldName}</label>
+ </li>
+ `,
+ )}
+ </menu>
+ </span>
+
+ <span class="action-button" title="Toggle fields">
+ <md-icon-button @click=${
+ this.toggleMoreActionsMenu
+ } class="more-actions-button">
+ <md-icon >more_vert</md-icon>
+ </md-icon-button>
+
+ <md-menu quick fixed
+ ?open=${this._moreActionsMenuOpen}
+ .anchor=${this.moreActionsButtonEl}
+ @closed=${() => {
+ this._moreActionsMenuOpen = false;
+ }}>
+
+ <md-menu-item headline="Add view" @click=${
+ this.handleAddView
+ } role="button" title="Add a view">
+ <md-icon slot="start" data-variant="icon">new_window</md-icon>
+ </md-menu-item>
+
+ <md-menu-item headline="Download logs (.txt)" @click=${
+ this.handleDownloadLogs
+ } role="button" title="Download current logs as a plaintext file">
+ <md-icon slot="start" data-variant="icon">download</md-icon>
+ </md-menu-item>
+ </md-menu>
+ </span>
+
+ <span class="action-button" title="Close view" ?hidden=${
+ this.hideCloseButton
+ }>
+ <md-icon-button @click=${this.handleCloseViewClick}>
+ <md-icon>close</md-icon>
+ </md-icon-button>
+ </span>
+
+ <span class="action-button" hidden>
+ <md-icon-button>
+ <md-icon>more_horiz</md-icon>
+ </md-icon-button>
+ </span>
+ </div>
+ `;
+ }
+}
diff --git a/pw_web/log-viewer/src/components/log-view/log-view.styles.ts b/pw_web/log-viewer/src/components/log-view/log-view.styles.ts
new file mode 100644
index 000000000..43c2eeadb
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-view/log-view.styles.ts
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { css } from 'lit';
+
+export const styles = css`
+ :host {
+ border: var(--sys-log-viewer-view-outline-width) solid
+ var(--sys-log-viewer-color-view-outline);
+ border-radius: var(--sys-log-viewer-view-corner-radius);
+ color: var(--md-sys-color-on-surface);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+`;
diff --git a/pw_web/log-viewer/src/components/log-view/log-view.ts b/pw_web/log-viewer/src/components/log-view/log-view.ts
new file mode 100644
index 000000000..900637e5c
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-view/log-view.ts
@@ -0,0 +1,352 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LitElement, PropertyValues, html } from 'lit';
+import { customElement, property, query, state } from 'lit/decorators.js';
+import { styles } from './log-view.styles';
+import { LogList } from '../log-list/log-list';
+import { TableColumn, LogEntry, State } from '../../shared/interfaces';
+import { LocalStorageState, StateStore } from '../../shared/state';
+import { LogFilter } from '../../utils/log-filter/log-filter';
+import '../log-list/log-list';
+import '../log-view-controls/log-view-controls';
+import { titleCaseToKebabCase } from '../../utils/strings';
+
+type FilterFunction = (logEntry: LogEntry) => boolean;
+
+/**
+ * A component that filters and displays incoming log entries in an encapsulated
+ * instance. Each `LogView` contains a log list and a set of log view controls
+ * for configurable viewing of filtered logs.
+ *
+ * @element log-view
+ */
+@customElement('log-view')
+export class LogView extends LitElement {
+ static styles = styles;
+
+ /**
+ * The component's global `id` attribute. This unique value is set whenever a
+ * view is created in a log viewer instance.
+ */
+ @property({ type: String })
+ id = `${this.localName}-${crypto.randomUUID()}`;
+
+ /** An array of log entries to be displayed. */
+ @property({ type: Array })
+ logs: LogEntry[] = [];
+
+ /** Indicates whether this view is one of multiple instances. */
+ @property({ type: Boolean })
+ isOneOfMany = false;
+
+ /** Whether line wrapping in table cells should be used. */
+ @state()
+ _lineWrap = false;
+
+ /** The field keys (column values) for the incoming log entries. */
+ @state()
+ private _columnData: TableColumn[] = [];
+
+ /** A string representing the value contained in the search field. */
+ @state()
+ public searchText = '';
+
+ /** A StateStore object that stores state of views */
+ @state()
+ _stateStore: StateStore = new LocalStorageState();
+
+ @query('log-list') _logList!: LogList;
+
+ /**
+ * An array containing the logs that remain after the current filter has been
+ * applied.
+ */
+ private _filteredLogs: LogEntry[] = [];
+
+ /** A function used for filtering rows that contain a certain substring. */
+ private _stringFilter: FilterFunction = () => true;
+
+ /**
+ * A function used for filtering rows that contain a timestamp within a
+ * certain window.
+ */
+ private _timeFilter: FilterFunction = () => true;
+
+ private _state: State;
+
+ private _debounceTimeout: NodeJS.Timeout | null = null;
+
+ /** The number of elements in the `logs` array since last updated. */
+ private _lastKnownLogLength: number = 0;
+
+ /** The amount of time, in ms, before the filter expression is executed. */
+ private readonly FILTER_DELAY = 100;
+
+ constructor() {
+ super();
+ this._state = this._stateStore.getState();
+ }
+
+ protected firstUpdated(): void {
+ const viewConfigArr = this._state.logViewConfig;
+ const index = viewConfigArr.findIndex((i) => this.id === i.viewID);
+
+ // Get column data from local storage, if it exists
+ if (index !== -1) {
+ const storedColumnData = viewConfigArr[index].columnData;
+ this._columnData = storedColumnData;
+ }
+ }
+
+ updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+
+ if (changedProperties.has('logs')) {
+ const newLogs = this.logs.slice(this._lastKnownLogLength);
+ this._lastKnownLogLength = this.logs.length;
+
+ this.updateFieldsFromNewLogs(newLogs);
+ }
+
+ if (changedProperties.has('logs') || changedProperties.has('searchText')) {
+ this.filterLogs();
+ }
+
+ if (changedProperties.has('_columnData')) {
+ this._state = { logViewConfig: this._state.logViewConfig };
+ this._stateStore.setState({
+ logViewConfig: this._state.logViewConfig,
+ });
+ }
+ }
+
+ /**
+ * Updates the log filter based on the provided event type.
+ *
+ * @param {CustomEvent} event - The custom event containing the information to
+ * update the filter.
+ */
+ private updateFilter(event: CustomEvent) {
+ this.searchText = event.detail.inputValue;
+ const logViewConfig = this._state.logViewConfig;
+ const index = logViewConfig.findIndex((i) => this.id === i.viewID);
+
+ switch (event.type) {
+ case 'input-change':
+ if (this._debounceTimeout) {
+ clearTimeout(this._debounceTimeout);
+ }
+
+ if (index !== -1) {
+ logViewConfig[index].search = this.searchText;
+ this._state = { logViewConfig: logViewConfig };
+ this._stateStore.setState({ logViewConfig: logViewConfig });
+ }
+
+ if (!this.searchText) {
+ this._stringFilter = () => true;
+ return;
+ }
+
+ // Run the filter after the timeout delay
+ this._debounceTimeout = setTimeout(() => {
+ const filters = LogFilter.parseSearchQuery(this.searchText).map(
+ (condition) => LogFilter.createFilterFunction(condition),
+ );
+ this._stringFilter =
+ filters.length > 0
+ ? (logEntry: LogEntry) =>
+ filters.some((filter) => filter(logEntry))
+ : () => true;
+
+ this.filterLogs();
+ this.requestUpdate();
+ }, this.FILTER_DELAY);
+ break;
+ case 'clear-logs':
+ this._timeFilter = (logEntry) =>
+ logEntry.timestamp > event.detail.timestamp;
+ break;
+ default:
+ break;
+ }
+
+ this.filterLogs();
+ this.requestUpdate();
+ }
+
+ private updateFieldsFromNewLogs(newLogs: LogEntry[]): void {
+ if (!this._columnData) {
+ this._columnData = [];
+ }
+
+ newLogs.forEach((log) => {
+ log.fields.forEach((field) => {
+ if (!this._columnData.some((col) => col.fieldName === field.key)) {
+ this._columnData.push({
+ fieldName: field.key,
+ characterLength: 0,
+ manualWidth: null,
+ isVisible: true,
+ });
+ }
+ });
+ });
+ }
+
+ public getFields(): string[] {
+ return this._columnData
+ .filter((column) => column.isVisible)
+ .map((column) => column.fieldName);
+ }
+
+ /**
+ * Toggles the visibility of columns in the log list based on the provided
+ * event.
+ *
+ * @param {CustomEvent} event - The click event containing the field being
+ * toggled.
+ */
+ private toggleColumns(event: CustomEvent) {
+ const logViewConfig = this._state.logViewConfig;
+ const index = logViewConfig.findIndex((i) => this.id === i.viewID);
+
+ if (index === -1) {
+ return;
+ }
+
+ // Find the relevant column in _columnData
+ const column = this._columnData.find(
+ (col) => col.fieldName === event.detail.field,
+ );
+
+ if (!column) {
+ return;
+ }
+
+ // Toggle the column's visibility
+ column.isVisible = event.detail.isChecked;
+
+ // Clear the manually-set width of the last visible column
+ const lastVisibleColumn = this._columnData
+ .slice()
+ .reverse()
+ .find((col) => col.isVisible);
+ if (lastVisibleColumn) {
+ lastVisibleColumn.manualWidth = null;
+ }
+
+ // Trigger the change in column data and request an update
+ this._columnData = [...this._columnData];
+ this._logList.requestUpdate();
+ }
+
+ /**
+ * Toggles the wrapping of text in each row.
+ *
+ * @param {CustomEvent} event - The click event.
+ */
+ private toggleWrapping() {
+ this._lineWrap = !this._lineWrap;
+ }
+
+ /**
+ * Combines filter expressions and filters the logs. The filtered
+ * logs are stored in the `_filteredLogs` property.
+ */
+ private filterLogs() {
+ const combinedFilter = (logEntry: LogEntry) =>
+ this._timeFilter(logEntry) && this._stringFilter(logEntry);
+
+ const newFilteredLogs = this.logs.filter(combinedFilter);
+
+ if (
+ JSON.stringify(newFilteredLogs) !== JSON.stringify(this._filteredLogs)
+ ) {
+ this._filteredLogs = newFilteredLogs;
+ }
+ }
+
+ private updateColumnData(event: CustomEvent) {
+ this._columnData = event.detail;
+ }
+
+ /**
+ * Generates a log file in the specified format and initiates its download.
+ *
+ * @param {CustomEvent} event - The click event.
+ */
+ private downloadLogs(event: CustomEvent) {
+ const headers = this.logs[0]?.fields.map((field) => field.key) || [];
+ const maxWidths = headers.map((header) => header.length);
+ const viewTitle = event.detail.viewTitle;
+ const fileName = viewTitle ? titleCaseToKebabCase(viewTitle) : 'logs';
+
+ this.logs.forEach((log) => {
+ log.fields.forEach((field, columnIndex) => {
+ maxWidths[columnIndex] = Math.max(
+ maxWidths[columnIndex],
+ field.value.toString().length,
+ );
+ });
+ });
+
+ const headerRow = headers
+ .map((header, columnIndex) => header.padEnd(maxWidths[columnIndex]))
+ .join('\t');
+ const separator = '';
+ const logRows = this.logs.map((log) => {
+ const values = log.fields.map((field, columnIndex) =>
+ field.value.toString().padEnd(maxWidths[columnIndex]),
+ );
+ return values.join('\t');
+ });
+
+ const formattedLogs = [headerRow, separator, ...logRows].join('\n');
+ const blob = new Blob([formattedLogs], { type: 'text/plain' });
+ const downloadLink = document.createElement('a');
+ downloadLink.href = URL.createObjectURL(blob);
+ downloadLink.download = `${fileName}.txt`;
+ downloadLink.click();
+
+ URL.revokeObjectURL(downloadLink.href);
+ }
+
+ render() {
+ return html` <log-view-controls
+ .columnData=${this._columnData}
+ .viewId=${this.id}
+ .hideCloseButton=${!this.isOneOfMany}
+ .stateStore=${this._stateStore}
+ @input-change="${this.updateFilter}"
+ @clear-logs="${this.updateFilter}"
+ @column-toggle="${this.toggleColumns}"
+ @wrap-toggle="${this.toggleWrapping}"
+ @download-logs="${this.downloadLogs}"
+ role="toolbar"
+ >
+ </log-view-controls>
+
+ <log-list
+ .columnData=${[...this._columnData]}
+ .lineWrap=${this._lineWrap}
+ .viewId=${this.id}
+ .logs=${this._filteredLogs}
+ .searchText=${this.searchText}
+ @update-column-data="${this.updateColumnData}"
+ >
+ </log-list>`;
+ }
+}
diff --git a/pw_web/log-viewer/src/components/log-viewer.styles.ts b/pw_web/log-viewer/src/components/log-viewer.styles.ts
new file mode 100644
index 000000000..0bf3a88e6
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-viewer.styles.ts
@@ -0,0 +1,68 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { css } from 'lit';
+
+export const styles = css`
+ * {
+ box-sizing: border-box;
+ }
+
+ :host {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ height: var(--sys-log-viewer-height);
+ width: 100%;
+
+ /* Material Web properties */
+ --md-icon-font: 'Material Symbols Rounded';
+ --md-icon-size: 1.25rem;
+ --md-filled-button-label-text-type: 'Roboto Flex', Arial, sans-serif;
+ --md-outlined-button-label-text-type: 'Roboto Flex', Arial, sans-serif;
+ --md-icon-button-unselected-icon-color: var(
+ --md-sys-color-on-surface-variant
+ );
+ --md-icon-button-unselected-hover-icon-color: var(
+ --md-sys-color-on-primary-container
+ );
+ --md-filled-button-container-color: var(--sys-log-viewer-color-primary);
+
+ /* Log View */
+ --sys-log-viewer-height: 100%;
+ --sys-log-viewer-view-outline-width: 1px;
+ --sys-log-viewer-view-corner-radius: 0.5rem;
+
+ /* Log List */
+ --sys-log-viewer-table-cell-padding: 0.375rem 0.75rem;
+ --sys-log-viewer-table-cell-icon-size: 1.125rem;
+ }
+
+ .grid-container {
+ display: grid;
+ grid-gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(27rem, 1fr));
+ height: 100%;
+ overflow: hidden;
+ }
+
+ .add-button {
+ width: 8rem;
+ height: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+`;
diff --git a/pw_web/log-viewer/src/components/log-viewer.ts b/pw_web/log-viewer/src/components/log-viewer.ts
new file mode 100644
index 000000000..06c695309
--- /dev/null
+++ b/pw_web/log-viewer/src/components/log-viewer.ts
@@ -0,0 +1,193 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LitElement, PropertyValues, html } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { repeat } from 'lit/directives/repeat.js';
+import {
+ TableColumn,
+ LogEntry,
+ LogViewConfig,
+ State,
+} from '../shared/interfaces';
+import { StateStore } from '../shared/state';
+import { styles } from './log-viewer.styles';
+import { themeDark } from '../themes/dark';
+import { themeLight } from '../themes/light';
+import { LogView } from './log-view/log-view';
+import CloseViewEvent from '../events/close-view';
+
+type ColorScheme = 'dark' | 'light';
+
+/**
+ * The root component which renders one or more log views for displaying
+ * structured log entries.
+ *
+ * @element log-viewer
+ */
+@customElement('log-viewer')
+export class LogViewer extends LitElement {
+ static styles = [styles, themeDark, themeLight];
+
+ /** An array of log entries to be displayed. */
+ @property({ type: Array })
+ logs: LogEntry[] = [];
+
+ @property({ type: String, reflect: true })
+ colorScheme?: ColorScheme;
+
+ /** An array of rendered log view instances. */
+ @state()
+ _logViews: LogView[] = [];
+
+ /** An object that stores the state of log views */
+ @state()
+ _stateStore: StateStore;
+
+ private _state: State;
+
+ constructor(state: StateStore) {
+ super();
+ this._stateStore = state;
+ this._state = this._stateStore.getState();
+ }
+
+ protected firstUpdated(): void {
+ if (this._state.logViewConfig.length == 0) {
+ this.addLogView();
+ return;
+ }
+
+ const viewState = this._state.logViewConfig;
+ const viewEls = [];
+ for (const i in viewState) {
+ const view = new LogView();
+ view.id = viewState[i].viewID;
+ view.searchText = viewState[i].search;
+ viewEls.push(view);
+ this._logViews = viewEls;
+ }
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.addEventListener('close-view', this.handleCloseView);
+
+ // If color scheme isn't set manually, retrieve it from localStorage
+ if (!this.colorScheme) {
+ const storedScheme = localStorage.getItem(
+ 'colorScheme',
+ ) as ColorScheme | null;
+ if (storedScheme) {
+ this.colorScheme = storedScheme;
+ }
+ }
+ }
+
+ updated(changedProperties: PropertyValues) {
+ super.updated(changedProperties);
+
+ if (changedProperties.has('colorScheme') && this.colorScheme) {
+ // Only store in localStorage if color scheme is 'dark' or 'light'
+ if (this.colorScheme === 'light' || this.colorScheme === 'dark') {
+ localStorage.setItem('colorScheme', this.colorScheme);
+ } else {
+ localStorage.removeItem('colorScheme');
+ }
+ }
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ this.removeEventListener('close-view', this.handleCloseView);
+ }
+
+ /** Creates a new log view in the `_logViews` arrray. */
+ private addLogView() {
+ const newView = new LogView();
+ const newViewState = this.addLogViewState(newView);
+ const viewStates: State = { logViewConfig: this._state.logViewConfig };
+ viewStates.logViewConfig.push(newViewState);
+ this._logViews = [...this._logViews, newView];
+ this._stateStore.setState(viewStates);
+ this._state = viewStates;
+ }
+
+ /** Creates a new log view state to store in the state object. */
+ private addLogViewState(view: LogView): LogViewConfig {
+ const fieldColumns = [];
+ const fields = view.getFields();
+
+ for (const i in fields) {
+ const col: TableColumn = {
+ isVisible: true,
+ fieldName: fields[i],
+ characterLength: 0,
+ manualWidth: null,
+ };
+ fieldColumns.push(col);
+ }
+
+ const obj = {
+ columnData: fieldColumns,
+ search: '',
+ viewID: view.id,
+ viewTitle: 'Log View',
+ };
+
+ return obj as LogViewConfig;
+ }
+
+ /**
+ * Removes a log view when its Close button is clicked.
+ *
+ * @param event The event object dispatched by the log view controls.
+ */
+ private handleCloseView(event: CloseViewEvent) {
+ const viewId = event.detail.viewId;
+ const index = this._logViews.findIndex((view) => view.id === viewId);
+ this._logViews = this._logViews.filter((view) => view.id !== viewId);
+
+ if (index > -1) {
+ this._state.logViewConfig.splice(index, 1);
+ this._stateStore.setState(this._state);
+ }
+ }
+
+ render() {
+ return html`
+ <div class="grid-container">
+ ${repeat(
+ this._logViews,
+ (view) => view.id,
+ (view) => html`
+ <log-view
+ id=${view.id}
+ .logs=${this.logs}
+ .isOneOfMany=${this._logViews.length > 1}
+ .stateStore=${this._stateStore}
+ @add-view="${this.addLogView}"
+ ></log-view>
+ `,
+ )}
+ </div>
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'log-viewer': LogViewer;
+ }
+}
diff --git a/pw_web/log-viewer/src/createLogViewer.ts b/pw_web/log-viewer/src/createLogViewer.ts
new file mode 100644
index 000000000..10ce61859
--- /dev/null
+++ b/pw_web/log-viewer/src/createLogViewer.ts
@@ -0,0 +1,63 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LogViewer as RootComponent } from './components/log-viewer';
+import { StateStore, LocalStorageState } from './shared/state';
+import { LogEntry } from '../src/shared/interfaces';
+import { LogSource } from '../src/log-source';
+
+import '@material/web/button/filled-button.js';
+import '@material/web/button/outlined-button.js';
+import '@material/web/checkbox/checkbox.js';
+import '@material/web/field/outlined-field.js';
+import '@material/web/textfield/outlined-text-field.js';
+import '@material/web/textfield/filled-text-field.js';
+import '@material/web/icon/icon.js';
+import '@material/web/iconbutton/icon-button.js';
+import '@material/web/menu/menu.js';
+import '@material/web/menu/menu-item.js';
+
+export function createLogViewer(
+ logSource: LogSource,
+ root: HTMLElement,
+ state: StateStore = new LocalStorageState(),
+) {
+ const logViewer = new RootComponent(state);
+ const logs: LogEntry[] = [];
+ root.appendChild(logViewer);
+ let lastUpdateTimeoutId: NodeJS.Timeout;
+
+ // Define an event listener for the 'logEntry' event
+ const logEntryListener = (logEntry: LogEntry) => {
+ logs.push(logEntry);
+ logViewer.logs = logs;
+ if (lastUpdateTimeoutId) {
+ clearTimeout(lastUpdateTimeoutId);
+ }
+
+ // Call requestUpdate at most once every 100 milliseconds.
+ lastUpdateTimeoutId = setTimeout(() => {
+ const updatedLogs = [...logs];
+ logViewer.logs = updatedLogs;
+ }, 100);
+ };
+
+ // Add the event listener to the LogSource instance
+ logSource.addEventListener('logEntry', logEntryListener);
+
+ // Method to destroy and unsubscribe
+ return () => {
+ logSource.removeEventListener('logEntry', logEntryListener);
+ };
+}
diff --git a/pw_web/log-viewer/src/custom/json-log-source.ts b/pw_web/log-viewer/src/custom/json-log-source.ts
new file mode 100644
index 000000000..8914b1bc9
--- /dev/null
+++ b/pw_web/log-viewer/src/custom/json-log-source.ts
@@ -0,0 +1,131 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LogSource } from '../log-source';
+import { LogEntry, FieldData, Severity } from '../shared/interfaces';
+
+import log_data from './log_data.json';
+
+interface LevelToSeverity {
+ [level: number]: Severity;
+}
+
+export class JsonLogSource extends LogSource {
+ private intervalId: NodeJS.Timeout | null = null;
+ private logIndex: number = 0;
+ private previousLogTime: number = 0;
+
+ private logLevelToSeverity: LevelToSeverity = {
+ 10: Severity.DEBUG,
+ 20: Severity.INFO,
+ 21: Severity.INFO,
+ 30: Severity.WARNING,
+ 40: Severity.ERROR,
+ 50: Severity.CRITICAL,
+ 70: Severity.CRITICAL,
+ };
+
+ private nonAdditionalDataFields = [
+ '_hosttime',
+ 'levelname',
+ 'levelno',
+ 'args',
+ 'fields',
+ 'message',
+ 'time',
+ ];
+
+ constructor() {
+ super();
+ }
+
+ start(): void {
+ this.updateLogTime();
+
+ const getInterval = () => {
+ // Get the current log time
+ const next_log_time = Number(log_data[this.logIndex].time);
+ const wait_ms = 1000 * (next_log_time - this.previousLogTime);
+
+ this.updateLogTime();
+ return Math.round(wait_ms);
+ };
+
+ const readLogEntry = () => {
+ const logEntry = this.readLogEntryFromJson();
+ this.emitEvent('logEntry', logEntry);
+
+ const nextInterval = getInterval();
+ setTimeout(readLogEntry, nextInterval);
+ };
+
+ readLogEntry();
+ }
+
+ stop(): void {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ }
+
+ private updateLogTime(): void {
+ this.previousLogTime = Number(log_data[this.logIndex].time);
+ }
+
+ private updateLogIndex(): void {
+ this.logIndex += 1;
+ if (this.logIndex >= log_data.length) {
+ this.logIndex = 0;
+ }
+ }
+
+ readLogEntryFromJson(): LogEntry {
+ const data = log_data[this.logIndex];
+
+ const host_log_time = new Date(0); // Date set to epoch seconds 0
+ const host_log_epoch_seconds = Number(data.time);
+ host_log_time.setUTCSeconds(Math.trunc(host_log_epoch_seconds));
+ const host_log_epoch_milliseconds = Math.trunc(
+ 1000 * (host_log_epoch_seconds - Math.trunc(host_log_epoch_seconds)),
+ );
+ host_log_time.setUTCMilliseconds(host_log_epoch_milliseconds);
+
+ const fields: Array<FieldData> = [
+ { key: 'severity', value: this.logLevelToSeverity[data.levelno] },
+ { key: 'time', value: host_log_time },
+ ];
+
+ Object.keys(data.fields).forEach((columnName) => {
+ if (this.nonAdditionalDataFields.indexOf(columnName) === -1) {
+ // @ts-ignore
+ fields.push({ key: columnName, value: data.fields[columnName] });
+ }
+ });
+
+ fields.push({ key: 'message', value: data.message });
+ fields.push({ key: 'py_file', value: data.py_file || '' });
+ fields.push({ key: 'py_logger', value: data.py_logger || '' });
+
+ const logEntry: LogEntry = {
+ severity: this.logLevelToSeverity[data.levelno],
+ timestamp: new Date(),
+ fields: fields,
+ };
+
+ this.updateLogIndex();
+
+ return logEntry;
+ }
+}
diff --git a/pw_web/log-viewer/src/custom/log_data.json b/pw_web/log-viewer/src/custom/log_data.json
new file mode 100644
index 000000000..d006386d3
--- /dev/null
+++ b/pw_web/log-viewer/src/custom/log_data.json
@@ -0,0 +1,37 @@
+[
+ {
+ "message": "Sample log_data.json",
+ "levelno": 30,
+ "levelname": "WRN",
+ "time": "1692302986.4599075",
+ "time_string": "2023-08-17T13:09:46",
+ "fields": {
+ "module": "pigweedjs",
+ "timestamp": "1.0"
+ }
+ },
+ {
+ "message": "Log message 1",
+ "levelno": 20,
+ "levelname": "INF",
+ "time": "1692302986.4599075",
+ "time_string": "2023-08-17T13:09:46",
+ "fields": {
+ "module": "device",
+ "file": "sample_file.cc",
+ "timestamp": "1.0"
+ }
+ },
+ {
+ "message": "Log message 2",
+ "levelno": 20,
+ "levelname": "INF",
+ "time": "1692303000.1080465",
+ "time_string": "2023-08-17T13:10:00",
+ "fields": {
+ "module": "device",
+ "file": "sample_file.cc",
+ "timestamp": "14.0"
+ }
+ }
+]
diff --git a/pw_web/log-viewer/src/custom/mock-log-source.ts b/pw_web/log-viewer/src/custom/mock-log-source.ts
new file mode 100644
index 000000000..3f7afb96b
--- /dev/null
+++ b/pw_web/log-viewer/src/custom/mock-log-source.ts
@@ -0,0 +1,132 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LogSource } from '../log-source';
+import { LogEntry, Severity } from '../shared/interfaces';
+
+export class MockLogSource extends LogSource {
+ private intervalId: NodeJS.Timeout | null = null;
+
+ constructor() {
+ super();
+ }
+
+ start(): void {
+ const getRandomInterval = () => {
+ return Math.floor(Math.random() * (200 - 50 + 1) + 50);
+ };
+
+ const readLogEntry = () => {
+ const logEntry = this.readLogEntryFromHost();
+ this.emitEvent('logEntry', logEntry);
+
+ const nextInterval = getRandomInterval();
+ setTimeout(readLogEntry, nextInterval);
+ };
+
+ readLogEntry();
+ }
+
+ stop(): void {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ this.intervalId = null;
+ }
+ }
+
+ getSeverity(): Severity {
+ interface ValueWeightPair {
+ severity: Severity;
+ weight: number;
+ }
+
+ const valueWeightPairs: ValueWeightPair[] = [
+ { severity: Severity.INFO, weight: 7.45 },
+ { severity: Severity.DEBUG, weight: 0.25 },
+ { severity: Severity.WARNING, weight: 1.5 },
+ { severity: Severity.ERROR, weight: 0.5 },
+ { severity: Severity.CRITICAL, weight: 0.05 },
+ ];
+
+ const totalWeight = valueWeightPairs.reduce(
+ (acc, pair) => acc + pair.weight,
+ 0,
+ );
+ let randomValue = Severity.INFO;
+ let randomNum = Math.random() * totalWeight;
+
+ for (let i = 0; i < valueWeightPairs.length; i++) {
+ randomNum -= valueWeightPairs[i].weight;
+ if (randomNum <= 0) {
+ randomValue = valueWeightPairs[i].severity;
+ break;
+ }
+ }
+
+ return randomValue;
+ }
+
+ private formattedTS = () => {
+ const date = new Date();
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const day = date.getDate().toString().padStart(2, '0');
+ const year = date.getFullYear().toString().padStart(4, '0');
+ const hour = date.getHours().toString().padStart(2, '0');
+ const minute = date.getMinutes().toString().padStart(2, '0');
+ const second = date.getSeconds().toString().padStart(2, '0');
+ const millisecond = date.getMilliseconds().toString().padStart(3, '0');
+
+ const formattedDate = `${month}-${day}-${year} ${hour}:${minute}:${second}.${millisecond}`;
+ return formattedDate;
+ };
+
+ readLogEntryFromHost(): LogEntry {
+ // Emulate reading log data from a host device
+ const sources = ['application', 'server', 'database', 'network'];
+ const messages = [
+ 'Request processed successfully!',
+ 'An unexpected error occurred while performing the operation.',
+ 'Connection timed out. Please check your network settings.',
+ 'Invalid input detected. Please provide valid data.',
+ 'Database connection lost. Attempting to reconnect.',
+ 'User authentication failed. Invalid credentials provided.',
+ 'System reboot initiated. Please wait for the system to come back online.',
+ 'File not found. (The requested file does not exist).',
+ 'Data corruption detected. Initiating recovery process.',
+ 'Network congestion detected. Traffic is high, please try again later.',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam condimentum auctor justo, sit amet condimentum nibh facilisis non. Quisque in quam a urna dignissim cursus. Suspendisse egestas nisl sed massa dictum dictum. In tincidunt arcu nec odio eleifend, vel pharetra justo iaculis. Vivamus quis tellus ac velit vehicula consequat. Nam eu felis sed risus hendrerit faucibus ac id lacus. Vestibulum tincidunt tellus in ex feugiat interdum. Nulla sit amet luctus neque. Mauris et aliquet nunc, vel finibus massa. Curabitur laoreet eleifend nibh eget luctus. Fusce sodales augue nec purus faucibus, vel tristique enim vehicula. Aenean eu magna eros. Fusce accumsan dignissim dui auctor scelerisque. Proin ultricies nunc vel tincidunt facilisis.',
+ ];
+ const severity = this.getSeverity();
+ const timestamp: Date = new Date();
+
+ const formattedTimestamp: string = this.formattedTS();
+ const getRandomValue = (values: string[]) => {
+ const randomIndex = Math.floor(Math.random() * values.length);
+ return values[randomIndex];
+ };
+
+ const logEntry: LogEntry = {
+ severity: severity,
+ timestamp: timestamp,
+ fields: [
+ { key: 'severity', value: severity },
+ { key: 'timestamp', value: formattedTimestamp },
+ { key: 'source', value: getRandomValue(sources) },
+ { key: 'message', value: getRandomValue(messages) },
+ ],
+ };
+
+ return logEntry;
+ }
+}
diff --git a/pw_web/log-viewer/src/events/add-view.ts b/pw_web/log-viewer/src/events/add-view.ts
new file mode 100644
index 000000000..abe48877a
--- /dev/null
+++ b/pw_web/log-viewer/src/events/add-view.ts
@@ -0,0 +1,23 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+interface AddViewEvent extends CustomEvent {}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'add-view': AddViewEvent;
+ }
+}
+
+export default AddViewEvent;
diff --git a/pw_web/log-viewer/src/events/clear-logs.ts b/pw_web/log-viewer/src/events/clear-logs.ts
new file mode 100644
index 000000000..8ae69a258
--- /dev/null
+++ b/pw_web/log-viewer/src/events/clear-logs.ts
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+interface ClearLogsEvent extends CustomEvent {
+ detail: {
+ timestamp: Date;
+ };
+}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'clear-logs': ClearLogsEvent;
+ }
+}
+
+export default ClearLogsEvent;
diff --git a/pw_web/log-viewer/src/events/close-view.ts b/pw_web/log-viewer/src/events/close-view.ts
new file mode 100644
index 000000000..307752359
--- /dev/null
+++ b/pw_web/log-viewer/src/events/close-view.ts
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+interface CloseViewEvent extends CustomEvent {
+ detail: {
+ viewId: string;
+ };
+}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'close-view': CloseViewEvent;
+ }
+}
+
+export default CloseViewEvent;
diff --git a/pw_transfer/test_server.proto b/pw_web/log-viewer/src/events/column-toggle.ts
index c7b658e0d..8bacaee42 100644
--- a/pw_transfer/test_server.proto
+++ b/pw_web/log-viewer/src/events/column-toggle.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -12,13 +12,17 @@
// License for the specific language governing permissions and limitations under
// the License.
-syntax = "proto3";
-
-import 'pw_protobuf_protos/common.proto';
-
-package pw.transfer;
+interface ColumnToggleEvent extends CustomEvent {
+ detail: {
+ field: string;
+ isChecked: boolean;
+ };
+}
-// Manages the transfer service test RPC server (test_rpc_server.cc).
-service TestServer {
- rpc ReloadTransferFiles(pw.protobuf.Empty) returns (pw.protobuf.Empty);
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'column-toggle': ColumnToggleEvent;
+ }
}
+
+export default ColumnToggleEvent;
diff --git a/pw_web/log-viewer/src/events/download-logs.ts b/pw_web/log-viewer/src/events/download-logs.ts
new file mode 100644
index 000000000..fc9b54aa6
--- /dev/null
+++ b/pw_web/log-viewer/src/events/download-logs.ts
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+interface DownloadLogsEvent extends CustomEvent {
+ detail: {
+ format: string;
+ viewTitle: string;
+ };
+}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'download-logs': DownloadLogsEvent;
+ }
+}
+
+export default DownloadLogsEvent;
diff --git a/pw_web/log-viewer/src/events/input-change.ts b/pw_web/log-viewer/src/events/input-change.ts
new file mode 100644
index 000000000..29b69f91a
--- /dev/null
+++ b/pw_web/log-viewer/src/events/input-change.ts
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+interface InputChangeEvent extends CustomEvent {
+ detail: {
+ inputValue: string;
+ };
+}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'input-change': InputChangeEvent;
+ }
+}
+
+export default InputChangeEvent;
diff --git a/pw_web/log-viewer/src/events/wrap-toggle.ts b/pw_web/log-viewer/src/events/wrap-toggle.ts
new file mode 100644
index 000000000..de4f1022a
--- /dev/null
+++ b/pw_web/log-viewer/src/events/wrap-toggle.ts
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+interface WrapToggleEvent extends CustomEvent {
+ detail: {
+ isChecked: boolean;
+ };
+}
+
+declare global {
+ interface GlobalEventHandlersEventMap {
+ 'wrap-toggle': WrapToggleEvent;
+ }
+}
+
+export default WrapToggleEvent;
diff --git a/pw_web/log-viewer/src/index.css b/pw_web/log-viewer/src/index.css
new file mode 100644
index 000000000..04a79a2c2
--- /dev/null
+++ b/pw_web/log-viewer/src/index.css
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2023 The Pigweed Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,300;8..144,400;8..144,500;8..144,600&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block');
+
+:root {
+ background-color: #fff;
+ font-family: "Roboto Flex", Arial, sans-serif;
+ font-synthesis: none;
+ font-weight: 400;
+ line-height: 1.5;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-text-size-adjust: 100%;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ background-color: #131314;
+ }
+}
+
+* {
+ box-sizing: border-box;
+}
+
+button {
+ font-family: "Roboto Flex";
+}
+
+main {
+ height: 100vh;
+ padding: 16px;
+ width: 100vw;
+}
+
+@media (min-width: 840px) {
+ main {
+ padding: 24px;
+ }
+}
+
+a {
+ color: var(--md-sys-color-primary);
+ font-weight: 500;
+ text-decoration: inherit;
+}
+
+a:hover {
+ color: var(--md-sys-color-secondary);
+}
+
+body {
+ display: grid;
+ place-content: start;
+ margin: 0;
+} \ No newline at end of file
diff --git a/pw_web/log-viewer/src/index.ts b/pw_web/log-viewer/src/index.ts
new file mode 100644
index 000000000..b50399561
--- /dev/null
+++ b/pw_web/log-viewer/src/index.ts
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { JsonLogSource } from './custom/json-log-source';
+import { createLogViewer } from './createLogViewer';
+
+const logSource = new JsonLogSource();
+const containerEl = document.querySelector(
+ '#log-viewer-container',
+) as HTMLElement;
+
+if (containerEl) {
+ createLogViewer(logSource, containerEl);
+}
+
+// Start reading log data
+logSource.start();
diff --git a/pw_web/log-viewer/src/log-source.ts b/pw_web/log-viewer/src/log-source.ts
new file mode 100644
index 000000000..d000a46d0
--- /dev/null
+++ b/pw_web/log-viewer/src/log-source.ts
@@ -0,0 +1,52 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LogEntry } from './shared/interfaces';
+
+export abstract class LogSource {
+ private eventListeners: {
+ eventType: string;
+ listener: (data: LogEntry) => void;
+ }[];
+
+ constructor() {
+ this.eventListeners = [];
+ }
+
+ addEventListener(
+ eventType: string,
+ listener: (data: LogEntry) => void,
+ ): void {
+ this.eventListeners.push({ eventType, listener });
+ }
+
+ removeEventListener(
+ eventType: string,
+ listener: (data: LogEntry) => void,
+ ): void {
+ this.eventListeners = this.eventListeners.filter(
+ (eventListener) =>
+ eventListener.eventType !== eventType ||
+ eventListener.listener !== listener,
+ );
+ }
+
+ emitEvent(eventType: string, data: LogEntry): void {
+ this.eventListeners.forEach((eventListener) => {
+ if (eventListener.eventType === eventType) {
+ eventListener.listener(data);
+ }
+ });
+ }
+}
diff --git a/pw_web/log-viewer/src/shared/interfaces.ts b/pw_web/log-viewer/src/shared/interfaces.ts
new file mode 100644
index 000000000..2b1ac5946
--- /dev/null
+++ b/pw_web/log-viewer/src/shared/interfaces.ts
@@ -0,0 +1,50 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+export interface FieldData {
+ key: string;
+ value: string | boolean | number | object;
+}
+
+export interface TableColumn {
+ fieldName: string;
+ characterLength: number;
+ manualWidth: number | null;
+ isVisible: boolean;
+}
+
+export interface LogEntry {
+ severity?: Severity;
+ timestamp: Date;
+ fields: FieldData[];
+}
+
+export interface LogViewConfig {
+ columnData: TableColumn[];
+ search: string;
+ viewID: string;
+ viewTitle: string;
+}
+
+export enum Severity {
+ DEBUG = 'DEBUG',
+ INFO = 'INFO',
+ WARNING = 'WARNING',
+ ERROR = 'ERROR',
+ CRITICAL = 'CRITICAL',
+}
+
+export interface State {
+ logViewConfig: LogViewConfig[];
+}
diff --git a/pw_web/log-viewer/src/shared/state.ts b/pw_web/log-viewer/src/shared/state.ts
new file mode 100644
index 000000000..87a379f87
--- /dev/null
+++ b/pw_web/log-viewer/src/shared/state.ts
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { State } from './interfaces';
+
+/**
+ * Abstract Class for StateStore.
+ */
+export abstract class StateStore {
+ abstract getState(): State;
+ abstract setState(state: State): void;
+}
+
+/**
+ * LocalStorage version of StateStore
+ */
+export class LocalStorageState extends StateStore {
+ getState(): State {
+ try {
+ const state = localStorage.getItem('logState') as string;
+ return state == null ? { logViewConfig: [] } : JSON.parse(state);
+ } catch (e) {
+ console.error(e);
+ return { logViewConfig: [] };
+ }
+ }
+
+ setState(state: State): void {
+ localStorage.setItem('logState', JSON.stringify(state));
+ }
+}
diff --git a/pw_web/log-viewer/src/themes/dark.ts b/pw_web/log-viewer/src/themes/dark.ts
new file mode 100644
index 000000000..a9744f520
--- /dev/null
+++ b/pw_web/log-viewer/src/themes/dark.ts
@@ -0,0 +1,211 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { css } from 'lit';
+
+export const themeDark = css`
+ /* Automatic theme styles */
+ @media (prefers-color-scheme: dark) {
+ :host {
+ color-scheme: dark;
+
+ /* Material Design tokens */
+ --md-sys-color-primary: #a8c7fa;
+ --md-sys-color-primary-60: #4c8df6;
+ --md-sys-color-primary-container: #0842a0;
+ --md-sys-color-on-primary: #062e6f;
+ --md-sys-color-on-primary-container: #d3e3fd;
+ --md-sys-color-inverse-primary: #0b57d0;
+ --md-sys-color-secondary: #7fcfff;
+ --md-sys-color-secondary-container: #004a77;
+ --md-sys-color-on-secondary: #003355;
+ --md-sys-color-on-secondary-container: #c2e7ff;
+ --md-sys-color-tertiary: #6dd58c;
+ --md-sys-color-tertiary-container: #0f5223;
+ --md-sys-color-on-tertiary: #0a3818;
+ --md-sys-color-on-tertiary-container: #c4eed0;
+ --md-sys-color-surface: #131314;
+ --md-sys-color-surface-dim: #131314;
+ --md-sys-color-surface-bright: #37393b;
+ --md-sys-color-surface-container-lowest: #0e0e0e;
+ --md-sys-color-surface-container-low: #1b1b1b;
+ --md-sys-color-surface-container: #1e1f20;
+ --md-sys-color-surface-container-high: #282a2c;
+ --md-sys-color-surface-container-highest: #333537;
+ --md-sys-color-on-surface: #e3e3e3;
+ --md-sys-color-on-surface-variant: #c4c7c5;
+ --md-sys-color-inverse-surface: #e3e3e3;
+ --md-sys-color-inverse-on-surface: #303030;
+ --md-sys-color-outline: #8e918f;
+ --md-sys-color-outline-variant: #444746;
+ --md-sys-color-shadow: #000000;
+ --md-sys-color-scrim: #000000;
+ --md-sys-color-inverse-surface-rgb: 230, 225, 229;
+
+ /* General */
+ --sys-log-viewer-color-primary: var(--md-sys-color-primary);
+ --sys-log-viewer-color-on-primary: var(--md-sys-color-on-primary);
+
+ /* Log Viewer */
+ --sys-log-viewer-color-bg: var(--md-sys-color-surface);
+
+ /* Log View */
+ --sys-log-viewer-color-view-outline: var(--md-sys-color-outline-variant);
+
+ /* Log View Controls */
+ --sys-log-viewer-color-controls-bg: var(
+ --md-sys-color-surface-container-high
+ );
+ --sys-log-viewer-color-controls-text: var(
+ --md-sys-color-on-surface-variant
+ );
+ --sys-log-viewer-color-controls-input-outline: transparent;
+ --sys-log-viewer-color-controls-input-bg: var(--md-sys-color-surface);
+ --sys-log-viewer-color-controls-button-enabled: var(
+ --md-sys-color-primary-container
+ );
+
+ /* Log List */
+ --sys-log-viewer-color-table-header-bg: var(
+ --md-sys-color-surface-container
+ );
+ --sys-log-viewer-color-table-header-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-bg: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-table-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-cell-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-overflow-indicator: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-table-mark: var(--md-sys-color-primary-container);
+ --sys-log-viewer-color-table-mark-text: var(
+ --md-sys-color-on-primary-container
+ );
+ --sys-log-viewer-color-table-mark-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-table-row-highlight: var(
+ --md-sys-color-inverse-surface-rgb
+ );
+
+ /* Severity */
+ --sys-log-viewer-color-error-bright: #e46962;
+ --sys-log-viewer-color-surface-error: #601410;
+ --sys-log-viewer-color-on-surface-error: #f9dedc;
+ --sys-log-viewer-color-orange-bright: #ee9836;
+ --sys-log-viewer-color-surface-yellow: #402d00;
+ --sys-log-viewer-color-on-surface-yellow: #ffdfa0;
+ --sys-log-viewer-color-debug: var(--md-sys-color-primary-60);
+ }
+ }
+
+ /* Manual theme styles */
+ :host([colorscheme='dark']) {
+ color-scheme: dark;
+
+ /* Material Design tokens */
+ --md-sys-color-primary: #a8c7fa;
+ --md-sys-color-primary-60: #4c8df6;
+ --md-sys-color-primary-container: #0842a0;
+ --md-sys-color-on-primary: #062e6f;
+ --md-sys-color-on-primary-container: #d3e3fd;
+ --md-sys-color-inverse-primary: #0b57d0;
+ --md-sys-color-secondary: #7fcfff;
+ --md-sys-color-secondary-container: #004a77;
+ --md-sys-color-on-secondary: #003355;
+ --md-sys-color-on-secondary-container: #c2e7ff;
+ --md-sys-color-tertiary: #6dd58c;
+ --md-sys-color-tertiary-container: #0f5223;
+ --md-sys-color-on-tertiary: #0a3818;
+ --md-sys-color-on-tertiary-container: #c4eed0;
+ --md-sys-color-surface: #131314;
+ --md-sys-color-surface-dim: #131314;
+ --md-sys-color-surface-bright: #37393b;
+ --md-sys-color-surface-container-lowest: #0e0e0e;
+ --md-sys-color-surface-container-low: #1b1b1b;
+ --md-sys-color-surface-container: #1e1f20;
+ --md-sys-color-surface-container-high: #282a2c;
+ --md-sys-color-surface-container-highest: #333537;
+ --md-sys-color-on-surface: #e3e3e3;
+ --md-sys-color-on-surface-variant: #c4c7c5;
+ --md-sys-color-inverse-surface: #e3e3e3;
+ --md-sys-color-inverse-on-surface: #303030;
+ --md-sys-color-outline: #8e918f;
+ --md-sys-color-outline-variant: #444746;
+ --md-sys-color-shadow: #000000;
+ --md-sys-color-scrim: #000000;
+ --md-sys-color-inverse-surface-rgb: 230, 225, 229;
+
+ /* General */
+ --sys-log-viewer-color-primary: var(--md-sys-color-primary);
+ --sys-log-viewer-color-on-primary: var(--md-sys-color-on-primary);
+
+ /* Log Viewer */
+ --sys-log-viewer-color-bg: var(--md-sys-color-surface);
+
+ /* Log View */
+ --sys-log-viewer-color-view-outline: var(--md-sys-color-outline-variant);
+
+ /* Log View Controls */
+ --sys-log-viewer-color-controls-bg: var(
+ --md-sys-color-surface-container-high
+ );
+ --sys-log-viewer-color-controls-text: var(
+ --md-sys-color-on-surface-variant
+ );
+ --sys-log-viewer-color-controls-input-outline: transparent;
+ --sys-log-viewer-color-controls-input-bg: var(--md-sys-color-surface);
+ --sys-log-viewer-color-controls-button-enabled: var(
+ --md-sys-color-primary-container
+ );
+
+ /* Log List */
+ --sys-log-viewer-color-table-header-bg: var(
+ --md-sys-color-surface-container
+ );
+ --sys-log-viewer-color-table-header-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-bg: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-table-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-cell-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-overflow-indicator: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-table-mark: var(--md-sys-color-primary-container);
+ --sys-log-viewer-color-table-mark-text: var(
+ --md-sys-color-on-primary-container
+ );
+ --sys-log-viewer-color-table-mark-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-table-row-highlight: var(
+ --md-sys-color-inverse-surface-rgb
+ );
+
+ /* Severity */
+ --sys-log-viewer-color-error-bright: #e46962;
+ --sys-log-viewer-color-surface-error: #601410;
+ --sys-log-viewer-color-on-surface-error: #f9dedc;
+ --sys-log-viewer-color-orange-bright: #ee9836;
+ --sys-log-viewer-color-surface-yellow: #402d00;
+ --sys-log-viewer-color-on-surface-yellow: #ffdfa0;
+ --sys-log-viewer-color-debug: var(--md-sys-color-primary-60);
+ }
+`;
diff --git a/pw_web/log-viewer/src/themes/light.ts b/pw_web/log-viewer/src/themes/light.ts
new file mode 100644
index 000000000..e179cfb95
--- /dev/null
+++ b/pw_web/log-viewer/src/themes/light.ts
@@ -0,0 +1,213 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { css } from 'lit';
+
+export const themeLight = css`
+ /* Automatic theme styles */
+ @media (prefers-color-scheme: light) {
+ :host {
+ color-scheme: light;
+
+ /* Material Design tokens */
+ --md-sys-color-primary: #0b57d0;
+ --md-sys-color-primary-70: #7cacf8;
+ --md-sys-color-primary-90: #d3e3fd;
+ --md-sys-color-primary-95: #ecf3fe;
+ --md-sys-color-primary-99: #fafbff;
+ --md-sys-color-primary-container: #d3e3fd;
+ --md-sys-color-on-primary: #ffffff;
+ --md-sys-color-on-primary-container: #041e49;
+ --md-sys-color-inverse-primary: #a8c7fa;
+ --md-sys-color-secondary: #00639b;
+ --md-sys-color-secondary-container: #c2e7ff;
+ --md-sys-color-on-secondary: #ffffff;
+ --md-sys-color-on-secondary-container: #001d35;
+ --md-sys-color-tertiary: #146c2e;
+ --md-sys-color-tertiary-container: #c4eed0;
+ --md-sys-color-on-tertiary: #ffffff;
+ --md-sys-color-on-tertiary-container: #072711;
+ --md-sys-color-surface: #ffffff;
+ --md-sys-color-surface-dim: #d3dbe5;
+ --md-sys-color-surface-bright: #ffffff;
+ --md-sys-color-surface-container-lowest: #ffffff;
+ --md-sys-color-surface-container-low: #f8fafd;
+ --md-sys-color-surface-container: #f0f4f9;
+ --md-sys-color-surface-container-high: #e9eef6;
+ --md-sys-color-surface-container-highest: #dde3ea;
+ --md-sys-color-on-surface: #1f1f1f;
+ --md-sys-color-on-surface-variant: #444746;
+ --md-sys-color-inverse-surface: #303030;
+ --md-sys-color-inverse-on-surface: #f2f2f2;
+ --md-sys-color-outline: #747775;
+ --md-sys-color-outline-variant: #c4c7c5;
+ --md-sys-color-shadow: #000000;
+ --md-sys-color-scrim: #000000;
+ --md-sys-color-inverse-surface-rgb: 49, 48, 51;
+
+ /* General */
+ --sys-log-viewer-color-primary: var(--md-sys-color-primary);
+ --sys-log-viewer-color-on-primary: var(--md-sys-color-on-primary);
+
+ /* Log Viewer */
+ --sys-log-viewer-color-bg: var(--md-sys-color-surface);
+
+ /* Log View */
+ --sys-log-viewer-color-view-outline: var(--md-sys-color-outline);
+
+ /* Log View Controls */
+ --sys-log-viewer-color-controls-bg: var(--md-sys-color-primary-90);
+ --sys-log-viewer-color-controls-text: var(
+ --md-sys-color-on-primary-container
+ );
+ --sys-log-viewer-color-controls-input-outline: transparent;
+ --sys-log-viewer-color-controls-input-bg: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-controls-button-enabled: var(
+ --md-sys-color-primary-70
+ );
+
+ /* Log List */
+ --sys-log-viewer-color-table-header-bg: var(--md-sys-color-primary-95);
+ --sys-log-viewer-color-table-header-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-bg: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-table-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-cell-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-overflow-indicator: var(
+ --md-sys-color-surface-container
+ );
+ --sys-log-viewer-color-table-mark: var(--md-sys-color-primary-container);
+ --sys-log-viewer-color-table-mark-text: var(
+ --md-sys-color-on-primary-container
+ );
+ --sys-log-viewer-color-table-mark-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-table-row-highlight: var(
+ --md-sys-color-inverse-surface-rgb
+ );
+
+ /* Severity */
+ --sys-log-viewer-color-error-bright: #dc362e;
+ --sys-log-viewer-color-surface-error: #fcefee;
+ --sys-log-viewer-color-on-surface-error: #8c1d18;
+ --sys-log-viewer-color-orange-bright: #f49f2a;
+ --sys-log-viewer-color-surface-yellow: #fef9eb;
+ --sys-log-viewer-color-on-surface-yellow: #783616;
+ --sys-log-viewer-color-debug: var(--md-sys-color-primary);
+ }
+ }
+
+ /* Manual theme styles */
+ :host([colorscheme='light']) {
+ color-scheme: light;
+
+ /* Material Design tokens */
+ --md-sys-color-primary: #0b57d0;
+ --md-sys-color-primary-70: #7cacf8;
+ --md-sys-color-primary-90: #d3e3fd;
+ --md-sys-color-primary-95: #ecf3fe;
+ --md-sys-color-primary-99: #fafbff;
+ --md-sys-color-primary-container: #d3e3fd;
+ --md-sys-color-on-primary: #ffffff;
+ --md-sys-color-on-primary-container: #041e49;
+ --md-sys-color-inverse-primary: #a8c7fa;
+ --md-sys-color-secondary: #00639b;
+ --md-sys-color-secondary-container: #c2e7ff;
+ --md-sys-color-on-secondary: #ffffff;
+ --md-sys-color-on-secondary-container: #001d35;
+ --md-sys-color-tertiary: #146c2e;
+ --md-sys-color-tertiary-container: #c4eed0;
+ --md-sys-color-on-tertiary: #ffffff;
+ --md-sys-color-on-tertiary-container: #072711;
+ --md-sys-color-surface: #ffffff;
+ --md-sys-color-surface-dim: #d3dbe5;
+ --md-sys-color-surface-bright: #ffffff;
+ --md-sys-color-surface-container-lowest: #ffffff;
+ --md-sys-color-surface-container-low: #f8fafd;
+ --md-sys-color-surface-container: #f0f4f9;
+ --md-sys-color-surface-container-high: #e9eef6;
+ --md-sys-color-surface-container-highest: #dde3ea;
+ --md-sys-color-on-surface: #1f1f1f;
+ --md-sys-color-on-surface-variant: #444746;
+ --md-sys-color-inverse-surface: #303030;
+ --md-sys-color-inverse-on-surface: #f2f2f2;
+ --md-sys-color-outline: #747775;
+ --md-sys-color-outline-variant: #c4c7c5;
+ --md-sys-color-shadow: #000000;
+ --md-sys-color-scrim: #000000;
+ --md-sys-color-inverse-surface-rgb: 49, 48, 51;
+
+ /* General */
+ --sys-log-viewer-color-primary: var(--md-sys-color-primary);
+ --sys-log-viewer-color-on-primary: var(--md-sys-color-on-primary);
+
+ /* Log Viewer */
+ --sys-log-viewer-color-bg: var(--md-sys-color-surface);
+
+ /* Log View */
+ --sys-log-viewer-color-view-outline: var(--md-sys-color-outline);
+
+ /* Log View Controls */
+ --sys-log-viewer-color-controls-bg: var(--md-sys-color-primary-90);
+ --sys-log-viewer-color-controls-text: var(
+ --md-sys-color-on-primary-container
+ );
+ --sys-log-viewer-color-controls-input-outline: transparent;
+ --sys-log-viewer-color-controls-input-bg: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-controls-button-enabled: var(
+ --md-sys-color-primary-70
+ );
+
+ /* Log List */
+ --sys-log-viewer-color-table-header-bg: var(--md-sys-color-primary-95);
+ --sys-log-viewer-color-table-header-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-bg: var(
+ --md-sys-color-surface-container-lowest
+ );
+ --sys-log-viewer-color-table-text: var(--md-sys-color-on-surface);
+ --sys-log-viewer-color-table-cell-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-overflow-indicator: var(
+ --md-sys-color-surface-container
+ );
+ --sys-log-viewer-color-table-mark: var(--md-sys-color-primary-container);
+ --sys-log-viewer-color-table-mark-text: var(
+ --md-sys-color-on-primary-container
+ );
+ --sys-log-viewer-color-table-mark-outline: var(
+ --md-sys-color-outline-variant
+ );
+ --sys-log-viewer-color-table-row-highlight: var(
+ --md-sys-color-inverse-surface-rgb
+ );
+
+ /* Severity */
+ --sys-log-viewer-color-error-bright: #dc362e;
+ --sys-log-viewer-color-surface-error: #fcefee;
+ --sys-log-viewer-color-on-surface-error: #8c1d18;
+ --sys-log-viewer-color-orange-bright: #f49f2a;
+ --sys-log-viewer-color-surface-yellow: #fef9eb;
+ --sys-log-viewer-color-on-surface-yellow: #783616;
+ --sys-log-viewer-color-debug: var(--md-sys-color-primary);
+ }
+`;
diff --git a/pw_web/log-viewer/src/utils/log-filter/log-filter.models.ts b/pw_web/log-viewer/src/utils/log-filter/log-filter.models.ts
new file mode 100644
index 000000000..2fabd7b51
--- /dev/null
+++ b/pw_web/log-viewer/src/utils/log-filter/log-filter.models.ts
@@ -0,0 +1,61 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+export enum ConditionType {
+ StringSearch,
+ ColumnSearch,
+ ExactPhraseSearch,
+ AndExpression,
+ OrExpression,
+ NotExpression,
+}
+
+export type StringSearchCondition = {
+ type: ConditionType.StringSearch;
+ searchString: string;
+};
+
+export type ColumnSearchCondition = {
+ type: ConditionType.ColumnSearch;
+ column: string;
+ value?: string;
+};
+
+export type ExactPhraseSearchCondition = {
+ type: ConditionType.ExactPhraseSearch;
+ exactPhrase: string;
+};
+
+export type AndExpressionCondition = {
+ type: ConditionType.AndExpression;
+ expressions: FilterCondition[];
+};
+
+export type OrExpressionCondition = {
+ type: ConditionType.OrExpression;
+ expressions: FilterCondition[];
+};
+
+export type NotExpressionCondition = {
+ type: ConditionType.NotExpression;
+ expression: FilterCondition;
+};
+
+export type FilterCondition =
+ | ColumnSearchCondition
+ | StringSearchCondition
+ | ExactPhraseSearchCondition
+ | AndExpressionCondition
+ | OrExpressionCondition
+ | NotExpressionCondition;
diff --git a/pw_web/log-viewer/src/utils/log-filter/log-filter.ts b/pw_web/log-viewer/src/utils/log-filter/log-filter.ts
new file mode 100644
index 000000000..344bba790
--- /dev/null
+++ b/pw_web/log-viewer/src/utils/log-filter/log-filter.ts
@@ -0,0 +1,254 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LogEntry } from '../../shared/interfaces';
+import { FilterCondition, ConditionType } from './log-filter.models';
+
+export class LogFilter {
+ /**
+ * Generates a structured representation of filter conditions which can be
+ * used to filter log entries.
+ *
+ * @param {string} searchQuery - The search query string provided.
+ * @returns {function[]} An array of filter functions, each representing a
+ * set of conditions grouped by logical operators, for filtering log
+ * entries.
+ */
+ static parseSearchQuery(searchQuery: string): FilterCondition[] {
+ const filters: FilterCondition[] = [];
+ const orGroups = searchQuery.split(/\s*\|\s*/);
+
+ for (let i = 0; i < orGroups.length; i++) {
+ let orGroup = orGroups[i];
+
+ if (orGroup.includes('(') && !orGroup.includes(')')) {
+ let j = i + 1;
+ while (j < orGroups.length && !orGroups[j].includes(')')) {
+ orGroup += ` | ${orGroups[j]}`;
+ j++;
+ }
+
+ if (j < orGroups.length) {
+ orGroup += ` | ${orGroups[j]}`;
+ i = j;
+ }
+ }
+
+ const andConditions = orGroup.match(
+ /\([^()]*\)|"[^"]+"|[^\s:]+:[^\s]+|[^\s]+/g,
+ );
+
+ const andFilters: FilterCondition[] = [];
+
+ if (andConditions) {
+ for (const condition of andConditions) {
+ if (condition.startsWith('(') && condition.endsWith(')')) {
+ const nestedConditions = condition.slice(1, -1).trim();
+ andFilters.push(...this.parseSearchQuery(nestedConditions));
+ } else if (condition.startsWith('"') && condition.endsWith('"')) {
+ const exactPhrase = condition.slice(1, -1).trim();
+ andFilters.push({
+ type: ConditionType.ExactPhraseSearch,
+ exactPhrase,
+ });
+ } else if (condition.startsWith('!')) {
+ const column = condition.slice(1, condition.indexOf(':'));
+ const value = condition.slice(condition.indexOf(':') + 1);
+ andFilters.push({
+ type: ConditionType.NotExpression,
+ expression: {
+ type: ConditionType.ColumnSearch,
+ column,
+ value,
+ },
+ });
+ } else if (condition.endsWith(':')) {
+ const column = condition.slice(0, condition.indexOf(':'));
+ andFilters.push({
+ type: ConditionType.ColumnSearch,
+ column,
+ });
+ } else if (condition.includes(':')) {
+ const column = condition.slice(0, condition.indexOf(':'));
+ const value = condition.slice(condition.indexOf(':') + 1);
+ andFilters.push({
+ type: ConditionType.ColumnSearch,
+ column,
+ value,
+ });
+ } else {
+ andFilters.push({
+ type: ConditionType.StringSearch,
+ searchString: condition,
+ });
+ }
+ }
+ }
+
+ if (andFilters.length > 0) {
+ if (andFilters.length === 1) {
+ filters.push(andFilters[0]);
+ } else {
+ filters.push({
+ type: ConditionType.AndExpression,
+ expressions: andFilters,
+ });
+ }
+ }
+ }
+
+ if (filters.length === 0) {
+ filters.push({
+ type: ConditionType.StringSearch,
+ searchString: '',
+ });
+ }
+
+ if (filters.length > 1) {
+ return [
+ {
+ type: ConditionType.OrExpression,
+ expressions: filters,
+ },
+ ];
+ }
+
+ return filters;
+ }
+
+ /**
+ * Takes a condition node, which represents a specific filter condition, and
+ * recursively generates a filter function that can be applied to log
+ * entries.
+ *
+ * @param {FilterCondition} condition - A filter condition to convert to a
+ * function.
+ * @returns {function} A function for filtering log entries based on the
+ * input condition and its logical operators.
+ */
+ static createFilterFunction(
+ condition: FilterCondition,
+ ): (logEntry: LogEntry) => boolean {
+ switch (condition.type) {
+ case ConditionType.StringSearch:
+ return (logEntry) =>
+ this.checkStringInColumns(logEntry, condition.searchString);
+ case ConditionType.ExactPhraseSearch:
+ return (logEntry) =>
+ this.checkExactPhraseInColumns(logEntry, condition.exactPhrase);
+ case ConditionType.ColumnSearch:
+ return (logEntry) =>
+ this.checkColumn(logEntry, condition.column, condition.value);
+ case ConditionType.NotExpression: {
+ const innerFilter = this.createFilterFunction(condition.expression);
+ return (logEntry) => !innerFilter(logEntry);
+ }
+ case ConditionType.AndExpression: {
+ const andFilters = condition.expressions.map((expr) =>
+ this.createFilterFunction(expr),
+ );
+ return (logEntry) => andFilters.every((filter) => filter(logEntry));
+ }
+ case ConditionType.OrExpression: {
+ const orFilters = condition.expressions.map((expr) =>
+ this.createFilterFunction(expr),
+ );
+ return (logEntry) => orFilters.some((filter) => filter(logEntry));
+ }
+ default:
+ // Return a filter that matches all entries
+ return () => true;
+ }
+ }
+
+ /**
+ * Checks if the column exists in a log entry and then performs a value
+ * search on the column's value.
+ *
+ * @param {LogEntry} logEntry - The log entry to be searched.
+ * @param {string} column - The name of the column (log entry field) to be
+ * checked for filtering.
+ * @param {string} value - An optional string that represents the value used
+ * for filtering.
+ * @returns {boolean} True if the specified column exists in the log entry,
+ * or if a value is provided, returns true if the value matches a
+ * substring of the column's value (case-insensitive).
+ */
+ private static checkColumn(
+ logEntry: LogEntry,
+ column: string,
+ value?: string,
+ ): boolean {
+ const fieldData = logEntry.fields.find((field) => field.key === column);
+ if (!fieldData) return false;
+
+ if (value === undefined) {
+ return true;
+ }
+
+ const searchRegex = new RegExp(value, 'i');
+ return searchRegex.test(fieldData.value.toString());
+ }
+
+ /**
+ * Checks if the provided search string exists in any of the log entry
+ * columns (excluding `severity`).
+ *
+ * @param {LogEntry} logEntry - The log entry to be searched.
+ * @param {string} searchString - The search string to be matched against
+ * the log entry fields.
+ * @returns {boolean} True if the search string is found in any of the log
+ * entry fields, otherwise false.
+ */
+ private static checkStringInColumns(
+ logEntry: LogEntry,
+ searchString: string,
+ ): boolean {
+ const escapedSearchString = this.escapeRegEx(searchString);
+ const columnsToSearch = logEntry.fields.filter(
+ (field) => field.key !== 'severity',
+ );
+ return columnsToSearch.some((field) =>
+ new RegExp(escapedSearchString, 'i').test(field.value.toString()),
+ );
+ }
+
+ /**
+ * Checks if the exact phrase exists in any of the log entry columns
+ * (excluding `severity`).
+ *
+ * @param {LogEntry} logEntry - The log entry to be searched.
+ * @param {string} exactPhrase - The exact phrase to search for within the
+ * log entry columns.
+ * @returns {boolean} True if the exact phrase is found in any column,
+ * otherwise false.
+ */
+ private static checkExactPhraseInColumns(
+ logEntry: LogEntry,
+ exactPhrase: string,
+ ): boolean {
+ const escapedExactPhrase = this.escapeRegEx(exactPhrase);
+ const searchRegex = new RegExp(escapedExactPhrase, 'i');
+ const columnsToSearch = logEntry.fields.filter(
+ (field) => field.key !== 'severity',
+ );
+ return columnsToSearch.some((field) =>
+ searchRegex.test(field.value.toString()),
+ );
+ }
+
+ private static escapeRegEx(text: string) {
+ return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+ }
+}
diff --git a/pw_web/log-viewer/src/utils/log-filter/log-filter_test.ts b/pw_web/log-viewer/src/utils/log-filter/log-filter_test.ts
new file mode 100644
index 000000000..63faa39a0
--- /dev/null
+++ b/pw_web/log-viewer/src/utils/log-filter/log-filter_test.ts
@@ -0,0 +1,173 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { LogFilter } from './log-filter';
+import { Severity, LogEntry } from '../../shared/interfaces';
+import { describe, expect, test } from '@jest/globals';
+import testData from './test-data';
+
+describe('LogFilter', () => {
+ describe('parseSearchQuery()', () => {
+ describe('parses search queries correctly', () => {
+ testData.forEach(({ query, expected }) => {
+ test(`parses "${query}" correctly`, () => {
+ const filters = LogFilter.parseSearchQuery(query);
+ expect(filters).toEqual(expected);
+ });
+ });
+ });
+ });
+
+ describe('createFilterFunction()', () => {
+ describe('filters log entries correctly', () => {
+ const logEntry1: LogEntry = {
+ timestamp: new Date(),
+ severity: Severity.INFO,
+ fields: [
+ { key: 'source', value: 'application' },
+ {
+ key: 'message',
+ value: 'Request processed successfully!',
+ },
+ ],
+ };
+
+ const logEntry2: LogEntry = {
+ timestamp: new Date(),
+ severity: Severity.WARNING,
+ fields: [
+ { key: 'source', value: 'database' },
+ {
+ key: 'message',
+ value: 'Database connection lost. Attempting to reconnect.',
+ },
+ ],
+ };
+
+ const logEntry3: LogEntry = {
+ timestamp: new Date(),
+ severity: Severity.ERROR,
+ fields: [
+ { key: 'source', value: 'network' },
+ {
+ key: 'message',
+ value:
+ 'An unexpected error occurred while performing the operation.',
+ },
+ ],
+ };
+
+ const logEntries = [logEntry1, logEntry2, logEntry3];
+
+ test('should filter by simple string search', () => {
+ const searchQuery = 'error';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry3]);
+ });
+
+ test('should filter by column-specific search', () => {
+ const searchQuery = 'source:database';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry2]);
+ });
+
+ test('should filter by exact phrase', () => {
+ const searchQuery = '"Request processed successfully!"';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry1]);
+ });
+
+ test('should filter by column presence', () => {
+ const searchQuery = 'source:';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([
+ logEntry1,
+ logEntry2,
+ logEntry3,
+ ]);
+ });
+
+ test('should handle AND expressions', () => {
+ const searchQuery = 'source:network message:error';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry3]);
+ });
+
+ test('should handle OR expressions', () => {
+ const searchQuery = 'source:database | source:network';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry2, logEntry3]);
+ });
+
+ test('should handle NOT expressions', () => {
+ const searchQuery = '!source:database';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry1, logEntry3]);
+ });
+
+ test('should handle a combination of AND and OR expressions', () => {
+ const searchQuery = '(source:database | source:network) message:error';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry3]);
+ });
+
+ test('should handle a combination of AND, OR, and NOT expressions', () => {
+ const searchQuery =
+ '(source:application | source:database) !message:request';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([logEntry2]);
+ });
+
+ test('should handle an empty query', () => {
+ const searchQuery = '';
+ const filters = LogFilter.parseSearchQuery(searchQuery).map((node) =>
+ LogFilter.createFilterFunction(node),
+ );
+ expect(filters.length).toBe(1);
+ expect(logEntries.filter(filters[0])).toEqual([
+ logEntry1,
+ logEntry2,
+ logEntry3,
+ ]);
+ });
+ });
+ });
+});
diff --git a/pw_web/log-viewer/src/utils/log-filter/test-data.ts b/pw_web/log-viewer/src/utils/log-filter/test-data.ts
new file mode 100644
index 000000000..e7fd7c138
--- /dev/null
+++ b/pw_web/log-viewer/src/utils/log-filter/test-data.ts
@@ -0,0 +1,361 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { ConditionType } from './log-filter.models';
+
+const testData = [
+ {
+ query: 'error',
+ expected: [
+ {
+ type: ConditionType.StringSearch,
+ searchString: 'error',
+ },
+ ],
+ },
+ {
+ query: 'source:database',
+ expected: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ ],
+ },
+ {
+ query: '"Request processed successfully!"',
+ expected: [
+ {
+ type: ConditionType.ExactPhraseSearch,
+ exactPhrase: 'Request processed successfully!',
+ },
+ ],
+ },
+ {
+ query: 'source:',
+ expected: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ },
+ ],
+ },
+ {
+ query: 'source:network message:error',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'network',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'message',
+ value: 'error',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: 'source:database | source:network',
+ expected: [
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'network',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: '!source:database',
+ expected: [
+ {
+ type: ConditionType.NotExpression,
+ expression: {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ },
+ ],
+ },
+ {
+ query: 'message:error (source:database | source:network)',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'message',
+ value: 'error',
+ },
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'network',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: '(source:database | source:network) message:error',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'network',
+ },
+ ],
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'message',
+ value: 'error',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: '(source:application | source:database) !message:request',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'application',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ ],
+ },
+ {
+ type: ConditionType.NotExpression,
+ expression: {
+ type: ConditionType.ColumnSearch,
+ column: 'message',
+ value: 'request',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: '',
+ expected: [
+ {
+ type: ConditionType.StringSearch,
+ searchString: '',
+ },
+ ],
+ },
+ {
+ // Note: AND takes priority over OR in evaluation.
+ query: 'source:database message:error | source:network message:error',
+ expected: [
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'message',
+ value: 'error',
+ },
+ ],
+ },
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'network',
+ },
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'message',
+ value: 'error',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: 'source:database | error',
+ expected: [
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ {
+ type: ConditionType.StringSearch,
+ searchString: 'error',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: 'source:application request',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'application',
+ },
+ {
+ type: ConditionType.StringSearch,
+ searchString: 'request',
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ query: 'source: application request',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ },
+ {
+ type: ConditionType.StringSearch,
+ searchString: 'application',
+ },
+ {
+ type: ConditionType.StringSearch,
+ searchString: 'request',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: 'source:network | (source:database lorem)',
+ expected: [
+ {
+ type: ConditionType.OrExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'network',
+ },
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ColumnSearch,
+ column: 'source',
+ value: 'database',
+ },
+ {
+ type: ConditionType.StringSearch,
+ searchString: 'lorem',
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ query: '"unexpected error" "the operation"',
+ expected: [
+ {
+ type: ConditionType.AndExpression,
+ expressions: [
+ {
+ type: ConditionType.ExactPhraseSearch,
+ exactPhrase: 'unexpected error',
+ },
+ {
+ type: ConditionType.ExactPhraseSearch,
+ exactPhrase: 'the operation',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export default testData;
diff --git a/pw_web/log-viewer/src/utils/strings.ts b/pw_web/log-viewer/src/utils/strings.ts
new file mode 100644
index 000000000..0214c0287
--- /dev/null
+++ b/pw_web/log-viewer/src/utils/strings.ts
@@ -0,0 +1,22 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+export function titleCaseToKebabCase(input: string) {
+ return input
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, '') // Remove special characters except spaces and hyphens
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
+ .replace(/-+/g, '-') // Replace consecutive hyphens with a single hyphen
+ .trim(); // Remove leading and trailing spaces
+}
diff --git a/pw_web/log-viewer/tsconfig.json b/pw_web/log-viewer/tsconfig.json
new file mode 100644
index 000000000..bfb2b5c72
--- /dev/null
+++ b/pw_web/log-viewer/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2021",
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "module": "ESNext",
+ "lib": [
+ "ES2021",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+ "incremental": true
+ },
+ "include": [
+ "src/**/*",
+ "src/createLogViewer.ts"
+ ]
+}
diff --git a/pw_web/log-viewer/vite.config.js b/pw_web/log-viewer/vite.config.js
new file mode 100644
index 000000000..34fdcfdb1
--- /dev/null
+++ b/pw_web/log-viewer/vite.config.js
@@ -0,0 +1,19 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import {defineConfig} from 'vite';
+
+export default defineConfig({
+ assetsInclude: ['**/*.pegjs'],
+});
diff --git a/pw_web/testing.rst b/pw_web/testing.rst
new file mode 100644
index 000000000..8b7f29f9c
--- /dev/null
+++ b/pw_web/testing.rst
@@ -0,0 +1,57 @@
+.. _module-pw_web-testing:
+
+=====================
+Manual Test Procedure
+=====================
+``pw_web`` is a web based log viewer and the manual tests here are intended
+to address situations where automated tests are not able to cover.
+
+Test Sections
+=============
+
+Log View Controls
+^^^^^^^^^^^^^^^^^
+
+.. list-table::
+ :widths: 5 45 45 5
+ :header-rows: 1
+
+ * - #
+ - Test Action
+ - Expected Result
+ - ✅
+
+ * - 1
+ - | Input bar is empty
+ | Press the :guilabel:`clear logs` button (trash can with lines)
+ - | Logs are cleared and entries after time of button press are addded.
+ - |checkbox|
+
+ * - 2
+ - | Input bar has a single word filter
+ | Press the :guilabel:`clear logs` button (trash can with lines)
+ - | Logs are cleared and filtered entries after time of button press are addded.
+ - |checkbox|
+
+ * - 3
+ - | Table is autoscrolling, scroll up
+ - | Autoscroll is disabled and :guilabel:`jump to bottom` button is visible.
+ - |checkbox|
+
+ * - 4
+ - | Press :guilabel:`clear logs` button (trash can with lines)
+ - | Autoscroll is enabled and :guilabel:`jump to bottom` button is not visible.
+ - |checkbox|
+
+Add note to the commit message
+==============================
+Add a ``Testing:`` line to your commit message and mention the steps
+executed. For example:
+
+.. code-block:: text
+
+ Testing: Log Pane Steps 1-6
+
+.. |checkbox| raw:: html
+
+ <input type="checkbox">
diff --git a/pw_web/webconsole/.eslintrc.json b/pw_web/webconsole/.eslintrc.json
deleted file mode 100644
index bffb357a7..000000000
--- a/pw_web/webconsole/.eslintrc.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "extends": "next/core-web-vitals"
-}
diff --git a/pw_web/webconsole/common/device.ts b/pw_web/webconsole/common/device.ts
index 5ba735b90..7e9773fa2 100644
--- a/pw_web/webconsole/common/device.ts
+++ b/pw_web/webconsole/common/device.ts
@@ -12,8 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Device} from "pigweedjs";
-import {createDefaultProtoCollection} from "./protos";
+import { Device } from 'pigweedjs';
+import { createDefaultProtoCollection } from './protos';
/**
* Returns an instance of Device, ensures there is only one Device in
diff --git a/pw_web/webconsole/common/logService.ts b/pw_web/webconsole/common/logService.ts
index 106520c90..f993c617f 100644
--- a/pw_web/webconsole/common/logService.ts
+++ b/pw_web/webconsole/common/logService.ts
@@ -12,14 +12,15 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Device} from "pigweedjs";
+import { Device } from 'pigweedjs';
export async function listenToDefaultLogService(
device: Device,
- onFrame: (frame: Uint8Array) => void) {
+ onFrame: (frame: Uint8Array) => void,
+) {
const call = device.rpcs.pw.log.Logs.Listen((msg: any) => {
msg.getEntriesList().forEach((entry: any) => onFrame(entry.getMessage()));
- })
+ });
return () => {
call.cancel();
};
diff --git a/pw_web/webconsole/common/protos.ts b/pw_web/webconsole/common/protos.ts
index 3524ad5ec..7fea0681d 100644
--- a/pw_web/webconsole/common/protos.ts
+++ b/pw_web/webconsole/common/protos.ts
@@ -12,9 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
-
export async function createDefaultProtoCollection() {
// @ts-ignore
- const ProtoCollection = await import("pigweedjs/protos/collection.umd");
+ const ProtoCollection = await import('pigweedjs/protos/collection.umd');
return new ProtoCollection.ProtoCollection();
}
diff --git a/pw_web/webconsole/components/repl/autocomplete.ts b/pw_web/webconsole/components/repl/autocomplete.ts
index 24d7efb3a..5a9b6e14f 100644
--- a/pw_web/webconsole/components/repl/autocomplete.ts
+++ b/pw_web/webconsole/components/repl/autocomplete.ts
@@ -12,81 +12,84 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {CompletionContext} from '@codemirror/autocomplete'
-import {syntaxTree} from '@codemirror/language'
-import {Device} from "pigweedjs";
+import { CompletionContext } from '@codemirror/autocomplete';
+import { syntaxTree } from '@codemirror/language';
+import { Device } from 'pigweedjs';
-const completePropertyAfter = ['PropertyName', '.', '?.']
+const completePropertyAfter = ['PropertyName', '.', '?.'];
const dontCompleteIn = [
'TemplateString',
'LineComment',
'BlockComment',
'VariableDefinition',
- 'PropertyDefinition'
-]
-var objectPath = require("object-path");
+ 'PropertyDefinition',
+];
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const objectPath = require('object-path');
export function completeFromGlobalScope(context: CompletionContext) {
- let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
+ const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
if (
completePropertyAfter.includes(nodeBefore.name) &&
nodeBefore.parent?.name == 'MemberExpression'
) {
- let object = nodeBefore.parent.getChild('Expression')
+ const object = nodeBefore.parent.getChild('Expression');
if (object?.name == 'VariableName') {
- let from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from
- let variableName = context.state.sliceDoc(object.from, object.to)
+ const from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;
+ const variableName = context.state.sliceDoc(object.from, object.to);
// @ts-ignore
if (typeof window[variableName] == 'object') {
// @ts-ignore
- return completeProperties(from, window[variableName])
+ return completeProperties(from, window[variableName]);
}
- }
- else if (object?.name == 'MemberExpression') {
- let from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from
- let variableName = context.state.sliceDoc(object.from, object.to)
- let variable = resolveWindowVariable(variableName);
+ } else if (object?.name == 'MemberExpression') {
+ const from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;
+ const variableName = context.state.sliceDoc(object.from, object.to);
+ const variable = resolveWindowVariable(variableName);
// @ts-ignore
if (typeof variable == 'object') {
// @ts-ignore
- return completeProperties(from, variable, variableName)
+ return completeProperties(from, variable, variableName);
}
}
} else if (nodeBefore.name == 'VariableName') {
- return completeProperties(nodeBefore.from, window)
+ return completeProperties(nodeBefore.from, window);
} else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {
- return completeProperties(context.pos, window)
+ return completeProperties(context.pos, window);
}
- return null
+ return null;
}
-function completeProperties(from: number, object: Object, variableName?: string) {
- let options = []
- for (let name in object) {
+function completeProperties(
+ from: number,
+ object: object,
+ variableName?: string,
+) {
+ const options = [];
+ for (const name in object) {
// @ts-ignore
if (object[name] instanceof Function && variableName) {
+ // eslint-disable-next-line no-debugger
debugger;
options.push({
label: name,
// @ts-ignore
detail: getFunctionDetailText(`${variableName}.${name}`),
- type: 'function'
- })
- }
- else {
+ type: 'function',
+ });
+ } else {
options.push({
label: name,
- type: 'variable'
- })
+ type: 'variable',
+ });
}
-
}
return {
from,
options,
- validFor: /^[\w$]*$/
- }
+ validFor: /^[\w$]*$/,
+ };
}
function resolveWindowVariable(variableName: string) {
@@ -96,12 +99,14 @@ function resolveWindowVariable(variableName: string) {
}
function getFunctionDetailText(fullExpression: string): string {
- if (fullExpression.startsWith("device.rpcs.")) {
- fullExpression = fullExpression.replace("device.rpcs.", "");
+ if (fullExpression.startsWith('device.rpcs.')) {
+ fullExpression = fullExpression.replace('device.rpcs.', '');
}
- const args = ((window as any).device as Device).getMethodArguments(fullExpression);
+ const args = ((window as any).device as Device).getMethodArguments(
+ fullExpression,
+ );
if (args) {
- return `(${args.join(", ")})`;
+ return `(${args.join(', ')})`;
}
- return "";
+ return '';
}
diff --git a/pw_web/webconsole/components/repl/basicSetup.ts b/pw_web/webconsole/components/repl/basicSetup.ts
index e3f0640ce..980c979dd 100644
--- a/pw_web/webconsole/components/repl/basicSetup.ts
+++ b/pw_web/webconsole/components/repl/basicSetup.ts
@@ -13,29 +13,43 @@
// the License.
import {
- keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor,
- rectangularSelection, crosshairCursor,
- highlightActiveLineGutter
-} from "@codemirror/view"
-import {Extension, EditorState} from "@codemirror/state"
+ keymap,
+ highlightSpecialChars,
+ drawSelection,
+ highlightActiveLine,
+ dropCursor,
+ rectangularSelection,
+ crosshairCursor,
+ highlightActiveLineGutter,
+} from '@codemirror/view';
+import { Extension, EditorState } from '@codemirror/state';
import {
- defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching,
- foldGutter, foldKeymap
-} from "@codemirror/language"
-import {defaultKeymap, history, historyKeymap} from "@codemirror/commands"
-import {searchKeymap, highlightSelectionMatches} from "@codemirror/search"
-import {autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete"
-import {lintKeymap} from "@codemirror/lint"
+ defaultHighlightStyle,
+ syntaxHighlighting,
+ indentOnInput,
+ bracketMatching,
+ foldGutter,
+ foldKeymap,
+} from '@codemirror/language';
+import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
+import {
+ autocompletion,
+ completionKeymap,
+ closeBrackets,
+ closeBracketsKeymap,
+} from '@codemirror/autocomplete';
+import { lintKeymap } from '@codemirror/lint';
const defaultKeymapMinusEnterAndArrowUpDown = defaultKeymap.map((keymap) => {
- if (keymap.key === "Enter") {
- return {...keymap, key: "Shift-Enter"};
+ if (keymap.key === 'Enter') {
+ return { ...keymap, key: 'Shift-Enter' };
}
- if (keymap.key === "ArrowUp") {
- return {...keymap, key: "Shift-ArrowUp"}
+ if (keymap.key === 'ArrowUp') {
+ return { ...keymap, key: 'Shift-ArrowUp' };
}
- if (keymap.key === "ArrowDown") {
- return {...keymap, key: "Shift-ArrowDown"}
+ if (keymap.key === 'ArrowDown') {
+ return { ...keymap, key: 'Shift-ArrowDown' };
}
return keymap;
});
@@ -49,7 +63,7 @@ export const basicSetup: Extension = (() => [
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
- syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
autocompletion(),
@@ -64,6 +78,6 @@ export const basicSetup: Extension = (() => [
...historyKeymap,
...foldKeymap,
...completionKeymap,
- ...lintKeymap
- ])
-])()
+ ...lintKeymap,
+ ]),
+])();
diff --git a/pw_web/webconsole/components/repl/localStorageArray.ts b/pw_web/webconsole/components/repl/localStorageArray.ts
index 318d8ff77..224b9c363 100644
--- a/pw_web/webconsole/components/repl/localStorageArray.ts
+++ b/pw_web/webconsole/components/repl/localStorageArray.ts
@@ -15,13 +15,13 @@
export default class LocalStorageArray {
data: string[] = [];
maxSize: number;
- key: string = "__pw_repl_history";
+ key: string = '__pw_repl_history';
- constructor(maxSize: number = 4) {
+ constructor(maxSize = 4) {
this.maxSize = maxSize;
const curHistory = localStorage.getItem(this.key);
if (curHistory) {
- this.data = JSON.parse(localStorage.getItem(this.key)!)
+ this.data = JSON.parse(localStorage.getItem(this.key)!);
}
}
diff --git a/pw_web/webconsole/components/uploadDb.tsx b/pw_web/webconsole/components/uploadDb.tsx
index 11a6ea9a5..3b01fb0bd 100644
--- a/pw_web/webconsole/components/uploadDb.tsx
+++ b/pw_web/webconsole/components/uploadDb.tsx
@@ -20,16 +20,6 @@ interface Props {
onUpload: (db: string) => void
}
-function testTokenDB(tokenCsv: string) {
- const lines = tokenCsv.trim().split(/\r?\n/).map(line => line.split(/,/));
- lines.forEach((line) => {
- // CSV has no columns or has malformed number.
- if (line.length < 2 || !/^[a-fA-F0-9]+$/.test(line[0])) {
- throw new Error("Not a valid token database.")
- }
- });
-}
-
export default function BtnUploadDB({onUpload}: Props) {
const [uploaded, setUploaded] = useState(false);
const [error, setError] = useState("");
@@ -46,7 +36,6 @@ export default function BtnUploadDB({onUpload}: Props) {
onChange={async e => {
const tokenCsv = await readFile(e.target.files![0]);
try {
- testTokenDB(tokenCsv);
onUpload(tokenCsv);
setUploaded(true);
setError("");
diff --git a/pw_web/webconsole/next.config.js b/pw_web/webconsole/next.config.js
index 6ac9a17f5..d5badc12c 100644
--- a/pw_web/webconsole/next.config.js
+++ b/pw_web/webconsole/next.config.js
@@ -16,6 +16,6 @@
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
-}
+};
- module.exports = nextConfig
+module.exports = nextConfig;
diff --git a/pw_web/webconsole/package-lock.json b/pw_web/webconsole/package-lock.json
new file mode 100644
index 000000000..bc7046bd2
--- /dev/null
+++ b/pw_web/webconsole/package-lock.json
@@ -0,0 +1,6437 @@
+{
+ "name": "webconsole",
+ "version": "0.1.0",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "webconsole",
+ "version": "0.1.0",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.1.0",
+ "@codemirror/lang-javascript": "^6.0.2",
+ "@codemirror/language": "^6.2.1",
+ "@codemirror/theme-one-dark": "^6.0.0",
+ "@emotion/react": "^11.10.0",
+ "@emotion/styled": "^11.10.0",
+ "@mui/material": "^5.9.3",
+ "codemirror": "^6.0.1",
+ "next": "12.2.3",
+ "object-path": "^0.11.8",
+ "pigweedjs": "latest",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-virtualized": "^9.22.3",
+ "react-virtualized-auto-sizer": "^1.0.6",
+ "react-window": "^1.8.7",
+ "xterm": "^4.19.0",
+ "xterm-addon-fit": "^0.5.0"
+ },
+ "devDependencies": {
+ "@types/node": "18.6.3",
+ "@types/react": "18.0.15",
+ "@types/react-dom": "18.0.6",
+ "@types/react-virtualized": "^9.21.21",
+ "@types/react-window": "^1.8.5",
+ "eslint": "8.21.0",
+ "eslint-config-next": "12.2.3",
+ "sass": "^1.54.0",
+ "typescript": "4.7.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.21.4",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.21.4",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.21.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.19.4",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.19.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.18.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "license": "MIT"
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.21.0",
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.11"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.21.4",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.19.4",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.5.0",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.6.0",
+ "@lezer/common": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.2.2",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.2.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/lang-javascript": {
+ "version": "6.1.6",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/javascript": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.2.0",
+ "license": "MIT"
+ },
+ "node_modules/@codemirror/theme-one-dark": {
+ "version": "6.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.9.4",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/state": "^6.1.4",
+ "style-mod": "^4.0.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.10.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/serialize": "^1.1.1",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.1.3"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.10.7",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/sheet": "^1.2.1",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "stylis": "4.1.3"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.0",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.0",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.10.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/cache": "^11.10.5",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/unitless": "^0.8.0",
+ "@emotion/utils": "^1.2.0",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.2.1",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.10.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/is-prop-valid": "^1.2.0",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.0",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.2.0",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.3.0",
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.10.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/gitignore-to-minimatch": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@lezer/common": {
+ "version": "1.0.2",
+ "license": "MIT"
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.1.4",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/javascript": {
+ "version": "1.4.2",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/highlight": "^1.1.3",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@mui/base": {
+ "version": "5.0.0-alpha.125",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@emotion/is-prop-valid": "^1.2.0",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.12.0",
+ "@popperjs/core": "^2.11.7",
+ "clsx": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/core-downloads-tracker": {
+ "version": "5.12.0",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ }
+ },
+ "node_modules/@mui/material": {
+ "version": "5.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@mui/base": "5.0.0-alpha.125",
+ "@mui/core-downloads-tracker": "^5.12.0",
+ "@mui/system": "^5.12.0",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.12.0",
+ "@types/react-transition-group": "^4.4.5",
+ "clsx": "^1.2.1",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0",
+ "react-transition-group": "^4.4.5"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/private-theming": {
+ "version": "5.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@mui/utils": "^5.12.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/styled-engine": {
+ "version": "5.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@emotion/cache": "^11.10.7",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.4.1",
+ "@emotion/styled": "^11.3.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/system": {
+ "version": "5.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@mui/private-theming": "^5.12.0",
+ "@mui/styled-engine": "^5.12.0",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.12.0",
+ "clsx": "^1.2.1",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/types": {
+ "version": "7.2.4",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/utils": {
+ "version": "5.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.21.0",
+ "@types/prop-types": "^15.7.5",
+ "@types/react-is": "^16.7.1 || ^17.0.0",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "12.2.3",
+ "license": "MIT"
+ },
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "12.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "glob": "7.1.7"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "12.2.3",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.7",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@protobuf-ts/protoc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.9.0.tgz",
+ "integrity": "sha512-8mQmw+OVqrpaHgmErle8heE5Z1ktX2vmWJmvFORSTcHxMfqHLv8RKTRroC9pkBQRgUaSGSXmy7n4ItdZnz41dw==",
+ "bin": {
+ "protoc": "protoc.js"
+ }
+ },
+ "node_modules/@rushstack/eslint-patch": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.4.3",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "18.6.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.5",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.0.15",
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-is": {
+ "version": "17.0.3",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.5",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-virtualized": {
+ "version": "9.21.21",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/react": "^17"
+ }
+ },
+ "node_modules/@types/react-virtualized/node_modules/@types/react": {
+ "version": "17.0.58",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-window": {
+ "version": "1.8.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.3",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "5.58.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "5.58.0",
+ "@typescript-eslint/types": "5.58.0",
+ "@typescript-eslint/typescript-estree": "5.58.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.58.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.58.0",
+ "@typescript-eslint/visitor-keys": "5.58.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "5.58.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.58.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "5.58.0",
+ "@typescript-eslint/visitor-keys": "5.58.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.58.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "5.58.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.8.2",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.1.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "deep-equal": "^2.0.5"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0",
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.7",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.6.3",
+ "dev": true,
+ "license": "MPL-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "deep-equal": "^2.0.5"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001478",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.3",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "devOptional": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "1.2.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/codemirror": {
+ "version": "6.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "license": "MIT"
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/crelt": {
+ "version": "1.0.5",
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "license": "MIT"
+ },
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-equal": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-get-iterator": "^1.1.2",
+ "get-intrinsic": "^1.1.3",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.1",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.21.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.0",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "8.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint/eslintrc": "^1.3.0",
+ "@humanwhocodes/config-array": "^0.10.4",
+ "@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.3",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.15.0",
+ "globby": "^11.1.0",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-next": {
+ "version": "12.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@next/eslint-plugin-next": "12.2.3",
+ "@rushstack/eslint-patch": "^1.1.3",
+ "@typescript-eslint/parser": "^5.21.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^2.7.1",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.29.4",
+ "eslint-plugin-react-hooks": "^4.5.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.23.0 || ^8.0.0",
+ "typescript": ">=3.3.1"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "2.7.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "debug": "^4.3.4",
+ "glob": "^7.2.0",
+ "is-glob": "^4.0.3",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript/node_modules/glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.7.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.27.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.1",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
+ "has": "^1.0.3",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.7.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.7",
+ "aria-query": "^5.1.3",
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "ast-types-flow": "^0.0.7",
+ "axe-core": "^4.6.2",
+ "axobject-query": "^3.1.1",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^3.3.3",
+ "language-tags": "=1.0.5",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.32.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "array.prototype.tosorted": "^1.1.1",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "object.hasown": "^1.1.2",
+ "object.values": "^1.1.6",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.4",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "7.1.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.5.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.15.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "license": "MIT"
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.7",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.20.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/google-protobuf": {
+ "version": "3.21.2",
+ "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz",
+ "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA=="
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/grapheme-splitter": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "5.2.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "4.3.0",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "license": "MIT"
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.12.0",
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.5",
+ "object.assign": "^4.1.3"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "language-subtag-registry": "~0.3.2"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "5.2.1",
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/next": {
+ "version": "12.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "@next/env": "12.2.3",
+ "@swc/helpers": "0.4.3",
+ "caniuse-lite": "^1.0.30001332",
+ "postcss": "8.4.14",
+ "styled-jsx": "5.0.2",
+ "use-sync-external-store": "1.2.0"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-android-arm-eabi": "12.2.3",
+ "@next/swc-android-arm64": "12.2.3",
+ "@next/swc-darwin-arm64": "12.2.3",
+ "@next/swc-darwin-x64": "12.2.3",
+ "@next/swc-freebsd-x64": "12.2.3",
+ "@next/swc-linux-arm-gnueabihf": "12.2.3",
+ "@next/swc-linux-arm64-gnu": "12.2.3",
+ "@next/swc-linux-arm64-musl": "12.2.3",
+ "@next/swc-linux-x64-gnu": "12.2.3",
+ "@next/swc-linux-x64-musl": "12.2.3",
+ "@next/swc-win32-arm64-msvc": "12.2.3",
+ "@next/swc-win32-ia32-msvc": "12.2.3",
+ "@next/swc-win32-x64-msvc": "12.2.3"
+ },
+ "peerDependencies": {
+ "fibers": ">= 3.1.0",
+ "node-sass": "^6.0.0 || ^7.0.0",
+ "react": "^17.0.2 || ^18.0.0-0",
+ "react-dom": "^17.0.2 || ^18.0.0-0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "fibers": {
+ "optional": true
+ },
+ "node-sass": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-path": {
+ "version": "0.11.8",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pigweedjs": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/pigweedjs/-/pigweedjs-0.0.8.tgz",
+ "integrity": "sha512-WDESCwqS7BT25wQW56xh5EdJOblKbUyicb8cBoQwAGXYC38rtHv5Waj3LeWEgqce5+Ga1y5/MSPhyg1IgxlYnw==",
+ "dependencies": {
+ "@protobuf-ts/protoc": "^2.7.0",
+ "google-protobuf": "^3.17.3",
+ "long": "^5.2.1",
+ "object-path": "^0.11.8",
+ "ts-protoc-gen": "^0.15.0"
+ },
+ "bin": {
+ "pw_protobuf_compiler": "dist/bin/pw_protobuf_compiler.js"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.14",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.2.0",
+ "license": "MIT"
+ },
+ "node_modules/react-lifecycles-compat": {
+ "version": "3.0.4",
+ "license": "MIT"
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/react-virtualized": {
+ "version": "9.22.4",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.7.2",
+ "clsx": "^1.0.4",
+ "dom-helpers": "^5.1.3",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-lifecycles-compat": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0",
+ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/react-virtualized-auto-sizer": {
+ "version": "1.0.14",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc",
+ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc"
+ }
+ },
+ "node_modules/react-window": {
+ "version": "1.8.8",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.0.0",
+ "memoize-one": ">=3.1.1 <6"
+ },
+ "engines": {
+ "node": ">8.0.0"
+ },
+ "peerDependencies": {
+ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "functions-have-names": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.2",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sass": {
+ "version": "1.62.0",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.0",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.4.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "internal-slot": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.4.3",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-mod": {
+ "version": "4.0.2",
+ "license": "MIT"
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.0.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.1.3",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-protoc-gen": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/ts-protoc-gen/-/ts-protoc-gen-0.15.0.tgz",
+ "integrity": "sha512-TycnzEyrdVDlATJ3bWFTtra3SCiEP0W0vySXReAuEygXCUr1j2uaVyL0DhzjwuUdQoW5oXPwk6oZWeA0955V+g==",
+ "dependencies": {
+ "google-protobuf": "^3.15.5"
+ },
+ "bin": {
+ "protoc-gen-ts": "bin/protoc-gen-ts"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.14.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.5.0",
+ "license": "0BSD"
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.7.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/w3c-keyname": {
+ "version": "2.2.6",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-weakmap": "^2.0.1",
+ "is-weakset": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/xterm": {
+ "version": "4.19.0",
+ "license": "MIT"
+ },
+ "node_modules/xterm-addon-fit": {
+ "version": "0.5.0",
+ "license": "MIT",
+ "peerDependencies": {
+ "xterm": "^4.0.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.21.4",
+ "requires": {
+ "@babel/highlight": "^7.18.6"
+ }
+ },
+ "@babel/helper-module-imports": {
+ "version": "7.21.4",
+ "requires": {
+ "@babel/types": "^7.21.4"
+ }
+ },
+ "@babel/helper-string-parser": {
+ "version": "7.19.4"
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.19.1"
+ },
+ "@babel/highlight": {
+ "version": "7.18.6",
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3"
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5"
+ },
+ "has-flag": {
+ "version": "3.0.0"
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.21.0",
+ "requires": {
+ "regenerator-runtime": "^0.13.11"
+ }
+ },
+ "@babel/types": {
+ "version": "7.21.4",
+ "requires": {
+ "@babel/helper-string-parser": "^7.19.4",
+ "@babel/helper-validator-identifier": "^7.19.1",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "@codemirror/autocomplete": {
+ "version": "6.5.0",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.6.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "@codemirror/commands": {
+ "version": "6.2.2",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.2.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "@codemirror/lang-javascript": {
+ "version": "6.1.6",
+ "requires": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/language": "^6.6.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/javascript": "^1.0.0"
+ }
+ },
+ "@codemirror/language": {
+ "version": "6.6.0",
+ "requires": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "@codemirror/lint": {
+ "version": "6.2.1",
+ "requires": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "@codemirror/search": {
+ "version": "6.3.0",
+ "requires": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "@codemirror/state": {
+ "version": "6.2.0"
+ },
+ "@codemirror/theme-one-dark": {
+ "version": "6.1.1",
+ "requires": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/highlight": "^1.0.0"
+ }
+ },
+ "@codemirror/view": {
+ "version": "6.9.4",
+ "requires": {
+ "@codemirror/state": "^6.1.4",
+ "style-mod": "^4.0.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
+ "@emotion/babel-plugin": {
+ "version": "11.10.6",
+ "requires": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/serialize": "^1.1.1",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.1.3"
+ }
+ },
+ "@emotion/cache": {
+ "version": "11.10.7",
+ "requires": {
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/sheet": "^1.2.1",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "stylis": "4.1.3"
+ }
+ },
+ "@emotion/hash": {
+ "version": "0.9.0"
+ },
+ "@emotion/is-prop-valid": {
+ "version": "1.2.0",
+ "requires": {
+ "@emotion/memoize": "^0.8.0"
+ }
+ },
+ "@emotion/memoize": {
+ "version": "0.8.0"
+ },
+ "@emotion/react": {
+ "version": "11.10.6",
+ "requires": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/cache": "^11.10.5",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0",
+ "@emotion/weak-memoize": "^0.3.0",
+ "hoist-non-react-statics": "^3.3.1"
+ }
+ },
+ "@emotion/serialize": {
+ "version": "1.1.1",
+ "requires": {
+ "@emotion/hash": "^0.9.0",
+ "@emotion/memoize": "^0.8.0",
+ "@emotion/unitless": "^0.8.0",
+ "@emotion/utils": "^1.2.0",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@emotion/sheet": {
+ "version": "1.2.1"
+ },
+ "@emotion/styled": {
+ "version": "11.10.6",
+ "requires": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.10.6",
+ "@emotion/is-prop-valid": "^1.2.0",
+ "@emotion/serialize": "^1.1.1",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0",
+ "@emotion/utils": "^1.2.0"
+ }
+ },
+ "@emotion/unitless": {
+ "version": "0.8.0"
+ },
+ "@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.0",
+ "requires": {}
+ },
+ "@emotion/utils": {
+ "version": "1.2.0"
+ },
+ "@emotion/weak-memoize": {
+ "version": "0.3.0"
+ },
+ "@eslint/eslintrc": {
+ "version": "1.4.1",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.4.0",
+ "globals": "^13.19.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ }
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.10.7",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.1",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/gitignore-to-minimatch": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.1",
+ "dev": true
+ },
+ "@lezer/common": {
+ "version": "1.0.2"
+ },
+ "@lezer/highlight": {
+ "version": "1.1.4",
+ "requires": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "@lezer/javascript": {
+ "version": "1.4.2",
+ "requires": {
+ "@lezer/highlight": "^1.1.3",
+ "@lezer/lr": "^1.3.0"
+ }
+ },
+ "@lezer/lr": {
+ "version": "1.3.3",
+ "requires": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "@mui/base": {
+ "version": "5.0.0-alpha.125",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "@emotion/is-prop-valid": "^1.2.0",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.12.0",
+ "@popperjs/core": "^2.11.7",
+ "clsx": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ }
+ },
+ "@mui/core-downloads-tracker": {
+ "version": "5.12.0"
+ },
+ "@mui/material": {
+ "version": "5.12.0",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "@mui/base": "5.0.0-alpha.125",
+ "@mui/core-downloads-tracker": "^5.12.0",
+ "@mui/system": "^5.12.0",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.12.0",
+ "@types/react-transition-group": "^4.4.5",
+ "clsx": "^1.2.1",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0",
+ "react-transition-group": "^4.4.5"
+ }
+ },
+ "@mui/private-theming": {
+ "version": "5.12.0",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "@mui/utils": "^5.12.0",
+ "prop-types": "^15.8.1"
+ }
+ },
+ "@mui/styled-engine": {
+ "version": "5.12.0",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "@emotion/cache": "^11.10.7",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ }
+ },
+ "@mui/system": {
+ "version": "5.12.0",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "@mui/private-theming": "^5.12.0",
+ "@mui/styled-engine": "^5.12.0",
+ "@mui/types": "^7.2.4",
+ "@mui/utils": "^5.12.0",
+ "clsx": "^1.2.1",
+ "csstype": "^3.1.2",
+ "prop-types": "^15.8.1"
+ }
+ },
+ "@mui/types": {
+ "version": "7.2.4",
+ "requires": {}
+ },
+ "@mui/utils": {
+ "version": "5.12.0",
+ "requires": {
+ "@babel/runtime": "^7.21.0",
+ "@types/prop-types": "^15.7.5",
+ "@types/react-is": "^16.7.1 || ^17.0.0",
+ "prop-types": "^15.8.1",
+ "react-is": "^18.2.0"
+ }
+ },
+ "@next/env": {
+ "version": "12.2.3"
+ },
+ "@next/eslint-plugin-next": {
+ "version": "12.2.3",
+ "dev": true,
+ "requires": {
+ "glob": "7.1.7"
+ }
+ },
+ "@next/swc-darwin-x64": {
+ "version": "12.2.3",
+ "optional": true
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@popperjs/core": {
+ "version": "2.11.7"
+ },
+ "@protobuf-ts/protoc": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.9.0.tgz",
+ "integrity": "sha512-8mQmw+OVqrpaHgmErle8heE5Z1ktX2vmWJmvFORSTcHxMfqHLv8RKTRroC9pkBQRgUaSGSXmy7n4ItdZnz41dw=="
+ },
+ "@rushstack/eslint-patch": {
+ "version": "1.2.0",
+ "dev": true
+ },
+ "@swc/helpers": {
+ "version": "0.4.3",
+ "requires": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "@types/json5": {
+ "version": "0.0.29",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "18.6.3",
+ "dev": true
+ },
+ "@types/parse-json": {
+ "version": "4.0.0"
+ },
+ "@types/prop-types": {
+ "version": "15.7.5"
+ },
+ "@types/react": {
+ "version": "18.0.15",
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-dom": {
+ "version": "18.0.6",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-is": {
+ "version": "17.0.3",
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-transition-group": {
+ "version": "4.4.5",
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-virtualized": {
+ "version": "9.21.21",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/react": "^17"
+ },
+ "dependencies": {
+ "@types/react": {
+ "version": "17.0.58",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ }
+ }
+ },
+ "@types/react-window": {
+ "version": "1.8.5",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.3"
+ },
+ "@typescript-eslint/parser": {
+ "version": "5.58.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "5.58.0",
+ "@typescript-eslint/types": "5.58.0",
+ "@typescript-eslint/typescript-estree": "5.58.0",
+ "debug": "^4.3.4"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "5.58.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.58.0",
+ "@typescript-eslint/visitor-keys": "5.58.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "5.58.0",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "5.58.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.58.0",
+ "@typescript-eslint/visitor-keys": "5.58.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "5.58.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "5.58.0",
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "acorn": {
+ "version": "8.8.2",
+ "dev": true
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "requires": {}
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.3",
+ "devOptional": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "aria-query": {
+ "version": "5.1.3",
+ "dev": true,
+ "requires": {
+ "deep-equal": "^2.0.5"
+ }
+ },
+ "array-buffer-byte-length": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "is-array-buffer": "^3.0.1"
+ }
+ },
+ "array-includes": {
+ "version": "3.1.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "is-string": "^1.0.7"
+ }
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "array.prototype.flat": {
+ "version": "1.3.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "array.prototype.flatmap": {
+ "version": "1.3.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "array.prototype.tosorted": {
+ "version": "1.1.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "es-shim-unscopables": "^1.0.0",
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "ast-types-flow": {
+ "version": "0.0.7",
+ "dev": true
+ },
+ "available-typed-arrays": {
+ "version": "1.0.5",
+ "dev": true
+ },
+ "axe-core": {
+ "version": "4.6.3",
+ "dev": true
+ },
+ "axobject-query": {
+ "version": "3.1.1",
+ "dev": true,
+ "requires": {
+ "deep-equal": "^2.0.5"
+ }
+ },
+ "babel-plugin-macros": {
+ "version": "3.1.0",
+ "requires": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "devOptional": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "devOptional": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0"
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001478"
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "chokidar": {
+ "version": "3.5.3",
+ "devOptional": true,
+ "requires": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "5.1.2",
+ "devOptional": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "clsx": {
+ "version": "1.2.1"
+ },
+ "codemirror": {
+ "version": "6.0.1",
+ "requires": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "dev": true
+ },
+ "convert-source-map": {
+ "version": "1.9.0"
+ },
+ "cosmiconfig": {
+ "version": "7.1.0",
+ "requires": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ }
+ },
+ "crelt": {
+ "version": "1.0.5"
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "csstype": {
+ "version": "3.1.2"
+ },
+ "damerau-levenshtein": {
+ "version": "1.0.8",
+ "dev": true
+ },
+ "debug": {
+ "version": "4.3.4",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "deep-equal": {
+ "version": "2.2.0",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-get-iterator": "^1.1.2",
+ "get-intrinsic": "^1.1.3",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.1",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ }
+ },
+ "deep-is": {
+ "version": "0.1.4",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "dom-helpers": {
+ "version": "5.2.1",
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "emoji-regex": {
+ "version": "9.2.2",
+ "dev": true
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es-abstract": {
+ "version": "1.21.2",
+ "dev": true,
+ "requires": {
+ "array-buffer-byte-length": "^1.0.0",
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "es-set-tostringtag": "^2.0.1",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.5",
+ "get-intrinsic": "^1.2.0",
+ "get-symbol-description": "^1.0.0",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has": "^1.0.3",
+ "has-property-descriptors": "^1.0.0",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "is-array-buffer": "^3.0.2",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.2",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.10",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.12.3",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.4.3",
+ "safe-regex-test": "^1.0.0",
+ "string.prototype.trim": "^1.2.7",
+ "string.prototype.trimend": "^1.0.6",
+ "string.prototype.trimstart": "^1.0.6",
+ "typed-array-length": "^1.0.4",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.9"
+ }
+ },
+ "es-get-iterator": {
+ "version": "1.1.3",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ }
+ },
+ "es-set-tostringtag": {
+ "version": "2.0.1",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.3",
+ "has": "^1.0.3",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "es-shim-unscopables": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0"
+ },
+ "eslint": {
+ "version": "8.21.0",
+ "dev": true,
+ "requires": {
+ "@eslint/eslintrc": "^1.3.0",
+ "@humanwhocodes/config-array": "^0.10.4",
+ "@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.1.1",
+ "eslint-utils": "^3.0.0",
+ "eslint-visitor-keys": "^3.3.0",
+ "espree": "^9.3.3",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^6.0.1",
+ "globals": "^13.15.0",
+ "globby": "^11.1.0",
+ "grapheme-splitter": "^1.0.4",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "regexpp": "^3.2.0",
+ "strip-ansi": "^6.0.1",
+ "strip-json-comments": "^3.1.0",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ }
+ },
+ "eslint-config-next": {
+ "version": "12.2.3",
+ "dev": true,
+ "requires": {
+ "@next/eslint-plugin-next": "12.2.3",
+ "@rushstack/eslint-patch": "^1.1.3",
+ "@typescript-eslint/parser": "^5.21.0",
+ "eslint-import-resolver-node": "^0.3.6",
+ "eslint-import-resolver-typescript": "^2.7.1",
+ "eslint-plugin-import": "^2.26.0",
+ "eslint-plugin-jsx-a11y": "^6.5.1",
+ "eslint-plugin-react": "^7.29.4",
+ "eslint-plugin-react-hooks": "^4.5.0"
+ }
+ },
+ "eslint-import-resolver-node": {
+ "version": "0.3.7",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.11.0",
+ "resolve": "^1.22.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-import-resolver-typescript": {
+ "version": "2.7.1",
+ "dev": true,
+ "requires": {
+ "debug": "^4.3.4",
+ "glob": "^7.2.0",
+ "is-glob": "^4.0.3",
+ "resolve": "^1.22.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.2.3",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "eslint-module-utils": {
+ "version": "2.7.4",
+ "dev": true,
+ "requires": {
+ "debug": "^3.2.7"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ }
+ }
+ },
+ "eslint-plugin-import": {
+ "version": "2.27.5",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "array.prototype.flatmap": "^1.3.1",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.7",
+ "eslint-module-utils": "^2.7.4",
+ "has": "^1.0.3",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.values": "^1.1.6",
+ "resolve": "^1.22.1",
+ "semver": "^6.3.0",
+ "tsconfig-paths": "^3.14.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-jsx-a11y": {
+ "version": "6.7.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.20.7",
+ "aria-query": "^5.1.3",
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "ast-types-flow": "^0.0.7",
+ "axe-core": "^4.6.2",
+ "axobject-query": "^3.1.1",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "has": "^1.0.3",
+ "jsx-ast-utils": "^3.3.3",
+ "language-tags": "=1.0.5",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-react": {
+ "version": "7.32.2",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flatmap": "^1.3.1",
+ "array.prototype.tosorted": "^1.1.1",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.6",
+ "object.fromentries": "^2.0.6",
+ "object.hasown": "^1.1.2",
+ "object.values": "^1.1.6",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.4",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.8"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "resolve": {
+ "version": "2.0.0-next.4",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.9.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-react-hooks": {
+ "version": "4.6.0",
+ "dev": true,
+ "requires": {}
+ },
+ "eslint-scope": {
+ "version": "7.1.1",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-utils": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "2.1.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.0",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.5.1",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.8.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.0"
+ }
+ },
+ "esquery": {
+ "version": "1.5.0",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ }
+ },
+ "estraverse": {
+ "version": "5.3.0",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "dev": true
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true
+ },
+ "fast-glob": {
+ "version": "3.2.12",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "dependencies": {
+ "glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ }
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true
+ },
+ "fastq": {
+ "version": "1.15.0",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "devOptional": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-root": {
+ "version": "1.1.0"
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "dev": true,
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.7",
+ "dev": true
+ },
+ "for-each": {
+ "version": "0.3.3",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "dev": true,
+ "optional": true
+ },
+ "function-bind": {
+ "version": "1.1.1"
+ },
+ "function.prototype.name": {
+ "version": "1.1.5",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0",
+ "functions-have-names": "^1.2.2"
+ }
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "functions-have-names": {
+ "version": "1.2.3",
+ "dev": true
+ },
+ "get-intrinsic": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.3"
+ }
+ },
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "glob": {
+ "version": "7.1.7",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.3"
+ }
+ },
+ "globals": {
+ "version": "13.20.0",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "globalthis": {
+ "version": "1.0.3",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3"
+ }
+ },
+ "globby": {
+ "version": "11.1.0",
+ "dev": true,
+ "requires": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ }
+ },
+ "google-protobuf": {
+ "version": "3.21.2",
+ "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz",
+ "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA=="
+ },
+ "gopd": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.3"
+ }
+ },
+ "grapheme-splitter": {
+ "version": "1.0.4",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-bigints": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "has-property-descriptors": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "has-proto": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "hoist-non-react-statics": {
+ "version": "3.3.2",
+ "requires": {
+ "react-is": "^16.7.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "16.13.1"
+ }
+ }
+ },
+ "ignore": {
+ "version": "5.2.4",
+ "dev": true
+ },
+ "immutable": {
+ "version": "4.3.0",
+ "devOptional": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "dev": true
+ },
+ "internal-slot": {
+ "version": "1.0.5",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.2.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "is-arguments": {
+ "version": "1.1.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-array-buffer": {
+ "version": "3.0.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.0",
+ "is-typed-array": "^1.1.10"
+ }
+ },
+ "is-arrayish": {
+ "version": "0.2.1"
+ },
+ "is-bigint": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "devOptional": true,
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.7",
+ "dev": true
+ },
+ "is-core-module": {
+ "version": "2.12.0",
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.5",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "devOptional": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "devOptional": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-map": {
+ "version": "2.0.2",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.2",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "devOptional": true
+ },
+ "is-number-object": {
+ "version": "1.0.7",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-regex": {
+ "version": "1.1.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-set": {
+ "version": "2.0.2",
+ "dev": true
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-typed-array": {
+ "version": "1.1.10",
+ "dev": true,
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-weakmap": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "is-weakref": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2"
+ }
+ },
+ "is-weakset": {
+ "version": "2.0.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "isarray": {
+ "version": "2.0.5",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "js-tokens": {
+ "version": "4.0.0"
+ },
+ "js-yaml": {
+ "version": "4.1.0",
+ "dev": true,
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "json-parse-even-better-errors": {
+ "version": "2.3.1"
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "json5": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.0"
+ }
+ },
+ "jsx-ast-utils": {
+ "version": "3.3.3",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.5",
+ "object.assign": "^4.1.3"
+ }
+ },
+ "language-subtag-registry": {
+ "version": "0.3.22",
+ "dev": true
+ },
+ "language-tags": {
+ "version": "1.0.5",
+ "dev": true,
+ "requires": {
+ "language-subtag-registry": "~0.3.2"
+ }
+ },
+ "levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "lines-and-columns": {
+ "version": "1.2.4"
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "dev": true,
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "dev": true
+ },
+ "long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "memoize-one": {
+ "version": "5.2.1"
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.5",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.8",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.2",
+ "dev": true
+ },
+ "nanoid": {
+ "version": "3.3.6"
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "dev": true
+ },
+ "next": {
+ "version": "12.2.3",
+ "requires": {
+ "@next/env": "12.2.3",
+ "@next/swc-android-arm-eabi": "12.2.3",
+ "@next/swc-android-arm64": "12.2.3",
+ "@next/swc-darwin-arm64": "12.2.3",
+ "@next/swc-darwin-x64": "12.2.3",
+ "@next/swc-freebsd-x64": "12.2.3",
+ "@next/swc-linux-arm-gnueabihf": "12.2.3",
+ "@next/swc-linux-arm64-gnu": "12.2.3",
+ "@next/swc-linux-arm64-musl": "12.2.3",
+ "@next/swc-linux-x64-gnu": "12.2.3",
+ "@next/swc-linux-x64-musl": "12.2.3",
+ "@next/swc-win32-arm64-msvc": "12.2.3",
+ "@next/swc-win32-ia32-msvc": "12.2.3",
+ "@next/swc-win32-x64-msvc": "12.2.3",
+ "@swc/helpers": "0.4.3",
+ "caniuse-lite": "^1.0.30001332",
+ "postcss": "8.4.14",
+ "styled-jsx": "5.0.2",
+ "use-sync-external-store": "1.2.0"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "devOptional": true
+ },
+ "object-assign": {
+ "version": "4.1.1"
+ },
+ "object-inspect": {
+ "version": "1.12.3",
+ "dev": true
+ },
+ "object-is": {
+ "version": "1.1.5",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "object-path": {
+ "version": "0.11.8"
+ },
+ "object.assign": {
+ "version": "4.1.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "object.entries": {
+ "version": "1.1.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "object.fromentries": {
+ "version": "2.0.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "object.hasown": {
+ "version": "1.1.2",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "object.values": {
+ "version": "1.1.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "dev": true,
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-json": {
+ "version": "5.2.0",
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7"
+ },
+ "path-type": {
+ "version": "4.0.0"
+ },
+ "picocolors": {
+ "version": "1.0.0"
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "devOptional": true
+ },
+ "pigweedjs": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/pigweedjs/-/pigweedjs-0.0.8.tgz",
+ "integrity": "sha512-WDESCwqS7BT25wQW56xh5EdJOblKbUyicb8cBoQwAGXYC38rtHv5Waj3LeWEgqce5+Ga1y5/MSPhyg1IgxlYnw==",
+ "requires": {
+ "@protobuf-ts/protoc": "^2.7.0",
+ "google-protobuf": "^3.17.3",
+ "long": "^5.2.1",
+ "object-path": "^0.11.8",
+ "ts-protoc-gen": "^0.15.0"
+ }
+ },
+ "postcss": {
+ "version": "8.4.14",
+ "requires": {
+ "nanoid": "^3.3.4",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "dev": true
+ },
+ "prop-types": {
+ "version": "15.8.1",
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "16.13.1"
+ }
+ }
+ },
+ "punycode": {
+ "version": "2.3.0",
+ "dev": true
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "dev": true
+ },
+ "react": {
+ "version": "18.2.0",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "react-dom": {
+ "version": "18.2.0",
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ }
+ },
+ "react-is": {
+ "version": "18.2.0"
+ },
+ "react-lifecycles-compat": {
+ "version": "3.0.4"
+ },
+ "react-transition-group": {
+ "version": "4.4.5",
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "react-virtualized": {
+ "version": "9.22.4",
+ "requires": {
+ "@babel/runtime": "^7.7.2",
+ "clsx": "^1.0.4",
+ "dom-helpers": "^5.1.3",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-lifecycles-compat": "^3.0.4"
+ }
+ },
+ "react-virtualized-auto-sizer": {
+ "version": "1.0.14",
+ "requires": {}
+ },
+ "react-window": {
+ "version": "1.8.8",
+ "requires": {
+ "@babel/runtime": "^7.0.0",
+ "memoize-one": ">=3.1.1 <6"
+ }
+ },
+ "readdirp": {
+ "version": "3.6.0",
+ "devOptional": true,
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.11"
+ },
+ "regexp.prototype.flags": {
+ "version": "1.4.3",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "functions-have-names": "^1.2.2"
+ }
+ },
+ "regexpp": {
+ "version": "3.2.0",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.22.2",
+ "requires": {
+ "is-core-module": "^2.11.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0"
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "safe-regex-test": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "is-regex": "^1.1.4"
+ }
+ },
+ "sass": {
+ "version": "1.62.0",
+ "devOptional": true,
+ "requires": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ }
+ },
+ "scheduler": {
+ "version": "0.23.0",
+ "requires": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "semver": {
+ "version": "7.4.0",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "slash": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "source-map": {
+ "version": "0.5.7"
+ },
+ "source-map-js": {
+ "version": "1.0.2"
+ },
+ "stop-iteration-iterator": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "internal-slot": "^1.0.4"
+ }
+ },
+ "string.prototype.matchall": {
+ "version": "4.0.8",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.4.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "string.prototype.trim": {
+ "version": "1.2.7",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.4",
+ "es-abstract": "^1.20.4"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true
+ },
+ "style-mod": {
+ "version": "4.0.2"
+ },
+ "styled-jsx": {
+ "version": "5.0.2",
+ "requires": {}
+ },
+ "stylis": {
+ "version": "4.1.3"
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "supports-preserve-symlinks-flag": {
+ "version": "1.0.0"
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "dev": true
+ },
+ "to-fast-properties": {
+ "version": "2.0.0"
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "devOptional": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "ts-protoc-gen": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/ts-protoc-gen/-/ts-protoc-gen-0.15.0.tgz",
+ "integrity": "sha512-TycnzEyrdVDlATJ3bWFTtra3SCiEP0W0vySXReAuEygXCUr1j2uaVyL0DhzjwuUdQoW5oXPwk6oZWeA0955V+g==",
+ "requires": {
+ "google-protobuf": "^3.15.5"
+ }
+ },
+ "tsconfig-paths": {
+ "version": "3.14.2",
+ "dev": true,
+ "requires": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "tslib": {
+ "version": "2.5.0"
+ },
+ "tsutils": {
+ "version": "3.21.0",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "dev": true
+ }
+ }
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "dev": true
+ },
+ "typed-array-length": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "is-typed-array": "^1.1.9"
+ }
+ },
+ "typescript": {
+ "version": "4.7.4",
+ "dev": true
+ },
+ "unbox-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.0.3",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "use-sync-external-store": {
+ "version": "1.2.0",
+ "requires": {}
+ },
+ "v8-compile-cache": {
+ "version": "2.3.0",
+ "dev": true
+ },
+ "w3c-keyname": {
+ "version": "2.2.6"
+ },
+ "which": {
+ "version": "2.0.2",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "which-collection": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "is-map": "^2.0.1",
+ "is-set": "^2.0.1",
+ "is-weakmap": "^2.0.1",
+ "is-weakset": "^2.0.1"
+ }
+ },
+ "which-typed-array": {
+ "version": "1.1.9",
+ "dev": true,
+ "requires": {
+ "available-typed-arrays": "^1.0.5",
+ "call-bind": "^1.0.2",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.0",
+ "is-typed-array": "^1.1.10"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "dev": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "xterm": {
+ "version": "4.19.0"
+ },
+ "xterm-addon-fit": {
+ "version": "0.5.0",
+ "requires": {}
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "yaml": {
+ "version": "1.10.2"
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "dev": true
+ }
+ }
+}
diff --git a/pw_web/webconsole/package.json b/pw_web/webconsole/package.json
index 91b7e1f2a..eb5da8f21 100644
--- a/pw_web/webconsole/package.json
+++ b/pw_web/webconsole/package.json
@@ -19,7 +19,7 @@
"codemirror": "^6.0.1",
"next": "12.2.3",
"object-path": "^0.11.8",
- "pigweedjs": "latest",
+ "pigweedjs": "file:../../",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-virtualized": "^9.22.3",
diff --git a/pw_web/webconsole/pages/index.tsx b/pw_web/webconsole/pages/index.tsx
index 20866870b..1a020a424 100644
--- a/pw_web/webconsole/pages/index.tsx
+++ b/pw_web/webconsole/pages/index.tsx
@@ -19,13 +19,22 @@ import Log from "../components/log";
import Repl from "../components/repl";
import Connect from "../components/connect";
import BtnUploadDB from '../components/uploadDb';
+
import {WebSerial, Device} from "pigweedjs";
-import {useState} from 'react';
-type WebSerialTransport = WebSerial.WebSerialTransport
+import {useState, useEffect} from 'react';
const Home: NextPage = () => {
const [device, setDevice] = useState<Device | undefined>(undefined);
const [tokenDB, setTokenDB] = useState("");
+ useEffect(() => {
+ if (!device || !tokenDB) return;
+ // Dynamically load logging component
+ import('pigweedjs/logging')
+ .then(({createLogViewer, PigweedRPCLogSource}) => {
+ const source = new PigweedRPCLogSource(device, tokenDB);
+ createLogViewer(source, document.querySelector('.log-container')!);
+ });
+ }, [device, tokenDB]);
return (
<div className={styles.container}>
<Head>
@@ -46,8 +55,7 @@ const Home: NextPage = () => {
</div>
<div className={styles.grid}>
- <div>
- <Log device={device} tokenDB={tokenDB}></Log>
+ <div className={'log-container ' + styles.logsContainer}>
</div>
<div>
<Repl device={device}></Repl>
diff --git a/pw_web/webconsole/styles/Home.module.scss b/pw_web/webconsole/styles/Home.module.scss
index 5f671c86a..cd63cee5f 100644
--- a/pw_web/webconsole/styles/Home.module.scss
+++ b/pw_web/webconsole/styles/Home.module.scss
@@ -67,6 +67,10 @@
}
}
+.logsContainer{
+ height: calc(100vh - 78px);
+}
+
@media (max-width: 600px) {
.grid {
width: 100%;
diff --git a/pw_web/webconsole/styles/globals.css b/pw_web/webconsole/styles/globals.css
index ac741785b..bc677fb2d 100644
--- a/pw_web/webconsole/styles/globals.css
+++ b/pw_web/webconsole/styles/globals.css
@@ -13,6 +13,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
+@import url('https://fonts.googleapis.com/css2?family=Google+Sans&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined&display=swap');
html,
body {
padding: 0;
diff --git a/pw_web/webconsole/tsconfig.json b/pw_web/webconsole/tsconfig.json
index 99710e857..e6bb8eb43 100644
--- a/pw_web/webconsole/tsconfig.json
+++ b/pw_web/webconsole/tsconfig.json
@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "es5",
- "lib": ["dom", "dom.iterable", "esnext"],
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -15,6 +19,12 @@
"jsx": "preserve",
"incremental": true
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}
diff --git a/pw_work_queue/BUILD.bazel b/pw_work_queue/BUILD.bazel
index ff8855c1d..ec5813b39 100644
--- a/pw_work_queue/BUILD.bazel
+++ b/pw_work_queue/BUILD.bazel
@@ -30,11 +30,11 @@ pw_cc_library(
name = "pw_work_queue",
srcs = ["work_queue.cc"],
hdrs = [
- "public/pw_work_queue/internal/circular_buffer.h",
"public/pw_work_queue/work_queue.h",
],
includes = ["public"],
deps = [
+ "//pw_containers:inline_queue",
"//pw_function",
"//pw_metric:metric",
"//pw_status",
diff --git a/pw_work_queue/BUILD.gn b/pw_work_queue/BUILD.gn
index a84e30a08..544247520 100644
--- a/pw_work_queue/BUILD.gn
+++ b/pw_work_queue/BUILD.gn
@@ -27,11 +27,9 @@ config("public_include_path") {
pw_source_set("pw_work_queue") {
public_configs = [ ":public_include_path" ]
- public = [
- "public/pw_work_queue/internal/circular_buffer.h",
- "public/pw_work_queue/work_queue.h",
- ]
+ public = [ "public/pw_work_queue/work_queue.h" ]
public_deps = [
+ "$dir_pw_containers:inline_queue",
"$dir_pw_sync:interrupt_spin_lock",
"$dir_pw_sync:lock_annotations",
"$dir_pw_sync:thread_notification",
@@ -55,6 +53,7 @@ pw_source_set("test_thread") {
# pw_source_set and a pw_source_set which provides the implementation of
# test_thread. See ":stl_work_queue_test" as an example.
pw_source_set("work_queue_test") {
+ testonly = pw_unit_test_TESTONLY
sources = [ "work_queue_test.cc" ]
deps = [
":pw_work_queue",
diff --git a/pw_work_queue/CMakeLists.txt b/pw_work_queue/CMakeLists.txt
index 32100a474..44ba6d359 100644
--- a/pw_work_queue/CMakeLists.txt
+++ b/pw_work_queue/CMakeLists.txt
@@ -16,11 +16,11 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_library(pw_work_queue STATIC
HEADERS
- public/pw_work_queue/internal/circular_buffer.h
public/pw_work_queue/work_queue.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
+ pw_containers.inline_queue
pw_sync.interrupt_spin_lock
pw_sync.lock_annotations
pw_sync.thread_notification
diff --git a/pw_work_queue/docs.rst b/pw_work_queue/docs.rst
index 7ae6efa5f..5cb02f685 100644
--- a/pw_work_queue/docs.rst
+++ b/pw_work_queue/docs.rst
@@ -6,99 +6,37 @@ pw_work_queue
The ``pw_work_queue`` module contains utilities for deferring work to be
executed by another thread.
-.. Warning::
- This module is still under construction, the API is not yet stable.
+.. warning::
----------
-WorkQueue
----------
-The ``pw::work_queue::WorkQueue`` class enables threads and interrupts to
-enqueue work as a ``pw::work_queue::WorkItem`` for execution by the work queue.
-
-The entire API is thread and interrupt safe.
-
-Queue Sizing
-============
-The number of outstanding work requests is limited based on the
-``pw::work_queue::WorkQueue``'s internal queue size. This must be set
-appropriately for the application by the user.
-
-The queue size is set trough either through the size of the ``queue_storage``
-buffer passed into the constructor or by using the templated
-``pw::work_queue::WorkQueueWithBuffer`` helper.
-
-.. Note:: While the queue is full, the queue will not accept further work.
-
-Cooperative Thread Cancellation
-===============================
-The class is a ``pw::thread::ThreadCore``, meaning it should be executed as a
-single thread. In order to facilitate clean shutdown, it provides a
-``RequestStop()`` API for cooperative cancellation which should be invoked
-before joining the thread.
-
-.. Note:: Once stop has been requested the queue will no longer accept further
- work.
-
-C++
-===
-.. cpp:class:: pw::work_queue::WorkQueue
-
- .. cpp:function:: Status PushWork(WorkItem work_item)
-
- Enqueues a work_item for execution by the work queue thread.
-
- Returns:
-
- * **Ok** - Success, entry was enqueued for execution.
- * **FailedPrecondition** - the work queue is shutting down, entries are no
- longer permitted.
- * **ResourceExhausted** - internal work queue is full, entry was not
- enqueued.
-
- .. cpp:function:: void CheckPushWork(WorkItem work_item)
-
- Queue work for execution. Crash if the work cannot be queued due to a
- full queue or a stopped worker thread.
-
- This call is recommended where possible since it saves error handling code
- at the callsite; and in many practical cases, it is a bug if the work
- queue is full (and so a crash is useful to detect the problem).
-
- **Precondition:** The queue must not overflow, i.e. be full.
-
- **Precondition:** The queue must not have been requested to stop, i.e. it
- must not be in the process of shutting down.
-
- .. cpp:function:: void RequestStop()
-
- Locks the queue to prevent further work enqueing, finishes outstanding
- work, then shuts down the worker thread.
-
- The WorkQueue cannot be resumed after stopping as the ThreadCore thread
- returns and may be joined. It must be reconstructed for re-use after
- the thread has been joined.
+ This module is still under construction; the API is not yet stable.
+-------
Example
-------
.. code-block:: cpp
- #include "pw_thread/detached_thread.h"
- #include "pw_work_queue/work_queue.h"
+ #include "pw_thread/detached_thread.h"
+ #include "pw_work_queue/work_queue.h"
- pw::work_queue::WorkQueueWithBuffer<10> work_queue;
+ pw::work_queue::WorkQueueWithBuffer<10> work_queue;
- pw::thread::Options& WorkQueueThreadOptions();
- void SomeLongRunningProcessing();
+ pw::thread::Options& WorkQueueThreadOptions();
+ void SomeLongRunningProcessing();
- void SomeInterruptHandler() {
- // Instead of executing the long running processing task in the interrupt,
- // the work_queue executes it on the interrupt's behalf.
- work_queue.CheckPushWork(SomeLongRunningProcessing);
- }
+ void SomeInterruptHandler() {
+ // Instead of executing the long running processing task in the interrupt,
+ // the work_queue executes it on the interrupt's behalf.
+ work_queue.CheckPushWork(SomeLongRunningProcessing);
+ }
- int main() {
- // Start up the work_queue as a detached thread which runs forever.
- pw::thread::DetachedThread(WorkQueueThreadOptions(), work_queue);
- }
+ int main() {
+ // Start up the work_queue as a detached thread which runs forever.
+ pw::thread::DetachedThread(WorkQueueThreadOptions(), work_queue);
+ }
+-------------
+API reference
+-------------
+.. doxygennamespace:: pw::work_queue
+ :members:
diff --git a/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h b/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h
deleted file mode 100644
index c2f4dc5dd..000000000
--- a/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-#pragma once
-
-#include <cstdint>
-#include <optional>
-
-#include "pw_assert/assert.h"
-#include "pw_span/span.h"
-
-namespace pw::work_queue::internal {
-
-// TODO(hepler): Replace this with a std::deque like container.
-template <typename T>
-class CircularBuffer {
- public:
- explicit constexpr CircularBuffer(span<T> buffer)
- : buffer_(buffer), head_(0), tail_(0), count_(0) {}
-
- bool empty() const { return count_ == 0; }
- bool full() const { return count_ == buffer_.size(); }
- size_t size() const { return count_; }
- size_t capacity() const { return buffer_.size(); }
-
- template <typename Ty>
- bool Push(Ty&& value) {
- PW_DASSERT(tail_ < buffer_.size());
-
- if (full()) {
- return false;
- }
-
- buffer_[tail_] = std::forward<Ty>(value);
- IncrementWithWrap(tail_);
- ++count_;
- return true;
- }
-
- std::optional<T> Pop() {
- PW_DASSERT(head_ < buffer_.size());
-
- if (empty()) {
- return std::nullopt;
- }
-
- T entry = std::move(buffer_[head_]);
- IncrementWithWrap(head_);
- --count_;
- return entry;
- }
-
- private:
- void IncrementWithWrap(size_t& index) const {
- index++;
- // Note: branch is faster than mod (%) on common embedded architectures.
- if (index == buffer_.size()) {
- index = 0;
- }
- }
-
- span<T> buffer_;
-
- size_t head_;
- size_t tail_;
- size_t count_;
-};
-
-} // namespace pw::work_queue::internal
diff --git a/pw_work_queue/public/pw_work_queue/work_queue.h b/pw_work_queue/public/pw_work_queue/work_queue.h
index 7afb74e02..de1fe25d9 100644
--- a/pw_work_queue/public/pw_work_queue/work_queue.h
+++ b/pw_work_queue/public/pw_work_queue/work_queue.h
@@ -17,6 +17,7 @@
#include <array>
#include <cstdint>
+#include "pw_containers/inline_queue.h"
#include "pw_function/function.h"
#include "pw_metric/metric.h"
#include "pw_span/span.h"
@@ -25,51 +26,75 @@
#include "pw_sync/lock_annotations.h"
#include "pw_sync/thread_notification.h"
#include "pw_thread/thread_core.h"
-#include "pw_work_queue/internal/circular_buffer.h"
namespace pw::work_queue {
using WorkItem = Function<void()>;
-// The WorkQueue class enables threads and interrupts to enqueue work as a
-// pw::work_queue::WorkItem for execution by the work queue.
-//
-// The entire API is thread and interrupt safe.
+/// Enables threads and interrupts to enqueue work as a
+/// `pw::work_queue::WorkItem` for execution by the work queue.
+///
+/// **Queue sizing**: The number of outstanding work requests is limited
+/// based on the internal queue size. The queue size is set through either
+/// the size of the `queue_storage` buffer passed into the constructor or by
+/// using the templated `pw::work_queue::WorkQueueWithBuffer` helper. When the
+/// queue is full, the queue will not accept further work.
+///
+/// **Cooperative thread cancellation**: The class is a
+/// `pw::thread::ThreadCore`, meaning it should be executed as a single thread.
+/// To facilitate clean shutdown, it provides a `RequestStop()` method for
+/// cooperative cancellation which should be invoked before joining the thread.
+/// Once a stop has been requested the queue will no longer accept further work.
+///
+/// The entire API is thread-safe and interrupt-safe.
class WorkQueue : public thread::ThreadCore {
public:
- // Note: the ThreadNotification prevents this from being constexpr.
- explicit WorkQueue(span<WorkItem> queue_storage)
- : stop_requested_(false), circular_buffer_(queue_storage) {}
+ /// @param[in] queue The work entries to enqueue.
+ ///
+ /// @param[in] queue_capacity The internal queue size which limits the number
+ /// of outstanding work requests.
+ ///
+ /// @note The `ThreadNotification` prevents this from being `constexpr`.
+ WorkQueue(InlineQueue<WorkItem>& queue, size_t queue_capacity)
+ : stop_requested_(false), queue_(queue) {
+ min_queue_remaining_.Set(static_cast<uint32_t>(queue_capacity));
+ }
- // Enqueues a work_item for execution by the work queue thread.
- //
- // Returns:
- // Ok - Success, entry was enqueued for execution.
- // FailedPrecondition - the work queue is shutting down, entries are no
- // longer permitted.
- // ResourceExhausted - internal work queue is full, entry was not enqueued.
+ /// Enqueues a `work_item` for execution by the work queue thread.
+ ///
+ /// @param[in] work_item The entry to enqueue.
+ ///
+ /// @returns
+ /// * @pw_status{OK} - Success. Entry was enqueued for execution.
+ /// * @pw_status{FAILED_PRECONDITION} - The work queue is shutting down.
+ /// Entries are no longer permitted.
+ /// * @pw_status{RESOURCE_EXHAUSTED} - Internal work queue is full.
+ /// Entry was not enqueued.
Status PushWork(WorkItem&& work_item) PW_LOCKS_EXCLUDED(lock_) {
return InternalPushWork(std::move(work_item));
}
- // Queue work for execution. Crash if the work cannot be queued due to a
- // full queue or a stopped worker thread.
- //
- // This call is recommended where possible since it saves error handling code
- // at the callsite; and in many practical cases, it is a bug if the work
- // queue is full (and so a crash is useful to detect the problem).
- //
- // Precondition: The queue must not overflow, i.e. be full.
- // Precondition: The queue must not have been requested to stop, i.e. it must
- // not be in the process of shutting down.
+ /// Queues work for execution. Crashes if the work cannot be queued due to a
+ /// full queue or a stopped worker thread.
+ ///
+ /// This call is recommended where possible since it saves error handling code
+ /// at the callsite; and in many practical cases, it is a bug if the work
+ /// queue is full (and so a crash is useful to detect the problem).
+ ///
+ /// @param[in] work_item The entry to enqueue.
+ ///
+ /// @pre
+ /// * The queue must not overflow, i.e. be full.
+ /// * The queue must not have been requested to stop, i.e. it must
+ /// not be in the process of shutting down.
void CheckPushWork(WorkItem&& work_item) PW_LOCKS_EXCLUDED(lock_);
- // Locks the queue to prevent further work enqueing, finishes outstanding
- // work, then shuts down the worker thread.
- //
- // The WorkQueue cannot be resumed after stopping as the ThreadCore thread
- // returns and may be joined. It must be reconstructed for re-use after
- // the thread has been joined.
+ /// Locks the queue to prevent further work enqueing, finishes outstanding
+ /// work, then shuts down the worker thread.
+ ///
+ /// The `WorkQueue` cannot be resumed after stopping because the `ThreadCore`
+ /// thread returns and may be joined. The `WorkQueue` must be reconstructed
+ /// for re-use after the thread has been joined.
void RequestStop() PW_LOCKS_EXCLUDED(lock_);
private:
@@ -78,7 +103,7 @@ class WorkQueue : public thread::ThreadCore {
sync::InterruptSpinLock lock_;
bool stop_requested_ PW_GUARDED_BY(lock_);
- internal::CircularBuffer<WorkItem> circular_buffer_ PW_GUARDED_BY(lock_);
+ InlineQueue<WorkItem>& queue_ PW_GUARDED_BY(lock_);
sync::ThreadNotification work_notification_;
// TODO(ewout): The group and/or its name token should be passed as a ctor
@@ -90,19 +115,16 @@ class WorkQueue : public thread::ThreadCore {
// metrics work as intended.
PW_METRIC_GROUP(metrics_, "pw::work_queue::WorkQueue");
PW_METRIC(metrics_, max_queue_used_, "max_queue_used", 0u);
- PW_METRIC(metrics_,
- min_queue_remaining_,
- "min_queue_remaining",
- static_cast<uint32_t>(circular_buffer_.capacity()));
+ PW_METRIC(metrics_, min_queue_remaining_, "min_queue_remaining", 0u);
};
template <size_t kWorkQueueEntries>
class WorkQueueWithBuffer : public WorkQueue {
public:
- constexpr WorkQueueWithBuffer() : WorkQueue(queue_storage_) {}
+ constexpr WorkQueueWithBuffer() : WorkQueue(queue_, kWorkQueueEntries) {}
private:
- std::array<WorkItem, kWorkQueueEntries> queue_storage_;
+ InlineQueue<WorkItem, kWorkQueueEntries> queue_;
};
} // namespace pw::work_queue
diff --git a/pw_work_queue/work_queue.cc b/pw_work_queue/work_queue.cc
index 62be12381..3890d5c61 100644
--- a/pw_work_queue/work_queue.cc
+++ b/pw_work_queue/work_queue.cc
@@ -21,8 +21,10 @@
namespace pw::work_queue {
void WorkQueue::RequestStop() {
- std::lock_guard lock(lock_);
- stop_requested_ = true;
+ {
+ std::lock_guard lock(lock_);
+ stop_requested_ = true;
+ } // Release lock before calling .release() on the semaphore.
work_notification_.release();
}
@@ -37,8 +39,11 @@ void WorkQueue::Run() {
std::optional<WorkItem> possible_work_item;
{
std::lock_guard lock(lock_);
- possible_work_item = circular_buffer_.Pop();
- work_remaining = !circular_buffer_.empty();
+ if (!queue_.empty()) {
+ possible_work_item.emplace(std::move(queue_.front()));
+ queue_.pop();
+ }
+ work_remaining = !queue_.empty();
stop_requested = stop_requested_;
}
if (!possible_work_item.has_value()) {
@@ -62,29 +67,30 @@ void WorkQueue::CheckPushWork(WorkItem&& work_item) {
}
Status WorkQueue::InternalPushWork(WorkItem&& work_item) {
- std::lock_guard lock(lock_);
+ {
+ std::lock_guard lock(lock_);
- if (stop_requested_) {
- // Entries are not permitted to be enqueued once stop has been requested.
- return Status::FailedPrecondition();
- }
+ if (stop_requested_) {
+ // Entries are not permitted to be enqueued once stop has been requested.
+ return Status::FailedPrecondition();
+ }
- if (circular_buffer_.full()) {
- return Status::ResourceExhausted();
- }
+ if (queue_.full()) {
+ return Status::ResourceExhausted();
+ }
- circular_buffer_.Push(std::move(work_item));
-
- // Update the watermarks for the queue.
- const uint32_t queue_entries = circular_buffer_.size();
- if (queue_entries > max_queue_used_.value()) {
- max_queue_used_.Set(queue_entries);
- }
- const uint32_t queue_remaining = circular_buffer_.capacity() - queue_entries;
- if (queue_remaining < min_queue_remaining_.value()) {
- min_queue_remaining_.Set(queue_entries);
- }
+ queue_.emplace(std::move(work_item));
+ // Update the watermarks for the queue.
+ const uint32_t queue_entries = queue_.size();
+ if (queue_entries > max_queue_used_.value()) {
+ max_queue_used_.Set(queue_entries);
+ }
+ const uint32_t queue_remaining = queue_.capacity() - queue_entries;
+ if (queue_remaining < min_queue_remaining_.value()) {
+ min_queue_remaining_.Set(queue_entries);
+ }
+ } // Release lock before calling .release() on the semaphore.
work_notification_.release();
return OkStatus();
}
diff --git a/rollup.config.js b/rollup.config.js
index fbe041be2..9add0a636 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -16,75 +16,110 @@ import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import pluginTypescript from '@rollup/plugin-typescript';
import path from 'path';
-import dts from 'rollup-plugin-dts';
import nodePolyfills from 'rollup-plugin-node-polyfills';
+import postcss from 'rollup-plugin-postcss';
import sourceMaps from 'rollup-plugin-sourcemaps';
-import tsConfig from './tsconfig.json';
-
export default [
// Bundle proto collection script
{
input: path.join('pw_protobuf_compiler', 'ts', 'build.ts'),
- output: [{
- file: path.join('dist', 'bin', 'pw_protobuf_compiler.js'),
- format: 'cjs',
- banner: '#!/usr/bin/env node\n\nconst window = null;'
- }],
+ output: [
+ {
+ file: path.join('dist', 'bin', 'pw_protobuf_compiler.js'),
+ format: 'cjs',
+ banner: '#!/usr/bin/env node\n\nconst window = null;',
+ },
+ ],
plugins: [
- pluginTypescript(
- {tsconfig: './tsconfig.json', exclude: ['**/*_test.ts']}),
+ pluginTypescript({
+ tsconfig: './tsconfig.json',
+ exclude: ['**/*_test.ts'],
+ }),
resolve(),
commonjs(),
// Resolve source maps to the original source
- sourceMaps()
- ]
+ sourceMaps(),
+ ],
},
// bundle proto collection template used by the above script
{
input: path.join(
- 'pw_protobuf_compiler', 'ts', 'ts_proto_collection.template.ts'),
- output: [{
- file: path.join('dist', 'bin', 'ts_proto_collection.template.js'),
- format: 'esm',
- banner: '/* eslint-disable */'
- }],
+ 'pw_protobuf_compiler',
+ 'ts',
+ 'ts_proto_collection.template.ts',
+ ),
+ output: [
+ {
+ file: path.join('dist', 'bin', 'ts_proto_collection.template.js'),
+ format: 'esm',
+ banner: '/* eslint-disable */',
+ },
+ ],
plugins: [
- pluginTypescript(
- {tsconfig: './tsconfig.json', exclude: ['**/*_test.ts']}),
+ pluginTypescript({
+ tsconfig: './tsconfig.json',
+ exclude: ['**/*_test.ts'],
+ }),
resolve(),
commonjs(),
// Resolve source maps to the original source
- sourceMaps()
- ]
+ sourceMaps(),
+ ],
},
// Bundle proto collection into one UMD file for consumption from browser
{
input: path.join('dist', 'protos', 'collection.ts'),
- output: [{
- file: path.join('dist', 'protos', 'collection.umd.js'),
- format: 'umd',
- sourcemap: true,
- name: 'PigweedProtoCollection',
- }],
+ output: [
+ {
+ file: path.join('dist', 'protos', 'collection.umd.js'),
+ format: 'umd',
+ sourcemap: true,
+ name: 'PigweedProtoCollection',
+ },
+ ],
plugins: [
- pluginTypescript({tsconfig: './tsconfig.json'}),
+ pluginTypescript({ tsconfig: './tsconfig.json' }),
commonjs(),
resolve(),
// Resolve source maps to the original source
- sourceMaps()
- ]
+ sourceMaps(),
+ ],
},
- // Types for proto collection
+ // Bundle Pigweed log component and modules
{
- input: path.join(
- 'dist', 'protos', 'types', 'dist', 'protos', 'collection.d.ts'),
- output:
- [{file: path.join('dist', 'protos', 'collection.d.ts'), format: 'es'}],
- plugins: [dts({compilerOptions: tsConfig.compilerOptions})]
+ input: path.join('ts', 'logging.ts'),
+ output: [
+ {
+ file: path.join('dist', 'logging.umd.js'),
+ format: 'umd',
+ sourcemap: true,
+ name: 'PigweedLogging',
+ inlineDynamicImports: true,
+ },
+ {
+ file: path.join('dist', 'logging.mjs'),
+ format: 'esm',
+ sourcemap: true,
+ inlineDynamicImports: true,
+ },
+ ],
+ plugins: [
+ postcss({ plugins: [] }),
+ pluginTypescript({
+ tsconfig: './tsconfig.json',
+ exclude: ['**/*_test.ts'],
+ }),
+ nodePolyfills(),
+ resolve(),
+ commonjs(),
+
+ // Resolve source maps to the original source
+ sourceMaps(),
+ ],
},
// Bundle Pigweed modules
{
@@ -100,17 +135,19 @@ export default [
file: path.join('dist', 'index.mjs'),
format: 'esm',
sourcemap: true,
- }
+ },
],
plugins: [
- pluginTypescript(
- {tsconfig: './tsconfig.json', exclude: ['**/*_test.ts']}),
+ pluginTypescript({
+ tsconfig: './tsconfig.json',
+ exclude: ['**/*_test.ts'],
+ }),
nodePolyfills(),
resolve(),
commonjs(),
// Resolve source maps to the original source
- sourceMaps()
- ]
- }
+ sourceMaps(),
+ ],
+ },
];
diff --git a/seed/0000-index.rst b/seed/0000-index.rst
index 35f1e753f..218a70e77 100644
--- a/seed/0000-index.rst
+++ b/seed/0000-index.rst
@@ -12,3 +12,17 @@ All pending, active, and resolved SEEDs are listed below.
0002-template
0101-pigweed.json
0102-module-docs
+ 0103: pw_protobuf: Past, present, and future<https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/133971>
+ 0104-display-support
+ 0105-pw_tokenizer-pw_log-nested-tokens
+ 0106: Project Template <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/155430>
+ 0107-communications
+ 0108-pw_emu-emulators-frontend
+ 0109-comms-buffers
+ 0110-memory-allocation-interfaces
+ 0111-build-systems
+ 0112-async-poll
+ 0113-bazel-cc-toolchain-api
+ 0114: Channels <http://pigweed-review.googlesource.com/c/pigweed/pigweed/+/175471>
+ 0115: pw_sensor Sensors <http://pigweed-review.googlesource.com/c/pigweed/pigweed/+/176760>
+ 0116: pw_net Sockets <http://pigweed-review.googlesource.com/c/pigweed/pigweed/+/177696>
diff --git a/seed/0001-the-seed-process.rst b/seed/0001-the-seed-process.rst
index 3b7958bd6..0ec702b1e 100644
--- a/seed/0001-the-seed-process.rst
+++ b/seed/0001-the-seed-process.rst
@@ -3,22 +3,12 @@
======================
0001: The SEED Process
======================
-
-.. card::
- :fas:`seedling` SEED-0001: :ref:`The SEED Process<seed-0001>`
-
- :octicon:`comment-discussion` Status:
- :bdg-secondary-line:`Open for Comments`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Last Call`
- :octicon:`chevron-right`
- :bdg-primary:`Accepted`
- :octicon:`kebab-horizontal`
- :bdg-secondary-line:`Rejected`
-
- :octicon:`calendar` Proposal Date: 2022-10-31
-
- :octicon:`code-review` CL: `pwrev/116577 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/116577>`_
+.. seed::
+ :number: 1
+ :name: The SEED Process
+ :status: Accepted
+ :proposal_date: 2022-10-31
+ :cl: 116577
-------
Summary
@@ -101,27 +91,47 @@ Process
Suppose you'd like to propose a new Pigweed RPC Over Smoke Signals protocol.
#. If you haven't already, clone the Pigweed repository and set it up locally,
- following the :ref:`docs-getting-started` guide.
+ following the :ref:`docs-get-started-upstream` guide.
-#. Copy the `SEED template <0002-template>`_ to create the RST file for your
- SEED. As you don't yet have a SEED number, use XXXX as a placeholder,
- followed by the lowercase title of the proposal, with words separated by
+#. Claim a number for your SEED. This should be the next sequential number
+ listed within the `SEED index`_'s ``toctree`` table. (We will use 5309 for
+ our example.)
+
+ .. _SEED index: https://cs.opensource.google/pigweed/pigweed/+/main:seed/0000-index.rst
+
+#. Create a new RST file for your SEED. Name the file with the number of your
+ SEED followed by the lowercase title of the proposal, with words separated by
hyphens.
.. code-block:: sh
- cp seed/0002-template.rst seed/XXXX-pw_rpc-over-smoke-signals.rst
+ touch seed/5309-pw_rpc-over-smoke-signals.rst
-#. Push up the template to Gerrit, marking it as a Work-In-Progress change.
- From here, you may fill the template out with the content of your proposal
- at your convenience.
+ Include your document in the GN build by modifying ``seed/BUILD.gn``:
-#. At any point, you may claim a SEED number by opening the
- `SEED index`_ and taking the next available number by inserting
- a row into the ``toctree`` table. Link the entry to the WIP change for your
- SEED.
+ .. code-block::
- .. _SEED index: https://cs.opensource.google/pigweed/pigweed/+/main:seed/0000-index.rst
+ # Insert your dependency to the doc group at the top of the file.
+ pw_doc_group("docs") {
+ group_deps = [
+ ":0001",
+ ...
+ ":5308",
+ ":5309",
+ ]
+ }
+
+ # Define a doc group target for your SEED.
+ pw_doc_group("5309") {
+ sources = [ "5309-pw_rpc-over-smoke-signals.rst" ]
+ }
+
+#. Push up your document to Gerrit, marking it as a Work-In-Progress change,
+ following the :ref:`docs-contributing` guide.
+
+#. Update the ``toctree`` in the SEED index adding a row for your SEED. The
+ entry should be labeled with the title of your SEED and link to your work
+ in progress change.
.. code-block:: rst
@@ -140,18 +150,34 @@ Suppose you'd like to propose a new Pigweed RPC Over Smoke Signals protocol.
git add seed/0000-index.rst
git commit -m "SEED-5309: Claim SEED number"
-#. Push up a changelist (CL) to Gerrit following the :ref:`docs-contributing`
- guide and add GWSQ as a reviewer. Set ``Pigweed-Auto-Submit`` to +1.
+#. Push a separate change to Gerrit with your SEED index modification and add
+ GWSQ as a reviewer. Set ``Pigweed-Auto-Submit`` to +1.
.. image:: 0001-the-seed-process/seed-index-gerrit.png
-#. Once your CL has been reviewed and submitted, the SEED number belongs to you.
- Update your document's template and filename with this number.
+#. Fill out your proposal document, using the :ref:`SEED template<seed-0002>` as
+ a guide.
+
+ If your SEED requires additional resources such as images, place them within
+ a subdirectory named identically to your document without the ``.rst``
+ extension. These should be listed as ``inputs`` in your SEED's GN doc group
+ target.
+
+ .. code-block::
+
+ seed/
+ ...
+ 5309-pw_rpc-over-smoke-signals.rst
+ 5309-pw_rpc-over-smoke-signals/
+ state-diagram.svg
#. When you feel you have enough substantive content in your proposal to be
reviewed, push it up to Gerrit and switch the change from WIP to Active.
This will begin the open comments period.
+#. Create a thread for your SEED in the ``#seed`` channel of Pigweed's
+ `Discord server <https://discord.gg/M9NSeTA>`_.
+
#. Engage with reviewers to iterate on your proposal through its comment period.
#. When a tentative decision has been reached, a Pigweed team member will
@@ -214,15 +240,19 @@ for comments.**
- The SEED remains open for as long as necessary. Internally, Pigweed's review
committee will regularly meet to consider active SEEDs and determine when to
advance to them the next stage.
+- Open SEEDs are assigned owners in the core Pigweed team, who are primarily
+ responsible for engaging with the author to move the SEED through its review
+ process.
:bdg-warning:`Last Call` **A tentative decision has been reached, but
commenters may raise final objections.**
- A tentative decision on the SEED has been made. The decision is issued at the
- best judgement of Pigweed's review committee when they feel there has been
- sufficient discussion on the tradeoffs of the proposal to do so.
-- Transition is triggered manually by a member of the Pigweed team, with a
- comment on the likely outcome of the SEED (acceptance / rejection).
+ best judgement of the SEED's owner within the Pigweed team when they feel
+ there has been sufficient discussion on the tradeoffs of the proposal to do
+ so.
+- Transition is triggered manually by its owner, with a comment on the likely
+ outcome of the SEED (acceptance / rejection).
- On entering Last Call, the visibility of the SEED is widely boosted through
Pigweed's communication channels (Discord, mailing list, Pigweed Live, etc.)
to solicit any strong objections from stakeholders.
diff --git a/seed/0002-template.rst b/seed/0002-template.rst
index d6700a361..844c6e18a 100644
--- a/seed/0002-template.rst
+++ b/seed/0002-template.rst
@@ -3,22 +3,12 @@
===================
0002: SEED Template
===================
-
-.. card::
- :fas:`seedling` SEED-0002: :ref:`SEED Template<seed-0002>`
-
- :octicon:`comment-discussion` Status:
- :bdg-primary:`Open for Comments`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Last Call`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Accepted`
- :octicon:`kebab-horizontal`
- :bdg-secondary-line:`Rejected`
-
- :octicon:`calendar` Proposal Date: 2022-11-30
-
- :octicon:`code-review` CL: `pwrev/123090 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/123090>`_
+.. seed::
+ :number: 2
+ :name: SEED Template
+ :status: Open for Comments
+ :proposal_date: 2022-11-30
+ :cl: 123090
-------
Summary
@@ -32,13 +22,14 @@ Motivation
Describe the purpose of this proposal. What problem is it trying to solve?
Include any relevant background information.
----------------
-Guide reference
----------------
-In this section, detail the user-facing impacts if the proposal were to be
-accepted. Treat it as a reference for the new feature, describing the changes to
-a user without background context on the proposal, with explanations of key
-concepts and examples.
+--------
+Proposal
+--------
+In this section, provide a detailed treatment of your proposal, and the
+user-facing impacts if the proposal were to be accepted. Treat it as a
+reference for the new feature, describing the changes to a user without
+background context on the proposal, with explanations of key concepts and
+examples.
For example, if the proposal adds a new library, this section should describe
its public API, and be written in the style of API documentation.
diff --git a/seed/0101-pigweed.json.rst b/seed/0101-pigweed.json.rst
index b8def7f82..0e207a67b 100644
--- a/seed/0101-pigweed.json.rst
+++ b/seed/0101-pigweed.json.rst
@@ -3,22 +3,12 @@
==================
0101: pigweed.json
==================
-
-.. card::
- :fas:`seedling` SEED-0101: :ref:`pigweed.json<seed-0101>`
-
- :octicon:`comment-discussion` Status:
- :bdg-secondary-line:`Open for Comments`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Last Call`
- :octicon:`chevron-right`
- :bdg-primary:`Accepted`
- :octicon:`kebab-horizontal`
- :bdg-secondary-line:`Rejected`
-
- :octicon:`calendar` Proposal Date: 2023-02-06
-
- :octicon:`code-review` CL: `pwrev/128010 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/128010>`_
+.. seed::
+ :number: 0101
+ :name: pigweed.json
+ :status: Accepted
+ :proposal_date: 2023-02-06
+ :cl: 128010
-------
Summary
@@ -37,26 +27,26 @@ something like this:
.. code-block::
- {
- "root_variable": "<PROJNAME>_ROOT",
- "cipd_package_files": ["tools/default.json"],
- "virtualenv": {
- "gn_args": ["dir_pw_third_party_stm32cube=\"\""],
- "gn_root": ".",
- "gn_targets": [":python.install"]
- },
- "optional_submodules": ["vendor/shhh-secret"],
- "gni_file": "build_overrides/pigweed_environment.gni"
- }
+ {
+ "root_variable": "<PROJNAME>_ROOT",
+ "cipd_package_files": ["tools/default.json"],
+ "virtualenv": {
+ "gn_args": ["dir_pw_third_party_stm32cube=\"\""],
+ "gn_root": ".",
+ "gn_targets": [":python.install"]
+ },
+ "optional_submodules": ["vendor/shhh-secret"],
+ "gni_file": "build_overrides/pigweed_environment.gni"
+ }
The plugins to the ``pw`` command-line utility are configured in ``PW_PLUGINS``,
which looks like this:
.. code-block::
- # <name> <Python module> <function>
- console pw_console.__main__ main
- format pw_presubmit.format_code _pigweed_upstream_main
+ # <name> <Python module> <function>
+ console pw_console.__main__ main
+ format pw_presubmit.format_code _pigweed_upstream_main
In addition, changes have been proposed to configure some of the behavior of
``pw format`` and the formatting steps of ``pw presubmit`` from config files,
@@ -140,54 +130,54 @@ documentation related to the option in question.
.. code-block::
- {
- "pw": {
- "pw_cli": {
- "plugins": {
- "console": {
- "module": "pw_console.__main__",
- "function": "main"
- },
- "format": {
- "module": "pw_presubmit.format_code",
- "function": "_pigweed_upstream_main"
- }
- }
- },
- "pw_env_setup": {
- "root_variable": "<PROJNAME>_ROOT",
- "rosetta": "allow",
- "gni_file": "build_overrides/pigweed_environment.gni",
- "cipd": {
- "package_files": [
- "tools/default.json"
- ]
- },
- "virtualenv": {
- "gn_args": [
- "dir_pw_third_party_stm32cube=\"\""
- ],
- "gn_targets": [
- "python.install"
- ],
- "gn_root": "."
- },
- "submodules": {
- "optional": [
- "vendor/shhh-secret"
- ]
- }
- },
- "pw_presubmit": {
- "format": {
- "python": {
- "formatter": "black",
- "black_path": "pyink"
- }
- }
- }
- }
- }
+ {
+ "pw": {
+ "pw_cli": {
+ "plugins": {
+ "console": {
+ "module": "pw_console.__main__",
+ "function": "main"
+ },
+ "format": {
+ "module": "pw_presubmit.format_code",
+ "function": "_pigweed_upstream_main"
+ }
+ }
+ },
+ "pw_env_setup": {
+ "root_variable": "<PROJNAME>_ROOT",
+ "rosetta": "allow",
+ "gni_file": "build_overrides/pigweed_environment.gni",
+ "cipd": {
+ "package_files": [
+ "tools/default.json"
+ ]
+ },
+ "virtualenv": {
+ "gn_args": [
+ "dir_pw_third_party_stm32cube=\"\""
+ ],
+ "gn_targets": [
+ "python.install"
+ ],
+ "gn_root": "."
+ },
+ "submodules": {
+ "optional": [
+ "vendor/shhh-secret"
+ ]
+ }
+ },
+ "pw_presubmit": {
+ "format": {
+ "python": {
+ "formatter": "black",
+ "black_path": "pyink"
+ }
+ }
+ }
+ }
+ }
Some teams will resist a new file at the root of their checkout, but this seed
won't be adding any files, it'll be combining at least one top-level file, maybe
diff --git a/seed/0102-module-docs.rst b/seed/0102-module-docs.rst
index 5ca9ab5bf..7cd8659e7 100644
--- a/seed/0102-module-docs.rst
+++ b/seed/0102-module-docs.rst
@@ -3,22 +3,26 @@
=====================================
0102: Consistent Module Documentation
=====================================
-
-.. card::
- :fas:`seedling` SEED-0102: :ref:`Consistent Module Documentation<seed-0102>`
-
- :octicon:`comment-discussion` Status:
- :bdg-secondary-line:`Open for Comments`
- :octicon:`chevron-right`
- :bdg-secondary-line:`Last Call`
- :octicon:`chevron-right`
- :bdg-primary:`Accepted`
- :octicon:`kebab-horizontal`
- :bdg-secondary-line:`Rejected`
-
- :octicon:`calendar` Proposal Date: 2023-02-10
-
- :octicon:`code-review` CL: `pwrev/128811 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/128811>`_, `pwrev/130410 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/130410>`_
+.. seed::
+ :number: 102
+ :name: Consistent Module Documentation
+ :status: Accepted
+ :proposal_date: 2023-02-10
+ :cl: 128811, 130410
+
+---------------------
+Status (October 2023)
+---------------------
+If you're looking for guidelines on how to author module docs, use these:
+:ref:`docs-contrib-moduledocs`
+
+.. caution::
+
+ SEED-0102 is still considered accepted because we are still using some parts
+ of it in our current module docs guidelines. However, over the course of
+ 2023 we discovered that other parts of the SEED-0102 plan didn't achieve our
+ goals. Therefore, at this point you should only read SEED-0102 for historical
+ context on how our docs guidelines have evolved.
-------
Summary
diff --git a/seed/0104-display-support.rst b/seed/0104-display-support.rst
new file mode 100644
index 000000000..948a4cfcb
--- /dev/null
+++ b/seed/0104-display-support.rst
@@ -0,0 +1,785 @@
+.. _seed-0104:
+
+=====================
+0104: Display Support
+=====================
+.. seed::
+ :number: 104
+ :name: Display Support
+ :status: Accepted
+ :proposal_date: 2023-06-12
+ :cl: 150793
+
+-------
+Summary
+-------
+Add support for graphics displays. This includes display drivers for a few
+popular display controllers, framebuffer management, and a framework to simplify
+adding a graphics display to a Pigweed application.
+
+----------
+Motivation
+----------
+Pigweed currently has no specific support for a display device. Projects that
+require a display currently must do the full implementation, including the
+display driver in most instances, to add display support.
+
+This proposes the addition of a basic framework for display devices, as well
+as implementations for a few common Pigweed test devices - specifically the
+STM32F429I. This enables developers to quickly and easily add display support
+for supported devices and an implementation to model when adding new device
+support.
+
+---------------
+Proposal
+---------------
+This proposes no changes to existing modules, but suggests several new modules
+that together define a framework for rendering to displays.
+
+
+New Modules
+-----------
+
+.. list-table::
+ :widths: 5 45
+ :header-rows: 1
+
+ * - Module
+ - Function
+
+ * - pw_display
+ - Manage draw thread, framebuffers, and driver
+
+ * - pw_display_driver
+ - Display driver interface definition
+
+ * - pw_display_driver_ili9341
+ - Display driver for the ILI9341 display controller
+
+ * - pw_display_driver_imgui
+ - Host display driver using `Dear ImGui <https://www.dearimgui.com/>`_
+
+ * - pw_display_driver_mipi
+ - Display driver for `MIPI DSI <https://www.mipi.org/specifications/dsi>`_ controllers
+
+ * - pw_display_driver_null
+ - Null display driver for headless devices
+
+ * - pw_display_driver_st7735
+ - Display driver for the `ST7735 <https://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf>`_ display controller
+
+ * - pw_display_driver_st7789
+ - Display driver for the ST7789 display controller
+
+ * - pw_simple_draw
+ - Very basic drawing library for test purposes
+
+ * - pw_framebuffer
+ - Manage access to pixel buffer.
+
+ * - pw_framebuffer_mcuxpresso
+ - Specialization of the framebuffer for the MCUxpresso devices
+
+ * - pw_geometry
+ - Basic shared math types such as 2D vectors, etc.
+
+ * - pw_pixel_pusher
+ - Transport of pixel data to display controller
+
+
+Math
+----
+``pw_geometry`` contains two helper structures for common values usually used as
+a pair.
+
+.. code-block:: cpp
+
+ namespace pw::math {
+
+ template <typename T>
+ struct Size {
+ T width;
+ T height;
+ };
+
+ template <typename T>
+ struct Vector2 {
+ T x;
+ T y;
+ };
+
+ } // namespace pw::math
+
+
+Framebuffer
+-----------
+A framebuffer is a small class that provides access to a pixel buffer. It
+keeps a copy of the pixel buffer metadata and provides accessor methods for
+those values.
+
+.. code-block:: cpp
+
+ namespace pw::framebuffer {
+
+ enum class PixelFormat {
+ None,
+ RGB565,
+ };
+
+ class Framebuffer {
+ public:
+ // Construct a default invalid framebuffer.
+ Framebuffer();
+
+ Framebuffer(void* data,
+ PixelFormat pixel_format,
+ pw::math::Size<uint16_t> size,
+ uint16_t row_bytes);
+
+ Framebuffer(const Framebuffer&) = delete;
+ Framebuffer(Framebuffer&& other);
+
+ Framebuffer& operator=(const Framebuffer&) = delete;
+ Framebuffer& operator=(Framebuffer&&);
+
+ bool is_valid() const;
+
+ pw::ConstByteSpan data() const;
+ pw::ByteSpan data();
+
+ PixelFormat pixel_format() const;
+
+ pw::math::Size<uint16_t> size();
+
+ uint16_t row_bytes() const;
+ };
+
+ } // namespace pw::framebuffer
+
+FrameBuffer is a moveable class that is intended to signify read/write
+privileges to the underlying pixel data. This makes it easier to track when the
+pixel data may be read from, or written to, without conflict.
+
+The framebuffer does not own the underlying pixel buffer. In other words
+the deletion of a framebuffer will not free the underlying pixel data.
+
+Framebuffers do not have methods for reading or writing to the underlying pixel
+buffer. This is the responsibility of the the selected graphics library which
+can be given the pixel buffer pointer retrieved by calling ``data()``.
+
+.. code-block:: cpp
+
+ constexpr size_t kWidth = 64;
+ constexpr size_t kHeight = 32;
+ uint16_t pixel_data[kWidth * kHeight];
+
+ void DrawScreen(Framebuffer* fb) {
+ // Clear framebuffer to black.
+ std::memset(fb->data(), 0, fb->height() * fb->row_bytes());
+
+ // Set first pixel to white.
+ uint16_t* pixel_data = static_cast<uint16_t*>(fb->data());
+ pixel_data[0] = 0xffff;
+ }
+
+ Framebuffer fb(pixel_data, {kWidth, kHeight},
+ PixelFormat::RGB565,
+ kWidth * sizeof(uint16_t));
+ DrawScreen(&fb);
+
+FramebufferPool
+---------------
+
+The FramebufferPool is intended to simplify the use of multiple framebuffers
+when multi-buffered rendering is being used. It is a collection of framebuffers
+which can be retrieved, used, and then returned to the pool for reuse. All
+framebuffers in the pool share identical attributes. A framebuffer that is
+returned to a caller of ``GetFramebuffer()`` can be thought of as "on loan" to
+that caller and will not be given to any other caller of ``GetFramebuffer()``
+until it has been returned by calling ``ReleaseFramebuffer()``.
+
+.. code-block:: cpp
+
+ namespace pw::framebuffer_pool {
+
+ class FramebufferPool {
+ public:
+ using BufferArray = std::array<void*, FRAMEBUFFER_COUNT>;
+
+ // Constructor parameters.
+ struct Config {
+ BufferArray fb_addr; // Address of each buffer in this pool.
+ pw::math::Size<uint16_t> dimensions; // width/height of each buffer.
+ uint16_t row_bytes; // row bytes of each buffer.
+ pw::framebuffer::PixelFormat pixel_format;
+ };
+
+ FramebufferPool(const Config& config);
+ virtual ~FramebufferPool();
+
+ uint16_t row_bytes() const;
+
+ pw::math::Size<uint16_t> dimensions() const;
+
+ pw::framebuffer::PixelFormat pixel_format() const;
+
+ // Return a framebuffer to the caller for use. This call WILL BLOCK until a
+ // framebuffer is returned for use. Framebuffers *must* be returned to this
+ // pool by a corresponding call to ReleaseFramebuffer. This function will only
+ // return a valid framebuffer.
+ //
+ // This call is thread-safe, but not interrupt safe.
+ virtual pw::framebuffer::Framebuffer GetFramebuffer();
+
+ // Return the framebuffer to the pool available for use by the next call to
+ // GetFramebuffer.
+ //
+ // This may be called on another thread or during an interrupt.
+ virtual Status ReleaseFramebuffer(pw::framebuffer::Framebuffer framebuffer);
+ };
+
+ } // namespace pw::framebuffer
+
+An example use of the framebuffer pool is:
+
+.. code-block:: cpp
+
+ // Retrieve a framebuffer for drawing. May block if pool has no framebuffers
+ // to issue.
+ FrameBuffer fb = framebuffer_pool.GetFramebuffer();
+
+ // Draw to the framebuffer.
+ UpdateDisplay(&fb);
+
+ // Return the framebuffer to the pool for reuse.
+ framebuffer_pool.ReleaseFramebuffer(std::move(fb));
+
+DisplayDriver
+-------------
+
+A DisplayDriver is usually the sole class responsible for communicating with the
+display controller. Its primary responsibilities are the display controller
+initialization, and the writing of pixel data when a display update is needed.
+
+This proposal supports multiple heterogenous display controllers. This could be:
+
+1. A single display of any given type (e.g. ILI9341).
+2. Two ILI9341 displays.
+3. Two ILI9341 displays and a second one of a different type.
+
+Because of this approach the DisplayDriver is defined as an interface:
+
+.. code-block:: cpp
+
+ namespace pw::display_driver {
+
+ class DisplayDriver {
+ public:
+ // Called on the completion of a write operation.
+ using WriteCallback = Callback<void(framebuffer::Framebuffer, Status)>;
+
+ virtual ~DisplayDriver() = default;
+
+ virtual Status Init() = 0;
+
+ virtual void WriteFramebuffer(pw::framebuffer::Framebuffer framebuffer,
+ WriteCallback write_callback) = 0;
+
+ virtual pw::math::Size<uint16_t> size() const = 0;
+ };
+
+ } // namespace pw::display_driver
+
+Each driver then provides a concrete implementation of the driver. Below is the
+definition of the display driver for the ILI9341:
+
+.. code-block:: cpp
+
+ namespace pw::display_driver {
+
+ class DisplayDriverILI9341 : public DisplayDriver {
+ public:
+ struct Config {
+ // Device specific initialization parameters.
+ };
+
+ DisplayDriverILI9341(const Config& config);
+
+ // DisplayDriver implementation:
+ Status Init() override;
+ void WriteFramebuffer(pw::framebuffer::Framebuffer framebuffer,
+ WriteCallback write_callback) override;
+ Status WriteRow(span<uint16_t> row_pixels,
+ uint16_t row_idx,
+ uint16_t col_idx) override;
+ pw::math::Size<uint16_t> size() const override;
+
+ private:
+ enum class Mode {
+ kData,
+ kCommand,
+ };
+
+ // A command and optional data to write to the ILI9341.
+ struct Command {
+ uint8_t command;
+ ConstByteSpan command_data;
+ };
+
+ // Toggle the reset GPIO line to reset the display controller.
+ Status Reset();
+
+ // Set the command/data mode of the display controller.
+ void SetMode(Mode mode);
+ // Write the command to the display controller.
+ Status WriteCommand(pw::spi::Device::Transaction& transaction,
+ const Command& command);
+ };
+
+ } // namespace pw::display_driver
+
+Here is an example retrieving a framebuffer from the framebuffer pool, drawing
+into the framebuffer, using the display driver to write the pixel data, and then
+returning the framebuffer back to the pool for use.
+
+.. code-block:: cpp
+
+ FrameBuffer fb = framebuffer_pool.GetFramebuffer();
+
+ // DrawScreen is a function that will draw to the framebuffer's underlying
+ // pixel buffer using a drawing library. See example above.
+ DrawScreen(&fb);
+
+ display_driver_.WriteFramebuffer(
+ std::move(framebuffer),
+ [&framebuffer_pool](pw::framebuffer::Framebuffer fb, Status status) {
+ // Return the framebuffer back to the pool for reuse once the display
+ // write is complete.
+ framebuffer_pool.ReleaseFramebuffer(std::move(fb));
+ });
+
+In the example above that the framebuffer (``fb``) is moved when calling
+``WriteFramebuffer()`` passing ownership to the display driver. From this point
+forward the application code may not access the framebuffer in any way. When the
+framebuffer write is complete the framebuffer is then moved to the callback
+which in turn moves it when calling ``ReleaseFramebuffer()``.
+
+``WriteFramebuffer()`` always does a write of the full framebuffer - sending all
+pixel data.
+
+``WriteFramebuffer()`` may be a blocking call, but on some platforms the driver
+may use a background write and the write callback is called when the write
+is complete. The write callback **may be called during an interrupt**.
+
+PixelPusher
+-----------
+Pixel data for Simple SPI based display controllers can be written to the
+display controller using ``pw_spi``. There are some controllers which use
+other interfaces (RGB, MIPI, etc.). Also, some vendors provide an API for
+interacting with these display controllers for writing pixel data.
+
+To allow the drivers to be hardware/vendor independent the ``PixelPusher``
+may be used. This defines an interface whose sole responsibility is to write
+a framebuffer to the display controller. Specializations of this will use
+``pw_spi`` or vendor proprietary calls to write pixel data.
+
+.. code-block:: cpp
+
+ namespace pw::pixel_pusher {
+
+ class PixelPusher {
+ public:
+ using WriteCallback = Callback<void(framebuffer::Framebuffer, Status)>;
+
+ virtual ~PixelPusher() = default;
+
+ virtual Status Init(
+ const pw::framebuffer_pool::FramebufferPool& framebuffer_pool) = 0;
+
+ virtual void WriteFramebuffer(framebuffer::Framebuffer framebuffer,
+ WriteCallback complete_callback) = 0;
+ };
+
+ } // namespace pw::pixel_pusher
+
+Display
+-------
+
+Each display has:
+
+1. One and only one display driver.
+2. A reference to a single framebuffer pool. This framebuffer pool may be shared
+ with other displays.
+3. A drawing thread, if so configured, for asynchronous display updates.
+
+.. code-block:: cpp
+
+ namespace pw::display {
+
+ class Display {
+ public:
+ // Called on the completion of an update.
+ using WriteCallback = Callback<void(Status)>;
+
+ Display(pw::display_driver::DisplayDriver& display_driver,
+ pw::math::Size<uint16_t> size,
+ pw::framebuffer_pool::FramebufferPool& framebuffer_pool);
+ virtual ~Display();
+
+ pw::framebuffer::Framebuffer GetFramebuffer();
+
+ void ReleaseFramebuffer(pw::framebuffer::Framebuffer framebuffer,
+ WriteCallback callback);
+
+ pw::math::Size<uint16_t> size() const;
+ };
+
+ } // namespace pw::display
+
+Once applications are initialized they typically will not directly interact with
+display drivers or framebuffer pools. These will be utilized by the display
+which will provide a simpler interface.
+
+``Display::GetFramebuffer()`` must always be called on the same thread and is not
+interrupt safe. It will block if there is no available framebuffer in the
+framebuffer pool waiting for a framebuffer to be returned.
+
+``Display::ReleaseFramebuffer()`` must be called for each framebuffer returned by
+``Display::GetFramebuffer()``. This will initiate the display update using the
+displays associated driver. The ``callback`` will be called when this update is
+complete.
+
+A simplified application rendering loop would resemble:
+
+.. code-block:: cpp
+
+ // Get a framebuffer for drawing.
+ FrameBuffer fb = display.GetFramebuffer();
+
+ // DrawScreen is a function that will draw to |fb|'s pixel buffer using a
+ // drawing library. See example above.
+ DrawScreen(&fb);
+
+ // Return the framebuffer to the display which will be written to the display
+ // controller by the display's display driver.
+ display.ReleaseFramebuffer(std::move(fb), [](Status){});
+
+Simple Drawing Module
+---------------------
+
+``pw_simple_draw`` was created for testing and verification purposes only. It is
+not intended to be feature rich or performant in any way. This is small
+collection of basic drawing primitives not intended to be used by shipping
+applications.
+
+.. code-block:: cpp
+
+ namespace pw::draw {
+
+ void DrawLine(pw::framebuffer::Framebuffer& fb,
+ int x1,
+ int y1,
+ int x2,
+ int y2,
+ pw::color::color_rgb565_t pen_color);
+
+ // Draw a circle at center_x, center_y with given radius and color. Only a
+ // one-pixel outline is drawn if filled is false.
+ void DrawCircle(pw::framebuffer::Framebuffer& fb,
+ int center_x,
+ int center_y,
+ int radius,
+ pw::color::color_rgb565_t pen_color,
+ bool filled);
+
+ void DrawHLine(pw::framebuffer::Framebuffer& fb,
+ int x1,
+ int x2,
+ int y,
+ pw::color::color_rgb565_t pen_color);
+
+ void DrawRect(pw::framebuffer::Framebuffer& fb,
+ int x1,
+ int y1,
+ int x2,
+ int y2,
+ pw::color::color_rgb565_t pen_color,
+ bool filled);
+
+ void DrawRectWH(pw::framebuffer::Framebuffer& fb,
+ int x,
+ int y,
+ int w,
+ int h,
+ pw::color::color_rgb565_t pen_color,
+ bool filled);
+
+ void Fill(pw::framebuffer::Framebuffer& fb,
+ pw::color::color_rgb565_t pen_color);
+
+ void DrawSprite(pw::framebuffer::Framebuffer& fb,
+ int x,
+ int y,
+ pw::draw::SpriteSheet* sprite_sheet,
+ int integer_scale);
+
+ void DrawTestPattern();
+
+ pw::math::Size<int> DrawCharacter(int ch,
+ pw::math::Vector2<int> pos,
+ pw::color::color_rgb565_t fg_color,
+ pw::color::color_rgb565_t bg_color,
+ const FontSet& font,
+ pw::framebuffer::Framebuffer& framebuffer);
+
+ pw::math::Size<int> DrawString(std::wstring_view str,
+ pw::math::Vector2<int> pos,
+ pw::color::color_rgb565_t fg_color,
+ pw::color::color_rgb565_t bg_color,
+ const FontSet& font,
+ pw::framebuffer::Framebuffer& framebuffer);
+
+ } // namespace pw::draw
+
+Class Interaction Diagram
+-------------------------
+
+.. mermaid::
+ :alt: Framebuffer Classes
+ :align: center
+
+ classDiagram
+ class FramebufferPool {
+ uint16_t row_bytes()
+ PixelFormat pixel_format()
+ dimensions() : Size~uint16_t~
+ row_bytes() : uint16_t
+ GetFramebuffer() : Framebuffer
+
+ BufferArray buffer_addresses_
+ Size~uint16_t~ buffer_dimensions_
+ uint16_t row_bytes_
+ PixelFormat pixel_format_
+ }
+
+ class Framebuffer {
+ is_valid() : bool const
+ data() : void* const
+ pixel_format() : PixelFormat const
+ size() : Size~uint16_t~ const
+ row_bytes() uint16_t const
+
+ void* pixel_data_
+ Size~uint16_t~ size_
+ PixelFormat pixel_format_
+ uint16_t row_bytes_
+ }
+
+ class DisplayDriver {
+ <<DisplayDriver>>
+ Init() : Status
+ WriteFramebuffer(Framebuffer fb, WriteCallback cb): void
+ dimensions() : Size~uint16_t~
+
+ PixelPusher& pixel_pusher_
+ }
+
+ class Display {
+ DisplayDriver& display_driver_
+ const Size~uint16_t~ size_
+ FramebufferPool& framebuffer_pool_
+
+ GetFramebuffer() : Framebuffer
+ }
+
+ class PixelPusher {
+ Init() : Status
+ WriteFramebuffer(Framebuffer fb, WriteCallback cb) : void
+ }
+
+ <<interface>> DisplayDriver
+ FramebufferPool --> "FRAMEBUFFER_COUNT" Framebuffer : buffer_addresses_
+
+ Display --> "1" DisplayDriver : display_driver_
+ Display --> "1" FramebufferPool : framebuffer_pool_
+ DisplayDriver --> "1" PixelPusher : pixel_pusher_
+
+---------------------
+Problem investigation
+---------------------
+With no direct display support in Pigweed and no example programs implementing
+a solution Pigweed developers are essentially on their own. Depending on their
+hardware this means starting with a GitHub project with a sample application
+from MCUXpresso or STMCube. Each of these use a specific HAL and may be
+coupled to other frameworks, such as FreeRTOS. This places the burden of
+substituting the HAL calls with the Pigweed API, making the sample program
+with the application screen choice, etc.
+
+This chore is time consuming and often requires that the application developer
+acquire some level of driver expertise. Having direct display support in
+Pigweed will allow the developer to more quickly add display support.
+
+The primary use-case being targeted is an application with a single display
+using multiple framebuffers with display update notifications delivered during
+an interrupt. The initial implementation is designed to support multiple
+heterogenous displays, but that will not be the focus of development or testing
+for the first release.
+
+Touch sensors, or other input devices, are not part of this effort. Display
+and touch input often accompany each other, but to simplify this already large
+display effort, touch will be added in a separate activity.
+
+There are many other embedded libraries for embedded drawing. Popular libraries
+are LVGL, emWin, GUIslice, HAGL, µGFX, and VGLite (to just name a few). These
+existing solutions generally offer one or more of: display drivers, drawing
+library, widget library. The display drivers usually rely on an abstraction
+layer, which they often refer to as a HAL, to interface with the underlying
+hardware API. This HAL may rely on macros, or sometimes a structure with
+function pointers for specific operations.
+
+The approach in this SEED was selected because it offers a low level API focused
+on display update performance. It offers no drawing or GUI library, but should
+be easily interfaced with those libraries.
+
+---------------
+Detailed design
+---------------
+
+This proposal suggests no changes to existing APIs. All changes introduce new
+modules that leverage the existing API. It supports static allocation of the
+pixel buffers and all display framework objects. Additionally pixel buffers
+may be hard-coded addresses or dynamically allocated from SRAM.
+
+The ``Framebuffer`` class is intended to simplify code that interacts with the
+pixel buffer. It includes the pixel buffer format, dimensions, and the buffer
+address. The framebuffer is 16 bytes in size (14 when packed). Framebuffer
+objects are created when requested and moved as a means of signifying ownership.
+In other words, whenever code has an actual framebuffer object it is allowed
+to both write to and read from the pixel buffer.
+
+The ``FramebufferPool`` is an object intended to simplify the management of a
+collection of framebuffers. It tracks those that are available for use and
+loans out framebuffers when requested. For single display devices this is
+generally not a difficult task as the application would maintain an array of
+framebuffers and a next available index. In this case framebuffers are always
+used in order and the buffer collection is implemented as a queue.
+
+Because RAM is often limited, the framebuffer pool is designed to be shared
+between multiple displays. Because display rendering and update may be at
+different speeds framebuffers do not need to be retrieved
+(via ``GetFramebuffer()``) and returned (via ``ReleaseFramebuffer()``) in the same
+order.
+
+Whenever possible asynchronous display updates will be used. Depending on the
+implementation this usually offloads the CPU from the pixel writing to the
+display controller. In this case the CPU will initiate the update and using
+some type of notification, usually an interrupt raised by a GPIO pin connected
+to the display, will be notified of the completion of the display update.
+Because of this the framebuffer pool ``ReleaseFramebuffer()`` call is interrupt
+safe.
+
+``FramebufferPool::GetFramebuffer()`` will block indefinitely if no framebuffer
+is available. This unburdens the application drawing loop from the task of
+managing framebuffers or tracking screen update completion.
+
+Testing
+-------
+All classes will be accompanied by a robust set of unit tests. These can be
+run on the host or the device. Test applications will be able to run on a
+workstation (i.e. not an MCU) in order to enable tests that depend on
+hardware available in most CPUs - like an MMU. This will enable the use of
+`AddressSanitizer <https://github.com/google/sanitizers/wiki/AddressSanitizer>`_
+based tests. Desktop tests will use
+`Xvfb <https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml>`_ to allow
+them to be run in a headless continuous integration environment.
+
+Performance
+-----------
+Display support will include performance tests. Although this proposal does not
+include a rendering library, it will include support for specific platforms
+that will utilize means of transferring pixel data to the display controller
+in the background.
+
+------------
+Alternatives
+------------
+
+One alternative is to create the necessary port/HAL, the terminology varies by
+library, for the popular embedded graphics libraries. This would make it easier
+for Pigweed applications to add display support - bot only for those supported
+libraries. This effort is intended to be more focused on performance, which is
+not always the focus of other libraries.
+
+Another alternative is to do nothing - leaving the job of adding display
+support to the developers. As a significant percentage of embedded projects
+contain a display, it will beneficial to have built-in display support in
+Pigweed. This will allow all user to benefit by the shared display expertise,
+continuous integration, testing, and performance testing.
+
+--------------
+Open questions
+--------------
+
+Parameter Configuration
+-----------------------
+One open question is what parameters to specify in initialization parameters
+to a driver ``Init()`` function, which to set in build flags via ``config(...)``
+in GN, and which to hard-code into the driver. The most ideal, from the
+perspective of reducing binary size, is to hard-code all values in a single
+block of contiguous data. The decision to support multiple displays requires
+that the display initialization parameters, at least some of them, be defined
+at runtime and cannot be hard-coded into the driver code - that is, if the
+goal is to allow two of the same display to be in use with different settings.
+
+Additionally many drivers support dozens of configuration values. The ILI9341
+has 82 different commands, some with complex values like gamma tables or
+multiple values packed into a single register.
+
+The current approach is to strike a balance where the most commonly set
+values, for example display width/height and pixel format, are configurable
+via build flags, and the remainder is hard-coded in the driver. If a developer
+wants to set a parameter that is currently hard-coded in the driver, for
+example display refresh rate or gamma table, they would need to copy the display
+driver from Pigweed, or create a Pigweed branch.
+
+``Display::WriteFramebuffer()`` always writes the full framebuffer. It is expected
+that partial updates will be supported. This will likely come as a separate
+function. This is being pushed off until needed to provide as much experience
+with the various display controller APIs as possible to increase the likelihood
+of a well crafted API.
+
+Module Hierarchy
+----------------
+At present Pigweed's module structure is flat and at the project root level.
+There are currently 134 top level ``pw_*`` directories. This proposal could
+significantly increase this count as each new display driver will be a new
+module. This might be a good time to consider putting modules into a hierarchy.
+
+Pixel Pusher
+------------
+``PixelPusher`` was created to remove the details of writing pixels from the
+display driver. Many displays support multiple ways to send pixel data. For
+example the ILI9341 supports SPI and a parallel bus for pixel transport.
+The `STM32F429I-DISC1 <https://www.st.com/en/evaluation-tools/32f429idiscovery.html>`_
+also has a display controller (`LTDC <https://www.st.com/resource/en/application_note/an4861-lcdtft-display-controller-ltdc-on-stm32-mcus-stmicroelectronics.pdf>`_)
+which uses an STM proprietary API. The ``PixelPusher`` was essentially created
+to allow that driver to use the LTDC API without the need to be coupled in any
+way to a vendor API.
+
+At present some display drivers use ``pw_spi`` to send commands to the display
+controller, and the ``PixelPusher`` for writing pixel data. It will probably
+be cleaner to move the command writes into the ``PixelPusher`` and remove any
+``pw_spi`` interaction from the display drivers. At this time ``PixelPusher``
+should be renamed.
+
+Copyrighted SDKs
+----------------
+Some vendors have copyrighted SDKs which cannot be included in the Pigweed
+source code unless the project is willing to have the source covered by more
+than one license. Additionally some SDKs have no simple download link and the
+vendor requires that a developer use a web application to build and download
+an SDK with the desired components. NXP's
+`MCUXpresso SDK Builder <https://mcuxpresso.nxp.com/en/welcome>`_ is an example
+of this. This download process makes it difficult to provide simple instructions
+to the developer and for creating reliable builds as it may be difficult to
+select an older SDK for download.
diff --git a/seed/0105-pw_tokenizer-pw_log-nested-tokens.rst b/seed/0105-pw_tokenizer-pw_log-nested-tokens.rst
new file mode 100644
index 000000000..a4f2966ab
--- /dev/null
+++ b/seed/0105-pw_tokenizer-pw_log-nested-tokens.rst
@@ -0,0 +1,470 @@
+.. _seed-0105:
+
+===============================================
+0105: Nested Tokens and Tokenized Log Arguments
+===============================================
+
+.. seed::
+ :number: 105
+ :name: Nested Tokens and Tokenized Log Arguments
+ :status: Accepted
+ :proposal_date: 2023-07-10
+ :cl: 154190
+
+-------
+Summary
+-------
+This SEED describes a number of extensions to the `pw_tokenizer <https://pigweed.dev/pw_tokenizer/>`_
+and `pw_log_tokenized <https://pigweed.dev/pw_log_tokenized>`_ modules to
+improve support for nesting tokens and add facilities for tokenizing arguments
+to logs such as strings or and enums. This SEED primarily addresses C/C++
+tokenization and Python/C++ detokenization.
+
+----------
+Motivation
+----------
+Currently, ``pw_tokenizer`` and ``pw_log_tokenized`` enable devices with limited
+memory to store long log format strings as hashed 32-bit tokens. When logs are
+moved off-device, host tooling can recover the full logs using token databases
+that were created when building the device image. However, logs may still have
+runtime string arguments that are stored and transferred 1:1 without additional
+encoding. This SEED aims to extend tokenization to these arguments to further
+reduce the weight of logging for embedded applications.
+
+The proposed changes affect both the tokenization module itself and the logging
+facilities built on top of tokenization.
+
+--------
+Proposal
+--------
+Logging enums such as ``pw::Status`` is one common special case where
+tokenization is particularly appropriate: enum values are conceptually
+already tokens mapping to their names, assuming no duplicate values. Logging
+enums frequently entails creating functions and string names that occupy space
+exclusively for logging purposes, which this proposal seeks to mitigate.
+Here, ``pw::Status::NotFound()`` is presented as an illustrative example of
+the several transformations that strings undergo during tokenization and
+detokenization, further complicated in the proposed design by nested tokens.
+
+.. list-table:: Enum Tokenization/Detokenization Phases
+ :widths: 20 45
+
+ * - (1) Source code
+ - ``PW_LOG("Status: " PW_LOG_ENUM_FMT(pw::Status), status.code())``
+ * - (2) Token database entries (token, string, domain)
+ - | ``16170adf, "Status: ${pw::Status}#%08x", ""``
+ | ``5 , "PW_STATUS_NOT_FOUND" , "pw::Status"``
+ * - (3) Wire format
+ - ``df 0a 17 16 0a`` (5 bytes)
+ * - (4) Top-level detokenized and formatted
+ - ``"Status: ${pw::Status}#00000005"``
+ * - (5) Fully detokenized
+ - ``"Status: PW_STATUS_NOT_FOUND"``
+
+Compared to log tokenization without nesting, string literals in token
+database entries may not be identical to what is typed in source code due
+to the use of macros and preprocessor string concatenation. The
+detokenizer also takes an additional step to recursively detokenize any
+nested tokens. In exchange for this added complexity, nested enum tokenization
+allows us to gain the readability of logging value names with zero additional
+runtime space or performance cost compared to logging the integral values
+directly with ``pw_log_tokenized``.
+
+.. note::
+ Without nested enum token support, users can select either readability or
+ reduced binary and transmission size, but not easily both:
+
+ .. list-table::
+ :widths: 15 20 20
+ :header-rows: 1
+
+ * -
+ - Raw integers
+ - String names
+ * - (1) Source code
+ - ``PW_LOG("Status: %x" , status.code())``
+ - ``PW_LOG("Status: %s" , pw_StatusString(status))``
+ * - (2) Token database entries (token, string, domain)
+ - ``03a83461, "Status: %x", ""``
+ - ``069c3ef0, "Status: %s", ""``
+ * - (3) Wire format
+ - ``61 34 a8 03 0a`` (5 bytes)
+ - ``f0 3e 9c 06 09 4e 4f 54 5f 46 4f 55 4e 44`` (14 bytes)
+ * - (4) Top-level detokenized and formatted
+ - ``"Status: 5"``
+ - ``"Status: PW_STATUS_NOT_FOUND"``
+ * - (5) Fully detokenized
+ - ``"Status: 5"``
+ - ``"Status: PW_STATUS_NOT_FOUND"``
+
+Tokenization (C/C++)
+====================
+The ``pw_log_tokenized`` module exposes a set of macros for creating and
+formatting nested tokens. Within format strings in the source code, tokens
+are specified using function-like PRI-style macros. These can be used to
+encode static information like the token domain or a numeric base encoding
+and are macro-expanded to string literals that are concatenated with the
+rest of the format string during preprocessing. Since ``pw_log`` generally
+uses printf syntax, only bases 8, 10, and 16 are supported for integer token
+arguments via ``%[odiuxX]``.
+
+The provided macros enforce the token specifier syntax and keep the argument
+types in sync when switching between other ``pw_log`` backends like
+``pw_log_basic``. These macros for basic usage are as follows:
+
+* ``PW_LOG_TOKEN`` and ``PW_LOG_TOKEN_EXPR`` are used to tokenize string args.
+* ``PW_LOG_TOKEN_FMT`` is used inside the format string to specify a token arg.
+* ``PW_LOG_TOKEN_TYPE`` is used if the type of a tokenized arg needs to be
+ referenced, e.g. as a ``ToString`` function return type.
+
+.. code-block:: cpp
+
+ #include "pw_log/log.h"
+ #include "pw_log/tokenized_args.h"
+
+ // token with default options base-16 and empty domain
+ // token database literal: "The sun will come out $#%08x!"
+ PW_LOG("The sun will come out " PW_LOG_TOKEN_FMT() "!", PW_LOG_TOKEN_EXPR("tomorrow"))
+ // after detokenization: "The sun will come out tomorrow!"
+
+Additional macros are also provided specifically for enum handling. The
+``TOKENIZE_ENUM`` macro creates ELF token database entries for each enum
+value with the specified token domain to prevent token collision between
+multiple tokenized enums. This macro is kept separate from the enum
+definition to allow things like tokenizing a preexisting enum defined in an
+external dependency.
+
+.. code-block:: cpp
+
+ // enums
+ namespace foo {
+
+ enum class Color { kRed, kGreen, kBlue };
+
+ // syntax TBD
+ TOKENIZE_ENUM(
+ foo::Color,
+ kRed,
+ kGreen,
+ kBlue
+ )
+
+ } // namespace foo
+
+ void LogColor(foo::Color color) {
+ // token database literal:
+ // "Color: [${foo::Color}10#%010d]"
+ PW_LOG("Color: [" PW_LOG_ENUM_FMT(foo::Color, 10) "]", color)
+ // after detokenization:
+ // e.g. "Color: kRed"
+ }
+
+.. admonition:: Nested Base64 tokens
+
+ ``PW_LOG_TOKEN_FMT`` can accept 64 as the base encoding for an argument, in
+ which case the argument should be a pre-encoded Base64 string argument
+ (e.g. ``QAzF39==``). However, this should be avoided when possible to
+ maximize space savings. Fully-formatted Base64 including the token prefix
+ may also be logged with ``%s`` as before.
+
+Detokenization (Python)
+=======================
+``Detokenizer.detokenize`` in Python (``Detokenizer::Detokenize`` in C++)
+will automatically recursively detokenize tokens of all known formats rather
+than requiring a separate call to ``detokenize_base64`` or similar.
+
+To support detokenizing domain-specific tokens, token databases support multiple
+domains, and ``database.py create`` will build a database with tokens from all
+domains by default. Specifying a domain during database creation will cause
+that domain to be treated as the default.
+
+When detokenization fails, tokens appear as-is in logs. If the detokenizer has
+the ``show_errors`` option set to ``True``, error messages may be printed
+inline following the raw token.
+
+Tokens
+======
+Many details described here are provided via the ``PW_LOG_TOKEN_FMT`` macro, so
+users should typically not be manually formatting tokens. However, if
+detokenization fails for any reason, tokens will appear with the following
+format in the final logs and should be easily recognizable.
+
+Nested tokens have the following structure in partially detokenized logs
+(transformation stage 4):
+
+.. code-block::
+
+ $[{DOMAIN}][BASE#]TOKEN
+
+The ``$`` is a common prefix required for all nested tokens. It is possible to
+configure a different common prefix if necessary, but using the default ``$``
+character is strongly recommended.
+
+.. list-table:: Options
+ :widths: 10 30
+
+ * - ``{DOMAIN}``
+ - Specifies the token domain. If this option is omitted, the default
+ (empty) domain is assumed.
+ * - ``BASE#``
+ - Defines the numeric base encoding of the token. Accepted values are 8,
+ 10, 16, and 64. If the hash symbol ``#`` is used without specifying a
+ number, the base is assumed to be 16. If the base option is omitted
+ entirely, the base defaults to 64 for backward compatibility. All
+ encodings except Base64 are not case sensitive.
+
+ This option may be expanded to support other bases in the future.
+ * - ``TOKEN`` (required)
+ - The numeric representation of the token in the given base encoding. All
+ encodings except Base64 are left-padded with zeroes to the maximum width
+ of a 32-bit integer in the given base. Base64 data may additionally encode
+ string arguments for the detokenized token, and therefore does not have a
+ maximum width. This is automatically handled by ``PW_LOG_TOKEN_FMT`` for
+ supported bases.
+
+When used in conjunction with ``pw_log_tokenized``, the token prefix (including
+any domain and base specifications) is tokenized as part of the log format
+string and therefore incurs zero additional memory or transmission cost over
+that of the original format string. Over the wire, tokens in bases 8, 10, and
+16 are transmitted as varint-encoded integers up to 5 bytes in size. Base64
+tokens continue to be encoded as strings.
+
+.. warning::
+ Tokens do not have a terminating character in general, which is why we
+ require them to be formatted with fixed width. Otherwise, following them
+ immediately with alphanumeric characters valid in their base encoding
+ will cause detokenization errors.
+
+.. admonition:: Recognizing raw nested tokens in strings
+
+ When a string is fully detokenized, there should no longer be any indication
+ of tokenization in the final result, e.g. detokenized logs should read the
+ same as plain string logs. However, if nested tokens cannot be detokenized for
+ any reason, they will appear in their raw form as below:
+
+ .. code-block::
+
+ // Base64 token with no arguments and empty domain
+ $QA19pfEQ
+
+ // Base-10 token
+ $10#0086025943
+
+ // Base-16 token with specified domain
+ ${foo_namespace::MyEnum}#0000001A
+
+ // Base64 token with specified domain
+ ${bar_namespace::MyEnum}QAQQQQ==
+
+
+---------------------
+Problem investigation
+---------------------
+Complex embedded device projects are perpetually seeking more RAM. For longer
+descriptive string arguments, even just a handful can take up hundreds of bytes
+that are frequently exclusively for logging purposes, without any impact on
+function.
+
+One of the most common potential use cases is for logging enum values.
+Inspection of one project revealed that enums accounted for some 90% of the
+string log arguments. We have encountered instances where, to save space,
+developers have avoided logging descriptive names in favor of raw enum values,
+forcing readers of logs look up or memorize the meanings of each number. Like
+with log format strings, we do know the set of possible string values that
+might be emitted in the final logs, so they should be able to be extracted
+into a token database at compile time.
+
+Another major challenge overall is maintaining a user interface
+that is easy to understand and use. The current primary interface through
+``pw_log`` provides printf-style formatting, which is familiar and succinct
+for basic applications.
+
+We also have to contend with the interchangeable backends of ``pw_log``. The
+``pw_log`` facade is intended as an opaque interface layer; adding syntax
+specifically for tokenized logging will break this abstraction barrier. Either
+this additional syntax would be ignored by other backends, or it might simply
+be incompatible (e.g. logging raw integer tokens instead of strings).
+
+Pigweed already supports one form of nested tokens via Base64 encoding. Base64
+tokens begin with ``'$'``, followed by Base64-encoded data, and may be padded
+with one or two trailing ``'='`` symbols. The Python
+``Detokenizer.detokenize_base64`` method recursively detokenizes Base64 by
+running a regex replacement on the formatted results of each iteration. Base64
+is not merely a token format, however; it can encode any binary data in a text
+format at the cost of reduced efficiency. Therefore, Base64 tokens may include
+not only a database token that may detokenize to a format string but also
+binary-encoded arguments. Other token types are not expected to include this
+additional argument data.
+
+---------------
+Detailed design
+---------------
+
+Tokenization
+============
+``pw_tokenizer`` and ``pw_log_tokenized`` already provide much of the necessary
+functionality to support tokenized arguments. The proposed API is fully
+backward-compatible with non-nested tokenized logging.
+
+Token arguments are indicated in log format strings via PRI-style macros that
+are exposed by a new ``pw_log/tokenized_args.h`` header. ``PW_LOG_TOKEN_FMT``
+supplies the ``$`` token prefix, brackets around the domain, the base specifier,
+and the printf-style specifier including padding and width, i.e. ``%011o`` for
+base-8, ``%010u`` for base-10, and ``%08X`` for base-16.
+
+For free-standing string arguments such as those where the literals are defined
+in the log statements themselves, tokenization is performed with macros from
+``pw_log/tokenized_args.h``. With the tokenized logging backend, these macros
+simply alias the corresponding ``PW_TOKENIZE`` macros, but they also revert to
+basic string formatting for other backends. This is achieved by placing an
+empty header file in the local ``public_overrides`` directory of
+``pw_log_tokenized`` and checking for it in ``pw_log/tokenized_args.h`` using
+the ``__has_include`` directive.
+
+For variable string arguments, the API is split across locations. The string
+literals are tokenized wherever they are defined, and the string format macros
+appear in the log format strings corresponding to those string arguments.
+
+When tokens use non-default domains, additional work may be required to create
+the domain name and store associated tokens in the ELF.
+
+Enum Tokenization
+-----------------
+We use existing ``pw_tokenizer`` utilities to record the raw enum values as
+tokens corresponding to their string names in the ELF. There is no change
+required for the backend implementation; we simply skip the token calculation
+step, since we already have a value to use, and specifying a token domain is
+generally required to isolate multiple enums from token collision.
+
+For ease of use, we can also provide a macro that wraps the enum value list
+and encapsulates the recording of each token value-string pair in the ELF.
+
+When actually logging the values, users pass the enum type name as the domain
+to format specifier macro ``PW_LOG_TOKEN()``, and the enum values can be
+passed as-is to ``PW_LOG`` (casting to integers as necessary for scoped enums).
+Since integers are varint-encoded over the wire, this will only require a
+single byte for most enums.
+
+.. admonition:: Logging pw::status
+
+ Note that while this immediately reduces transmission size, the code
+ space occupied by the string names in ``pw::Status::str()`` cannot be
+ recovered unless an entire project is converted to log ``pw::Status``
+ as tokens.
+
+ .. code:: cpp
+
+ #include "pw_log/log.h"
+ #include "pw_log/tokenized_args.h"
+ #include "pw_status/status.h"
+
+ pw::Status status = pw::Status::NotFound();
+
+ // "pw::Status: ${pw::Status}#%08d"
+ PW_LOG("pw::Status: " PW_LOG_TOKEN(pw::Status), status.code)
+ // "pw::Status: NOT_FOUND"
+
+Since the token mapping entries in the ELF are optimized out of the final
+binary, the enum domains are tokenized away as part of the log format strings,
+and we don't need to store separate tokens for each enum value, this addition
+to the API would would provide enum value names in logs with zero additional
+RAM cost. Compared to logging strings with ``ToString``-style functions, we
+save space on the string names as well as the functions themselves.
+
+Token Database
+==============
+Token databases will be expanded to include a column for domains, so that
+multiple domains can be encompassed in a single database rather than requiring
+separate databases for each domain. This is important because domains are being
+used to categorize tokens within a single project, rather than merely keeping
+separate projects distinct from each other. When creating a database
+from an ELF, a domain may be specified as the default domain instead of the
+empty domain. A list of domains or path to a file with a list of domains may
+also separately be specified to define which domains are to be included in
+the database; all domains are now included by default.
+
+When accessing a token database, both a domain and token value may be specified
+to access specific values. If a domain is not specified, the default domain
+will be assumed, retaining the same behavior as before.
+
+Detokenization
+==============
+Detokenization is relatively straightforward. When the detokenizer is called,
+it will first detokenize and format the top-level token and binary argument
+data. The detokenizer will then find and replace nested tokens in the resulting
+formatted string, then rescan the result for more nested tokens up to a fixed
+number of rescans.
+
+For each token type or format, ``pw_tokenizer`` defines a regular expression to
+match the expected formatted output token and a helper function to convert a
+token from a particular format to its mapped value. The regular expressions for
+each token type are combined into a single regex that matches any one of the
+formats. At each recursive step for every match, each detokenization format
+will be attempted, stopping at the first successful token type and then
+recursively replacing all nested tokens in the result. Only full data encoding-
+type tokens like Base64 will also require string/argument formatting as part of
+the recursive step.
+
+For non-Base64 tokens, a token's base encoding as specified by ``BASE#``
+determines its set of permissible alphanumeric characters and the
+maximum token width for regex matching.
+
+If nested detokenization fails for any reason, the formatted token will be
+printed as-is in the output logs. If ``show_errors`` is true for the
+detokenizer, errors will appear in parentheses immediately following the
+token. Supported errors include:
+
+* ``(token collision)``
+* ``(missing database)``
+* ``(token not found)``
+
+------------
+Alternatives
+------------
+
+Protobuf-based Tokenization
+===========================
+Tokenization may be expanded to function on structured data via protobufs.
+This can be used to make logging more flexible, as all manner of compile-time
+metadata can be freely attached to log arguments at effectively no cost.
+This will most likely involve a separate build process to generate and tokenize
+partially-populated protos and will significantly change the user API. It
+will also be a large break from the existing process in implementation, as
+the current system relies only on existing C preprocessor and C++ constexpr
+tricks to function.
+
+In this model, the token domain would likely be a fully-qualified
+namespace for or path to the proto definition.
+
+Implementing this approach also requires a method of passing ordered arguments
+to a partially-filled detokenized protobuf in a manner similar to printf-style
+string formatting, so that argument data can be efficiently encoded and
+transmitted alongside the protobuf's token, and the arguments to a particular
+proto can be disambiguated from arguments to the rest of a log statement.
+
+This approach will also most likely preclude plain string logging as is
+currently supported by ``pw_log``, as the implementations diverge dramatically.
+However, if pursued, this would likely be made the default logging schema
+across all platforms, including host devices.
+
+Custom Detokenization
+=====================
+Theoretically, individual projects could implement their own regex replacement
+schemes on top of Pigweed's detokenizer, allowing them to more flexibly define
+complex relationships between logged tokens via custom log format string
+syntax. However, Pigweed should provide utilities for nested tokenization in
+common cases such as logging enums.
+
+The changes proposed do not preclude additional custom detokenization schemas
+if absolutely necessary, and such practices do not appear to have been popular
+thus far in any case.
+
+--------------
+Open questions
+--------------
+Missing API definitions:
+
+* Updated APIs for creating and accessing token databases with multiple domains
+* Python nested tokenization
+* C++ nested detokenization
+
diff --git a/seed/0107-communications.rst b/seed/0107-communications.rst
new file mode 100644
index 000000000..98f38e570
--- /dev/null
+++ b/seed/0107-communications.rst
@@ -0,0 +1,640 @@
+.. _seed-0107:
+
+============================
+0107: Pigweed Communications
+============================
+.. seed::
+ :number: 107
+ :name: Communications
+ :status: Accepted
+ :proposal_date: 2023-07-19
+ :cl: 157090
+
+-------
+Summary
+-------
+Pigweed does not currently offer an end-to-end solution for network
+communications. This SEED proposes that Pigweed adopt a new sockets API as its
+primary networking abstraction. The sockets API will be backed by a new,
+lightweight embedded-focused network protocol stack, inspired by the Internet
+protocol suite (TCP/IP). It will also support full TCP/IP via an open source
+embedded TCP/IP stack or OS sockets. The new communications APIs will support
+asynchronous use and zero-copy transmission.
+
+This work is comprised of the following subareas:
+
+- `Sockets API`_
+- `Network protocol stack`_
+- `Async`_ API pattern
+- `Buffer management`_ system
+
+The Pigweed team will revisit :ref:`pw_rpc <module-pw_rpc>` after deploying the
+sockets API and network protocol stack.
+
+----------
+Background
+----------
+Pigweed's primary communications system is :ref:`pw_rpc <module-pw_rpc>`. pw_rpc
+makes it possible to call functions and exchange data with a remote system.
+Requests and responses are encoded as protobufs. pw_rpc was initially deployed
+to a system with its own network protocol stack, so the Pigweed team did not
+invest in building a network stack of its own.
+
+The TCP/IP model, as described in `RFC 1122
+<https://datatracker.ietf.org/doc/html/rfc1122>`_, organizes communications
+systems and protocols into four layers: Application, Transport, Internet (or
+Network), and Link (or Network access). Pigweed's current communications
+offerings fit into the TCP/IP model as follows:
+
++-----------------------+-----------------------------+
+| TCP/IP Model | Pigweed Modules |
++=======================+=============================+
+| Application | | :ref:`module-pw_transfer` |
+| | | :ref:`module-pw_rpc` |
++-----------------------+-----------------------------+
+| Transport | | :ref:`module-pw_router` |
++-----------------------+ | :ref:`module-pw_hdlc` |
+| Internet / Network | |
++-----------------------+ |
+| Link / Network access | |
++-----------------------+-----------------------------+
+
+Notably, Pigweed provides little functionality below the application layer. The
+pw_router and pw_hdlc modules only implement a subset of features needed at
+their layer in the communications stack.
+
+Challenges deploying pw_rpc
+===========================
+pw_rpc is application-layer communications module. It relies on a network layer
+to send packets between endpoints and doesn't provide any networking features
+itself. When initially developing pw_rpc, the Pigweed team focused its limited
+resources solely on this application-layer feature, which made it possible to
+deploy pw_rpc quickly to systems with existing networks.
+
+pw_rpc has been deployed to many projects with great results. However, since
+Pigweed does not provide a network stack, deploying pw_rpc to systems without
+existing stacks can be challenging. These systems have to develop their own
+solutions to transmit and route pw_rpc packets.
+
+As an example, one project based its network communications on Pigweed's
+:ref:`module-pw_hdlc` module. It used HDLC in a way more similar to IP,
+providing network-level addressing and features like quality-of-service. Source
+and destination addresses and ports were packed into the HDLC address field to
+facilitate routing and multiplexing. The :ref:`module-pw_router` module was
+developed to support static routing tables for HDLC frames through nodes in the
+system, and the :ref:`pw_transfer RPC service <module-pw_transfer>` was
+developed to provide reliable delivery of data.
+
+Learning from custom network stacks
+-----------------------------------
+Teams want to use Pigweed to build cool devices. Their goal isn't to build a
+network protocol stack, but they need one to use features like pw_rpc and
+pw_transfer. Given this, teams have little incentive to make the enormous time
+investment to develop a robust, reusable network stack. The practical approach
+is to assemble the minimum viable network stack from what's available.
+
+The Pigweed team has seen a few teams create custom network stacks for pw_rpc.
+While these projects were successful, their network stacks were not their
+primary focus. As a result, they had some shortcomings, including the following:
+
+- **Byte stuffing memory overhead** -- HDLC is a low-level protocol. It uses
+ `byte stuffing
+ <https://en.wikipedia.org/wiki/High-Level_Data_Link_Control#Asynchronous_framing>`_
+ to ensure frame integrity across unreliable links. Byte stuffing makes sense
+ on the wire, but not in memory. Storing byte stuffed frames requires double
+ the memory to account for worst-case byte stuffing. Some projects use HDLC
+ frames as network layer packets, so they are buffered in memory for routing,
+ which requires more memory than necessary.
+- **HDLC protocol overhead** -- HDLC's frame recovery and integrity features are
+ not needed across all links. For example, these features are unnecessary for
+ Bluetooth. However, when projects use HDLC for both the network and link
+ layers, it has to be used across all links.
+- **pw_transfer at the application layer** -- :ref:`pw_transfer
+ <module-pw_transfer>` supports reliable data transfers with :ref:`pw_rpc
+ <module-pw_rpc>`. It required significant investment to develop, but since it
+ is layered on top of pw_rpc, it has additional overhead and limited
+ reusability.
+- **Custom routing** -- Some network nodes have multiple routes between them.
+ Projects have had to write custom, non-portable logic to handle routing.
+- **pw_rpc channel IDs in routing** -- Some projects used pw_rpc channel IDs as
+ a network addresses. Channel IDs were assigned for the whole network ahead of
+ time. This has several downsides:
+
+ - Requires nodes to have knowledge of the global channel ID assignments
+ and routes between them, which can be difficult to keep in sync.
+ - Implies that all traffic is pw_rpc packets.
+ - Requires decoding pw_rpc packets at lower levels of the network stack.
+ - Complicates runtime assignment of channel IDs.
+
+- **Flow control** -- Projects' communications stacks have not supported flow
+ control. The network layer simply has to drop packets it cannot process.
+ There is no mechanism to tell the producer to slow down or wait for the
+ receiver to be ready.
+- **Accounting for the MTU** -- HDLC and pw_rpc have variable overheads, so it
+ is difficult to know how much memory to allocate for RPC payloads. If packets
+ are not sized properly with respect to the maximum transmission unit (MTU),
+ packets may be silently dropped.
+
+Problem summary
+===============
+These are the key issues of Pigweed's communications offerings based on the
+team's experiences deploying pw_rpc.
+
+**No cohesive full stack solution**
+
+Pigweed only provides a handful of communications modules. They were not
+designed to work together, and there is not enough to assemble a functioning
+network stack. Some projects have to create bespoke network protocols with
+limited reusability.
+
+**Layering violations**
+
+pw_transfer runs on top of pw_rpc instead of the transport layer, which adds
+overhead and prevents its use independent of pw_rpc. Using pw_rpc channels for
+routing ties the network to pw_rpc. Projects often use pw_hdlc for multiple
+network layers, which brings the encoding's overhead higher up the stack and
+across links that do not need it.
+
+**Inefficiency**
+
+Reliable data transfer requires pw_transfer, which runs on top of pw_rpc. This
+adds additional overhead and requires more CPU-intensive decoding operations.
+Using pw_rpc channel IDs in lower layers of the network requires expensive
+varint decodes, even when the packets are bound for other nodes.
+
+**Missing features**
+
+Each project has to develop its own version of common features, including:
+
+- **Addressing** -- There are no standard addressing schemes available to
+ Pigweed users.
+- **Routing** -- Projects must implement their own logic for routing packets,
+ which can be complex.
+- **Flow control** -- There is no way for the receiver to signal that it is ready
+ for more data or that it cannot receive any more, either at the protocol or
+ API level anywhere in the stack. Flow control is a crucial feature for
+ realistic networks with limited resources.
+- **Connections** -- Connections ensure the recipient is listening to
+ transmissions, and detect when the other end is no longer communicating.
+ pw_transfer maintains a connection, but it sits atop pw_rpc, so cannot be used
+ elsewhere.
+- **Quality of service (QoS)** -- Projects have developed basic QoS features in
+ HDLC, but there is no support in upstream Pigweed. Every project has to
+ develop its own custom implementation.
+
+-----
+Goals
+-----
+This SEED proposes a new communications system for Pigweed with the following
+goals:
+
+- **Practical end-to-end solution** -- Pigweed provides a full suite of APIs
+ and protocols that support simple and complex networking use cases.
+- **Robust, stable, and reliable** -- Pigweed communications "just work", even
+ under high load. The networking stack is thoroughly tested in both single and
+ multithreaded environments, with functional, load, fuzz, and performance
+ testing. Projects can easily test their own deployments with Pigweed tooling.
+- **Cohesive, yet modular** -- The network stack is holistically designed, but
+ modular. It is organized into layers that can be exchanged and configured
+ independently. Layering simplifies the stack, decouples protocol
+ implementations, and maximizes flexibility within a cohesive system.
+- **Efficient & performant** -- Pigweed’s network stack minimizes code size and
+ CPU usage. It provides for high throughput, low latency data transmission.
+ Memory allocation is configurable and adaptable to a project’s needs.
+- **Usable & easy to learn** -- Pigweed’s communications systems are backed by
+ thorough and up-to-date documentation. Getting started is easy using
+ Pigweed's tutorials and examples.
+
+--------
+Proposal
+--------
+Pigweed will unify its communications systems under a common sockets API. This
+entails the following:
+
+- **Sockets API** -- Pigweed will introduce a `sockets
+ API`_ to serve as its common networking interface.
+- **Lightweight protocol stack** -- Pigweed will provide a custom,
+ :ref:`lightweight network protocol stack <seed-0107-network-stack>` inspired
+ by IPv6, with UDP, TCP, and SCTP-like transport protocols.
+- **TCP/IP integration** -- Pigweed will offer sockets implementations for OS
+ sockets and an existing `embedded TCP/IP stack`_.
+- **Async** -- Pigweed will establish a new pattern for `async`_ programming and
+ use it in its networking APIs.
+- **Zero copy** -- Pigweed will develop a new `buffer management`_ system to
+ enable zero-copy networking.
+
+These features fit fit into the TCP/IP model as follows:
+
++-------------------------------------+-------------------------------------+
+| TCP/IP Model | Future Pigweed Comms Stack |
++=====================================+=====================================+
+| Application | | *Various modules including* |
+| | | *pw_rpc and pw_transfer.* |
+| | |
+| | |
+| | |
++-------------------------------------+-------------------------------------+
+| .. rst-class:: pw-text-center-align | .. rst-class:: pw-text-center-align |
+| | |
+| **OS Sockets** | **Pigweed Sockets** |
++-------------------------------------+-------------------------------------+
+| Transport | | UDP-like unreliable protocol |
+| | | TCP-like reliable protocol |
+| | | SCTP-like reliable protocol |
++-------------------------------------+-------------------------------------+
+| Network / Internet | | IPv6-like protocol |
++-------------------------------------+-------------------------------------+
+| Network access / Link | | HDLC |
+| | | others |
++-------------------------------------+-------------------------------------+
+
+Sockets API
+===========
+The new sockets API will become the primary networking abstraction in Pigweed.
+The API will support the following:
+
+- Creating sockets for bidirectional communications with other nodes in the
+ network.
+- Opening and closing connections for connection-oriented socket types.
+- Sending and receiving data, optionally :ref:`asynchronously
+ <seed-0107-async>`.
+- Reporting errors.
+
+The sockets API will support runtime polymorphism. In C++, it will be a virtual
+interface.
+
+**Rationale**
+
+A network socket represents a bidirectional communications channel with another
+node, which could be local or across the Internet. Network sockets form the API
+between an application and the network.
+
+Sockets are a proven, well-understood concept. Socket APIs such as Berkeley /
+POSIX sockets are familiar to anyone with Internet programming experience.
+
+Sockets APIs hide the details of the network protocol stack. A socket provides
+well-defined semantics for a communications channel, but applications do not
+need to know how data is sent and received. The same API can be used to exchange
+data with another process on the same machine or with a device across the world.
+
+.. admonition:: Sockets SEEDs
+
+ The Pigweed sockets API is described in SEED-0116. The sockets API is based
+ on ``pw_channel``, which is proposed in SEED-0114.
+
+Socket types
+------------
+Pigweed's sockets API will support the following sockets types.
+
+.. list-table::
+ :header-rows: 1
+
+ * - Berkeley socket type
+ - Internet protocol
+ - Payload type
+ - Connection-oriented
+ - Guaranteed, ordered delivery
+ - Description
+ * - ``SOCK_DGRAM``
+ - UDP
+ - Datagram
+ - ❌
+ - ❌
+ - Unreliable datagram
+ * - ``SOCK_STREAM``
+ - TCP
+ - Byte stream
+ - ✅
+ - ✅
+ - Reliable byte stream
+ * - ``SOCK_SEQPACKET``
+ - SCTP
+ - Datagram
+ - ✅
+ - ✅
+ - Reliable datagram
+
+Raw sockets (``SOCK_RAW``) may be supported in the future if required.
+``SOCK_CONN_DGRAM`` (unreliable connection-oriented datagram) sockets are
+uncommon and will not be supported.
+
+The socket's semantics will be expressed in the sockets API, e.g. with a
+different interface or class for each type. Instances of the connection-oriented
+socket types will be generated from a "listener" object.
+
+Pigweed's sockets API will draw inspiration from modern type safe APIs like
+Rust's `std::net sockets <https://doc.rust-lang.org/std/net/index.html>`_,
+rather than traditional APIs like POSIX sockets or Winsock. Pigweed sockets will
+map trivially to these APIs and implementations will be provided upstream.
+
+Using the sockets API
+---------------------
+The Pigweed sockets API will provide the interface between applications and the
+network. Any application can open a socket to communicate across the network.
+A future revision of ``pw_rpc`` will use the sockets API in place of its current
+``Channel`` API.
+
+The sockets API will support both synchronous and :ref:`asynchronous
+<seed-0107-async>` use. The synchronous API may be built using the async API.
+It will also support :ref:`zero-copy <seed-0107-buffers>` data transmission.
+
+Addressing
+----------
+The Pigweed sockets API will be aware of addresses. Addresses are used to refer
+to nodes in a network, including the socket's own node. With TCP/IP, the socket
+address includes an IP address and a port number.
+
+The POSIX sockets API supports different domains through address family
+constants such as ``AF_INET``, ``AF_INET6``, and ``AF_UNIX``. Addresses in these
+families are specified or accessed in various socket operations. Because the
+address format is not specified by the API, working with addresses is not type
+safe.
+
+Pigweed sockets will approach addressing differently, but details are yet to be
+determined. Possible approaches include:
+
+- Use IPv6 addresses exclusively. Systems with other addressing schemes map
+ these into IPv6 for use with Pigweed APIs.
+- Provide a polymorphic address class so sockets can work with addresses
+ generically.
+- Avoid addresses in the base sockets API. Instead, use implementation specific
+ derived classes to access addresses.
+
+Network protocol stack
+======================
+The sockets API will be backed by a network protocol stack. Pigweed will provide
+sockets implementations for following network protocol stacks:
+
+* Third party embedded TCP/IP stack, most likely `lwIP
+ <https://savannah.nongnu.org/projects/lwip/>`_.
+* Operating system TCP/IP stack via POSIX sockets or `Winsock
+ <https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-start-page-2>`_.
+* Custom :ref:`lightweight network protocol stack <seed-0107-network-stack>`.
+
+Embedded TCP/IP stack
+---------------------
+Pigweed will provide a sockets implementation for an embedded TCP/IP stack such
+as `lwIP <https://savannah.nongnu.org/projects/lwip/>`_.
+
+The sockets integration will be structured to avoid unnecessary dependencies on
+network stack features. For example, if a system is using IPv6 exclusively, the
+integration won't require IPv4 support, and the TCP/IP stack can be configured
+without it.
+
+**Rationale**
+
+The Internet protocol suite, or TCP/IP, is informed by decades of research and
+practical experience. It is much more than IP, TCP, and UDP; it's an alphabet
+soup of protocols that address a myriad of use cases and challenges.
+Implementing a functional TCP/IP stack is no small task. At time of writing,
+lwIP has about as many lines of C as Pigweed has C++ (excluding tests).
+
+The Pigweed team does not plan to implement a full TCP/IP stack. This is a major
+undertaking, and there are already established open source embedded TCP/IP
+stacks. Projects needing the full power of TCP/IP can use an embedded stack like
+`lwIP <https://savannah.nongnu.org/projects/lwip/>`_.
+
+Choosing between embedded TCP/IP and :ref:`Pigweed's stack <seed-0107-network-stack>`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+lwIP's `website <https://savannah.nongnu.org/projects/lwip/>`_ states that it
+requires tens of KB of RAM and about 40 KB of ROM. Using lwIP means using the
+same TCP/IP protocols that run the Internet. These protocols are feature rich,
+but have more overhead than is necessary for local communications within a small
+embedded system.
+
+Projects that can afford the resource requirements and protocol overhead of
+TCP/IP should use it. These projects can set up a local IPv4 or IPv6 network
+and use it for communications behind the Pigweed sockets API. Projects that
+cannot afford full TCP/IP can opt for Pigweed's :ref:`custom protocol stack
+<seed-0107-network-stack>`. Pigweed's custom stack will not have the depth of
+features and tooling of TCP/IP does, but will be sufficient for many systems.
+
+TCP/IP socket types
+^^^^^^^^^^^^^^^^^^^
+With an embedded TCP/IP stack, the Pigweed sockets API will be implemented as
+follows:
+
+- Unreliable datagram (``SOCK_DGRAM``) -- UDP
+- Reliable byte stream (``SOCK_STREAM``) -- TCP
+- Reliable datagram (``SOCK_SEQPACKET``) -- Lightweight framing over TCP. This
+ will be semantically similar to `SCTP
+ <https://datatracker.ietf.org/doc/html/rfc9260>`_, but integrations will not
+ use SCTP since it is not widely supported.
+
+.. _seed-0107-network-stack:
+
+Pigweed's custom network protocol stack
+---------------------------------------
+Pigweed will develop a custom, lightweight network protocol stack.
+
+This new protocol stack will be designed for small devices with relatively
+simple networks. It will scale to several interconnected cores that interface
+with a few external devices (e.g. over USB or Bluetooth). Depending on project
+requirements, it may or may not support dynamic network host configuration (e.g.
+DHCP or SLAAC).
+
+Pigweed's network protocol stack will be a strict subset of TCP/IP. This will
+include minimal, reduced overhead versions of UDP, TCP, and IPv6. Portions of
+other protocols such as ICMPv6 may be implemented as required.
+
+**Rationale**
+
+TCP/IP is too large and complex for some embedded systems. Systems for which
+TCP/IP is unnecessary can use Pigweed's lightweight embedded network protocol
+stack.
+
+Transport layer
+^^^^^^^^^^^^^^^
+Pigweed will provide transport layer protocols that implement the semantics of
+``SOCK_DGRAM``, ``SOCK_STREAM``, and ``SOCK_SEQPACKET``-like sockets.
+
+- ``SOCK_DRAM``-like sockets will be backed by a UDP-like protocol. This will
+ add source and destination ports to the IP-style packets for multiplexing on
+ top of the network layer.
+- ``SOCK_STREAM``-like sockets will be backed by a TCP-like protocol that uses
+ network layer packets to implement a reliable byte stream. It will be based on
+ TCP, but will not implement all of its features. The :ref:`module-pw_transfer`
+ module may serve as a starting point for the new protocol implementation.
+- ``SOCK_SEQPACKET``-like sockets will be implemented with a simple
+ message-oriented protocol on top of the TCP-like protocol. Applications like
+ pw_rpc will use ``SOCK_SEQPACKET`` sockets.
+
+Network layer
+^^^^^^^^^^^^^
+Pigweed will create a new network-layer protocol closely based on IPv6. Details
+are still to be determined, but the protocol is intended to be a strict subset
+of IPv6 and related protocols (e.g. ICMP, NDP) as needed. If a need arises, it
+is met by implementing the associated IP suite protocol. Packets will use
+compressed version of an IPv6 header (e.g. omit fields, use smaller addresses).
+
+This protocol will provide:
+
+- Unreliable packet delivery between source and destination.
+- Routing based on the source and destination addresses.
+- Quality of service (e.g. via the traffic class field).
+
+Packets may be routed at this layer independently of the link layer. Wire format
+details stay on the wire.
+
+Network access / link layer
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Pigweed's network stack will interact with the link layer through a generic
+interface. This will allow Pigweed to send network packets with any protocol
+over any physical interface.
+
+Pigweed already provides minimal support for one link layer protocol, HDLC.
+Other protocols (e.g. COBS, PPP) may be implemented. Some hardware interfaces
+(e.g. Bluetooth, USB) may not require an additional link-layer protocol.
+
+Language support
+----------------
+Pigweed today is primarily C++, but it supports Rust, C, Python, TypeScript, and
+Java to varying extents.
+
+Pigweed’s communications stack will be developed in either C++ or Rust to start,
+but it will be ported to all supported languages in time. The stack may have C
+APIs to facilitate interoperability between C++ and Rust.
+
+.. admonition:: Network protocol stack SEED
+
+ Pigweed's network protocol stack will be explored in an upcoming SEED.
+
+.. _seed-0107-async:
+
+Async
+=====
+Pigweed will develop a model for asynchronous programming and use it in its
+networking APIs, including sockets. Sockets will also support synchronous
+operations, but these may be implemented in terms of the asynchronous API.
+
+The Pigweed async model has not been designed yet. The :ref:`pw_async
+<module-pw_async>` module has a task dispatcher, but the pattern for async APIs
+has not been established. Further exploration is needed, but C++20 coroutines
+may be used for Pigweed async APIs where supported.
+
+**Rationale**
+
+Synchronous APIs require the thread to block while an operation completes. The
+thread and its resources cannot be used by the system until the task completes.
+Async APIs allow a single thread to handle multiple simultaneous tasks. The
+thread advances tasks until they need to wait for an external operation to
+complete, then switches to another task to avoid blocking.
+
+Threads are expensive in embedded systems. Each thread requires significant
+memory for its stack and kernel structures for bookkeeping. They occupy this
+memory all the time, even when they are not running. Furthermore, context
+switches between threads can take significant CPU time.
+
+Asynchronous programming avoids these downsides. Many asynchronous threads run
+on a single thread. Fewer threads are needed, and the resources of one thread
+are shared by multiple tasks. Since asynchronous systems run within one thread,
+no thread context switches occur.
+
+Networking involves many asynchronous tasks. For example, waiting for data to be
+sent through a network interface, for a connection request, or for data to
+arrive on one or more interfaces are all operations that benefit from
+asynchronous APIs. Network protocols themselves are heavily asynchronous.
+
+.. admonition:: Async SEED
+
+ Pigweed's async pattern is proposed in :ref:`SEED-0112 <seed-0112>`.
+
+.. _seed-0107-buffers:
+
+Buffer management
+=================
+Pigweed's networking APIs will support zero-copy data transmission. Applications
+will be able to request a buffer from a socket. When one is available, they fill
+it with data for transmission.
+
+Pigweed will develop a general purpose module for allocating and managing
+buffers. This will be used to implement zero-copy features for Pigweed's
+networking stack.
+
+As an example, zero-copy buffer allocation could work as follows:
+
+- The user requests a buffer from a socket.
+- The network protocol layer under the socket requests a buffer from the next
+ lower layer.
+- The bottom protocol layer allocates a buffer.
+- Each layer reserves part of the buffer for its headers or footers.
+- The remaining buffer is provided to the user to populate with their payload.
+- When the user is done, the buffer is released. Each layer of the network stack
+ processes the buffer as necessary.
+- Finally, at the lowest layer, the final buffer is sent over the hardware
+ interface.
+
+Zero-copy APIs will be :ref:`asynchronous <seed-0107-async>`.
+
+**Rationale**
+
+Networking involves transmitting large amounts of data. Copying network traffic
+can result in substantial CPU usage, particularly in nodes that route traffic to
+other nodes.
+
+A buffer management system that minimizes copying saves precious CPU cycles and
+power on constrained systems.
+
+.. admonition:: Buffer management SEED
+
+ Pigweed's buffer management system is proposed in :ref:`SEED-0109
+ <seed-0109>`.
+
+Vectored I/O
+------------
+Vectored or scatter/gather I/O allows users to serialize data from multiple
+buffers into a single output stream, or vice versa. For Pigweed's networking
+APIs, this could be used to, for example, store a packet header in one buffer
+and packet contents in one or more other buffers. These isolated chunks are
+serialized in order to the network interface.
+
+Vectored I/O minimizes copying, but is complex. Additionally, simple DMA engines
+may only operate on a single DMA buffer. Thus, vectored I/O could require
+either:
+
+- a copy into the DMA engine's buffer, which costs CPU time and memory, or
+- multiple, small DMAs, which involves extra interrupts and CPU time.
+
+Vectored I/O may be supported in Pigweed's communications stack, depending on
+project requirements.
+
+----------
+Next steps
+----------
+Pigweed's communications revamp will proceed loosely as follows:
+
+* Write SEEDs to explore existing solutions, distill requirements, and propose
+ new Pigweed features for these areas:
+
+ - Sockets API (SEED-0116)
+ - Async pattern (:ref:`SEED-0112 <seed-0112>`).
+ - Buffer management (:ref:`SEED-0109 <seed-0109>`)
+ - Network protocol stack
+
+* Implement the Sockets API.
+
+ - Document, integrate, and deploy the async programming pattern for Pigweed.
+ - Develop and test Pigweed's buffer management system.
+ - Use these features in the sockets API. If necessary, the synchronous,
+ copying API could be implemented first.
+
+* Deploy the sockets API for TCP/IP.
+
+ - Implement and unit test sockets for TCP/IP with POSIX and Winsock sockets.
+ - Implement and unit test sockets for an embedded TCP/IP stack.
+
+* Develop a test suite for Pigweed network communications.
+
+ - Create integration tests for networks with multiple nodes that cover basic
+ operation, high load, and packet loss.
+ - Write performance tests against the sockets API to measure network stack
+ performance.
+
+* Develop Pigweed's lightweight network protocol stack.
+
+ - Test the lightweight network protocol stack on hardware and in a simulated
+ environment.
+ - Write fuzz tests for the protocol stack.
+ - Write performance tests for the protocol stack.
+
+* Revisit other communications systems, including pw_rpc and pw_transfer.
diff --git a/seed/0108-pw_emu-emulators-frontend.rst b/seed/0108-pw_emu-emulators-frontend.rst
new file mode 100644
index 000000000..187410c89
--- /dev/null
+++ b/seed/0108-pw_emu-emulators-frontend.rst
@@ -0,0 +1,685 @@
+.. role:: python(code)
+ :language: python
+ :class: highlight
+
+.. _seed-0108:
+
+========================
+0108: Emulators Frontend
+========================
+.. seed::
+ :number: 108
+ :name: Emulators Frontend
+ :status: Accepted
+ :proposal_date: 2023-06-24
+ :cl: 158190
+
+-------
+Summary
+-------
+This SEED proposes a new Pigweed module that allows users to define emulator
+targets, start, control and interact with multiple running emulator instances,
+either through a command line interface or programmatically through Python APIs.
+
+-----------
+Definitions
+-----------
+An **emulator** is a program that allows users to run unmodified images compiled
+for :ref:`target <docs-targets>` on the host machine. The **host** is the machine that
+runs the Pigweed environment.
+
+An emulated **machine** or **board** is an emulator abstraction that typically
+has a correspondence in the real world - a product, an evaluation board, a
+hardware platform.
+
+An emulated machine can be extended / tweaked through runtime configuration
+options: add sensors on an i2c bus, connect a disk drive to a disk controller,
+etc.
+
+An emulator may use an **object model**, a hierarchical arrangement of emulator
+**objects** which are emulated devices (e.g. SPI controller) or internal
+emulator structures.
+
+An object can be accessed through an **object path** and can have
+**properties**. Device properties controls how the device is emulated
+(e.g. enables or disables certain features, defines memory sizes, etc.).
+
+A **channel** is a communication abstraction between the emulator and host
+processes. Examples of channels that an emulator can expose to the host:
+
+* An emulated UART could be exposed on the host as a `PTY
+ <https://en.wikipedia.org/wiki/Pseudoterminal>`_ or a socket.
+
+* A flash device could be exposed on the host as file.
+
+* A network device could be exposed on the host as a tun/tap interface.
+
+* A remote gdb interface could be exposed to the host as socket.
+
+A **monitor** is a control channel that allows the user to interactively or
+programmatically control the emulator: pause execution, inspect the emulator
+internal state, connect new devices, etc.
+
+----------
+Motivation
+----------
+While it is already possible to use emulators directly, there is a significant
+learning curve for using a specific emulator. Even for the same emulator each
+emulated machine (board) has its own peculiarities and it often requires tweaks
+to customize it to a specific project's needs through command line options or
+scripts (either native emulator scripts, if supported, or through helper shell
+scripts).
+
+Once started, the user is responsible for managing the emulator life-cycle,
+potentially for multiple instances. They also have to interact with it through
+various channels (monitor, debugger, serial ports) that requires some level of
+host resource management. Especially in the case of using multiple emulator
+instances manually managing host resources are burdensome.
+
+A frequent example is the default debugger ``localhost:1234`` port that can
+conflict with multiple emulator instances or with other debuggers running on the
+host. Another example: serials exposed over PTY have the pts number in
+``/dev/pts/`` allocated dynamically and it requires the user to retrieve it
+somehow.
+
+This gets even more complex when using different operating systems where some
+type of host resources are not available (e.g. no PTYs on Windows) or with
+limited functionality (e.g. UNIX sockets are supported on Windows > Win10 but
+only for stream sockets and there is no Python support available yet).
+
+Using emulators in CI is also difficult, in part because host resource
+management is getting more complicated due scaling (e.g. more chances of TCP
+port conflicts) and restrictions in the execution environment. But also because
+of a lack of high level APIs to control emulators and access their channels.
+
+--------
+Proposal
+--------
+Add a new Pigweed module that:
+
+* Allows users to define emulation :ref:`targets <docs-targets>` that
+ encapsulate the emulated machine configuration, the tools configuration and
+ the host channels configuration.
+
+* Provides a command line interface that manages multiple emulator instances and
+ provides interactive access to the emulator's host channels.
+
+* Provides a Python API to control emulator instances and access the emulator's
+ host channels.
+
+* Supports multiple emulators, QEMU and renode as a starting point.
+
+* Expose channels for gdb, monitor and user selected devices through
+ configurable host resources, sockets and PTYs as a starting point.
+
+The following sections will add more details about the configuration, the
+command line interface, the API for controlling and accessing emulators and the
+API for adding support for more emulators.
+
+
+Configuration
+=============
+The emulators configuration is part of the Pigweed root configuration file
+(``pigweed.json``) and reside in the ``pw:pw_emu`` namespace.
+
+Projects can define emulation targets in the Pigweed root configuration file and
+can also import predefined targets from other files. The pw_emu module provides
+a set of targets as examples and to promote reusability.
+
+For example, the following top level ``pigweed.json`` configuration includes a
+target fragment from the ``pw_emu/qemu-lm3s6965evb.json`` file:
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_emu": {
+ "target_files": [
+ "pw_emu/qemu-lm3s6965evb.json"
+ ]
+ }
+ }
+ }
+
+
+``pw_emu/qemu-lm3s6965evb.json`` defines the ``qemu-lm3s6965evb`` target
+that uses qemu as the emulator and lm3s6965evb as the machine, with the
+``serial0`` chardev exposed as ``serial0``:
+
+.. code-block::
+
+ {
+ "targets": {
+ "qemu-lm3s6965evb": {
+ "gdb": "arm-none-eabi-gdb",
+ "qemu": {
+ "executable": "qemu-system-arm",
+ "machine": "lm3s6965evb",
+ "channels": {
+ "chardevs": {
+ "serial0": {
+ "id": "serial0"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+This target emulates a stm32f405 SoC and is compatible with the
+:ref:`target-lm3s6965evb-qemu` Pigweed build target.
+
+The configuration defines a ``serial0`` channel to be the QEMU **chardev** with
+the ``serial0`` id. The default type of the channel is used, which is TCP and
+which is supported by all platforms. The user can change the type by adding a
+``type`` key set to the desired type (e.g. ``pty``).
+
+The following configuration fragment defines a target that uses renode:
+
+.. code-block::
+
+ {
+ "targets": {
+ "renode-stm32f4_discovery": {
+ "gdb": "arm-none-eabi-gdb",
+ "renode": {
+ "executable": "renode",
+ "machine": "platforms/boards/stm32f4_discovery-kit.repl",
+ "channels": {
+ "terminals": {
+ "serial0": {
+ "device-path": "sysbus.uart0",
+ "type": "pty"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+Note that ``machine`` is used to identify which renode script to use to load the
+plaform description from and ``terminals`` to define which UART devices to
+expose to the host. Also note the ``serial0`` channel is set to be exposed as a
+PTY on the host.
+
+The following channel types are currently supported:
+
+* ``pty``: supported on Mac and Linux; renode only supports PTYs for
+ ``terminals`` channels.
+
+* ``tcp``: supported on all platforms and for all channels; it is also the
+ default type if no channel type is configured.
+
+The channel configuration can be set at multiple levels: emulator, target, or
+specific channel. The channel configuration takes precedence, then the target
+channel configuration then the emulator channel configuration.
+
+The following expressions are replaced in the configuration strings:
+
+* ``$pw_emu_wdir{relative-path}``: replaces statement with an absolute path by
+ concatenating the emulator's working directory with the given relative path.
+
+* ``$pw_emu_channel_port{channel-name}``: replaces the statement with the port
+ number for the given channel name; the channel type should be ``tcp``.
+
+* ``$pw_emu_channel_host{channel-name}``: replaces the statement with the host
+ for the given channel name; the channel type should be ``tcp``.
+
+* ``$pw_emu_channel_path{channel-name}``: replaces the statement with the path
+ for the given channel name; the channel type should be ``pty``.
+
+Besides running QEMU and renode as the main emulator, the target configuration
+allows users to start other programs before or after starting the main emulator
+process. This allows extending the emulated target with simulation or emulation
+outside of the main emulator. For example, for BLE emulation the main emulator
+could emulate just the serial port while the HCI emulation done is in an
+external program (e.g. `bumble <https://google.github.io/bumble>`_, `netsim
+<https://android.googlesource.com/platform/tools/netsim>`_).
+
+
+Command line interface
+======================
+The command line interfaces enables users to control emulator instances and
+access their channels interactively.
+
+.. code-block:: text
+
+ usage: pw emu [-h] [-i STRING] [-w WDIR] {command} ...
+
+ Pigweed Emulators Frontend
+
+ start Launch the emulator and start executing, unless pause
+ is set.
+ restart Restart the emulator and start executing, unless pause
+ is set.
+ run Start the emulator and connect the terminal to a
+ channel. Stop the emulator when exiting the terminal
+ stop Stop the emulator
+ load Load an executable image via gdb. If pause is not set
+ start executing it.
+ reset Perform a software reset.
+ gdb Start a gdb interactive session
+ prop-ls List emulator object properties.
+ prop-get Show the emulator's object properties.
+ prop-set Show emulator's object properties.
+ gdb-cmds Run gdb commands in batch mode.
+ term Connect with an interactive terminal to an emulator
+ channel
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -i STRING, --instance STRING
+ instance to use (default: default)
+ -w WDIR, --wdir WDIR path to working directory (default: None)
+
+ commands usage:
+ usage: pw emu start [-h] [--file FILE] [--runner {None,qemu,renode}]
+ [--args ARGS] [--pause] [--debug] [--foreground]
+ {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2}
+ usage: pw emu restart [-h] [--file FILE] [--runner {None,qemu,renode}]
+ [--args ARGS] [--pause] [--debug] [--foreground]
+ {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2}
+ usage: pw emu stop [-h]
+ usage: pw emu run [-h] [--args ARGS] [--channel CHANNEL]
+ {qemu-lm3s6965evb,qemu-stm32vldiscovery,qemu-netduinoplus2} FILE
+ usage: pw emu load [-h] [--pause] FILE
+ usage: pw emu reset [-h]
+ usage: pw emu gdb [-h] [--executable FILE]
+ usage: pw emu prop-ls [-h] path
+ usage: pw emu prop-get [-h] path property
+ usage: pw emu prop-set [-h] path property value
+ usage: pw emu gdb-cmds [-h] [--pause] [--executable FILE] gdb-command [gdb-command ...]
+ usage: pw emu term [-h] channel
+
+For example, the ``run`` command is useful for quickly running ELF binaries on an
+emulated target and seeing / interacting with a serial channel. It starts an
+emulator, loads an images, connects to a channel and starts executing.
+
+.. code-block::
+
+ $ pw emu run qemu-netduinoplus2 out/stm32f429i_disc1_debug/obj/pw_snapshot/test/cpp_compile_test.elf
+
+ --- Miniterm on serial0 ---
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
+ INF [==========] Running all tests.
+ INF [ RUN ] Status.CompileTest
+ INF [ OK ] Status.CompileTest
+ INF [==========] Done running all tests.
+ INF [ PASSED ] 1 test(s).
+ --- exit ---
+
+Multiple emulator instances can be run and each emulator instance is identified
+by its working directory. The default working directory for ``pw emu`` is
+``$PW_PROJECT_ROOT/.pw_emu/<instance-id>`` where ``<instance-id>`` is a command
+line option that defaults to ``default``.
+
+For more complex usage patterns, the ``start`` command can be used which will
+launch an emulator instance in the background. Then, the user can debug the
+image with the ``gdb`` command, connect to a channel (e.g. serial port) with the
+``term`` command, reset the emulator with the ``reset`` command, inspect or
+change emulator properties with the ``prop-ls``, ``prop-get``, ``prop-set`` and
+finally stop the emulator instance with ``stop``.
+
+
+Python APIs
+===========
+The pw_emu module offers Python APIs to launch, control and interact with an
+emulator instance.
+
+The following is an example of using these APIs which implements a simplified
+version of the ``run`` pw_emu CLI command:
+
+.. code-block:: python
+
+ # start an emulator instance and load the image to execute
+ # pause the emulator after loading the image
+ emu = Emulator(args.wdir)
+ emu.start(args.target, args.file, pause=True)
+
+ # determine the channel type and create a pyserial compatible URL
+ chan_type = emu.get_channel_type(args.chan)
+ if chan_type == 'tcp':
+ host, port = emu.get_channel_addr(chan)
+ url = f'socket://{host}:{port}'
+ elif chan_type == 'pty':
+ url = emu.get_channel_path(chan)
+ else:
+ raise Error(f'unknown channel type `{chan_type}`')
+
+ # open the serial port and create a miniterm instance
+ serial = serial_for_url(url)
+ serial.timeout = 1
+ miniterm = Miniterm(serial)
+ miniterm.raw = True
+ miniterm.set_tx_encoding('UTF-8')
+ miniterm.set_rx_encoding('UTF-8')
+
+ # now that we are connected to the channel we can unpause
+ # this approach will prevent and data loses
+ emu.cont()
+
+ miniterm.start()
+ try:
+ miniterm.join(True)
+ except KeyBoardInterrupt:
+ pass
+ miniterm.stop()
+ miniterm.join()
+ miniterm.close()
+
+For convenience, a ``TemporaryEmulator`` class is also provided.
+
+It manages emulator instances that run in temporary working directories. The
+emulator instance is stopped and the working directory is cleared when the with
+block completes.
+
+It also supports interoperability with the pw emu cli, i.e. starting the
+emulator with the CLI and controlling / interacting with it from the API.
+
+Usage example:
+
+.. code-block:: python
+
+ # programmatically start and load an executable then access it
+ with TemporaryEmulator() as emu:
+ emu.start(target, file)
+ with emu.get_channel_stream(chan) as stream:
+ ...
+
+
+ # or start it form the command line then access it programmatically
+ with TemporaryEmulator() as emu:
+ build.bazel(
+ ctx,
+ "run",
+ exec_path,
+ "--run_under=pw emu start <target> --file "
+ )
+
+ with emu.get_channel_stream(chan) as stream:
+ ...
+
+
+Intended API shape
+------------------
+This is not an API reference, just an example of the probable shape of the final
+API.
+
+:python:`class Emulator` is used to launch, control and interact with an
+emulator instance:
+
+.. code-block:: python
+
+ def start(
+ self,
+ target: str,
+ file: Optional[os.PathLike] = None,
+ pause: bool = False,
+ debug: bool = False,
+ foreground: bool = False,
+ args: Optional[str] = None,
+ ) -> None:
+
+|nbsp|
+ Start the emulator for the given target.
+
+ If file is set that the emulator will load the file before starting.
+
+ If pause is True the emulator is paused until the debugger is connected.
+
+ If debug is True the emulator is run in foreground with debug output
+ enabled. This is useful for seeing errors, traces, etc.
+
+ If foreground is True the emulator is run in foreground otherwise it is
+ started in daemon mode. This is useful when there is another process
+ controlling the emulator's life cycle (e.g. cuttlefish)
+
+ args are passed directly to the emulator
+
+:python:`def running(self) -> bool:`
+ Check if the main emulator process is already running.
+
+:python:`def stop(self) -> None`
+ Stop the emulator
+
+:python:`def get_gdb_remote(self) -> str:`
+ Return a string that can be passed to the target remote gdb command.
+
+:python:`def get_gdb(self) -> Optional[str]:`
+ Returns the gdb command for current target.
+
+.. code-block:: python
+
+ def run_gdb_cmds(
+ commands : List[str],
+ executable: Optional[Path] = None,
+ pause: bool = False
+ ) -> subprocess.CompletedProcess:
+
+|nbsp|
+ Connect to the target and run the given commands silently
+ in batch mode.
+
+ The executable is optional but it may be required by some gdb
+ commands.
+
+ If pause is set do not continue execution after running the
+ given commands.
+
+:python:`def reset() -> None`
+ Performs a software reset
+
+:python:`def list_properties(self, path: str) -> List[Dict]`
+ Returns the property list for an emulator object.
+
+ The object is identified by a full path. The path is target specific and
+ the format of the path is emulator specific.
+
+ QEMU path example: /machine/unattached/device[10]
+
+ renode path example: sysbus.uart
+
+:python:`def set_property(path: str, prop: str, value: str) -> None`
+ Sets the value of an emulator's object property.
+
+:python:`def get_property(self, path: str, prop: str) -> None`
+ Returns the value of an emulator's object property.
+
+:python:`def get_channel_type(self, name: str) -> str`
+ Returns the channel type.
+
+ Currently ``pty`` or ``tcp`` are the only supported types.
+
+:python:`def get_channel_path(self, name: str) -> str:`
+ Returns the channel path. Raises InvalidChannelType if this is not a PTY
+ channel.
+
+:python:`def get_channel_addr(self, name: str) -> tuple:`
+ Returns a pair of (host, port) for the channel. Raises InvalidChannelType
+ if this is not a tcp channel.
+
+.. code-block:: python
+
+ def get_channel_stream(
+ name: str,
+ timeout: Optional[float] = None
+ ) -> io.RawIOBase:
+
+|nbsp|
+ Returns a file object for a given host exposed device.
+
+ If timeout is None than reads and writes are blocking. If timeout is zero the
+ stream is operating in non-blocking mode. Otherwise read and write will
+ timeout after the given value.
+
+:python:`def get_channels(self) -> List[str]:`
+ Returns the list of available channels.
+
+:python:`def cont(self) -> None:`
+ Resume the emulator's execution
+
+---------------------
+Problem investigation
+---------------------
+Pigweed is missing a tool for basic emulators control and as shown in the
+motivation section directly using emulators directly is difficult.
+
+While emulation is not a goal for every project, it is appealing for some due
+to the low cost and scalability. Offering a customizable emulators frontend in
+Pigweed will make this even better for downstream projects as the investment to
+get started with emulation will be lower - significantly lower for projects
+looking for basic usage.
+
+There are two main use-cases that this proposal is addressing:
+
+* Easier and robust interactive debugging and testing on emulators.
+
+* Basic APIs for controlling and accessing emulators to help with emulator
+ based testing (and trivial CI deployment - as long as the Pigweed bootstrap
+ process can run in CI).
+
+The proposal focuses on a set of fairly small number of commands and APIs in
+order to minimize complexity and gather feedback from users before adding more
+features.
+
+Since the state of emulated boards may different between emulators, to enable
+users access to more emulated targets, the goal of the module is to support
+multiple emulators from the start.
+
+Two emulators were selected for the initial implementation: QEMU and
+renode. Both run on all Pigweed currently supported hosts (Linux, Mac, Windows)
+and there is good overlap in terms of APIs to configure, start, control and
+access host exposed channels to start with the two for the initial
+implementation. These emulators also have good support for embedded targets
+(with QEMU more focused on MMU class machines and renode fully focused on
+microcontrollers) and are widely used in this space for emulation purposes.
+
+
+Prior art
+=========
+While there are several emulators frontends available, their main focus is on
+graphical interfaces (`aqemu <https://sourceforge.net/projects/aqemu/>`_,
+`GNOME Boxes <https://wiki.gnome.org/Apps/Boxes>`_,
+`QtEmu <https://gitlab.com/qtemu/gui>`_,
+`qt-virt-manager <https://f1ash.github.io/qt-virt-manager/>`_,
+`virt-manager <https://virt-manager.org/>`_) and virtualization (
+`virsh <https://www.libvirt.org/>`_,
+`guestfish <https://libguestfs.org/>`_).
+`qemu-init <https://github.com/mm1ke/qemu-init>`_ is a qemu CLI frontend but since
+it is written in bash it does not work on Windows nor is easy to retrofit it to
+add Python APIs for automation.
+
+.. inclusive-language: disable
+
+The QEMU project has a few `Python modules
+<https://github.com/qemu/qemu/tree/master/python/qemu>`_ that are used
+internally for testing and debugging QEMU. :python:`qemu.machine.QEMUMachine`
+implements a QEMU frontend that can start a QEMU process and can interact with
+it. However, it is clearly marked for internal use only, it is not published on
+pypi or with the QEMU binaries. It is also not as configurable for pw_emu's
+use-cases (e.g. does not support running the QEMU process in the background,
+does not multiple serial ports, does not support configuring how to expose the
+serial port, etc.). The :python:`qemu.qmp` module is `published on pypi
+<https://pypi.org/project/qemu.qmp/>`_ and can be potentially used by `pw_emu`
+to interact with the emulator over the QMP channel.
+
+.. inclusive-language: enable
+
+---------------
+Detailed design
+---------------
+The implementation supports QEMU and renode as emulators and works on
+Linux, Mac and Windows.
+
+Multiple instances are supported in order to enable developers who work on
+multiple downstream Pigweed projects to work unhindered and also to run
+multiple test instances in parallel on the same machine.
+
+Each instance is identified by a system absolute path that is also used to store
+state about the running instance such as pid files for running processes,
+current emulator and target, etc. This directory also contains information about
+how to access the emulator channels (e.g. socket ports, PTY paths)
+
+.. mermaid::
+
+ graph TD;
+ TemporaryEmulator & pw_emu_cli[pw emu cli] <--> Emulator
+ Emulator <--> Launcher & Connector
+ Launcher <--> Handles
+ Connector <--- Handles
+ Launcher <--> Config
+ Handles --Save--> WD --Load--> Handles
+ WD[Working Directory]
+
+The implementation uses the following classes:
+
+* :py:class:`pw_emu.Emulator`: the user visible APIs
+
+* :py:class:`pw_emu.core.Launcher`: an abstract class that starts an emulator
+ instance for a given configuration and target
+
+* :py:class:`pw_emu.core.Connector`: an abstract class that is the interface
+ between a running emulator and the user visible APIs
+
+* :py:class:`pw_emu.core.Handles`: class that stores specific information about
+ a running emulator instance such as ports to reach emulator channels; it is
+ populated by :py:class:`pw_emu.core.Launcher` and saved in the working
+ directory and used by :py:class:`pw_emu.core.Connector` to access the emulator
+ channels, process pids, etc.
+
+* :py:class:`pw_emu.core.Config`: loads the pw_emu configuration and provides
+ helper methods to get and validate configuration options
+
+
+Documentation update
+====================
+The following documentation should probably be updated to use ``pw emu`` instead
+of direct QEMU invocation: :ref:`module-pw_rust`,
+:ref:`target-lm3s6965evb-qemu`. The referenced QEMU targets are defined in
+fragment configuration files in the pw_emu module and included in the top level
+pigweed.json file.
+
+------------
+Alternatives
+------------
+UNIX sockets were investigated as an alternative to TCP for the host exposed
+channels. UNIX sockets main advantages over TCP is that it does not require
+dynamic port allocation which simplifies the bootstrap of the emulator (no need
+to query the emulator to determine which ports were allocated). Unfortunately,
+while Windows supports UNIX sockets since Win10, Python still does not support
+them on win32 platforms. renode also does not support UNIX sockets.
+
+--------------
+Open questions
+--------------
+
+Renode dynamic ports
+====================
+While renode allows passing 0 for ports to allocate a dynamic port, it does not
+have APIs to retrieve the allocated port. Until support for such a feature is
+added upstream, the following technique can be used to allocate a port
+dynamically:
+
+.. code-block:: python
+
+ sock = socket.socket(socket.SOCK_INET, socket.SOCK_STREAM)
+ sock.bind(('', 0))
+ _, port = socket.getsockname()
+ sock.close()
+
+There is a race condition that allows another program to fetch the same port,
+but it should work in most light use cases until the issue is properly resolved
+upstream.
+
+qemu_gcc target
+===============
+It should still be possible to call QEMU directly as described in
+:ref:`target-lm3s6965evb-qemu` however, once ``pw_emu`` is implemented it is
+probably better to define a lm3s6965evb emulation target and update the
+documentation to use ``pw emu`` instead of the direct QEMU invocation.
+
+
+.. |nbsp| unicode:: 0xA0
+ :trim:
diff --git a/seed/0109-comms-buffers.rst b/seed/0109-comms-buffers.rst
new file mode 100644
index 000000000..fed016259
--- /dev/null
+++ b/seed/0109-comms-buffers.rst
@@ -0,0 +1,490 @@
+.. _seed-0109:
+
+===========================
+0109: Communication Buffers
+===========================
+.. seed::
+ :number: 109
+ :name: Communication Buffers
+ :status: Accepted
+ :proposal_date: 2023-08-28
+ :cl: 168357
+
+-------
+Summary
+-------
+This SEED proposes that Pigweed adopt a standard buffer type for network-style
+communications. This buffer type will be used in the new sockets API
+(see the recently-accepted `Communications SEED 107
+<https://pigweed.dev/seed/0107-communications.html>`_ for more details), and
+will be carried throughout the communications stack as-appropriate.
+
+------------------------
+Top-Level Usage Examples
+------------------------
+
+Sending a Proto Into a Socket
+=============================
+.. code-block:: cpp
+
+ Allocator& msg_allocator = socket.Allocator();
+ size_t size = EncodedProtoSize(some_proto);
+ std::optional<pw::MultiBuf> buffer = msg_allocator.Allocate(size);
+ if (!buffer) { return; }
+ EncodeProto(some_proto, *buffer);
+ socket.Send(std::move(*buffer));
+
+``Socket`` s provide an allocator which should be used to create a
+``pw::MultiBuf``. The data to be sent is then filled into this buffer before
+being sent. ``Socket`` must accept ``pw::MultiBuf`` s which were not created
+by their own ``Allocator``, but this may incur a performance penalty.
+
+Zero-Copy Fragmentation
+=======================
+
+The example above has a hidden feature: zero-copy fragmentation! The socket
+can provide a ``pw::MultiBuf`` which is divided up into separate MTU-sized
+chunks, each of which has reserved space for headers and/or footers:
+
+.. code-block:: cpp
+
+ std::optional<pw::MultiBuf> Allocate(size_t size) {
+ if (size == 0) { return pw::MultiBuf(); }
+ size_t data_per_chunk = mtu_size_ - header_size_;
+ size_t num_chunks = 1 + ((size - 1) / data_per_chunk);
+ std::optional<pw::MultiBuf> buffer = pw::MultiBuf::WithCapacity(num_chunks);
+ if (!buffer) { return std::nullopt; }
+ for (size_t i = 0; i < num_chunks; i++) {
+ // Note: we could allocate smaller than `mtu_size_` for the final
+ // chunk. This is ommitted here for brevity.
+ std::optional<pw::MultiBuf::Chunk> chunk = internal_alloc_.Allocate(mtu_size_);
+ if (!chunk) { return std::nullopt; }
+ // Reserve the header size by discarding the front of each buffer.
+ chunk->DiscardFront(header_size_);
+ buffer->Chunks()[i] = std::move(chunk);
+ }
+ return *buffer;
+ }
+
+This code reserves ``header_size_`` bytes at the beginning of each ``Chunk``.
+When the caller writes into this memory and then passes it back to the socket,
+these bytes can be claimed for the header using the ``ClaimPrefix`` function.
+
+One usecase that seems to demand the ability to fragment like this is breaking
+up ``SOCK_SEQPACKET`` messages which (at least on Unix / Linux) may be much larger
+than the MTU size: up to ``SO_SNDBUF`` (see `this man page
+<https://man7.org/linux/man-pages/man7/socket.7.html>`_).
+
+Multiplexing Incoming Data
+==========================
+
+.. code-block:: cpp
+
+ [[nodiscard]] bool SplitAndSend(pw::MultiBuf buffer) {
+ std::optional<std::array<pw::MultiBuf, 2>> buffers =
+ std::move(buffer).Split(split_index);
+ if (!buffers) { return false; }
+ socket_1_.Send(std::move(buffers->at(0)));
+ socket_2_.Send(std::move(buffers->at(1)));
+ return true;
+ }
+
+Incoming buffers can be split without copying, and the results can be forwarded
+to multiple different ``Socket`` s, RPC services or clients.
+
+----------
+Motivation
+----------
+Today, a Pigweed communications stack typically involves a number of different
+buffers.
+
+``pw_rpc`` users, for example, frequently use direct-memory access (DMA) or
+other system primitives to read data into a buffer, apply some link-layer
+protocol such as HDLC which copies data into a second buffer, pass the resulting
+data into pw_rpc which parses it into its own buffer. Multiple sets of buffers
+are present on the output side as well. Between DMA in and DMA out, it's easy
+for data to pass through six or more different buffers.
+
+These independent buffer systems introduce both time and space overhead. Aside
+from the additional CPU time required to move the data around, users need to
+ensure that all of the different buffer pools involved along the way have enough
+space reserved to contain the entire message. Where caching is present, moving
+the memory between locations may create an additional delay by thrashing
+between memory regions.
+
+--------
+Proposal
+--------
+``pw::buffers::MultiBuf`` is a handle to a buffer optimized for use within
+Pigweed's communications stack. It provides efficient, low-overhead buffer
+management, and serves as a standard type for passing data between drivers,
+TCP/IP implementations, RPC 2.0, and transfer 2.0.
+
+A single ``MultiBuf`` is a list of ``Chunk`` s of data. Each ``Chunk``
+points to an exclusively-owned portion of a reference-counted allocation.
+``MultiBuf`` s can be easily split, joined, prefixed, postfixed, or infixed
+without needing to copy the underlying data.
+
+The memory pointed to by ``Chunk`` s is typically allocated from a pool
+provided by a ``Socket``. This allows the ``Socket`` to provide backpressure to
+callers, and to ensure that memory is placed in DMA or shared memory regions
+as-necessary.
+
+In-Memory Layout
+================
+
+This diagram shows an example in-memory relationship between two buffers:
+- ``Buffer1`` references one chunks from region A.
+- ``Buffer2`` references two chunk from regions A and B.
+
+.. mermaid::
+
+ graph TB;
+ Buffer1 --> Chunk1A
+ Chunk1A -- "[0..64]" --> MemoryRegionA(Memory Region A)
+ Chunk1A --> ChunkRegionTrackerA
+ Buffer2 --> Chunk2A & Chunk2B
+ Chunk2A --> ChunkRegionTrackerA
+ Chunk2A -- "[64..128]" --> MemoryRegionA(Memory Region A)
+ Chunk2B -- "[0..128]" --> MemoryRegionB
+ Chunk2B --> ChunkRegionTrackerB
+
+API
+===
+
+The primary API is as follows:
+
+.. code-block:: cpp
+
+ /// An object that manages a single allocated region which is referenced
+ /// by one or more chunks.
+ class ChunkRegionTracker {
+ public:
+ ChunkRegionTracker(ByteSpan);
+
+ /// Creates the first ``Chunk`` referencing a whole region of memory.
+ /// This must only be called once per ``ChunkRegionTracker``.
+ Chunk ChunkForRegion();
+
+ protected:
+ pw::Mutex lock();
+
+ /// Destroys the `ChunkRegionTracker`.
+ ///
+ /// Typical implementations will call `std::destroy_at(this)` and then
+ /// free the memory associated with the tracker.
+ virtual void Destroy();
+
+ /// Defines the entire span of the region being managed. ``Chunk`` s
+ /// referencing this tracker may not expand beyond this region
+ /// (or into one another).
+ ///
+ /// This region must not change for the lifetime of this
+ /// ``ChunkRegionTracker``.
+ virtual ByteSpan region();
+
+ private:
+ /// Used to manage the internal linked-list of ``Chunk`` s that allows
+ /// chunks to know whether or not they can expand to fill neighboring
+ /// regions of memory.
+ pw::Mutex lock_;
+ };
+
+ /// A handle to a contiguous refcounted slice of data.
+ ///
+ /// Note: this Chunk may acquire a ``pw::sync::Mutex`` during destruction, and
+ /// so must not be destroyed within ISR context.
+ class Chunk {
+ public:
+ Chunk();
+ Chunk(ChunkRegionTracker&);
+ Chunk(Chunk&& other) noexcept;
+ Chunk& operator=(Chunk&& other);
+ ~Chunk();
+ std::byte* data();
+ const std::byte* data() const;
+ ByteSpan span();
+ ConstByteSpan span() const;
+ size_t size() const;
+
+ std::byte& operator[](size_t index);
+ std::byte operator[](size_t index) const;
+
+ /// Decrements the reference count on the underlying chunk of data and empties
+ /// this handle so that `span()` now returns an empty (zero-sized) span.
+ ///
+ /// Does not modify the underlying data, but may cause it to be
+ /// released if this was the only remaining ``Chunk`` referring to it.
+ /// This is equivalent to ``{ Chunk _unused = std::move(chunk_ref); }``
+ void Release();
+
+ /// Attempts to add `prefix_bytes` bytes to the front of this buffer by
+ /// advancing its range backwards in memory. Returns `true` if the
+ /// operation succeeded.
+ ///
+ /// This will only succeed if this `Chunk` points to a section of a chunk
+ /// that has unreferenced bytes preceeding it. For example, a `Chunk`
+ /// which has been shrunk using `DiscardFront` can then be re-expanded using
+ /// `ClaimPrefix`.
+ ///
+ /// Note that this will fail if a preceding `Chunk` has claimed this
+ /// memory using `ClaimSuffix`.
+ [[nodiscard]] bool ClaimPrefix(size_t prefix_bytes);
+
+ /// Attempts to add `suffix_bytes` bytes to the back of this buffer by
+ /// advancing its range forwards in memory. Returns `true` if the
+ /// operation succeeded.
+ ///
+ /// This will only succeed if this `Chunk` points to a section of a chunk
+ /// that has unreferenced bytes following it. For example, a `Chunk`
+ /// which has been shrunk using `Truncate` can then be re-expanded using
+ /// `ClaimSuffix`.
+ ///
+ /// Note that this will fail if a following `Chunk` has claimed this
+ /// memory using `ClaimPrefix`.
+ [[nodiscard]] bool ClaimSuffix(size_t suffix_bytes);
+
+ /// Shrinks this handle to refer to the data beginning at offset ``new_start``.
+ ///
+ /// Does not modify the underlying data.
+ void DiscardFront(size_t new_start);
+
+ /// Shrinks this handle to refer to data in the range ``begin..<end``.
+ ///
+ /// Does not modify the underlying data.
+ void Slice(size_t begin, size_t end);
+
+ /// Shrinks this handle to refer to only the first ``len`` bytes.
+ ///
+ /// Does not modify the underlying data.
+ void Truncate(size_t len);
+
+ /// Splits this chunk in two, with the second chunk starting at `split_point`.
+ std::array<Chunk, 2> Split(size_t split_point) &&;
+ };
+
+ /// A handle to a sequence of potentially non-contiguous refcounted slices of
+ /// data.
+ class MultiBuf {
+ public:
+ struct Index {
+ size_t chunk_index;
+ size_t byte_index;
+ };
+
+ MultiBuf();
+
+ /// Creates a ``MultiBuf`` pointing to a single, contiguous chunk of data.
+ ///
+ /// Returns ``std::nullopt`` if the ``ChunkList`` allocator is out of memory.
+ static std::optional<MultiBuf> FromChunk(Chunk chunk);
+
+ /// Splits the ``MultiBuf`` into two separate buffers at ``split_point``.
+ ///
+ /// Returns ``std::nullopt`` if the ``ChunkList`` allocator is out of memory.
+ std::optional<std::array<MultiBuf, 2>> Split(Index split_point) &&;
+ std::optional<std::array<MultiBuf, 2>> Split(size_t split_point) &&;
+
+ /// Appends the contents of ``suffix`` to this ``MultiBuf`` without copying data.
+ /// Returns ``false`` if the ``ChunkList`` allocator is out of memory.
+ [[nodiscard]] bool Append(MultiBuf suffix);
+
+ /// Discards the first elements of ``MultiBuf`` up to (but not including)
+ /// ``new_start``.
+ ///
+ /// Returns ``false`` if the ``ChunkList`` allocator is out of memory.
+ [[nodiscard]] bool DiscardFront(Index new_start);
+ [[nodiscard]] bool DiscardFront(size_t new_start);
+
+ /// Shifts and truncates this handle to refer to data in the range
+ /// ``begin..<stop``.
+ ///
+ /// Does not modify the underlying data.
+ ///
+ /// Returns ``false`` if the ``ChunkList`` allocator is out of memory.
+ [[nodiscard]] bool Slice(size_t begin, size_t end);
+
+ /// Discards the tail of this ``MultiBuf`` starting with ``first_index_to_drop``.
+ /// Returns ``false`` if the ``ChunkList`` allocator is out of memory.
+ [[nodiscard]] bool Truncate(Index first_index_to_drop);
+ /// Discards the tail of this ``MultiBuf`` so that only ``len`` elements remain.
+ /// Returns ``false`` if the ``ChunkList`` allocator is out of memory.
+ [[nodiscard]] bool Truncate(size_t len);
+
+ /// Discards the elements beginning with ``cut_start`` and resuming at
+ /// ``resume_point``.
+ ///
+ /// Returns ``false`` if the ``ChunkList`` allocator is out of memory.
+ [[nodiscard]] bool DiscardSegment(Index cut_start, Index resume_point);
+
+ /// Returns an iterable over the ``Chunk``s of memory within this ``MultiBuf``.
+ auto Chunks();
+ auto Chunks() const;
+
+ /// Returns a ``BidirectionalIterator`` over the bytes in this ``MultiBuf``.
+ ///
+ /// Note that use of this iterator type may be less efficient than
+ /// performing chunk-wise operations due to the noncontiguous nature of
+ /// the iterator elements.
+ auto begin();
+ auto end();
+
+ /// Counts the total number of ``Chunk`` s.
+ ///
+ /// This function is ``O(n)`` in the number of ``Chunk`` s.
+ size_t CalculateNumChunks() const;
+
+ /// Counts the total size in bytes of all ``Chunk`` s combined.
+ ///
+ /// This function is ``O(n)`` in the number of ``Chunk`` s.
+ size_t CalculateTotalSize() const;
+
+ /// Returns an ``Index`` which can be used to provide constant-time access to
+ /// the element at ``position``.
+ ///
+ /// Any mutation of this ``MultiBuf`` (e.g. ``Append``, ``DiscardFront``,
+ /// ``Slice``, or ``Truncate``) may invalidate this ``Index``.
+ Index IndexAt(size_t position) const;
+ };
+
+
+ class MultiBufAllocationFuture {
+ public:
+ Poll<std::optional<Buffer>> Poll(Context&);
+ };
+
+ class MultiBufAllocationFuture {
+ public:
+ Poll<std::optional<MultiBuf::Chunk>> Poll(Context&);
+ };
+
+ class MultiBufAllocator {
+ public:
+ std::optional<MultiBuf> Allocate(size_t size);
+ std::optional<MultiBuf> Allocate(size_t min_size, size_t desired_size);
+ std::optional<MultiBuf::Chunk> AllocateContiguous(size_t size);
+ std::optional<MultiBuf::Chunk> AllocateContiguous(size_t min_size, size_t desired_size);
+
+ MultiBufAllocationFuture AllocateAsync(size_t size);
+ MultiBufAllocationFuture AllocateAsync(size_t min_size, size_t desired_size);
+ MultiBufChunkAllocationFuture AllocateContiguousAsync(size_t size);
+ MultiBufChunkAllocationFuture AllocateContiguousAsync(size_t min_size, size_t desired_size);
+
+ protected:
+ virtual std::optional<MultiBuf> DoAllocate(size_t min_size, size_t desired_size);
+ virtual std::optional<MultiBuf::Chunk> DoAllocateContiguous(size_t min_size, size_t desired_size);
+
+ /// Invoked by the ``MultiBufAllocator`` to signal waiting futures that buffers of
+ /// the provided sizes may be available for allocation.
+ void AllocationAvailable(size_t size_available, size_t contiguous_size_available);
+ };
+
+
+The ``Chunk`` s in the prototype are stored in fallible dynamically-allocated
+arrays, but they could be stored in vectors of a fixed maximum size. The ``Chunk`` s
+cannot be stored as an intrusively-linked list because this would require per-``Chunk``
+overhead in the underlying buffer, and there may be any number of ``Chunk`` s within
+the same buffer.
+
+Multiple ``Chunk`` s may not refer to the same memory, but they may refer to
+non-overlapping sections of memory within the same region. When one ``Chunk``
+within a region is deallocated, a neighboring chunk may expand to use its space.
+
+--------------------
+Vectorized Structure
+--------------------
+The most significant design choices made in this API is supporting vectorized
+IO via a list of ``Chunk`` s. While this does carry an additional overhead, it
+is strongly motivated by the desire to carry data throughout the communications
+stack without needing a copy. By carrying a list of ``Chunk`` s, ``MultiBuf``
+allows data to be prefixed, suffixed, infixed, or split without incurring the
+overhead of a separate allocation and copy.
+
+--------------------------------------------------------------------------
+Managing Allocations with ``MultiBufAllocator`` and ``ChunkRegionTracker``
+--------------------------------------------------------------------------
+``pw::MultiBuf`` is not associated with a concrete allocator implementation.
+Instead, it delegates the creation of buffers to implementations of
+the ``MultiBufAllocator`` base class. This allows different allocator
+implementations to vend out ``pw::MultiBuf`` s that are optimized for use with a
+particular communications stack.
+
+For example, a communications stack which runs off of shared memory or specific
+DMA'able regions might choose to allocate memory out of those regions to allow
+for zero-copy writes.
+
+Additionally, custom allocator implementations can reserve headers, footers, or
+even split a ``pw::MultiBuf`` into multiple chunks in order to provide zero-copy
+fragmentation.
+
+Deallocation of these regions is managed through the ``ChunkRegionTracker``.
+When no more ``Chunk`` s associated with a ``ChunkRegionTracker`` exist,
+the ``ChunkRegionTracker`` receives a ``Destroy`` call to release both the
+region and the ``ChunkRegionTracker``.
+
+The ``MultiBufAllocator`` can place the ``ChunkRegionTracker`` wherever it
+wishes, including as a header or footer for the underlying region allocation.
+This is not required, however, as memory in regions like shared or DMA'able
+memory might be limited, in which case the ``ChunkRegionTracker`` can be
+stored elsewhere.
+
+-----------------------------------------------------
+Compatibility With Existing Communications Interfaces
+-----------------------------------------------------
+
+lwIP
+====
+`Lightweight IP stack (lwIP)
+<https://www.nongnu.org/lwip>`_
+is a TCP/IP implementation designed for use on small embedded systems. Some
+Pigweed platforms may choose to use lwIP as the backend for their ``Socket``
+implementations, so it's important that ``pw::MultiBuf`` interoperate efficiently
+with their native buffer type.
+
+lwIP has its own buffer type, `pbuf
+<https://www.nongnu.org/lwip/2_1_x/structpbuf.html>`_ optimized for use with
+`zero-copy applications
+<https://www.nongnu.org/lwip/2_1_x/zerocopyrx.html>`_.
+``pbuf`` represents buffers as linked lists of reference-counted ``pbuf`` s
+which each have a pointer to their payload, a length, and some metadata. While
+this does not precisely match the representation of ``pw::MultiBuf``, it is
+possible to construct a ``pbuf`` list which points at the various chunks of a
+``pw::MultiBuf`` without needing to perform a copy of the data.
+
+Similarly, a ``pw::MultiBuf`` can refer to a ``pbuf`` by using each ``pbuf`` as
+a "``Chunk`` region", holding a reference count on the region's ``pbuf`` so
+long as any ``Chunk`` continues to reference the data referenced by that
+buffer.
+
+------------------
+Existing Solutions
+------------------
+
+Linux's ``sk_buff``
+===================
+Linux has a similar buffer structure called `sk_buff
+<https://docs.kernel.org/networking/skbuff.html#struct-sk-buff>`_.
+It differs in a few significant ways:
+
+It provides separate ``head``, ``data``, ``tail``, and ``end`` pointers.
+Other scatter-gather fragments are supplied using the ``frags[]`` structure.
+
+Separately, it has a ``frags_list`` of IP fragments which is created via calls to
+``ip_push_pending_frames``. Fragments are supplied by the ``frags[]`` payload in
+addition to the ``skb_shared_info.frag_list`` pointing to the tail.
+
+``sk_buff`` reference-counts not only the underlying chunks of memory, but also the
+``sk_buff`` structure itself. This allows for clones of ``sk_buff`` without
+manipulating the reference counts of the individual chunks. We anticipate that
+cloning a whole ``pw::buffers::MultiBuf`` will be uncommon enough that it is
+better to keep these structures single-owner (and mutable) rather than sharing
+and reference-counting them.
+
+FreeBSD's ``mbuf``
+==================
+FreeBSD uses a design called `mbuf
+<https://man.freebsd.org/cgi/man.cgi?query=mbuf>`_
+which interestingly allows data within the middle of a buffer to be given a
+specified alignment, allowing data to be prepended within the same buffer.
+This is similar to the structure of ``Chunk``, which may reference data in the
+middle of an allocated buffer, allowing prepending without a copy.
diff --git a/seed/0110-memory-allocation-interfaces.rst b/seed/0110-memory-allocation-interfaces.rst
new file mode 100644
index 000000000..931cf6391
--- /dev/null
+++ b/seed/0110-memory-allocation-interfaces.rst
@@ -0,0 +1,464 @@
+.. _seed-0110:
+
+==================================
+0110: Memory Allocation Interfaces
+==================================
+.. seed::
+ :number: 110
+ :name: Memory Allocation Interfaces
+ :status: Accepted
+ :proposal_date: 2023-09-06
+ :cl: 168772
+
+-------
+Summary
+-------
+With dynamic memory allocation becoming more common in embedded devices, Pigweed
+should provide standardized interfaces for working with different memory
+allocators, giving both upstream and downstream developers the flexibility to
+move beyond manually sizing their modules' resource pools.
+
+----------
+Motivation
+----------
+Traditionally, most embedded firmware have laid out their systems' memory usage
+statically, with every component's buffers and resources set at compile time.
+As systems grow larger and more complex, the ability to dynamically allocate
+memory has become more desirable by firmware authors.
+
+Pigweed has made basic explorations into dynamic allocation in the past: the
+``pw_allocator`` provides basic building blocks which projects can use to
+assemble their own allocators. ``pw_allocator`` also provides a proof-of-concept
+allocator (``FreeListHeap``) based off of these building blocks.
+
+Since then, Pigweed has become a part of more complex projects and has
+developed more advanced capabilities into its own modules. As the project has
+progressed, several developers --- both on the core and customer teams --- have
+noted areas where dynamic allocation would simplify code and improve memory
+usage by enabling sharing and eliminating large reservations.
+
+--------
+Proposal
+--------
+
+Allocator Interfaces
+====================
+The core of the ``pw_allocator`` module will define abstract interfaces for
+memory allocation. Several interfaces are provided with different allocator
+properties, all of which inherit from a base generic ``Allocator``.
+
+Core Allocators
+---------------
+
+Allocator
+^^^^^^^^^
+
+.. code-block:: c++
+
+ class Allocator {
+ public:
+ class Layout {
+ public:
+ constexpr Layout(
+ size_t size, std::align_val_t alignment = alignof(std::max_align_t))
+ : size_(size), alignment_(alignment) {}
+
+ // Creates a Layout for the given type.
+ template <typename T>
+ static constexpr Layout Of() {
+ return Layout(sizeof(T), alignof(T));
+ }
+
+ size_t size() const { return size_; }
+ size_t alignment() const { return alignment_; }
+
+ private:
+ size_t size_;
+ size_t alignment_;
+ };
+
+ template <typename T, typename... Args>
+ T* New(Args&&... args);
+
+ template <typename T>
+ void Delete(T* obj);
+
+ template <typename T, typename... Args>
+ std::optional<UniquePtr<T>> MakeUnique(Args&&... args);
+
+ void* Allocate(Layout layout) {
+ return DoAllocate(layout);
+ }
+
+ void Deallocate(void* ptr, Layout layout) {
+ return DoDeallocate(layout);
+ }
+
+ bool Resize(void* ptr, Layout old_layout, size_t new_size) {
+ if (ptr == nullptr) {
+ return false;
+ }
+ return DoResize(ptr, old_layout, new_size);
+ }
+
+ void* Reallocate(void* ptr, Layout old_layout, size_t new_size) {
+ return DoReallocate(void* ptr, Layout old_layout, size_t new_size);
+ }
+
+ protected:
+ virtual void* DoAllocate(Layout layout) = 0;
+ virtual void DoDeallocate(void* ptr, Layout layout) = 0;
+
+ virtual bool DoResize(void* ptr, Layout old_layout, size_t new_size) {
+ return false;
+ }
+
+ virtual void* DoReallocate(void* ptr, Layout old_layout, size_t new_size) {
+ if (new_size == 0) {
+ DoDeallocate(ptr, old_layout);
+ return nullptr;
+ }
+
+ if (DoResize(ptr, old_layout, new_size)) {
+ return ptr;
+ }
+
+ void* new_ptr = DoAllocate(new_layout);
+ if (new_ptr == nullptr) {
+ return nullptr;
+ }
+
+ if (ptr != nullptr && old_layout.size() != 0) {
+ std::memcpy(new_ptr, ptr, std::min(old_layout.size(), new_size));
+ DoDeallocate(ptr, old_layout);
+ }
+
+ return new_ptr;
+ }
+ };
+
+``Allocator`` is the most generic and fundamental interface provided by the
+module, representing any object capable of dynamic memory allocation.
+
+The ``Allocator`` interface makes no guarantees about its implementation.
+Consumers of the generic interface must not make any assumptions around
+allocator behavior, thread safety, or performance.
+
+**Layout**
+
+Allocation parameters are passed to the allocator through a ``Layout`` object.
+This object ensures that the values provided to the allocator are valid, as well
+as providing some convenient helper functions for common allocation use cases,
+such as allocating space for a specific type of object.
+
+**Virtual functions**
+
+Implementers of the allocator interface are responsible for providing the
+following operations:
+
+* ``DoAllocate`` (required): Obtains a block of memory from the allocator with a
+ requested size and power-of-two alignment. Returns ``nullptr`` if the
+ allocation cannot be performed.
+
+ The size and alignment values in the provided layout are guaranteed to be
+ valid.
+
+ Memory returned from ``DoAllocate`` is uninitialized.
+
+* ``DoDeallocate`` (required): Releases a block of memory back to the allocator.
+
+ If ``ptr`` is ``nullptr``, does nothing.
+
+ If ``ptr`` was not previously obtained from this allocator the behavior is
+ undefined.
+
+* ``DoResize`` (optional): Extends or shrinks a previously-allocated block of
+ memory in place. If this operation cannot be performed, returns ``false``.
+
+ ``ptr`` is guaranteed to be non-null. If ``ptr`` was not previously obtained
+ from this allocator the behavior is undefined.
+
+ If the allocated block is grown, the memory in the extended region is
+ uninitialized.
+
+* ``DoReallocate`` (optional): Extends or shrinks a previously-allocated block
+ of memory, potentially copying its data to a different location. A default
+ implementation is provided, which first attempts to call ``Resize``, falling
+ back to allocating a new block and copying data if it fails.
+
+ If ``ptr`` is ``nullptr``, behaves identically to ``Allocate(new_layout)``.
+
+ If the new block cannot be allocated, returns ``nullptr``, leaving the
+ original allocation intact.
+
+ If ``new_layout.size == 0``, frees the old block and returns ``nullptr``.
+
+ If the allocated block is grown, the memory in the extended region is
+ uninitialized.
+
+**Provided functions**
+
+* ``New``: Allocates memory for an object from the allocator and constructs it.
+
+* ``Delete``: Destructs and releases memory for a previously-allocated object.
+
+* ``MakeUnique``: Allocates and constructs an object wrapped in a ``UniquePtr``
+ which owns it and manages its release.
+
+Allocator Utilities
+===================
+In addition to allocator interfaces, ``pw_allocator`` will provide utilities for
+working with allocators in a system.
+
+UniquePtr
+---------
+``pw::allocator::UniquePtr`` is a "smart pointer" analogous to
+``std::unique_ptr``, designed to work with Pigweed allocators. It owns and
+manages an allocated object, automatically deallocating its memory when it goes
+out of scope.
+
+Unlike ``std::unique_ptr``, Pigweed's ``UniquePtr`` cannot be manually
+constructed from an existing non-null pointer; it must be done through the
+``Allocator::MakeUnique`` API. This is required as the allocator associated with
+the object allocation must be known in order to release it.
+
+Usage reporting
+---------------
+``pw_allocator`` will not require any usage reporting as part of its core
+interfaces to keep them minimal and reduce implementation burden.
+
+However, ``pw_allocator`` encourages setting up reporting and will provide
+utilities for doing so. Initially, this consists of a layered proxy allocator
+which wraps another allocator implementation with basic usage reporting through
+``pw_metric``.
+
+.. code-block:: c++
+
+ class AllocatorMetricProxy : public Allocator {
+ public:
+ constexpr explicit AllocatorMetricProxy(metric::Token token)
+ : memusage_(token) {}
+
+ // Sets the wrapped allocator.
+ void Initialize(Allocator& allocator);
+
+ // Exposed usage statistics.
+ metric::Group& memusage() { return memusage_; }
+ size_t used() const { return used_.value(); }
+ size_t peak() const { return peak_.value(); }
+ size_t count() const { return count_.value(); }
+
+ // Implements the Allocator interface by forwarding through to the
+ // sub-allocator provided to Initialize.
+
+ };
+
+Integration with C++ polymorphic memory resources
+-------------------------------------------------
+The C++ standard library has similar allocator interfaces to those proposed
+defined as part of its PMR library. The reasons why Pigweed is not using these
+directly are :ref:`described below <seed-0110-why-not-pmr>`; however, Pigweed
+will provide a wrapper which exposes a Pigweed allocator through the PMR
+``memory_resource`` interface. An example of how this wrapper might look is
+presented here.
+
+.. code-block:: c++
+
+ template <typename Allocator>
+ class MemoryResource : public std::pmr::memory_resource {
+ public:
+ template <typename... Args>
+ MemoryResource(Args&&... args) : allocator_(std::forward<Args>(args)...) {}
+
+ private:
+ void* do_allocate(size_t bytes, size_t alignment) override {
+ void* p = allocator_.Allocate(bytes, alignment);
+ PW_ASSERT(p != nullptr); // Cannot throw in Pigweed code.
+ return p;
+ }
+
+ void do_deallocate(void* p, size_t bytes, size_t alignment) override {
+ allocator_.Deallocate(p, bytes, alignment);
+ }
+
+ bool do_is_equal(const std::pmr::memory_resource&) override {
+ // Pigweed allocators do not yet support the concept of equality; this
+ // remains an open question for the future.
+ return false;
+ }
+
+ Allocator allocator_;
+ };
+
+Future Considerations
+=====================
+
+Allocator traits
+----------------
+It can be useful for users to know additional details about a specific
+implementation of an allocator to determine whether it is suitable for their
+use case. For example, some allocators may have internal synchronization,
+removing the need for external locking. Certain allocators may be suitable for
+uses in specialized contexts such as interrupts.
+
+To enable users to enforce these types of requirements, it would be useful to
+provide a way for allocator implementations to define certain traits.
+Originally, this proposal accommodated for this by defining derived allocator
+interfaces which semantically enforced additional implementation contracts.
+However, this approach could have led to an explosion of different allocator
+types throughout the codebase for each permutation of traits. As such, it was
+removed from the initial allocator plan for future reinvestigation.
+
+Dynamic collections
+-------------------
+The ``pw_containers`` module defines several collections such as ``pw::Vector``.
+These collections are modeled after STL equivalents, though being
+embedded-friendly, they reserve a fixed maximum size for their elements.
+
+With the addition of dynamic allocation to Pigweed, these containers will be
+expanded to support the use of allocators. Unless absolutely necessary, upstream
+containers should be designed to work on the base ``Allocator`` interface ---
+not any of its derived classes --- to offer maximum flexibility to projects
+using them.
+
+.. code-block:: c++
+
+ template <typename T>
+ class DynamicVector {
+ DynamicVector(Allocator& allocator);
+ };
+
+Per-allocation tagging
+----------------------
+Another interface which was originally proposed but shelved for the time being
+allowed for the association of an integer tag with each specific call to
+``Allocate``. This can be incredibly useful for debugging, but requires
+allocator implementations to store additional information with each allocation.
+This added complexity to allocators, so it was temporarily removed to focus on
+refining the core allocator interface.
+
+The proposed 32-bit integer tags happen to be the same as the tokens generated
+from strings by the ``pw_tokenizer`` module. Combining the two could result in
+the ability to precisely track the source of allocations in a project.
+
+For example, ``pw_allocator`` could provide a macro which tokenizes a user
+string to an allocator tag, automatically inserting additional metadata such as
+the file and line number of the allocation.
+
+.. code-block:: c++
+
+ void GenerateAndProcessData(TaggedAllocator& allocator) {
+ void* data = allocator->AllocatedTagged(
+ Layout::Sized(kDataSize), PW_ALLOCATOR_TAG("my data buffer"));
+ if (data == nullptr) {
+ return;
+ }
+
+ GenerateData(data);
+ ProcessData(data);
+
+ allocator->Deallocate(data);
+ }
+
+Allocator implementations
+-------------------------
+Over time, Pigweed expects to implement a handful of different allocators
+covering the interfaces proposed here. No specific new implementations are
+suggested as part of this proposal. Pigweed's existing ``FreeListHeap``
+allocator will be refactored to implement the ``Allocator`` interface.
+
+---------------------
+Problem Investigation
+---------------------
+
+Use cases and requirements
+==========================
+
+* **General-purpose memory allocation.** The target of ``pw_allocator`` is
+ general-purpose dynamic memory usage by typical applications, rather than
+ specialized types of memory allocation that may be required by lower-level
+ code such as drivers.
+
+* **Generic interfaces with minimal policy.** Every project has different
+ resources and requirements, and particularly in constrained systems, memory
+ management is often optimized for their specific use cases. Pigweed's core
+ allocation interfaces should offer as broad of an implementation contract as
+ possible and not bake in assumptions about how they will be run.
+
+* **RTOS or bare metal usage.** While many systems make use of an RTOS which
+ provides utilities such as threads and synchronization primitives, Pigweed
+ also targets systems which run without one. As such, the core allocators
+ should not be tied to any RTOS requirements, and accommodations should be made
+ for different system contexts.
+
+Out of scope
+------------
+
+* **Asynchronous allocation.** As this proposal is centered around simple
+ general-purpose allocation, it does not consider asynchronous allocations.
+ While these are important use cases, they are typically more specialized and
+ therefore outside the scope of this proposal. Pigweed is considering some
+ forms of asynchronous memory allocation, such as the proposal in the
+ :ref:`Communication Buffers SEED <seed-0109>`.
+
+* **Direct STL integration.** The C++ STL makes heavy use of dynamic memory and
+ offers several ways for projects to plug in their own allocators. This SEED
+ does not propose any direct Pigweed to STL-style allocator adapters, nor does
+ it offer utilities for replacing the global ``new`` and ``delete`` operators.
+ These are additions which may come in future changes.
+
+ It is still possible to use Pigweed allocators with the STL in an indirect way
+ by going through the PMR interface, which is discussed later.
+
+* **Global Pigweed allocators.** Pigweed modules will not assume a global
+ allocator instantiation. Any usage of allocators by modules should rely on
+ dependency injection, leaving consumers with control over how they choose to
+ manage their memory usage.
+
+Alternative solutions
+=====================
+
+.. _seed-0110-why-not-pmr:
+
+C++ polymorphic allocators
+--------------------------
+C++17 introduced the ``<memory_resource>`` header with support for polymorphic
+memory resources (PMR), i.e. allocators. This library defines many allocator
+interfaces similar to those in this proposal. Naturally, this raises the
+question of whether Pigweed can use them directly, benefitting from the larger
+C++ ecosystem.
+
+The primary issue with PMR with regards to Pigweed is that the interfaces
+require the use of C++ language features prohibited by Pigweed. The allocator
+is expected to throw an exception in the case of failure, and equality
+comparisons require RTTI. The team is not prepared to change or make exceptions
+to this policy, prohibiting the direct usage of PMR.
+
+Despite this, Pigweed's allocator interfaces have taken inspiration from the
+design of PMR, incorporating many of its ideas. The core proposed ``Allocator``
+interface is similar to ``std::pmr::memory_resource``, making it possible to
+wrap Pigweed allocators with a PMR adapter for use with the C++ STL, albeit at
+the cost of an extra layer of virtual indirection.
+
+--------------
+Open Questions
+--------------
+This SEED proposal is only a starting point for the improvement of the
+``pw_allocator`` module, and Pigweed's memory management story in general.
+
+There are several open questions around Pigweed allocators which the team
+expects to answer in future SEEDs:
+
+* Should generic interfaces for asynchronous allocations be provided, and how
+ would they look?
+
+* Reference counted allocations and "smart pointers": where do they fit in?
+
+* The concept of allocator equality is essential to enable certain use cases,
+ such as efficiently using dynamic containers with their own allocators.
+ This proposal excludes APIs paralleling PMR's ``is_equal`` due to RTTI
+ requirements. Could Pigweed allocators implement a watered-down version of an
+ RTTI / type ID system to support this?
+
+* How do allocators integrate with the monolithic ``pw_system`` as a starting
+ point for projects?
diff --git a/seed/0111-build-systems.rst b/seed/0111-build-systems.rst
new file mode 100644
index 000000000..a8ad6f68c
--- /dev/null
+++ b/seed/0111-build-systems.rst
@@ -0,0 +1,269 @@
+.. _seed-0111:
+
+===============================================
+0111: Make Bazel Pigweed's Primary Build System
+===============================================
+.. seed::
+ :number: 111
+ :name: Make Bazel Pigweed's Primary Build System
+ :status: Accepted
+ :proposal_date: 2023-09-26
+ :cl: 171695
+
+-------
+Summary
+-------
+This SEED proposes that Pigweed transition to using `Bazel
+<https://bazel.build/>`_ as its primary build system, replacing `GN
+<https://gn.googlesource.com/gn/>`_ in that role.
+
+Pigweed is and will continue to be a multi-build-system project. As modular
+middleware, Pigweed aspires to be easy to integrate with existing embedded
+projects whatever their build system. To facilitate this, we provide BUILD
+files for multiple systems (Bazel, CMake, GN, Soong), as well as other
+distributables where applicable (npm packages, Python wheels, etc).
+
+But Pigweed is more than just a collection of modules. Pigweed offers a quick,
+ergonomic way to start a new embedded project, as well as developer tooling
+that lets it scale from a prototype to production deployment. And if you're
+starting a new project using Pigweed from day one, you will ask: which build
+system *should* I use? This is what we mean by Pigweed's *primary* build
+system.
+
+Pigweed's primary build system has been GN, but soon will be Bazel.
+
+----------
+Motivation
+----------
+GN has been Pigweed's primary build system since inception, and we've developed
+an extensive GN build that was used to successfully ship products at scale. GN
+is fast and extensible, and its flexible toolchain abstraction is well-suited
+to the multi-target builds that arise in most embedded projects.
+
+But GN has limitations:
+
+#. **Small community.** GN is a niche build system: the only major open-source
+ projects that use it are Chromium and Fuchsia. It is not championed by any
+ major nonprofit or corporation. Few users or open-source contributors come
+ to Pigweed with any past experience with GN.
+#. **No reusable rulesets.** Pigweed has written and maintains all its GN
+ rules: for C/C++, for Python, for Go (though those are deprecated). With
+ Rust entering Pigweed, we are now developing GN rules for Rust. There are
+ no built-in or community-provided rules we could adopt instead. Developing
+ all rules in-house gives us flexibility, but requires large up-front and
+ ongoing investments.
+#. **No hermetic builds.** GN offers no sandboxing and relies on timestamps to
+ decide if outputs need to be rebuilt. This has undesirable consequences:
+
+ * The boundary between the environment produced by
+ :ref:`module-pw_env_setup` and GN is blurred, making GN-built Pigweed as
+ a whole hostile to systems like Docker or remote execution services.
+ * Incremental builds can become corrupted. Deleting the output directory
+ and environment is an undesirable but necessary piece of every Pigweed
+ developer's toolkit.
+ * Reliably running only affected tests in CQ is not possible.
+
+We would like Pigweed to recommend a build system that does not suffer from these
+limitations.
+
+These limitations are not new. What's changed is the build system landscape.
+When Pigweed was started years ago, GN was the best choice for a project
+emphasizing multi-target builds. But the alternatives have now matured.
+
+--------
+Proposal
+--------
+The proposal is to make Bazel the recommended build system to use with Pigweed,
+and the best overall build system for embedded developers. This will involve a
+combination of contributions to Pigweed itself, to existing open-source Bazel
+rules we wish to reuse, and when necessary to core Bazel.
+
+The vision is not merely to achieve feature parity with Pigweed's GN offering
+while addressing the limitations identified above, but to fully utilize the
+capabilities provided by Bazel to produce the best possible developer
+experience. For example, Bazel offers native support for external dependency
+management and remote build execution. We will make it easy for Pigweed
+projects to leverage features like these.
+
+* **What about GN?** Pigweed's GN support will continue, focusing on
+ maintenance rather than new build features. No earlier than 2026, if no
+ Pigweed projects are using GN, we may remove GN support. *The approval of
+ this SEED does not imply approval of removing GN support.* This decision is
+ explicitly deferred until a future date.
+
+* **What about CMake?** Because of its wide adoption in the C++ community,
+ CMake will be supported indefinitely at the current level.
+
+-------
+Roadmap
+-------
+This section lists the high-level milestones for Pigweed's Bazel support, and
+then dives into the specific work needed to reach them.
+
+This roadmap is our plan of record as of the time of writing, but like all SEED
+content it represents a snapshot in time. We are not as committed to the
+specific dates as we are to the general direction.
+
+There's no specific action that users must take by any date. But our
+recommendations about build system choice (embodied in docs and in what we tell
+people when they ask us) will change at some point.
+
+Milestones
+==========
+* **M0: Good for Most.** We can recommend Bazel as the build system for most
+ new projects. We may not have full parity with GN yet, but we're close enough
+ that the benefits of adopting Bazel exceed the costs, even in the short run.
+ The target date for this milestone is the end of 2023.
+
+ * Out of scope for M0: Windows support. We have to start somewhere, and we're
+ starting with Linux and MacOS.
+
+* **M1: Good for All.** We can recommend Bazel for all new Pigweed projects,
+ including ones that need Windows support. The target date is end of Q1
+ 2024. After this date, we don't expect any new projects to use GN.
+
+* **M2: Best.** We develop compelling features for embedded within the
+ Bazel ecosystem. This will happen throughout 2024.
+
+Technical tracks
+================
+There are three main technical tracks:
+
+* **Configurable toolchains** exist for host and embedded, for C++ and Rust.
+ A separate upcoming SEED will cover this area in detail, but the high-level
+ goal is to make it straightforward to create families of related toolchains
+ for embedded targets. This is required for milestone M0, except for Windows
+ support, which is part of M1. The overall tracking issue is `b/300458513
+ <https://issues.pigweed.dev/issues/300458513>`_.
+
+* **Core build patterns** (facades, multi-platform build, third-party crate
+ deps for Rust) are established, documented, and usable.
+
+ * M0:
+
+ * Module configuration is supported in Bazel, `b/234872811
+ <https://issues.pigweed.dev/issues/234872811>`_.
+ * Bazel proto codegen is feature-complete, `b/301328390
+ <https://issues.pigweed.dev/issues/301328390>`_.
+ * Multiplatform build is ergonomic thanks to the adoption of
+ `platform_data
+ <https://github.com/bazelbuild/proposals/blob/main/designs/2023-06-08-standard-platform-transitions.md#depend-on-a-target-built-for-a-different-platform>`_
+ and `platform-based flags
+ <https://github.com/bazelbuild/proposals/blob/main/designs/2023-06-08-platform-based-flags.md>`_, `b/301334234
+ <https://issues.pigweed.dev/issues/301334234>`_.
+ * Clang sanitizers (asan, msan, tsan) are easy to enable in the Bazel build, `b/301487567
+ <https://issues.pigweed.dev/issues/301487567>`_.
+
+ * M1:
+
+ * On-device testing pattern for Bazel projects developed and documented, `b/301332139
+ <https://issues.pigweed.dev/issues/301332139>`_.
+ * Sphinx documentation can be built with Bazel.
+ * OSS Fuzz integration through Bazel.
+
+* **Bootstrap** for Bazel projects is excellent. This includes offering
+ interfaces to Pigweed developer tooling like :ref:`module-pw_console`,
+ :ref:`module-pw_cli`, etc.
+
+ * M0: GN-free bootstrap for Bazel-based projects is designed and prototyped, `b/274658181
+ <https://issues.pigweed.dev/issues/274658181>`_.
+
+ * M1: Pigweed is straightforward to manage as a Bazel dependency, `b/301336229
+ <https://issues.pigweed.dev/issues/301336229>`_.
+
+* **Onboarding** for users new to Pigweed-on-Bazel is easy thanks to
+ excellent documentation, including examples.
+
+ * M0:
+
+ * There is a Bazel example project for Pigweed, `b/299994234
+ <https://issues.pigweed.dev/issues/299994234>`_.
+ * We have a "build system support matrix" that compares the features
+ available in the three main build systems (Bazel, CMake, GN),
+ `b/301481759 <https://issues.pigweed.dev/issues/301481759>`_.
+
+ * M1:
+
+ * The sample project has Bazel support, `b/302150820
+ <https://issues.pigweed.dev/issues/302150820>`_.
+
+------------
+Alternatives
+------------
+The main alternatives to investing in Bazel are championing GN or switching to
+a different build system.
+
+Champion GN
+===========
+Pigweed does not have the resources to bring GN to parity with modern build
+systems like Bazel, Buck2, or Meson. This is an area where we should partner
+with another large project rather than build capabilities ourselves.
+
+CMake
+=====
+CMake is `the most popular build system for C++ projects
+<https://www.jetbrains.com/lp/devecosystem-2021/cpp/#Which-project-models-or-build-systems-do-you-regularly-use>`_,
+by a significant margin. We already offer some CMake support in Pigweed. But
+it's not a viable candidate for Pigweed's primary build system:
+
+* **No multi-toolchain builds** Unlike Bazel and GN, CMake does not support
+ multi-toolchain builds.
+* **No Python or Rust support** Again unlike Bazel and GN, CMake is primarily
+ focused on building C++ code. But Pigweed is a multilingual project, and
+ Python and Rust need first-class treatment.
+* **No hermetic builds** Unlike Bazel, CMake does not support sandboxing.
+
+Many developers are attracted to CMake by its IDE support. Fortunately, `IDE
+support for Bazel is also well-developed <https://bazel.build/install/ide>`_.
+
+Other build systems
+===================
+There are other multi-lingual, correctness-emphasizing build systems out there,
+most prominently `Meson <https://mesonbuild.com/>`_ and `Buck2
+<https://buck2.build/>`_. We did not consider them realistic targets for
+migration at this time. They offer similar features to Bazel, and we have an
+existing Bazel build that's in use by some projects, as well as a closer
+relationship with the Bazel community.
+
+--------------
+Open questions
+--------------
+Additional SEEDs related to Bazel support are anticipated but have not yet been
+written. They will be linked from here once they exist.
+
+* `SEED-0113
+ <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/173453>`_:
+ Modular Bazel C/C++ toolchain API
+* SEED-????: Pigweed CI/CQ for Bazel
+
+----------------------------
+Appendix: Why Bazel is great
+----------------------------
+This SEED has not focused on why Bazel is a great build system. This is because
+we are not choosing Bazel over other major build systems, like Meson or Buck2,
+for its specific features. We are motivated to recommend a new build system
+because of GN's limitations, and we choose Bazel because we have a pre-existing
+community of Bazel users, developers with Bazel experience, and a close
+relationship with the Bazel core team.
+
+But actually, Bazel *is* great! Here are some things we like best about it:
+
+* **Correct incremental builds.** It's great to be able to trust the build
+ system to just do the right thing, including on a rebuild.
+* **External dependency management.** Bazel can manage external dependencies
+ for you, including lazily downloading them only when needed. By leveraging
+ this, we expect to speed up Pigweed bootstrap from several minutes to
+ several seconds.
+* **Remote build execution** Bazel has excellent native support for `executing
+ build actions in a distributed manner on workers in the cloud
+ <https://bazel.build/remote/rbe>`_. Although embedded builds are typically
+ small, build latency and infra test latency is a recurring concern among
+ Pigweed users, and leveraging remote builds should allow us to dramatically
+ improve performance in this area.
+* **Python environment management.** The Python rules for Bazel take care of
+ standing up a Python interpreter with a project-specific virtual
+ environment, a functionality we had to develop in-house for our GN build.
+* **Multilingual support.** Bazel comes with official or widely adopted
+ third-party rules for C++, Python, Java, Go, Rust, and other langauges.
+* **Active community.** The Bazel Slack is always helpful, and GitHub issues
+ tend to receive swift attention.
diff --git a/seed/0112-async-poll.rst b/seed/0112-async-poll.rst
new file mode 100644
index 000000000..e205062d0
--- /dev/null
+++ b/seed/0112-async-poll.rst
@@ -0,0 +1,615 @@
+.. _seed-0112:
+
+======================
+0112: Async Poll Model
+======================
+.. seed::
+ :number: 0112
+ :name: Async Poll Model
+ :status: Accepted
+ :proposal_date: 2023-9-19
+ :cl: 168337
+
+-------
+Summary
+-------
+This SEED proposes the development of a new “informed-Poll”-based pw::async
+library. The “informed Poll” model, popularized by
+`Rust’s Future trait, <https://doc.rust-lang.org/std/future/trait.Future.html>`_
+offers an alternative to callback-based APIs. Rather than invoking a separate
+callback for every event, the informed Poll model runs asynchronous ``Task`` s.
+
+A ``Task`` is an asynchronous unit of work. Informed Poll-based asynchronous
+systems use ``Task`` s similar to how synchronous systems use threads.
+Users implement the ``Poll`` function of a ``Task`` in order to define the
+asynchronous behavior of a routine.
+
+.. code-block:: cpp
+
+ class Task {
+ public:
+ /// Does some work, returning ``Complete`` if done.
+ /// If not complete, returns ``Pending`` and arranges for `cx.waker()` to be
+ /// awoken when `Task:Poll` should be invoked again.
+ virtual pw::MaybeReady<Complete> Poll(pw::async::Context& cx);
+ };
+
+Users can start running a ``Task`` by ``Post`` ing it to a ``Dispatcher``.
+``Dispatcher`` s are asynchronous event loops which are responsible for calling
+``Poll`` every time the ``Task`` indicates that it is ready to make progress.
+
+This API structure allows Pigweed async code to operate efficiently, with low
+memory overhead, zero dynamic allocations, and simpler state management.
+
+Pigweed’s new async APIs will enable multi-step asynchronous operations without
+queuing multiple callbacks. Here is an example in which a proxy object receives
+data and then sends it out before completing:
+
+.. code-block:: cpp
+
+ class ProxyOneMessage : public Task {
+ public:
+ /// Proxies one ``Data`` packet from a ``Receiver`` to a ``Sender``.
+ ///
+ /// Returns:
+ /// ``pw::async::Complete`` when the task has completed. This happens
+ /// after a ``Data`` packet has been received and sent, or an error
+ /// has occurred and been logged.
+ /// ``pw::async::Pending`` if unable to complete. ``cx.waker()`` will be
+ /// awoken when ``Poll`` should be re-invoked.
+ pw::async::MaybeReady<pw::async::Complete> Poll(pw::async::Context& cx) {
+ if (!send_future_) {
+ // ``PollRead`` checks for available data or errors.
+ pw::async::MaybeReady<pw::Result<Data>> new_data = receiver_.PollRead(cx);
+ if (new_data.is_pending()) {
+ return pw::async::Pending;
+ }
+ if (!new_data->ok()) {
+ PW_LOG_ERROR("Receiving failed: %s", data->status().str());
+ return pw::async::Complete;
+ }
+ Data& data = **new_data;
+ send_future_ = sender_.Send(std::move(data));
+ }
+ // ``PollSend`` attempts to send `data_`, returning `Pending` if
+ // `sender_` was not yet able to accept `data_`.
+ pw::async::MaybeReady<pw::Status> sent = send_future_.Poll(cx);
+ if (sent.is_pending()) {
+ return pw::async::Pending;
+ }
+ if (!sent->ok()) {
+ PW_LOG_ERROR("Sending failed: %s", sent->str());
+ }
+ return pw::async::Complete;
+ }
+
+ private:
+ // ``SendFuture`` is some type returned by `Sender::Send` that offers a
+ // ``Poll`` method similar to the one on ``Task``.
+ std::optional<SendFuture> send_future_;
+ // `receiver_` and `sender_` are provided by the `ProxyOneMessage` constructor.
+ Receiver receiver_;
+ Sender sender_;
+ };
+
+ // --- Usage ---
+ // ``static`` is used for simplicity, but real ``Task`` s can have temporary
+ // lifetimes.
+ static ProxyOneMessage proxy(receiver, sender);
+
+ // Runs `proxy` until it completes, either by successfully receiving and
+ // sending a message, or by exiting early after logging an error.
+ dispatcher.Post(proxy);
+
+--------
+Proposal
+--------
+This SEED proposes that Pigweed develop a set of async APIs and utilities
+designed around the informed Poll model. If early trials with partner teams are
+successful, this new library will be used as the basis for future async code in
+Pigweed.
+
+-----
+Goals
+-----
+The goals of this SEED are as follows:
+
+* Establish community consensus that informed ``Poll`` is the best async model
+ for Pigweed to pursue.
+* Outline an initial API for ``Dispatcher`` implementors (platform authors) and
+ top-level ``Task`` writers.
+
+----------
+Motivation
+----------
+The purpose of this SEED is to gather agreement that ``Poll``-based async
+APIs are worth pursuing. We believe that these APIs provide the needed support
+for:
+
+* Small code size
+* Environments without dynamic allocation
+* Creating reusable building blocks and high-level modules
+
+The current ``Task`` API is limited in these respects: a single ``Task`` must
+be created and stored for every individual asynchronous event. ``Task`` s
+cannot be reused, and the memory allocated for a ``Task`` can only be reclaimed
+after a ``Task`` has been completed or cancelled, resulting in complex
+semantics for multithreaded environments or those with interrupt-driven events.
+
+Completing a sequence of events therefore requires either dynamic allocation
+or statically saving a separate ``Task`` worth of memory for every kind of
+event that may occur.
+
+Additionally, every asynchronous layer requires introducing another round of
+callbacks whose semantics may be unclear and whose captures may add lifetime
+challenges.
+
+This proposal resolves these issues by choosing an alternative approach.
+
+-----------
+API Summary
+-----------
+
+A Note On Specificity
+=====================
+This SEED provides API outlines in order to more clearly explain the intended
+API direction. The specific function signatures shown here are not meant to be
+authoritative, and are subject to change. As the implementation develops
+support for more platforms and features, some additions, changes, or removals
+may be necessary and will be considered as part of the regular CL review
+process.
+
+With that in mind, asynchronous ``Task`` s in this model could adopt an API
+like the following:
+
+The ``MaybeReady`` Type
+=======================
+Functions return ``MaybeReady<T>`` to indicate that their result may or may
+not be available yet. ``MaybeReady<T>`` is a generic sum type similar to
+``std::optional<T>``. It has two variants, ``Ready(T)`` or ``Pending``.
+
+The API is similar to ``std::optional<T>``, but ``MaybeReady<T>`` provides extra
+semantic clarification that the absense of a value means that it is not ready
+yet.
+
+Paired with the ``Complete`` type, ``MaybeReady<Complete>`` acts like
+``bool IsComplete``, but provides more semantic information to the user than
+returning a simple ``bool``.
+
+.. code-block:: cpp
+
+ /// A value that is ready, and
+ template<typename T>
+ struct Ready<T> { value: T };
+
+ /// A content-less struct that indicates a not-ready value.
+ struct Pending {};
+
+ /// A value of type `T` that is possibly available.
+ ///
+ /// This is similar to ``std::optional<T>``, but provides additional
+ /// semantic indication that the value is not ready yet (still pending).
+ /// This can aid in making type signatures such as
+ /// ``MaybeReady<std::optional<Item>>`` easier to understand, and provides
+ /// clearer naming like `IsReady` (compared to ``has_value()``).
+ template<typename T>
+ class MaybeReady {
+ public:
+ /// Implicitly converts from ``T``, ``Ready<T>`` or ``Pending``.
+ MaybeReady(T);
+ MaybeReady(Ready<T>);
+ MaybeReady(Pending);
+ bool IsReady();
+ T Value() &&;
+ ...
+ };
+
+ /// A content-less struct that indicates completion.
+ struct Complete {};
+
+Note that the ``Pending`` type takes no type arguments, and so can be created
+and returned from macros that don't know which ``T`` is returned by the
+function they are in. For example:
+
+.. code-block:: cpp
+
+ // Simplified assignment macro
+ #define PW_ASSIGN_IF_READY(lhs, expr) \
+ auto __priv = (expr); \
+ if (!__priv.IsReady()) { \
+ return pw::async::Pending; \
+ } \
+ lhs = std::move(__priv.Value()) \
+
+ MaybeReady<Bar> PollCreateBar(Context& cx);
+
+ Poll<Foo> DoSomething(Context& cx) {
+ PW_ASSIGN_IF_READY(Bar b, PollCreateBar(cx));
+ return CreateFoo();
+ }
+
+This is similar to the role of the ``std::nullopt_t`` type.
+
+The ``Dispatcher`` Type
+=======================
+Dispatchers are the event loops responsible for running ``Task`` s. They sleep
+when there is no work to do, and wake up when there are ``Task`` s ready to
+make progress.
+
+On some platforms, the ``Dispatcher`` may also provide special hooks in order
+to support single-threaded asynchronous I/O.
+
+.. code-block:: cpp
+
+ class Dispatcher {
+ public:
+ /// Tells the ``Dispatcher`` to run ``Task`` to completion.
+ /// This method does not block.
+ ///
+ /// After ``Post`` is called, ``Task::Poll`` will be invoked once.
+ /// If ``Task::Poll`` does not complete, the ``Dispatcher`` will wait
+ /// until the ``Task`` is "awoken", at which point it will call ``Poll``
+ /// again until the ``Task`` completes.
+ void Post(Task&);
+ ...
+ };
+
+The ``Waker`` Type
+==================
+A ``Waker`` is responsible for telling a ``Dispatcher`` when a ``Task`` is
+ready to be ``Poll`` ed again. This allows ``Dispatcher`` s to intelligently
+schedule calls to ``Poll`` rather than retrying in a loop (this is the
+"informed" part of "informed Poll").
+
+When a ``Dispatcher`` calls ``Task::Poll``, it provides a ``Waker`` that will
+enqueue the ``Task`` when awoken. ``Dispatcher`` s can implement this
+functionality by having ``Waker`` add the ``Task`` to an intrusive linked list,
+add a pointer to the ``Task`` to a ``Dispatcher``-managed vector, or by pushing
+a ``Task`` ID onto a system-level async construct such as ``epoll``.
+
+.. code-block:: cpp
+
+ /// An object which can respond to asynchronous events by queueing work to
+ /// be done in response, such as placing a ``Task`` on a ``Dispatcher`` loop.
+ class Waker {
+ public:
+ /// Wakes up the ``Waker``'s creator, alerting it that an asynchronous
+ /// event has occurred that may allow it to make progress.
+ ///
+ /// ``Wake`` operates on an rvalue reference (``&&``) in order to indicate
+ /// that the event that was waited on has been completed. This makes it
+ /// possible to track the outstanding events that may cause a ``Task`` to
+ /// wake up and make progress.
+ void Wake() &&;
+
+ /// Creates a second ``Waker`` from this ``Waker``.
+ ///
+ /// ``Clone`` is made explicit in order to allow for easier tracking of
+ /// the different ``Waker``s that may wake up a ``Task``.
+ Waker Clone(Token wait_reason_indicator) &;
+ ...
+ };
+
+The ``Wake`` function itself may be called by any system with knowledge that
+the ``Task`` is now ready to make progress. This can be done from an interrupt,
+from a separate task, from another thread, or from any other function that
+knows that the `Poll`'d type may be able to make progress.
+
+The ``Context`` Type
+====================
+``Context`` is a bundle of arguments supplied to ``Task::Poll`` that give the
+``Task`` information about its asynchronous environment. The most important
+parts of the ``Context`` are the ``Dispatcher``, which is used to ``Post``
+new ``Task`` s, and the ``Waker``, which is used to tell the ``Dispatcher``
+when to run this ``Task`` again.
+
+.. code-block:: cpp
+
+ class Context {
+ public:
+ Context(Dispatcher&, Waker&);
+ Dispatcher& Dispatcher();
+ Waker& Waker();
+ ...
+ };
+
+The ``Task`` Type
+=================
+Finally, the ``Task`` type is implemented by users in order to run some
+asynchronous work. When a new asynchronous "thread" of execution must be run,
+users can create a new ``Task`` object and send it to be run on a
+``Dispatcher``.
+
+.. code-block:: cpp
+
+ /// A task which may complete one or more asynchronous operations.
+ ///
+ /// ``Task`` s should be actively ``Poll`` ed to completion, either by a
+ /// ``Dispatcher`` or by a parent ``Task`` object.
+ class Task {
+ public:
+ MaybeReady<Complete> Poll(Context&);
+ ...
+ protected:
+ /// Returns whether or not the ``Task`` has completed.
+ ///
+ /// If the ``Task`` has not completed, `Poll::Pending` will be returned,
+ /// and `context.Waker()` will receive a `Wake()` call when the ``Task``
+ /// is ready to make progress and should be ``Poll`` ed again.
+ virtual MaybeReady<Complete> DoPoll(Context&) = 0;
+ ...
+ };
+
+This structure makes it possible to run complex asynchronous ``Task`` s
+containing multiple concurrent or sequential asynchronous events.
+
+------------------------------------
+Relationship to Futures and Promises
+------------------------------------
+The terms "future" and "promise" are unfortunately quite overloaded. This SEED
+does not propose a "method chaining" API (e.g. ``.AndThen([](..) { ... }``), nor
+is creating reference-counted, blocking handles to the output of other threads
+a la ``std::future``.
+
+Where this SEED refers to ``Future`` types (e.g. ``SendFuture`` in the summary
+example), it means only a type which offers a ``Poll(Context&)`` method and
+return some ``MaybeReady<T>`` value. This common pattern can be used to build
+various asynchronous state machines which optionally return a value upon
+completion.
+
+---------------------------------------------
+Usage In The Rust Ecosystem Shows Feasability
+---------------------------------------------
+The ``Poll``-based ``Task`` approach suggested here is similar to the one
+adopted by Rust's
+`Future type <https://doc.rust-lang.org/stable/std/future/trait.Future.html>`_.
+The ``Task`` class in this SEED is analogous to Rust's ``Future<Output = ()>``
+type. This model has proven usable on small environments without dynamic allocation.
+
+Due to compiler limitations, Rust's ``async fn`` language feature will often
+generate ``Future`` s which suffer from code size issues. However,
+manual implementations of Rust's ``Future`` trait (not using ``async fn``) do
+not have this issue.
+
+We believe the success of Rust's ``Poll``-based ``Future`` type demonstrates
+that the approach taken in this SEED can meet the needs of Pigweed users.
+
+---------
+Code Size
+---------
+`Some experiments have been done
+<https://pigweed-review.googlesource.com/c/pigweed/experimental/+/154570>`_
+to compare the size of the code generated by
+a ``Poll``-based approach with code generated with the existing ``pw::async``
+APIs. These experiments have so far found that the ``Poll``-based approach
+creates binaries with smaller code size due to an increased opportunity for
+inlining, static dispatch, and a smaller number of separate ``Task`` objects.
+
+The experimental ``pw_async_bench`` examples show that the ``Poll``-based
+approach offers more than 2kB of savings on a small ``Socket``-like example.
+
+------------------------
+The ``pw::async`` Facade
+------------------------
+This SEED proposes changing ``Dispatcher`` from a virtual base into a
+platform-specific concrete type.
+
+The existing ``pw::async::Dispatcher`` class is ``virtual`` in order to support
+use of an alternative ``Dispatcher`` implementation in tests. However, this
+approach assumes that ``Task`` s are capable of running on arbitrary
+implementations of the ``Dispatcher`` virtual interface. In practice, this is
+not the case.
+
+Different platforms will use different native ``Dispatcher`` waiting primitives
+including ``epoll``, ``kqueue``, IOCP, Fuchsia's ``libasync``/``zx_port``, and
+lower-level waiting primitives such as Zephyr's RTIO queue.
+
+Each of these primitives is strongly coupled with native async events, such as
+IO or buffer readiness. In order to support ``Dispatcher``-native IO events,
+IO objects must be able to guarantee that they are running on a compatible
+``Dispatcher``. In Pigweed, this can be accomplished through the use of the
+facade pattern.
+
+The facade patterns allows for concrete, platform-dependent definitions of the
+``Task``, ``Context``, ``Waker``, and ``Dispatcher`` types. This allows these
+objects to interact with one another as necessary to implement fast scheduling
+with minimal in-memory or code size overhead.
+
+This approach enables storing platform-specific per- ``Task`` scheduling details
+inline with the ``Task`` itself, enabling zero-allocation ``Task`` scheduling
+without the need for additional resource pools.
+
+This also allows for native integration with platform-specific I/O primitives
+including ``epoll``, ``kqueue``, IOCP, and others, but also lower-level
+waiting primitives such as Zephyr's RTIO queue.
+
+Testing
+=======
+Moving ``Dispatcher`` to a non-virtual facade means that the previous approach
+of testing with a ``FakeDispatcher`` would require a separate toolchain in
+order to provide a different instantiation of the ``Dispatcher`` type. However,
+we can adopt a simpler approach: the ``Dispatcher`` type can offer minimial
+testing primitives natively:
+
+.. code-block:: cpp
+
+ class Dispatcher {
+ public:
+ ...
+
+ /// Runs tasks until none are able to make immediate progress.
+ ///
+ /// Returns whether a ``Task`` was run.
+ bool RunUntilStalled();
+
+ /// Enable mock time, initializing the mock timer to some "zero"-like
+ /// value.
+ void InitializeMockTime();
+
+ /// Advances the mock timer forwards by ``duration``.
+ void AdvanceMockTime(chrono::SystemClock::duration duration);
+ };
+
+These primitives are sufficient for testing with mock time. They allow
+test authors to avoid deadlocks, timeouts, or race conditions.
+
+Downsides of Built-in Testing Functions
+---------------------------------------
+Requiring concrete ``Dispatcher`` types to include the testing functions above
+means that the production ``Dispatcher`` implementations will have code in them
+that is only needed for testing.
+
+However, these additions are minimal: mocking time introduces a single branch
+for each timer access, which is still likely to be more efficient than the
+virtual function call that was required under the previous model.
+
+Advantages of Built-in Testing Functions
+----------------------------------------
+Testing with a "real" ``Dispatcher`` implementation ensures that:
+
+* All ``pw::async`` platforms provide support for testing
+* The ``Dispatcher`` used for testing will support the same I/O operations and
+ features provided by the production ``Dispatcher``
+* Tests will run under conditions as-close-to-production as possible. This will
+ allow catching bugs that are caused by the interaction of the code and the
+ particular ``Dispatcher`` on which it runs.
+
+Enabling Dynamic ``Task`` Lifetimes
+===================================
+While some ``Task`` s may be static, others may not be. For these, we need a
+mechanism to ensure that:
+
+* ``Task`` resources are not destroyed while ``Waker`` s that may post them
+ to a ``Dispatcher`` remain.
+* ``Task`` resources are not destroyed while the ``Task`` itself is running
+ or is queued to run.
+
+In order to enable this, platforms should clear all ``Waker`` s referencing a
+``Task`` when the ``Task`` completes: that ``Task`` will make no further
+progress, so ``Wake`` ing it serves no purpose.
+
+Once all ``Waker`` s have been cleared and the ``Task`` has finished running
+on the ``Dispatcher``, the ``Dispatcher`` should call that ``Task`` s
+``Cleanup`` function so that the ``Task`` can free any associated dynamic
+resources. During this ``Cleanup`` function, no other resources of ``Task``
+may be accessed by the application author until the ``Task`` has been
+re-initialized. If the memory associated with the ``Task`` is to be reused,
+the ``Task`` object itself must be reinitialized by invoking the ``Init``
+function.
+
+.. code-block:: cpp
+
+ class Task {
+ public:
+ ...
+ void Init();
+ virtual void Cleanup();
+ ...
+ };
+
+This allows downstream ``Task`` inheritors to implement dynamic free-ing of
+``Task`` resources, while also allowing the ``Dispatcher`` implementation the
+opportunity to clean up its own resources stored inside of the ``Task`` base
+class.
+
+Waker
+=====
+``Waker`` s will at first only be created via the ``Dispatcher``
+implementation, via cloning, or by the null constructor. Later on, the API may
+be expanded to allow for waking sub-tasks. The necessity of this at Pigweed's
+scale has not yet been determined.
+
+Timer
+=====
+``pw::async`` will additionally provide a ``Timer`` type. A ``Timer`` can be
+``Poll``'d by a ``Task`` in order to determine if a certain amount of time has
+passed. This can be used to implement timeouts or to schedule work.
+
+One possible ``Timer`` API would be as follows:
+
+.. code-block:: cpp
+
+ class Timer {
+ public:
+ Timer(Context&, chrono::SystemClock::time_point deadline);
+ Timer(Context&, chrono::SystemClock::duration delay);
+ pw::MaybeReady<Complete> Poll(Context&);
+ ...
+ };
+
+In order to enable this, the ``Dispatcher`` base class will include the
+following functions which implementations should use to trigger timers:
+
+.. code-block:: cpp
+
+ class DispatcherBase {
+ public:
+ ...
+ protected:
+ /// Returns the time of the earliest timer currently scheduled to fire.
+ std::optional<chrono::SystemClock::time_point> EarliestTimerExpiry();
+
+ /// Marks all ``Timer`` s with a time before ``time_point`` as complete,
+ /// and awakens any associated tasks.
+ ///
+ /// Returns whether any ``Timer`` objects were marked complete.
+ bool AwakenTimersUpTo(chrono::SystemClock::time_point);
+
+ /// Invoked when a new earliest ``Timer`` is created.
+ ///
+ /// ``Dispatcher`` implementations can override this to receive
+ /// notifications when a new timer is added.
+ virtual void NewEarliestTimer();
+ ...
+ };
+
+---------------------
+C++ Coroutine Support
+---------------------
+The informed ``Poll`` approach is well-suited to
+`C++20's coroutines <https://en.cppreference.com/w/cpp/language/coroutines>`_.
+Coroutines using the ``co_await`` and ``co_return`` expressions can
+automatically create and wait on ``Task`` types, whose base class will
+implement the ``std::coroutine_traits`` interface on C++20 and later.
+
+Dynamic Allocation
+==================
+Note that C++ coroutines allocate their state dynamically using
+``operator new``, and therefore are not usable on systems in which dynamic
+allocation is not available or where recovery from allocation failure is
+required.
+
+------------
+Rust Interop
+------------
+Rust uses a similar informed ``Poll`` model for its ``Future`` trait. This
+allows ``pw::async`` code to invoke Rust-based ``Future`` s by creating a
+Rust ``Waker`` which invokes the C++ ``Waker``, and performing cross-language
+``Poll`` ing.
+
+Rust support is not currently planned for the initial version of ``pw::async``,
+but will likely come in the future as Pigweed support for Rust expands.
+
+------------------------------------------------
+Support for Traditional Callback-Style Codebases
+------------------------------------------------
+One concern is interop with codebases which adopt a more traditional
+callback-driven design, such as the one currently supported by ``pw::async``.
+These models will continue to be supported under the new design, and can be
+modeled as a ``Task`` which runs a single function when ``Poll`` ed.
+
+---------
+Migration
+---------
+For ease of implementation and in order to ensure a smooth transition, this API
+will initially live alongside the current ``pw::async`` interface. This API
+will first be tested with one or more trial usages in order to stabilize the
+interface and ensure its suitability for Pigweed users.
+
+Following that, the previous ``pw::async`` implementation will be deprecated.
+A shim will be provided to allow users of the previous API to easily migrate
+their code onto the new ``pw::async`` implementation. After migrating to the
+new implementation, users can gradually transition to the new ``Poll``-based
+APIs as-desired. It will be possible to intermix legacy-style and
+``Poll``-based async code within the same dispatcher loop, allowing legacy
+codebases to adopt the ``Poll``-based model for new subsystems.
diff --git a/seed/0113-bazel-cc-toolchain-api.rst b/seed/0113-bazel-cc-toolchain-api.rst
new file mode 100644
index 000000000..758e32350
--- /dev/null
+++ b/seed/0113-bazel-cc-toolchain-api.rst
@@ -0,0 +1,724 @@
+.. _seed-0113:
+
+===========================================
+0113: Add modular Bazel C/C++ toolchain API
+===========================================
+.. seed::
+ :number: 0113
+ :name: Add modular Bazel C/C++ toolchain API
+ :status: Accepted
+ :proposal_date: 2023-09-28
+ :cl: 173453
+
+-------
+Summary
+-------
+This SEED proposes custom Starlark rules for declaring C/C++ toolchains in
+Bazel, and scalable patterns for sharing modular components of C/C++ toolchain
+definitions.
+
+----------
+Motivation
+----------
+There is a lot of boilerplate involved in standing up a Bazel C/C++ toolchain.
+While a good portion of the verbosity of specifying a new toolchain is
+important, necessary machinery, nearly as much suffers from one or more of the
+following problems:
+
+- **Underdocumented patterns**: The
+ `create_cc_toolchain_config_info() <https://bazel.build/rules/lib/toplevel/cc_common#create_cc_toolchain_config_info>`_
+ method has an attribute called ``target_cpu`` that doesn't have an associated
+ list of expected values, which suggests the argument is for bookkeeping
+ purposes and doesn't affect Bazel behavior. The reality is this argument
+ *does* have expected values that change behavior, but these are undocumented
+ (see `this GitHub issue <https://github.com/bazelbuild/bazel/issues/19353>`_
+ for more information).
+
+- **Not inherently modular**: Bazel expects the overwhelming majority of a
+ C/C++ toolchain to be specified as part of a call to
+ ``create_cc_toolchain_config_info()``. Because this is a Starlark method,
+ there's a lot of flexibility with how you construct a toolchain config, but
+ very little by way of existing patterns for creating something that is
+ testable, sharable, or in other ways modular. The existing
+ `tutorial for creating a C/C++ toolchain <https://bazel.build/tutorials/ccp-toolchain-config#configuring_the_c_toolchain>`_
+ illustrates expanding out the toolchain definition as a no-argument Starlark
+ rule.
+
+As Pigweed fully embraces multi-platform builds, it is critical for both
+Pigweed and users of Pigweed that it is easy to stand up custom toolchain
+definitions that work for the relevant hardware project-specific code/libraries.
+
+This SEED seeks to address the shortcomings in Bazel C/C++ toolchain
+declaration by establishing patterns and providing custom build rules that
+are owned and maintained by Pigweed.
+
+--------
+Proposal
+--------
+1. Introduce rules for defining C/C++ toolchains
+================================================
+In an effort to improve the experience of defining a C/C++ toolchain, Pigweed
+will introduce new Bazel rules that allow toolchains to share common boilerplate
+without hindering power-user functionality.
+
+While this work centers around an improved experience for defining a toolchain
+via ``create_cc_toolchain_config_info()``, other build rules will be introduced
+for closely related aspects of the toolchain definition process.
+
+An example of what these rules would look like in practice is as follows:
+
+.. code-block::
+
+ # A tool that can be used by various build actions.
+ pw_cc_tool(
+ name = "clang_tool",
+ path = "@cipd_llvm_toolchain//:bin/clang",
+ additional_files = [
+ "@cipd_llvm_toolchain//:all",
+ ],
+ )
+
+ # A mapping of a tool to actions, with flag sets that define behaviors.
+ pw_cc_action_config(
+ name = "clang",
+ actions = ALL_ASM_ACTIONS + ALL_C_COMPILER_ACTIONS,
+ tools = [
+ ":clang_tool",
+ ],
+ flag_sets = [
+ # Most flags should NOT end up here. Only unconditional flags that
+ # should ALWAYS be bound to this tool (e.g. static library
+ # workaround fix for macOS).
+ "@pw_toolchain//flag_sets:generate_depfile",
+ ],
+ )
+
+ # A trivial flag to be consumed by a C/C++ toolchain.
+ pw_cc_flag_set(
+ name = "werror",
+ actions = ALL_COMPILE_ACTIONS,
+ flags = [
+ "-Werror",
+ ],
+ )
+
+ # A list of flags that can be added to a toolchain configuration.
+ pw_cc_flag_set(
+ name = "user_compile_options",
+ actions = ALL_COMPILE_ACTIONS,
+ flag_groups = [
+ ":user_compile_options_flags",
+ ]
+ )
+
+ # A more complex compiler flag that requires template expansion.
+ pw_cc_flag_group(
+ name = "user_compile_options_flags",
+ flags = ["%{user_compile_flags}"],
+ iterate_over = "user_compile_flags",
+ expand_if_available = "user_compile_flags",
+ )
+
+ # The underlying definition of a complete C/C++ toolchain.
+ pw_cc_toolchain(
+ name = "host_toolchain_linux",
+ action_configs = [
+ ":clang",
+ ":clang++",
+ # ...
+ ],
+ additional_files = ":linux_sysroot_files",
+ action_config_flag_sets = [
+ "@pw_toolchain//flag_sets:no_canonical_prefixes",
+ ":user_compile_options",
+ ":werror",
+ ],
+ features = [
+ "@pw_toolchain//features:c++17",
+ ],
+ target_cpu = "x86_64",
+ target_system_name = "x86_64-unknown-linux-gnu",
+ toolchain_identifier = "host-toolchain-linux",
+ )
+
+ # Toolchain resolution parameters for the above C/C++ toolchain.
+ toolchain(
+ name = "host_cc_toolchain_linux",
+ exec_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ target_compatible_with = [
+ "@platforms//os:linux",
+ ],
+ toolchain = ":host_toolchain_linux",
+ toolchain_type = "@bazel_tools//tools/cpp:toolchain_type",
+ )
+
+2. Provide standard toolchain building-blocks
+=============================================
+Pigweed will build out a repository of sharable instantiations of the
+aforementioned custom rules to give projects the resources they need to quickly
+and easily assemble toolchains for desktop and embedded targets. This includes,
+but is not limited to:
+
+- Rules that define tool sets for common toolchains (LLVM/clang, GNU/gcc).
+- Fully specified, modular
+ `features <https://bazel.build/docs/cc-toolchain-config-reference#features>`_.
+- Common flag sets that users may want to apply directly to their toolchains.
+ (enabling/disabling warnings, C++ standard version, etc.)
+- Platform/architecture support rules, including host OS SDK integrations
+ (Xcode, Windows SDK) and architecture-specific flag sets.
+
+These components will help establish patterns that will make it significantly
+easier for Pigweed users (and Bazel users at large) to define their own
+toolchains.
+
+---------------------
+Problem investigation
+---------------------
+This section explores previous work, and details why existing solutions don't
+meet Pigweed's needs.
+
+bazelembedded/rules_cc_toolchain
+================================
+The `rules_cc_toolchain <https://github.com/bazelembedded/rules_cc_toolchain>`_
+as part of the larger bazelembedded suite was actually the initial foundation
+of Pigweed's Bazel build. While this served as a very good initial foundation,
+it didn't provide the flexibility needed to easily stand up additional
+toolchains in ways that gave downstream projects sufficient control over the
+flags, libraries, tools, and sysroot.
+
+To work around the limited configurability of toolchain flags, Pigweed employed
+the following workarounds:
+
+#. Place ``copts`` and ``linkopts`` in ``.bazelrc``: This was problematic
+ because ``.bazelrc`` is not intrinsically shared with or propagated to
+ downstream users of Pigweed. Also, flags here are unilaterally applied
+ without OS-specific considerations.
+#. Attach flags to build targets with custom wrappers: This approach
+ intrinsically requires the existence of the ``pw_cc_library``, which
+ introduces difficulty around consistent interoperability with other Bazel
+ projects (among other issues detailed in
+ `b/267498492 <https://issues.pigweed.dev/issues/267498492>`_).
+
+Some other issues encountered when working with this solution include:
+
+- These rules intended to be modular, but in practice were relatively tightly
+ coupled.
+- Transitive dependencies throughout the toolchain definition process resulted
+ in some hard-to-debug issues (see
+ `this pull request <https://github.com/bazelembedded/rules_cc_toolchain/pull/39>`_
+ and `b/254518544 <https://issues.pigweed.dev/issues/254518544>`_.
+
+bazelembedded/modular_cc_toolchains
+===================================
+The `modular_cc_toolchains <https://github.com/bazelembedded/modular_cc_toolchains>`_
+repository is a new attempt as part of the bazelembedded suite at providing
+truly modular toolchain rules. The proposed direction is much more in-line
+with the needs of Pigweed, but at the moment the repository exists as an
+initial draft of ideas rather than a complete implementation.
+
+This repository greatly inspired Pigweed's initial prototype for modular
+toolchains, but diverges significantly from the underlying Bazel C/C++
+toolchain building-blocks. If this work was already complete and
+well-established, it probably would have satisfied some of Pigweed's key needs.
+
+lowRISC/crt
+===========
+The `compiler repository toolkit <https://github.com/lowRISC/crt>`_ is another
+scalable approach at toolchains. This repository strives to be an all-in-one
+repository for embedded toolchains, and does a very good job at providing
+scalable models for establishing toolchains. This repository is relatively
+monolithic, though, and doesn't necessarily address the concern of quickly
+and easily standing up custom toolchains. Instead, it's more suited towards
+contributing new one-size-fits-all toolchains to ``crt`` directly.
+
+Android's toolchain
+===================
+Android's Bazel-based build has invested heavily in toolchains, but they're
+very tightly coupled to the use cases of Android. For example,
+`this <https://cs.android.com/android/platform/superproject/main/+/main:build/bazel/toolchains/clang/host/linux-x86/cc_toolchain_features.bzl;l=375-389;drc=097d710c349758fc2732497fe5c3d1b0a32fa4a8>`_ binds ``-fstrict-aliasing`` to a condition based on the target architecture.
+These toolchains scale for the purpose of Android, but unfortunately are
+inherently not modular or reusable outside of that context.
+
+Due to the sheer amount of investment in these toolchains, though, they serve
+as a good reference for building out a complete toolchain in Bazel.
+
+Pigweed's modular Bazel toolchain prototype
+===========================================
+As part of an exploratory phase of getting toolchains set up for Linux and
+macOS,
+`an initial prototype <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/157634>`_
+for modular Bazel toolchains was drafted and deployed to Pigweed. This work
+introduced two key build rules: ``pw_cc_toolchain_feature`` and
+``pw_cc_toolchain``. With both of these rules, it’s possible to instantiate a
+vast array of toolchain variants without writing a single line of Starlark. A
+few examples of these building blocks in action are provided below.
+
+.. code-block::
+
+ # pw_cc_toolchain example taken from https://cs.opensource.google/pigweed/pigweed/+/main:pw_toolchain/host_clang/BUILD.bazel;l=113-143;drc=7df1768d915fe11dae05751f70f143e60acfb17a.
+
+ pw_cc_toolchain(
+ name = "host_toolchain_linux",
+ abi_libc_version = "unknown",
+ abi_version = "unknown",
+ all_files = ":all_linux_files",
+ ar = "@llvm_toolchain//:bin/llvm-ar",
+
+ # TODO: b/305737273 - Globbing all files for every action has a
+ # performance hit, make these more granular.
+ ar_files = ":all_linux_files",
+ as_files = ":all_linux_files",
+ compiler = "unknown",
+ compiler_files = ":all_linux_files",
+ coverage_files = ":all_linux_files",
+ cpp = "@llvm_toolchain//:bin/clang++",
+ dwp_files = ":all_linux_files",
+ feature_deps = [
+ ":linux_sysroot",
+ "@pw_toolchain//features:no_canonical_prefixes",
+ ],
+ gcc = "@llvm_toolchain//:bin/clang",
+ gcov = "@llvm_toolchain//:bin/llvm-cov",
+ host_system_name = "unknown",
+ ld = "@llvm_toolchain//:bin/clang++",
+ linker_files = ":all_linux_files",
+ objcopy_files = ":all_linux_files",
+ strip = "@llvm_toolchain//:bin/llvm-strip",
+ strip_files = ":all_linux_files",
+ supports_param_files = 0,
+ target_cpu = "unknown",
+ target_libc = "unknown",
+ target_system_name = "unknown",
+ toolchain_identifier = "host-toolchain-linux",
+ )
+
+ # pw_cc_toolchain_feature examples taken from https://cs.opensource.google/pigweed/pigweed/+/main:pw_toolchain_bazel/features/BUILD.bazel;l=21-34;drc=f96fd31675d136bd37a7f3840102cb256d555cea.
+
+ # Disables linking of the default C++ standard library to allow linking of a
+ # different version.
+ pw_cc_toolchain_feature(
+ name = "no_default_cpp_stdlib",
+ linkopts = ["-nostdlib++"],
+ )
+
+ # Prevent relative paths from being converted to absolute paths.
+ # Note: This initial prototype made this a feature, but it should instead
+ # exist as a flag_set.
+ pw_cc_toolchain_feature(
+ name = "no_canonical_prefixes",
+ copts = [
+ "-no-canonical-prefixes",
+ ],
+ )
+
+What’s worth noting is that the ``pw_cc_toolchain_feature`` build rule looks
+very similar to a GN ``config``. This was no mistake, and was an attempt to
+substantially reduce the boiler plate for creating new sharable compiler flag
+groups.
+
+Unfortunately, it quickly became apparent that this approach limited control
+over the underlying toolchain definition creation process. In order to support
+``always_link`` on macOS, a custom logic and flags had to be directly baked into
+the rule used to declare toolchains
+(`relevant change <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168614/17/pw_toolchain_bazel/cc_toolchain/private/cc_toolchain.bzl>`_).
+While workarounds like this should be possible, the fact that this had to be
+upstreamed internally to ``pw_cc_toolchain`` exposed limitations in the
+abstraction patterns that were established. Such limitations could preclude
+some project from using ``pw_cc_toolchain`` at all.
+
+---------------
+Detailed design
+---------------
+The core design proposal is to transform the providers used by
+``cc_common.create_cc_toolchain_config_info()`` into build rules. The approach
+has been prototyped
+`here <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/168351/1>`_,
+and retains API compatibility with the initial prototype as a proof-of-concept.
+
+One core pattern established by this design is transforming content that would
+typically live as Starlark to instead live in build files. This is done to
+make it easier to read and reference existing work.
+
+Implementation requirements
+===========================
+Compatibility with native C/C++ rules
+-------------------------------------
+The core of Pigweed's toolchain build rules will rely on the providers
+defined as part of Bazel's
+`rules_cc <https://github.com/bazelbuild/rules_cc/blob/main/cc/cc_toolchain_config_lib.bzl>`_. This means that the new rules can interop with
+existing work that directly uses these toolchain primitives. It also provides
+a clear path for migrating existing toolchains piece-by-piece (which may be
+written completely in Starlark).
+
+Any extensions beyond the existing providers (e.g. specifying
+``additional_files`` on a ``pw_cc_tool``) must happen parallel to existing
+providers so that rules that consume the ``cc_toolchain_config_lib`` providers
+can work with vanilla providers.
+
+Compatibility with Bazel rules ecosystem
+----------------------------------------
+In following with the larger Bazel rules ecosystem, the toolchain building
+blocks will be designed such that they can be used independently from Pigweed.
+This allows this work to be used for non-embedded projects, and reduces the
+overhead for standing up a custom Bazel C/C++ toolchain in any arbitrary
+project.
+
+Initially, the work will live as ``pw_toolchain_bazel`` in the main Pigweed
+repository to facilitate testing. This module must not depend on any other
+aspects of Pigweed. As the toolchain rules mature, they will eventually be
+available as a separate repository to match the modularity patterns used by
+the larger Bazel rules ecosystem.
+
+Introduce ``pw_cc_flag_set`` and ``pw_cc_flag_group``
+=====================================================
+The majority of build flags would be expressed as ``pw_cc_flag_set`` and
+``pw_cc_flag_group`` pairs.
+
+.. code-block::
+
+ # A simple flag_set with a single flag.
+ pw_cc_flag_set(
+ name = "werror",
+ # Only applies to C/C++ compile actions (i.e. no assemble/link/ar).
+ actions = ALL_CPP_COMPILER_ACTIONS + ALL_C_COMPILER_ACTIONS,
+ flags = [
+ "-Werror",
+ ],
+ )
+
+ # A flag_group that potentially expands to multiple flags.
+ pw_cc_flag_group(
+ name = "user_compile_options_flags",
+ flags = ["%{user_compile_flags}"],
+ iterate_over = "user_compile_flags",
+ expand_if_available = "user_compile_flags",
+ )
+
+ # A flag_set that relies on a non-trivial or non-constant expression of
+ # flags.
+ pw_cc_flag_set(
+ name = "user_compile_options",
+ actions = ALL_COMPILE_ACTIONS,
+ flag_groups = [
+ ":user_compile_options_flags",
+ ]
+ )
+
+These closely mimic the API of ``cc_toolchain_config_lib.flag_set()`` and
+``cc_toolchain_config_lib.flag_group()``, with the following exceptions:
+
+**pw_cc_flag_set**
+
+*Added*
+
+- ``flags`` (added): Express a constant, trivial list of flags. If this is
+ specified, ``flag_groups`` may not be specified. This eliminates the need
+ for specifying a corresponding ``pw_cc_flag_group`` for every
+ ``pw_cc_flag_set`` for most flags.
+
+**pw_cc_flag_group**
+
+*Removed*
+
+- ``expand_if_true``\, ``expand_if_false``\, ``expand_if_equal``\: More complex
+ rules that rely on these should live as custom Starlark rules that provide a
+ ``FlagGroupInfo``\, or ``FlagSetInfo`` (depending on which is more ergonomic
+ to express the intent). See :ref:`pw_cc_flag_set-exceptions` below for an
+ example that illustrates how express more complex ``flag_group``\s that rely
+ on these attributes.
+
+Application of flags
+--------------------
+Flags can be applied to a toolchain in three ways. This section attempts to
+provide initial guidance for where flags should be applied, though it's likely
+better practices will evolve as this work sees more use. For the latest
+guidance, please consult the official documentation when it rolls out to
+``pw_toolchain_bazel``.
+
+Flags unconditionally applied to a toolchain
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The majority of flags fall into this category. Architecture flags,
+globally-applied warnings, global defines, and other similar flags should be
+applied in the ``action_config_flag_sets`` attribute of a ``pw_cc_toolchain``
+(see :ref:`pw_cc_toolchain-toolchain-declarations` for more information). Each
+``pw_cc_flag_set`` (or other rule that provides a ``FlagSetInfo`` provider)
+listed in ``action_config_flag_sets`` is unconditionally applied to every tool
+that matches the ``actions`` listed in the flag set.
+
+.. _application-of-flags-feature-flags:
+
+Feature flags
+~~~~~~~~~~~~~
+Flag sets applied as features may or may not be enabled even if they are listed
+in the ``features`` attribute of a ``pw_cc_toolchain``. The
+`official Bazel documentation on features <https://bazel.build/docs/cc-toolchain-config-reference#features>`_
+provides some good guidance on when features should be employed. To summarize,
+features should be used when either they should be controllable by users
+invoking the build, or if they affect build behavior beyond simply
+adding/removing flags (e.g. by introducing additional build actions).
+
+Flags unconditionally applied to a tool
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+These flags are flags that are bound to a particular tool. These are not
+expressed as part of a ``pw_cc_toolchain``, and are instead bound to a
+``pw_cc_action_config``. This means that the flag set is unconditionally
+applied to every user of that action config. These kinds of flag applications
+should be reserved for flags required to assemble a working set of tools (such
+as generating a depfile, or adding support for static library link handling
+:ref:`as illustrated below <pw_cc_flag_set-exceptions>`).
+
+Flag application order
+~~~~~~~~~~~~~~~~~~~~~~
+When invoking the underlying tools, the intended order of flags is as follows:
+
+#. Flags listed in the ``flag_sets`` list of a ``pw_cc_action_config``.
+#. Flags listed in ``action_config_flag_sets`` of a ``pw_cc_toolchain``.
+#. Flags listed in ``features`` of a ``pw_cc_toolchain``.
+
+These lists are intended to be sensitive to ordering, earlier items in the lists
+should appear in the final tool invocation flags before later items in the list.
+
+As transitive dependencies between features/flags are not supported as part of
+this proposal, exact traversal of transitive flag dependencies will be left
+to be decided if/when that feature is introduced. This proposal suggests
+postorder handling of flags as the most intuitive order.
+
+.. _pw_cc_flag_set-exceptions:
+
+Exceptions
+----------
+Some flags are too complex to be nicely expressed in a Bazel build file. These
+flag sets or flag groups will need to be expressed in Starlark as custom rules.
+Fortunately, this will interop well with simpler flag sets since the underlying
+providers are all the same.
+
+**Example**
+
+In a Starlark file (e.g. ``//tools/llvm/llvm_ar_patch.bzl``), the required
+``flag_set`` can be defined:
+
+.. code-block::
+
+ # Starlark rules in a .bzl file for a relatively complicated workaround for
+ # what would normally be inherently managed by Bazel internally.
+ # TODO: b/297413805 - Remove this implementation.
+
+ def _pw_cc_static_libs_to_link_impl():
+ """Returns a flag_set provider that sets up static libraries to link."""
+ return flag_set(
+ actions = [
+ ACTION_NAMES.cpp_link_static_library,
+ ],
+ flag_groups = [
+ flag_group(
+ expand_if_available = "libraries_to_link",
+ iterate_over = "libraries_to_link",
+ flag_groups = [
+ flag_group(
+ expand_if_equal = variable_with_value(
+ name = "libraries_to_link.type",
+ value = "object_file",
+ ),
+ flags = ["%{libraries_to_link.name}"],
+ ),
+ flag_group(
+ expand_if_equal = variable_with_value(
+ name = "libraries_to_link.type",
+ value = "object_file_group",
+ ),
+ flags = ["%{libraries_to_link.object_files}"],
+ iterate_over = "libraries_to_link.object_files",
+ ),
+ ],
+ ),
+ ],
+ )
+
+ pw_cc_static_libs_to_link = rule(
+ implementation = _pw_cc_static_libs_to_link_impl,
+ provides = [FlagSetInfo],
+ )
+
+And then in the ``BUILD.bazel`` file, the rules would be used as if they
+were a ``pw_cc_flag_set``:
+
+.. code-block::
+
+ load(
+ "@pw_toolchain//tools/llvm:llvm_ar_patch.bzl",
+ "pw_cc_static_libs_to_link"
+ )
+
+ pw_cc_static_libs_to_link(
+ name = "static_library_action_flags",
+ )
+
+ pw_cc_action_config(
+ name = "llvm_ar",
+ actions = ACTION_NAMES.cpp_link_static_library,
+ tools = [
+ ":llvm_ar_tool",
+ ],
+ flag_sets = [
+ ":static_library_action_flags",
+ ],
+ )
+
+Introduce ``pw_cc_feature`` and ``pw_cc_feature_set``
+=====================================================
+These types are just permutations of the ``cc_toolchain_config_lib.feature()``
+and ``cc_toolchain_config_lib.with_feature_set()`` API. For guidance on when
+these should be used, see
+:ref:`application of feature flags <application-of-flags-feature-flags>`.
+
+.. code-block::
+
+ pw_cc_feature_set(
+ name = "static_pie_requirements",
+ with_features = ["pie"],
+ # If this doesn't work when certain features are enabled, they should
+ # be specified as ``without_features``.
+ )
+
+ pw_cc_feature(
+ name = "static_pie",
+ flag_sets = [
+ "//flag_sets:static_pie",
+ ],
+ implies = ["static_link_flag"],
+ requires = [
+ ":static_pie_requirements",
+ ],
+ )
+
+Introduce ``pw_cc_action_config`` and ``pw_cc_tool``
+====================================================
+These are closely related to the ``ActionConfigInfo`` and ``ToolInfo``
+providers, but allow additional files to be attached and a list of actions to
+be attached rather than a single action.
+
+.. code-block::
+
+ pw_cc_tool(
+ name = "clang_tool",
+ path = "@llvm_toolchain//:bin/clang",
+ additional_files = [
+ "@llvm_toolchain//:all",
+ ],
+ )
+
+ pw_cc_action_config(
+ name = "clang",
+ actions = ALL_ASM_ACTIONS + ALL_C_COMPILER_ACTIONS,
+ tools = [
+ ":clang_tool",
+ ],
+ flag_sets = [
+ # Most flags should NOT end up here. Only unconditional flags that
+ # should ALWAYS be bound to this tool (e.g. static library
+ # workaround fix for macOS).
+ "//flag_sets:generate_depfile",
+ ],
+ )
+
+.. _pw_cc_toolchain-toolchain-declarations:
+
+Toolchain declarations
+======================
+In following with the other proposed rules, ``pw_cc_toolchain`` largely
+follows the API of ``cc_common.create_cc_toolchain_config_info()``. Most of the
+attributes are logically passed through, with the following exceptions:
+
+- **action_config_flag_sets**: Flag sets to apply to action configs. Since flag
+ sets are intrinsically bound to actions, there’s no need to divide them at
+ this level.
+- **additional_files**: Now that tools can spec out required files, those
+ should be propagated and mostly managed internally. The ``\*_files`` members
+ will still be available, but shouldn’t see much use. additional_files is like
+ “all_files”, but applies to all action_configs.
+
+.. code-block::
+
+ pw_cc_toolchain(
+ name = "host_toolchain_linux",
+ abi_libc_version = "unknown", # We should consider how to move this out in the future.
+ abi_version = "unknown",
+ action_configs = [
+ "@llvm_toolchain//tools:clang",
+ "@llvm_toolchain//tools:clang++",
+ "@llvm_toolchain//tools:lld",
+ "@llvm_toolchain//tools:llvm_ar",
+ "@llvm_toolchain//tools:llvm_cov",
+ "@llvm_toolchain//tools:llvm_strip",
+ ],
+ additional_files = ":linux_sysroot_files",
+ action_config_flag_sets = [
+ ":linux_sysroot",
+ "@pw_toolchain//flag_collections:strict_warnings",
+ "@pw_toolchain//flag_sets:no_canonical_prefixes",
+ ],
+ features = [
+ "@pw_toolchain//features:c++17",
+ ],
+ host_system_name = "unknown",
+ supports_param_files = 0, # Seems like this should be attached to a pw_cc_action_config...
+ target_cpu = "unknown",
+ target_libc = "unknown",
+ target_system_name = "unknown",
+ toolchain_identifier = "host-toolchain-linux",
+ cxx_builtin_include_directories = [
+ "%package(@llvm_toolchain//)%/include/x86_64-unknown-linux-gnu/c++/v1",
+ "%package(@llvm_toolchain//)%/include/c++/v1",
+ "%package(@llvm_toolchain//)%/lib/clang/17/include",
+ "%sysroot%/usr/local/include",
+ "%sysroot%/usr/include/x86_64-linux-gnu",
+ "%sysroot%/usr/include",
+ ],
+ )
+
+------------
+Alternatives
+------------
+Improve Bazel's native C/C++ toolchain rules
+============================================
+Improving Bazel's native rules for defining C/C++ toolchains is out of the
+scope of Pigweed's work. Changing the underlying toolchain API as Bazel
+understands it is a massive undertaking from the perspective of migrating
+existing code. We hope that the custom rule effort can help guide future
+decisions when it comes to toolchain scalability and maintainability.
+
+----------
+Next steps
+----------
+Rust toolchain interop
+======================
+Pigweed's Rust toolchains have some interoperability concerns and requirements.
+The extend of this needs to be thoroughly investigated as a next step to ensure
+that the Rust/C/C++ toolchain experience is relatively unified and ergonomic.
+
+More maintainable ``cxx_builtin_include_directories``
+=====================================================
+In the future, it would be nice to have a more sharable solution for managing
+``cxx_builtin_include_directories`` on a ``pw_cc_toolchain``. This could
+plausibly be done by allowing ``pw_cc_flag_set`` to express
+``cxx_builtin_include_directories`` so they can be propagated back up to the
+``pw_cc_toolchain``.
+
+Feature name collision guidance
+===============================
+Features support relatively complex relationships among each other, but
+traditionally rely on string names to express these relationships rather than
+labels. This introduces significant ambiguity, as it's possible for multiple
+features to use the same logical name so long as they aren't both employed in
+the same toolchain. In practice, the only way to tell what features will end up
+enabled is to manually unpack what features a toolchain pulls in, and
+cross-reference it against the output of
+`--experimental_save_feature_state <https://bazel.build/reference/command-line-reference#flag--experimental_save_feature_state>`_.
+
+One potential solution to this problem is to add a mechanism for expressing
+features as labels, which will allow relationships to be expressed more
+concretely, and help prevent unintended naming collisions. This would not
+replace the ability to express relationships with features not accessible via
+labels, but rather live alongside it.
diff --git a/seed/BUILD.gn b/seed/BUILD.gn
index 6a5158fc6..62aa25808 100644
--- a/seed/BUILD.gn
+++ b/seed/BUILD.gn
@@ -23,6 +23,15 @@ pw_doc_group("docs") {
":0002",
":0101",
":0102",
+ ":0104",
+ ":0105",
+ ":0107",
+ ":0108",
+ ":0109",
+ ":0110",
+ ":0111",
+ ":0112",
+ ":0113",
]
}
@@ -42,3 +51,39 @@ pw_doc_group("0101") {
pw_doc_group("0102") {
sources = [ "0102-module-docs.rst" ]
}
+
+pw_doc_group("0104") {
+ sources = [ "0104-display-support.rst" ]
+}
+
+pw_doc_group("0105") {
+ sources = [ "0105-pw_tokenizer-pw_log-nested-tokens.rst" ]
+}
+
+pw_doc_group("0107") {
+ sources = [ "0107-communications.rst" ]
+}
+
+pw_doc_group("0108") {
+ sources = [ "0108-pw_emu-emulators-frontend.rst" ]
+}
+
+pw_doc_group("0109") {
+ sources = [ "0109-comms-buffers.rst" ]
+}
+
+pw_doc_group("0110") {
+ sources = [ "0110-memory-allocation-interfaces.rst" ]
+}
+
+pw_doc_group("0111") {
+ sources = [ "0111-build-systems.rst" ]
+}
+
+pw_doc_group("0112") {
+ sources = [ "0112-async-poll.rst" ]
+}
+
+pw_doc_group("0113") {
+ sources = [ "0113-bazel-cc-toolchain-api.rst" ]
+}
diff --git a/targets/BUILD.bazel b/targets/BUILD.bazel
index 486d1ed0b..66a1ccd5e 100644
--- a/targets/BUILD.bazel
+++ b/targets/BUILD.bazel
@@ -11,3 +11,210 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
+
+# Bazel build flags.
+#
+# See https://pigweed.dev/build_system.html#facades-and-backends-tutorial for
+# how these flags are used.
+#
+# Please keep this list sorted by name, i.e. the following command should yield
+# no output:
+#
+# t=$(grep 'name = ' targets/BUILD.bazel); diff <(echo $t) <(echo $t | sort)
+
+package(default_visibility = ["//visibility:public"])
+
+label_flag(
+ name = "freertos_config",
+ build_setting_default = "@pigweed//third_party/freertos:freertos_config",
+)
+
+label_flag(
+ name = "mcuxpresso_sdk",
+ build_setting_default = "@pigweed//third_party/mcuxpresso:default_mcuxpresso_sdk",
+)
+
+label_flag(
+ name = "pw_assert_backend",
+ build_setting_default = "@pigweed//pw_assert:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_assert_backend_impl",
+ build_setting_default = "@pigweed//pw_assert:backend_impl_multiplexer",
+)
+
+label_flag(
+ name = "pw_async_task_backend",
+ build_setting_default = "@pigweed//pw_async_basic:task",
+)
+
+label_flag(
+ name = "pw_async_fake_dispatcher_backend",
+ build_setting_default = "@pigweed//pw_async_basic:fake_dispatcher",
+)
+
+label_flag(
+ name = "pw_boot_backend",
+ build_setting_default = "@pigweed//pw_boot:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_chrono_system_clock_backend",
+ build_setting_default = "@pigweed//pw_chrono:system_clock_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_chrono_system_timer_backend",
+ build_setting_default = "@pigweed//pw_chrono:system_timer_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_crypto_ecdsa_backend",
+ build_setting_default = "@pigweed//pw_crypto:ecdsa_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_crypto_sha256_backend",
+ build_setting_default = "@pigweed//pw_crypto:sha256_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_fuzzer_fuzztest_backend",
+ build_setting_default = "@pigweed//pw_fuzzer:fuzztest_stub",
+)
+
+label_flag(
+ name = "pw_interrupt_backend",
+ build_setting_default = "@pigweed//pw_interrupt:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_log_backend",
+ build_setting_default = "@pigweed//pw_log:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_log_string_handler_backend",
+ build_setting_default = "@pigweed//pw_log_string:handler_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_log_tokenized_handler_backend",
+ build_setting_default = "@pigweed//pw_log_tokenized:base64_over_hdlc",
+)
+
+label_flag(
+ name = "pw_malloc_backend",
+ build_setting_default = "@pigweed//pw_malloc:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_perf_test_timer_backend",
+ build_setting_default = "@pigweed//pw_perf_test:timer_multiplexer",
+)
+
+label_flag(
+ name = "pw_rpc_system_server_backend",
+ build_setting_default = "@pigweed//pw_rpc/system_server:system_server_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_binary_semaphore_backend",
+ build_setting_default = "@pigweed//pw_sync:binary_semaphore_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_counting_semaphore_backend",
+ build_setting_default = "@pigweed//pw_sync:counting_semaphore_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_interrupt_spin_lock_backend",
+ build_setting_default = "@pigweed//pw_sync:interrupt_spin_lock_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_mutex_backend",
+ build_setting_default = "@pigweed//pw_sync:mutex_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_recursive_mutex_backend",
+ build_setting_default = "@pigweed//pw_sync:recursive_mutex_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_thread_notification_backend",
+ build_setting_default = "@pigweed//pw_sync:thread_notification_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_timed_mutex_backend",
+ build_setting_default = "@pigweed//pw_sync:timed_mutex_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sync_timed_thread_notification_backend",
+ build_setting_default = "@pigweed//pw_sync:timed_thread_notification_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_sys_io_backend",
+ build_setting_default = "@pigweed//pw_sys_io:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_system_target_hooks_backend",
+ build_setting_default = "@pigweed//pw_system:target_hooks_multiplexer",
+)
+
+label_flag(
+ name = "pw_thread_id_backend",
+ build_setting_default = "@pigweed//pw_thread:id_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_thread_iteration_backend",
+ build_setting_default = "@pigweed//pw_thread:iteration_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_thread_sleep_backend",
+ build_setting_default = "@pigweed//pw_thread:sleep_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_thread_test_thread_context_backend",
+ build_setting_default = "@pigweed//pw_thread:test_thread_context_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_thread_thread_backend",
+ build_setting_default = "@pigweed//pw_thread:thread_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_thread_yield_backend",
+ build_setting_default = "@pigweed//pw_thread:yield_backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_trace_backend",
+ build_setting_default = "@pigweed//pw_trace:backend_multiplexer",
+)
+
+label_flag(
+ name = "pw_unit_test_googletest_backend",
+ build_setting_default = "@pigweed//pw_unit_test:light",
+)
+
+label_flag(
+ name = "pw_unit_test_main",
+ build_setting_default = "@pigweed//pw_unit_test:simple_printing_main",
+)
+
+label_flag(
+ name = "target_rtos",
+ build_setting_default = "@pigweed//pw_build/constraints/rtos:none",
+)
diff --git a/targets/android/target_docs.rst b/targets/android/target_docs.rst
index b8a92a7f3..371a7c186 100644
--- a/targets/android/target_docs.rst
+++ b/targets/android/target_docs.rst
@@ -9,3 +9,58 @@ Native Development Kit (NDK).
.. warning::
This target is under construction, not ready for use, and the documentation
is incomplete.
+
+Setup
+=====
+You must first download and unpack a copy of the `Android NDK`_ and let Pigweed
+know where that is located using the ``pw_android_toolchain_NDK_PATH`` build
+arg.
+
+.. _Android NDK: https://developer.android.com/ndk
+
+You can set Pigweed build options using ``gn args out``.
+
+Building
+========
+To build for this Pigweed target, simply build the top-level "android" Ninja
+target. You can set Pigweed build options using ``gn args out`` or by running:
+
+.. code-block:: sh
+
+ gn gen out --args='
+ pw_android_toolchain_NDK_PATH="/path/to/android/ndk"'
+
+On a Windows machine it's easier to run:
+
+.. code-block:: sh
+
+ gn args out
+
+That will open a text file where you can paste the args in:
+
+.. code-block:: text
+
+ pw_android_toolchain_NDK_PATH = "/path/to/android/ndk"
+
+Save the file and close the text editor.
+
+Then build with:
+
+.. code-block:: sh
+
+ ninja -C out android
+
+This will build Pigweed for all supported Android CPU targets at the default
+optimization level, currently arm, arm64, x64, and x86.
+
+To build for a specific CPU target only, at the default optimization level:
+
+.. code-block:: sh
+
+ ninja -C out arm64_android
+
+Or to build for a specific CPU target and optimization level:
+
+.. code-block:: sh
+
+ ninja -C out arm64_android_size_optimized
diff --git a/targets/android/target_toolchains.gni b/targets/android/target_toolchains.gni
index ef1a6f700..7ec8ed0b6 100644
--- a/targets/android/target_toolchains.gni
+++ b/targets/android/target_toolchains.gni
@@ -28,8 +28,27 @@ if (pw_android_toolchain_NDK_PATH != "") {
# Facade backends
pw_assert_BACKEND = dir_pw_assert_basic
+ pw_chrono_SYSTEM_CLOCK_BACKEND = "$dir_pw_chrono_stl:system_clock"
+ pw_chrono_SYSTEM_TIMER_BACKEND = "$dir_pw_chrono_stl:system_timer"
pw_log_BACKEND = dir_pw_log_basic
+ pw_sync_BINARY_SEMAPHORE_BACKEND =
+ "$dir_pw_sync_stl:binary_semaphore_backend"
+ pw_sync_CONDITION_VARIABLE_BACKEND =
+ "$dir_pw_sync_stl:condition_variable_backend"
+ pw_sync_COUNTING_SEMAPHORE_BACKEND =
+ "$dir_pw_sync_stl:counting_semaphore_backend"
+ pw_sync_MUTEX_BACKEND = "$dir_pw_sync_stl:mutex_backend"
+ pw_sync_TIMED_MUTEX_BACKEND = "$dir_pw_sync_stl:timed_mutex_backend"
+ pw_sync_INTERRUPT_SPIN_LOCK_BACKEND = "$dir_pw_sync_stl:interrupt_spin_lock"
+ pw_sync_THREAD_NOTIFICATION_BACKEND =
+ "$dir_pw_sync:binary_semaphore_thread_notification_backend"
+ pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND =
+ "$dir_pw_sync:binary_semaphore_timed_thread_notification_backend"
pw_sys_io_BACKEND = dir_pw_sys_io_stdio
+ pw_thread_ID_BACKEND = "$dir_pw_thread_stl:id"
+ pw_thread_SLEEP_BACKEND = "$dir_pw_thread_stl:sleep"
+ pw_thread_THREAD_BACKEND = "$dir_pw_thread_stl:thread"
+ pw_thread_YIELD_BACKEND = "$dir_pw_thread_stl:yield"
pw_build_LINK_DEPS = []
pw_build_LINK_DEPS += [
diff --git a/targets/apollo4/BUILD.bazel b/targets/apollo4/BUILD.bazel
new file mode 100644
index 000000000..5f656707c
--- /dev/null
+++ b/targets/apollo4/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# This is just a stub to silence warnings saying that files are
+# missing from the bazel build.
+filegroup(
+ name = "apollo4_files",
+ srcs = [
+ "boot.cc",
+ "vector_table.c",
+ ],
+)
diff --git a/targets/apollo4/BUILD.gn b/targets/apollo4/BUILD.gn
new file mode 100644
index 000000000..fada56355
--- /dev/null
+++ b/targets/apollo4/BUILD.gn
@@ -0,0 +1,107 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pigweed/third_party/ambiq/ambiq.gni")
+import("$dir_pw_build/error.gni")
+import("$dir_pw_build/linker_script.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_cpu_exception/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_malloc/backend.gni")
+import("$dir_pw_system/backend.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("target_toolchains") {
+ toolchains = pw_target_toolchain_apollo4_list
+}
+
+pw_build_assert("check_ambiq_product_defined") {
+ condition = pw_third_party_ambiq_PRODUCT == "apollo4p" ||
+ pw_third_party_ambiq_PRODUCT == "apollo4b"
+ message = "Build argument pw_third_party_ambiq_PRODUCT must be one of " +
+ "the following values: 'apollo4p' or 'apollo4b'."
+ visibility = [ ":*" ]
+}
+
+config("disable_warnings") {
+ cflags = [ "-Wno-undef" ]
+ visibility = [ ":*" ]
+}
+
+config("pw_malloc_active") {
+ if (pw_malloc_BACKEND != "") {
+ defines = [ "PW_MALLOC_ACTIVE=1" ]
+ } else {
+ defines = [ "PW_MALLOC_ACTIVE=0" ]
+ }
+}
+
+config("pw_system_active") {
+ if (pw_system_TARGET_HOOKS_BACKEND != "") {
+ ldflags = [
+ "-Wl,--defsym=SysTick_Handler=xPortSysTickHandler",
+ "-Wl,--defsym=PendSV_Handler=xPortPendSVHandler",
+ "-Wl,--defsym=SVC_Handler=vPortSVCHandler",
+ ]
+ }
+}
+
+config("pw_cpu_exception_active") {
+ if (pw_cpu_exception_ENTRY_BACKEND != "") {
+ ldflags = [
+ "-Wl,--defsym=HardFault_Handler=pw_cpu_exception_Entry",
+ "-Wl,--defsym=MemManage_Handler=pw_cpu_exception_Entry",
+ "-Wl,--defsym=BusFault_Handler=pw_cpu_exception_Entry",
+ "-Wl,--defsym=UsageFault_Handler=pw_cpu_exception_Entry",
+ "-Wl,--defsym=NMI_Handler=pw_cpu_exception_Entry",
+ ]
+ }
+}
+
+if (current_toolchain != default_toolchain) {
+ pw_source_set("boot") {
+ public_configs = [ ":pw_malloc_active" ]
+
+ all_dependent_configs = [
+ ":pw_system_active",
+ ":pw_cpu_exception_active",
+ ]
+
+ public_deps = [ "$dir_pw_third_party/ambiq:sdk" ]
+
+ deps = [
+ ":check_ambiq_product_defined",
+ "$dir_pw_boot",
+ "$dir_pw_boot_cortex_m",
+ "$dir_pw_preprocessor",
+ "$dir_pw_sys_io_ambiq_sdk",
+ ]
+
+ if (pw_malloc_BACKEND != "") {
+ deps += [ "$dir_pw_malloc" ]
+ }
+
+ sources = [
+ "boot.cc",
+ "vector_table.c",
+ ]
+ }
+}
+
+pw_doc_group("target_docs") {
+ sources = [ "target_docs.rst" ]
+}
diff --git a/targets/apollo4/OWNERS b/targets/apollo4/OWNERS
new file mode 100644
index 000000000..0eb639f0f
--- /dev/null
+++ b/targets/apollo4/OWNERS
@@ -0,0 +1 @@
+elizarovv@google.com
diff --git a/targets/apollo4/apollo4_executable.gni b/targets/apollo4/apollo4_executable.gni
new file mode 100644
index 000000000..49878f22f
--- /dev/null
+++ b/targets/apollo4/apollo4_executable.gni
@@ -0,0 +1,33 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_malloc/backend.gni")
+
+# Executable wrapper that includes some baremetal startup code.
+template("apollo4_executable") {
+ target("executable", target_name) {
+ forward_variables_from(invoker, "*")
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ "$dir_pigweed/targets/apollo4:boot" ]
+ if (pw_malloc_BACKEND != "") {
+ if (!defined(configs)) {
+ configs = []
+ }
+ configs += [ "$dir_pw_malloc:pw_malloc_wrapper_config" ]
+ }
+ }
+}
diff --git a/targets/apollo4/boot.cc b/targets/apollo4/boot.cc
new file mode 100644
index 000000000..5072f692d
--- /dev/null
+++ b/targets/apollo4/boot.cc
@@ -0,0 +1,37 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_boot_cortex_m/boot.h"
+
+#include "pw_malloc/malloc.h"
+#include "pw_sys_io_ambiq_sdk/init.h"
+
+PW_EXTERN_C void pw_boot_PreStaticMemoryInit() {}
+
+PW_EXTERN_C void pw_boot_PreStaticConstructorInit() {
+ if constexpr (PW_MALLOC_ACTIVE == 1) {
+ pw_MallocInit(&pw_boot_heap_low_addr, &pw_boot_heap_high_addr);
+ }
+
+ pw_sys_io_Init();
+}
+
+PW_EXTERN_C void pw_boot_PreMainInit() {}
+
+PW_EXTERN_C PW_NO_RETURN void pw_boot_PostMain() {
+ // In case main() returns, just sit here until the device is reset.
+ while (true) {
+ }
+ PW_UNREACHABLE;
+}
diff --git a/targets/apollo4/target_docs.rst b/targets/apollo4/target_docs.rst
new file mode 100644
index 000000000..7462be0f4
--- /dev/null
+++ b/targets/apollo4/target_docs.rst
@@ -0,0 +1,165 @@
+.. _target-apollo4:
+
+===========================
+Ambiq Apollo4
+===========================
+-----
+Setup
+-----
+To use this target, Pigweed must be set up to use the `AmbiqSuite SDK`_ HAL
+for the Apollo4 series which can be downloaded from the Ambiq website.
+
+.. _AmbiqSuite SDK: https://ambiq.com/apollo4-blue-plus
+
+Once the AmbiqSuite SDK package has been downloaded and extracted, the user
+needs to set ``dir_pw_third_party_ambiq_SDK`` build arg to the location of
+extracted directory:
+
+.. code-block:: sh
+
+ $ gn args out
+
+Then add the following lines to that text file:
+
+.. code-block::
+
+ # Path to the extracted AmbiqSuite SDK package.
+ dir_pw_third_party_ambiq_SDK = "/path/to/AmbiqSuite_R4.3.0"
+
+Usage
+=====
+The Apollo4 is configured to output logs and test results over the UART to an
+on-board J-Link Debug Probe (Virtual COM Port) at a baud rate of 115200.
+
+Once the AmbiqSuite SDK is configured, the unit tests for the Apollo4 board
+can be build with a command:
+
+.. code-block:: sh
+
+ ninja -C out apollo4
+
+If using out as a build directory, tests will be located in out/apollo4/obj/[module name]/[test_name].elf.
+
+Flashing using SEGGER's J-Link
+==============================
+Flashing the Apollo4 board can be done using the J-Flash Lite GUI program or from
+a command line using ``JLinkExe`` program. The latter requires a script which
+describes the steps of programming. Here is an example bash script to flash
+an Apollo4 board using ``JLinkExe`` program:
+
+.. code-block:: sh
+
+ #!/bin/bash
+ function flash_jlink()
+ {
+ local TMP_FLASH_SCRIPT=/tmp/gdb-flash.txt
+
+ cat > $TMP_FLASH_SCRIPT <<- EOF
+ r
+ h
+ loadfile $1
+ r
+ q
+ EOF
+
+ JLinkExe -NoGui 1 -device AMAP42KK-KBR -if SWD -speed 4000 -autoconnect 1 -CommanderScript $TMP_FLASH_SCRIPT
+
+ rm "$TMP_FLASH_SCRIPT"
+ }
+
+ flash_jlink $@
+
+Then call this script:
+
+.. code-block:: sh
+
+ bash ./flash_amap4.sh ./out/apollo4_debug/obj/pw_log/test/basic_log_test.elf
+
+In this case the basic log test is debugged, but substitute your own ELF file.
+
+Debugging
+=========
+Debugging can be done using the on-board J-Link Debug Probe. First you need to
+start ``JLinkGDBServer`` and connect to the on-board J-Link Debug Probe.
+
+.. code-block:: sh
+
+ JLinkGDBServer -select USB \
+ -device AMAP42KK-KBR \
+ -endian little \
+ -if SWD \
+ -speed 4000 \
+ -noir -LocalhostOnly \
+ -singlerun \
+ -nogui \
+ -excdbg \
+ -rtos GDBServer/RTOSPlugin_FreeRTOS.dylib
+
+The ``-rtos`` option is for `Thread Aware Debugging`_.
+
+.. _Thread Aware Debugging: https://www.segger.com/products/debug-probes/j-link/tools/j-link-gdb-server/thread-aware-debugging/
+
+Then on the second terminal window use ``arm-none-eabi-gdb`` to load an executable
+into the target, debug, and run it.
+
+.. code-block:: sh
+
+ arm-none-eabi-gdb -q out/apollo4_debug/obj/pw_log/test/basic_log_test.elf
+
+This can be combined with a simple bash script. Here is an example of one:
+
+.. code-block:: sh
+
+ #!/bin/bash
+
+ function debug_jlink()
+ {
+ local TMP_GDB_SCRIPT=/tmp/gdb-debug.txt
+
+ # Create GDB script.
+
+ cat > $TMP_GDB_SCRIPT <<- EOF
+
+ # Backtrace all threads.
+
+ define btall
+ thread apply all backtrace
+ end
+
+ target remote localhost:2331
+ load
+ monitor reset
+ monitor halt
+ b pw_boot_Entry
+
+ EOF
+
+ # Start GDB server.
+
+ set -m
+ JLinkGDBServer -select USB \
+ -device AMAP42KK-KBR \
+ -endian little \
+ -if SWD \
+ -speed 4000 \
+ -noir -LocalhostOnly \
+ -singlerun \
+ -nogui \
+ -excdbg \
+ -rtos GDBServer/RTOSPlugin_FreeRTOS.dylib &
+ set +m
+
+ # Debug program.
+
+ arm-none-eabi-gdb -q $1 -x $TMP_GDB_SCRIPT
+
+ rm "$TMP_GDB_SCRIPT"
+ }
+
+ debug_jlink $@
+
+Then call this script:
+
+.. code-block:: sh
+
+ bash ./debug_amap4.sh ./out/apollo4_debug/obj/pw_log/test/basic_log_test.elf
diff --git a/targets/apollo4/target_toolchains.gni b/targets/apollo4/target_toolchains.gni
new file mode 100644
index 000000000..e614f71da
--- /dev/null
+++ b/targets/apollo4/target_toolchains.gni
@@ -0,0 +1,149 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_log/backend.gni")
+import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
+
+_target_config = {
+ pw_third_party_ambiq_PRODUCT = "apollo4p"
+
+ # Use the logging main.
+ pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+
+ # Use ARM Cycle Counts
+ pw_perf_test_TIMER_INTERFACE_BACKEND = "$dir_pw_perf_test:arm_cortex_timer"
+
+ # Configuration options for Pigweed executable targets.
+ pw_build_EXECUTABLE_TARGET_TYPE = "apollo4_executable"
+
+ pw_build_EXECUTABLE_TARGET_TYPE_FILE =
+ get_path_info("apollo4_executable.gni", "abspath")
+
+ # Path to the bloaty config file for the output binaries.
+ pw_bloat_BLOATY_CONFIG = "$dir_pw_boot_cortex_m/bloaty_config.bloaty"
+
+ # Configure backend for assert facade.
+ pw_assert_BACKEND = dir_pw_assert_basic
+
+ pw_boot_BACKEND = "$dir_pw_boot_cortex_m"
+ pw_cpu_exception_ENTRY_BACKEND =
+ "$dir_pw_cpu_exception_cortex_m:cpu_exception"
+ pw_cpu_exception_HANDLER_BACKEND = "$dir_pw_cpu_exception:basic_handler"
+ pw_cpu_exception_SUPPORT_BACKEND = "$dir_pw_cpu_exception_cortex_m:support"
+ pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
+ "$dir_pw_sync_baremetal:interrupt_spin_lock"
+
+ # Configure backends for pw_sync's facades.
+ pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+
+ # Configure backend for logging facade.
+ pw_log_BACKEND = "$dir_pw_log_basic"
+
+ # Configure backend for pw_sys_io facade.
+ pw_sys_io_BACKEND = "$dir_pw_sys_io_ambiq_sdk"
+
+ # Configure backend for pw_rpc_system_server.
+ pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
+ pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
+
+ pw_malloc_BACKEND = "$dir_pw_malloc_freelist"
+
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x00018000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+
+ "PW_BOOT_FLASH_BEGIN=0x00018200",
+ "PW_BOOT_FLASH_SIZE=1951K",
+
+ "PW_BOOT_HEAP_SIZE=100K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+
+ "PW_BOOT_RAM_BEGIN=0x10000000",
+ "PW_BOOT_RAM_SIZE=1408K",
+ ]
+
+ pw_build_LINK_DEPS = [] # Explicit list overwrite required by GN
+ pw_build_LINK_DEPS = [
+ "$dir_pw_assert:impl",
+ "$dir_pw_log:impl",
+ "$dir_pw_cpu_exception:entry_impl",
+ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support",
+ ]
+
+ current_cpu = "arm"
+ current_os = ""
+}
+
+_toolchain_properties = {
+ final_binary_extension = ".elf"
+}
+
+_target_default_configs = [
+ "$dir_pw_build:extra_strict_warnings",
+ "$dir_pw_toolchain/arm_gcc:enable_float_printf",
+]
+
+pw_target_toolchain_apollo4 = {
+ _excluded_members = [
+ "defaults",
+ "name",
+ ]
+
+ debug = {
+ name = "apollo4_debug"
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_debug
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ forward_variables_from(_toolchain_properties, "*")
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_target_config, "*")
+ default_configs += _target_default_configs
+ }
+ }
+
+ speed_optimized = {
+ name = "apollo4_speed_optimized"
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_speed_optimized
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ forward_variables_from(_toolchain_properties, "*")
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_target_config, "*")
+ default_configs += _target_default_configs
+ }
+ }
+
+ size_optimized = {
+ name = "apollo4_size_optimized"
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_size_optimized
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ forward_variables_from(_toolchain_properties, "*")
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_target_config, "*")
+ default_configs += _target_default_configs
+ }
+ }
+}
+
+# This list just contains the members of the above scope for convenience to make
+# it trivial to generate all the toolchains in this file via a
+# `generate_toolchains` target.
+pw_target_toolchain_apollo4_list = [
+ pw_target_toolchain_apollo4.debug,
+ pw_target_toolchain_apollo4.speed_optimized,
+ pw_target_toolchain_apollo4.size_optimized,
+]
diff --git a/targets/apollo4/vector_table.c b/targets/apollo4/vector_table.c
new file mode 100644
index 000000000..a05e15366
--- /dev/null
+++ b/targets/apollo4/vector_table.c
@@ -0,0 +1,222 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <stdbool.h>
+
+#include "pw_boot/boot.h"
+#include "pw_boot_cortex_m/boot.h"
+#include "pw_preprocessor/compiler.h"
+
+// Default handler to insert into the ARMv7-M vector table (below).
+// This function exists for convenience. If a device isn't doing what you
+// expect, it might have hit a fault and ended up here.
+static void DefaultFaultHandler(void) {
+ while (true) {
+ // Wait for debugger to attach.
+ }
+}
+
+// This typedef is for convenience when building the vector table. With the
+// exception of SP_main (0th entry in the vector table), all the entries of the
+// vector table are function pointers.
+typedef void (*InterruptHandler)(void);
+
+void SVC_Handler(void) PW_ALIAS(DefaultFaultHandler);
+void PendSV_Handler(void) PW_ALIAS(DefaultFaultHandler);
+void SysTick_Handler(void) PW_ALIAS(DefaultFaultHandler);
+
+void MemManage_Handler(void) PW_ALIAS(DefaultFaultHandler);
+void BusFault_Handler(void) PW_ALIAS(DefaultFaultHandler);
+void UsageFault_Handler(void) PW_ALIAS(DefaultFaultHandler);
+void DebugMon_Handler(void) PW_ALIAS(DefaultFaultHandler);
+
+void am_brownout_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_watchdog_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_rtc_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_vcomp_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_ioslave_ios_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_ioslave_acc_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster0_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster1_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster2_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster3_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster4_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster5_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster6_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_iomaster7_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_ctimer_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_uart_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_uart1_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_uart2_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_uart3_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_adc_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_mspi0_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_mspi1_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_mspi2_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_clkgen_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_cryptosec_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_sdio_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_usb_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpu_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_disp_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_dsi_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr0_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr1_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr2_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr3_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr4_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr5_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr6_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimer_cmpr7_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_stimerof_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_audadc0_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_dspi2s0_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_dspi2s1_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_dspi2s2_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_dspi2s3_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_pdm0_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_pdm1_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_pdm2_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_pdm3_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio0_001f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio0_203f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio0_405f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio0_607f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio1_001f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio1_203f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio1_405f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_gpio1_607f_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer00_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer01_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer02_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer03_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer04_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer05_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer06_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer07_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer08_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer09_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer10_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer11_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer12_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer13_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer14_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_timer15_isr(void) PW_ALIAS(DefaultFaultHandler);
+void am_cachecpu_isr(void) PW_ALIAS(DefaultFaultHandler);
+
+PW_KEEP_IN_SECTION(".vector_table")
+const InterruptHandler vector_table[] = {
+ // Cortex-M CPU specific interrupt handlers.
+ (InterruptHandler)(&pw_boot_stack_high_addr),
+ pw_boot_Entry, // The reset handler
+ DefaultFaultHandler, // The NMI handler
+ DefaultFaultHandler, // The hard fault handler
+ DefaultFaultHandler, // The MemManage_Handler
+ DefaultFaultHandler, // The BusFault_Handler
+ DefaultFaultHandler, // The UsageFault_Handler
+ 0, // Reserved
+ 0, // Reserved
+ 0, // Reserved
+ 0, // Reserved
+ SVC_Handler, // SVCall handler
+ DefaultFaultHandler, // Debug monitor handler
+ 0, // Reserved
+ PendSV_Handler, // The PendSV handler
+ SysTick_Handler, // The SysTick handler
+ // Vendor specific peripheral interrupt handlers.
+ am_brownout_isr, // 0: Brownout (rstgen)
+ am_watchdog_isr, // 1: Watchdog (WDT)
+ am_rtc_isr, // 2: RTC
+ am_vcomp_isr, // 3: Voltage Comparator
+ am_ioslave_ios_isr, // 4: I/O Responder general
+ am_ioslave_acc_isr, // 5: I/O Responder access
+ am_iomaster0_isr, // 6: I/O Controller 0
+ am_iomaster1_isr, // 7: I/O Controller 1
+ am_iomaster2_isr, // 8: I/O Controller 2
+ am_iomaster3_isr, // 9: I/O Controller 3
+ am_iomaster4_isr, // 10: I/O Controller 4
+ am_iomaster5_isr, // 11: I/O Controller 5
+ am_iomaster6_isr, // 12: I/O Controller 6 (I3C/I2C/SPI)
+ am_iomaster7_isr, // 13: I/O Controller 7 (I3C/I2C/SPI)
+ am_ctimer_isr, // 14: OR of all timerX interrupts
+ am_uart_isr, // 15: UART0
+ am_uart1_isr, // 16: UART1
+ am_uart2_isr, // 17: UART2
+ am_uart3_isr, // 18: UART3
+ am_adc_isr, // 19: ADC
+ am_mspi0_isr, // 20: MSPI0
+ am_mspi1_isr, // 21: MSPI1
+ am_mspi2_isr, // 22: MSPI2
+ am_clkgen_isr, // 23: ClkGen
+ am_cryptosec_isr, // 24: Crypto Secure
+ DefaultFaultHandler, // 25: Reserved
+ am_sdio_isr, // 26: SDIO
+ am_usb_isr, // 27: USB
+ am_gpu_isr, // 28: GPU
+ am_disp_isr, // 29: DISP
+ am_dsi_isr, // 30: DSI
+ DefaultFaultHandler, // 31: Reserved
+ am_stimer_cmpr0_isr, // 32: System Timer Compare0
+ am_stimer_cmpr1_isr, // 33: System Timer Compare1
+ am_stimer_cmpr2_isr, // 34: System Timer Compare2
+ am_stimer_cmpr3_isr, // 35: System Timer Compare3
+ am_stimer_cmpr4_isr, // 36: System Timer Compare4
+ am_stimer_cmpr5_isr, // 37: System Timer Compare5
+ am_stimer_cmpr6_isr, // 38: System Timer Compare6
+ am_stimer_cmpr7_isr, // 39: System Timer Compare7
+ am_stimerof_isr, // 40: System Timer Cap Overflow
+ DefaultFaultHandler, // 41: Reserved
+ am_audadc0_isr, // 42: Audio ADC
+ DefaultFaultHandler, // 43: Reserved
+ am_dspi2s0_isr, // 44: I2S0
+ am_dspi2s1_isr, // 45: I2S1
+ am_dspi2s2_isr, // 46: I2S2
+ am_dspi2s3_isr, // 47: I2S3
+ am_pdm0_isr, // 48: PDM0
+ am_pdm1_isr, // 49: PDM1
+ am_pdm2_isr, // 50: PDM2
+ am_pdm3_isr, // 51: PDM3
+ DefaultFaultHandler, // 52: Reserved
+ DefaultFaultHandler, // 53: Reserved
+ DefaultFaultHandler, // 54: Reserved
+ DefaultFaultHandler, // 55: Reserved
+ am_gpio0_001f_isr, // 56: GPIO N0 pins 0-31
+ am_gpio0_203f_isr, // 57: GPIO N0 pins 32-63
+ am_gpio0_405f_isr, // 58: GPIO N0 pins 64-95
+ am_gpio0_607f_isr, // 59: GPIO N0 pins 96-104, virtual 105-127
+ am_gpio1_001f_isr, // 60: GPIO N1 pins 0-31
+ am_gpio1_203f_isr, // 61: GPIO N1 pins 32-63
+ am_gpio1_405f_isr, // 62: GPIO N1 pins 64-95
+ am_gpio1_607f_isr, // 63: GPIO N1 pins 96-104, virtual 105-127
+ DefaultFaultHandler, // 64: Reserved
+ DefaultFaultHandler, // 65: Reserved
+ DefaultFaultHandler, // 66: Reserved
+ am_timer00_isr, // 67: timer0
+ am_timer01_isr, // 68: timer1
+ am_timer02_isr, // 69: timer2
+ am_timer03_isr, // 70: timer3
+ am_timer04_isr, // 71: timer4
+ am_timer05_isr, // 72: timer5
+ am_timer06_isr, // 73: timer6
+ am_timer07_isr, // 74: timer7
+ am_timer08_isr, // 75: timer8
+ am_timer09_isr, // 76: timer9
+ am_timer10_isr, // 77: timer10
+ am_timer11_isr, // 78: timer11
+ am_timer12_isr, // 79: timer12
+ am_timer13_isr, // 80: timer13
+ am_timer14_isr, // 81: timer14
+ am_timer15_isr, // 82: timer15
+ am_cachecpu_isr // 83: CPU cache
+};
diff --git a/targets/apollo4_pw_system/BUILD.bazel b/targets/apollo4_pw_system/BUILD.bazel
new file mode 100644
index 000000000..d83f06e8b
--- /dev/null
+++ b/targets/apollo4_pw_system/BUILD.bazel
@@ -0,0 +1,27 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# This is just a stub to silence warnings saying that files are
+# missing from the bazel build.
+filegroup(
+ name = "apollo4_pw_system_files",
+ srcs = [
+ "config/FreeRTOSConfig.h",
+ "main.cc",
+ ],
+)
diff --git a/targets/apollo4_pw_system/BUILD.gn b/targets/apollo4_pw_system/BUILD.gn
new file mode 100644
index 000000000..d37d20a1b
--- /dev/null
+++ b/targets/apollo4_pw_system/BUILD.gn
@@ -0,0 +1,87 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_system/system_target.gni")
+
+config("config_includes") {
+ include_dirs = [ "config" ]
+}
+
+if (current_toolchain != default_toolchain) {
+ pw_source_set("main") {
+ deps = [
+ "$dir_pigweed/targets/apollo4:boot",
+ "$dir_pw_system",
+ "$dir_pw_third_party/freertos",
+ ]
+
+ sources = [ "main.cc" ]
+ }
+
+ pw_source_set("apollo4_freertos_config") {
+ public_configs = [ ":config_includes" ]
+ public_deps = [ "$dir_pw_third_party/freertos:config_assert" ]
+ public = [ "config/FreeRTOSConfig.h" ]
+ }
+}
+
+pw_system_target("apollo4_pw_system") {
+ cpu = PW_SYSTEM_CPU.CORTEX_M4F
+ scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
+
+ build_args = {
+ pw_third_party_ambiq_PRODUCT = "apollo4p"
+ pw_log_BACKEND = dir_pw_log_tokenized
+
+ pw_third_party_freertos_CONFIG =
+ "$dir_pigweed/targets/apollo4_pw_system:apollo4_freertos_config"
+ pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
+ pw_sys_io_BACKEND = "$dir_pw_sys_io_ambiq_sdk"
+ pw_log_BACKEND = "$dir_pw_log_basic"
+ pw_cpu_exception_ENTRY_BACKEND =
+ "$dir_pw_cpu_exception_cortex_m:cpu_exception"
+ pw_cpu_exception_HANDLER_BACKEND = "$dir_pw_cpu_exception:basic_handler"
+ pw_cpu_exception_SUPPORT_BACKEND = "$dir_pw_cpu_exception_cortex_m:support"
+
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x00018000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+
+ "PW_BOOT_FLASH_BEGIN=0x00018200",
+ "PW_BOOT_FLASH_SIZE=1951K",
+
+ "PW_BOOT_HEAP_SIZE=100K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+
+ "PW_BOOT_RAM_BEGIN=0x10000000",
+ "PW_BOOT_RAM_SIZE=1408K",
+ ]
+
+ pw_build_LINK_DEPS += [
+ "$dir_pigweed/targets/apollo4_pw_system:main",
+ "$dir_pw_assert:impl",
+ "$dir_pw_log:impl",
+ "$dir_pw_cpu_exception:entry_impl",
+ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support",
+ ]
+ }
+}
+
+pw_doc_group("target_docs") {
+ sources = [ "target_docs.rst" ]
+}
diff --git a/targets/apollo4_pw_system/OWNERS b/targets/apollo4_pw_system/OWNERS
new file mode 100644
index 000000000..0eb639f0f
--- /dev/null
+++ b/targets/apollo4_pw_system/OWNERS
@@ -0,0 +1 @@
+elizarovv@google.com
diff --git a/targets/apollo4_pw_system/config/FreeRTOSConfig.h b/targets/apollo4_pw_system/config/FreeRTOSConfig.h
new file mode 100644
index 000000000..8ccd4c97d
--- /dev/null
+++ b/targets/apollo4_pw_system/config/FreeRTOSConfig.h
@@ -0,0 +1,81 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <stdint.h>
+
+// Externally defined variables that must be forward-declared for FreeRTOS to
+// use them.
+extern uint32_t SystemCoreClock;
+
+// Disable formatting to make it easier to compare with other config files.
+// clang-format off
+
+#define configUSE_PREEMPTION 1
+#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
+
+#define configSUPPORT_STATIC_ALLOCATION 1
+#define configSUPPORT_DYNAMIC_ALLOCATION 0
+
+#define configCPU_CLOCK_HZ SystemCoreClock
+#define configTICK_RATE_HZ 1000
+#define configMAX_PRIORITIES 16
+#define configMINIMAL_STACK_SIZE 128
+
+#define configMAX_TASK_NAME_LEN 16
+#define configUSE_16_BIT_TICKS 0
+
+#define configUSE_MUTEXES 1
+#define configUSE_RECURSIVE_MUTEXES 0
+#define configUSE_COUNTING_SEMAPHORES 1
+#define configQUEUE_REGISTRY_SIZE 8
+#define configUSE_QUEUE_SETS 0
+#define configUSE_NEWLIB_REENTRANT 0
+#define configENABLE_BACKWARD_COMPATIBILITY 0
+#define configRECORD_STACK_HIGH_ADDRESS 1
+
+#define configUSE_IDLE_HOOK 0
+#define configUSE_TICK_HOOK 0
+#define configCHECK_FOR_STACK_OVERFLOW 2
+#define configUSE_MALLOC_FAILED_HOOK 1
+
+#define configGENERATE_RUN_TIME_STATS 0
+#define configUSE_TRACE_FACILITY 0
+
+#define configUSE_TIMERS 1
+#define configTIMER_TASK_PRIORITY 3
+#define configTIMER_QUEUE_LENGTH 10
+#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE
+
+#define configKERNEL_INTERRUPT_PRIORITY (0x7 << 5)
+#define configMAX_SYSCALL_INTERRUPT_PRIORITY (0x4 << 5)
+#define NVIC_configKERNEL_INTERRUPT_PRIORITY (0x7)
+#define NVIC_configMAX_SYSCALL_INTERRUPT_PRIORITY (0x4)
+
+/* Optional functions - most linkers will remove unused functions anyway. */
+#define INCLUDE_vTaskPrioritySet 1
+#define INCLUDE_uxTaskPriorityGet 1
+#define INCLUDE_vTaskDelete 1
+#define INCLUDE_vTaskSuspend 1
+#define INCLUDE_xResumeFromISR 0
+#define INCLUDE_vTaskDelayUntil 0
+#define INCLUDE_vTaskDelay 1
+#define INCLUDE_xTaskGetSchedulerState 1
+#define INCLUDE_xTaskGetCurrentTaskHandle 1
+#define INCLUDE_uxTaskGetStackHighWaterMark2 1
+#define INCLUDE_uxTaskGetStackHighWaterMark 1
+
+// Instead of defining configASSERT(), include a header that provides a
+// definition that redirects to pw_assert.
+#include "pw_third_party/freertos/config_assert.h"
diff --git a/targets/apollo4_pw_system/main.cc b/targets/apollo4_pw_system/main.cc
new file mode 100644
index 000000000..35d383f3e
--- /dev/null
+++ b/targets/apollo4_pw_system/main.cc
@@ -0,0 +1,74 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#define PW_LOG_MODULE_NAME "pw_system"
+
+#include "FreeRTOS.h"
+#include "pw_log/log.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_string/util.h"
+#include "pw_system/init.h"
+#include "task.h"
+
+// System core clock value definition, usually provided by the CMSIS package.
+uint32_t SystemCoreClock = 96'000'000ul;
+
+namespace {
+
+#if configCHECK_FOR_STACK_OVERFLOW != 0
+std::array<char, configMAX_TASK_NAME_LEN> temp_thread_name_buffer;
+#endif // configCHECK_FOR_STACK_OVERFLOW
+
+#if configUSE_TIMERS == 1
+std::array<StackType_t, configTIMER_TASK_STACK_DEPTH> freertos_timer_stack;
+StaticTask_t freertos_timer_tcb;
+#endif // configUSE_TIMERS == 1
+
+std::array<StackType_t, configMINIMAL_STACK_SIZE> freertos_idle_stack;
+StaticTask_t freertos_idle_tcb;
+} // namespace
+
+#if configCHECK_FOR_STACK_OVERFLOW != 0
+PW_EXTERN_C void vApplicationStackOverflowHook(TaskHandle_t, char* pcTaskName) {
+ pw::string::Copy(pcTaskName, temp_thread_name_buffer);
+ PW_CRASH("Stack OVF for task %s", temp_thread_name_buffer.data());
+}
+#endif // configCHECK_FOR_STACK_OVERFLOW
+
+#if configUSE_TIMERS == 1
+PW_EXTERN_C void vApplicationGetTimerTaskMemory(
+ StaticTask_t** ppxTimerTaskTCBBuffer,
+ StackType_t** ppxTimerTaskStackBuffer,
+ uint32_t* pulTimerTaskStackSize) {
+ *ppxTimerTaskTCBBuffer = &freertos_timer_tcb;
+ *ppxTimerTaskStackBuffer = freertos_timer_stack.data();
+ *pulTimerTaskStackSize = freertos_timer_stack.size();
+}
+#endif // configUSE_TIMERS == 1
+
+PW_EXTERN_C void vApplicationGetIdleTaskMemory(
+ StaticTask_t** ppxIdleTaskTCBBuffer,
+ StackType_t** ppxIdleTaskStackBuffer,
+ uint32_t* pulIdleTaskStackSize) {
+ *ppxIdleTaskTCBBuffer = &freertos_idle_tcb;
+ *ppxIdleTaskStackBuffer = freertos_idle_stack.data();
+ *pulIdleTaskStackSize = freertos_idle_stack.size();
+}
+
+int main() {
+ pw::system::Init();
+ vTaskStartScheduler();
+
+ PW_UNREACHABLE;
+}
diff --git a/targets/apollo4_pw_system/target_docs.rst b/targets/apollo4_pw_system/target_docs.rst
new file mode 100644
index 000000000..6877360df
--- /dev/null
+++ b/targets/apollo4_pw_system/target_docs.rst
@@ -0,0 +1,111 @@
+.. _target-apollo4-pw-system:
+
+============================
+Ambiq Apollo4 with pw_system
+============================
+
+.. warning::
+
+ This target is in a very preliminary state and is under active development.
+ This demo gives a preview of the direction we are heading with
+ :ref:`pw_system<module-pw_system>`, but it is not yet ready for production
+ use.
+
+This target configuration uses :ref:`pw_system<module-pw_system>` on top of
+FreeRTOS and the `AmbiqSuite SDK
+<https://ambiq.com/apollo4-blue-plus>`_ HAL.
+
+-----
+Setup
+-----
+To use this target, Pigweed must be set up to use FreeRTOS and the AmbiqSuite
+SDK HAL for the Apollo4 series. The FreeRTOS repository can be downloaded via
+``pw package``, and the `AmbiqSuite SDK`_ can be downloaded from the Ambiq
+website.
+
+.. _AmbiqSuite SDK: https://ambiq.com/apollo4-blue-plus
+
+Once the AmbiqSuite SDK package has been downloaded and extracted, the user
+needs to set ``dir_pw_third_party_ambiq_SDK`` build arg to the location of
+extracted directory:
+
+.. code-block:: sh
+
+ $ gn args out
+
+Then add the following lines to that text file:
+
+.. code-block::
+
+ # Path to the extracted AmbiqSuite SDK package.
+ dir_pw_third_party_ambiq_SDK = "/path/to/AmbiqSuite_R4.3.0"
+
+ # Path to the FreeRTOS source directory.
+ dir_pw_third_party_freertos = "/path/to/pigweed/third_party/freertos"
+
+-----------------------------
+Building and Running the Demo
+-----------------------------
+This target has an associated demo application that can be built and then
+flashed to a device with the following commands:
+
+.. code-block:: sh
+
+ ninja -C out pw_system_demo
+
+.. seealso::
+
+ The :ref:`target-apollo4` for more info on flashing the Apollo4 board.
+
+Once the board has been flashed, you can connect to it and send RPC commands
+via the Pigweed console:
+
+.. code-block:: sh
+
+ pw-system-console -d /dev/{ttyX} -b 115200 \
+ --proto-globs pw_rpc/echo.proto \
+ --token-databases \
+ out/apollo4_pw_system.size_optimized/obj/pw_system/bin/system_example.elf
+
+Replace ``{ttyX}`` with the appropriate device on your machine. On Linux this
+may look like ``ttyACM0``, and on a Mac it may look like ``cu.usbmodem***``.
+
+When the console opens, try sending an Echo RPC request. You should get back
+the same message you sent to the device.
+
+.. code-block:: pycon
+
+ >>> device.rpcs.pw.rpc.EchoService.Echo(msg="Hello, Pigweed!")
+ (Status.OK, pw.rpc.EchoMessage(msg='Hello, Pigweed!'))
+
+You can also try out our thread snapshot RPC service, which should return a
+stack usage overview of all running threads on the device in Host Logs.
+
+.. code-block:: pycon
+
+ >>> device.rpcs.pw.thread.proto.ThreadSnapshotService.GetPeakStackUsage()
+
+Example output:
+
+.. code-block::
+
+ 20220826 09:47:22 INF PendingRpc(channel=1, method=pw.thread.ThreadSnapshotService.GetPeakStackUsage) completed: Status.OK
+ 20220826 09:47:22 INF Thread State
+ 20220826 09:47:22 INF 5 threads running.
+ 20220826 09:47:22 INF
+ 20220826 09:47:22 INF Thread (UNKNOWN): IDLE
+ 20220826 09:47:22 INF Est CPU usage: unknown
+ 20220826 09:47:22 INF Stack info
+ 20220826 09:47:22 INF Current usage: 0x20002da0 - 0x???????? (size unknown)
+ 20220826 09:47:22 INF Est peak usage: 390 bytes, 76.77%
+ 20220826 09:47:22 INF Stack limits: 0x20002da0 - 0x20002ba4 (508 bytes)
+ 20220826 09:47:22 INF
+ 20220826 09:47:22 INF ...
+
+You are now up and running!
+
+.. seealso::
+
+ The :ref:`module-pw_console`
+ :bdg-ref-primary-line:`module-pw_console-user_guide` for more info on using
+ the the pw_console UI.
diff --git a/targets/arduino/BUILD.bazel b/targets/arduino/BUILD.bazel
index 07e2150c0..39c178e95 100644
--- a/targets/arduino/BUILD.bazel
+++ b/targets/arduino/BUILD.bazel
@@ -28,7 +28,7 @@ pw_cc_library(
],
hdrs = [
],
- # TODO(b/259149817): pw_sys_io_arduino cannot find "Arduino.h"
+ # TODO: b/259149817 - pw_sys_io_arduino cannot find "Arduino.h"
tags = ["manual"],
deps = [
"//pw_arduino_build:pw_arduino_build_header",
diff --git a/targets/arduino/BUILD.gn b/targets/arduino/BUILD.gn
index 2f62200c2..5d0fd1d69 100644
--- a/targets/arduino/BUILD.gn
+++ b/targets/arduino/BUILD.gn
@@ -77,14 +77,20 @@ if (pw_arduino_build_CORE_PATH != "") {
"{object_files}",
])
- # TODO(tonymd): Determine if libs are needed.
- # Teensy4 core recipe uses: '-larm_cortexM7lfsp_math -lm -lstdc++'
+ # Teensy4 core recipe uses: '-larm_cortexM7lfsp_math -lm -lstdc++'
libs = filter_exclude(
exec_script(arduino_builder_script,
arduino_show_command_args + [ "--ld-lib-names" ],
"list lines"),
- # Exclude stdc++ which causes linking errors for teensy cores.
- [ "\bstdc++\b" ])
+ [
+ # Exclude stdc++ and precompiled math libraries which causes
+ # linking errors for teensy cores.
+ "\bstdc++\b",
+ "\barm_cortexM0l_math\b",
+ "\barm_cortexM4lf_math\b",
+ "\barm_cortexM4l_math\b",
+ "\barm_cortexM7lfsp_math\b",
+ ])
}
pw_source_set("pre_init") {
diff --git a/targets/arduino/target_docs.rst b/targets/arduino/target_docs.rst
index 379da1b5a..1735e6789 100644
--- a/targets/arduino/target_docs.rst
+++ b/targets/arduino/target_docs.rst
@@ -51,100 +51,101 @@ Installing Arduino Cores
The ``arduino_builder`` utility can install Arduino cores automatically. It's
recommended to install them to into ``third_party/arduino/cores/``.
-.. code:: sh
+.. code-block:: sh
- # Setup pigweed environment.
- source activate.sh
- # Install an arduino core
- arduino_builder install-core --prefix ./third_party/arduino/cores/ --core-name teensy
+ # Setup pigweed environment.
+ . ./activate.sh
+ # Install an arduino core, only teensy is supported
+ pw package install teensy
Building
========
To build for this Pigweed target, simply build the top-level "arduino" Ninja
target. You can set Arduino build options using ``gn args out`` or by running:
-.. code:: sh
+.. code-block:: sh
- gn gen out --args='
- pw_arduino_build_CORE_PATH="//third_party/arduino/cores"
- pw_arduino_build_CORE_NAME="teensy"
- pw_arduino_build_PACKAGE_NAME="teensy/avr"
- pw_arduino_build_BOARD="teensy40"
- pw_arduino_build_MENU_OPTIONS=["menu.usb.serial", "menu.keys.en-us"]'
+ gn gen out --args='
+ pw_arduino_build_CORE_PATH = "//environment/packages"
+ pw_arduino_build_CORE_NAME = "teensy"
+ pw_arduino_build_PACKAGE_NAME = "avr/1.58.1"
+ pw_arduino_build_BOARD = "teensy40"
+ pw_arduino_build_MENU_OPTIONS=["menu.usb.serial", "menu.keys.en-us"]'
On a Windows machine it's easier to run:
-.. code:: sh
+.. code-block:: sh
- gn args out
+ gn args out
That will open a text file where you can paste the args in:
-.. code:: text
+.. code-block:: text
- pw_arduino_build_CORE_PATH = "//third_party/arduino/cores"
- pw_arduino_build_CORE_NAME = "teensy"
- pw_arduino_build_PACKAGE_NAME="teensy/avr"
- pw_arduino_build_BOARD = "teensy40"
- pw_arduino_build_MENU_OPTIONS = ["menu.usb.serial", "menu.keys.en-us"]
+ pw_arduino_build_CORE_PATH = "//environment/packages"
+ pw_arduino_build_CORE_NAME = "teensy"
+ pw_arduino_build_PACKAGE_NAME = "avr/1.58.1"
+ pw_arduino_build_BOARD = "teensy40"
+ pw_arduino_build_MENU_OPTIONS = ["menu.usb.serial", "menu.keys.en-us"]
Save the file and close the text editor.
Then build with:
-.. code:: sh
+.. code-block:: sh
ninja -C out arduino
To see supported boards and Arduino menu options for a given core:
-.. code:: sh
+.. code-block:: sh
- arduino_builder --arduino-package-path ./third_party/arduino/cores/teensy \
- --arduino-package-name teensy/avr \
- list-boards
+ arduino_builder --arduino-package-path ./environment/packages/teensy \
+ --arduino-package-name avr/1.58.1 \
+ list-boards
-.. code:: text
+.. code-block:: text
- Board Name Description
- teensy41 Teensy 4.1
- teensy40 Teensy 4.0
- teensy36 Teensy 3.6
- teensy35 Teensy 3.5
- teensy31 Teensy 3.2 / 3.1
+ Board Name Description
+ teensy41 Teensy 4.1
+ teensy40 Teensy 4.0
+ teensy36 Teensy 3.6
+ teensy35 Teensy 3.5
+ teensy31 Teensy 3.2 / 3.1
You may wish to set different arduino build options in
``pw_arduino_build_MENU_OPTIONS``. Run this to see what's available for your core:
-.. code:: sh
+.. code-block:: sh
- arduino_builder --arduino-package-path ./third_party/arduino/cores/teensy \
- --arduino-package-name teensy/avr \
- list-menu-options --board teensy40
+ arduino_builder --arduino-package-path ./environment/packages/teensy \
+ --arduino-package-name avr/1.58.1 \
+ list-menu-options \
+ --board teensy40
That will show all menu options that can be added to ``gn args out``.
-.. code:: text
-
- All Options
- ----------------------------------------------------------------
- menu.usb.serial Serial
- menu.usb.serial2 Dual Serial
- menu.usb.serial3 Triple Serial
- menu.usb.keyboard Keyboard
- menu.usb.touch Keyboard + Touch Screen
- menu.usb.hidtouch Keyboard + Mouse + Touch Screen
- menu.usb.hid Keyboard + Mouse + Joystick
- menu.usb.serialhid Serial + Keyboard + Mouse + Joystick
- menu.usb.midi MIDI
- ...
-
- Default Options
- --------------------------------------
- menu.usb.serial Serial
- menu.speed.600 600 MHz
- menu.opt.o2std Faster
- menu.keys.en-us US English
+.. code-block:: text
+
+ All Options
+ ----------------------------------------------------------------
+ menu.usb.serial Serial
+ menu.usb.serial2 Dual Serial
+ menu.usb.serial3 Triple Serial
+ menu.usb.keyboard Keyboard
+ menu.usb.touch Keyboard + Touch Screen
+ menu.usb.hidtouch Keyboard + Mouse + Touch Screen
+ menu.usb.hid Keyboard + Mouse + Joystick
+ menu.usb.serialhid Serial + Keyboard + Mouse + Joystick
+ menu.usb.midi MIDI
+ ...
+
+ Default Options
+ --------------------------------------
+ menu.usb.serial Serial
+ menu.speed.600 600 MHz
+ menu.opt.o2std Faster
+ menu.keys.en-us US English
Testing
=======
@@ -159,23 +160,23 @@ If using ``out`` as a build directory, tests will be located in
Tests can be flashed and run using the `arduino_unit_test_runner` tool. Here is
a sample bash script to run all tests on a Linux machine.
-.. code:: sh
+.. code-block:: sh
- #!/bin/bash
- gn gen out --export-compile-commands \
- --args='pw_arduino_build_CORE_PATH="//third_party/arduino/cores"
- pw_arduino_build_CORE_NAME="teensy"
- pw_arduino_build_PACKAGE_NAME="teensy/avr"
- pw_arduino_build_BOARD="teensy40"
- pw_arduino_build_MENU_OPTIONS=["menu.usb.serial", "menu.keys.en-us"]' && \
- ninja -C out arduino
+ #!/bin/bash
+ gn gen out --export-compile-commands \
+ --args='pw_arduino_build_CORE_PATH="environment/packages"
+ pw_arduino_build_CORE_NAME="teensy"
+ pw_arduino_build_PACKAGE_NAME="avr/1.58.1"
+ pw_arduino_build_BOARD="teensy40"
+ pw_arduino_build_MENU_OPTIONS=["menu.usb.serial", "menu.keys.en-us"]' && \
+ ninja -C out arduino
- for f in $(find out/arduino_debug/obj/ -iname "*.elf"); do
- arduino_unit_test_runner --verbose \
- --config-file ./out/arduino_debug/gen/arduino_builder_config.json \
- --upload-tool teensyloader \
- out/arduino_debug/obj/pw_string/test/format_test.elf
- done
+ for f in $(find out/arduino_debug/obj/ -iname "*.elf"); do
+ arduino_unit_test_runner --verbose \
+ --config-file ./out/arduino_debug/gen/arduino_builder_config.json \
+ --upload-tool teensyloader \
+ $f
+ done
Using the test server
---------------------
@@ -184,7 +185,7 @@ Tests may also be run using the `pw_arduino_use_test_server = true` GN arg.
The server must be run with an `arduino_builder` config file so it can locate
the correct Arduino core, compiler path, and Arduino board used.
-.. code:: sh
+.. code-block:: sh
arduino_test_server --verbose \
--config-file ./out/arduino_debug/gen/arduino_builder_config.json
@@ -209,7 +210,7 @@ GN Target Example
Here is an example `pw_executable` gn rule that includes some Teensyduino
libraries.
-.. code:: text
+.. code-block:: text
import("//build_overrides/pigweed.gni")
import("$dir_pw_arduino_build/arduino.gni")
diff --git a/targets/arduino/target_toolchains.gni b/targets/arduino/target_toolchains.gni
index 3b74b8b96..82ef602ed 100644
--- a/targets/arduino/target_toolchains.gni
+++ b/targets/arduino/target_toolchains.gni
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/defaults.gni")
import("$dir_pw_sys_io/backend.gni")
import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
@@ -65,10 +66,10 @@ _toolchain_properties = {
final_binary_extension = ".elf"
}
-_target_default_configs = [
- "$dir_pw_toolchain/arm_gcc:enable_float_printf",
- "$dir_pigweed/targets/arduino:arduino_build",
-]
+_target_default_configs = pigweed_default_configs + [
+ "$dir_pw_toolchain/arm_gcc:enable_float_printf",
+ "$dir_pigweed/targets/arduino:arduino_build",
+ ]
pw_target_toolchain_arduino = {
_excluded_members = [
diff --git a/targets/default_config.BUILD b/targets/default_config.BUILD
deleted file mode 100644
index b2d33d5a5..000000000
--- a/targets/default_config.BUILD
+++ /dev/null
@@ -1,177 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-package(default_visibility = ["//visibility:public"])
-
-# TODO(b/236321905): Support backends other than boringSSL.
-label_flag(
- name = "pw_crypto_sha256_backend",
- build_setting_default = "@pigweed//pw_crypto:sha256_boringssl",
-)
-
-# TODO(b/236321905): Support backends other than boringSSL.
-label_flag(
- name = "pw_crypto_ecdsa_backend",
- build_setting_default = "@pigweed//pw_crypto:ecdsa_boringssl",
-)
-
-label_flag(
- name = "pw_log_backend",
- build_setting_default = "@pigweed//pw_log:backend_multiplexer",
-)
-
-label_flag(
- name = "pw_log_string_handler_backend",
- build_setting_default = "@pigweed//pw_log_string:handler_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_log_tokenized_handler_backend",
- build_setting_default = "@pigweed//pw_log_tokenized:base64_over_hdlc",
-)
-
-label_flag(
- name = "pw_assert_backend",
- build_setting_default = "@pigweed//pw_assert:backend_multiplexer",
-)
-
-label_flag(
- name = "pw_boot_backend",
- build_setting_default = "@pigweed//pw_boot:backend_multiplexer",
-)
-
-label_flag(
- name = "pw_chrono_system_clock_backend",
- build_setting_default = "@pigweed//pw_chrono:system_clock_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_chrono_system_timer_backend",
- build_setting_default = "@pigweed//pw_chrono:system_timer_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_rpc_system_server_backend",
- build_setting_default = "@pigweed//pw_rpc/system_server:system_server_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_binary_semaphore_backend",
- build_setting_default = "@pigweed//pw_sync:binary_semaphore_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_counting_semaphore_backend",
- build_setting_default = "@pigweed//pw_sync:counting_semaphore_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_mutex_backend",
- build_setting_default = "@pigweed//pw_sync:mutex_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_timed_mutex_backend",
- build_setting_default = "@pigweed//pw_sync:timed_mutex_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_recursive_mutex_backend",
- build_setting_default = "@pigweed//pw_sync:recursive_mutex_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_interrupt_spin_lock_backend",
- build_setting_default = "@pigweed//pw_sync:interrupt_spin_lock_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_thread_notification_backend",
- build_setting_default = "@pigweed//pw_sync:thread_notification_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sync_timed_thread_notification_backend",
- build_setting_default = "@pigweed//pw_sync:timed_thread_notification_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_interrupt_backend",
- build_setting_default = "@pigweed//pw_interrupt:backend_multiplexer",
-)
-
-label_flag(
- name = "pw_malloc_backend",
- build_setting_default = "@pigweed//pw_malloc:backend_multiplexer",
-)
-
-label_flag(
- name = "pw_thread_id_backend",
- build_setting_default = "@pigweed//pw_thread:id_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_thread_iteration_backend",
- build_setting_default = "@pigweed//pw_thread:iteration_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_thread_sleep_backend",
- build_setting_default = "@pigweed//pw_thread:sleep_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_thread_thread_backend",
- build_setting_default = "@pigweed//pw_thread:thread_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_thread_yield_backend",
- build_setting_default = "@pigweed//pw_thread:yield_backend_multiplexer",
-)
-
-label_flag(
- name = "pw_sys_io_backend",
- build_setting_default = "@pigweed//pw_sys_io:backend_multiplexer",
-)
-
-label_flag(
- name = "pw_system_target_hooks_backend",
- build_setting_default = "@pigweed//pw_system:target_hooks_multiplexer",
-)
-
-label_flag(
- name = "pw_unit_test_googletest_backend",
- build_setting_default = "@pigweed//pw_unit_test:light",
-)
-
-label_flag(
- name = "target_rtos",
- build_setting_default = "@pigweed//pw_build/constraints/rtos:none",
-)
-
-label_flag(
- name = "pw_perf_test_timer_backend",
- build_setting_default = "@pigweed//pw_perf_test:timer_multiplexer",
-)
-
-label_flag(
- name = "pw_trace_backend",
- build_setting_default = "@pigweed//pw_trace:backend_multiplexer",
-)
-
-label_flag(
- name = "freertos_config",
- build_setting_default = "@pigweed//third_party/freertos:freertos_config",
-)
diff --git a/targets/docs/BUILD.gn b/targets/docs/BUILD.gn
index e6c723a56..eb9a000b1 100644
--- a/targets/docs/BUILD.gn
+++ b/targets/docs/BUILD.gn
@@ -44,6 +44,10 @@ generate_toolchain("docs") {
"defaults",
"name",
])
+
+ # Docs assume output binary location, and don't support extensions.
+ final_binary_extension = ""
+
defaults = {
forward_variables_from(_base_toolchain.defaults, "*")
diff --git a/targets/docs/target_docs.rst b/targets/docs/target_docs.rst
index e130e8a17..92ad55e8b 100644
--- a/targets/docs/target_docs.rst
+++ b/targets/docs/target_docs.rst
@@ -14,7 +14,7 @@ Building
To build for this target, invoke ninja with the top-level "docs" group as the
target to build.
-.. code:: sh
+.. code-block:: sh
$ gn gen out
$ ninja -C out docs
diff --git a/targets/emcraft_sf2_som/BUILD.bazel b/targets/emcraft_sf2_som/BUILD.bazel
index 92ee012b4..d1cd31268 100644
--- a/targets/emcraft_sf2_som/BUILD.bazel
+++ b/targets/emcraft_sf2_som/BUILD.bazel
@@ -32,7 +32,7 @@ pw_cc_library(
"config/FreeRTOSConfig.h",
"config/sf2_mss_hal_conf.h",
],
- # TODO(b/260642311): This target doesn't build
+ # TODO: b/260642311 - This target doesn't build
tags = ["manual"],
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
diff --git a/targets/emcraft_sf2_som/BUILD.gn b/targets/emcraft_sf2_som/BUILD.gn
index 36bd46b70..115a90201 100644
--- a/targets/emcraft_sf2_som/BUILD.gn
+++ b/targets/emcraft_sf2_som/BUILD.gn
@@ -20,7 +20,6 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_malloc/backend.gni")
import("$dir_pw_system/system_target.gni")
import("$dir_pw_third_party/smartfusion_mss/mss.gni")
-import("$dir_pw_tokenizer/backend.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
config("pw_malloc_active") {
@@ -46,7 +45,7 @@ if (current_toolchain != default_toolchain) {
# (256K - Bootloader - InSystemProgrammer = 192K)
"PW_BOOT_CODE_SIZE=0x30000",
- # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+ # TODO: b/235348465 - Currently "pw_tokenizer/detokenize_test" requires at
# least 6K bytes in heap when using pw_malloc_freelist. The heap size
# required for tests should be investigated.
"PW_BOOT_HEAP_SIZE=4M",
@@ -75,7 +74,7 @@ if (current_toolchain != default_toolchain) {
# (256K - Bootloader - InSystemProgrammer = 192K)
"PW_BOOT_FLASH_SIZE=0x30000",
- # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+ # TODO: b/235348465 - Currently "pw_tokenizer/detokenize_test" requires at
# least 6K bytes in heap when using pw_malloc_freelist. The heap size
# required for tests should be investigated.
"PW_BOOT_HEAP_SIZE=4M",
diff --git a/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld b/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld
index dd3f2c6b2..8fbe6c972 100644
--- a/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld
+++ b/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld
@@ -65,7 +65,7 @@ ENTRY(pw_boot_Entry)
MEMORY
{
- /* TODO(b/234892223): Make it possible for projects to freely customize
+ /* TODO: b/234892223 - Make it possible for projects to freely customize
* memory regions.
*/
diff --git a/targets/emcraft_sf2_som/target_docs.rst b/targets/emcraft_sf2_som/target_docs.rst
index 2389f67a0..272312ee9 100644
--- a/targets/emcraft_sf2_som/target_docs.rst
+++ b/targets/emcraft_sf2_som/target_docs.rst
@@ -15,18 +15,21 @@ MSS HAL for the SmartFusion series. The supported repositories can be
downloaded via ``pw package``, and then the build must be manually configured
to point to the locations the repositories were downloaded to.
-.. code:: sh
+.. code-block:: sh
pw package install freertos
pw package install smartfusion_mss
pw package install nanopb
gn args out
- # Add these lines.
- dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
- dir_pw_third_party_smartfusion_mss =
- pw_env_setup_PACKAGE_ROOT + "/smartfusion_mss"
- dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
+
+Then add the following lines to that text file:
+
+.. code-block::
+
+ dir_pw_third_party_freertos = getenv("PW_PACKAGE_ROOT") + "/freertos"
+ dir_pw_third_party_smartfusion_mss = getenv("PW_PACKAGE_ROOT") + "/smartfusion_mss"
+ dir_pw_third_party_nanopb = getenv("PW_PACKAGE_ROOT") + "/nanopb"
Building and running the demo
=============================
diff --git a/targets/host/pw_add_test_executable.cmake b/targets/host/pw_add_test_executable.cmake
index 53b7e8acc..d61c9b9f7 100644
--- a/targets/host/pw_add_test_executable.cmake
+++ b/targets/host/pw_add_test_executable.cmake
@@ -41,6 +41,8 @@ function(pw_add_test_executable NAME TEST_DEP)
set(main pw_unit_test.logging_main)
elseif("${test_backend}" STREQUAL "pw_third_party.googletest")
set(main pw_third_party.googletest.gmock_main)
+ elseif("${test_backend}" STREQUAL "pw_third_party.fuzztest")
+ set(main pw_third_party.fuzztest_gtest_main)
else()
message(FATAL_ERROR
"Unsupported test backend selected for host test executables")
diff --git a/targets/host/system_rpc_server.cc b/targets/host/system_rpc_server.cc
index 8ff75751a..8704be48c 100644
--- a/targets/host/system_rpc_server.cc
+++ b/targets/host/system_rpc_server.cc
@@ -55,7 +55,7 @@ void Init() {
log_basic::SetOutput([](std::string_view log) {
std::fprintf(stderr, "%.*s\n", static_cast<int>(log.size()), log.data());
hdlc::WriteUIFrame(1, as_bytes(span<const char>(log)), socket_stream)
- .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ .IgnoreError(); // TODO: b/242598609 - Handle Status properly
});
PW_LOG_INFO("Starting pw_rpc server on port %d", socket_port);
diff --git a/targets/host/target_docs.rst b/targets/host/target_docs.rst
index 266ce71d1..99826a03b 100644
--- a/targets/host/target_docs.rst
+++ b/targets/host/target_docs.rst
@@ -49,14 +49,12 @@ downstream projects.
Toolchains for other C++ standards
==================================
-Most Pigweed code requires C++17, but a few modules, such as ``pw_tokenizer``,
-work with C++14. All Pigweed code is compatible with C++20. Pigweed defines
-toolchains for testing with C++14 and C++20.
+Pigweed code requires C++17 or newer and is fully compatible with C++20. Pigweed
+defines a toolchain for testing with C++20.
-* ``pw_strict_host_clang_debug_cpp14`` -- Builds with ``-std=c++14``.
* ``pw_strict_host_clang_size_optimized_cpp20`` -- Builds with ``-std=c++20``.
-These toolchains are only permitted for use in upstream pigweed, but downstream
+This toolchains is only permitted for use in upstream pigweed, but downstream
users may create similar toolchains as needed.
--------
diff --git a/targets/host/target_toolchains.gni b/targets/host/target_toolchains.gni
index 5c325aa68..4d6b3e7db 100644
--- a/targets/host/target_toolchains.gni
+++ b/targets/host/target_toolchains.gni
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_async/backend.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
@@ -37,9 +38,13 @@ _host_common = {
pw_perf_test_MAIN_FUNCTION = "$dir_pw_perf_test:log_perf_handler_main"
# Configure backend for assert facade.
- pw_assert_BACKEND = "$dir_pw_assert_basic"
+ pw_assert_BACKEND = "$dir_pw_assert:print_and_abort_check_backend"
pw_assert_LITE_BACKEND = "$dir_pw_assert:print_and_abort_assert_backend"
+ # Configure backend for async facade.
+ pw_async_TASK_BACKEND = "$dir_pw_async_basic:task"
+ pw_async_FAKE_DISPATCHER_BACKEND = "$dir_pw_async_basic:fake_dispatcher"
+
# Configure backend for logging facade.
pw_log_BACKEND = "$dir_pw_log_basic"
@@ -85,6 +90,8 @@ _host_common = {
pw_thread_YIELD_BACKEND = "$dir_pw_thread_stl:yield"
pw_thread_SLEEP_BACKEND = "$dir_pw_thread_stl:sleep"
pw_thread_THREAD_BACKEND = "$dir_pw_thread_stl:thread"
+ pw_thread_TEST_THREAD_CONTEXT_BACKEND =
+ "$dir_pw_thread_stl:test_thread_context"
pw_build_LINK_DEPS = [] # Explicit list overwrite required by GN
pw_build_LINK_DEPS = [
@@ -189,9 +196,13 @@ pw_target_toolchain_host = {
forward_variables_from(_toolchain_base, "*", _excluded_members)
defaults = {
forward_variables_from(_toolchain_base.defaults, "*")
- forward_variables_from(_host_common, "*")
+ forward_variables_from(_host_common, "*", [ "pw_unit_test_MAIN" ])
forward_variables_from(_os_specific_config, "*")
default_configs += _clang_default_configs
+
+ # Always use the FuzzTest wrappers around gtest and gtest_main.
+ pw_unit_test_MAIN = "$dir_pw_fuzzer:fuzztest_main"
+ pw_unit_test_GOOGLETEST_BACKEND = "$dir_pw_fuzzer:gtest"
}
}
@@ -327,7 +338,7 @@ pw_target_toolchain_host_list = [
_pigweed_internal = {
pw_status_CONFIG = "$dir_pw_status:check_if_used"
- # TODO(b/241565082): Enable NC testing in GN Windows when it is fixed.
+ # TODO: b/241565082 - Enable NC testing in GN Windows when it is fixed.
pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = host_os != "win"
}
@@ -372,20 +383,24 @@ pw_internal_host_toolchains = [
}
},
{
- name = "pw_strict_host_gcc_debug"
- _toolchain_base = pw_toolchain_host_gcc.debug
+ name = "pw_strict_host_clang_fuzz"
+ _toolchain_base = pw_toolchain_host_clang.fuzz
forward_variables_from(_toolchain_base, "*", _excluded_members)
defaults = {
forward_variables_from(_toolchain_base.defaults, "*")
- forward_variables_from(_host_common, "*")
+ forward_variables_from(_host_common, "*", [ "pw_unit_test_MAIN" ])
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _internal_gcc_default_configs
+ default_configs += _internal_clang_default_configs
+
+ # Always use the FuzzTest wrapper around gtest_main.
+ pw_unit_test_MAIN = "$dir_pw_fuzzer:fuzztest_main"
+ pw_unit_test_GOOGLETEST_BACKEND = "$dir_pw_fuzzer:gtest"
}
},
{
- name = "pw_strict_host_gcc_speed_optimized"
- _toolchain_base = pw_toolchain_host_gcc.speed_optimized
+ name = "pw_strict_host_gcc_debug"
+ _toolchain_base = pw_toolchain_host_gcc.debug
forward_variables_from(_toolchain_base, "*", _excluded_members)
defaults = {
forward_variables_from(_toolchain_base.defaults, "*")
@@ -396,8 +411,8 @@ pw_internal_host_toolchains = [
}
},
{
- name = "pw_strict_host_gcc_size_optimized"
- _toolchain_base = pw_toolchain_host_gcc.size_optimized
+ name = "pw_strict_host_gcc_speed_optimized"
+ _toolchain_base = pw_toolchain_host_gcc.speed_optimized
forward_variables_from(_toolchain_base, "*", _excluded_members)
defaults = {
forward_variables_from(_toolchain_base.defaults, "*")
@@ -408,27 +423,15 @@ pw_internal_host_toolchains = [
}
},
{
- name = "pw_strict_host_clang_debug_cpp14"
- _toolchain_base = pw_toolchain_host_clang.debug
+ name = "pw_strict_host_gcc_size_optimized"
+ _toolchain_base = pw_toolchain_host_gcc.size_optimized
forward_variables_from(_toolchain_base, "*", _excluded_members)
defaults = {
forward_variables_from(_toolchain_base.defaults, "*")
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _internal_clang_default_configs
-
- # Set the C++ standard to C++14 instead of the default (C++17).
- pw_toolchain_CXX_STANDARD = pw_toolchain_STANDARD.CXX14
-
- # Do not do negative compilation testing with C++14 since the code may
- # fail to compile for different reasons than in C++17 or newer.
- pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false
-
- # Select C++14-compatible backends.
- pw_assert_BACKEND = "$dir_pw_assert:print_and_abort_check_backend"
- pw_log_BACKEND = "$dir_pw_log_null"
- pw_unit_test_MAIN = "$dir_pw_unit_test:printf_main"
+ default_configs += _internal_gcc_default_configs
}
},
{
@@ -461,7 +464,22 @@ pw_internal_host_toolchains = [
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
default_configs += _internal_clang_default_configs
- default_configs += [ "$dir_pw_minimal_cpp_stdlib:use_minimal_cpp_stdlib" ]
+ default_public_deps = [ "$dir_pw_minimal_cpp_stdlib" ]
+ }
+ },
+ {
+ name = "pw_strict_host_clang_debug_dynamic_allocation"
+ _toolchain_base = pw_toolchain_host_clang.debug
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_host_common, "*")
+ forward_variables_from(_pigweed_internal, "*")
+ forward_variables_from(_os_specific_config, "*")
+ default_configs += _internal_clang_default_configs
+
+ pw_function_CONFIG = "$dir_pw_function:enable_dynamic_allocation"
+ pw_rpc_CONFIG = "$dir_pw_rpc:use_dynamic_allocation"
}
},
]
diff --git a/targets/host_device_simulator/target_docs.rst b/targets/host_device_simulator/target_docs.rst
index c2b437d97..169382579 100644
--- a/targets/host_device_simulator/target_docs.rst
+++ b/targets/host_device_simulator/target_docs.rst
@@ -20,7 +20,7 @@ package to build for the
:bdg-ref-primary-line:`target-stm32f429i-disc1-stm32cube` target at the same
time.
-.. code:: sh
+.. code-block:: sh
pw package install nanopb
pw package install freertos
@@ -37,17 +37,17 @@ time.
Instead of the ``gn gen out`` with args set on the command line above you can
run:
- .. code:: sh
+ .. code-block:: sh
gn args out
Then add the following lines to that text file:
- .. code::
+ .. code-block::
- dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
- dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
- dir_pw_third_party_stm32cube_f4 = pw_env_setup_PACKAGE_ROOT + "/stm32cube_f4"
+ dir_pw_third_party_nanopb = getenv("PW_PACKAGE_ROOT") + "/nanopb"
+ dir_pw_third_party_freertos = getenv("PW_PACKAGE_ROOT") + "/freertos"
+ dir_pw_third_party_stm32cube_f4 = getenv("PW_PACKAGE_ROOT") + "/stm32cube_f4"
-----------------------------
Building and Running the Demo
@@ -55,17 +55,17 @@ Building and Running the Demo
This target has an associated demo application that can be built and then
run with the following commands:
-.. code:: sh
+.. code-block:: sh
ninja -C out pw_system_demo
-.. code:: sh
+.. code-block:: sh
./out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
To communicate with the launched process run this in a separate shell:
-.. code:: sh
+.. code-block:: sh
pw-system-console -s default --proto-globs pw_rpc/echo.proto \
--token-databases out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
@@ -75,7 +75,7 @@ To communicate with the launched process run this in a separate shell:
Alternatively you can run the system_example app in the background, then
launch the console on the same line with:
- .. code:: sh
+ .. code-block:: sh
./out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
& \
@@ -86,7 +86,7 @@ To communicate with the launched process run this in a separate shell:
Exit the console via the menu or pressing :kbd:`Ctrl-d` twice. Then stop the
system_example app with:
- .. code:: sh
+ .. code-block:: sh
killall system_example
@@ -94,14 +94,14 @@ In the bottom-most pane labeled ``Python Repl`` you should be able to send RPC
commands to the simulated device process. For example, you can send an RPC
message that will be echoed back:
-.. code:: pycon
+.. code-block:: pycon
>>> device.rpcs.pw.rpc.EchoService.Echo(msg='Hello, world!')
(Status.OK, pw.rpc.EchoMessage(msg='Hello, world!'))
Or run unit tests included on the simulated device:
-.. code:: pycon
+.. code-block:: pycon
>>> device.run_tests()
True
diff --git a/targets/lm3s6965evb_qemu/BUILD.gn b/targets/lm3s6965evb_qemu/BUILD.gn
index 56481dd5e..19a534bb4 100644
--- a/targets/lm3s6965evb_qemu/BUILD.gn
+++ b/targets/lm3s6965evb_qemu/BUILD.gn
@@ -23,7 +23,7 @@ generate_toolchains("target_toolchains") {
toolchains = pw_target_toolchain_lm3s6965evb_qemu_list
}
-# TODO(b/232587313): This config is only used in the clang build for this
+# TODO: b/232587313 - This config is only used in the clang build for this
# target, see target_toolchains.gni or the associated bug for more details.
config("disable_lock_annotations") {
cflags = [ "-Wno-thread-safety-analysis" ]
diff --git a/targets/lm3s6965evb_qemu/py/BUILD.gn b/targets/lm3s6965evb_qemu/py/BUILD.gn
index 53d6e0583..204c0f152 100644
--- a/targets/lm3s6965evb_qemu/py/BUILD.gn
+++ b/targets/lm3s6965evb_qemu/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"lm3s6965evb_qemu_utils/__init__.py",
diff --git a/targets/lm3s6965evb_qemu/py/setup.py b/targets/lm3s6965evb_qemu/py/setup.py
deleted file mode 100644
index 7842ebd4a..000000000
--- a/targets/lm3s6965evb_qemu/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""lm3s6965evb_qemu_utils"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/targets/lm3s6965evb_qemu/target_docs.rst b/targets/lm3s6965evb_qemu/target_docs.rst
index b5f5b84f7..123d2d4f4 100644
--- a/targets/lm3s6965evb_qemu/target_docs.rst
+++ b/targets/lm3s6965evb_qemu/target_docs.rst
@@ -15,7 +15,7 @@ Building
To build for this Pigweed target, simply build the top-level "qemu_gcc" Ninja
target.
-.. code:: sh
+.. code-block:: sh
$ ninja -C out qemu_gcc
@@ -34,7 +34,7 @@ Running Without GDB
When running without GDB, the firmware will execute normally without requiring
further interaction.
-.. code:: sh
+.. code-block:: sh
$ qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb \
-nographic -no-reboot \
@@ -46,7 +46,7 @@ When running with GDB, execution of the binary will begin in a halted state. To
begin running the code, you must connect using GDB, set any breakpoints you
wish, and then continue execution.
-.. code:: sh
+.. code-block:: sh
# Start the VM and GDB server.
$ qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb \
@@ -56,7 +56,7 @@ wish, and then continue execution.
In another window
-.. code:: sh
+.. code-block:: sh
$ arm-none-eabi-gdb path/to/firmare.elf
(gdb) target remote :3333
diff --git a/targets/lm3s6965evb_qemu/target_toolchains.gni b/targets/lm3s6965evb_qemu/target_toolchains.gni
index 25779b893..746415400 100644
--- a/targets/lm3s6965evb_qemu/target_toolchains.gni
+++ b/targets/lm3s6965evb_qemu/target_toolchains.gni
@@ -83,7 +83,7 @@ _clang_target_default_configs = [
"$dir_pw_build:extra_strict_warnings",
"$dir_pw_toolchain/arm_clang:enable_float_printf",
- # TODO(b/232587313): Disable lock annotations for this target because the
+ # TODO: b/232587313 - Disable lock annotations for this target because the
# clang toolchain currently relies on the standard library headers provided by
# arm-none-eabi-gcc, and thus do not have thread safety lock annotations on
# things like std::lock_guard. Thread safety checks will not work until
diff --git a/targets/mimxrt595_evk/target_docs.rst b/targets/mimxrt595_evk/target_docs.rst
index 7b6b3c814..47a9505fa 100644
--- a/targets/mimxrt595_evk/target_docs.rst
+++ b/targets/mimxrt595_evk/target_docs.rst
@@ -29,7 +29,7 @@ the manifest file within the unpacked SDK, and then setting the
``pw_third_party_mcuxpresso_SDK`` to the ``sample_sdk`` source set within the
Pigweed target directory.
-.. code:: sh
+.. code-block:: sh
$ gn args out
# Modify and save the args file to use the sample SDK.
@@ -41,7 +41,7 @@ Building
Once configured, to build for this Pigweed target, simply build the top-level
"mimxrt595" Ninja target.
-.. code:: sh
+.. code-block:: sh
$ ninja -C out mimxrt595
@@ -103,29 +103,29 @@ Use ``arm-none-eabi-gdb`` to load an executable into the target, debug, and run
it.
.. code-block::
- :emphasize-lines: 1,6,10,12,20
-
- (gdb) target remote :2331
- Remote debugging using :2331
- warning: No executable has been specified and target does not support
- determining executable automatically. Try using the "file" command.
- 0x08000000 in ?? ()
- (gdb) file out/mimxrt595_evk_debug/obj/pw_status/test/status_test.elf
- A program is being debugged already.
- Are you sure you want to change the file? (y or n) y
- Reading symbols from out/mimxrt595_evk_debug/obj/pw_status/test/status_test.elf...
- (gdb) monitor reset
- Resetting target
- (gdb) load
- Loading section .flash_config, size 0x200 lma 0x8000400
- Loading section .vector_table, size 0x168 lma 0x8001000
- Loading section .code, size 0xb34c lma 0x8001180
- Loading section .ARM, size 0x8 lma 0x800c4d0
- Loading section .static_init_ram, size 0x3c8 lma 0x800c4d8
- Start address 0x080048d0, load size 47748
- Transfer rate: 15542 KB/sec, 6821 bytes/write.
- (gdb) monitor reset
- Resetting target
+ :emphasize-lines: 1,6,10,12,20
+
+ (gdb) target remote :2331
+ Remote debugging using :2331
+ warning: No executable has been specified and target does not support
+ determining executable automatically. Try using the "file" command.
+ 0x08000000 in ?? ()
+ (gdb) file out/mimxrt595_evk_debug/obj/pw_status/test/status_test.elf
+ A program is being debugged already.
+ Are you sure you want to change the file? (y or n) y
+ Reading symbols from out/mimxrt595_evk_debug/obj/pw_status/test/status_test.elf...
+ (gdb) monitor reset
+ Resetting target
+ (gdb) load
+ Loading section .flash_config, size 0x200 lma 0x8000400
+ Loading section .vector_table, size 0x168 lma 0x8001000
+ Loading section .code, size 0xb34c lma 0x8001180
+ Loading section .ARM, size 0x8 lma 0x800c4d0
+ Loading section .static_init_ram, size 0x3c8 lma 0x800c4d8
+ Start address 0x080048d0, load size 47748
+ Transfer rate: 15542 KB/sec, 6821 bytes/write.
+ (gdb) monitor reset
+ Resetting target
You can now set any breakpoints you wish, and ``continue`` to run the
executable.
diff --git a/targets/mimxrt595_evk_freertos/BUILD.bazel b/targets/mimxrt595_evk_freertos/BUILD.bazel
new file mode 100644
index 000000000..aea2350ed
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/BUILD.bazel
@@ -0,0 +1,80 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_linker_script",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_linker_script(
+ name = "flash_linker_script",
+ defines = [
+ "PW_BOOT_FLASH_BEGIN=0x08001180",
+ "PW_BOOT_FLASH_SIZE=0x001FEE80",
+ "PW_BOOT_HEAP_SIZE=200K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20080000",
+ "PW_BOOT_RAM_SIZE=0x00280000",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08001000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=0x00000180",
+ ],
+ linker_script = "mimxrt595_flash.ld",
+)
+
+pw_cc_library(
+ name = "boot",
+ srcs = [
+ "boot.cc",
+ "vector_table.c",
+ ],
+ defines = [
+ "PW_MALLOC_ACTIVE=1",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/board:mimxrt595_evk",
+ "@platforms//cpu:armv8-m",
+ ],
+ deps = [
+ ":flash_linker_script",
+ "//pw_boot",
+ "//pw_boot_cortex_m",
+ "//pw_preprocessor",
+ "//pw_sys_io_mcuxpresso",
+ "@pigweed//targets:mcuxpresso_sdk",
+ ],
+ alwayslink = 1,
+)
+
+pw_cc_library(
+ name = "freertos_config",
+ hdrs = [
+ "FreeRTOSConfig.h",
+ ],
+ includes = ["./"],
+ deps = ["//third_party/freertos:config_assert"],
+)
+
+pw_cc_library(
+ name = "tasks",
+ srcs = [
+ "tasks.c",
+ ],
+ target_compatible_with = ["//pw_build/constraints/rtos:freertos"],
+ deps = ["@freertos"],
+)
diff --git a/targets/mimxrt595_evk_freertos/BUILD.gn b/targets/mimxrt595_evk_freertos/BUILD.gn
new file mode 100644
index 000000000..c692fad79
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/BUILD.gn
@@ -0,0 +1,219 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/linker_script.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_malloc/backend.gni")
+import("$dir_pw_third_party/freertos/freertos.gni")
+import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("target_toolchains.gni")
+
+generate_toolchains("target_toolchains") {
+ toolchains = pw_target_toolchain_mimxrt595_evk_freertos_list
+}
+
+declare_args() {
+ # When compiling with an MCUXpresso SDK, this variable is set to the path of
+ # the manifest file within the SDK installation. When set, a pw_source_set
+ # for a sample project SDK is created at
+ # "//targets/mimxrt595_evk_freertos:sdk".
+ pw_target_mimxrt595_evk_freertos_MANIFEST = ""
+
+ # This list should contain the necessary defines for setting linker script
+ # memory regions. While we don't directly use the pw_boot_cortex_m linker
+ # script, these are deliberately matching to make being able to later easier.
+ pw_target_mimxrt595_evk_freertos_LINK_CONFIG_DEFINES = []
+}
+
+config("pw_malloc_active") {
+ if (pw_malloc_BACKEND != "") {
+ defines = [ "PW_MALLOC_ACTIVE=1" ]
+ }
+}
+
+config("disable_warnings") {
+ cflags = [
+ "-Wno-cast-qual",
+ "-Wno-redundant-decls",
+ "-Wno-undef",
+ "-Wno-unused-parameter",
+ "-Wno-unused-variable",
+ "-Wno-error=strict-prototypes",
+ ]
+ visibility = [ ":*" ]
+}
+
+config("freestanding") {
+ cflags = [
+ "-ffreestanding",
+ "-fno-builtin",
+ ]
+ asmflags = cflags
+ ldflags = cflags
+ visibility = [ ":*" ]
+}
+
+config("sdk_defines") {
+ defines = [
+ "CPU_MIMXRT595SFFOC_cm33",
+ "DEBUG_CONSOLE_TRANSFER_NON_BLOCKING",
+ "SDK_DEBUGCONSOLE=1",
+ ]
+ visibility = [ ":*" ]
+}
+
+if (current_toolchain != default_toolchain) {
+ pw_linker_script("flash_linker_script") {
+ defines = pw_target_mimxrt595_evk_freertos_LINK_CONFIG_DEFINES
+ linker_script = "mimxrt595_flash.ld"
+ }
+}
+
+if (pw_third_party_mcuxpresso_SDK != "") {
+ # Startup and vector table for NXP MIMXRT595-EVK.
+ pw_source_set("boot") {
+ public_configs = [ ":pw_malloc_active" ]
+ deps = [
+ "$dir_pw_boot",
+ "$dir_pw_boot_cortex_m:armv8m",
+ "$dir_pw_malloc",
+ "$dir_pw_preprocessor",
+ "$dir_pw_sys_io_mcuxpresso",
+ pw_third_party_mcuxpresso_SDK,
+ ]
+ if (pw_malloc_BACKEND != "") {
+ deps += [ "$dir_pw_malloc" ]
+ }
+ sources = [
+ "boot.cc",
+ "vector_table.c",
+ ]
+ }
+}
+
+if (pw_third_party_mcuxpresso_SDK == "//targets/mimxrt595_evk_freertos:sdk") {
+ pw_mcuxpresso_sdk("sdk") {
+ manifest = pw_target_mimxrt595_evk_freertos_MANIFEST
+ include = [
+ "project_template.evkmimxrt595.MIMXRT595S",
+ "component.serial_manager_uart.MIMXRT595S",
+ "platform.drivers.flexcomm_i2c.MIMXRT595S",
+ "platform.drivers.flexcomm_spi.MIMXRT595S",
+ "platform.drivers.flexcomm_usart_freertos.MIMXRT595S",
+ "platform.drivers.flexio_spi.MIMXRT595S",
+ "platform.drivers.pint.MIMXRT595S",
+ "platform.drivers.power.MIMXRT595S",
+ "platform.drivers.lpc_gpio.MIMXRT595S",
+ "platform.drivers.mu.MIMXRT595S",
+ "utility.debug_console.MIMXRT595S",
+ ]
+ exclude = [
+ "device.MIMXRT595S_startup.MIMXRT595S",
+
+ # Don't need to generate build rules for freertos, we use pigweed's.
+ "middleware.freertos-kernel.MIMXRT595S",
+ ]
+
+ public_configs = [
+ ":disable_warnings",
+ ":freestanding",
+ ":sdk_defines",
+ ]
+
+ public_deps = [
+ ":mimxrt595_config",
+ "$dir_pw_third_party/freertos",
+ ]
+ }
+
+ config("config_public_includes") {
+ include_dirs = [ "board" ]
+ visibility = [ ":*" ]
+ }
+
+ config("config_public_defines") {
+ defines = [
+ "CPU_MIMXRT595SFFOC_cm33",
+ "DEBUG_CONSOLE_DISABLE_RTOS_SYNCHRONIZATION",
+ "FSL_RTOS_FREE_RTOS",
+ "FSL_SDK_DRIVER_QUICK_ACCESS_ENABLE=1",
+ "SDK_DEBUGCONSOLE=1",
+ "SDK_OS_FREE_RTOS",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ # Project-specific board configuration.
+ pw_source_set("mimxrt595_config") {
+ public_configs = [
+ ":config_public_defines",
+ ":config_public_includes",
+ ":disable_warnings",
+ ":sdk__defines",
+ ":sdk__includes",
+ ]
+ }
+
+ # Project-specific FreeRTOS configurations.
+ config("freertos_config_public_includes") {
+ include_dirs = [ "." ]
+ visibility = [ ":*" ]
+ }
+
+ pw_source_set("freertos_config") {
+ public_configs = [
+ ":config_public_defines",
+ ":config_public_includes",
+ ":disable_warnings",
+ ":freertos_config_public_includes",
+ ]
+ public_deps = [ "$dir_pw_third_party/freertos:config_assert" ]
+ public = [ "FreeRTOSConfig.h" ]
+ }
+
+ # Project-specific FreeRTOS port.
+ _freertos_port_dir =
+ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM33_NTZ/non_secure"
+ config("freertos_port_public_includes") {
+ include_dirs = [
+ "$_freertos_port_dir",
+ "$dir_pw_third_party_freertos/include",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ pw_source_set("freertos_port") {
+ public_configs = [ ":freertos_port_public_includes" ]
+ public = [
+ "$_freertos_port_dir/portasm.h",
+ "$_freertos_port_dir/portmacro.h",
+ ]
+ configs = [ ":disable_warnings" ]
+ sources = [
+ "$_freertos_port_dir/port.c",
+ "$_freertos_port_dir/portasm.c",
+ "$dir_pw_third_party_freertos/portable/MemMang/heap_4.c",
+ "tasks.c",
+ ]
+ deps = [ ":freertos_config" ]
+ }
+}
+
+pw_doc_group("target_docs") {
+ sources = [ "target_docs.rst" ]
+}
diff --git a/targets/mimxrt595_evk_freertos/FreeRTOSConfig.h b/targets/mimxrt595_evk_freertos/FreeRTOSConfig.h
new file mode 100644
index 000000000..de343b21a
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/FreeRTOSConfig.h
@@ -0,0 +1,155 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#pragma once
+
+// Disable formatting to make it easier to compare with other config files.
+// clang-format off
+
+extern uint32_t SystemCoreClock;
+
+/*-----------------------------------------------------------
+ * Application specific definitions.
+ *
+ * These definitions should be adjusted for your particular hardware and
+ * application requirements.
+ *
+ * THESE PARAMETERS ARE DESCRIBED WITHIN THE 'CONFIGURATION' SECTION OF THE
+ * FreeRTOS API DOCUMENTATION AVAILABLE ON THE FreeRTOS.org WEB SITE.
+ *
+ * See http://www.freertos.org/a00110.html.
+ *----------------------------------------------------------*/
+#ifndef configENABLE_FPU
+ #define configENABLE_FPU 1
+#endif
+#ifndef configENABLE_MPU
+ #define configENABLE_MPU 0
+#endif
+#ifndef configENABLE_TRUSTZONE
+ #define configENABLE_TRUSTZONE 0
+#endif
+#ifndef configRUN_FREERTOS_SECURE_ONLY
+ #define configRUN_FREERTOS_SECURE_ONLY 1
+#endif
+
+#define configUSE_PREEMPTION 1
+#define configUSE_TICKLESS_IDLE 0
+#define configCPU_CLOCK_HZ (SystemCoreClock)
+#define configTICK_RATE_HZ ((TickType_t)1000)
+#define configMAX_PRIORITIES 20
+#define configMINIMAL_STACK_SIZE ((unsigned short)256)
+#define configMAX_TASK_NAME_LEN 20
+#define configUSE_16_BIT_TICKS 0
+#define configIDLE_SHOULD_YIELD 1
+#define configUSE_TASK_NOTIFICATIONS 1
+#define configUSE_MUTEXES 1
+#define configUSE_RECURSIVE_MUTEXES 1
+#define configUSE_COUNTING_SEMAPHORES 1
+#define configUSE_ALTERNATIVE_API 0 /* Deprecated! */
+#define configQUEUE_REGISTRY_SIZE 8
+#define configUSE_QUEUE_SETS 1
+#define configUSE_TIME_SLICING 1
+#define configUSE_NEWLIB_REENTRANT 0
+#define configENABLE_BACKWARD_COMPATIBILITY 1
+#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
+#define configUSE_APPLICATION_TASK_TAG 0
+
+/* Used memory allocation (heap_x.c) */
+#define configFRTOS_MEMORY_SCHEME 4
+/* Tasks.c additions (e.g. Thread Aware Debug capability) */
+#define configINCLUDE_FREERTOS_TASK_C_ADDITIONS_H 0
+
+/* Memory allocation related definitions. */
+#define configSUPPORT_STATIC_ALLOCATION 1
+#define configSUPPORT_DYNAMIC_ALLOCATION 1
+#define configTOTAL_HEAP_SIZE ((size_t)(200 * 1024))
+#define configAPPLICATION_ALLOCATED_HEAP 0
+
+/* Hook function related definitions. */
+#define configUSE_IDLE_HOOK 0
+#define configUSE_TICK_HOOK 0
+#define configCHECK_FOR_STACK_OVERFLOW 0
+#define configUSE_MALLOC_FAILED_HOOK 0
+#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
+
+/* Run time and task stats gathering related definitions. */
+#define configGENERATE_RUN_TIME_STATS 0
+#define configUSE_TRACE_FACILITY 1
+#define configUSE_STATS_FORMATTING_FUNCTIONS 1
+
+/* Task aware debugging. */
+#define configRECORD_STACK_HIGH_ADDRESS 1
+
+/* Co-routine related definitions. */
+#define configUSE_CO_ROUTINES 0
+#define configMAX_CO_ROUTINE_PRIORITIES 2
+
+/* Software timer related definitions. */
+#define configUSE_TIMERS 1
+#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1)
+#define configTIMER_QUEUE_LENGTH 10
+#define configTIMER_TASK_STACK_DEPTH (configMINIMAL_STACK_SIZE * 2)
+
+// Instead of defining configASSERT(), include a header that provides a
+// definition that redirects to pw_assert.
+#include "pw_third_party/freertos/config_assert.h"
+
+/* Optional functions - most linkers will remove unused functions anyway. */
+#define INCLUDE_vTaskPrioritySet 1
+#define INCLUDE_uxTaskPriorityGet 1
+#define INCLUDE_vTaskDelete 1
+#define INCLUDE_vTaskSuspend 1
+#define INCLUDE_xResumeFromISR 1
+#define INCLUDE_vTaskDelayUntil 1
+#define INCLUDE_vTaskDelay 1
+#define INCLUDE_xTaskGetSchedulerState 1
+#define INCLUDE_xTaskGetCurrentTaskHandle 1
+#define INCLUDE_uxTaskGetStackHighWaterMark 0
+#define INCLUDE_xTaskGetIdleTaskHandle 0
+#define INCLUDE_eTaskGetState 0
+#define INCLUDE_xEventGroupSetBitFromISR 1
+#define INCLUDE_xTimerPendFunctionCall 1
+#define INCLUDE_xTaskAbortDelay 0
+#define INCLUDE_xTaskGetHandle 0
+#define INCLUDE_xTaskResumeFromISR 1
+
+#ifdef __NVIC_PRIO_BITS
+/* __BVIC_PRIO_BITS will be specified when CMSIS is being used. */
+#define configPRIO_BITS __NVIC_PRIO_BITS
+#else
+#define configPRIO_BITS 4 /* 15 priority levels */
+#endif
+
+/* The lowest interrupt priority that can be used in a call to a "set priority"
+function. */
+#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY ((1U << (configPRIO_BITS)) - 1)
+
+/* The highest interrupt priority that can be used by any interrupt service
+routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL
+INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
+PRIORITY THAN THIS! (higher priorities are lower numeric values. */
+#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 2
+
+/* Interrupt priorities used by the kernel port layer itself. These are generic
+to all Cortex-M ports, and do not rely on any particular library functions. */
+#define configKERNEL_INTERRUPT_PRIORITY (configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
+/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
+See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
+#define configMAX_SYSCALL_INTERRUPT_PRIORITY (configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS))
+
+/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
+standard names. */
+#define vPortSVCHandler SVC_Handler
+#define vPortPendSVHandler PendSV_Handler
+#define vPortSysTickHandler SysTick_Handler
diff --git a/targets/mimxrt595_evk_freertos/OWNERS b/targets/mimxrt595_evk_freertos/OWNERS
new file mode 100644
index 000000000..710183e4f
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/OWNERS
@@ -0,0 +1,2 @@
+afoxley@google.com
+keybuk@google.com
diff --git a/targets/mimxrt595_evk_freertos/boot.cc b/targets/mimxrt595_evk_freertos/boot.cc
new file mode 100644
index 000000000..695b744d5
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/boot.cc
@@ -0,0 +1,53 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_boot/boot.h"
+
+#include "board.h"
+#include "clock_config.h"
+#include "peripherals.h"
+#include "pin_mux.h"
+#include "pw_boot_cortex_m/boot.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_sys_io_mcuxpresso/init.h"
+
+#if PW_MALLOC_ACTIVE
+#include "pw_malloc/malloc.h"
+#endif // PW_MALLOC_ACTIVE
+
+void pw_boot_PreStaticMemoryInit() {
+ // Call CMSIS SystemInit code.
+ SystemInit();
+}
+
+void pw_boot_PreStaticConstructorInit() {
+#if PW_MALLOC_ACTIVE
+ pw_MallocInit(&pw_boot_heap_low_addr, &pw_boot_heap_high_addr);
+#endif // PW_MALLOC_ACTIVE
+}
+
+void pw_boot_PreMainInit() {
+ BOARD_InitPins();
+ BOARD_InitBootClocks();
+ BOARD_InitPeripherals();
+
+ pw_sys_io_mcuxpresso_Init();
+}
+
+PW_NO_RETURN void pw_boot_PostMain() {
+ // In case main() returns, just sit here until the device is reset.
+ while (true) {
+ }
+ PW_UNREACHABLE;
+}
diff --git a/targets/mimxrt595_evk_freertos/mimxrt595_executable.gni b/targets/mimxrt595_evk_freertos/mimxrt595_executable.gni
new file mode 100644
index 000000000..b9f462e4c
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/mimxrt595_executable.gni
@@ -0,0 +1,64 @@
+# Copyright 2021 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/exec.gni")
+import("$dir_pw_malloc/backend.gni")
+
+# Executable wrapper that includes some baremetal startup code.
+template("mimxrt595_executable") {
+ group(target_name) {
+ deps = [
+ ":${target_name}__binary",
+ ":${target_name}__elf",
+ ]
+ }
+
+ # .elf binary created by the standard toolchain.
+ _base_target_name = target_name
+ executable("${target_name}__elf") {
+ output_name = "${_base_target_name}"
+ forward_variables_from(invoker, "*")
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ "//targets/mimxrt595_evk_freertos:boot" ]
+ if (pw_malloc_BACKEND != "") {
+ if (!defined(configs)) {
+ configs = []
+ }
+ configs += [ "$dir_pw_malloc:pw_malloc_wrapper_config" ]
+ }
+ }
+
+ # .bin binary created by extracting from the toolchain output.
+ pw_exec("${target_name}__binary") {
+ if (defined(invoker.output_dir)) {
+ _output_dir = invoker.output_dir
+ } else {
+ _output_dir = target_out_dir
+ }
+
+ outputs = [ "${_output_dir}/${_base_target_name}.bin" ]
+ deps = [ ":${_base_target_name}__elf" ]
+
+ program = "arm-none-eabi-objcopy"
+ args = [
+ "-Obinary",
+ "<TARGET_FILE(:${_base_target_name}__elf)>",
+ rebase_path(outputs[0], root_build_dir),
+ ]
+ }
+}
diff --git a/targets/mimxrt595_evk_freertos/mimxrt595_flash.ld b/targets/mimxrt595_evk_freertos/mimxrt595_flash.ld
new file mode 100644
index 000000000..f07000877
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/mimxrt595_flash.ld
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2023 The Pigweed Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+/* This linker script is derived from pw_boot_cortex_m/basic_cortex_m.ld for use
+ * with the NXP MIMXRT595-EVK, booting from FLASH.
+ */
+
+/* Provide useful error messages when required configurations are not set. */
+#ifndef PW_BOOT_VECTOR_TABLE_BEGIN
+#error "PW_BOOT_VECTOR_TABLE_BEGIN is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_VECTOR_TABLE_BEGIN
+
+#ifndef PW_BOOT_VECTOR_TABLE_SIZE
+#error "PW_BOOT_VECTOR_TABLE_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_VECTOR_TABLE_SIZE
+
+#ifndef PW_BOOT_FLASH_BEGIN
+#error "PW_BOOT_FLASH_BEGIN is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_FLASH_BEGIN
+
+#ifndef PW_BOOT_FLASH_SIZE
+#error "PW_BOOT_FLASH_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_FLASH_SIZE
+
+#ifndef PW_BOOT_RAM_BEGIN
+#error "PW_BOOT_RAM_BEGIN is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_RAM_BEGIN
+
+#ifndef PW_BOOT_RAM_SIZE
+#error "PW_BOOT_RAM_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_RAM_SIZE
+
+#ifndef PW_BOOT_HEAP_SIZE
+#error "PW_BOOT_HEAP_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_HEAP_SIZE
+
+#ifndef PW_BOOT_MIN_STACK_SIZE
+#error "PW_BOOT_MIN_STACK_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_MIN_STACK_SIZE
+
+
+/* Note: This technically doesn't set the firmware's entry point. Setting the
+ * firmware entry point is done by setting vector_table[1]
+ * (Reset_Handler). However, this DOES tell the compiler how to optimize
+ * when --gc-sections is enabled.
+ */
+ENTRY(pw_boot_Entry)
+
+MEMORY
+{
+ /* Flash Config for bootloader */
+ FLASH_CONFIG(rx) : \
+ ORIGIN = 0x08000400, \
+ LENGTH = 0x00000200
+ /* Vector Table (typically in flash) */
+ VECTOR_TABLE(rx) : \
+ ORIGIN = PW_BOOT_VECTOR_TABLE_BEGIN, \
+ LENGTH = PW_BOOT_VECTOR_TABLE_SIZE
+ /* Internal Flash */
+ FLASH(rx) : \
+ ORIGIN = PW_BOOT_FLASH_BEGIN, \
+ LENGTH = PW_BOOT_FLASH_SIZE
+ /* Internal SRAM */
+ RAM(rwx) : \
+ ORIGIN = PW_BOOT_RAM_BEGIN, \
+ LENGTH = PW_BOOT_RAM_SIZE
+ /* USB SRAM */
+ USB_SRAM(rw) : \
+ ORIGIN = 0x40140000, \
+ LENGTH = 0x00004000
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These segments
+ * are used by pw_bloat.bloaty_config to create the utilization data source
+ * for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
+}
+
+SECTIONS
+{
+ .flash_config :
+ {
+ . = ALIGN(4);
+ KEEP(*(.flash_conf))
+ } >FLASH_CONFIG
+
+ /* This is the link-time vector table. If used, the VTOR (Vector Table Offset
+ * Register) MUST point to this memory location in order to be used. This can
+ * be done by ensuring this section exists at the default location of the VTOR
+ * so it's used on reset, or by explicitly setting the VTOR in a bootloader
+ * manually to point to &pw_boot_vector_table_addr before interrupts are
+ * enabled.
+ *
+ * The ARMv8-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates this
+ * device's vector table.
+ */
+ .vector_table : ALIGN(512)
+ {
+ pw_boot_vector_table_addr = .;
+ KEEP(*(.vector_table))
+ } >VECTOR_TABLE
+
+ /* Represents unused space in the VECTOR_TABLE segment. This MUST be the last
+ * section assigned to the VECTOR_TABLE region.
+ */
+ .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
+ } >VECTOR_TABLE
+
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ . = ALIGN(4);
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ /* Constants.*/
+ *(.rodata)
+ *(.rodata*)
+
+ /* Glue ARM to Thumb code, and vice-versa */
+ *(.glue_7)
+ *(.glue_7t)
+
+ /* Exception handling frame */
+ *(.eh_frame)
+
+ /* .preinit_array, .init_array, .fini_array are used by libc.
+ * Each section is a list of function pointers that are called pre-main and
+ * post-exit for object initialization and tear-down.
+ * Since the region isn't explicitly referenced, specify KEEP to prevent
+ * link-time garbage collection. SORT is used for sections that have strict
+ * init/de-init ordering requirements. */
+ . = ALIGN(4);
+ PROVIDE_HIDDEN(__preinit_array_start = .);
+ KEEP(*(.preinit_array*))
+ PROVIDE_HIDDEN(__preinit_array_end = .);
+
+ PROVIDE_HIDDEN(__init_array_start = .);
+ KEEP(*(SORT(.init_array.*)))
+ KEEP(*(.init_array*))
+ PROVIDE_HIDDEN(__init_array_end = .);
+
+ PROVIDE_HIDDEN(__fini_array_start = .);
+ KEEP(*(SORT(.fini_array.*)))
+ KEEP(*(.fini_array*))
+ PROVIDE_HIDDEN(__fini_array_end = .);
+ } >FLASH
+
+ /* Used by unwind-arm/ */
+ .ARM : ALIGN(4) {
+ __exidx_start = .;
+ *(.ARM.exidx*)
+ __exidx_end = .;
+ } >FLASH
+
+ /* Explicitly initialized global and static data. (.data)*/
+ .static_init_ram : ALIGN(4)
+ {
+ *(CodeQuickAccess)
+ *(DataQuickAccess)
+ *(.data)
+ *(.data*)
+ . = ALIGN(4);
+ } >RAM AT> FLASH
+
+ /* Represents unused space in the FLASH segment. This MUST be the last section
+ * assigned to the FLASH region.
+ */
+ .FLASH.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
+ } >FLASH
+
+ /* The .zero_init_ram, .heap, and .stack sections below require (NOLOAD)
+ * annotations for LLVM lld, but not GNU ld, because LLVM's lld intentionally
+ * interprets the linker file differently from ld:
+ *
+ * https://discourse.llvm.org/t/lld-vs-ld-section-type-progbits-vs-nobits/5999/3
+ *
+ * Zero initialized global/static data (.bss) is initialized in
+ * pw_boot_Entry() via memset(), so the section doesn't need to be loaded from
+ * flash. The .heap and .stack sections don't require any initialization,
+ * as they only represent allocated memory regions, so they also do not need
+ * to be loaded.
+ */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
+ {
+ *(.bss)
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } >RAM
+
+ .heap (NOLOAD) : ALIGN(4)
+ {
+ pw_boot_heap_low_addr = .;
+ . = . + PW_BOOT_HEAP_SIZE;
+ . = ALIGN(4);
+ pw_boot_heap_high_addr = .;
+ } >RAM
+
+ /* Link-time check for stack overlaps.
+ *
+ * The ARMv8-M architecture requires 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts, so this region is 8-byte aligned (see
+ * ARMv8-M Architecture Reference Manual DDI0553 section B3.8).
+ */
+ .stack (NOLOAD) : ALIGN(8)
+ {
+ /* Set the address that the main stack pointer should be initialized to. */
+ pw_boot_stack_low_addr = .;
+ HIDDEN(_stack_size = ORIGIN(RAM) + LENGTH(RAM) - .);
+ /* Align the stack to a lower address to ensure it isn't out of range. */
+ HIDDEN(_stack_high = (. + _stack_size) & ~0x7);
+ ASSERT(_stack_high - . >= PW_BOOT_MIN_STACK_SIZE,
+ "Error: Not enough RAM for desired minimum stack size.");
+ . = _stack_high;
+ pw_boot_stack_high_addr = .;
+ } >RAM
+
+ /* Represents unused space in the RAM segment. This MUST be the last section
+ * assigned to the RAM region.
+ */
+ .RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
+ } >RAM
+
+ m_usb_bdt (NOLOAD) :
+ {
+ . = ALIGN(512);
+ *(m_usb_bdt)
+ } >USB_SRAM
+
+ m_usb_global (NOLOAD) :
+ {
+ *(m_usb_global)
+ } >USB_SRAM
+
+ /* Represents unused space in the USB_SRAM segment. This MUST be the last
+ * section assigned to the USB_SRAM region.
+ */
+ .USB_SRAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(USB_SRAM) + LENGTH(USB_SRAM));
+ } >USB_SRAM
+
+ /* Discard unwind info. */
+ .ARM.extab 0x0 (INFO) :
+ {
+ KEEP(*(.ARM.extab*))
+ }
+
+ .ARM.attributes 0 : { *(.ARM.attributes) }
+}
+
+/* Symbols used by core_init.c: */
+/* Start of .static_init_ram in FLASH. */
+_pw_static_init_flash_start = LOADADDR(.static_init_ram);
+
+/* Region of .static_init_ram in RAM. */
+_pw_static_init_ram_start = ADDR(.static_init_ram);
+_pw_static_init_ram_end = _pw_static_init_ram_start + SIZEOF(.static_init_ram);
+
+/* Region of .zero_init_ram. */
+_pw_zero_init_ram_start = ADDR(.zero_init_ram);
+_pw_zero_init_ram_end = _pw_zero_init_ram_start + SIZEOF(.zero_init_ram);
+
+/* Size of image for bootloader header. */
+_pw_image_size = _pw_static_init_flash_start + (_pw_static_init_ram_end - _pw_static_init_ram_start) - pw_boot_vector_table_addr;
+
+/* arm-none-eabi expects `end` symbol to point to start of heap for sbrk. */
+PROVIDE(end = _pw_zero_init_ram_end);
diff --git a/targets/mimxrt595_evk_freertos/target_docs.rst b/targets/mimxrt595_evk_freertos/target_docs.rst
new file mode 100644
index 000000000..e8a405e66
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/target_docs.rst
@@ -0,0 +1,162 @@
+.. _target-mimxrt595-evk-freertos:
+
+======================
+mimxrt595-evk-freertos
+======================
+The NXP MIMXRT595-EVK_ evaluation board is a demonstration target for on-device
+Pigweed development. This target is for the Cortex-M33 ARM core on the RT595.
+It supports FreeRTOS Pigweed backends and includes drivers from the NXP SDK that
+Pigweed has backends for.
+
+.. _MIMXRT595-EVK: https://www.nxp.com/design/development-boards/i-mx-evaluation-and-development-boards/i-mx-rt595-evaluation-kit:MIMXRT595-EVK
+
+-----------
+Configuring
+-----------
+Step 1: Download SDK
+====================
+To configure this Pigweed target you will first need to download an NXP
+`MCUXpresso SDK`_ for your device and unpack it within your project source tree.
+
+.. _MCUXpresso SDK: https://mcuxpresso.nxp.com/en/welcome
+
+Step 2: Create SDK Source Set
+=============================
+You'll next need to create a source set based on the downloaded SDK, and set
+the ``pw_third_party_mcuxpresso_SDK`` build arg to the name of the source set
+you create. See :ref:`module-pw_build_mcuxpresso` for more details.
+
+Alternatively to get started you can start with the basic project template by
+setting the ``pw_target_mimxrt595_evk_freertos_MANIFEST`` build arg to the location of
+the manifest file within the unpacked SDK, and then setting the
+``pw_third_party_mcuxpresso_SDK`` to the ``sdk`` source set within the
+Pigweed target directory.
+
+.. code-block:: sh
+
+ $ gn args out
+ # Modify and save the args file to use the sample SDK.
+ pw_target_mimxrt595_evk_freertos_MANIFEST = "//third_party/mcuxpresso/sdk/EVK-MIMXRT595_manifest_v3_8.xml"
+ pw_third_party_mcuxpresso_SDK = "//targets/mimxrt595_evk_freertos:sdk"
+
+Step 3: Install Freertos Source and Configure Location
+======================================================
+
+.. code-block:: sh
+
+ pw package install freertos
+
+ gn args out
+
+Then add the following line to that text file:
+
+.. code-block::
+
+ dir_pw_third_party_freertos = getenv("PW_PACKAGE_ROOT") + "/freertos"
+
+--------
+Building
+--------
+Once configured, to build for this Pigweed target, build the top-level
+"mimxrt595_freertos" Ninja target.
+
+.. code-block:: sh
+
+ $ ninja -C out mimxrt595_freertos
+
+---------------------
+Running and Debugging
+---------------------
+First Time Setup
+================
+The MIMXRT595-EVK comes with an integrated Link2 debug probe that can operate in
+either CMSIS-DAP or SEGGER J-Link mode. CMSIS-DAP is how the board will likely
+come by default, but J-Link is the mode that is currently known to work, so
+you'll need to flash the Link2 with the correct firmware.
+
+1. Download and install the LPCScrypt_ utility from the NXP website.
+
+2. Place a jumper over **JP1** (not **J1**). If you're having trouble locating
+ this, it's in the top-right of the board in a block of four jumpers closest
+ to the USB ports.
+
+3. Connect a USB cable into the top-right USB port (**J40**) and your computer.
+
+4. Run ``scripts/boot_lpcscrypt`` from the LPCScrypt installation.
+
+5. Run ``scripts/program_JLINK`` from the LPCScrypt installation, press the
+ *SPACE* key to update the firmware.
+
+6. Unplug the USB cable and remove the **JP1** jumper.
+
+Now is also a good time to download and install the J-Link_ package from the
+SEGGER website.
+
+.. _LPCScrypt: https://www.nxp.com/design/microcontrollers-developer-resources/lpcscrypt-v2-1-2:LPCSCRYPT
+.. _J-Link: https://www.segger.com/downloads/jlink/
+
+General Setup
+=============
+Each time you prepare the MIMXRT595-EVK for use, you'll need to do a few steps.
+You don't need to repeat these if you leave everything setup and don't
+disconnect or reboot.
+
+1. Ensure the **SW7** DIP switches are set to Off-Off-On (boot from QSPI Flash).
+
+2. Connect a USB cable into the top-right USB port (**J40**) and your computer.
+
+3. Start the J-Link GDB Server and leave this running:
+
+ .. code-block:: sh
+
+ JLinkGDBServer -select USB -device MIMXRT595S -endian little -if SWD -speed 4000 -noir
+
+On Linux, you may need to install the `libncurses5` library to use the tools:
+
+.. code-block:: sh
+
+ sudo apt install libncurses5
+
+Running and Debugging
+=====================
+Use ``arm-none-eabi-gdb`` to load an executable into the target, debug, and run
+it.
+
+.. code-block::
+ :emphasize-lines: 1,6,10,12,20
+
+ (gdb) target remote :2331
+ Remote debugging using :2331
+ warning: No executable has been specified and target does not support
+ determining executable automatically. Try using the "file" command.
+ 0x08000000 in ?? ()
+ (gdb) file out/mimxrt595_evk_freertos_debug/obj/pw_status/test/status_test.elf
+ A program is being debugged already.
+ Are you sure you want to change the file? (y or n) y
+ Reading symbols from out/mimxrt595_evk_freertos_debug/obj/pw_status/test/status_test.elf...
+ (gdb) monitor reset
+ Resetting target
+ (gdb) load
+ Loading section .flash_config, size 0x200 lma 0x8000400
+ Loading section .vector_table, size 0x168 lma 0x8001000
+ Loading section .code, size 0xb34c lma 0x8001180
+ Loading section .ARM, size 0x8 lma 0x800c4d0
+ Loading section .static_init_ram, size 0x3c8 lma 0x800c4d8
+ Start address 0x080048d0, load size 47748
+ Transfer rate: 15542 KB/sec, 6821 bytes/write.
+ (gdb) monitor reset
+ Resetting target
+
+You can now set any breakpoints you wish, and ``continue`` to run the
+executable.
+
+To reset the target use ``monitor reset``.
+
+To load an updated version of the same file, after resetting the target,
+use ``load`` and a second ``monitor reset`` as shown above.
+
+To debug a new file, use ``file`` before ``load``.
+
+Debug console is available on the USB serial port, e.g. ``/dev/ttyACM0``
+(Linux) or ``/dev/tty.usbmodem*`` (Mac).
+
diff --git a/targets/mimxrt595_evk_freertos/target_toolchains.gni b/targets/mimxrt595_evk_freertos/target_toolchains.gni
new file mode 100644
index 000000000..6ca799237
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/target_toolchains.gni
@@ -0,0 +1,152 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_boot/backend.gni")
+import("$dir_pw_log/backend.gni")
+import("$dir_pw_rpc/system_server/backend.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_sys_io/backend.gni")
+
+import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
+
+_target_config = {
+ # Use the logging main.
+ pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+
+ # Configuration options for Pigweed executable targets.
+ pw_build_EXECUTABLE_TARGET_TYPE = "mimxrt595_executable"
+
+ pw_build_EXECUTABLE_TARGET_TYPE_FILE =
+ get_path_info("mimxrt595_executable.gni", "abspath")
+
+ # Facade backends
+ pw_assert_BACKEND = dir_pw_assert_basic
+ pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv8m"
+ pw_chrono_SYSTEM_CLOCK_BACKEND = "$dir_pw_chrono_freertos:system_clock"
+ pw_chrono_SYSTEM_TIMER_BACKEND = "$dir_pw_chrono_freertos:system_timer"
+ pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv8m"
+ pw_log_BACKEND = dir_pw_log_basic
+ pw_malloc_BACKEND = dir_pw_malloc_freelist
+ pw_sync_BINARY_SEMAPHORE_BACKEND = "$dir_pw_sync_freertos:binary_semaphore"
+ pw_sync_COUNTING_SEMAPHORE_BACKEND =
+ "$dir_pw_sync_freertos:counting_semaphore"
+ pw_sync_MUTEX_BACKEND = "$dir_pw_sync_freertos:mutex"
+ pw_sync_TIMED_MUTEX_BACKEND = "$dir_pw_sync_freertos:timed_mutex"
+ pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
+ "$dir_pw_sync_freertos:interrupt_spin_lock"
+ pw_sync_THREAD_NOTIFICATION_BACKEND =
+ "$dir_pw_sync_freertos:thread_notification"
+ pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND =
+ "$dir_pw_sync_freertos:timed_thread_notification"
+ pw_sys_io_BACKEND = "$dir_pw_sys_io_mcuxpresso"
+ pw_thread_ID_BACKEND = "$dir_pw_thread_freertos:id"
+ pw_thread_SLEEP_BACKEND = "$dir_pw_thread_freertos:sleep"
+ pw_thread_THREAD_BACKEND = "$dir_pw_thread_freertos:thread"
+ pw_thread_YIELD_BACKEND = "$dir_pw_thread_freertos:yield"
+
+ pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
+
+ # Override the default pw_boot_cortex_m linker script and set the memory
+ # regions for the target.
+ pw_boot_cortex_m_LINKER_SCRIPT =
+ "//targets/mimxrt595_evk_freertos:flash_linker_script"
+ pw_target_mimxrt595_evk_freertos_LINK_CONFIG_DEFINES = [
+ "PW_BOOT_FLASH_BEGIN=0x08001180",
+ "PW_BOOT_FLASH_SIZE=0x001FEE80",
+ "PW_BOOT_HEAP_SIZE=200K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20080000",
+ "PW_BOOT_RAM_SIZE=0x00280000",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08001000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=0x00000180",
+ ]
+
+ # Use FreeRTOS with locally specified config and port.
+ pw_third_party_freertos_CONFIG =
+ "//targets/mimxrt595_evk_freertos:freertos_config"
+ pw_third_party_freertos_PORT =
+ "//targets/mimxrt595_evk_freertos:freertos_port"
+
+ pw_build_LINK_DEPS = [
+ "$dir_pw_assert:impl",
+ "$dir_pw_log:impl",
+ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support",
+ ]
+
+ current_cpu = "arm"
+ current_os = ""
+}
+
+_toolchain_properties = {
+ final_binary_extension = ".elf"
+}
+
+_target_default_configs = [
+ "$dir_pw_build:extra_strict_warnings",
+ "$dir_pw_toolchain/arm_gcc:enable_float_printf",
+]
+
+pw_target_toolchain_mimxrt595_evk_freertos = {
+ _excluded_members = [
+ "defaults",
+ "name",
+ ]
+
+ debug = {
+ name = "mimxrt595_evk_freertos_debug"
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m33f_debug
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ forward_variables_from(_toolchain_properties, "*")
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_target_config, "*")
+ default_configs += _target_default_configs
+ }
+ }
+
+ speed_optimized = {
+ name = "mimxrt595_evk_freertos_speed_optimized"
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m33f_speed_optimized
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ forward_variables_from(_toolchain_properties, "*")
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_target_config, "*")
+ default_configs += _target_default_configs
+ }
+ }
+
+ size_optimized = {
+ name = "mimxrt595_evk_freertos_size_optimized"
+ _toolchain_base = pw_toolchain_arm_gcc.cortex_m33f_size_optimized
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ forward_variables_from(_toolchain_properties, "*")
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_target_config, "*")
+ default_configs += _target_default_configs
+ }
+ }
+}
+
+# This list just contains the members of the above scope for convenience to make
+# it trivial to generate all the toolchains in this file via a
+# `generate_toolchains` target.
+pw_target_toolchain_mimxrt595_evk_freertos_list = [
+ pw_target_toolchain_mimxrt595_evk_freertos.debug,
+ pw_target_toolchain_mimxrt595_evk_freertos.speed_optimized,
+ pw_target_toolchain_mimxrt595_evk_freertos.size_optimized,
+]
diff --git a/targets/mimxrt595_evk_freertos/tasks.c b/targets/mimxrt595_evk_freertos/tasks.c
new file mode 100644
index 000000000..7d214e0e1
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/tasks.c
@@ -0,0 +1,40 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "FreeRTOS.h"
+
+// This file provides implementations for idle and timer task memory when
+// configSUPPORT_STATIC_ALLOCATION is set to 1.
+
+void vApplicationGetIdleTaskMemory(StaticTask_t** ppxIdleTaskTCBBuffer,
+ StackType_t** ppxIdleTaskStackBuffer,
+ uint32_t* pulIdleTaskStackSize) {
+ static StaticTask_t tcb;
+ static StackType_t stack[configMINIMAL_STACK_SIZE];
+
+ *ppxIdleTaskTCBBuffer = &tcb;
+ *ppxIdleTaskStackBuffer = stack;
+ *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
+}
+
+void vApplicationGetTimerTaskMemory(StaticTask_t** ppxTimerTaskTCBBuffer,
+ StackType_t** ppxTimerTaskStackBuffer,
+ uint32_t* pulTimerTaskStackSize) {
+ static StaticTask_t tcb;
+ static StackType_t stack[configTIMER_TASK_STACK_DEPTH];
+
+ *ppxTimerTaskTCBBuffer = &tcb;
+ *ppxTimerTaskStackBuffer = stack;
+ *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
+}
diff --git a/targets/mimxrt595_evk_freertos/vector_table.c b/targets/mimxrt595_evk_freertos/vector_table.c
new file mode 100644
index 000000000..f3fd92f20
--- /dev/null
+++ b/targets/mimxrt595_evk_freertos/vector_table.c
@@ -0,0 +1,414 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <stdbool.h>
+
+#include "pw_boot/boot.h"
+#include "pw_boot_cortex_m/boot.h"
+#include "pw_preprocessor/compiler.h"
+
+// Extern symbols provided by linker script.
+// This symbol contains the size of the image.
+extern uint8_t _pw_image_size;
+
+// Default handler to insert into the ARMv8-M vector table (below).
+// This function exists for convenience. If a device isn't doing what you
+// expect, it might have hit a fault and ended up here.
+static void DefaultFaultHandler(void) {
+ while (true) {
+ // Wait for debugger to attach.
+ }
+}
+
+// Default interrupt handler that entries in the ARMv8-M vector table (below)
+// are aliased to, allowing them to be replaced at link time with OS or SDK
+// implementations. If a device isn't doing what you expect, it might have
+// raised an interrupt and ended up here.
+static void DefaultInterruptHandler(void) {
+ while (true) {
+ // Wait for debugger to attach.
+ }
+}
+
+// Default handlers to insert into the ARMv8-M vector table (below) that
+// are likely to be replaced by OS implementations.
+void SVC_Handler(void) PW_ALIAS(DefaultInterruptHandler);
+void DebugMon_Handler(void) PW_ALIAS(DefaultInterruptHandler);
+void PendSV_Handler(void) PW_ALIAS(DefaultInterruptHandler);
+void SysTick_Handler(void) PW_ALIAS(DefaultInterruptHandler);
+
+// Default handlers to insert into the ARMv8-M vector table (below) that
+// are call a driver implementation that may be provided by the SDK.
+static void WDT0_IRQHandler(void);
+static void DMA0_IRQHandler(void);
+static void GPIO_INTA_IRQHandler(void);
+static void GPIO_INTB_IRQHandler(void);
+static void PIN_INT0_IRQHandler(void);
+static void PIN_INT1_IRQHandler(void);
+static void PIN_INT2_IRQHandler(void);
+static void PIN_INT3_IRQHandler(void);
+static void UTICK0_IRQHandler(void);
+static void MRT0_IRQHandler(void);
+static void CTIMER0_IRQHandler(void);
+static void CTIMER1_IRQHandler(void);
+static void SCT0_IRQHandler(void);
+static void CTIMER3_IRQHandler(void);
+static void FLEXCOMM0_IRQHandler(void);
+static void FLEXCOMM1_IRQHandler(void);
+static void FLEXCOMM2_IRQHandler(void);
+static void FLEXCOMM3_IRQHandler(void);
+static void FLEXCOMM4_IRQHandler(void);
+static void FLEXCOMM5_IRQHandler(void);
+static void FLEXCOMM14_IRQHandler(void);
+static void FLEXCOMM15_IRQHandler(void);
+static void ADC0_IRQHandler(void);
+static void Reserved39_IRQHandler(void);
+static void ACMP_IRQHandler(void);
+static void DMIC0_IRQHandler(void);
+static void Reserved42_IRQHandler(void);
+static void HYPERVISOR_IRQHandler(void);
+static void SECURE_VIOLATION_IRQHandler(void);
+static void HWVAD0_IRQHandler(void);
+static void Reserved46_IRQHandler(void);
+static void RNG_IRQHandler(void);
+static void RTC_IRQHandler(void);
+static void DSP_TIE_EXPSTATE1_IRQHandler(void);
+static void MU_A_IRQHandler(void);
+static void PIN_INT4_IRQHandler(void);
+static void PIN_INT5_IRQHandler(void);
+static void PIN_INT6_IRQHandler(void);
+static void PIN_INT7_IRQHandler(void);
+static void CTIMER2_IRQHandler(void);
+static void CTIMER4_IRQHandler(void);
+static void OS_EVENT_IRQHandler(void);
+static void FLEXSPI0_FLEXSPI1_IRQHandler(void);
+static void FLEXCOMM6_IRQHandler(void);
+static void FLEXCOMM7_IRQHandler(void);
+static void USDHC0_IRQHandler(void);
+static void USDHC1_IRQHandler(void);
+static void SGPIO_INTA_IRQHandler(void);
+static void SGPIO_INTB_IRQHandler(void);
+static void I3C0_IRQHandler(void);
+static void USB0_IRQHandler(void);
+static void USB0_NEEDCLK_IRQHandler(void);
+static void WDT1_IRQHandler(void);
+static void USB_PHYDCD_IRQHandler(void);
+static void DMA1_IRQHandler(void);
+static void PUF_IRQHandler(void);
+static void POWERQUAD_IRQHandler(void);
+static void CASPER_IRQHandler(void);
+static void PMU_PMIC_IRQHandler(void);
+static void HASHCRYPT_IRQHandler(void);
+static void FLEXCOMM8_IRQHandler(void);
+static void FLEXCOMM9_IRQHandler(void);
+static void FLEXCOMM10_IRQHandler(void);
+static void FLEXCOMM11_IRQHandler(void);
+static void FLEXCOMM12_IRQHandler(void);
+static void FLEXCOMM13_IRQHandler(void);
+static void FLEXCOMM16_IRQHandler(void);
+static void I3C1_IRQHandler(void);
+static void FLEXIO_IRQHandler(void);
+static void LCDIF_IRQHandler(void);
+static void GPU_IRQHandler(void);
+static void MIPI_IRQHandler(void);
+static void Reserved88_IRQHandler(void);
+static void SDMA_IRQHandler(void);
+
+// This is the device's interrupt vector table. It's not referenced in any
+// code because the platform (EVKMIMXRT595) expects this table to be present
+// at the beginning of flash. The exact address is specified in the pw_boot
+// configuration as part of the target config.
+
+// This typedef is for convenience when building the vector table. With the
+// exception of SP_main (0th entry in the vector table), image length (8th),
+// type (9th), reserved 10th entry, and image load address (13th entry), all
+// the entries of the vector table are function pointers.
+typedef void (*InterruptHandler)(void);
+
+PW_KEEP_IN_SECTION(".vector_table")
+const InterruptHandler vector_table[] = {
+ // Core Level - CM33
+
+ // The starting location of the stack pointer.
+ // This address is NOT an interrupt handler/function pointer, it is simply
+ // the address that the main stack pointer should be initialized to. The
+ // value is reinterpret casted because it needs to be in the vector table.
+ [0] = (InterruptHandler)(&pw_boot_stack_high_addr),
+
+ // Reset handler, dictates how to handle reset interrupt. This is the
+ // address that the Program Counter (PC) is initialized to at boot.
+ [1] = pw_boot_Entry,
+
+ // NMI handler.
+ [2] = DefaultFaultHandler,
+ // HardFault handler.
+ [3] = DefaultFaultHandler,
+ // MemManage (MPU Fault) handler.
+ [4] = DefaultFaultHandler,
+ // BusFault handler.
+ [5] = DefaultFaultHandler,
+ // UsageFault handler.
+ [6] = DefaultFaultHandler,
+ // SecureFault handler.
+ [7] = DefaultFaultHandler,
+ // Image Length.
+ [8] = (InterruptHandler)(&_pw_image_size),
+ // Image Type.
+ [9] = 0,
+ // Reserved.
+ [10] = 0,
+ // SVCall handler.
+ [11] = SVC_Handler,
+ // DebugMon handler.
+ [12] = DebugMon_Handler,
+ // Image Load Address.
+ [13] = (InterruptHandler)(&pw_boot_vector_table_addr),
+ // PendSV handler.
+ [14] = PendSV_Handler,
+ // SysTick handler.
+ [15] = SysTick_Handler,
+
+ // Chip Level - MIMXRT595S_cm33
+
+ // Watchdog timer interrupt.
+ [16] = WDT0_IRQHandler,
+ // DMA interrupt.
+ [17] = DMA0_IRQHandler,
+ // GPIO Interrupt A.
+ [18] = GPIO_INTA_IRQHandler,
+ // GPIO Interrupt B.
+ [19] = GPIO_INTB_IRQHandler,
+ // General Purpose Input/Output interrupt 0.
+ [20] = PIN_INT0_IRQHandler,
+ // General Purpose Input/Output interrupt 1.
+ [21] = PIN_INT1_IRQHandler,
+ // General Purpose Input/Output interrupt 2.
+ [22] = PIN_INT2_IRQHandler,
+ // General Purpose Input/Output interrupt 3.
+ [23] = PIN_INT3_IRQHandler,
+ // Micro-tick Timer.
+ [24] = UTICK0_IRQHandler,
+ // Multi-Rate Timer.
+ [25] = MRT0_IRQHandler,
+ // Standard counter/timer CTIMER0.
+ [26] = CTIMER0_IRQHandler,
+ // Standard counter/timer CTIMER1.
+ [27] = CTIMER1_IRQHandler,
+ // SCTimer/PWM.
+ [28] = SCT0_IRQHandler,
+ // Standard counter/timer CTIMER3.
+ [29] = CTIMER3_IRQHandler,
+ // FlexComm interrupt.
+ [30] = FLEXCOMM0_IRQHandler,
+ // FlexComm interrupt.
+ [31] = FLEXCOMM1_IRQHandler,
+ // FlexComm interrupt.
+ [32] = FLEXCOMM2_IRQHandler,
+ // FlexComm interrupt.
+ [33] = FLEXCOMM3_IRQHandler,
+ // FlexComm interrupt.
+ [34] = FLEXCOMM4_IRQHandler,
+ // FlexComm interrupt.
+ [35] = FLEXCOMM5_IRQHandler,
+ // FlexComm interrupt. Standalone SPI.
+ [36] = FLEXCOMM14_IRQHandler,
+ // FlexComm interrupt. Standalone I2C.
+ [37] = FLEXCOMM15_IRQHandler,
+ // Analog-to-Digital Converter interrupt.
+ [38] = ADC0_IRQHandler,
+ // Reserved interrupt.
+ [39] = Reserved39_IRQHandler,
+ // Analog comparator Interrupts.
+ [40] = ACMP_IRQHandler,
+ // Digital Microphone Interface interrupt.
+ [41] = DMIC0_IRQHandler,
+ // Reserved interrupt.
+ [42] = Reserved42_IRQHandler,
+ // Hypervisor interrupt.
+ [43] = HYPERVISOR_IRQHandler,
+ // Secure violation interrupt.
+ [44] = SECURE_VIOLATION_IRQHandler,
+ // Hardware Voice Activity Detector interrupt.
+ [45] = HWVAD0_IRQHandler,
+ // Reserved interrupt.
+ [46] = Reserved46_IRQHandler,
+ // Random Number Generator interrupt.
+ [47] = RNG_IRQHandler,
+ // Real Time Clock Alarm interrupt OR Wakeup timer interrupt.
+ [48] = RTC_IRQHandler,
+ // DSP interrupt.
+ [49] = DSP_TIE_EXPSTATE1_IRQHandler,
+ // Messaging Unit - Side A.
+ [50] = MU_A_IRQHandler,
+ // General Purpose Input/Output interrupt 4.
+ [51] = PIN_INT4_IRQHandler,
+ // General Purpose Input/Output interrupt 5.
+ [52] = PIN_INT5_IRQHandler,
+ // General Purpose Input/Output interrupt 6.
+ [53] = PIN_INT6_IRQHandler,
+ // General Purpose Input/Output interrupt 7.
+ [54] = PIN_INT7_IRQHandler,
+ // Standard counter/timer CTIMER2.
+ [55] = CTIMER2_IRQHandler,
+ // Standard counter/timer CTIMER4.
+ [56] = CTIMER4_IRQHandler,
+ // Event timer M33 Wakeup/interrupt.
+ [57] = OS_EVENT_IRQHandler,
+ // FlexSPI0_IRQ OR FlexSPI1_IRQ.
+ [58] = FLEXSPI0_FLEXSPI1_IRQHandler,
+ // FlexComm interrupt.
+ [59] = FLEXCOMM6_IRQHandler,
+ // FlexComm interrupt.
+ [60] = FLEXCOMM7_IRQHandler,
+ // USDHC interrupt.
+ [61] = USDHC0_IRQHandler,
+ // USDHC interrupt.
+ [62] = USDHC1_IRQHandler,
+ // Secure GPIO HS interrupt 0.
+ [63] = SGPIO_INTA_IRQHandler,
+ // Secure GPIO HS interrupt 1.
+ [64] = SGPIO_INTB_IRQHandler,
+ // Improved Inter Integrated Circuit 0 interrupt.
+ [65] = I3C0_IRQHandler,
+ // USB device.
+ [66] = USB0_IRQHandler,
+ // USB Activity Wake-up Interrupt.
+ [67] = USB0_NEEDCLK_IRQHandler,
+ // Watchdog timer 1 interrupt.
+ [68] = WDT1_IRQHandler,
+ // USBPHY DCD interrupt.
+ [69] = USB_PHYDCD_IRQHandler,
+ // DMA interrupt.
+ [70] = DMA1_IRQHandler,
+ // QuidKey interrupt.
+ [71] = PUF_IRQHandler,
+ // Powerquad interrupt.
+ [72] = POWERQUAD_IRQHandler,
+ // Caspar interrupt.
+ [73] = CASPER_IRQHandler,
+ // Power Management Control interrupt.
+ [74] = PMU_PMIC_IRQHandler,
+ // SHA interrupt.
+ [75] = HASHCRYPT_IRQHandler,
+ // FlexComm interrupt.
+ [76] = FLEXCOMM8_IRQHandler,
+ // FlexComm interrupt.
+ [77] = FLEXCOMM9_IRQHandler,
+ // FlexComm interrupt.
+ [78] = FLEXCOMM10_IRQHandler,
+ // FlexComm interrupt.
+ [79] = FLEXCOMM11_IRQHandler,
+ // FlexComm interrupt.
+ [80] = FLEXCOMM12_IRQHandler,
+ // FlexComm interrupt.
+ [81] = FLEXCOMM13_IRQHandler,
+ // FlexComm interrupt.
+ [82] = FLEXCOMM16_IRQHandler,
+ // Improved Inter Integrated Circuit 1 interrupt.
+ [83] = I3C1_IRQHandler,
+ // Flexible I/O interrupt.
+ [84] = FLEXIO_IRQHandler,
+ // Liquid Crystal Display interface interrupt.
+ [85] = LCDIF_IRQHandler,
+ // Graphics Processor Unit interrupt.
+ [86] = GPU_IRQHandler,
+ // MIPI interrupt.
+ [87] = MIPI_IRQHandler,
+ // Reserved interrupt.
+ [88] = Reserved88_IRQHandler,
+ // Smart DMA Engine Controller interrupt.
+ [89] = SDMA_IRQHandler,
+};
+
+// Define handlers that call out to a driver handler provided by the SDK.
+#define DRIVER_HANDLER(_IRQHandler, _DriverIRQHandler) \
+ void _DriverIRQHandler(void) PW_ALIAS(DefaultInterruptHandler); \
+ static void _IRQHandler(void) { _DriverIRQHandler(); }
+
+DRIVER_HANDLER(WDT0_IRQHandler, WDT0_DriverIRQHandler);
+DRIVER_HANDLER(DMA0_IRQHandler, DMA0_DriverIRQHandler);
+DRIVER_HANDLER(GPIO_INTA_IRQHandler, GPIO_INTA_DriverIRQHandler);
+DRIVER_HANDLER(GPIO_INTB_IRQHandler, GPIO_INTB_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT0_IRQHandler, PIN_INT0_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT1_IRQHandler, PIN_INT1_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT2_IRQHandler, PIN_INT2_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT3_IRQHandler, PIN_INT3_DriverIRQHandler);
+DRIVER_HANDLER(UTICK0_IRQHandler, UTICK0_DriverIRQHandler);
+DRIVER_HANDLER(MRT0_IRQHandler, MRT0_DriverIRQHandler);
+DRIVER_HANDLER(CTIMER0_IRQHandler, CTIMER0_DriverIRQHandler);
+DRIVER_HANDLER(CTIMER1_IRQHandler, CTIMER1_DriverIRQHandler);
+DRIVER_HANDLER(SCT0_IRQHandler, SCT0_DriverIRQHandler);
+DRIVER_HANDLER(CTIMER3_IRQHandler, CTIMER3_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM0_IRQHandler, FLEXCOMM0_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM1_IRQHandler, FLEXCOMM1_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM2_IRQHandler, FLEXCOMM2_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM3_IRQHandler, FLEXCOMM3_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM4_IRQHandler, FLEXCOMM4_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM5_IRQHandler, FLEXCOMM5_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM14_IRQHandler, FLEXCOMM14_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM15_IRQHandler, FLEXCOMM15_DriverIRQHandler);
+DRIVER_HANDLER(ADC0_IRQHandler, ADC0_DriverIRQHandler);
+DRIVER_HANDLER(Reserved39_IRQHandler, Reserved39_DriverIRQHandler);
+DRIVER_HANDLER(ACMP_IRQHandler, ACMP_DriverIRQHandler);
+DRIVER_HANDLER(DMIC0_IRQHandler, DMIC0_DriverIRQHandler);
+DRIVER_HANDLER(Reserved42_IRQHandler, Reserved42_DriverIRQHandler);
+DRIVER_HANDLER(HYPERVISOR_IRQHandler, HYPERVISOR_DriverIRQHandler);
+DRIVER_HANDLER(SECURE_VIOLATION_IRQHandler, SECURE_VIOLATION_DriverIRQHandler);
+DRIVER_HANDLER(HWVAD0_IRQHandler, HWVAD0_DriverIRQHandler);
+DRIVER_HANDLER(Reserved46_IRQHandler, Reserved46_DriverIRQHandler);
+DRIVER_HANDLER(RNG_IRQHandler, RNG_DriverIRQHandler);
+DRIVER_HANDLER(RTC_IRQHandler, RTC_DriverIRQHandler);
+DRIVER_HANDLER(DSP_TIE_EXPSTATE1_IRQHandler,
+ DSP_TIE_EXPSTATE1_DriverIRQHandler);
+DRIVER_HANDLER(MU_A_IRQHandler, MU_A_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT4_IRQHandler, PIN_INT4_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT5_IRQHandler, PIN_INT5_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT6_IRQHandler, PIN_INT6_DriverIRQHandler);
+DRIVER_HANDLER(PIN_INT7_IRQHandler, PIN_INT7_DriverIRQHandler);
+DRIVER_HANDLER(CTIMER2_IRQHandler, CTIMER2_DriverIRQHandler);
+DRIVER_HANDLER(CTIMER4_IRQHandler, CTIMER4_DriverIRQHandler);
+DRIVER_HANDLER(OS_EVENT_IRQHandler, OS_EVENT_DriverIRQHandler);
+DRIVER_HANDLER(FLEXSPI0_FLEXSPI1_IRQHandler,
+ FLEXSPI0_FLEXSPI1_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM6_IRQHandler, FLEXCOMM6_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM7_IRQHandler, FLEXCOMM7_DriverIRQHandler);
+DRIVER_HANDLER(USDHC0_IRQHandler, USDHC0_DriverIRQHandler);
+DRIVER_HANDLER(USDHC1_IRQHandler, USDHC1_DriverIRQHandler);
+DRIVER_HANDLER(SGPIO_INTA_IRQHandler, SGPIO_INTA_DriverIRQHandler);
+DRIVER_HANDLER(SGPIO_INTB_IRQHandler, SGPIO_INTB_DriverIRQHandler);
+DRIVER_HANDLER(I3C0_IRQHandler, I3C0_DriverIRQHandler);
+DRIVER_HANDLER(USB0_IRQHandler, USB0_DriverIRQHandler);
+DRIVER_HANDLER(USB0_NEEDCLK_IRQHandler, USB0_NEEDCLK_DriverIRQHandler);
+DRIVER_HANDLER(WDT1_IRQHandler, WDT1_DriverIRQHandler);
+DRIVER_HANDLER(USB_PHYDCD_IRQHandler, USB_PHYDCD_DriverIRQHandler);
+DRIVER_HANDLER(DMA1_IRQHandler, DMA1_DriverIRQHandler);
+DRIVER_HANDLER(PUF_IRQHandler, PUF_DriverIRQHandler);
+DRIVER_HANDLER(POWERQUAD_IRQHandler, POWERQUAD_DriverIRQHandler);
+DRIVER_HANDLER(CASPER_IRQHandler, CASPER_DriverIRQHandler);
+DRIVER_HANDLER(PMU_PMIC_IRQHandler, PMU_PMIC_DriverIRQHandler);
+DRIVER_HANDLER(HASHCRYPT_IRQHandler, HASHCRYPT_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM8_IRQHandler, FLEXCOMM8_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM9_IRQHandler, FLEXCOMM9_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM10_IRQHandler, FLEXCOMM10_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM11_IRQHandler, FLEXCOMM11_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM12_IRQHandler, FLEXCOMM12_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM13_IRQHandler, FLEXCOMM13_DriverIRQHandler);
+DRIVER_HANDLER(FLEXCOMM16_IRQHandler, FLEXCOMM16_DriverIRQHandler);
+DRIVER_HANDLER(I3C1_IRQHandler, I3C1_DriverIRQHandler);
+DRIVER_HANDLER(FLEXIO_IRQHandler, FLEXIO_DriverIRQHandler);
+DRIVER_HANDLER(LCDIF_IRQHandler, LCDIF_DriverIRQHandler);
+DRIVER_HANDLER(GPU_IRQHandler, GPU_DriverIRQHandler);
+DRIVER_HANDLER(MIPI_IRQHandler, MIPI_DriverIRQHandler);
+DRIVER_HANDLER(Reserved88_IRQHandler, Reserved88_DriverIRQHandler);
+DRIVER_HANDLER(SDMA_IRQHandler, SDMA_DriverIRQHandler);
diff --git a/targets/rp2040/BUILD.bazel b/targets/rp2040/BUILD.bazel
index ddd50ed85..d37fe0d8c 100644
--- a/targets/rp2040/BUILD.bazel
+++ b/targets/rp2040/BUILD.bazel
@@ -25,7 +25,7 @@ licenses(["notice"])
# is missing from the bazel build. There's no plans yet to do a Bazel build for
# the Pi Pico.
#
-# TODO(b/260639642): Support Pico in the Bazel build.
+# TODO: b/260639642 - Support Pico in the Bazel build.
pw_cc_library(
name = "pico_logging_test_main",
srcs = [
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index 329d95ea6..9e2c9e536 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -20,6 +20,10 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
+declare_args() {
+ pw_targets_ENABLE_RP2040_TEST_RUNNER = false
+}
+
if (current_toolchain != default_toolchain) {
pw_source_set("pico_logging_test_main") {
deps = [
@@ -30,6 +34,13 @@ if (current_toolchain != default_toolchain) {
]
sources = [ "pico_logging_test_main.cc" ]
}
+
+ # We don't want this linked into the boot_stage2 binary, so make the printf
+ # float config a source set added to pw_build_LINK_DEPS (which is dropped on
+ # the boot_stage2 binary) rather than as a default_config.
+ pw_source_set("float_printf_adapter") {
+ all_dependent_configs = [ "$dir_pw_toolchain/arm_gcc:enable_float_printf" ]
+ }
}
generate_toolchain("rp2040") {
@@ -47,13 +58,19 @@ generate_toolchain("rp2040") {
pw_build_EXECUTABLE_TARGET_TYPE = "pico_executable"
pw_build_EXECUTABLE_TARGET_TYPE_FILE =
get_path_info("pico_executable.gni", "abspath")
+ if (pw_targets_ENABLE_RP2040_TEST_RUNNER) {
+ _test_runner_script = "py/rp2040_utils/unit_test_client.py"
+ pw_unit_test_AUTOMATIC_RUNNER =
+ get_path_info(_test_runner_script, "abspath")
+ }
pw_unit_test_MAIN = "$dir_pigweed/targets/rp2040:pico_logging_test_main"
pw_assert_BACKEND = dir_pw_assert_basic
pw_log_BACKEND = dir_pw_log_basic
- pw_sys_io_BACKEND = dir_pw_sys_io_pico
+ pw_sys_io_BACKEND = dir_pw_sys_io_rp2040
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
+ pw_chrono_SYSTEM_CLOCK_BACKEND = "$dir_pw_chrono_rp2040:system_clock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
@@ -63,8 +80,11 @@ generate_toolchain("rp2040") {
pw_build_LINK_DEPS = [
"$dir_pw_assert:impl",
"$dir_pw_log:impl",
+ get_path_info(":float_printf_adapter", "abspath"),
]
+ default_configs += [ "$dir_pw_build:extra_strict_warnings" ]
+
current_cpu = "arm"
current_os = ""
}
diff --git a/targets/rp2040/pico_executable.gni b/targets/rp2040/pico_executable.gni
index baba6f3c0..14eb1026c 100644
--- a/targets/rp2040/pico_executable.gni
+++ b/targets/rp2040/pico_executable.gni
@@ -33,8 +33,11 @@ template("pico_executable") {
forward_variables_from(invoker, "*")
}
- pw_exec(_uf2_name) {
+ pw_exec(target_name) {
_elf2uf2_target = "$dir_pw_third_party/pico_sdk/src:elf2uf2($dir_pigweed/targets/host:host_clang_debug)"
+ if (host_os == "win") {
+ _elf2uf2_target = "$dir_pw_third_party/pico_sdk/src:elf2uf2($dir_pigweed/targets/host:host_gcc_debug)"
+ }
_uf2_out_path = "${target_out_dir}/${_uf2_name}"
deps = [
":${_elf_name}",
@@ -47,8 +50,5 @@ template("pico_executable") {
]
outputs = [ _uf2_out_path ]
}
- group(target_name) {
- deps = [ ":${_uf2_name}" ]
- }
}
}
diff --git a/targets/rp2040/py/BUILD.gn b/targets/rp2040/py/BUILD.gn
new file mode 100644
index 000000000..ef0eee7a0
--- /dev/null
+++ b/targets/rp2040/py/BUILD.gn
@@ -0,0 +1,37 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ ]
+ sources = [
+ "rp2040_utils/__init__.py",
+ "rp2040_utils/device_detector.py",
+ "rp2040_utils/unit_test_client.py",
+ "rp2040_utils/unit_test_runner.py",
+ "rp2040_utils/unit_test_server.py",
+ ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_unit_test/py",
+ ]
+}
diff --git a/targets/rp2040/py/pyproject.toml b/targets/rp2040/py/pyproject.toml
new file mode 100644
index 000000000..78668a709
--- /dev/null
+++ b/targets/rp2040/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/targets/rp2040/py/rp2040_utils/__init__.py b/targets/rp2040/py/rp2040_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/__init__.py
diff --git a/targets/rp2040/py/rp2040_utils/device_detector.py b/targets/rp2040/py/rp2040_utils/device_detector.py
new file mode 100644
index 000000000..04172c4ac
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/device_detector.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Detects attached Raspberry Pi Pico boards."""
+
+from dataclasses import dataclass
+import logging
+from typing import Dict, List
+
+import serial.tools.list_ports # type: ignore
+import usb # type: ignore
+
+import pw_cli.log
+
+# Vendor/device ID to search for in USB devices.
+_RASPBERRY_PI_VENDOR_ID = 0x2E8A
+_PICO_USB_SERIAL_DEVICE_ID = 0x000A
+_PICO_BOOTLOADER_DEVICE_ID = 0x0003
+_PICO_DEVICE_IDS = (
+ _PICO_USB_SERIAL_DEVICE_ID,
+ _PICO_BOOTLOADER_DEVICE_ID,
+)
+
+_LOG = logging.getLogger('pi_pico_detector')
+
+
+@dataclass
+class BoardInfo:
+ """Information about a connected Pi Pico board."""
+
+ serial_port: str
+ bus: int
+ port: int
+
+ # As a board is flashed and reset, the USB address can change. This method
+ # uses the USB bus and port to try and find the desired device. Using the
+ # serial number sounds appealing, but unfortunately the application's serial
+ # number is different from the bootloader's.
+ def address(self) -> int:
+ devices = usb.core.find(
+ find_all=True,
+ idVendor=_RASPBERRY_PI_VENDOR_ID,
+ )
+ for device in devices:
+ if device.idProduct not in _PICO_DEVICE_IDS:
+ raise ValueError(
+ 'Unknown device type on bus %d port %d'
+ % (self.bus, self.port)
+ )
+ if device.port_number == self.port:
+ return device.address
+ raise ValueError(
+ (
+ 'No Pico found, it may have been disconnected or flashed with '
+ 'an incompatible application'
+ )
+ )
+
+
+@dataclass
+class _BoardSerialInfo:
+ """Information that ties a serial number to a serial com port."""
+
+ serial_port: str
+ serial_number: str
+
+
+@dataclass
+class _BoardUsbInfo:
+ """Information that ties a serial number to a USB information"""
+
+ serial_number: str
+ bus: int
+ port: int
+
+
+def _detect_pico_usb_info() -> Dict[str, _BoardUsbInfo]:
+ """Finds Raspberry Pi Pico devices and retrieves USB info for each one."""
+ boards: Dict[str, _BoardUsbInfo] = {}
+ devices = usb.core.find(
+ find_all=True,
+ idVendor=_RASPBERRY_PI_VENDOR_ID,
+ )
+
+ if not devices:
+ return boards
+
+ for device in devices:
+ if device.idProduct == _PICO_USB_SERIAL_DEVICE_ID:
+ boards[device.serial_number] = _BoardUsbInfo(
+ serial_number=device.serial_number,
+ bus=device.bus,
+ port=device.port_number,
+ )
+ elif device.idProduct == _PICO_BOOTLOADER_DEVICE_ID:
+ _LOG.error(
+ 'Found a Pi Pico in bootloader mode on bus %d address %d',
+ device.bus,
+ device.address,
+ )
+ _LOG.error(
+ (
+ 'Please flash and reboot the Pico into an application '
+ 'utilizing USB serial to properly detect it'
+ )
+ )
+
+ else:
+ _LOG.error('Unknown/incompatible Raspberry Pi detected')
+ _LOG.error(
+ (
+ 'Make sure your Pi Pico is running an application '
+ 'utilizing USB serial'
+ )
+ )
+ return boards
+
+
+def _detect_pico_serial_ports() -> Dict[str, _BoardSerialInfo]:
+ """Finds the serial com port associated with each Raspberry Pi Pico."""
+ boards = {}
+ all_devs = serial.tools.list_ports.comports()
+ for dev in all_devs:
+ if (
+ dev.vid == _RASPBERRY_PI_VENDOR_ID
+ and dev.pid == _PICO_USB_SERIAL_DEVICE_ID
+ ):
+ if dev.serial_number is None:
+ raise ValueError('Found pico with no serial number')
+ boards[dev.serial_number] = _BoardSerialInfo(
+ serial_port=dev.device,
+ serial_number=dev.serial_number,
+ )
+ return boards
+
+
+def detect_boards() -> List[BoardInfo]:
+ """Detects attached Raspberry Pi Pico boards in USB serial mode.
+
+ Returns:
+ A list of all found boards as BoardInfo objects.
+ """
+ serial_devices = _detect_pico_serial_ports()
+ pico_usb_info = _detect_pico_usb_info()
+ boards = []
+ for serial_number, usb_info in pico_usb_info.items():
+ if serial_number in serial_devices:
+ serial_info = serial_devices[serial_number]
+ boards.append(
+ BoardInfo(
+ serial_port=serial_info.serial_port,
+ bus=usb_info.bus,
+ port=usb_info.port,
+ )
+ )
+ return boards
+
+
+def main():
+ """Detects and then prints all attached Raspberry Pi Picos."""
+ pw_cli.log.install()
+
+ boards = detect_boards()
+ if not boards:
+ _LOG.info('No attached boards detected')
+ for idx, board in enumerate(boards):
+ _LOG.info('Board %d:', idx)
+ _LOG.info(' %s', board)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/targets/rp2040/py/rp2040_utils/py.typed b/targets/rp2040/py/rp2040_utils/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/py.typed
diff --git a/targets/rp2040/py/rp2040_utils/unit_test_client.py b/targets/rp2040/py/rp2040_utils/unit_test_client.py
new file mode 100644
index 000000000..dfbc31208
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/unit_test_client.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launch a pw_target_runner client that sends a test request."""
+
+import argparse
+import subprocess
+import sys
+
+try:
+ from rp2040_utils import unit_test_server
+except ImportError:
+ # Load from this directory if rp2040_utils is not available.
+ import unit_test_server # type: ignore
+
+_TARGET_CLIENT_COMMAND = 'pw_target_runner_client'
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument('binary', help='The target test binary to run')
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=unit_test_server.DEFAULT_PORT,
+ help='Port the test server is located on',
+ )
+
+ return parser.parse_args()
+
+
+def launch_client(binary: str, server_port: int) -> int:
+ """Sends a test request to the specified server port."""
+ cmd = (_TARGET_CLIENT_COMMAND, '-binary', binary, '-port', str(server_port))
+ return subprocess.call(cmd)
+
+
+def main() -> int:
+ """Launch a test by sending a request to a pw_target_runner_server."""
+ args = parse_args()
+ return launch_client(args.binary, args.server_port)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/targets/rp2040/py/rp2040_utils/unit_test_runner.py b/targets/rp2040/py/rp2040_utils/unit_test_runner.py
new file mode 100644
index 000000000..a53a9500c
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/unit_test_runner.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""This script flashes and runs unit tests on Raspberry Pi Pico boards."""
+
+import argparse
+import logging
+import subprocess
+import sys
+import time
+from pathlib import Path
+
+import serial # type: ignore
+
+import pw_cli.log
+from pw_unit_test import serial_test_runner
+from pw_unit_test.serial_test_runner import (
+ SerialTestingDevice,
+ DeviceNotFound,
+ TestingFailure,
+)
+from rp2040_utils import device_detector
+from rp2040_utils.device_detector import BoardInfo
+
+
+_LOG = logging.getLogger("unit_test_runner")
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ 'binary', type=Path, help='The target test binary to run'
+ )
+ parser.add_argument(
+ '--usb-bus',
+ type=int,
+ help='The bus this Pi Pico is on',
+ )
+ parser.add_argument(
+ '--usb-port',
+ type=int,
+ help='The port of this Pi Pico on the specified USB bus',
+ )
+ parser.add_argument(
+ '--serial-port',
+ type=str,
+ help='The name of the serial port to connect to when running tests',
+ )
+ parser.add_argument(
+ '-b',
+ '--baud',
+ type=int,
+ default=115200,
+ help='Baud rate to use for serial communication with target device',
+ )
+ parser.add_argument(
+ '--test-timeout',
+ type=float,
+ default=5.0,
+ help='Maximum communication delay in seconds before a '
+ 'test is considered unresponsive and aborted',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action='store_true',
+ help='Output additional logs as the script runs',
+ )
+
+ return parser.parse_args()
+
+
+class PiPicoTestingDevice(SerialTestingDevice):
+ """A SerialTestingDevice implementation for the Pi Pico."""
+
+ def __init__(self, board_info: BoardInfo, baud_rate=115200):
+ self._board_info = board_info
+ self._baud_rate = baud_rate
+
+ def load_binary(self, binary: Path) -> None:
+ cmd = (
+ 'picotool',
+ 'load',
+ '-x',
+ str(binary),
+ '--bus',
+ str(self._board_info.bus),
+ '--address',
+ str(self._board_info.address()),
+ '-F',
+ )
+ process = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
+ if process.returncode:
+ err = (
+ 'Command failed: ' + ' '.join(cmd),
+ str(self._board_info),
+ process.stdout.decode('utf-8', errors='replace'),
+ )
+ raise TestingFailure('\n\n'.join(err))
+ # Wait for serial port to enumerate. This will retry forever.
+ while True:
+ try:
+ serial.Serial(
+ baudrate=self.baud_rate(), port=self.serial_port()
+ )
+ except serial.serialutil.SerialException:
+ time.sleep(0.001)
+ else:
+ break
+
+ def serial_port(self) -> str:
+ return self._board_info.serial_port
+
+ def baud_rate(self) -> int:
+ return self._baud_rate
+
+
+def _run_test(
+ device: PiPicoTestingDevice, binary: Path, test_timeout: float
+) -> bool:
+ return serial_test_runner.run_device_test(device, binary, test_timeout)
+
+
+def run_device_test(
+ binary: Path,
+ test_timeout: float,
+ serial_port: str,
+ baud_rate: int,
+ usb_bus: int,
+ usb_port: int,
+) -> bool:
+ """Flashes, runs, and checks an on-device test binary.
+
+ Returns true on test pass.
+ """
+ board = BoardInfo(serial_port, usb_bus, usb_port)
+ return _run_test(
+ PiPicoTestingDevice(board, baud_rate), binary, test_timeout
+ )
+
+
+def detect_and_run_test(binary: Path, test_timeout: float, baud_rate: int):
+ _LOG.debug('Attempting to automatically detect dev board')
+ boards = device_detector.detect_boards()
+ if not boards:
+ error = 'Could not find an attached device'
+ _LOG.error(error)
+ raise DeviceNotFound(error)
+ return _run_test(
+ PiPicoTestingDevice(boards[0], baud_rate), binary, test_timeout
+ )
+
+
+def main():
+ """Set up runner, and then flash/run device test."""
+ args = parse_args()
+ log_level = logging.DEBUG if args.verbose else logging.INFO
+ pw_cli.log.install(level=log_level)
+
+ test_passed = False
+ if not args.serial_port:
+ test_passed = detect_and_run_test(
+ args.binary, args.test_timeout, args.baud
+ )
+ else:
+ test_passed = run_device_test(
+ args.binary,
+ args.test_timeout,
+ args.serial_port,
+ args.baud,
+ args.usb_bus,
+ args.usb_port,
+ )
+
+ sys.exit(0 if test_passed else 1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/targets/rp2040/py/rp2040_utils/unit_test_server.py b/targets/rp2040/py/rp2040_utils/unit_test_server.py
new file mode 100644
index 000000000..62c5b9461
--- /dev/null
+++ b/targets/rp2040/py/rp2040_utils/unit_test_server.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+"""Launch a pw_target_runner server to use for multi-device testing."""
+
+import argparse
+import logging
+import sys
+import tempfile
+from typing import IO, List, Optional
+
+import pw_cli.process
+import pw_cli.log
+
+try:
+ from rp2040_utils import device_detector
+except ImportError:
+ # Load from this directory if rp2040_utils is not available.
+ import device_detector # type: ignore
+
+_LOG = logging.getLogger('unit_test_server')
+
+DEFAULT_PORT = 34172
+
+_TEST_RUNNER_COMMAND = 'rp2040_unit_test_runner'
+_TEST_SERVER_COMMAND = 'pw_target_runner_server'
+
+
+def parse_args():
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=DEFAULT_PORT,
+ help='Port to launch the pw_target_runner_server on',
+ )
+ parser.add_argument(
+ '--server-config',
+ type=argparse.FileType('r'),
+ help='Path to server config file',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs',
+ )
+
+ return parser.parse_args()
+
+
+def generate_runner(command: str, arguments: List[str]) -> str:
+ """Generates a text-proto style pw_target_runner_server configuration."""
+ # TODO(amontanez): Use a real proto library to generate this when we have
+ # one set up.
+ for i, arg in enumerate(arguments):
+ arguments[i] = f' args: "{arg}"'
+ runner = ['runner {', f' command:"{command}"']
+ runner.extend(arguments)
+ runner.append('}\n')
+ return '\n'.join(runner)
+
+
+def generate_server_config() -> IO[bytes]:
+ """Returns a temporary generated file for use as the server config."""
+ boards = device_detector.detect_boards()
+ if not boards:
+ _LOG.critical('No attached boards detected')
+ sys.exit(1)
+ config_file = tempfile.NamedTemporaryFile()
+ _LOG.debug('Generating test server config at %s', config_file.name)
+ _LOG.debug('Found %d attached devices', len(boards))
+
+ # TODO: b/290245354 - Multi-device flashing doesn't work due to limitations
+ # of picotool. Limit to one device even if multiple are connected.
+ if boards:
+ boards = boards[:1]
+
+ for board in boards:
+ test_runner_args = [
+ '--usb-bus',
+ str(board.bus),
+ '--usb-port',
+ str(board.port),
+ '--serial-port',
+ board.serial_port,
+ ]
+ config_file.write(
+ generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode(
+ 'utf-8'
+ )
+ )
+ config_file.flush()
+ return config_file
+
+
+def launch_server(
+ server_config: Optional[IO[bytes]], server_port: Optional[int]
+) -> int:
+ """Launch a device test server with the provided arguments."""
+ if server_config is None:
+ # Auto-detect attached boards if no config is provided.
+ server_config = generate_server_config()
+
+ cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name]
+
+ if server_port is not None:
+ cmd.extend(['-port', str(server_port)])
+
+ return pw_cli.process.run(*cmd, log_output=True).returncode
+
+
+def main():
+ """Launch a device test server with the provided arguments."""
+ args = parse_args()
+
+ log_level = logging.DEBUG if args.verbose else logging.INFO
+ pw_cli.log.install(level=log_level)
+
+ exit_code = launch_server(args.server_config, args.server_port)
+ sys.exit(exit_code)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/targets/rp2040/py/setup.cfg b/targets/rp2040/py/setup.cfg
new file mode 100644
index 000000000..d49b4a20b
--- /dev/null
+++ b/targets/rp2040/py/setup.cfg
@@ -0,0 +1,34 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+[metadata]
+name = rp2040_utils
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Target-specific python scripts for the rp2040 target
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+ pyserial~=3.5.0
+ pyusb
+
+[options.entry_points]
+console_scripts =
+ rp2040_unit_test_runner = rp2040_utils.unit_test_runner:main
+ rp2040_detector = rp2040_utils.device_detector:main
+
+[options.package_data]
+rp2040_utils = py.typed
diff --git a/targets/rp2040/target_docs.rst b/targets/rp2040/target_docs.rst
index 201c7366c..04c77c46c 100644
--- a/targets/rp2040/target_docs.rst
+++ b/targets/rp2040/target_docs.rst
@@ -4,9 +4,9 @@
Raspberry Pi Pico
-----------------
.. warning::
- This target is in an early state and is under active development. Usability
- is not very polished, and many features/configuration options that work in
- upstream Pi Pico CMake build have not yet been ported to the GN build.
+ This target is in an early state and is under active development. Usability
+ is not very polished, and many features/configuration options that work in
+ upstream Pi Pico CMake build have not yet been ported to the GN build.
Setup
=====
@@ -14,27 +14,27 @@ To use this target, Pigweed must be set up to build against the Raspberry Pi
Pico SDK. This can be downloaded via ``pw package``, and then the build must be
manually configured to point to the location of the downloaded SDK.
-.. code:: sh
+.. code-block:: sh
- pw package install pico_sdk
+ pw package install pico_sdk
- gn args out
- # Add this line.
- PICO_SRC_DIR = getenv("PW_PACKAGE_ROOT") + "/pico_sdk"
+ gn args out
+ # Add this line.
+ PICO_SRC_DIR = getenv("PW_PACKAGE_ROOT") + "/pico_sdk"
Linux
-----
On linux, you may need to update your udev rules at
``/etc/udev/rules.d/49-pico.rules`` to include the following:
-.. code:: none
+.. code-block:: none
- SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
- KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
- SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
- KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
- SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
- KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
Usage
=====
@@ -44,9 +44,9 @@ baud rate of 115200.
Once the pico SDK is configured, the Pi Pico will build as part of the default
GN build:
-.. code:: sh
+.. code-block:: sh
- ninja -C out
+ ninja -C out
Pigweed's build will produce ELF and UF2 files for each unit test built for the
Pi Pico.
@@ -66,43 +66,43 @@ Unlike some other targets, the RP2040 does not automatically run tests on boot.
To run a test, flash it to the RP2040 and connect to the serial port and then
press the spacebar to start the test:
-.. code:: none
-
- $ python -m serial.tools.miniterm --raw /dev/ttyACM0 115200
- --- Miniterm on /dev/cu.usbmodem142401 115200,8,N,1 ---
- --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---INF [==========] Running all tests.
- INF [ RUN ] Status.Default
- INF [ OK ] Status.Default
- INF [ RUN ] Status.ConstructWithStatusCode
- INF [ OK ] Status.ConstructWithStatusCode
- INF [ RUN ] Status.AssignFromStatusCode
- INF [ OK ] Status.AssignFromStatusCode
- INF [ RUN ] Status.Ok_OkIsTrue
- INF [ OK ] Status.Ok_OkIsTrue
- INF [ RUN ] Status.NotOk_OkIsFalse
- INF [ OK ] Status.NotOk_OkIsFalse
- INF [ RUN ] Status.Code
- INF [ OK ] Status.Code
- INF [ RUN ] Status.EqualCodes
- INF [ OK ] Status.EqualCodes
- INF [ RUN ] Status.IsError
- INF [ OK ] Status.IsError
- INF [ RUN ] Status.IsNotError
- INF [ OK ] Status.IsNotError
- INF [ RUN ] Status.Strings
- INF [ OK ] Status.Strings
- INF [ RUN ] Status.UnknownString
- INF [ OK ] Status.UnknownString
- INF [ RUN ] Status.Update
- INF [ OK ] Status.Update
- INF [ RUN ] StatusCLinkage.CallCFunctionWithStatus
- INF [ OK ] StatusCLinkage.CallCFunctionWithStatus
- INF [ RUN ] StatusCLinkage.TestStatusFromC
- INF [ OK ] StatusCLinkage.TestStatusFromC
- INF [ RUN ] StatusCLinkage.TestStatusStringsFromC
- INF [ OK ] StatusCLinkage.TestStatusStringsFromC
- INF [==========] Done running all tests.
- INF [ PASSED ] 15 test(s).
+.. code-block:: none
+
+ $ python -m serial.tools.miniterm --raw /dev/ttyACM0 115200
+ --- Miniterm on /dev/cu.usbmodem142401 115200,8,N,1 ---
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---INF [==========] Running all tests.
+ INF [ RUN ] Status.Default
+ INF [ OK ] Status.Default
+ INF [ RUN ] Status.ConstructWithStatusCode
+ INF [ OK ] Status.ConstructWithStatusCode
+ INF [ RUN ] Status.AssignFromStatusCode
+ INF [ OK ] Status.AssignFromStatusCode
+ INF [ RUN ] Status.Ok_OkIsTrue
+ INF [ OK ] Status.Ok_OkIsTrue
+ INF [ RUN ] Status.NotOk_OkIsFalse
+ INF [ OK ] Status.NotOk_OkIsFalse
+ INF [ RUN ] Status.Code
+ INF [ OK ] Status.Code
+ INF [ RUN ] Status.EqualCodes
+ INF [ OK ] Status.EqualCodes
+ INF [ RUN ] Status.IsError
+ INF [ OK ] Status.IsError
+ INF [ RUN ] Status.IsNotError
+ INF [ OK ] Status.IsNotError
+ INF [ RUN ] Status.Strings
+ INF [ OK ] Status.Strings
+ INF [ RUN ] Status.UnknownString
+ INF [ OK ] Status.UnknownString
+ INF [ RUN ] Status.Update
+ INF [ OK ] Status.Update
+ INF [ RUN ] StatusCLinkage.CallCFunctionWithStatus
+ INF [ OK ] StatusCLinkage.CallCFunctionWithStatus
+ INF [ RUN ] StatusCLinkage.TestStatusFromC
+ INF [ OK ] StatusCLinkage.TestStatusFromC
+ INF [ RUN ] StatusCLinkage.TestStatusStringsFromC
+ INF [ OK ] StatusCLinkage.TestStatusStringsFromC
+ INF [==========] Done running all tests.
+ INF [ PASSED ] 15 test(s).
This is done because the serial port enumerated by the Pi Pico goes away on
reboot, so it's not safe to run tests until the port has fully enumerated and
@@ -111,3 +111,50 @@ receives the space character (0x20) as a signal to start running the tests.
The RP2040 does not yet provide an automated test runner with build system
integration.
+
+Automated test runner
+---------------------
+This target supports automatically running on-device tests as part of the GN
+build thanks to a custom ``pw_unit_test_AUTOMATIC_RUNNER`` script.
+
+Step 1: Start test server
+^^^^^^^^^^^^^^^^^^^^^^^^^
+To allow Ninja to properly serialize tests to run on device, Ninja will send
+test requests to a server running in the background. The first step is to launch
+this server. By default, the script will attempt to automatically detect an
+attached Pi Pico running an application with USB serial enabled, then using
+it for testing. To override this behavior, provide a custom server configuration
+file with ``--server-config``.
+
+.. code-block:: sh
+
+ $ python -m rp2040_utils.unit_test_server
+
+.. tip::
+
+ If the server can't find any attached devices, ensure your Pi Pico is
+ already running an application that utilizes USB serial.
+
+.. Warning::
+
+ If you connect or disconnect any boards, you'll need to restart the test
+ server for hardware changes to take effect.
+
+Step 2: Configure GN
+^^^^^^^^^^^^^^^^^^^^
+By default, this hardware target has incremental testing disabled. Enabling the
+``pw_targets_ENABLE_RP2040_TEST_RUNNER`` build arg tells GN to send requests to
+a running ``rp2040_utils.unit_test_server``.
+
+.. code-block:: sh
+
+ $ gn args out
+ # Modify and save the args file to use pw_target_runner.
+ pw_targets_ENABLE_RP2040_TEST_RUNNER = true
+
+Step 3: Build changes
+^^^^^^^^^^^^^^^^^^^^^
+Now, whenever you run ``ninja -C out pi_pico``, all tests affected by changes
+since the last build will be rebuilt and then run on the attached device.
+Alternatively, you may use ``pw watch`` to set up Pigweed to trigger
+builds/tests whenever changes to source files are detected.
diff --git a/targets/rp2040_pw_system/BUILD.gn b/targets/rp2040_pw_system/BUILD.gn
index 286164e60..c2c06f50c 100644
--- a/targets/rp2040_pw_system/BUILD.gn
+++ b/targets/rp2040_pw_system/BUILD.gn
@@ -18,7 +18,6 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_system/system_target.gni")
-import("$dir_pw_tokenizer/backend.gni")
import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
diff --git a/targets/rp2040_pw_system/config/FreeRTOSConfig.h b/targets/rp2040_pw_system/config/FreeRTOSConfig.h
index 728408487..d15db70c9 100644
--- a/targets/rp2040_pw_system/config/FreeRTOSConfig.h
+++ b/targets/rp2040_pw_system/config/FreeRTOSConfig.h
@@ -39,7 +39,7 @@
#define configUSE_COUNTING_SEMAPHORES 0
#define configQUEUE_REGISTRY_SIZE 10
#define configUSE_QUEUE_SETS 0
-#define configUSE_TIME_SLICING 0
+#define configUSE_TIME_SLICING 1
#define configUSE_NEWLIB_REENTRANT 0
#define configENABLE_BACKWARD_COMPATIBILITY 0
#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
diff --git a/targets/rp2040_pw_system/target_docs.rst b/targets/rp2040_pw_system/target_docs.rst
index 035c9b209..4201a7008 100644
--- a/targets/rp2040_pw_system/target_docs.rst
+++ b/targets/rp2040_pw_system/target_docs.rst
@@ -18,12 +18,12 @@ baremetal approach.
-----
Setup
-----
-To use this target, Pigweed must be set up to use FreeRTOS and the STM32Cube HAL
-for the STM32F4 series. The supported repositories can be downloaded via
-``pw package``, and then the build must be manually configured to point to the
-locations the repositories were downloaded to.
+To use this target, Pigweed must be set up to use FreeRTOS and the Pico SDK
+HAL. The supported repositories can be downloaded via ``pw package``, and then
+the build must be manually configured to point to the locations the repositories
+were downloaded to.
-.. code:: sh
+.. code-block:: sh
pw package install nanopb
pw package install freertos
@@ -40,17 +40,17 @@ locations the repositories were downloaded to.
Instead of the ``gn gen out`` with args set on the command line above you can
run:
- .. code:: sh
+ .. code-block:: sh
gn args out
Then add the following lines to that text file:
- .. code::
+ .. code-block::
- dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
- dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
- PICO_SRC_DIR = pw_env_setup_PACKAGE_ROOT + "/pico_sdk"
+ dir_pw_third_party_nanopb = getenv("PW_PACKAGE_ROOT") + "/nanopb"
+ dir_pw_third_party_freertos = getenv("PW_PACKAGE_ROOT") + "/freertos"
+ PICO_SRC_DIR = getenv("PW_PACKAGE_ROOT") + "/pico_sdk"
-----------------------------
Building and Running the Demo
@@ -60,7 +60,7 @@ flashed to a device with the following commands:
**Build**
-.. code:: sh
+.. code-block:: sh
ninja -C out pw_system_demo
@@ -82,7 +82,7 @@ flashed to a device with the following commands:
**Install RaspberryPi's OpenOCD Fork:**
- .. code:: sh
+ .. code-block:: sh
git clone https://github.com/raspberrypi/openocd.git \
--branch picoprobe \
@@ -99,7 +99,7 @@ flashed to a device with the following commands:
**Setup udev rules (Linux only):**
- .. code:: sh
+ .. code-block:: sh
cat <<EOF > 49-picoprobe.rules
SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000[43a]", MODE:="0666"
@@ -110,7 +110,7 @@ flashed to a device with the following commands:
**Flash the Pico:**
- .. code:: sh
+ .. code-block:: sh
~/apps/openocd/bin/openocd -f ~/apps/openocd/share/openocd/scripts/interface/picoprobe.cfg -f ~/apps/openocd/share/openocd/scripts/target/rp2040.cfg -c 'program out/rp2040_pw_system.size_optimized/obj/pw_system/bin/system_example.elf verify reset exit'
@@ -119,7 +119,7 @@ flashed to a device with the following commands:
Once the board has been flashed, you can connect to it and send RPC commands
via the Pigweed console:
-.. code:: sh
+.. code-block:: sh
pw-system-console -d /dev/{ttyX} -b 115200 \
--proto-globs pw_rpc/echo.proto \
@@ -132,7 +132,7 @@ may look like ``ttyACM0``, and on a Mac it may look like ``cu.usbmodem***``.
When the console opens, try sending an Echo RPC request. You should get back
the same message you sent to the device.
-.. code:: pycon
+.. code-block:: pycon
>>> device.rpcs.pw.rpc.EchoService.Echo(msg="Hello, Pigweed!")
(Status.OK, pw.rpc.EchoMessage(msg='Hello, Pigweed!'))
@@ -140,13 +140,13 @@ the same message you sent to the device.
You can also try out our thread snapshot RPC service, which should return a
stack usage overview of all running threads on the device in Host Logs.
-.. code:: pycon
+.. code-block:: pycon
>>> device.snapshot_peak_stack_usage()
Example output:
-.. code::
+.. code-block::
20220826 09:47:22 INF PendingRpc(channel=1, method=pw.thread.ThreadSnapshotService.GetPeakStackUsage) completed: Status.OK
20220826 09:47:22 INF Thread State
diff --git a/targets/stm32f429i_disc1/BUILD.bazel b/targets/stm32f429i_disc1/BUILD.bazel
index 988b58801..6aef9ad8f 100644
--- a/targets/stm32f429i_disc1/BUILD.bazel
+++ b/targets/stm32f429i_disc1/BUILD.bazel
@@ -15,9 +15,6 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
-)
-load(
- "//pw_build/bazel_internal:pigweed_internal.bzl",
"pw_linker_script",
)
@@ -58,7 +55,7 @@ pw_cc_library(
"//pw_preprocessor",
"//pw_sys_io_baremetal_stm32f429",
],
- # TODO(b/251939135): Remove the need for alwayslink by rethinking how
+ # TODO: b/251939135 - Remove the need for alwayslink by rethinking how
# pw_boot_cortex_m is structured in the build system.
alwayslink = 1,
)
diff --git a/targets/stm32f429i_disc1/boot.cc b/targets/stm32f429i_disc1/boot.cc
index 586159b13..e47dec0d3 100644
--- a/targets/stm32f429i_disc1/boot.cc
+++ b/targets/stm32f429i_disc1/boot.cc
@@ -25,7 +25,7 @@
// compile time but does NOT require it to be evaluated at compile time and we
// have to be incredibly careful that this does not end up in the .data section.
void pw_boot_PreStaticMemoryInit() {
-// TODO(b/264897542): Whether the FPU is enabled should be an Arm target trait.
+// TODO: b/264897542 - Whether the FPU is enabled should be an Arm target trait.
#if PW_ARMV7M_ENABLE_FPU
// Enable FPU if built using hardware FPU instructions.
// CPCAR mask that enables FPU. (ARMv7-M Section B3.2.20)
diff --git a/targets/stm32f429i_disc1/py/BUILD.gn b/targets/stm32f429i_disc1/py/BUILD.gn
index a0f356d83..271ca130e 100644
--- a/targets/stm32f429i_disc1/py/BUILD.gn
+++ b/targets/stm32f429i_disc1/py/BUILD.gn
@@ -20,7 +20,6 @@ pw_python_package("py") {
setup = [
"pyproject.toml",
"setup.cfg",
- "setup.py",
]
sources = [
"stm32f429i_disc1_utils/__init__.py",
diff --git a/targets/stm32f429i_disc1/py/setup.py b/targets/stm32f429i_disc1/py/setup.py
deleted file mode 100644
index 9ce25598b..000000000
--- a/targets/stm32f429i_disc1/py/setup.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-"""stm32f429i_disc1_utils"""
-
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
diff --git a/targets/stm32f429i_disc1/target_docs.rst b/targets/stm32f429i_disc1/target_docs.rst
index fcf6d73b0..30d6ce56a 100644
--- a/targets/stm32f429i_disc1/target_docs.rst
+++ b/targets/stm32f429i_disc1/target_docs.rst
@@ -11,7 +11,7 @@ Building
To build for this Pigweed target, simply build the top-level "stm32f429i" Ninja
target.
-.. code:: sh
+.. code-block:: sh
$ ninja -C out stm32f429i
@@ -27,7 +27,7 @@ If using ``out`` as a build directory, tests will be located in
on device, the stm32f429i-disc1 target provides a helper script that flashes the
test to a device and then runs it.
-.. code:: sh
+.. code-block:: sh
# Setup pigweed environment.
$ source activate.sh
@@ -43,7 +43,7 @@ tests, but it can be restricted it to specific ``pw_test_group`` targets using
the ``--group`` argument. Alternatively, individual test binaries can be
specified with the ``--test`` option.
-.. code:: sh
+.. code-block:: sh
# Setup Pigweed environment.
$ source activate.sh
@@ -76,7 +76,7 @@ with ``--server-config``.
If you unplug or plug in any boards, you'll need to restart the test server
for hardware changes to properly be detected.
-.. code:: sh
+.. code-block:: sh
$ stm32f429i_disc1_test_server
@@ -86,7 +86,7 @@ By default, this hardware target has incremental testing via
``pw_target_runner`` disabled. Enabling the ``pw_use_test_server`` build arg
tells GN to send requests to a running ``stm32f429i_disc1_test_server``.
-.. code:: sh
+.. code-block:: sh
$ gn args out
# Modify and save the args file to use pw_target_runner.
@@ -113,20 +113,20 @@ OpenOCD requires a few steps. Summary version of the steps:
#. Connect OpenOCD to the device in terminal A. Leave this running
- .. code:: sh
+ .. code-block:: sh
$ openocd -f targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg
#. Connect GDB to the running OpenOCD instance in terminal B
- .. code:: sh
+ .. code-block:: sh
$ arm-none-eabi-gdb -ex "target remote :3333" \
out/stm32f429i_disc1_debug/obj/pw_assert/test/assert_facade_test.elf
#. Flash (``load``), run (``mon reset run; continue``), and debug
- .. code:: none
+ .. code-block:: none
(gdb) set print pretty on
(gdb) load
@@ -141,13 +141,13 @@ Step 1: Start an OpenOCD server and connect to the device
OpenOCD is a persistent server that you run and leave running to bridge between
GDB and the device. To run it for the Discovery board:
-.. code:: sh
+.. code-block:: sh
$ openocd -f targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg
Typical output:
-.. code:: none
+.. code-block:: none
Open On-Chip Debugger 0.10.0+dev-01243-ge41c0f49-dirty (2020-05-21-10:27)
Licensed under GNU GPL v2
@@ -171,7 +171,7 @@ Step 2: Start GDB and connect to the OpenOCD server
Start GDB pointing to the correct .elf file, and tell it to connect to the
OpenOCD server (running on port 333 by default).
-.. code:: sh
+.. code-block:: sh
$ arm-none-eabi-gdb -ex "target remote :3333" \
out/stm32f429i_disc1_debug/obj/pw_assert/test/assert_facade_test.elf
@@ -179,7 +179,7 @@ OpenOCD server (running on port 333 by default).
In this case the assert facade test is debugged, but substitute your own ELF
file. This should produce output similar to the following:
-.. code:: none
+.. code-block:: none
GNU gdb (GNU Arm Embedded Toolchain 9-2020-q2-update) 8.3.1.20191211-git
Copyright (C) 2019 Free Software Foundation, Inc.
@@ -207,13 +207,13 @@ Now that the GDB instance is connected to the device, you can flash, run, and de
To flash
-.. code:: none
+.. code-block:: none
(gdb) load
This will produce output similar to:
-.. code:: none
+.. code-block:: none
(gdb) load
Loading section .vector_table, size 0x10 lma 0x8000000
@@ -225,14 +225,14 @@ This will produce output similar to:
To reset the device and halt on the first instruction (before main):
-.. code:: none
+.. code-block:: none
(gdb) mon reset run
This will produce output similar to:
-.. code:: none
+.. code-block:: none
(gdb) mon reset run
Unable to match requested speed 2000 kHz, using 1800 kHz
diff --git a/targets/stm32f429i_disc1/target_toolchains.gni b/targets/stm32f429i_disc1/target_toolchains.gni
index d7c0fdf4b..a4123e60f 100644
--- a/targets/stm32f429i_disc1/target_toolchains.gni
+++ b/targets/stm32f429i_disc1/target_toolchains.gni
@@ -14,6 +14,8 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_boot/backend.gni")
+import("$dir_pw_boot_cortex_m/toolchain.gni")
import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_rpc/system_server/backend.gni")
@@ -26,7 +28,7 @@ declare_args() {
}
_target_config = {
- # TODO(b/241565082): Enable NC testing in GN Windows when it is fixed.
+ # TODO: b/241565082 - Enable NC testing in GN Windows when it is fixed.
pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = host_os != "win"
# Use the logging main.
@@ -66,11 +68,12 @@ _target_config = {
pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
pw_malloc_BACKEND = dir_pw_malloc_freelist
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = []
pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
"PW_BOOT_FLASH_BEGIN=0x08000200",
"PW_BOOT_FLASH_SIZE=1024K",
- # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+ # TODO: b/235348465 - Currently "pw_tokenizer/detokenize_test" requires at
# least 6K bytes in heap when using pw_malloc_freelist. The heap size
# required for tests should be investigated.
#
diff --git a/targets/stm32f429i_disc1_stm32cube/BUILD.bazel b/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
index 7922f2968..44109fa46 100644
--- a/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
+++ b/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
@@ -16,6 +16,7 @@ load(
"//pw_build:pigweed.bzl",
"pw_cc_binary",
"pw_cc_library",
+ "pw_linker_script",
)
package(default_visibility = ["//visibility:public"])
@@ -44,16 +45,28 @@ constraint_value(
constraint_setting = "//third_party/freertos:freertos_config_setting",
)
-# TODO(b/261506064): Additional constraint values for configuring stm32cube
-# need to be added here, once constraint settings for stm32cube are defined.
+# TODO: b/301334234 - Set the flags currently in the stm32f429i config in
+# .bazelrc using this platform, once that's supported.
platform(
name = "platform",
constraint_values = [
":freertos_config_cv",
"//pw_build/constraints/rtos:freertos",
+ "//pw_interrupt_cortex_m:backend",
+ "//pw_sys_io_stm32cube:backend",
"@freertos//:port_ARM_CM4F",
+ "@freertos//:disable_task_statics",
+ "@platforms//cpu:armv7e-m",
+ "@pw_toolchain//constraints/arm_mcpu:cortex-m4",
],
- parents = ["@bazel_embedded//platforms:cortex_m4_fpu"],
+)
+
+pw_cc_library(
+ name = "hal_config",
+ hdrs = [
+ "config/stm32f4xx_hal_conf.h",
+ ],
+ includes = ["config"],
)
pw_cc_library(
@@ -62,21 +75,42 @@ pw_cc_library(
"boot.cc",
"vector_table.c",
],
- hdrs = [
- "config/stm32f4xx_hal_conf.h",
- ],
+ copts = ["-Wno-return-type"],
+ defines = ["PW_MALLOC_ACTIVE=1"],
target_compatible_with = [":freertos_config_cv"],
deps = [
":freertos_config",
+ "//pw_assert",
"//pw_boot",
"//pw_boot_cortex_m",
"//pw_malloc",
"//pw_preprocessor",
"//pw_string",
"//pw_sys_io_stm32cube",
+ "//pw_system:init",
"//third_party/stm32cube",
"@freertos",
],
+ alwayslink = 1,
+)
+
+pw_linker_script(
+ name = "linker_script",
+ defines = [
+ "PW_BOOT_FLASH_BEGIN=0x08000200",
+ "PW_BOOT_FLASH_SIZE=2048K",
+
+ # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+ # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+ # required for tests should be investigated.
+ "PW_BOOT_HEAP_SIZE=7K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20000000",
+ "PW_BOOT_RAM_SIZE=192K",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ],
+ linker_script = "//pw_boot_cortex_m:basic_cortex_m.ld",
)
pw_cc_binary(
diff --git a/targets/stm32f429i_disc1_stm32cube/BUILD.gn b/targets/stm32f429i_disc1_stm32cube/BUILD.gn
index da7d9cab7..87a07f88e 100644
--- a/targets/stm32f429i_disc1_stm32cube/BUILD.gn
+++ b/targets/stm32f429i_disc1_stm32cube/BUILD.gn
@@ -19,7 +19,7 @@ import("$dir_pw_docgen/docs.gni")
import("$dir_pw_malloc/backend.gni")
import("$dir_pw_system/system_target.gni")
import("$dir_pw_third_party/stm32cube/stm32cube.gni")
-import("$dir_pw_tokenizer/backend.gni")
+import("$dir_pw_toolchain/arm_clang/toolchains.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
config("pw_malloc_active") {
@@ -64,37 +64,51 @@ if (current_toolchain != default_toolchain) {
}
}
+common_link_deps =
+ [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]
+
+common_build_args = {
+ pw_log_BACKEND = dir_pw_log_tokenized
+ pw_log_tokenized_HANDLER_BACKEND = "$dir_pw_system:log_backend.impl"
+ pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_freertos_config"
+ pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
+ pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
+ dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
+ pw_third_party_stm32cube_PRODUCT = "STM32F429xx"
+ pw_third_party_stm32cube_CONFIG =
+ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_hal_config"
+ pw_third_party_stm32cube_CORE_INIT = ""
+ pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
+ "PW_BOOT_FLASH_BEGIN=0x08000200",
+ "PW_BOOT_FLASH_SIZE=2048K",
+
+ # TODO: b/235348465 - Currently "pw_tokenizer/detokenize_test" requires at
+ # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+ # required for tests should be investigated.
+ "PW_BOOT_HEAP_SIZE=7K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20000000",
+ "PW_BOOT_RAM_SIZE=192K",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ]
+}
+
pw_system_target("stm32f429i_disc1_stm32cube") {
cpu = PW_SYSTEM_CPU.CORTEX_M4F
scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
- link_deps = [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]
- build_args = {
- pw_log_BACKEND = dir_pw_log_tokenized
- pw_log_tokenized_HANDLER_BACKEND = "$dir_pw_system:log_backend.impl"
- pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_freertos_config"
- pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
- pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
- dir_pw_third_party_stm32cube = dir_pw_third_party_stm32cube_f4
- pw_third_party_stm32cube_PRODUCT = "STM32F429xx"
- pw_third_party_stm32cube_CONFIG =
- "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_hal_config"
- pw_third_party_stm32cube_CORE_INIT = ""
- pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
- "PW_BOOT_FLASH_BEGIN=0x08000200",
- "PW_BOOT_FLASH_SIZE=2048K",
+ link_deps = common_link_deps
+ build_args = common_build_args
+}
+
+pw_system_target("stm32f429i_disc1_stm32cube_clang") {
+ cpu = PW_SYSTEM_CPU.CORTEX_M4F
+ scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
+ system_toolchain = pw_toolchain_arm_clang
- # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
- # least 6K bytes in heap when using pw_malloc_freelist. The heap size
- # required for tests should be investigated.
- "PW_BOOT_HEAP_SIZE=7K",
- "PW_BOOT_MIN_STACK_SIZE=1K",
- "PW_BOOT_RAM_BEGIN=0x20000000",
- "PW_BOOT_RAM_SIZE=192K",
- "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
- ]
- }
+ link_deps = common_link_deps
+ build_args = common_build_args
}
pw_doc_group("target_docs") {
diff --git a/targets/stm32f429i_disc1_stm32cube/boot.cc b/targets/stm32f429i_disc1_stm32cube/boot.cc
index 8f00947bf..adca712db 100644
--- a/targets/stm32f429i_disc1_stm32cube/boot.cc
+++ b/targets/stm32f429i_disc1_stm32cube/boot.cc
@@ -17,6 +17,7 @@
#include <array>
#include "FreeRTOS.h"
+#include "pw_assert/check.h"
#include "pw_boot_cortex_m/boot.h"
#include "pw_malloc/malloc.h"
#include "pw_preprocessor/compiler.h"
diff --git a/targets/stm32f429i_disc1_stm32cube/config/stm32f4xx_hal_conf.h b/targets/stm32f429i_disc1_stm32cube/config/stm32f4xx_hal_conf.h
index a3f2e8f49..f7d2c082f 100644
--- a/targets/stm32f429i_disc1_stm32cube/config/stm32f4xx_hal_conf.h
+++ b/targets/stm32f429i_disc1_stm32cube/config/stm32f4xx_hal_conf.h
@@ -14,6 +14,8 @@
#pragma once
+#include <stdint.h>
+
/* Clock setup */
#define HSI_VALUE 16000000U
#define LSI_VALUE 32000U
diff --git a/targets/stm32f429i_disc1_stm32cube/target_docs.rst b/targets/stm32f429i_disc1_stm32cube/target_docs.rst
index 3eaedc89b..7ddc80685 100644
--- a/targets/stm32f429i_disc1_stm32cube/target_docs.rst
+++ b/targets/stm32f429i_disc1_stm32cube/target_docs.rst
@@ -24,7 +24,7 @@ for the STM32F4 series. The supported repositories can be downloaded via
``pw package``, and then the build must be manually configured to point to the
locations the repositories were downloaded to.
-.. code:: sh
+.. code-block:: sh
pw package install nanopb
pw package install freertos
@@ -41,17 +41,17 @@ locations the repositories were downloaded to.
Instead of the ``gn gen out`` with args set on the command line above you can
run:
- .. code:: sh
+ .. code-block:: sh
gn args out
Then add the following lines to that text file:
- .. code::
+ .. code-block::
- dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
- dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
- dir_pw_third_party_stm32cube_f4 = pw_env_setup_PACKAGE_ROOT + "/stm32cube_f4"
+ dir_pw_third_party_nanopb = getenv("PW_PACKAGE_ROOT") + "/nanopb"
+ dir_pw_third_party_freertos = getenv("PW_PACKAGE_ROOT") + "/freertos"
+ dir_pw_third_party_stm32cube_f4 = getenv("PW_PACKAGE_ROOT") + "/stm32cube_f4"
-----------------------------
Building and Running the Demo
@@ -59,11 +59,11 @@ Building and Running the Demo
This target has an associated demo application that can be built and then
flashed to a device with the following commands:
-.. code:: sh
+.. code-block:: sh
ninja -C out pw_system_demo
-.. code:: sh
+.. code-block:: sh
openocd -f targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg \
-c "program out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf reset exit"
@@ -71,7 +71,7 @@ flashed to a device with the following commands:
Once the board has been flashed, you can connect to it and send RPC commands
via the Pigweed console:
-.. code:: sh
+.. code-block:: sh
pw-system-console -d /dev/{ttyX} -b 115200 \
--proto-globs pw_rpc/echo.proto \
@@ -84,7 +84,7 @@ may look like ``ttyACM0``, and on a Mac it may look like ``cu.usbmodem***``.
When the console opens, try sending an Echo RPC request. You should get back
the same message you sent to the device.
-.. code:: pycon
+.. code-block:: pycon
>>> device.rpcs.pw.rpc.EchoService.Echo(msg="Hello, Pigweed!")
(Status.OK, pw.rpc.EchoMessage(msg='Hello, Pigweed!'))
@@ -92,13 +92,13 @@ the same message you sent to the device.
You can also try out our thread snapshot RPC service, which should return a
stack usage overview of all running threads on the device in Host Logs.
-.. code:: pycon
+.. code-block:: pycon
>>> device.snapshot_peak_stack_usage()
Example output:
-.. code::
+.. code-block::
20220826 09:47:22 INF PendingRpc(channel=1, method=pw.thread.ThreadSnapshotService.GetPeakStackUsage) completed: Status.OK
20220826 09:47:22 INF Thread State
diff --git a/third_party/Android.bp b/third_party/Android.bp
index 3ae33b4e7..384e3d9b2 100644
--- a/third_party/Android.bp
+++ b/third_party/Android.bp
@@ -17,20 +17,20 @@ package {
}
cc_library_headers {
- name: "fuschia_sdk_lib_fit",
+ name: "fuchsia_sdk_lib_fit",
cpp_std: "c++20",
vendor_available: true,
export_include_dirs: [
"fuchsia/repo/sdk/lib/fit/include",
],
header_libs: [
- "fuschia_sdk_lib_stdcompat",
+ "fuchsia_sdk_lib_stdcompat",
],
host_supported: true,
}
cc_library_headers {
- name: "fuschia_sdk_lib_stdcompat",
+ name: "fuchsia_sdk_lib_stdcompat",
cpp_std: "c++20",
vendor_available: true,
export_include_dirs: [
diff --git a/third_party/abseil-cpp/BUILD.gn b/third_party/abseil-cpp/BUILD.gn
new file mode 100644
index 000000000..a8a54375f
--- /dev/null
+++ b/third_party/abseil-cpp/BUILD.gn
@@ -0,0 +1,56 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+if (dir_pw_third_party_abseil_cpp != "") {
+ config("abseil_cpp_public_config1") {
+ include_dirs = [ "$dir_pw_third_party_abseil_cpp" ]
+ }
+
+ config("abseil_cpp_config1") {
+ cflags = [
+ "-DNOMINMAX",
+ "-Wall",
+ "-Wcast-qual",
+ "-Wconversion-null",
+ "-Wextra",
+ "-Wformat-security",
+ "-Wmissing-declarations",
+ "-Woverlength-strings",
+ "-Wpointer-arith",
+ "-Wundef",
+ "-Wunused-local-typedefs",
+ "-Wunused-result",
+ "-Wvarargs",
+ "-Wvla",
+ "-Wwrite-strings",
+ ]
+ }
+
+ config("abseil_cpp_config2") {
+ ldflags = [ "-pthread" ]
+ }
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/abseil-cpp/OWNERS b/third_party/abseil-cpp/OWNERS
new file mode 100644
index 000000000..419cfd37a
--- /dev/null
+++ b/third_party/abseil-cpp/OWNERS
@@ -0,0 +1 @@
+aarongreen@google.com
diff --git a/third_party/abseil-cpp/abseil-cpp.gni b/third_party/abseil-cpp/abseil-cpp.gni
new file mode 100644
index 000000000..bc5cd3a5b
--- /dev/null
+++ b/third_party/abseil-cpp/abseil-cpp.gni
@@ -0,0 +1,23 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+
+declare_args() {
+ # If compiling tests with Abseil C++, this variable is set to the path to the
+ # Abseil C++ installation. When set, a pw_source_set for the Abseil C++ library
+ # is created at "$dir_pw_third_party/abseil-cpp".
+ dir_pw_third_party_abseil_cpp = ""
+}
diff --git a/third_party/abseil-cpp/absl/algorithm/BUILD.gn b/third_party/abseil-cpp/absl/algorithm/BUILD.gn
new file mode 100644
index 000000000..dff4bc9ce
--- /dev/null
+++ b/third_party/abseil-cpp/absl/algorithm/BUILD.gn
@@ -0,0 +1,50 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/algorithm.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/algorithm:algorithm
+pw_source_set("algorithm") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/algorithm/algorithm.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/algorithm:container
+pw_source_set("container") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/algorithm/container.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":algorithm",
+ "../base:core_headers",
+ "../meta:type_traits",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/base/BUILD.gn b/third_party/abseil-cpp/absl/base/BUILD.gn
new file mode 100644
index 000000000..51814dd73
--- /dev/null
+++ b/third_party/abseil-cpp/absl/base/BUILD.gn
@@ -0,0 +1,374 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/base.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/base:atomic_hook
+pw_source_set("atomic_hook") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/atomic_hook.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":core_headers",
+ ]
+}
+
+# Generated from //absl/base:base
+pw_source_set("base") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/call_once.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/casts.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/cycleclock.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/low_level_scheduling.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/per_thread_tls.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/sysinfo.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/thread_identity.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/tsan_mutex_interface.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/unscaledcycleclock.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/cycleclock.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/sysinfo.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/thread_identity.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/unscaledcycleclock.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ "../..:abseil_cpp_config2",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":atomic_hook",
+ ":base_internal",
+ ":config",
+ ":core_headers",
+ ":cycleclock_internal",
+ ":dynamic_annotations",
+ ":log_severity",
+ ":raw_logging_internal",
+ ":spinlock_wait",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/base:base_internal
+pw_source_set("base_internal") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/hide_ptr.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/identity.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/inline_variable.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/invoke.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/scheduling_mode.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/base:config
+pw_source_set("config") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/config.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/options.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/policy_checks.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+}
+
+# Generated from //absl/base:core_headers
+pw_source_set("core_headers") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/attributes.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/const_init.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/macros.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/optimization.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/port.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/thread_annotations.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/thread_annotations.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ ":config" ]
+}
+
+# Generated from //absl/base:cycleclock_internal
+pw_source_set("cycleclock_internal") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/cycleclock_config.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/unscaledcycleclock_config.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":base_internal",
+ ":config",
+ ]
+}
+
+# Generated from //absl/base:dynamic_annotations
+pw_source_set("dynamic_annotations") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/base/dynamic_annotations.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/dynamic_annotations.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":core_headers",
+ ]
+}
+
+# Generated from //absl/base:endian
+pw_source_set("endian") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/endian.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/unaligned_access.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":base",
+ ":config",
+ ":core_headers",
+ ]
+}
+
+# Generated from //absl/base:errno_saver
+pw_source_set("errno_saver") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/errno_saver.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ ":config" ]
+}
+
+# Generated from //absl/base:fast_type_id
+pw_source_set("fast_type_id") {
+ visibility = [ "../*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/fast_type_id.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ ":config" ]
+}
+
+# Generated from //absl/base:log_severity
+pw_source_set("log_severity") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/base/log_severity.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/base/log_severity.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":core_headers",
+ ]
+}
+
+# Generated from //absl/base:malloc_internal
+pw_source_set("malloc_internal") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/direct_mmap.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/low_level_alloc.h",
+ ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/low_level_alloc.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ "../..:abseil_cpp_config2",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":base",
+ ":base_internal",
+ ":config",
+ ":core_headers",
+ ":dynamic_annotations",
+ ":raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/base:prefetch
+pw_source_set("prefetch") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/prefetch.h",
+ "$dir_pw_third_party_abseil_cpp/absl/base/prefetch.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":core_headers",
+ ]
+}
+
+# Generated from //absl/base:pretty_function
+pw_source_set("pretty_function") {
+ visibility = [ "../*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/pretty_function.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [ "../../configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+}
+
+# Generated from //absl/base:raw_logging_internal
+pw_source_set("raw_logging_internal") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/raw_logging.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/raw_logging.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":atomic_hook",
+ ":config",
+ ":core_headers",
+ ":errno_saver",
+ ":log_severity",
+ ]
+}
+
+# Generated from //absl/base:spinlock_wait
+pw_source_set("spinlock_wait") {
+ visibility = [ ":*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock_wait.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock_akaros.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock_linux.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock_posix.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock_wait.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/base/internal/spinlock_win32.inc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":base_internal",
+ ":core_headers",
+ ":errno_saver",
+ ]
+}
+
+# Generated from //absl/base:strerror
+pw_source_set("strerror") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/strerror.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/strerror.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":core_headers",
+ ":errno_saver",
+ ]
+}
+
+# Generated from //absl/base:throw_delegate
+pw_source_set("throw_delegate") {
+ visibility = [ "../*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/throw_delegate.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/base/internal/throw_delegate.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":raw_logging_internal",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/cleanup/BUILD.gn b/third_party/abseil-cpp/absl/cleanup/BUILD.gn
new file mode 100644
index 000000000..3f9348f0d
--- /dev/null
+++ b/third_party/abseil-cpp/absl/cleanup/BUILD.gn
@@ -0,0 +1,54 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/cleanup.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/cleanup:cleanup
+pw_source_set("cleanup") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/cleanup/cleanup.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cleanup_internal",
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/cleanup:cleanup_internal
+pw_source_set("cleanup_internal") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/cleanup/internal/cleanup.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:base_internal",
+ "../base:core_headers",
+ "../utility",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/container/BUILD.gn b/third_party/abseil-cpp/absl/container/BUILD.gn
new file mode 100644
index 000000000..4d2077985
--- /dev/null
+++ b/third_party/abseil-cpp/absl/container/BUILD.gn
@@ -0,0 +1,426 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/container.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/container:btree
+pw_source_set("btree") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/btree_map.h",
+ "$dir_pw_third_party_abseil_cpp/absl/container/btree_set.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/btree.h",
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/btree_container.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":common",
+ ":common_policy_traits",
+ ":compressed_tuple",
+ ":container_memory",
+ ":layout",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../base:throw_delegate",
+ "../memory",
+ "../meta:type_traits",
+ "../strings",
+ "../strings:cord",
+ "../types:compare",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/container:common
+pw_source_set("common") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/common.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../meta:type_traits",
+ "../types:optional",
+ ]
+}
+
+# Generated from //absl/container:common_policy_traits
+pw_source_set("common_policy_traits") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/common_policy_traits.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../meta:type_traits" ]
+}
+
+# Generated from //absl/container:compressed_tuple
+pw_source_set("compressed_tuple") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/compressed_tuple.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../utility" ]
+}
+
+# Generated from //absl/container:container_memory
+pw_source_set("container_memory") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/container_memory.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../memory",
+ "../meta:type_traits",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/container:fixed_array
+pw_source_set("fixed_array") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/fixed_array.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":compressed_tuple",
+ "../algorithm",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:throw_delegate",
+ "../memory",
+ ]
+}
+
+# Generated from //absl/container:flat_hash_map
+pw_source_set("flat_hash_map") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/flat_hash_map.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":container_memory",
+ ":hash_function_defaults",
+ ":raw_hash_map",
+ "../algorithm:container",
+ "../base:core_headers",
+ "../memory",
+ ]
+}
+
+# Generated from //absl/container:flat_hash_set
+pw_source_set("flat_hash_set") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/flat_hash_set.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":container_memory",
+ ":hash_function_defaults",
+ ":raw_hash_set",
+ "../algorithm:container",
+ "../base:core_headers",
+ "../memory",
+ ]
+}
+
+# Generated from //absl/container:hash_function_defaults
+pw_source_set("hash_function_defaults") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hash_function_defaults.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../hash",
+ "../strings",
+ "../strings:cord",
+ ]
+}
+
+# Generated from //absl/container:hash_policy_traits
+pw_source_set("hash_policy_traits") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hash_policy_traits.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":common_policy_traits",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/container:hashtable_debug
+pw_source_set("hashtable_debug") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hashtable_debug.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ ":hashtable_debug_hooks" ]
+}
+
+# Generated from //absl/container:hashtable_debug_hooks
+pw_source_set("hashtable_debug_hooks") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hashtable_debug_hooks.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/container:hashtablez_sampler
+pw_source_set("hashtablez_sampler") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hashtablez_sampler.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hashtablez_sampler.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/hashtablez_sampler_force_weak_definition.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../debugging:stacktrace",
+ "../memory",
+ "../profiling:exponential_biased",
+ "../profiling:sample_recorder",
+ "../synchronization",
+ "../time",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/container:inlined_vector
+pw_source_set("inlined_vector") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/inlined_vector.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":inlined_vector_internal",
+ "../algorithm",
+ "../base:core_headers",
+ "../base:throw_delegate",
+ "../memory",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/container:inlined_vector_internal
+pw_source_set("inlined_vector_internal") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/inlined_vector.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":compressed_tuple",
+ "../base:core_headers",
+ "../memory",
+ "../meta:type_traits",
+ "../types:span",
+ ]
+}
+
+# Generated from //absl/container:layout
+pw_source_set("layout") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/internal/layout.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../meta:type_traits",
+ "../strings",
+ "../types:span",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/container:node_hash_map
+pw_source_set("node_hash_map") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/node_hash_map.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":container_memory",
+ ":hash_function_defaults",
+ ":node_slot_policy",
+ ":raw_hash_map",
+ "../algorithm:container",
+ "../base:core_headers",
+ "../memory",
+ ]
+}
+
+# Generated from //absl/container:node_hash_set
+pw_source_set("node_hash_set") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/container/node_hash_set.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":hash_function_defaults",
+ ":node_slot_policy",
+ ":raw_hash_set",
+ "../algorithm:container",
+ "../base:core_headers",
+ "../memory",
+ ]
+}
+
+# Generated from //absl/container:node_slot_policy
+pw_source_set("node_slot_policy") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/node_slot_policy.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/container:raw_hash_map
+pw_source_set("raw_hash_map") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/raw_hash_map.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":container_memory",
+ ":raw_hash_set",
+ "../base:throw_delegate",
+ ]
+}
+
+# Generated from //absl/container:raw_hash_set
+pw_source_set("raw_hash_set") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/raw_hash_set.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/container/internal/raw_hash_set.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":common",
+ ":compressed_tuple",
+ ":container_memory",
+ ":hash_policy_traits",
+ ":hashtable_debug_hooks",
+ ":hashtablez_sampler",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:endian",
+ "../base:prefetch",
+ "../base:raw_logging_internal",
+ "../hash",
+ "../memory",
+ "../meta:type_traits",
+ "../numeric:bits",
+ "../utility",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/crc/BUILD.gn b/third_party/abseil-cpp/absl/crc/BUILD.gn
new file mode 100644
index 000000000..1555fd1d8
--- /dev/null
+++ b/third_party/abseil-cpp/absl/crc/BUILD.gn
@@ -0,0 +1,160 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/crc.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/crc:cpu_detect
+pw_source_set("cpu_detect") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/cpu_detect.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/cpu_detect.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:config",
+ ]
+}
+
+# Generated from //absl/crc:crc32c
+pw_source_set("crc32c") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/crc/crc32c.h",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc32c.h",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_memcpy.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/crc/crc32c.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc32c_inline.h",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_memcpy_fallback.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_memcpy_x86_64.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_non_temporal_memcpy.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cpu_detect",
+ ":crc_internal",
+ ":non_temporal_memcpy",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:endian",
+ "../base:prefetch",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/crc:crc_cord_state
+pw_source_set("crc_cord_state") {
+ visibility = [
+ ":*",
+ "../strings:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_cord_state.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_cord_state.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":crc32c",
+ "../base:config",
+ "../numeric:bits",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/crc:crc_internal
+pw_source_set("crc_internal") {
+ visibility = [ ":*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc.h",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc32_x86_arm_combined_simd.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_internal.h",
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/crc_x86_arm_combined.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cpu_detect",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:endian",
+ "../base:prefetch",
+ "../base:raw_logging_internal",
+ "../memory",
+ "../numeric:bits",
+ ]
+}
+
+# Generated from //absl/crc:non_temporal_arm_intrinsics
+pw_source_set("non_temporal_arm_intrinsics") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/non_temporal_arm_intrinsics.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/crc:non_temporal_memcpy
+pw_source_set("non_temporal_memcpy") {
+ visibility = [ ":*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/crc/internal/non_temporal_memcpy.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":non_temporal_arm_intrinsics",
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/debugging/BUILD.gn b/third_party/abseil-cpp/absl/debugging/BUILD.gn
new file mode 100644
index 000000000..10f3ec462
--- /dev/null
+++ b/third_party/abseil-cpp/absl/debugging/BUILD.gn
@@ -0,0 +1,201 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/debugging.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/debugging:debugging_internal
+pw_source_set("debugging_internal") {
+ visibility = [ ":*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/address_is_readable.h",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/elf_mem_image.h",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/vdso_support.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/address_is_readable.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/elf_mem_image.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/vdso_support.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:errno_saver",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/debugging:demangle_internal
+pw_source_set("demangle_internal") {
+ visibility = [ ":*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/demangle.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/demangle.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/debugging:examine_stack
+pw_source_set("examine_stack") {
+ visibility = [
+ ":*",
+ "../log/internal:*",
+ ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/examine_stack.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/examine_stack.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":stacktrace",
+ ":symbolize",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/debugging:failure_signal_handler
+pw_source_set("failure_signal_handler") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/failure_signal_handler.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/failure_signal_handler.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":examine_stack",
+ ":stacktrace",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/debugging:leak_check
+pw_source_set("leak_check") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/debugging/leak_check.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/debugging/leak_check.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/debugging:stacktrace
+pw_source_set("stacktrace") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/debugging/stacktrace.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_aarch64-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_arm-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_config.h",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_emscripten-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_generic-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_powerpc-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_riscv-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_unimplemented-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_win32-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/stacktrace_x86-inl.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/stacktrace.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":debugging_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/debugging:symbolize
+pw_source_set("symbolize") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/internal/symbolize.h",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize_darwin.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize_elf.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize_emscripten.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize_unimplemented.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/debugging/symbolize_win32.inc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":debugging_internal",
+ ":demangle_internal",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:malloc_internal",
+ "../base:raw_logging_internal",
+ "../strings",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/flags/BUILD.gn b/third_party/abseil-cpp/absl/flags/BUILD.gn
new file mode 100644
index 000000000..0f9fa2e68
--- /dev/null
+++ b/third_party/abseil-cpp/absl/flags/BUILD.gn
@@ -0,0 +1,331 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/flags.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/flags:commandlineflag
+pw_source_set("commandlineflag") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/flags/commandlineflag.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/commandlineflag.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":commandlineflag_internal",
+ "../base:config",
+ "../base:fast_type_id",
+ "../strings",
+ "../types:optional",
+ ]
+}
+
+# Generated from //absl/flags:commandlineflag_internal
+pw_source_set("commandlineflag_internal") {
+ visibility = [ ":*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/commandlineflag.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/commandlineflag.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:fast_type_id",
+ ]
+}
+
+# Generated from //absl/flags:config
+pw_source_set("config") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/config.h",
+ "$dir_pw_third_party_abseil_cpp/absl/flags/usage_config.h",
+ ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/usage_config.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":path_util",
+ ":program_name",
+ "../base:config",
+ "../base:core_headers",
+ "../strings",
+ "../synchronization",
+ ]
+}
+
+# Generated from //absl/flags:flag
+pw_source_set("flag") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/declare.h",
+ "$dir_pw_third_party_abseil_cpp/absl/flags/flag.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/flag.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/flag_msvc.inc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":flag_internal",
+ ":reflection",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/flags:flag_internal
+pw_source_set("flag_internal") {
+ visibility = [
+ ":*",
+ "../base/*",
+ ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/flag.h",
+ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/sequence_lock.h",
+ ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/flag.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":commandlineflag",
+ ":commandlineflag_internal",
+ ":config",
+ ":marshalling",
+ ":reflection",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../memory",
+ "../meta:type_traits",
+ "../strings",
+ "../synchronization",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/flags:marshalling
+pw_source_set("marshalling") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/flags/marshalling.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/marshalling.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../base:log_severity",
+ "../strings",
+ "../strings:str_format",
+ "../types:optional",
+ ]
+}
+
+# Generated from //absl/flags:parse
+pw_source_set("parse") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/parse.h",
+ "$dir_pw_third_party_abseil_cpp/absl/flags/parse.h",
+ ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/parse.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":commandlineflag",
+ ":commandlineflag_internal",
+ ":config",
+ ":flag",
+ ":flag_internal",
+ ":private_handle_accessor",
+ ":program_name",
+ ":reflection",
+ ":usage",
+ ":usage_internal",
+ "../algorithm:container",
+ "../base:config",
+ "../base:core_headers",
+ "../strings",
+ "../synchronization",
+ ]
+}
+
+# Generated from //absl/flags:path_util
+pw_source_set("path_util") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/path_util.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/flags:private_handle_accessor
+pw_source_set("private_handle_accessor") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/private_handle_accessor.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/private_handle_accessor.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":commandlineflag",
+ ":commandlineflag_internal",
+ "../base:config",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/flags:program_name
+pw_source_set("program_name") {
+ visibility = [
+ ":*",
+ "../log:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/program_name.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/program_name.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":path_util",
+ "../base:config",
+ "../base:core_headers",
+ "../strings",
+ "../synchronization",
+ ]
+}
+
+# Generated from //absl/flags:reflection
+pw_source_set("reflection") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/registry.h",
+ "$dir_pw_third_party_abseil_cpp/absl/flags/reflection.h",
+ ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/reflection.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":commandlineflag",
+ ":commandlineflag_internal",
+ ":config",
+ ":private_handle_accessor",
+ "../base:config",
+ "../base:core_headers",
+ "../container:flat_hash_map",
+ "../strings",
+ "../synchronization",
+ ]
+}
+
+# Generated from //absl/flags:usage
+pw_source_set("usage") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/flags/usage.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/usage.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":usage_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../strings",
+ "../synchronization",
+ ]
+}
+
+# Generated from //absl/flags:usage_internal
+pw_source_set("usage_internal") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/usage.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/flags/internal/usage.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":commandlineflag",
+ ":config",
+ ":flag",
+ ":flag_internal",
+ ":path_util",
+ ":private_handle_accessor",
+ ":program_name",
+ ":reflection",
+ "../base:config",
+ "../base:core_headers",
+ "../container:flat_hash_map",
+ "../strings",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/functional/BUILD.gn b/third_party/abseil-cpp/absl/functional/BUILD.gn
new file mode 100644
index 000000000..46742ea1c
--- /dev/null
+++ b/third_party/abseil-cpp/absl/functional/BUILD.gn
@@ -0,0 +1,83 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/functional.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/functional:any_invocable
+pw_source_set("any_invocable") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/functional/any_invocable.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/functional/internal/any_invocable.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../meta:type_traits",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/functional:bind_front
+pw_source_set("bind_front") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/functional/bind_front.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/functional/internal/front_binder.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:base_internal",
+ "../container:compressed_tuple",
+ "../meta:type_traits",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/functional:function_ref
+pw_source_set("function_ref") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/functional/function_ref.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/functional/internal/function_ref.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":any_invocable",
+ "../base:base_internal",
+ "../base:core_headers",
+ "../meta:type_traits",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/hash/BUILD.gn b/third_party/abseil-cpp/absl/hash/BUILD.gn
new file mode 100644
index 000000000..c630e84e9
--- /dev/null
+++ b/third_party/abseil-cpp/absl/hash/BUILD.gn
@@ -0,0 +1,90 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/hash.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/hash:city
+pw_source_set("city") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/hash/internal/city.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/hash/internal/city.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../base:endian",
+ ]
+}
+
+# Generated from //absl/hash:hash
+pw_source_set("hash") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/hash/hash.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/hash/internal/hash.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/hash/internal/hash.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":city",
+ ":low_level_hash",
+ "../base:config",
+ "../base:core_headers",
+ "../base:endian",
+ "../container:fixed_array",
+ "../functional:function_ref",
+ "../meta:type_traits",
+ "../numeric:bits",
+ "../numeric:int128",
+ "../strings",
+ "../types:optional",
+ "../types:variant",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/hash:low_level_hash
+pw_source_set("low_level_hash") {
+ visibility = [ ":*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/hash/internal/low_level_hash.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/hash/internal/low_level_hash.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:endian",
+ "../numeric:int128",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/log/BUILD.gn b/third_party/abseil-cpp/absl/log/BUILD.gn
new file mode 100644
index 000000000..a5392ef9d
--- /dev/null
+++ b/third_party/abseil-cpp/absl/log/BUILD.gn
@@ -0,0 +1,245 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/log.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/log:absl_check
+pw_source_set("absl_check") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/absl_check.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "internal:check_impl" ]
+}
+
+# Generated from //absl/log:absl_log
+pw_source_set("absl_log") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/absl_log.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "internal:log_impl" ]
+}
+
+# Generated from //absl/log:check
+pw_source_set("check") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/check.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "internal:check_impl",
+ "internal:check_op",
+ "internal:conditions",
+ "internal:log_message",
+ "internal:strip",
+ ]
+}
+
+# Generated from //absl/log:die_if_null
+pw_source_set("die_if_null") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/die_if_null.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/die_if_null.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":log",
+ "../base:config",
+ "../base:core_headers",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/log:flags
+pw_source_set("flags") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/flags.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/flags.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":globals",
+ "../base:config",
+ "../base:core_headers",
+ "../base:log_severity",
+ "../flags:flag",
+ "../flags:marshalling",
+ "../strings",
+ "internal:config",
+ "internal:flags",
+ ]
+}
+
+# Generated from //absl/log:globals
+pw_source_set("globals") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/globals.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/globals.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:atomic_hook",
+ "../base:config",
+ "../base:core_headers",
+ "../base:log_severity",
+ "../base:raw_logging_internal",
+ "../hash",
+ "../strings",
+ ]
+}
+
+# Generated from //absl/log:initialize
+pw_source_set("initialize") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/initialize.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/initialize.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":globals",
+ "../base:config",
+ "../time",
+ "internal:globals",
+ ]
+}
+
+# Generated from //absl/log:log
+pw_source_set("log") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/log.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "internal:log_impl" ]
+}
+
+# Generated from //absl/log:log_entry
+pw_source_set("log_entry") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/log_entry.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/log_entry.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../base:log_severity",
+ "../strings",
+ "../time",
+ "../types:span",
+ "internal:config",
+ ]
+}
+
+# Generated from //absl/log:log_sink
+pw_source_set("log_sink") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/log_sink.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/log_sink.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":log_entry",
+ "../base:config",
+ ]
+}
+
+# Generated from //absl/log:log_sink_registry
+pw_source_set("log_sink_registry") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/log_sink_registry.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":log_sink",
+ "../base:config",
+ "internal:log_sink_set",
+ ]
+}
+
+# Generated from //absl/log:log_streamer
+pw_source_set("log_streamer") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/log_streamer.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":absl_log",
+ "../base:config",
+ "../base:log_severity",
+ "../strings",
+ "../strings:internal",
+ "../types:optional",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/log:structured
+pw_source_set("structured") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/structured.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../strings",
+ "internal:structured",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/log/internal/BUILD.gn b/third_party/abseil-cpp/absl/log/internal/BUILD.gn
new file mode 100644
index 000000000..5d6b7de95
--- /dev/null
+++ b/third_party/abseil-cpp/absl/log/internal/BUILD.gn
@@ -0,0 +1,413 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/log/internal.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/log/internal:append_truncated
+pw_source_set("append_truncated") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/append_truncated.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../strings",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/log/internal:check_impl
+pw_source_set("check_impl") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/check_impl.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":check_op",
+ ":conditions",
+ ":log_message",
+ ":strip",
+ "../../base:core_headers",
+ ]
+}
+
+# Generated from //absl/log/internal:check_op
+pw_source_set("check_op") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/check_op.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/check_op.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":nullguard",
+ ":nullstream",
+ ":strip",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../strings",
+ ]
+}
+
+# Generated from //absl/log/internal:conditions
+pw_source_set("conditions") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/conditions.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/conditions.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":voidify",
+ "../../base",
+ "../../base:config",
+ "../../base:core_headers",
+ ]
+}
+
+# Generated from //absl/log/internal:config
+pw_source_set("config") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/config.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../base:core_headers",
+ ]
+}
+
+# Generated from //absl/log/internal:flags
+pw_source_set("flags") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/flags.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../../flags:flag" ]
+}
+
+# Generated from //absl/log/internal:format
+pw_source_set("format") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_format.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_format.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":append_truncated",
+ ":config",
+ ":globals",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:log_severity",
+ "../../strings",
+ "../../strings:str_format",
+ "../../time",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/log/internal:globals
+pw_source_set("globals") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/globals.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/globals.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:log_severity",
+ "../../base:raw_logging_internal",
+ "../../strings",
+ "../../time",
+ ]
+}
+
+# Generated from //absl/log/internal:log_impl
+pw_source_set("log_impl") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_impl.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":conditions",
+ ":log_message",
+ ":strip",
+ ]
+}
+
+# Generated from //absl/log/internal:log_message
+pw_source_set("log_message") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_message.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_message.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":append_truncated",
+ ":format",
+ ":globals",
+ ":log_sink_set",
+ ":nullguard",
+ ":proto",
+ "..:globals",
+ "..:log_entry",
+ "..:log_sink",
+ "..:log_sink_registry",
+ "../../base",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:errno_saver",
+ "../../base:log_severity",
+ "../../base:raw_logging_internal",
+ "../../base:strerror",
+ "../../container:inlined_vector",
+ "../../debugging:examine_stack",
+ "../../memory",
+ "../../strings",
+ "../../time",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/log/internal:log_sink_set
+pw_source_set("log_sink_set") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_sink_set.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/log_sink_set.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":config",
+ ":globals",
+ "..:globals",
+ "..:log_entry",
+ "..:log_sink",
+ "../../base",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:log_severity",
+ "../../base:raw_logging_internal",
+ "../../cleanup",
+ "../../strings",
+ "../../synchronization",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/log/internal:nullguard
+pw_source_set("nullguard") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/nullguard.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/nullguard.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../base:core_headers",
+ ]
+}
+
+# Generated from //absl/log/internal:nullstream
+pw_source_set("nullstream") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/nullstream.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:log_severity",
+ "../../strings",
+ ]
+}
+
+# Generated from //absl/log/internal:proto
+pw_source_set("proto") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/proto.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/proto.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../strings",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/log/internal:strip
+pw_source_set("strip") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/strip.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":log_message",
+ ":nullstream",
+ "../../base:log_severity",
+ ]
+}
+
+# Generated from //absl/log/internal:structured
+pw_source_set("structured") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/structured.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":log_message",
+ "../../base:config",
+ "../../strings",
+ ]
+}
+
+# Generated from //absl/log/internal:voidify
+pw_source_set("voidify") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/log/internal/voidify.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../../base:config" ]
+}
diff --git a/third_party/abseil-cpp/absl/memory/BUILD.gn b/third_party/abseil-cpp/absl/memory/BUILD.gn
new file mode 100644
index 000000000..a7d9912a4
--- /dev/null
+++ b/third_party/abseil-cpp/absl/memory/BUILD.gn
@@ -0,0 +1,37 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/memory.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/memory:memory
+pw_source_set("memory") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/memory/memory.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:core_headers",
+ "../meta:type_traits",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/meta/BUILD.gn b/third_party/abseil-cpp/absl/meta/BUILD.gn
new file mode 100644
index 000000000..6487ca63d
--- /dev/null
+++ b/third_party/abseil-cpp/absl/meta/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/meta.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/meta:type_traits
+pw_source_set("type_traits") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/meta/type_traits.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
diff --git a/third_party/abseil-cpp/absl/numeric/BUILD.gn b/third_party/abseil-cpp/absl/numeric/BUILD.gn
new file mode 100644
index 000000000..aadb49d01
--- /dev/null
+++ b/third_party/abseil-cpp/absl/numeric/BUILD.gn
@@ -0,0 +1,75 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/numeric.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/numeric:bits
+pw_source_set("bits") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/numeric/bits.h",
+ "$dir_pw_third_party_abseil_cpp/absl/numeric/internal/bits.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/numeric:int128
+pw_source_set("int128") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/numeric/int128.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/numeric/int128.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/numeric/int128_have_intrinsic.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/numeric/int128_no_intrinsic.inc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":bits",
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/numeric:representation
+pw_source_set("representation") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/numeric/internal/representation.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
diff --git a/third_party/abseil-cpp/absl/profiling/BUILD.gn b/third_party/abseil-cpp/absl/profiling/BUILD.gn
new file mode 100644
index 000000000..c98ffbf37
--- /dev/null
+++ b/third_party/abseil-cpp/absl/profiling/BUILD.gn
@@ -0,0 +1,75 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/profiling.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/profiling:exponential_biased
+pw_source_set("exponential_biased") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/profiling/internal/exponential_biased.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/profiling/internal/exponential_biased.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [ "../../configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/profiling:periodic_sampler
+pw_source_set("periodic_sampler") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/profiling/internal/periodic_sampler.h",
+ ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/profiling/internal/periodic_sampler.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":exponential_biased",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/profiling:sample_recorder
+pw_source_set("sample_recorder") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/profiling/internal/sample_recorder.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../synchronization",
+ "../time",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/random/BUILD.gn b/third_party/abseil-cpp/absl/random/BUILD.gn
new file mode 100644
index 000000000..96231c29f
--- /dev/null
+++ b/third_party/abseil-cpp/absl/random/BUILD.gn
@@ -0,0 +1,137 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/random.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/random:bit_gen_ref
+pw_source_set("bit_gen_ref") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/bit_gen_ref.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":random",
+ "../base:core_headers",
+ "../base:fast_type_id",
+ "../meta:type_traits",
+ "internal:distribution_caller",
+ "internal:fast_uniform_bits",
+ ]
+}
+
+# Generated from //absl/random:distributions
+pw_source_set("distributions") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/bernoulli_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/beta_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/discrete_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/distributions.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/exponential_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/gaussian_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/log_uniform_int_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/poisson_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/uniform_int_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/uniform_real_distribution.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/zipf_distribution.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/discrete_distribution.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/random/gaussian_distribution.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../meta:type_traits",
+ "../numeric:bits",
+ "../strings",
+ "internal:distribution_caller",
+ "internal:fast_uniform_bits",
+ "internal:fastmath",
+ "internal:generate_real",
+ "internal:iostream_state_saver",
+ "internal:traits",
+ "internal:uniform_helper",
+ "internal:wide_multiply",
+ ]
+}
+
+# Generated from //absl/random:random
+pw_source_set("random") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/random.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":distributions",
+ ":seed_sequences",
+ "internal:nonsecure_base",
+ "internal:pcg_engine",
+ "internal:pool_urbg",
+ "internal:randen_engine",
+ ]
+}
+
+# Generated from //absl/random:seed_gen_exception
+pw_source_set("seed_gen_exception") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/seed_gen_exception.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/seed_gen_exception.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/random:seed_sequences
+pw_source_set("seed_sequences") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/seed_sequences.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/random/seed_sequences.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":seed_gen_exception",
+ "../base:config",
+ "../types:span",
+ "internal:pool_urbg",
+ "internal:salted_seed_seq",
+ "internal:seed_material",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/random/internal/BUILD.gn b/third_party/abseil-cpp/absl/random/internal/BUILD.gn
new file mode 100644
index 000000000..7e6594004
--- /dev/null
+++ b/third_party/abseil-cpp/absl/random/internal/BUILD.gn
@@ -0,0 +1,497 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/random/internal.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/random/internal:distribution_caller
+pw_source_set("distribution_caller") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/distribution_caller.h",
+ ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../base:fast_type_id",
+ "../../utility",
+ ]
+}
+
+# Generated from //absl/random/internal:fast_uniform_bits
+pw_source_set("fast_uniform_bits") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/fast_uniform_bits.h",
+ ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":traits",
+ "../../base:config",
+ "../../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/random/internal:fastmath
+pw_source_set("fastmath") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/fastmath.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../../numeric:bits" ]
+}
+
+# Generated from //absl/random/internal:generate_real
+pw_source_set("generate_real") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/generate_real.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":fastmath",
+ ":traits",
+ "../../meta:type_traits",
+ "../../numeric:bits",
+ ]
+}
+
+# Generated from //absl/random/internal:iostream_state_saver
+pw_source_set("iostream_state_saver") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/iostream_state_saver.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../meta:type_traits",
+ "../../numeric:int128",
+ ]
+}
+
+# Generated from //absl/random/internal:mock_helpers
+pw_source_set("mock_helpers") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/mock_helpers.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [ "../../../configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:fast_type_id",
+ "../../types:optional",
+ ]
+}
+
+# Generated from //absl/random/internal:nanobenchmark
+pw_source_set("nanobenchmark") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/nanobenchmark.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [ "../../../configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":platform",
+ ":randen_engine",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/random/internal:nonsecure_base
+pw_source_set("nonsecure_base") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/nonsecure_base.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":pool_urbg",
+ ":salted_seed_seq",
+ ":seed_material",
+ "../../base:core_headers",
+ "../../container:inlined_vector",
+ "../../meta:type_traits",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/random/internal:pcg_engine
+pw_source_set("pcg_engine") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/pcg_engine.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":fastmath",
+ ":iostream_state_saver",
+ "../../base:config",
+ "../../meta:type_traits",
+ "../../numeric:bits",
+ "../../numeric:int128",
+ ]
+}
+
+# Generated from //absl/random/internal:platform
+pw_source_set("platform") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_traits.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_round_keys.cc",
+ ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../../base:config" ]
+}
+
+# Generated from //absl/random/internal:pool_urbg
+pw_source_set("pool_urbg") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/pool_urbg.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/pool_urbg.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ "../../..:abseil_cpp_config2",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":randen",
+ ":seed_material",
+ ":traits",
+ "..:seed_gen_exception",
+ "../../base",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:endian",
+ "../../base:raw_logging_internal",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/random/internal:randen
+pw_source_set("randen") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":platform",
+ ":randen_hwaes",
+ ":randen_slow",
+ "../../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/random/internal:randen_engine
+pw_source_set("randen_engine") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_engine.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":iostream_state_saver",
+ ":randen",
+ "../../base:endian",
+ "../../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/random/internal:randen_hwaes
+pw_source_set("randen_hwaes") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_detect.h",
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_hwaes.h",
+ ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_detect.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":platform",
+ ":randen_hwaes_impl",
+ "../../base:config",
+ ]
+}
+
+# Generated from //absl/random/internal:randen_hwaes_impl
+pw_source_set("randen_hwaes_impl") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_hwaes.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_hwaes.h",
+ ]
+ cflags = [
+ "-Wno-pass-failed",
+ "-maes",
+ "-msse4.1",
+ ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":platform",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../numeric:int128",
+ ]
+}
+
+# Generated from //absl/random/internal:randen_slow
+pw_source_set("randen_slow") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_slow.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/randen_slow.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":platform",
+ "../../base:config",
+ "../../base:core_headers",
+ "../../base:endian",
+ "../../numeric:int128",
+ ]
+}
+
+# Generated from //absl/random/internal:salted_seed_seq
+pw_source_set("salted_seed_seq") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/random/internal/salted_seed_seq.h",
+ ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":seed_material",
+ "../../container:inlined_vector",
+ "../../meta:type_traits",
+ "../../types:optional",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/random/internal:seed_material
+pw_source_set("seed_material") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/seed_material.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/seed_material.cc" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":fast_uniform_bits",
+ "../../base:core_headers",
+ "../../base:dynamic_annotations",
+ "../../base:raw_logging_internal",
+ "../../strings",
+ "../../types:optional",
+ "../../types:span",
+ ]
+}
+
+# Generated from //absl/random/internal:traits
+pw_source_set("traits") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/traits.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../../base:config",
+ "../../numeric:bits",
+ "../../numeric:int128",
+ ]
+}
+
+# Generated from //absl/random/internal:uniform_helper
+pw_source_set("uniform_helper") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/uniform_helper.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":traits",
+ "../../base:config",
+ "../../meta:type_traits",
+ "../../numeric:int128",
+ ]
+}
+
+# Generated from //absl/random/internal:wide_multiply
+pw_source_set("wide_multiply") {
+ visibility = [
+ ":*",
+ "..:*",
+ ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/random/internal/wide_multiply.h" ]
+ public_configs = [ "../../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../../configs:internal_disabled_warnings",
+ "../../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":traits",
+ "../../base:config",
+ "../../numeric:bits",
+ "../../numeric:int128",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/status/BUILD.gn b/third_party/abseil-cpp/absl/status/BUILD.gn
new file mode 100644
index 000000000..fc4c33c57
--- /dev/null
+++ b/third_party/abseil-cpp/absl/status/BUILD.gn
@@ -0,0 +1,80 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/status.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/status:status
+pw_source_set("status") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/status/status.h",
+ "$dir_pw_third_party_abseil_cpp/absl/status/status_payload_printer.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/status/internal/status_internal.h",
+ "$dir_pw_third_party_abseil_cpp/absl/status/status.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/status/status_payload_printer.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:atomic_hook",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../base:strerror",
+ "../container:inlined_vector",
+ "../debugging:stacktrace",
+ "../debugging:symbolize",
+ "../functional:function_ref",
+ "../strings",
+ "../strings:cord",
+ "../strings:str_format",
+ "../types:optional",
+ ]
+}
+
+# Generated from //absl/status:statusor
+pw_source_set("statusor") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/status/statusor.h" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/status/internal/statusor_internal.h",
+ "$dir_pw_third_party_abseil_cpp/absl/status/statusor.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":status",
+ "../base",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../meta:type_traits",
+ "../strings",
+ "../types:variant",
+ "../utility",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/strings/BUILD.gn b/third_party/abseil-cpp/absl/strings/BUILD.gn
new file mode 100644
index 000000000..1150b29b1
--- /dev/null
+++ b/third_party/abseil-cpp/absl/strings/BUILD.gn
@@ -0,0 +1,408 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/strings.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/strings:cord
+pw_source_set("cord") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/cord.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/cord_buffer.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/cord.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/cord_analysis.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/cord_analysis.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/cord_buffer.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cord_internal",
+ ":cordz_functions",
+ ":cordz_info",
+ ":cordz_statistics",
+ ":cordz_update_scope",
+ ":cordz_update_tracker",
+ ":internal",
+ ":strings",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:endian",
+ "../base:raw_logging_internal",
+ "../container:fixed_array",
+ "../container:inlined_vector",
+ "../crc:crc_cord_state",
+ "../functional:function_ref",
+ "../meta:type_traits",
+ "../numeric:bits",
+ "../types:optional",
+ "../types:span",
+ ]
+}
+
+# Generated from //absl/strings:cord_internal
+pw_source_set("cord_internal") {
+ visibility = [ ":*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_data_edge.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_internal.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_btree.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_btree_navigator.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_btree_reader.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_consume.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_crc.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_flat.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_ring.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_ring_reader.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_internal.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_btree.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_btree_navigator.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_btree_reader.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_consume.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_crc.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cord_rep_ring.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":strings",
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../base:endian",
+ "../base:raw_logging_internal",
+ "../base:throw_delegate",
+ "../container:compressed_tuple",
+ "../container:container_memory",
+ "../container:inlined_vector",
+ "../container:layout",
+ "../crc:crc_cord_state",
+ "../functional:function_ref",
+ "../meta:type_traits",
+ "../types:span",
+ ]
+}
+
+# Generated from //absl/strings:cordz_functions
+pw_source_set("cordz_functions") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_functions.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_functions.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../profiling:exponential_biased",
+ ]
+}
+
+# Generated from //absl/strings:cordz_handle
+pw_source_set("cordz_handle") {
+ visibility = [ "../*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_handle.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_handle.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:config",
+ "../base:raw_logging_internal",
+ "../synchronization",
+ ]
+}
+
+# Generated from //absl/strings:cordz_info
+pw_source_set("cordz_info") {
+ visibility = [ "../*" ]
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_info.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_info.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cord_internal",
+ ":cordz_functions",
+ ":cordz_handle",
+ ":cordz_statistics",
+ ":cordz_update_tracker",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../container:inlined_vector",
+ "../debugging:stacktrace",
+ "../synchronization",
+ "../time",
+ "../types:span",
+ ]
+}
+
+# Generated from //absl/strings:cordz_sample_token
+pw_source_set("cordz_sample_token") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_sample_token.h",
+ ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_sample_token.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cordz_handle",
+ ":cordz_info",
+ "../base:config",
+ ]
+}
+
+# Generated from //absl/strings:cordz_statistics
+pw_source_set("cordz_statistics") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_statistics.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cordz_update_tracker",
+ "../base:config",
+ ]
+}
+
+# Generated from //absl/strings:cordz_update_scope
+pw_source_set("cordz_update_scope") {
+ visibility = [ "../*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_update_scope.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":cord_internal",
+ ":cordz_info",
+ ":cordz_update_tracker",
+ "../base:config",
+ "../base:core_headers",
+ ]
+}
+
+# Generated from //absl/strings:cordz_update_tracker
+pw_source_set("cordz_update_tracker") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/cordz_update_tracker.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/strings:internal
+pw_source_set("internal") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/char_map.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/escaping.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/ostringstream.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/resize_uninitialized.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/utf8.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/escaping.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/ostringstream.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/utf8.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:core_headers",
+ "../base:endian",
+ "../base:raw_logging_internal",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/strings:str_format
+pw_source_set("str_format") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/strings/str_format.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ ":str_format_internal" ]
+}
+
+# Generated from //absl/strings:str_format_internal
+pw_source_set("str_format_internal") {
+ visibility = [ ":*" ]
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/arg.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/bind.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/checker.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/constexpr_parser.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/extension.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/float_conversion.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/output.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/parser.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/arg.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/bind.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/extension.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/float_conversion.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/output.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_format/parser.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":strings",
+ "../base:config",
+ "../base:core_headers",
+ "../functional:function_ref",
+ "../meta:type_traits",
+ "../numeric:bits",
+ "../numeric:int128",
+ "../numeric:representation",
+ "../types:optional",
+ "../types:span",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/strings:strings
+pw_source_set("strings") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/ascii.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/charconv.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/escaping.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/damerau_levenshtein_distance.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/has_absl_stringify.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/string_constant.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/match.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/numbers.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_cat.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_join.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_replace.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_split.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/string_view.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/strip.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/substitute.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/strings/ascii.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/charconv.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/escaping.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/charconv_bigint.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/charconv_bigint.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/charconv_parse.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/charconv_parse.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/damerau_levenshtein_distance.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/memutil.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/memutil.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/stl_type_traits.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_join_internal.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/str_split_internal.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/stringify_sink.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/internal/stringify_sink.h",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/match.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/numbers.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_cat.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_replace.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/str_split.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/string_view.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/strings/substitute.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":internal",
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:endian",
+ "../base:raw_logging_internal",
+ "../base:throw_delegate",
+ "../memory",
+ "../meta:type_traits",
+ "../numeric:bits",
+ "../numeric:int128",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/synchronization/BUILD.gn b/third_party/abseil-cpp/absl/synchronization/BUILD.gn
new file mode 100644
index 000000000..bca5f07d7
--- /dev/null
+++ b/third_party/abseil-cpp/absl/synchronization/BUILD.gn
@@ -0,0 +1,119 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/synchronization.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/synchronization:graphcycles_internal
+pw_source_set("graphcycles_internal") {
+ visibility = [ "../*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/graphcycles.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/graphcycles.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../base:malloc_internal",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/synchronization:kernel_timeout_internal
+pw_source_set("kernel_timeout_internal") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/kernel_timeout.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/kernel_timeout.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../time",
+ ]
+}
+
+# Generated from //absl/synchronization:synchronization
+pw_source_set("synchronization") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/barrier.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/blocking_counter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/create_thread_identity.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/futex.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/futex_waiter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/per_thread_sem.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/pthread_waiter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/sem_waiter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/stdcpp_waiter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/waiter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/waiter_base.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/win32_waiter.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/mutex.h",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/notification.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/barrier.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/blocking_counter.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/create_thread_identity.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/futex_waiter.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/per_thread_sem.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/pthread_waiter.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/sem_waiter.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/stdcpp_waiter.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/waiter_base.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/internal/win32_waiter.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/mutex.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/synchronization/notification.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ "../..:abseil_cpp_config2",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":graphcycles_internal",
+ ":kernel_timeout_internal",
+ "../base",
+ "../base:atomic_hook",
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../base:dynamic_annotations",
+ "../base:malloc_internal",
+ "../base:raw_logging_internal",
+ "../debugging:stacktrace",
+ "../debugging:symbolize",
+ "../time",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/time/BUILD.gn b/third_party/abseil-cpp/absl/time/BUILD.gn
new file mode 100644
index 000000000..090217e68
--- /dev/null
+++ b/third_party/abseil-cpp/absl/time/BUILD.gn
@@ -0,0 +1,57 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/time.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/time:time
+pw_source_set("time") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/time/civil_time.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/clock.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/time.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/time/civil_time.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/clock.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/duration.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/format.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/get_current_time_chrono.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/get_current_time_posix.inc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/time.cc",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base",
+ "../base:config",
+ "../base:core_headers",
+ "../base:raw_logging_internal",
+ "../numeric:int128",
+ "../strings",
+ "../types:optional",
+ "internal/cctz:civil_time",
+ "internal/cctz:time_zone",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/time/internal/cctz/BUILD.gn b/third_party/abseil-cpp/absl/time/internal/cctz/BUILD.gn
new file mode 100644
index 000000000..6b03f1701
--- /dev/null
+++ b/third_party/abseil-cpp/absl/time/internal/cctz/BUILD.gn
@@ -0,0 +1,65 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/time/internal/cctz.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/time/internal/cctz:civil_time
+pw_source_set("civil_time") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/include/cctz/civil_time.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/civil_time_detail.cc" ]
+ public_configs = [ "../../../..:abseil_cpp_public_config1" ]
+ configs = [ "../../../../configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../../../base:config" ]
+}
+
+# Generated from //absl/time/internal/cctz:time_zone
+pw_source_set("time_zone") {
+ public = [
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/include/cctz/time_zone.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/include/cctz/zone_info_source.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_fixed.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_fixed.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_format.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_if.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_if.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_impl.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_impl.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_info.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_info.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_libc.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_libc.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_lookup.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_posix.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/time_zone_posix.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/tzfile.h",
+ "$dir_pw_third_party_abseil_cpp/absl/time/internal/cctz/src/zone_info_source.cc",
+ ]
+ public_configs = [ "../../../..:abseil_cpp_public_config1" ]
+ configs = [ "../../../../configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":civil_time",
+ "../../../base:config",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/types/BUILD.gn b/third_party/abseil-cpp/absl/types/BUILD.gn
new file mode 100644
index 000000000..e4577c0ca
--- /dev/null
+++ b/third_party/abseil-cpp/absl/types/BUILD.gn
@@ -0,0 +1,183 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/types.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/types:any
+pw_source_set("any") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/any.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":bad_any_cast",
+ "../base:config",
+ "../base:core_headers",
+ "../base:fast_type_id",
+ "../meta:type_traits",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/types:bad_any_cast
+pw_source_set("bad_any_cast") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/bad_any_cast.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":bad_any_cast_impl",
+ "../base:config",
+ ]
+}
+
+# Generated from //absl/types:bad_any_cast_impl
+pw_source_set("bad_any_cast_impl") {
+ visibility = [ ":*" ]
+ sources = [
+ "$dir_pw_third_party_abseil_cpp/absl/types/bad_any_cast.cc",
+ "$dir_pw_third_party_abseil_cpp/absl/types/bad_any_cast.h",
+ ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/types:bad_optional_access
+pw_source_set("bad_optional_access") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/bad_optional_access.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/types/bad_optional_access.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/types:bad_variant_access
+pw_source_set("bad_variant_access") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/bad_variant_access.h" ]
+ sources =
+ [ "$dir_pw_third_party_abseil_cpp/absl/types/bad_variant_access.cc" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:config",
+ "../base:raw_logging_internal",
+ ]
+}
+
+# Generated from //absl/types:compare
+pw_source_set("compare") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/compare.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:core_headers",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/types:optional
+pw_source_set("optional") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/optional.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/types/internal/optional.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":bad_optional_access",
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../memory",
+ "../meta:type_traits",
+ "../utility",
+ ]
+}
+
+# Generated from //absl/types:span
+pw_source_set("span") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/span.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/types/internal/span.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../algorithm",
+ "../base:core_headers",
+ "../base:throw_delegate",
+ "../meta:type_traits",
+ ]
+}
+
+# Generated from //absl/types:variant
+pw_source_set("variant") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/types/variant.h" ]
+ sources = [ "$dir_pw_third_party_abseil_cpp/absl/types/internal/variant.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ ":bad_variant_access",
+ "../base:base_internal",
+ "../base:config",
+ "../base:core_headers",
+ "../meta:type_traits",
+ "../utility",
+ ]
+}
diff --git a/third_party/abseil-cpp/absl/utility/BUILD.gn b/third_party/abseil-cpp/absl/utility/BUILD.gn
new file mode 100644
index 000000000..c135b7644
--- /dev/null
+++ b/third_party/abseil-cpp/absl/utility/BUILD.gn
@@ -0,0 +1,51 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for absl/utility.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Generated from //absl/utility:if_constexpr
+pw_source_set("if_constexpr") {
+ public =
+ [ "$dir_pw_third_party_abseil_cpp/absl/utility/internal/if_constexpr.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [ "../base:config" ]
+}
+
+# Generated from //absl/utility:utility
+pw_source_set("utility") {
+ public = [ "$dir_pw_third_party_abseil_cpp/absl/utility/utility.h" ]
+ public_configs = [ "../..:abseil_cpp_public_config1" ]
+ configs = [
+ "../../configs:internal_disabled_warnings",
+ "../..:abseil_cpp_config1",
+ ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ public_deps = [
+ "../base:base_internal",
+ "../base:config",
+ "../meta:type_traits",
+ ]
+}
diff --git a/third_party/abseil-cpp/configs/BUILD.gn b/third_party/abseil-cpp/configs/BUILD.gn
new file mode 100644
index 000000000..3d82ecec2
--- /dev/null
+++ b/third_party/abseil-cpp/configs/BUILD.gn
@@ -0,0 +1,39 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+
+# Targets that include Abseil C++ headers need to include this config.
+config("disabled_warnings") {
+ cflags = [ "-Wno-gcc-compat" ]
+}
+
+# This config should only be used to build the Abseil C++ library itself.
+config("internal_disabled_warnings") {
+ cflags = [
+ "-Wno-gcc-compat",
+ "-Wno-switch-enum",
+ ]
+}
+
+# Include path for Abseil C++.
+#
+# This is needed as the library is used to build FuzzTest in a dedicated
+# toolchain, and `public_configs` do not propagate across toolchain boundaries
+# by default.
+config("public_include_path") {
+ include_dirs = [ dir_pw_third_party_abseil_cpp ]
+}
diff --git a/third_party/abseil-cpp/docs.rst b/third_party/abseil-cpp/docs.rst
new file mode 100644
index 000000000..5b42b3dc7
--- /dev/null
+++ b/third_party/abseil-cpp/docs.rst
@@ -0,0 +1,56 @@
+.. _module-pw_third_party_abseil_cpp:
+
+==========
+Abseil C++
+==========
+The ``$dir_pw_third_party/abseil-cpp/`` module provides build files to allow
+optionally including upstream Abseil C++.
+
+.. _module-pw_third_party_abseil_cpp-using_upstream:
+
+-------------------------
+Using upstream Abseil C++
+-------------------------
+If you want to use Abseil C++, you must do the following:
+
+Submodule
+=========
+Add Abseil C++ to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add https://github.com/abseil/abseil-cpp.git \
+ third_party/abseil-cpp/src
+
+GN
+==
+* Set the GN var ``dir_pw_third_party_abseil-cpp`` to the location of the
+ Abseil C++ source.
+
+ If you used the command above, this will be
+ ``//third_party/abseil-cpp/src``
+
+ This can be set in your args.gn or .gn file like:
+ ``dir_pw_third_party_abseil_cpp = "//third_party/abseil-cpp/src"``
+
+Updating
+========
+The GN build files are generated from the third-party Bazel build files using
+$dir_pw_build/py/pw_build/generate_3p_gn.py.
+
+The script uses data taken from ``$dir_pw_third_party/abseil-cpp/repo.json``.
+
+The script should be re-run whenever the submodule is updated or the JSON file
+is modified. Specify the location of the Bazel repository can be specified using
+the ``-w`` option, e.g.
+
+.. code-block:: sh
+
+ python pw_build/py/pw_build/generate_3p_gn.py \
+ -w third_party/abseil-cpp/src
+
+Version
+=======
+The update script was last run for revision `67f9650`_.
+
+.. _67f9650: https://github.com/abseil/abseil-cpp/tree/67f9650c93a4fa04728a5b754ae8297d2c55d898
diff --git a/third_party/abseil-cpp/repo.json b/third_party/abseil-cpp/repo.json
new file mode 100644
index 000000000..f37847af0
--- /dev/null
+++ b/third_party/abseil-cpp/repo.json
@@ -0,0 +1,7 @@
+{
+ "name": "Abseil C++",
+ "add": [
+ "$dir_pw_third_party/abseil-cpp/configs:internal_disabled_warnings"
+ ],
+ "remove": [ "$dir_pw_fuzzer:instrumentation" ]
+}
diff --git a/third_party/ambiq/BUILD.gn b/third_party/ambiq/BUILD.gn
new file mode 100644
index 000000000..eaaf5140d
--- /dev/null
+++ b/third_party/ambiq/BUILD.gn
@@ -0,0 +1,216 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("ambiq.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+# This file defines a GN source_set for an external installation of Ambiq SDK.
+# To use, checkout the Ambiq Apollo4 SDK source into a directory, then set
+# the build arg dir_pw_third_party_ambiq_SDK to point to that directory. The
+# Ambiq Apollo 4 SDK library will be available in GN
+# at "$dir_pw_third_party/apollo4".
+if (dir_pw_third_party_ambiq_SDK != "") {
+ config("apollo4p_sdk_defines") {
+ defines = [
+ "AM_PART_APOLLO4P=1",
+ "apollo4p_evb=1",
+ "gcc=1",
+ "AM_PACKAGE_BGA=1",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ config("apollo4b_sdk_defines") {
+ defines = [
+ "AM_PART_APOLLO4B=1",
+ "apollo4b_evb=1",
+ "gcc=1",
+ "AM_PACKAGE_BGA=1",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ config("disable_warnings") {
+ cflags = [
+ "-Wno-sign-compare",
+ "-Wno-unused-parameter",
+ "-Wno-cast-qual",
+ "-Wno-shadow",
+ "-Wno-implicit-fallthrough",
+ "-Wno-maybe-uninitialized",
+ ]
+ cflags_c = [ "-Wno-old-style-declaration" ]
+ visibility = [ ":*" ]
+ }
+
+ config("apollo4_include_dirs") {
+ include_dirs = [
+ "$dir_pw_third_party_ambiq_SDK/devices",
+ "$dir_pw_third_party_ambiq_SDK/utils",
+ "$dir_pw_third_party_ambiq_SDK/CMSIS/ARM/Include",
+ "$dir_pw_third_party_ambiq_SDK/CMSIS/AmbiqMicro/Include",
+ ]
+ }
+
+ config("apollo4p_include_dirs") {
+ include_dirs = [
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p",
+ "$dir_pw_third_party_ambiq_SDK/boards/apollo4p_evb/bsp",
+ ]
+ }
+
+ config("apollo4b_include_dirs") {
+ include_dirs = [
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b",
+ "$dir_pw_third_party_ambiq_SDK/boards/apollo4b_evb/bsp",
+ ]
+ }
+
+ pw_source_set("apollo4p") {
+ remove_configs = [ "$dir_pw_build:extra_strict_warnings" ]
+
+ public_configs = [
+ ":disable_warnings",
+ ":apollo4_include_dirs",
+ ":apollo4p_include_dirs",
+ ":apollo4p_sdk_defines",
+ ]
+
+ sources = [
+ "$dir_pw_third_party_ambiq_SDK/boards/apollo4p_evb/bsp/am_bsp.c",
+ "$dir_pw_third_party_ambiq_SDK/boards/apollo4p_evb/bsp/am_bsp_pins.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_access.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_adc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_audadc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_dcu.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_global.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_gpio.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_i2s.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_otp.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_pdm.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_pin.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_pwrctrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_queue.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_security.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_stimer.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_timer.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_usb.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/am_hal_wdt.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_bootrom_helper.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_cachectrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_card.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_card_host.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_clkgen.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_cmdq.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_dsi.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_fault.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_interrupt.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_iom.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_ios.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_itm.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_itm.h",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_mcuctrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_mpu.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_mram.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_mspi.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_reset.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_rtc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_sdhc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_secure_ota.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_sysctrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_systick.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_tpiu.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_uart.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4p/hal/mcu/am_hal_utils.c",
+ ]
+ }
+
+ pw_source_set("apollo4b") {
+ remove_configs = [ "$dir_pw_build:extra_strict_warnings" ]
+
+ public_configs = [
+ ":disable_warnings",
+ ":apollo4_include_dirs",
+ ":apollo4b_include_dirs",
+ ":apollo4b_sdk_defines",
+ ]
+
+ sources = [
+ "$dir_pw_third_party_ambiq_SDK/boards/apollo4b_evb/bsp/am_bsp.c",
+ "$dir_pw_third_party_ambiq_SDK/boards/apollo4b_evb/bsp/am_bsp_pins.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_access.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_adc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_audadc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_dcu.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_global.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_gpio.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_i2s.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_otp.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_pdm.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_pin.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_pwrctrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_queue.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_security.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_stimer.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_timer.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_usb.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/am_hal_wdt.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_bootrom_helper.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_cachectrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_card.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_card_host.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_clkgen.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_cmdq.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_dsi.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_fault.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_interrupt.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_iom.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_ios.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_itm.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_itm.h",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_mcuctrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_mpu.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_mram.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_mspi.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_reset.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_rtc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_sdhc.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_secure_ota.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_sysctrl.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_systick.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_tpiu.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_uart.c",
+ "$dir_pw_third_party_ambiq_SDK/mcu/apollo4b/hal/mcu/am_hal_utils.c",
+ ]
+ }
+
+ pw_source_set("sdk") {
+ if (pw_third_party_ambiq_PRODUCT == "apollo4p") {
+ public_deps = [ ":apollo4p" ]
+ } else if (pw_third_party_ambiq_PRODUCT == "apollo4b") {
+ public_deps = [ ":apollo4b" ]
+ }
+ }
+}
diff --git a/third_party/ambiq/ambiq.gni b/third_party/ambiq/ambiq.gni
new file mode 100644
index 000000000..c0db57eab
--- /dev/null
+++ b/third_party/ambiq/ambiq.gni
@@ -0,0 +1,24 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # If compiling a project against an Ambiq Apollo SDK, this variable is set to the path to the
+ # Ambiq Suite SDK directory.
+ dir_pw_third_party_ambiq_SDK = ""
+
+ # The Product specified in as much detail as possible.
+ # i.e. "apollo4p", "apollo4b", etc.
+ pw_third_party_ambiq_PRODUCT = ""
+}
diff --git a/third_party/boringssl/BUILD.bazel b/third_party/boringssl/BUILD.bazel
index 292b3f575..b4179fdf0 100644
--- a/third_party/boringssl/BUILD.bazel
+++ b/third_party/boringssl/BUILD.bazel
@@ -20,9 +20,11 @@ load(
pw_cc_library(
name = "sysdeps",
- srcs = ["crypto_sysrand.cc"],
hdrs = ["sysdeps/sys/socket.h"],
copts = ["-Wno-unused-parameter"],
includes = ["sysdeps"],
- deps = ["@boringssl//:bssl"],
+ deps = [
+ "//pw_assert",
+ "@boringssl//:bssl",
+ ],
)
diff --git a/third_party/boringssl/BUILD.gn b/third_party/boringssl/BUILD.gn
index b0174f332..73acfe1d9 100644
--- a/third_party/boringssl/BUILD.gn
+++ b/third_party/boringssl/BUILD.gn
@@ -50,6 +50,9 @@ if (pw_third_party_boringssl_ALIAS != "") {
# The ARM assembly code is only for cortex-A.
"OPENSSL_NO_ASM",
+ # socklen_t is not defined
+ "OPENSSL_NO_SOCK",
+
# Disable assert, which may additionally link in unwanted binaries via
# argument evaluation.
"NDEBUG",
@@ -77,7 +80,7 @@ if (pw_third_party_boringssl_ALIAS != "") {
]
pw_source_set("boringssl") {
- sources = [ "crypto_sysrand.cc" ]
+ sources = []
foreach(source, crypto_sources - excluded_sources + ssl_sources) {
sources += [ "$dir_pw_third_party_boringssl/$source" ]
}
@@ -88,7 +91,16 @@ if (pw_third_party_boringssl_ALIAS != "") {
# Can be removed once posix socket layer in Pigweed is supported.
include_dirs = [ "sysdeps" ]
- public_deps = [ "$dir_pw_tls_client:time" ]
+ public_deps = [
+ "$dir_pw_assert",
+ "$dir_pw_tls_client:time",
+ ]
+
+ # Consume //third_party/boringssl via a Pigweed module only.
+ visibility = [
+ "$dir_pw_tls_client:*",
+ "$dir_pw_tls_client_boringssl:*",
+ ]
}
} else {
group("boringssl") {
diff --git a/third_party/boringssl/crypto_sysrand.cc b/third_party/boringssl/crypto_sysrand.cc
deleted file mode 100644
index 34fe1bb02..000000000
--- a/third_party/boringssl/crypto_sysrand.cc
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-// https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-#include "src/crypto/fipsmodule/rand/internal.h"
-
-extern "C" {
-// OPENSSL_URANDOM is defined automatically based on platform flags.
-// See crypto/fipsmodule/rand/internal.h
-#ifdef OPENSSL_URANDOM
-// When OPENSSL_URANDOM is defined, boringssl assumes linux and
-// reads from "dev/urandom" for generating randoms bytes.
-// We mock the required file io functions to accomodate it for now.
-// TODO(zyecheng): Ask BoringSSL team if there are ways to disable
-// OPENSSL_URANDOM, potentially by adding a OPENSSL_PIGWEED flag in
-// crypto/fipsmodule/rand/internal.h. If not, we need to keep these
-// mockings.
-
-#define URANDOM_FILE_FD 123
-int open(const char* file, int, ...) {
- if (strcmp(file, "/dev/urandom") == 0) {
- return URANDOM_FILE_FD;
- }
- return -1;
-}
-
-ssize_t read(int fd, void*, size_t len) {
- if (fd == URANDOM_FILE_FD) {
- // TODO(zyecheng): Add code to generate random bytes.
- }
- return static_cast<ssize_t>(len);
-}
-
-#else
-// When OPENSSL_URANDOM is not defined, BoringSSL expects an implementation of
-// the following function for generating random bytes.
-void CRYPTO_sysrand(uint8_t*, size_t) {
- // TODO(zyecheng): Add code to generate random bytes.
-}
-#endif
-}
diff --git a/third_party/boringssl/docs.rst b/third_party/boringssl/docs.rst
index f2442f5e8..7e16e2c0e 100644
--- a/third_party/boringssl/docs.rst
+++ b/third_party/boringssl/docs.rst
@@ -49,7 +49,7 @@ The GN variables needed are defined in
.. code-block::
- dir_pw_third_party_boringssl = "//third_party/boringssl/src"
+ dir_pw_third_party_boringssl = "//third_party/boringssl/src"
#. Having a non-empty ``dir_pw_third_party_boringssl`` variable causes GN to
attempt to include the ``BUILD.generated.gni`` file from the sources even
diff --git a/third_party/chre/BUILD.bazel b/third_party/chre/BUILD.bazel
new file mode 100644
index 000000000..e74b91a52
--- /dev/null
+++ b/third_party/chre/BUILD.bazel
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+filegroup(
+ name = "chre",
+ srcs = [
+ "integration_test.cc",
+ "version.cc",
+ ],
+)
diff --git a/third_party/chre/BUILD.gn b/third_party/chre/BUILD.gn
new file mode 100644
index 000000000..e5381d9ab
--- /dev/null
+++ b/third_party/chre/BUILD.gn
@@ -0,0 +1,203 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/error.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+import("chre.gni")
+
+# This file defines a GN source_set for an external installation of chre.
+# To use, checkout the chre source into a directory, then set the build arg
+# dir_pw_third_party_chre to point to that directory. The chre library
+# will be available in GN at "$dir_pw_third_party/chre".
+if (dir_pw_third_party_chre == "") {
+} else {
+ config("disable_warnings") {
+ cflags = [
+ "-Wno-cast-qual",
+ "-Wno-int-in-bool-context",
+ "-Wno-thread-safety-analysis",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ config("default_chre_config_defines") {
+ cflags = [
+ "-DCHRE_MESSAGE_TO_HOST_MAX_SIZE=2048",
+ "-DCHRE_MINIMUM_LOG_LEVEL=CHRE_LOG_LEVEL_DEBUG",
+ "-DCHRE_ASSERTIONS_DISABLED",
+ "-DCHRE_FILENAME=__FILE__",
+ "-DCHRE_PATCH_VERSION=1",
+ "-DCHRE_PLATFORM_ID=1",
+ "-DCHRE_FIRST_SUPPORTED_API_VERSION=CHRE_API_VERSION_1_1",
+ "-DCHRE_VARIANT_SUPPLIES_STATIC_NANOAPP_LIST",
+ "-DCHRE_NANOAPP_INTERNAL",
+ ]
+ }
+
+ pw_source_set("default_chre_config") {
+ public_configs = [ ":default_chre_config_defines" ]
+ }
+
+ pw_source_set("config") {
+ public_deps = [ pw_chre_CONFIG ]
+ }
+
+ config("public_includes") {
+ include_dirs = [
+ "$dir_pw_third_party_chre/core/include",
+ "$dir_pw_third_party_chre/chre_api/include",
+ "$dir_pw_third_party_chre/chre_api/include/chre_api",
+ "$dir_pw_third_party_chre/pal/include",
+ "$dir_pw_third_party_chre/platform/include",
+ "$dir_pw_third_party_chre/platform/shared/include",
+ "$dir_pw_third_party_chre/util/include",
+ "$dir_pw_third_party_chre/apps/include",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ pw_source_set("chre_headers") {
+ public_configs = [ ":public_includes" ]
+ public_deps = [ ":config" ]
+ }
+
+ pw_source_set("shared_platform") {
+ public_configs = [ ":disable_warnings" ]
+ sources = [
+ "$dir_pw_third_party_chre/platform/shared/assert.cc",
+ "$dir_pw_third_party_chre/platform/shared/chre_api_core.cc",
+ "$dir_pw_third_party_chre/platform/shared/chre_api_re.cc",
+ "$dir_pw_third_party_chre/platform/shared/chre_api_user_settings.cc",
+ "$dir_pw_third_party_chre/platform/shared/chre_api_version.cc",
+ "$dir_pw_third_party_chre/platform/shared/memory_manager.cc",
+ "$dir_pw_third_party_chre/platform/shared/nanoapp/nanoapp_dso_util.cc",
+ "$dir_pw_third_party_chre/platform/shared/pal_system_api.cc",
+ "$dir_pw_third_party_chre/platform/shared/system_time.cc",
+ ]
+
+ # CHRE does not compile on MacOS, so work around this.
+ if (current_os == "mac") {
+ sources += [ "version.cc" ]
+ } else {
+ sources += [ "$dir_pw_third_party_chre/platform/shared/version.cc" ]
+ }
+
+ public_deps = [
+ ":chre_headers",
+ "$pw_chre_PLATFORM_BACKEND_HEADERS",
+ ]
+ remove_configs = [ "$dir_pw_build:internal_strict_warnings" ]
+ }
+
+ pw_source_set("chre") {
+ public_configs = [ ":disable_warnings" ]
+ sources = [
+ "$dir_pw_third_party_chre/core/debug_dump_manager.cc",
+ "$dir_pw_third_party_chre/core/event.cc",
+ "$dir_pw_third_party_chre/core/event_loop.cc",
+ "$dir_pw_third_party_chre/core/event_loop_manager.cc",
+ "$dir_pw_third_party_chre/core/event_ref_queue.cc",
+ "$dir_pw_third_party_chre/core/host_comms_manager.cc",
+ "$dir_pw_third_party_chre/core/host_notifications.cc",
+ "$dir_pw_third_party_chre/core/init.cc",
+ "$dir_pw_third_party_chre/core/log.cc",
+ "$dir_pw_third_party_chre/core/nanoapp.cc",
+ "$dir_pw_third_party_chre/core/settings.cc",
+ "$dir_pw_third_party_chre/core/static_nanoapps.cc",
+ "$dir_pw_third_party_chre/core/timer_pool.cc",
+ "$dir_pw_third_party_chre/util/buffer_base.cc",
+ "$dir_pw_third_party_chre/util/dynamic_vector_base.cc",
+ "$dir_pw_third_party_chre/util/nanoapp/audio.cc",
+ "$dir_pw_third_party_chre/util/nanoapp/callbacks.cc",
+ "$dir_pw_third_party_chre/util/nanoapp/debug.cc",
+ "$dir_pw_third_party_chre/util/nanoapp/wifi.cc",
+ "$dir_pw_third_party_chre/util/system/debug_dump.cc",
+ ]
+
+ public_deps = [
+ ":chre_headers",
+ "$pw_chre_PLATFORM_BACKEND",
+ ]
+ remove_configs = [ "$dir_pw_build:internal_strict_warnings" ]
+ }
+
+ pw_source_set("example_apps") {
+ sources = [
+ "$dir_pw_third_party_chre/apps/debug_dump_world/debug_dump_world.cc",
+ "$dir_pw_third_party_chre/apps/hello_world/hello_world.cc",
+ "$dir_pw_third_party_chre/apps/message_world/message_world.cc",
+ "$dir_pw_third_party_chre/apps/spammer/spammer.cc",
+ "$dir_pw_third_party_chre/apps/timer_world/timer_world.cc",
+ "$dir_pw_third_party_chre/apps/unload_tester/unload_tester.cc",
+ ]
+ public_deps = [ ":chre" ]
+ }
+
+ config("test_includes") {
+ include_dirs = [
+ "$dir_pw_third_party_chre/platform/shared",
+ "$dir_pw_third_party_chre/test/simulation/inc",
+ ]
+ visibility = [ ":*" ]
+ }
+
+ config("tests_disable_warnings") {
+ cflags = [ "-Wno-sign-compare" ]
+ visibility = [ ":*" ]
+ }
+
+ pw_test("unit_tests") {
+ enable_if =
+ dir_pw_third_party_chre != "" && pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ sources = [
+ "$dir_pw_third_party_chre/util/tests/blocking_queue_test.cc",
+ "$dir_pw_third_party_chre/util/tests/buffer_test.cc",
+ "$dir_pw_third_party_chre/util/tests/conditional_lock_guard_test.cc",
+ "$dir_pw_third_party_chre/util/tests/debug_dump_test.cc",
+ "$dir_pw_third_party_chre/util/tests/lock_guard_test.cc",
+ "$dir_pw_third_party_chre/util/tests/memory_pool_test.cc",
+ "$dir_pw_third_party_chre/util/tests/optional_test.cc",
+ "$dir_pw_third_party_chre/util/tests/ref_base_test.cc",
+ "$dir_pw_third_party_chre/util/tests/shared_ptr_test.cc",
+ "$dir_pw_third_party_chre/util/tests/singleton_test.cc",
+ "$dir_pw_third_party_chre/util/tests/time_test.cc",
+ "$dir_pw_third_party_chre/util/tests/unique_ptr_test.cc",
+ ]
+ public_deps = [ ":chre" ]
+ public_configs = [ ":tests_disable_warnings" ]
+ remove_configs = [ "$dir_pw_build:internal_strict_warnings" ]
+ }
+
+ pw_test("integration_tests") {
+ enable_if =
+ dir_pw_third_party_chre != "" && pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ sources = [
+ "$dir_pw_third_party_chre/test/simulation/memory_test.cc",
+ "$dir_pw_third_party_chre/test/simulation/test_util.cc",
+ "$dir_pw_third_party_chre/test/simulation/timer_test.cc",
+ "integration_test.cc",
+ ]
+
+ public_deps = [ ":chre" ]
+ public_configs = [
+ ":test_includes",
+ ":tests_disable_warnings",
+ ]
+ remove_configs = [ "$dir_pw_build:internal_strict_warnings" ]
+ }
+}
diff --git a/third_party/chre/chre.gni b/third_party/chre/chre.gni
new file mode 100644
index 000000000..3bf57079c
--- /dev/null
+++ b/third_party/chre/chre.gni
@@ -0,0 +1,29 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+declare_args() {
+ # If compiling backends with chre, this variable is set to the path to the
+ # chre installation. When set, a pw_source_set for the chre library is
+ # created at "$dir_pw_third_party/chre".
+ dir_pw_third_party_chre = ""
+
+ # The configuration for building CHRE.
+ pw_chre_CONFIG = "//third_party/chre:default_chre_config"
+
+ # CHRE's platform backend headers. The default is the Pigweed backend.
+ pw_chre_PLATFORM_BACKEND_HEADERS = "//pw_chre:chre_backend_headers"
+
+ # CHRE's platform backend implementation. The default is the Pigweed backend.
+ pw_chre_PLATFORM_BACKEND = "//pw_chre:chre_backend"
+}
diff --git a/third_party/chre/integration_test.cc b/third_party/chre/integration_test.cc
new file mode 100644
index 000000000..acbaaec78
--- /dev/null
+++ b/third_party/chre/integration_test.cc
@@ -0,0 +1,118 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include <chrono>
+#include <functional>
+#include <thread>
+
+#include "chre/core/event_loop_manager.h"
+#include "chre/core/init.h"
+#include "chre/platform/memory.h"
+#include "chre/platform/system_timer.h"
+#include "chre/util/fixed_size_blocking_queue.h"
+#include "chre/util/memory.h"
+#include "chre/util/non_copyable.h"
+#include "chre/util/singleton.h"
+#include "chre/util/time.h"
+#include "chre_api/chre/version.h"
+#include "gtest/gtest.h"
+#include "test_base.h"
+#include "test_util.h"
+
+namespace chre {
+
+void TestBase::SetUp() {
+ TestEventQueueSingleton::init();
+ chre::init();
+ EventLoopManagerSingleton::get()->lateInit();
+
+ mChreThread = std::thread(
+ []() { EventLoopManagerSingleton::get()->getEventLoop().run(); });
+
+ auto callback = [](void*) {
+ LOGE("Test timed out ...");
+ TestEventQueueSingleton::get()->pushEvent(
+ CHRE_EVENT_SIMULATION_TEST_TIMEOUT);
+ };
+
+ ASSERT_TRUE(mSystemTimer.init());
+ ASSERT_TRUE(mSystemTimer.set(
+ callback, nullptr /*data*/, Nanoseconds(getTimeoutNs())));
+}
+
+void TestBase::TearDown() {
+ mSystemTimer.cancel();
+ TestEventQueueSingleton::get()->flush();
+ EventLoopManagerSingleton::get()->getEventLoop().stop();
+ mChreThread.join();
+ chre::deinit();
+ TestEventQueueSingleton::deinit();
+ deleteNanoappInfos();
+}
+
+template class Singleton<TestEventQueue>;
+
+TEST_F(TestBase, CanLoadAndStartSingleNanoapp) {
+ constexpr uint64_t kAppId = 0x0123456789abcdef;
+ constexpr uint32_t kAppVersion = 0;
+ constexpr uint32_t kAppPerms = 0;
+
+ UniquePtr<Nanoapp> nanoapp = createStaticNanoapp("Test nanoapp",
+ kAppId,
+ kAppVersion,
+ kAppPerms,
+ defaultNanoappStart,
+ defaultNanoappHandleEvent,
+ defaultNanoappEnd);
+
+ EventLoopManagerSingleton::get()->deferCallback(
+ SystemCallbackType::FinishLoadingNanoapp,
+ std::move(nanoapp),
+ testFinishLoadingNanoappCallback);
+ waitForEvent(CHRE_EVENT_SIMULATION_TEST_NANOAPP_LOADED);
+}
+
+TEST_F(TestBase, CanLoadAndStartMultipleNanoapps) {
+ constexpr uint64_t kAppId1 = 0x123;
+ constexpr uint64_t kAppId2 = 0x456;
+ constexpr uint32_t kAppVersion = 0;
+ constexpr uint32_t kAppPerms = 0;
+ loadNanoapp("Test nanoapp",
+ kAppId1,
+ kAppVersion,
+ kAppPerms,
+ defaultNanoappStart,
+ defaultNanoappHandleEvent,
+ defaultNanoappEnd);
+
+ loadNanoapp("Test nanoapp",
+ kAppId2,
+ kAppVersion,
+ kAppPerms,
+ defaultNanoappStart,
+ defaultNanoappHandleEvent,
+ defaultNanoappEnd);
+
+ uint16_t id1;
+ EXPECT_TRUE(EventLoopManagerSingleton::get()
+ ->getEventLoop()
+ .findNanoappInstanceIdByAppId(kAppId1, &id1));
+ uint16_t id2;
+ EXPECT_TRUE(EventLoopManagerSingleton::get()
+ ->getEventLoop()
+ .findNanoappInstanceIdByAppId(kAppId2, &id2));
+
+ EXPECT_NE(id1, id2);
+}
+} // namespace chre
diff --git a/pw_log_tokenized/public/pw_log_tokenized/base64_over_hdlc.h b/third_party/chre/version.cc
index af3153238..4f3d5a110 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/base64_over_hdlc.h
+++ b/third_party/chre/version.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
@@ -11,9 +11,15 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-#pragma once
-// The HDLC address to which to write Base64-encoded tokenized logs.
-#ifndef PW_LOG_TOKENIZED_BASE64_LOG_HDLC_ADDRESS
-#define PW_LOG_TOKENIZED_BASE64_LOG_HDLC_ADDRESS 1
-#endif // PW_LOG_TOKENIZED_BASE64_LOG_HDLC_ADDRESS
+#include "chre/platform/version.h"
+
+#ifndef CHRE_VERSION_STRING
+#define CHRE_VERSION_STRING "undefined"
+#endif // CHRE_VERSION_STRING
+
+namespace chre {
+
+const char* getChreVersionString() { return CHRE_VERSION_STRING; }
+
+} // namespace chre
diff --git a/third_party/emboss/BUILD.gn b/third_party/emboss/BUILD.gn
index a0ef87dda..21b8ba524 100644
--- a/third_party/emboss/BUILD.gn
+++ b/third_party/emboss/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
+import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/emboss/emboss.gni")
@@ -23,16 +24,35 @@ config("default_config") {
# EMBOSS_CHECK is used to check preconditions on application logic (e.g.
# Write() checks the [requires: ...] attribute).
defines = [
- "EMBOSS_CHECK=PW_DCHECK",
+ "EMBOSS_CHECK=PW_DASSERT",
"EMBOSS_CHECK_ABORTS",
- "EMBOSS_DCHECK=PW_DCHECK",
+ "EMBOSS_DCHECK=PW_DASSERT",
"EMBOSS_DCHECK_ABORTS",
]
+ cflags_cc = [
+ "-include",
+ rebase_path("$dir_pw_assert/public/pw_assert/assert.h", root_build_dir),
+ ]
+}
+
+pw_source_set("default_overrides") {
+ public_configs = [ ":default_config" ]
+ public_deps = [ "$dir_pw_assert" ]
+}
+
+config("disable_warnings") {
+ cflags_cc = [
+ "-Wno-unused-parameter",
+ "-Wno-deprecated-copy",
+ "-Wno-format-invalid-specifier",
+ ]
}
-source_set("cpp_utils") {
- public_configs = [ pw_third_party_emboss_CONFIG ]
- sources = [
+pw_source_set("cpp_utils") {
+ # emboss depends on a separate checkout not included in pigweed, so
+ # ignore gn check for this module.
+ check_includes = false
+ public = [
"$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic.h",
"$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic_all_known_generated.h",
"$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic_maximum_operation_generated.h",
@@ -49,6 +69,9 @@ source_set("cpp_utils") {
"$dir_pw_third_party_emboss/runtime/cpp/emboss_text_util.h",
"$dir_pw_third_party_emboss/runtime/cpp/emboss_view_parameters.h",
]
+ public_configs = [ ":disable_warnings" ]
+ public_deps = [ pw_third_party_emboss_CONFIG ]
+ visibility = [ "*" ]
}
# Exists solely to satisfy presubmit. embossc_runner.py is used for real in
@@ -62,10 +85,3 @@ action("embossc_runner") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
-
-# Flags that are needed to compile targets that depend on Emboss.
-# TODO(benlawson): Fix Emboss upstream so this can be removed
-# (https://github.com/google/emboss/issues/69)
-config("flags") {
- cflags = [ "-Wno-unused-parameter" ]
-}
diff --git a/third_party/emboss/build_defs.gni b/third_party/emboss/build_defs.gni
index 74bd061ae..e5ca2046f 100644
--- a/third_party/emboss/build_defs.gni
+++ b/third_party/emboss/build_defs.gni
@@ -72,7 +72,7 @@ template("emboss_cc_library") {
default_import_dir = get_path_info(default_import_dir, "dir")
default_import_dir = rebase_path(default_import_dir, root_build_dir)
- action(target_name + "_header") {
+ action("${target_name}_header") {
script = "$dir_pw_third_party/emboss/embossc_runner.py"
args = [
@@ -89,7 +89,7 @@ template("emboss_cc_library") {
foreach(dir, invoker.import_dirs) {
args += [
"--import-dir",
- dir,
+ rebase_path(dir, root_build_dir),
]
}
}
@@ -141,27 +141,28 @@ template("emboss_cc_library") {
outputs = [ compiled_header_path ]
}
- config(target_name + "_emboss_config") {
+ config("${target_name}_emboss_config") {
include_dirs = [
"$dir_pw_third_party_emboss",
root_gen_dir,
]
}
+ # Since the emboss_cc_library template is used in non-Pigweed environments, this target is not pw_source_set,
+ # which restricts visibility by default.
source_set(target_name) {
forward_variables_from(invoker, "*")
sources = [ compiled_header_path ]
- if (!defined(invoker.deps)) {
- deps = []
- }
- deps += [ "$dir_pw_third_party/emboss:cpp_utils" ]
- public_deps = [ ":" + target_name + "_header" ]
+ public_deps = [
+ ":${target_name}_header",
+ "$dir_pw_third_party/emboss:cpp_utils",
+ ]
if (!defined(invoker.public_configs)) {
public_configs = []
}
- public_configs += [ ":" + target_name + "_emboss_config" ]
+ public_configs += [ ":${target_name}_emboss_config" ]
}
}
diff --git a/third_party/emboss/docs.rst b/third_party/emboss/docs.rst
index 1cabc8aae..478a79ada 100644
--- a/third_party/emboss/docs.rst
+++ b/third_party/emboss/docs.rst
@@ -34,8 +34,9 @@ installation. If using the submodule path from above, add the following to the
Optionally, configure the Emboss defines documented at
`dir_pw_third_party_emboss/runtime/cpp/emboss_defines.h
<https://github.com/google/emboss/blob/master/runtime/cpp/emboss_defines.h>`_
-by setting the ``pw_third_party_emboss_CONFIG`` variable to a config that
-overrides the defines. By default, checks will use PW_DCHECK.
+by setting the ``pw_third_party_emboss_CONFIG`` variable to a source set that
+includes a public config overriding the defines. By default, checks will
+use PW_DASSERT.
..
inclusive-language: enable
@@ -48,11 +49,11 @@ To generate its bindings, you can add the following to ``//some/path/to/BUILD.gn
.. code-block::
- import("$dir_pw_third_party/emboss/build_defs.gni")
+ import("$dir_pw_third_party/emboss/build_defs.gni")
- emboss_cc_library("protocol") {
- source = "my-protocol.emb"
- }
+ emboss_cc_library("protocol") {
+ source = "my-protocol.emb"
+ }
This generates a source set of the same name as the target, in this case "protocol".
To use the bindings, list this target as a dependency in GN and include the generated
@@ -60,4 +61,5 @@ header by adding ``.h`` to the path of your Emboss source file:
.. code-block::
- #include <some/path/to/protocol.emb.h>
+ #include <some/path/to/protocol.emb.h>
+
diff --git a/third_party/emboss/emboss.gni b/third_party/emboss/emboss.gni
index 93d8396d7..a9c52ec1a 100644
--- a/third_party/emboss/emboss.gni
+++ b/third_party/emboss/emboss.gni
@@ -19,7 +19,7 @@ declare_args() {
# source code.
dir_pw_third_party_emboss = ""
- # config target for overriding Emboss defines (e.g. EMBOSS_CHECK).
+ # target for overriding Emboss defines (e.g. EMBOSS_CHECK).
pw_third_party_emboss_CONFIG =
- "$dir_pigweed/third_party/emboss:default_config"
+ "$dir_pigweed/third_party/emboss:default_overrides"
}
diff --git a/third_party/emboss/embossc_runner.py b/third_party/emboss/embossc_runner.py
index 7943c210a..27cad2c3a 100644
--- a/third_party/emboss/embossc_runner.py
+++ b/third_party/emboss/embossc_runner.py
@@ -13,6 +13,7 @@
# the License.
import sys
+import importlib.machinery
import importlib.util
loader = importlib.machinery.SourceFileLoader("embossc", sys.argv[1])
diff --git a/third_party/freertos/BUILD.bazel b/third_party/freertos/BUILD.bazel
index 01256d41b..c5a31ae45 100644
--- a/third_party/freertos/BUILD.bazel
+++ b/third_party/freertos/BUILD.bazel
@@ -36,8 +36,9 @@ constraint_setting(
name = "port",
)
+# Cortex-M33 with No TrustZone
constraint_value(
- name = "port_ARM_CM7",
+ name = "port_ARM_CM33_NTZ",
constraint_setting = ":port",
)
@@ -46,6 +47,16 @@ constraint_value(
constraint_setting = ":port",
)
+constraint_value(
+ name = "port_ARM_CM7",
+ constraint_setting = ":port",
+)
+
+constraint_value(
+ name = "port_Xtensa",
+ constraint_setting = ":port",
+)
+
pw_cc_library(
name = "freertos",
srcs = [
@@ -56,15 +67,29 @@ pw_cc_library(
"stream_buffer.c",
"timers.c",
] + select({
+ ":port_ARM_CM33_NTZ": [
+ "portable/GCC/ARM_CM33_NTZ/non_secure/port.c",
+ "portable/GCC/ARM_CM33_NTZ/non_secure/portasm.c",
+ ],
":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/port.c"],
":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/port.c"],
+ ":port_Xtensa": [
+ "portable/ThirdParty/XCC/Xtensa/mpu.S",
+ "portable/ThirdParty/XCC/Xtensa/port.c",
+ "portable/ThirdParty/XCC/Xtensa/portasm.S",
+ "portable/ThirdParty/XCC/Xtensa/portclib.c",
+ "portable/ThirdParty/XCC/Xtensa/portmpu.c",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_context.S",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_coproc_handler.S",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_intr.c",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_intr_asm.S",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_intr_wrapper.c",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_overlay_os_hook.c",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_vectors.S",
+ ],
"//conditions:default": [],
}),
- includes = ["include/"] + select({
- ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F"],
- ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1"],
- "//conditions:default": [],
- }),
+ includes = ["include/"],
textual_hdrs = [
"include/FreeRTOS.h",
"include/StackMacros.h",
@@ -82,14 +107,12 @@ pw_cc_library(
"include/stream_buffer.h",
"include/task.h",
"include/timers.h",
- ] + select({
- ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
- ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
- "//conditions:default": [],
- }),
+ ],
deps = [
+ ":freertos_malloc",
+ ":freertos_port_headers",
":pigweed_tasks_c",
- "@pigweed_config//:freertos_config",
+ "@pigweed//targets:freertos_config",
],
# Required because breaking out tasks_c results in the dependencies between
# the libraries not being quite correct: to link pigweed_tasks_c you
@@ -97,6 +120,60 @@ pw_cc_library(
alwayslink = 1,
)
+pw_cc_library(
+ name = "freertos_port_headers",
+ hdrs = select({
+ ":port_ARM_CM33_NTZ": [
+ "portable/GCC/ARM_CM33_NTZ/non_secure/portasm.h",
+ "portable/GCC/ARM_CM33_NTZ/non_secure/portmacro.h",
+ ],
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
+ ":port_Xtensa": [
+ "portable/ThirdParty/XCC/Xtensa/portbenchmark.h",
+ "portable/ThirdParty/XCC/Xtensa/portmacro.h",
+ "portable/ThirdParty/XCC/Xtensa/porttrace.h",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_api.h",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_config.h",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_context.h",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_rtos.h",
+ "portable/ThirdParty/XCC/Xtensa/xtensa_timer.h",
+ ],
+ "//conditions:default": [],
+ }),
+ includes = select({
+ ":port_ARM_CM33_NTZ": ["portable/GCC/ARM_CM33_NTZ/non_secure"],
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/"],
+ ":port_Xtensa": ["portable/ThirdParty/XCC/Xtensa"],
+ "//conditions:default": [],
+ }),
+)
+
+# Headers transitively included by using "FreeRTOS.h"
+pw_cc_library(
+ name = "freertos_headers",
+ hdrs = [
+ "include/FreeRTOS.h",
+ "include/deprecated_definitions.h",
+ "include/list.h",
+ "include/mpu_wrappers.h",
+ "include/portable.h",
+ "include/projdefs.h",
+ "include/stack_macros.h",
+ "include/task.h",
+ "include/timers.h",
+ ],
+ includes = [
+ "include/",
+ ],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":freertos_port_headers",
+ "@pigweed//targets:freertos_config",
+ ],
+)
+
# Constraint setting used to determine if task statics should be disabled.
constraint_setting(
name = "disable_tasks_statics_setting",
@@ -122,36 +199,13 @@ pw_cc_library(
],
"//conditions:default": [],
}),
- includes = [
- "include/",
- ] + select({
- ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/"],
- ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/"],
- "//conditions:default": [],
- }),
local_defines = select({
":disable_task_statics": [
"static=",
],
"//conditions:default": [],
}),
- # tasks.c transitively includes all these headers :/
- textual_hdrs = [
- "include/FreeRTOS.h",
- "include/portable.h",
- "include/projdefs.h",
- "include/list.h",
- "include/deprecated_definitions.h",
- "include/mpu_wrappers.h",
- "include/stack_macros.h",
- "include/task.h",
- "include/timers.h",
- ] + select({
- ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
- ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
- "//conditions:default": [],
- }),
- deps = ["@pigweed_config//:freertos_config"],
+ deps = [":freertos_headers"],
)
# Constraint setting used to select the FreeRTOSConfig version.
@@ -174,6 +228,50 @@ pw_cc_library(
target_compatible_with = ["@platforms//:incompatible"],
)
+# Constraint setting for malloc implementation.
+constraint_setting(
+ name = "malloc",
+ default_constraint_value = ":no_malloc",
+)
+
+constraint_value(
+ name = "no_malloc",
+ constraint_setting = ":malloc",
+)
+
+constraint_value(
+ name = "malloc_heap_1",
+ constraint_setting = ":malloc",
+)
+
+constraint_value(
+ name = "malloc_heap_2",
+ constraint_setting = ":malloc",
+)
+
+constraint_value(
+ name = "malloc_heap_3",
+ constraint_setting = ":malloc",
+)
+
+constraint_value(
+ name = "malloc_heap_4",
+ constraint_setting = ":malloc",
+)
+
+pw_cc_library(
+ name = "freertos_malloc",
+ srcs = select({
+ ":malloc_heap_1": ["portable/MemMang/heap_1.c"],
+ ":malloc_heap_2": ["portable/MemMang/heap_2.c"],
+ ":malloc_heap_3": ["portable/MemMang/heap_3.c"],
+ ":malloc_heap_4": ["portable/MemMang/heap_4.c"],
+ ":no_malloc": [],
+ }),
+ visibility = ["//visibility:private"],
+ deps = [":freertos_headers"],
+)
+
# Exported for
# pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
exports_files(
diff --git a/third_party/freertos/BUILD.gn b/third_party/freertos/BUILD.gn
index f4c12bb07..919f94499 100644
--- a/third_party/freertos/BUILD.gn
+++ b/third_party/freertos/BUILD.gn
@@ -120,12 +120,37 @@ if (dir_pw_third_party_freertos == "") {
]
}
+ # ARM CM33 port of FreeRTOS
+ config("arm_cm33_includes") {
+ include_dirs =
+ [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM33_NTZ/non_secure" ]
+ visibility = [ ":arm_cm33" ]
+ }
+
+ pw_source_set("arm_cm33") {
+ public_configs = [
+ ":arm_cm33_includes",
+ ":public_includes",
+ ]
+ public_deps = [ pw_third_party_freertos_CONFIG ]
+ public = [
+ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM33_NTZ/non_secure/portasm.h",
+ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM33_NTZ/non_secure/portmacro.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM33_NTZ/non_secure/port.c",
+ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM33_NTZ/non_secure/portasm.c",
+ ]
+ configs = [ ":disable_warnings" ]
+ }
+
# ARM CM7 port of FreeRTOS
config("arm_cm7_includes") {
include_dirs = [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM7/r0p1" ]
visibility = [ ":arm_cm7" ]
}
+ # NB: Use :arm_cm7_after_r0p1 instead if you can
pw_source_set("arm_cm7") {
public_configs = [
":arm_cm7_includes",
@@ -139,6 +164,14 @@ if (dir_pw_third_party_freertos == "") {
configs = [ ":disable_warnings" ]
}
+ # CM7 r0p0 and r0p1 cores have errata that requires workarounds. Freertos
+ # recommends using the CM4F port on newer CM7 core revisions for better
+ # performance.
+ # See Freertos' ARM_CM7/ReadMe.txt.
+ pw_source_set("arm_cm7_after_r0p1") {
+ public_deps = [ ":arm_cm4f" ]
+ }
+
# ARM CM4F port of FreeRTOS.
config("arm_cm4f_includes") {
include_dirs = [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM4F" ]
diff --git a/third_party/freertos/docs.rst b/third_party/freertos/docs.rst
index 695dd7d32..05b29b99f 100644
--- a/third_party/freertos/docs.rst
+++ b/third_party/freertos/docs.rst
@@ -41,22 +41,30 @@ In order to use this you are expected to set the following variables from
Bazel
=====
+.. There's a bug in the Bazel docs site which is causing the link to the evergreen
+.. section on constraint settings to 404. So for now, we'll just link to the
+.. v5.4.0 doc on constraint settings. When the Bazel bug is fixed we can return the
+.. URL to https://bazel.build/reference/be/platform#constraint_setting
+
In Bazel, the FreeRTOS build is configured through `constraint_settings
-<https://bazel.build/reference/be/platform#constraint_setting>`_. The `platform
+<https://docs.bazel.build/versions/5.4.0/be/platform.html#constraint_setting>`_. The `platform
<https://bazel.build/extending/platforms>`_ you are building for must specify
values for the following settings:
* ``//third_party/freertos:port``, to set which FreeRTOS port to use. You can
select a value from those defined in ``third_party/freertos/BUILD.bazel``.
+* ``//third_party/freertos:malloc``, to set which FreeRTOS malloc
+ implementation to use. You can select a value from those defined in
+ ``third_party/freertos/BUILD.bazel``.
* ``//third_party/freertos:disable_task_statics_setting``, to determine
whether statics should be disabled during compilation of the tasks.c source
file (see next section). This setting has only two possible values, also
defined in ``third_party/freertos/BUILD.bazel``.
-In addition, you need to set the ``@pigweed_config//:freertos_config`` label
+In addition, you need to set the ``@pigweed//targets:freertos_config`` label
flag to point to the library target providing the FreeRTOS config header. See
:ref:`docs-build_system-bazel_configuration` for a discussion of how to work
-with ``@pigweed_config``.
+with our label flags.
.. _third_party-freertos_disable_task_statics:
@@ -92,8 +100,8 @@ this configuration, which correctly sets ``configASSERT`` to use ``PW_CHECK` and
-----------------------------
OS Abstraction Layers Support
-----------------------------
-Support for Pigweed's :ref:`docs-os_abstraction_layers` are provided for
-FreeRTOS via the following modules:
+Support for Pigweed's :ref:`docs-os` are provided for FreeRTOS via the following
+modules:
* :ref:`module-pw_chrono_freertos`
* :ref:`module-pw_sync_freertos`
diff --git a/third_party/freertos/public/pw_third_party/freertos/config_assert.h b/third_party/freertos/public/pw_third_party/freertos/config_assert.h
index 11d8048ed..f7123df4f 100644
--- a/third_party/freertos/public/pw_third_party/freertos/config_assert.h
+++ b/third_party/freertos/public/pw_third_party/freertos/config_assert.h
@@ -29,5 +29,5 @@ extern "C++" {
#endif // PW_THIRD_PARTY_FREERTOS_NO_STATICS == 1
#ifdef __cplusplus
-} // extern "C++"
+} // extern "C++"
#endif // __cplusplus
diff --git a/third_party/fuchsia/BUILD.bazel b/third_party/fuchsia/BUILD.bazel
index d3d90a74f..e373015df 100644
--- a/third_party/fuchsia/BUILD.bazel
+++ b/third_party/fuchsia/BUILD.bazel
@@ -22,12 +22,6 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "config",
- hdrs = ["public/pw_function/config.h"],
- includes = ["public"],
-)
-
-pw_cc_library(
name = "fit",
srcs = [
"repo/sdk/lib/fit/include/lib/fit/internal/compiler.h",
@@ -54,6 +48,7 @@ pw_cc_library(
"repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h",
"repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h",
"repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/functional.h",
"repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h",
"repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h",
"repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h",
diff --git a/third_party/fuchsia/BUILD.gn b/third_party/fuchsia/BUILD.gn
index 2773a80d1..73c440d32 100644
--- a/third_party/fuchsia/BUILD.gn
+++ b/third_party/fuchsia/BUILD.gn
@@ -18,44 +18,22 @@ import("$dir_pw_build/python.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
+import("fuchsia.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
-config("fit_public_include_path") {
- include_dirs = [ "repo/sdk/lib/fit/include" ]
- visibility = [ ":*" ]
-}
-
-config("stdcompat_public_include_path") {
- include_dirs = [ "repo/sdk/lib/stdcompat/include" ]
- visibility = [ ":*" ]
-}
-
-pw_source_set("fit") {
- public_configs = [ ":fit_public_include_path" ]
- public_deps = [
- ":stdcompat",
- dir_pw_assert,
- ]
- public = [
- "repo/sdk/lib/fit/include/lib/fit/function.h",
- "repo/sdk/lib/fit/include/lib/fit/nullable.h",
- "repo/sdk/lib/fit/include/lib/fit/result.h",
- "repo/sdk/lib/fit/include/lib/fit/traits.h",
- ]
- sources = [
- "repo/sdk/lib/fit/include/lib/fit/internal/compiler.h",
- "repo/sdk/lib/fit/include/lib/fit/internal/function.h",
- "repo/sdk/lib/fit/include/lib/fit/internal/result.h",
- "repo/sdk/lib/fit/include/lib/fit/internal/utility.h",
- ]
+group("fit") {
+ public_deps = [ "$dir_pw_third_party_fuchsia/sdk/lib/fit" ]
}
pw_test("function_tests") {
sources = [ "repo/sdk/lib/fit/test/function_tests.cc" ]
- deps = [ ":fit" ]
+ deps = [
+ ":fit",
+ dir_pw_polyfill,
+ ]
# Define EXPECT_NULL(), which Pigweed's test framework does not have
defines = [ "EXPECT_NULL(arg)=EXPECT_EQ((arg), nullptr)" ]
@@ -64,25 +42,8 @@ pw_test("function_tests") {
remove_configs = [ "$dir_pw_build:strict_warnings" ]
}
-pw_source_set("stdcompat") {
- public_configs = [ ":stdcompat_public_include_path" ]
- public = [
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h",
- ]
- sources = [
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h",
- "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h",
- ]
+group("stdcompat") {
+ public_deps = [ "$dir_pw_third_party_fuchsia/sdk/lib/stdcompat" ]
}
pw_python_script("generate_fuchsia_patch") {
diff --git a/third_party/fuchsia/copy.bara.sky b/third_party/fuchsia/copy.bara.sky
index 7d51519e8..96508d99e 100644
--- a/third_party/fuchsia/copy.bara.sky
+++ b/third_party/fuchsia/copy.bara.sky
@@ -14,7 +14,9 @@
fuchsia_repo_files = [
".clang-format",
+ "LICENSE",
# fit
+ "sdk/lib/fit/include/lib/fit/defer.h",
"sdk/lib/fit/include/lib/fit/function.h",
"sdk/lib/fit/include/lib/fit/internal/compiler.h",
"sdk/lib/fit/include/lib/fit/internal/function.h",
@@ -35,14 +37,19 @@ fuchsia_repo_files = [
"sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h",
"sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h",
"sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/functional.h",
"sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h",
"sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h",
"sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h",
+ # lazy_init
+ "zircon/system/ulib/lazy_init/include/lib/lazy_init/internal/storage.h",
+ "zircon/system/ulib/lazy_init/include/lib/lazy_init/lazy_init.h",
+ "zircon/system/ulib/lazy_init/include/lib/lazy_init/options.h",
]
core.workflow(
name = "default",
- description = "Imports files from Fuchsia's fit library",
+ description = "Imports files from Fuchsia's fit, stdcompat and lazy_init libraries",
origin = git.origin(
url = "https://fuchsia.googlesource.com/fuchsia",
ref = "main",
@@ -55,7 +62,8 @@ core.workflow(
checker = leakr.disable_check("Syncing between OSS projects"),
),
origin_files = glob(fuchsia_repo_files),
- destination_files = glob(["third_party/fuchsia/repo/**"]),
+ # Exclude BUILD.gn files to keep Pigweed's versions.
+ destination_files = glob(["third_party/fuchsia/repo/**"], exclude = ["**/BUILD.gn"]),
authoring = authoring.pass_thru("Fuchsia Authors <noreply@google.com>"),
transformations = [
core.move("", "third_party/fuchsia/repo"),
@@ -65,7 +73,7 @@ core.workflow(
core.replace("#include <zxtest/zxtest.h>", "#include \"gtest/gtest.h\""),
# Show all commits but exclude the author to reduce line length.
metadata.squash_notes(
- "third_party/fuchsia: Copybara import of the fit library\n\n",
+ "third_party/fuchsia: Copybara import\n\n",
show_author = False,
),
],
diff --git a/third_party/fuchsia/fuchsia.gni b/third_party/fuchsia/fuchsia.gni
new file mode 100644
index 000000000..3f47a2233
--- /dev/null
+++ b/third_party/fuchsia/fuchsia.gni
@@ -0,0 +1,21 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # Path to the Fuchsia sources to use in Pigweed's build. Defaults to Pigweed's
+ # mirror of the few Fuchsia source files it uses.
+ dir_pw_third_party_fuchsia = "$dir_pw_third_party/fuchsia/repo"
+}
diff --git a/third_party/fuchsia/generate_fuchsia_patch.py b/third_party/fuchsia/generate_fuchsia_patch.py
index 9025a139e..09377a040 100755
--- a/third_party/fuchsia/generate_fuchsia_patch.py
+++ b/third_party/fuchsia/generate_fuchsia_patch.py
@@ -14,7 +14,7 @@
# the License.
"""Generates a patch file for sources from Fuchsia's fit and stdcompat.
-Run this script to update third_party/fuchsia/function.patch.
+Run this script to update third_party/fuchsia/pigweed_adaptations.patch.
"""
from pathlib import Path
@@ -40,6 +40,10 @@ HEADER = f'''# Copyright {datetime.today().year} The Pigweed Authors
# License for the specific language governing permissions and limitations under
# the License.
+# DO NOT EDIT! This file is generated by {Path(__file__).name}.
+#
+# To make changes, update and run ./{Path(__file__).name}.
+
# Patch the fit::function implementation for use in Pigweed:
#
# - Use PW_ASSERT instead of __builtin_abort.
@@ -80,7 +84,7 @@ def _clone_fuchsia(temp_path: Path) -> Path:
return temp_path / 'fuchsia'
-# TODO(b/248257406): Replace typing.List with list. # pylint: disable=fixme
+# TODO: b/248257406 - Replace typing.List with list. # pylint: disable=fixme
def _read_files(script: Path) -> List[Path]:
with script.open() as file:
paths_list: List[str] = eval( # pylint: disable=eval-used
@@ -108,9 +112,24 @@ def _patch_assert(text: str) -> str:
return _add_include_before_namespace(replaced, 'pw_assert/assert.h')
+_CONSTINIT = re.compile(r'\b__CONSTINIT\b')
+
+
+def _patch_constinit(text: str) -> str:
+ replaced = _CONSTINIT.sub('PW_CONSTINIT', text)
+
+ if replaced == text:
+ return text
+
+ replaced = replaced.replace('#include <zircon/compiler.h>\n', '')
+ return _add_include_before_namespace(
+ replaced, "pw_polyfill/language_feature_macros.h"
+ )
+
+
_INVOKE_PATCH = (
'\n'
- ' // TODO(b/241567321): Remove "no sanitize" after pw_protobuf is fixed.\n'
+ ' // TODO: b/241567321 - Remove "no sanitize" after pw_protobuf is fixed.\n'
' Result invoke(Args... args) const PW_NO_SANITIZE("function") {'
)
@@ -129,6 +148,7 @@ def _patch_invoke(file: Path, text: str) -> str:
def _patch(file: Path) -> Optional[str]:
text = file.read_text()
updated = _patch_assert(text)
+ updated = _patch_constinit(updated)
updated = _patch_invoke(file, updated)
return None if text == updated else updated
diff --git a/third_party/fuchsia/pigweed_adaptations.patch b/third_party/fuchsia/pigweed_adaptations.patch
index 4f7ea5b61..fd6bcd58c 100644
--- a/third_party/fuchsia/pigweed_adaptations.patch
+++ b/third_party/fuchsia/pigweed_adaptations.patch
@@ -12,6 +12,10 @@
# License for the specific language governing permissions and limitations under
# the License.
+# DO NOT EDIT! This file is generated by generate_fuchsia_patch.py.
+#
+# To make changes, update and run ./generate_fuchsia_patch.py.
+
# Patch the fit::function implementation for use in Pigweed:
#
# - Use PW_ASSERT instead of __builtin_abort.
@@ -29,7 +33,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/funct
namespace fit {
namespace internal {
-@@ -87,7 +89,7 @@ inline const void* unshared_target_type_id(void* /*bits*/, const void* impl_ops)
+@@ -88,7 +90,7 @@ inline const void* unshared_target_type_id(void* /*bits*/, const void* impl_ops)
// elsewhere in the header as an inline variable.
template <typename Unused = void>
struct null_target {
@@ -38,17 +42,17 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/funct
static const target_ops<void> ops;
-@@ -493,7 +495,8 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
+@@ -507,7 +509,8 @@ class function_base<inline_target_size, require_inline, Result(Args...), Allocat
// Note that fit::callback will release the target immediately after
// invoke() (also affecting any share()d copies).
// Aborts if the function's target is empty.
- Result invoke(Args... args) const {
-+ // TODO(b/241567321): Remove "no sanitize" after pw_protobuf is fixed.
++ // TODO: b/241567321 - Remove "no sanitize" after pw_protobuf is fixed.
+ Result invoke(Args... args) const PW_NO_SANITIZE("function") {
// Down cast the ops to the derived type that this function was instantiated
// with, which includes the invoke function.
//
-@@ -523,7 +526,7 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
+@@ -537,7 +540,7 @@ class function_base<inline_target_size, require_inline, Result(Args...), Allocat
template <typename SharedFunction>
void copy_shared_target_to(SharedFunction& copy) {
copy.destroy_target();
@@ -57,7 +61,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/funct
shared_target_type<SharedFunction>::copy_shared_ptr(base::bits(), copy.bits());
copy.set_ops(base::ops());
}
-@@ -553,7 +556,7 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
+@@ -567,7 +570,7 @@ class function_base<inline_target_size, require_inline, Result(Args...), Allocat
void check_target_type() const {
if (target_type<Callable>::ops.target_type_id(nullptr, &target_type<Callable>::ops) !=
base::target_type_id()) {
@@ -78,39 +82,36 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h b/t
namespace fit {
// Determines whether a type can be compared with nullptr.
-@@ -130,28 +132,28 @@ class nullable<T, true> final {
+@@ -130,25 +132,25 @@ class nullable<T, true> final {
if (has_value()) {
return value_;
- } else {
-- __builtin_abort();
-+ PW_ASSERT(false);
}
+- __builtin_abort();
++ PW_ASSERT(false);
}
constexpr const T& value() const& {
if (has_value()) {
return value_;
- } else {
-- __builtin_abort();
-+ PW_ASSERT(false);
}
+- __builtin_abort();
++ PW_ASSERT(false);
}
constexpr T&& value() && {
if (has_value()) {
return std::move(value_);
- } else {
-- __builtin_abort();
-+ PW_ASSERT(false);
}
+- __builtin_abort();
++ PW_ASSERT(false);
}
constexpr const T&& value() const&& {
if (has_value()) {
return std::move(value_);
- } else {
-- __builtin_abort();
-+ PW_ASSERT(false);
}
+- __builtin_abort();
++ PW_ASSERT(false);
}
+ template <typename U = T>
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
@@ -123,7 +124,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
namespace fit {
// Convenience type to indicate failure without elaboration.
-@@ -280,25 +282,25 @@ class LIB_FIT_NODISCARD result<E, T> {
+@@ -286,25 +288,25 @@ class [[nodiscard]] result<E, T> {
if (is_error()) {
return storage_.error_or_value.error;
}
@@ -153,7 +154,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Moves the underlying error and returns it as an instance of fit::error, simplifying
-@@ -309,7 +311,7 @@ class LIB_FIT_NODISCARD result<E, T> {
+@@ -315,7 +317,7 @@ class [[nodiscard]] result<E, T> {
if (is_error()) {
return error<E>(std::move(storage_.error_or_value.error));
}
@@ -162,7 +163,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Accessors for the underlying value.
-@@ -319,25 +321,25 @@ class LIB_FIT_NODISCARD result<E, T> {
+@@ -325,25 +327,25 @@ class [[nodiscard]] result<E, T> {
if (is_ok()) {
return storage_.error_or_value.value;
}
@@ -192,7 +193,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Moves the underlying value and returns it as an instance of fit::success, simplifying
-@@ -348,7 +350,7 @@ class LIB_FIT_NODISCARD result<E, T> {
+@@ -354,7 +356,7 @@ class [[nodiscard]] result<E, T> {
if (is_ok()) {
return success<T>(std::move(storage_.error_or_value.value));
}
@@ -201,7 +202,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Contingent accessors for the underlying value.
-@@ -377,13 +379,13 @@ class LIB_FIT_NODISCARD result<E, T> {
+@@ -383,13 +385,13 @@ class [[nodiscard]] result<E, T> {
if (is_ok()) {
return ::fit::internal::arrow_operator<T>::forward(storage_.error_or_value.value);
}
@@ -217,7 +218,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Accessors for the underlying value. This is a syntax sugar for value().
-@@ -406,7 +408,7 @@ class LIB_FIT_NODISCARD result<E, T> {
+@@ -412,7 +414,7 @@ class [[nodiscard]] result<E, T> {
storage_.error_or_value.error += std::move(error.value_);
return *this;
}
@@ -226,7 +227,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Maps a result<E, T> to a result<E2, T> by transforming the error through
-@@ -517,25 +519,25 @@ class LIB_FIT_NODISCARD result<E> {
+@@ -523,25 +525,25 @@ class [[nodiscard]] result<E> {
if (is_error()) {
return storage_.error_or_value.error;
}
@@ -256,7 +257,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Moves the underlying error and returns it as an instance of fit::error, simplifying
-@@ -546,7 +548,7 @@ class LIB_FIT_NODISCARD result<E> {
+@@ -552,7 +554,7 @@ class [[nodiscard]] result<E> {
if (is_error()) {
return error<E>(std::move(storage_.error_or_value.error));
}
@@ -265,7 +266,7 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Augments the error value of the result with the given value. The operator E::operator+=(F) must
-@@ -560,7 +562,7 @@ class LIB_FIT_NODISCARD result<E> {
+@@ -566,7 +568,7 @@ class [[nodiscard]] result<E> {
storage_.error_or_value.error += std::move(error.value_);
return *this;
}
@@ -274,3 +275,34 @@ diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/thi
}
// Maps a result<E, T> to a result<E2, T> by transforming the error through
+diff --git a/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc b/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
+--- a/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
++++ b/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
+@@ -4,7 +4,6 @@
+
+ #include <lib/fit/function.h>
+ #include <lib/stdcompat/bit.h>
+-#include <zircon/compiler.h>
+
+ #include <algorithm>
+ #include <array>
+@@ -15,6 +14,8 @@
+
+ #include <zxtest/zxtest.h>
+
++#include "pw_polyfill/language_feature_macros.h"
++
+ namespace {
+
+ using ::std::size_t;
+@@ -1029,8 +1030,8 @@ TEST(FunctionTests, callback_with_custom_allocator) {
+ EXPECT_EQ(1, cbheapdestroy);
+ }
+
+-__CONSTINIT const fit::function<void()> kDefaultConstructed;
+-__CONSTINIT const fit::function<void()> kNullptrConstructed(nullptr);
++PW_CONSTINIT const fit::function<void()> kDefaultConstructed;
++PW_CONSTINIT const fit::function<void()> kNullptrConstructed(nullptr);
+
+ TEST(FunctionTests, null_constructors_are_constexpr) {
+ EXPECT_EQ(kDefaultConstructed, nullptr);
diff --git a/third_party/fuchsia/repo/LICENSE b/third_party/fuchsia/repo/LICENSE
new file mode 100644
index 000000000..7ed244f42
--- /dev/null
+++ b/third_party/fuchsia/repo/LICENSE
@@ -0,0 +1,24 @@
+Copyright 2019 The Fuchsia Authors.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/BUILD.gn b/third_party/fuchsia/repo/sdk/lib/fit/BUILD.gn
new file mode 100644
index 000000000..e789cda5c
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/BUILD.gn
@@ -0,0 +1,45 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This build file is defined by Pigweed, not Fuchsia. It only refers to sources
+# imported into the Pigweed repo.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("public_include_path") {
+ include_dirs = [ "include" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("fit") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_third_party/fuchsia:stdcompat",
+ dir_pw_assert,
+ ]
+ public = [
+ "include/lib/fit/function.h",
+ "include/lib/fit/nullable.h",
+ "include/lib/fit/result.h",
+ "include/lib/fit/traits.h",
+ ]
+ sources = [
+ "include/lib/fit/internal/compiler.h",
+ "include/lib/fit/internal/function.h",
+ "include/lib/fit/internal/result.h",
+ "include/lib/fit/internal/utility.h",
+ ]
+}
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/defer.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/defer.h
new file mode 100644
index 000000000..907a3b77e
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/defer.h
@@ -0,0 +1,145 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_DEFER_H_
+#define LIB_FIT_DEFER_H_
+
+#include <utility>
+
+#include "function.h"
+#include "nullable.h"
+
+namespace fit {
+
+// A move-only deferred action wrapper with RAII semantics.
+// This class is not thread safe.
+//
+// The wrapper holds a function-like callable target with no arguments
+// which it invokes when it goes out of scope unless canceled, called, or
+// moved to a wrapper in a different scope.
+//
+// See |fit::defer()| for idiomatic usage.
+template <typename T>
+class deferred_action final {
+ public:
+ // Creates a deferred action without a pending target.
+ deferred_action() = default;
+ explicit deferred_action(decltype(nullptr)) {}
+
+ // Creates a deferred action with a pending target.
+ explicit deferred_action(T target) : target_(std::move(target)) {}
+
+ // Creates a deferred action with a pending target moved from another
+ // deferred action, leaving the other one without a pending target.
+ deferred_action(deferred_action&& other) : target_(std::move(other.target_)) {
+ other.target_.reset();
+ }
+
+ // Invokes and releases the deferred action's pending target (if any).
+ ~deferred_action() { call(); }
+
+ // Returns true if the deferred action has a pending target.
+ explicit operator bool() const { return !!target_; }
+
+ // Invokes and releases the deferred action's pending target (if any),
+ // then move-assigns it from another deferred action, leaving the latter
+ // one without a pending target.
+ deferred_action& operator=(deferred_action&& other) {
+ if (&other == this)
+ return *this;
+ call();
+ target_ = std::move(other.target_);
+ other.target_.reset();
+ return *this;
+ }
+
+ // Invokes and releases the deferred action's pending target (if any).
+ void call() {
+ if (target_) {
+ // Move to a local to guard against re-entrance.
+ T local_target = std::move(*target_);
+ target_.reset();
+ local_target();
+ }
+ }
+
+ // Releases the deferred action's pending target (if any) without
+ // invoking it.
+ void cancel() { target_.reset(); }
+ deferred_action& operator=(decltype(nullptr)) {
+ cancel();
+ return *this;
+ }
+
+ // Assigns a new target to the deferred action.
+ deferred_action& operator=(T target) {
+ target_ = std::move(target);
+ return *this;
+ }
+
+ deferred_action(const deferred_action& other) = delete;
+ deferred_action& operator=(const deferred_action& other) = delete;
+
+ private:
+ nullable<T> target_;
+};
+
+template <typename T>
+bool operator==(const deferred_action<T>& action, decltype(nullptr)) {
+ return !action;
+}
+template <typename T>
+bool operator==(decltype(nullptr), const deferred_action<T>& action) {
+ return !action;
+}
+template <typename T>
+bool operator!=(const deferred_action<T>& action, decltype(nullptr)) {
+ return !!action;
+}
+template <typename T>
+bool operator!=(decltype(nullptr), const deferred_action<T>& action) {
+ return !!action;
+}
+
+// Defers execution of a function-like callable target with no arguments
+// until the value returned by this function goes out of scope unless canceled,
+// called, or moved to a wrapper in a different scope.
+//
+// // This example prints "Hello..." then "Goodbye!".
+// void test() {
+// auto d = fit::defer([]{ puts("Goodbye!"); });
+// puts("Hello...");
+// }
+//
+// // This example prints nothing because the deferred action is canceled.
+// void do_nothing() {
+// auto d = fit::defer([]{ puts("I'm not here."); });
+// d.cancel();
+// }
+//
+// // This example shows how the deferred action can be reassigned assuming
+// // the new target has the same type and the old one, in this case by
+// // representing the target as a |fit::closure|.
+// void reassign() {
+// auto d = fit::defer<fit::closure>([] { puts("This runs first."); });
+// d = fit::defer<fit::closure>([] { puts("This runs afterwards."); });
+// }
+template <typename T>
+__attribute__((__warn_unused_result__)) inline deferred_action<T> defer(T target) {
+ return deferred_action<T>(std::move(target));
+}
+
+// Alias for a deferred_action using a fit::callback.
+using deferred_callback = deferred_action<fit::callback<void()>>;
+
+// Defers execution of a fit::callback with no arguments. See |fit::defer| for
+// details.
+__attribute__((__warn_unused_result__)) inline deferred_callback defer_callback(
+ fit::callback<void()> target) {
+ return deferred_callback(std::move(target));
+}
+
+} // namespace fit
+
+#endif // LIB_FIT_DEFER_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h
index fba3e95c2..2403dee3b 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h
@@ -2,9 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_FUNCTION_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_FUNCTION_H_
+#ifndef LIB_FIT_FUNCTION_H_
+#define LIB_FIT_FUNCTION_H_
+#include <cstddef>
+#include <memory>
#include <type_traits>
#include "internal/function.h"
@@ -13,14 +15,14 @@
namespace fit {
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
class function_impl {
static_assert(std::is_function<FunctionType>::value,
"fit::function must be instantiated with a function type, such as void() or "
"int(char*, bool)");
};
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
class callback_impl {
static_assert(std::is_function<FunctionType>::value,
"fit::callback must be instantiated with a function type, such as void() or "
@@ -33,6 +35,10 @@ class callback_impl {
// function.
constexpr size_t default_inline_target_size = sizeof(void*) * 2;
+// The default allocator used for allocating callables on the heap. Its `value_type` is irrelevant,
+// since it must support rebinding.
+using default_callable_allocator = std::allocator<std::byte>;
+
// A |fit::function| is a move-only polymorphic function wrapper.
//
// If you need a class with similar characteristics that also ensures
@@ -61,6 +67,9 @@ constexpr size_t default_inline_target_size = sizeof(void*) * 2;
// fit within a function without requiring heap allocation.
// Defaults to |default_inline_target_size|.
//
+// |Allocator| is the Allocator used for heap allocation, if required. Its `value_type` is
+// irrelevant, since it must support rebinding.
+//
// Class members are documented in |fit::function_impl|, below.
//
// EXAMPLES
@@ -70,9 +79,10 @@ constexpr size_t default_inline_target_size = sizeof(void*) * 2;
// -
// https://fuchsia.googlesource.com/fuchsia/+/HEAD/sdk/lib/fit/test/examples/function_example2.cc
//
-template <typename T, size_t inline_target_size = default_inline_target_size>
+template <typename T, size_t inline_target_size = default_inline_target_size,
+ typename Allocator = default_callable_allocator>
using function = function_impl<internal::RoundUpToWord(inline_target_size),
- /*require_inline=*/false, T>;
+ /*require_inline=*/false, T, Allocator>;
// A move-only callable object wrapper that forces callables to be stored inline
// and never performs heap allocation.
@@ -82,7 +92,7 @@ using function = function_impl<internal::RoundUpToWord(inline_target_size),
// compile.
template <typename T, size_t inline_target_size = default_inline_target_size>
using inline_function = function_impl<internal::RoundUpToWord(inline_target_size),
- /*require_inline=*/true, T>;
+ /*require_inline=*/true, T, default_callable_allocator>;
// Synonym for a function which takes no arguments and produces no result.
using closure = function<void()>;
@@ -153,11 +163,15 @@ using closure = function<void()>;
// fit within a callback without requiring heap allocation.
// Defaults to |default_inline_target_size|.
//
+// |Allocator| is the Allocator used for heap allocation, if required. Its `value_type` is
+// irrelevant, since it must support rebinding.
+//
// Class members are documented in |fit::callback_impl|, below.
//
-template <typename T, size_t inline_target_size = default_inline_target_size>
-using callback =
- callback_impl<internal::RoundUpToWord(inline_target_size), /*require_inline=*/false, T>;
+template <typename T, size_t inline_target_size = default_inline_target_size,
+ typename Allocator = default_callable_allocator>
+using callback = callback_impl<internal::RoundUpToWord(inline_target_size),
+ /*require_inline=*/false, T, Allocator>;
// A move-only, run-once, callable object wrapper that forces callables to be
// stored inline and never performs heap allocation.
@@ -167,19 +181,22 @@ using callback =
// compile.
template <typename T, size_t inline_target_size = default_inline_target_size>
using inline_callback = callback_impl<internal::RoundUpToWord(inline_target_size),
- /*require_inline=*/true, T>;
+ /*require_inline=*/true, T, default_callable_allocator>;
-template <size_t inline_target_size, bool require_inline, typename Result, typename... Args>
-class function_impl<inline_target_size, require_inline, Result(Args...)> final
- : private ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)> {
- using base = ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+template <size_t inline_target_size, bool require_inline, typename Allocator, typename Result,
+ typename... Args>
+class function_impl<inline_target_size, require_inline, Result(Args...), Allocator> final
+ : private ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...),
+ Allocator> {
+ using base = ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...),
+ Allocator>;
// function_base requires private access during share()
- friend class ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+ friend base;
// supports target() for shared functions
friend const void* ::fit::internal::get_target_type_id<>(
- const function_impl<inline_target_size, require_inline, Result(Args...)>&);
+ const function_impl<inline_target_size, require_inline, Result(Args...), Allocator>&);
template <typename U>
using not_self_type = ::fit::internal::not_same_type<function_impl, U>;
@@ -228,10 +245,9 @@ class function_impl<inline_target_size, require_inline, Result(Args...)> final
// unexpected behavior of a |fit::function| that would otherwise fail after
// one call. To explicitly allow this, simply wrap the |fit::callback| in a
// pass-through lambda before passing it to the |fit::function|.
- template <size_t other_inline_target_size, bool other_require_inline>
- function_impl(
- ::fit::callback_impl<other_inline_target_size, other_require_inline, Result(Args...)>) =
- delete;
+ template <size_t other_inline_target_size, bool other_require_inline, typename OtherAllocator>
+ function_impl(::fit::callback_impl<other_inline_target_size, other_require_inline,
+ Result(Args...), OtherAllocator>) = delete;
// Creates a function with a target moved from another function,
// leaving the other function with an empty target.
@@ -273,10 +289,9 @@ class function_impl<inline_target_size, require_inline, Result(Args...)> final
// fail after one call. To explicitly allow this, simply wrap the
// |fit::callback| in a pass-through lambda before assigning it to the
// |fit::function|.
- template <size_t other_inline_target_size, bool other_require_inline>
- function_impl& operator=(
- ::fit::callback_impl<other_inline_target_size, other_require_inline, Result(Args...)>) =
- delete;
+ template <size_t other_inline_target_size, bool other_require_inline, typename OtherAllocator>
+ function_impl& operator=(::fit::callback_impl<other_inline_target_size, other_require_inline,
+ Result(Args...), OtherAllocator>) = delete;
// Move assignment
function_impl& operator=(function_impl&& other) noexcept {
@@ -313,44 +328,49 @@ class function_impl<inline_target_size, require_inline, Result(Args...)> final
}
};
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-void swap(function_impl<inline_target_size, require_inline, FunctionType>& a,
- function_impl<inline_target_size, require_inline, FunctionType>& b) {
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+void swap(function_impl<inline_target_size, require_inline, FunctionType, Allocator>& a,
+ function_impl<inline_target_size, require_inline, FunctionType, Allocator>& b) {
a.swap(b);
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator==(const function_impl<inline_target_size, require_inline, FunctionType>& f,
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator==(const function_impl<inline_target_size, require_inline, FunctionType, Allocator>& f,
decltype(nullptr)) {
return !f;
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator==(decltype(nullptr),
- const function_impl<inline_target_size, require_inline, FunctionType>& f) {
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator==(
+ decltype(nullptr),
+ const function_impl<inline_target_size, require_inline, FunctionType, Allocator>& f) {
return !f;
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator!=(const function_impl<inline_target_size, require_inline, FunctionType>& f,
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator!=(const function_impl<inline_target_size, require_inline, FunctionType, Allocator>& f,
decltype(nullptr)) {
return !!f;
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator!=(decltype(nullptr),
- const function_impl<inline_target_size, require_inline, FunctionType>& f) {
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator!=(
+ decltype(nullptr),
+ const function_impl<inline_target_size, require_inline, FunctionType, Allocator>& f) {
return !!f;
}
-template <size_t inline_target_size, bool require_inline, typename Result, typename... Args>
-class callback_impl<inline_target_size, require_inline, Result(Args...)> final
- : private ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)> {
- using base = ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+template <size_t inline_target_size, bool require_inline, typename Allocator, typename Result,
+ typename... Args>
+class callback_impl<inline_target_size, require_inline, Result(Args...), Allocator> final
+ : private ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...),
+ Allocator> {
+ using base = ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...),
+ Allocator>;
// function_base requires private access during share()
- friend class ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+ friend base;
// supports target() for shared functions
friend const void* ::fit::internal::get_target_type_id<>(
- const callback_impl<inline_target_size, require_inline, Result(Args...)>&);
+ const callback_impl<inline_target_size, require_inline, Result(Args...), Allocator>&);
template <typename U>
using not_self_type = ::fit::internal::not_same_type<callback_impl, U>;
@@ -469,30 +489,32 @@ class callback_impl<inline_target_size, require_inline, Result(Args...)> final
}
};
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-void swap(callback_impl<inline_target_size, require_inline, FunctionType>& a,
- callback_impl<inline_target_size, require_inline, FunctionType>& b) {
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+void swap(callback_impl<inline_target_size, require_inline, FunctionType, Allocator>& a,
+ callback_impl<inline_target_size, require_inline, FunctionType, Allocator>& b) {
a.swap(b);
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator==(const callback_impl<inline_target_size, require_inline, FunctionType>& f,
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator==(const callback_impl<inline_target_size, require_inline, FunctionType, Allocator>& f,
decltype(nullptr)) {
return !f;
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator==(decltype(nullptr),
- const callback_impl<inline_target_size, require_inline, FunctionType>& f) {
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator==(
+ decltype(nullptr),
+ const callback_impl<inline_target_size, require_inline, FunctionType, Allocator>& f) {
return !f;
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator!=(const callback_impl<inline_target_size, require_inline, FunctionType>& f,
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator!=(const callback_impl<inline_target_size, require_inline, FunctionType, Allocator>& f,
decltype(nullptr)) {
return !!f;
}
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
-bool operator!=(decltype(nullptr),
- const callback_impl<inline_target_size, require_inline, FunctionType>& f) {
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
+bool operator!=(
+ decltype(nullptr),
+ const callback_impl<inline_target_size, require_inline, FunctionType, Allocator>& f) {
return !!f;
}
@@ -537,4 +559,4 @@ auto bind_member(T* instance) {
} // namespace fit
-#endif // LIB_FIT_INCLUDE_LIB_FIT_FUNCTION_H_
+#endif // LIB_FIT_FUNCTION_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h
index 1bd806414..2b989a780 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h
@@ -2,20 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_COMPILER_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_COMPILER_H_
+#ifndef LIB_FIT_INTERNAL_COMPILER_H_
+#define LIB_FIT_INTERNAL_COMPILER_H_
-// Annotate a class or function with C++17's [[nodiscard]] or similar where supported by the
-// compiler.
-//
-// C++14 doesn't support [[nodiscard]], but Clang allows __attribute__((warn_unused_result))
-// to be placed on class declarations. GCC only allows the attribute to be used on methods.
-#if __cplusplus >= 201703L
-#define LIB_FIT_NODISCARD [[nodiscard]]
-#elif defined(__clang__)
-#define LIB_FIT_NODISCARD __attribute__((__warn_unused_result__))
-#else
-#define LIB_FIT_NODISCARD /* nothing */
-#endif
-
-#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_COMPILER_H_
+#endif // LIB_FIT_INTERNAL_COMPILER_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
index dd655f3d0..b2ac66626 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_FUNCTION_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_FUNCTION_H_
+#ifndef LIB_FIT_INTERNAL_FUNCTION_H_
+#define LIB_FIT_INTERNAL_FUNCTION_H_
#include <lib/stdcompat/bit.h>
#include <stddef.h>
@@ -68,7 +68,8 @@ struct target_ops final : public base_target_ops {
static_assert(sizeof(target_ops<void>) == sizeof(void (*)()) * 5, "Unexpected target_ops padding");
-template <typename Callable, bool is_inline, bool is_shared, typename Result, typename... Args>
+template <typename Callable, bool is_inline, bool is_shared, typename Allocator, typename Result,
+ typename... Args>
struct target;
inline void trivial_target_destroy(void* /*bits*/) {}
@@ -96,9 +97,10 @@ struct null_target {
static_assert(std::is_same<Unused, void>::value, "Only instantiate null_target with void");
};
-template <typename Result, typename... Args>
-struct target<decltype(nullptr), /*is_inline=*/true, /*is_shared=*/false, Result, Args...> final
- : public null_target<> {};
+template <typename Allocator, typename Result, typename... Args>
+struct target<decltype(nullptr), /*is_inline=*/true, /*is_shared=*/false, Allocator, Result,
+ Args...>
+ final : public null_target<> {};
inline void* null_target_get(void* /*bits*/) { return nullptr; }
inline void null_target_move(void* /*from_bits*/, void* /*to_bits*/) {}
@@ -117,10 +119,8 @@ inline void inline_trivial_target_move(void* from_bits, void* to_bits) {
std::memcpy(to_bits, from_bits, size_bytes);
}
-template <typename Callable, typename Result, typename... Args>
-struct target<Callable,
- /*is_inline=*/true, /*is_shared=*/false, Result, Args...>
- final {
+template <typename Callable, typename Allocator, typename Result, typename... Args>
+struct target<Callable, /*is_inline=*/true, /*is_shared=*/false, Allocator, Result, Args...> final {
template <typename Callable_>
static void initialize(void* bits, Callable_&& target) {
new (bits) Callable(std::forward<Callable_>(target));
@@ -160,23 +160,25 @@ struct target<Callable,
inline void* inline_target_get(void* bits) { return bits; }
-template <typename Callable, typename Result, typename... Args>
-constexpr target_ops<Result, Args...> target<Callable,
- /*is_inline=*/true,
- /*is_shared=*/false, Result, Args...>::ops = {
- &unshared_target_type_id, &inline_target_get, target::get_move_function(),
- target::get_destroy_function(), &target::invoke};
+template <typename Callable, typename Allocator, typename Result, typename... Args>
+constexpr target_ops<Result, Args...>
+ target<Callable, /*is_inline=*/true, /*is_shared=*/false, Allocator, Result, Args...>::ops = {
+ &unshared_target_type_id, &inline_target_get, target::get_move_function(),
+ target::get_destroy_function(), &target::invoke};
// vtable for pointer to target function
-template <typename Callable, typename Result, typename... Args>
-struct target<Callable,
- /*is_inline=*/false, /*is_shared=*/false, Result, Args...>
+template <typename Callable, typename Allocator, typename Result, typename... Args>
+struct target<Callable, /*is_inline=*/false, /*is_shared=*/false, Allocator, Result, Args...>
final {
template <typename Callable_>
static void initialize(void* bits, Callable_&& target) {
auto ptr = static_cast<Callable**>(bits);
- *ptr = new Callable(std::forward<Callable_>(target));
+ CallableAllocator allocator;
+ *ptr = CallableAllocatorTraits::allocate(allocator, 1u);
+ if (*ptr) {
+ CallableAllocatorTraits::construct(allocator, *ptr, std::forward<Callable_>(target));
+ }
}
static Result invoke(void* bits, Args... args) {
auto& target = **static_cast<Callable**>(bits);
@@ -189,19 +191,33 @@ struct target<Callable,
}
static void destroy(void* bits) {
auto ptr = static_cast<Callable**>(bits);
- delete *ptr;
+ if (*ptr) {
+ CallableAllocator allocator;
+ CallableAllocatorTraits::destroy(allocator, *ptr);
+ CallableAllocatorTraits::deallocate(allocator, *ptr, 1u);
+ *ptr = nullptr;
+ }
}
static const target_ops<Result, Args...> ops;
+
+ private:
+ using AllocatorTraits = std::allocator_traits<Allocator>;
+ using CallableAllocator = typename AllocatorTraits::template rebind_alloc<Callable>;
+ using CallableAllocatorTraits = std::allocator_traits<CallableAllocator>;
+
+ static_assert(CallableAllocatorTraits::is_always_equal::value,
+ "Objects of type Allocator must always be equal to each other: an Allocator object "
+ "must be able to deallocate the memory allocated by a different Allocator object.");
};
inline void* heap_target_get(void* bits) { return *static_cast<void**>(bits); }
-template <typename Callable, typename Result, typename... Args>
-constexpr target_ops<Result, Args...> target<Callable,
- /*is_inline=*/false,
- /*is_shared=*/false, Result, Args...>::ops = {
- &unshared_target_type_id, &heap_target_get, &target::move, &target::destroy, &target::invoke};
+template <typename Callable, typename Allocator, typename Result, typename... Args>
+constexpr target_ops<Result, Args...>
+ target<Callable, /*is_inline=*/false, /*is_shared=*/false, Allocator, Result, Args...>::ops = {
+ &unshared_target_type_id, &heap_target_get, &target::move, &target::destroy,
+ &target::invoke};
// vtable for fit::function std::shared_ptr to target function
@@ -212,13 +228,12 @@ const void* get_target_type_id(const SharedFunction& function_or_callback) {
// For this vtable,
// Callable by definition will be either a fit::function or fit::callback
-template <typename SharedFunction, typename Result, typename... Args>
-struct target<SharedFunction,
- /*is_inline=*/false, /*is_shared=*/true, Result, Args...>
+template <typename SharedFunction, typename Allocator, typename Result, typename... Args>
+struct target<SharedFunction, /*is_inline=*/false, /*is_shared=*/true, Allocator, Result, Args...>
final {
static void initialize(void* bits, SharedFunction target) {
new (bits) std::shared_ptr<SharedFunction>(
- std::move(std::make_shared<SharedFunction>(std::move(target))));
+ std::move(std::allocate_shared<SharedFunction, Allocator>(Allocator(), std::move(target))));
}
static void copy_shared_ptr(void* from_bits, void* to_bits) {
auto& from_shared_ptr = *static_cast<std::shared_ptr<SharedFunction>*>(from_bits);
@@ -246,10 +261,9 @@ struct target<SharedFunction,
static const target_ops<Result, Args...> ops;
};
-template <typename SharedFunction, typename Result, typename... Args>
-constexpr target_ops<Result, Args...> target<SharedFunction,
- /*is_inline=*/false,
- /*is_shared=*/true, Result, Args...>::ops = {
+template <typename SharedFunction, typename Allocator, typename Result, typename... Args>
+constexpr target_ops<Result, Args...> target<
+ SharedFunction, /*is_inline=*/false, /*is_shared=*/true, Allocator, Result, Args...>::ops = {
&target::target_type_id, &target::get, &target::move, &target::destroy, &target::invoke};
// Calculates the alignment to use for a function of the provided
@@ -388,13 +402,14 @@ class alignas(FunctionAlignment(inline_target_size)) generic_function_base {
const base_target_ops* ops_;
};
-template <size_t inline_target_size, bool require_inline, typename FunctionType>
+template <size_t inline_target_size, bool require_inline, typename FunctionType, typename Allocator>
class function_base;
// Function implementation details that require the function signature.
// See |fit::function| and |fit::callback| documentation for more information.
-template <size_t inline_target_size, bool require_inline, typename Result, typename... Args>
-class function_base<inline_target_size, require_inline, Result(Args...)>
+template <size_t inline_target_size, bool require_inline, typename Allocator, typename Result,
+ typename... Args>
+class function_base<inline_target_size, require_inline, Result(Args...), Allocator>
: public generic_function_base<inline_target_size> {
using base = generic_function_base<inline_target_size>;
@@ -407,11 +422,10 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
template <typename Callable>
using target_type = target<Callable, (sizeof(Callable) <= inline_target_size),
- /*is_shared=*/false, Result, Args...>;
+ /*is_shared=*/false, Allocator, Result, Args...>;
template <typename SharedFunction>
- using shared_target_type = target<SharedFunction,
- /*is_inline=*/false,
- /*is_shared=*/true, Result, Args...>;
+ using shared_target_type =
+ target<SharedFunction, /*is_inline=*/false, /*is_shared=*/true, Allocator, Result, Args...>;
using ops_type = const target_ops<Result, Args...>*;
@@ -495,7 +509,7 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
// Note that fit::callback will release the target immediately after
// invoke() (also affecting any share()d copies).
// Aborts if the function's target is empty.
- // TODO(b/241567321): Remove "no sanitize" after pw_protobuf is fixed.
+ // TODO: b/241567321 - Remove "no sanitize" after pw_protobuf is fixed.
Result invoke(Args... args) const PW_NO_SANITIZE("function") {
// Down cast the ops to the derived type that this function was instantiated
// with, which includes the invoke function.
@@ -564,4 +578,4 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
} // namespace internal
} // namespace fit
-#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_FUNCTION_H_
+#endif // LIB_FIT_INTERNAL_FUNCTION_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h
index 18a0c8751..fe90c10d3 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_RESULT_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_RESULT_H_
+#ifndef LIB_FIT_INTERNAL_RESULT_H_
+#define LIB_FIT_INTERNAL_RESULT_H_
#include <lib/fit/internal/compiler.h>
#include <lib/stdcompat/type_traits.h>
@@ -258,12 +258,12 @@ struct storage_type<storage_class_e::non_trivial, E, T> {
return *this;
}
- constexpr storage_type(storage_type&& other) noexcept(
- std::is_nothrow_move_constructible<E>::value&& std::is_nothrow_move_constructible<T>::value) {
+ constexpr storage_type(storage_type&& other) noexcept(std::is_nothrow_move_constructible_v<E> &&
+ std::is_nothrow_move_constructible_v<T>) {
move_from(std::move(other));
}
constexpr storage_type& operator=(storage_type&& other) noexcept(
- std::is_nothrow_move_assignable<E>::value&& std::is_nothrow_move_assignable<T>::value) {
+ std::is_nothrow_move_assignable_v<E> && std::is_nothrow_move_assignable_v<T>) {
destroy();
move_from(std::move(other));
return *this;
@@ -375,12 +375,11 @@ struct storage_type<storage_class_e::non_trivial, E> {
return *this;
}
- constexpr storage_type(storage_type&& other) noexcept(
- std::is_nothrow_move_constructible<E>::value) {
+ constexpr storage_type(storage_type&& other) noexcept(std::is_nothrow_move_constructible_v<E>) {
move_from(std::move(other));
}
constexpr storage_type& operator=(storage_type&& other) noexcept(
- std::is_nothrow_move_assignable<E>::value) {
+ std::is_nothrow_move_assignable_v<E>) {
destroy();
move_from(std::move(other));
return *this;
@@ -442,4 +441,4 @@ using storage = storage_type<storage_class_trait<E, Ts...>, E, Ts...>;
} // namespace internal
} // namespace fit
-#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_RESULT_H_
+#endif // LIB_FIT_INTERNAL_RESULT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h
index 2d1f8a03a..14f887725 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_UTILITY_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_UTILITY_H_
+#ifndef LIB_FIT_INTERNAL_UTILITY_H_
+#define LIB_FIT_INTERNAL_UTILITY_H_
#include <lib/stdcompat/type_traits.h>
@@ -135,4 +135,4 @@ struct is_nothrow_swappable<T,
} // namespace internal
} // namespace fit
-#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_UTILITY_H_
+#endif // LIB_FIT_INTERNAL_UTILITY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
index d6ba9b7ff..56892693b 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
@@ -41,8 +41,8 @@ constexpr inline bool is_null(T&&) {
// with nullptr.
template <typename T>
struct is_nullable
- : public std::integral_constant<bool, std::is_constructible<T, decltype(nullptr)>::value &&
- std::is_assignable<T&, decltype(nullptr)>::value &&
+ : public std::integral_constant<bool, std::is_constructible_v<T, decltype(nullptr)> &&
+ std::is_assignable_v<T&, decltype(nullptr)> &&
is_comparable_with_null<T>::value> {};
template <>
struct is_nullable<void> : public std::false_type {};
@@ -62,8 +62,8 @@ struct is_nullable<void> : public std::false_type {};
// TODO(fxbug.dev/4681): fit::nullable does not precisely mirror
// cpp17::optional. This should be corrected to avoid surprises when switching
// between the types.
-template <typename T, bool = (is_nullable<T>::value && std::is_constructible<T, T&&>::value &&
- std::is_assignable<T&, T&&>::value)>
+template <typename T, bool = (is_nullable<T>::value && std::is_constructible_v<T, T&&> &&
+ std::is_assignable_v<T&, T&&>)>
class nullable final {
public:
using value_type = T;
@@ -131,30 +131,26 @@ class nullable<T, true> final {
constexpr T& value() & {
if (has_value()) {
return value_;
- } else {
- PW_ASSERT(false);
}
+ PW_ASSERT(false);
}
constexpr const T& value() const& {
if (has_value()) {
return value_;
- } else {
- PW_ASSERT(false);
}
+ PW_ASSERT(false);
}
constexpr T&& value() && {
if (has_value()) {
return std::move(value_);
- } else {
- PW_ASSERT(false);
}
+ PW_ASSERT(false);
}
constexpr const T&& value() const&& {
if (has_value()) {
return std::move(value_);
- } else {
- PW_ASSERT(false);
}
+ PW_ASSERT(false);
}
template <typename U = T>
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
index a7f77cac9..86bbb9041 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_RESULT_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_RESULT_H_
+#ifndef LIB_FIT_RESULT_H_
+#define LIB_FIT_RESULT_H_
#include <lib/fit/internal/compiler.h>
#include <lib/fit/internal/result.h>
@@ -192,7 +192,7 @@ class success<> {
#if __cplusplus >= 201703L
// Deduction guides to simplify zero and single argument success expressions in C++17.
-success()->success<>;
+success() -> success<>;
template <typename T>
success(T) -> success<T>;
@@ -226,9 +226,15 @@ constexpr success<> ok() { return success<>{}; }
template <typename E, typename... Ts>
class result;
+// This suppresses the '-Wctad-maybe-unsupported' compiler warning when CTAD is used.
+//
+// See https://github.com/llvm/llvm-project/blob/42874f6/libcxx/include/__config#L1259-L1261.
+template <class... Tag>
+result(typename Tag::__allow_ctad...) -> result<Tag...>;
+
// Specialization of result for one value type.
template <typename E, typename T>
-class LIB_FIT_NODISCARD result<E, T> {
+class [[nodiscard]] result<E, T> {
static_assert(!::fit::internal::is_success_v<E>,
"fit::success may not be used as the error type of fit::result!");
static_assert(!cpp17::is_same_v<failed, std::decay_t<T>>,
@@ -473,7 +479,7 @@ class LIB_FIT_NODISCARD result<E, T> {
// Specialization of the result type for zero values.
template <typename E>
-class LIB_FIT_NODISCARD result<E> {
+class [[nodiscard]] result<E> {
static_assert(!::fit::internal::is_success_v<E>,
"fit::success may not be used as the error type of fit::result!");
@@ -797,4 +803,4 @@ constexpr bool operator>=(const T& lhs, const result<F, U>& rhs) {
} // namespace fit
-#endif // LIB_FIT_INCLUDE_LIB_FIT_RESULT_H_
+#endif // LIB_FIT_RESULT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h
index 916a47507..f2c8c61f0 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_FIT_INCLUDE_LIB_FIT_TRAITS_H_
-#define LIB_FIT_INCLUDE_LIB_FIT_TRAITS_H_
+#ifndef LIB_FIT_TRAITS_H_
+#define LIB_FIT_TRAITS_H_
#include <lib/stdcompat/type_traits.h>
@@ -178,4 +178,4 @@ constexpr bool is_detected_convertible_v = is_detected_convertible<To, Op, Args.
} // namespace fit
-#endif // LIB_FIT_INCLUDE_LIB_FIT_TRAITS_H_
+#endif // LIB_FIT_TRAITS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc b/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
index c2a220002..34304db9d 100644
--- a/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
+++ b/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
@@ -6,11 +6,20 @@
#include <lib/stdcompat/bit.h>
#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <type_traits>
#include "gtest/gtest.h"
+#include "pw_polyfill/language_feature_macros.h"
+
namespace {
+using ::std::size_t;
+
using Closure = void();
using ClosureWrongReturnType = int();
using BinaryOp = int(int a, int b);
@@ -50,6 +59,12 @@ struct EmptyFunction<R(Args...)> {
(Args...) = nullptr;
};
+struct alignas(4 * alignof(void*)) LargeAlignedCallable {
+ void operator()() { calls += 1; }
+
+ int8_t calls = 0;
+};
+
// An object whose state we can examine from the outside.
struct SlotMachine {
void operator()() { value++; }
@@ -88,6 +103,92 @@ class DestructionObserver {
int* counter_;
};
+// Simple Allocator that only allows 1 object to be allocated/deallocated per call. All template
+// instantiations share the same resources.
+template <typename T>
+class SingleObjectAllocator;
+
+// Shared resources for SingleObjectAllocator template instantiations.
+class SingleObjectAllocatorResources final {
+ private:
+ template <typename T>
+ friend class SingleObjectAllocator;
+
+ // Tuned to current tests. May need to be changed if test requirements change.
+ static constexpr size_t kObjectSize{HugeCallableSize};
+ static constexpr size_t kObjectAlignment{alignof(LargeAlignedCallable)};
+ static constexpr size_t kMaxNumObjects{2u};
+
+ struct Obj {
+ alignas(kObjectAlignment) std::byte obj[kObjectSize];
+ };
+
+ inline static std::array<Obj, kMaxNumObjects> heap_;
+ // Store `bool`s separately instead of in `Obj`, so there doesn't end up being a large amount of
+ // padding from the aligned `obj` arrays in the `Obj` structs.
+ inline static std::array<bool, kMaxNumObjects> in_use_{};
+};
+
+template <typename T>
+class SingleObjectAllocator final {
+ public:
+ using value_type = T;
+ using pointer = value_type*;
+ using size_type = size_t;
+
+ struct is_always_equal : public std::true_type {};
+
+ constexpr SingleObjectAllocator() = default;
+ template <typename U>
+ constexpr SingleObjectAllocator(const SingleObjectAllocator<U>&) {}
+ template <typename U>
+ constexpr SingleObjectAllocator(SingleObjectAllocator<U>&&) {}
+
+ pointer allocate(size_type n) {
+ if (n != 1u) {
+ return nullptr;
+ }
+ for (size_t index{0u}; index < in_use_.size(); ++index) {
+ if (!in_use_.at(index)) {
+ in_use_.at(index) = true;
+ return reinterpret_cast<pointer>(&heap_.at(index).obj);
+ }
+ }
+ return nullptr;
+ }
+
+ void deallocate(pointer p, size_type n) {
+ ASSERT_EQ(n, 1u);
+ for (size_t index{0u}; index < heap_.size(); ++index) {
+ if (reinterpret_cast<pointer>(&heap_.at(index).obj) == p) {
+ ASSERT_TRUE(in_use_.at(index));
+ in_use_.at(index) = false;
+ return;
+ }
+ }
+ FAIL();
+ }
+
+ constexpr size_type max_size() const { return 1u; }
+
+ template <typename U>
+ constexpr bool operator==(const SingleObjectAllocator<U>&) const {
+ return true;
+ }
+ template <typename U>
+ constexpr bool operator!=(const SingleObjectAllocator<U>&) const {
+ return false;
+ }
+
+ private:
+ // Shared resources.
+ static_assert(sizeof(value_type) <= SingleObjectAllocatorResources::kObjectSize);
+ static_assert(alignof(value_type) <= SingleObjectAllocatorResources::kObjectAlignment);
+ inline static auto& heap_ = SingleObjectAllocatorResources::heap_;
+ inline static auto& in_use_ = SingleObjectAllocatorResources::in_use_;
+ static_assert(heap_.size() == in_use_.size());
+};
+
template <typename ClosureFunction>
void closure() {
static_assert(fit::is_nullable<ClosureFunction>::value, "");
@@ -676,6 +777,44 @@ TEST(FunctionTests, sharing) {
#endif
}
+TEST(FunctionTests, sharing_with_custom_allocator) {
+ int fheapvalue = 1;
+ int fheapdestroy = 0;
+ fit::function<Closure, fit::default_inline_target_size, SingleObjectAllocator<std::byte>> fheap =
+ [&fheapvalue, big = Big(), d = DestructionObserver(&fheapdestroy)] { fheapvalue++; };
+ fit::function<Closure, fit::default_inline_target_size, SingleObjectAllocator<std::byte>>
+ fheapshare1 = fheap.share();
+ fit::function<Closure, fit::default_inline_target_size, SingleObjectAllocator<std::byte>>
+ fheapshare2 = fheap.share();
+ fit::function<Closure, fit::default_inline_target_size, SingleObjectAllocator<std::byte>>
+ fheapshare3 = fheapshare1.share();
+ EXPECT_TRUE(!!fheap);
+ EXPECT_TRUE(!!fheapshare1);
+ EXPECT_TRUE(!!fheapshare2);
+ EXPECT_TRUE(!!fheapshare3);
+ fheap();
+ EXPECT_EQ(2, fheapvalue);
+ fheapshare1();
+ EXPECT_EQ(3, fheapvalue);
+ fheapshare2();
+ EXPECT_EQ(4, fheapvalue);
+ fheapshare3();
+ EXPECT_EQ(5, fheapvalue);
+ fheapshare2();
+ EXPECT_EQ(6, fheapvalue);
+ fheap();
+ EXPECT_EQ(7, fheapvalue);
+ EXPECT_EQ(0, fheapdestroy);
+ fheap = nullptr;
+ EXPECT_EQ(0, fheapdestroy);
+ fheapshare3 = nullptr;
+ EXPECT_EQ(0, fheapdestroy);
+ fheapshare2 = nullptr;
+ EXPECT_EQ(0, fheapdestroy);
+ fheapshare1 = nullptr;
+ EXPECT_EQ(1, fheapdestroy);
+}
+
struct Obj {
void Call() { calls++; }
@@ -874,18 +1013,25 @@ TEST(FunctionTests, callback_once) {
#endif
}
-#if defined(__cpp_constinit)
-#define CONSTINIT constinit
-#elif defined(__clang__)
-#define CONSTINIT [[clang::require_constant_initialization]]
-#else
-#define CONSTINIT
-#endif // __cpp_constinit
+TEST(FunctionTests, callback_with_custom_allocator) {
+ int cbheapvalue = 1;
+ int cbheapdestroy = 0;
+ fit::callback<Closure, fit::default_inline_target_size, SingleObjectAllocator<std::byte>> cbheap =
+ [&cbheapvalue, big = Big(), d = DestructionObserver(&cbheapdestroy)] { cbheapvalue++; };
-CONSTINIT const fit::function<void()> kDefaultConstructed;
-CONSTINIT const fit::function<void()> kNullptrConstructed(nullptr);
+ EXPECT_TRUE(!!cbheap);
+ EXPECT_FALSE(cbheap == nullptr);
+ EXPECT_EQ(1, cbheapvalue);
+ EXPECT_EQ(0, cbheapdestroy);
+ cbheap();
+ EXPECT_FALSE(!!cbheap);
+ EXPECT_TRUE(cbheap == nullptr);
+ EXPECT_EQ(2, cbheapvalue);
+ EXPECT_EQ(1, cbheapdestroy);
+}
-#undef CONSTINIT
+PW_CONSTINIT const fit::function<void()> kDefaultConstructed;
+PW_CONSTINIT const fit::function<void()> kNullptrConstructed(nullptr);
TEST(FunctionTests, null_constructors_are_constexpr) {
EXPECT_EQ(kDefaultConstructed, nullptr);
@@ -893,17 +1039,27 @@ TEST(FunctionTests, null_constructors_are_constexpr) {
}
TEST(FunctionTests, function_with_callable_aligned_larger_than_inline_size) {
- struct alignas(4 * alignof(void*)) LargeAlignedCallable {
- void operator()() { calls += 1; }
+ static_assert(sizeof(LargeAlignedCallable) > sizeof(void*), "Should not fit inline in function.");
- char calls = 0;
- };
+ fit::function<void(), sizeof(void*)> function = LargeAlignedCallable();
- static_assert(sizeof(LargeAlignedCallable) > sizeof(void*), "Should not fit inline in function");
+ static_assert(alignof(LargeAlignedCallable) > alignof(decltype(function)));
- fit::function<void(), sizeof(void*)> function = LargeAlignedCallable();
+ // Verify that the allocated target is aligned correctly.
+ LargeAlignedCallable* callable_ptr = function.target<LargeAlignedCallable>();
+ EXPECT_EQ(cpp20::bit_cast<uintptr_t>(callable_ptr) % alignof(LargeAlignedCallable), 0u);
+
+ function();
+ EXPECT_EQ(callable_ptr->calls, 1);
+}
- static_assert(alignof(LargeAlignedCallable) > alignof(decltype(function)), "");
+TEST(FunctionTests, function_with_callable_aligned_larger_than_inline_size_with_custom_allocator) {
+ static_assert(sizeof(LargeAlignedCallable) > sizeof(void*), "Should not fit inline in function.");
+
+ fit::function<void(), sizeof(void*), SingleObjectAllocator<std::byte>> function =
+ LargeAlignedCallable();
+
+ static_assert(alignof(LargeAlignedCallable) > alignof(decltype(function)));
// Verify that the allocated target is aligned correctly.
LargeAlignedCallable* callable_ptr = function.target<LargeAlignedCallable>();
@@ -1024,8 +1180,6 @@ template class assert_move_only<fit::callback<void()>>;
} // namespace test_copy_move_constructions
-} // namespace
-
namespace test_conversions {
static_assert(std::is_convertible<Closure, fit::function<Closure>>::value, "");
static_assert(std::is_convertible<BinaryOp, fit::function<BinaryOp>>::value, "");
@@ -1075,6 +1229,14 @@ TEST(FunctionTests, closure_fit_inline_function_Closure_HugeCallableSize) {
TEST(FunctionTests, binary_op_fit_inline_function_BinaryOp_HugeCallableSize) {
binary_op<fit::inline_function<BinaryOp, HugeCallableSize>>();
}
+TEST(FunctionTests, closure_fit_function_Closure_SingleObjectAllocator) {
+ closure<
+ fit::function<Closure, fit::default_inline_target_size, SingleObjectAllocator<std::byte>>>();
+}
+TEST(FunctionTests, binary_op_fit_function_BinaryOp_SingleObjectAllocator) {
+ binary_op<
+ fit::function<BinaryOp, fit::default_inline_target_size, SingleObjectAllocator<std::byte>>>();
+}
TEST(FunctionTests, bind_return_reference) {
struct TestClass {
@@ -1090,3 +1252,5 @@ TEST(FunctionTests, bind_return_reference) {
fit::function<int&()> func_deprecated = fit::bind_member(&instance, &TestClass::member);
EXPECT_EQ(&func_deprecated(), &instance.member_);
}
+
+} // namespace
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/BUILD.gn b/third_party/fuchsia/repo/sdk/lib/stdcompat/BUILD.gn
new file mode 100644
index 000000000..a17135a37
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/BUILD.gn
@@ -0,0 +1,46 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# This build file is defined by Pigweed, not Fuchsia. It only refers to sources
+# imported into the Pigweed repo.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("public_include_path") {
+ include_dirs = [ "include" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("stdcompat") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "include/lib/stdcompat/bit.h",
+ "include/lib/stdcompat/functional.h",
+ "include/lib/stdcompat/memory.h",
+ "include/lib/stdcompat/optional.h",
+ "include/lib/stdcompat/type_traits.h",
+ "include/lib/stdcompat/utility.h",
+ "include/lib/stdcompat/version.h",
+ ]
+ sources = [
+ "include/lib/stdcompat/internal/bit.h",
+ "include/lib/stdcompat/internal/constructors.h",
+ "include/lib/stdcompat/internal/exception.h",
+ "include/lib/stdcompat/internal/storage.h",
+ "include/lib/stdcompat/internal/type_traits.h",
+ "include/lib/stdcompat/internal/utility.h",
+ ]
+}
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h
index 1e9309417..d68a19878 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_BIT_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_BIT_H_
+#ifndef LIB_STDCOMPAT_BIT_H_
+#define LIB_STDCOMPAT_BIT_H_
#include <cstring>
#include <limits>
@@ -143,7 +143,7 @@ constexpr std::enable_if_t<std::is_unsigned<T>::value, bool> has_single_bit(T va
}
template <typename T>
-constexpr std::enable_if_t<std::is_unsigned<T>::value, T> bit_width(T value) {
+constexpr std::enable_if_t<std::is_unsigned<T>::value, int> bit_width(T value) {
return internal::bit_width(value);
}
@@ -182,4 +182,4 @@ enum class endian {
} // namespace cpp20
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_BIT_H_
+#endif // LIB_STDCOMPAT_BIT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h
index 34d44e0d5..e14122cbd 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_FUNCTIONAL_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_FUNCTIONAL_H_
+#ifndef LIB_STDCOMPAT_FUNCTIONAL_H_
+#define LIB_STDCOMPAT_FUNCTIONAL_H_
#include "internal/functional.h"
@@ -47,4 +47,4 @@ constexpr ::cpp20::internal::front_binder_t<F, Args...> bind_front(F&& f, Args&&
} // namespace cpp20
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_FUNCTIONAL_H_
+#endif // LIB_STDCOMPAT_FUNCTIONAL_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h
index 92b33a633..62f046a74 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_BIT_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_BIT_H_
+#ifndef LIB_STDCOMPAT_INTERNAL_BIT_H_
+#define LIB_STDCOMPAT_INTERNAL_BIT_H_
#include <limits>
#include <type_traits>
@@ -183,9 +183,9 @@ popcount(T value) noexcept {
}
template <typename T>
-constexpr std::enable_if_t<is_bit_type<T>::value, T> bit_width(T value) {
- const T zeros_left =
- (value == 0) ? std::numeric_limits<T>::digits : static_cast<T>(count_zeros_from_left(value));
+constexpr std::enable_if_t<is_bit_type<T>::value, int> bit_width(T value) {
+ const int zeros_left = (value == 0) ? std::numeric_limits<T>::digits
+ : static_cast<int>(count_zeros_from_left(value));
return std::numeric_limits<T>::digits - zeros_left;
}
@@ -209,7 +209,7 @@ bit_ceil(T value) {
template <typename T>
constexpr std::enable_if_t<is_bit_type<T>::value, T> bit_floor(T value) {
- return static_cast<T>(T(1) << (bit_width(value) - T(1)));
+ return value == 0 ? 0 : static_cast<T>(T(1) << (static_cast<T>(bit_width(value)) - T(1)));
}
template <unsigned little, unsigned big>
@@ -227,4 +227,4 @@ constexpr unsigned native_endianess() {
} // namespace internal
} // namespace cpp20
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_BIT_H_
+#endif // LIB_STDCOMPAT_INTERNAL_BIT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h
index e86f2f108..ba592ac7b 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
+#ifndef LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
+#define LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
#include <cstddef>
#include <type_traits>
@@ -97,4 +97,4 @@ struct modulate_copy_and_move
} // namespace internal
} // namespace cpp17
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
+#endif // LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h
index 74f4e8c08..f04a41201 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
+#ifndef LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
+#define LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
#include <exception>
@@ -53,4 +53,4 @@ inline constexpr void throw_or_abort_if_any(const char* reason, AbortIf... abort
} // namespace internal
} // namespace cpp17
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
+#endif // LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/functional.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/functional.h
new file mode 100644
index 000000000..94700d2ef
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/functional.h
@@ -0,0 +1,146 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INTERNAL_FUNCTIONAL_H_
+#define LIB_STDCOMPAT_INTERNAL_FUNCTIONAL_H_
+
+#include <cstddef>
+#include <functional>
+
+#include "../type_traits.h"
+#include "../utility.h"
+
+namespace cpp17 {
+namespace internal {
+
+// Definitions for the invoke functions in internal/type_traits.h.
+// These collectively implement INVOKE from [func.require] ¶ 1.
+template <typename MemFn, typename Class, typename T, typename... Args>
+constexpr auto invoke(MemFn Class::*f, T&& obj, Args&&... args)
+ -> std::enable_if_t<invoke_pmf_base<MemFn, Class, T>,
+ decltype((std::forward<T>(obj).*f)(std::forward<Args>(args)...))> {
+ return (std::forward<T>(obj).*f)(std::forward<Args>(args)...);
+}
+
+template <typename MemFn, typename Class, typename T, typename... Args>
+constexpr auto invoke(MemFn Class::*f, T&& obj, Args&&... args)
+ -> std::enable_if_t<invoke_pmf_refwrap<MemFn, Class, T>,
+ decltype((obj.get().*f)(std::forward<Args>(args)...))> {
+ return (obj.get().*f)(std::forward<Args>(args)...);
+}
+
+template <typename MemFn, typename Class, typename T, typename... Args>
+constexpr auto invoke(MemFn Class::*f, T&& obj, Args&&... args)
+ -> std::enable_if_t<invoke_pmf_other<MemFn, Class, T>,
+ decltype(((*std::forward<T>(obj)).*f)(std::forward<Args>(args)...))> {
+ return (*std::forward<T>(obj).*f)(std::forward<Args>(args)...);
+}
+
+template <typename MemObj, typename Class, typename T>
+constexpr auto invoke(MemObj Class::*f, T&& obj)
+ -> std::enable_if_t<invoke_pmd_base<MemObj, Class, T>, decltype(std::forward<T>(obj).*f)> {
+ return std::forward<T>(obj).*f;
+}
+
+template <typename MemObj, typename Class, typename T>
+constexpr auto invoke(MemObj Class::*f, T&& obj)
+ -> std::enable_if_t<invoke_pmd_refwrap<MemObj, Class, T>, decltype(obj.get().*f)> {
+ return obj.get().*f;
+}
+
+template <typename MemObj, typename Class, typename T>
+constexpr auto invoke(MemObj Class::*f, T&& obj)
+ -> std::enable_if_t<invoke_pmd_other<MemObj, Class, T>, decltype((*std::forward<T>(obj)).*f)> {
+ return (*std::forward<T>(obj)).*f;
+}
+
+template <typename F, typename... Args>
+constexpr auto invoke(F&& f, Args&&... args)
+ -> decltype(std::forward<F>(f)(std::forward<Args>(args)...)) {
+ return std::forward<F>(f)(std::forward<Args>(args)...);
+}
+
+} // namespace internal
+} // namespace cpp17
+
+namespace cpp20 {
+namespace internal {
+
+template <typename Invocable, typename BoundTuple, std::size_t... Is, typename... CallArgs>
+constexpr decltype(auto) invoke_with_bound(Invocable&& invocable, BoundTuple&& bound_args,
+ std::index_sequence<Is...>, CallArgs&&... call_args) {
+ return ::cpp17::internal::invoke(std::forward<Invocable>(invocable),
+ std::get<Is>(std::forward<BoundTuple>(bound_args))...,
+ std::forward<CallArgs>(call_args)...);
+}
+
+template <typename FD, typename... BoundArgs>
+class front_binder {
+ using bound_indices = std::index_sequence_for<BoundArgs...>;
+
+ public:
+ template <typename F, typename... Args>
+ explicit constexpr front_binder(cpp17::in_place_t, F&& f, Args&&... args) noexcept(
+ cpp17::conjunction_v<std::is_nothrow_constructible<FD, F>,
+ std::is_nothrow_constructible<BoundArgs, Args>...>)
+ : fd_(std::forward<F>(f)), bound_args_(std::forward<Args>(args)...) {
+ // [func.bind.front] ¶ 2
+ static_assert(cpp17::is_constructible_v<FD, F>,
+ "Must be able to construct decayed callable type.");
+ static_assert(cpp17::is_move_constructible_v<FD>, "Callable type must be move-constructible.");
+ static_assert(cpp17::conjunction_v<std::is_constructible<BoundArgs, Args>...>,
+ "Must be able to construct decayed bound argument types.");
+ static_assert(cpp17::conjunction_v<std::is_move_constructible<BoundArgs>...>,
+ "Bound argument types must be move-constructible.");
+ }
+
+ constexpr front_binder(const front_binder&) = default;
+ constexpr front_binder& operator=(const front_binder&) = default;
+ constexpr front_binder(front_binder&&) noexcept = default;
+ constexpr front_binder& operator=(front_binder&&) noexcept = default;
+
+ template <typename... CallArgs>
+ constexpr cpp17::invoke_result_t<FD&, BoundArgs&..., CallArgs&&...>
+ operator()(CallArgs&&... call_args) & noexcept(
+ cpp17::is_nothrow_invocable_v<FD&, BoundArgs&..., CallArgs&&...>) {
+ return invoke_with_bound(fd_, bound_args_, bound_indices(),
+ std::forward<CallArgs>(call_args)...);
+ }
+
+ template <typename... CallArgs>
+ constexpr cpp17::invoke_result_t<const FD&, const BoundArgs&..., CallArgs&&...>
+ operator()(CallArgs&&... call_args) const& noexcept(
+ cpp17::is_nothrow_invocable_v<const FD&, const BoundArgs&..., CallArgs&&...>) {
+ return invoke_with_bound(fd_, bound_args_, bound_indices(),
+ std::forward<CallArgs>(call_args)...);
+ }
+
+ template <typename... CallArgs>
+ constexpr cpp17::invoke_result_t<FD&&, BoundArgs&&..., CallArgs&&...>
+ operator()(CallArgs&&... call_args) && noexcept(
+ cpp17::is_nothrow_invocable_v<FD&&, BoundArgs&&..., CallArgs&&...>) {
+ return invoke_with_bound(std::move(fd_), std::move(bound_args_), bound_indices(),
+ std::forward<CallArgs>(call_args)...);
+ }
+
+ template <typename... CallArgs>
+ constexpr cpp17::invoke_result_t<const FD&&, const BoundArgs&&..., CallArgs&&...>
+ operator()(CallArgs&&... call_args) const&& noexcept(
+ cpp17::is_nothrow_invocable_v<const FD&&, const BoundArgs&&..., CallArgs&&...>) {
+ return invoke_with_bound(std::move(fd_), std::move(bound_args_), bound_indices(),
+ std::forward<CallArgs>(call_args)...);
+ }
+
+ private:
+ FD fd_;
+ std::tuple<BoundArgs...> bound_args_;
+};
+
+template <typename F, typename... BoundArgs>
+using front_binder_t = front_binder<std::decay_t<F>, std::decay_t<BoundArgs>...>;
+
+} // namespace internal
+} // namespace cpp20
+
+#endif // LIB_STDCOMPAT_INTERNAL_FUNCTIONAL_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h
index 16c64d559..25570cae5 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h
@@ -2,12 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_STORAGE_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_STORAGE_H_
+#ifndef LIB_STDCOMPAT_INTERNAL_STORAGE_H_
+#define LIB_STDCOMPAT_INTERNAL_STORAGE_H_
#include <cstddef>
#include <cstdint>
#include <limits>
+#include <new>
#include <type_traits>
#include "utility.h"
@@ -844,4 +845,4 @@ using storage_type = decltype(make_storage<Ts...>(std::index_sequence_for<Ts...>
} // namespace internal
} // namespace cpp17
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_STORAGE_H_
+#endif // LIB_STDCOMPAT_INTERNAL_STORAGE_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h
index dced28d9f..23b544b57 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h
@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
+#ifndef LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
+#define LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
+#include <functional>
#include <type_traits>
namespace cpp17 {
@@ -17,11 +18,12 @@ static constexpr bool is_reference_wrapper<std::reference_wrapper<T>> = true;
// These are from [func.require] ¶ 1.1-7
template <typename MemFn, typename Class, typename T>
-static constexpr bool invoke_pmf_base = std::is_member_function_pointer<MemFn Class::*>::value&&
- std::is_base_of<Class, std::remove_reference_t<T>>::value;
+static constexpr bool invoke_pmf_base = std::is_member_function_pointer<MemFn Class::*>::value &&
+ std::is_base_of<Class, std::remove_reference_t<T>>::value;
template <typename MemFn, typename Class, typename T>
-static constexpr bool invoke_pmf_refwrap = std::is_member_function_pointer<MemFn Class::*>::value&&
+static constexpr bool invoke_pmf_refwrap =
+ std::is_member_function_pointer<MemFn Class::*>::value &&
is_reference_wrapper<std::remove_cv_t<std::remove_reference_t<T>>>;
template <typename MemFn, typename Class, typename T>
@@ -30,11 +32,12 @@ static constexpr bool invoke_pmf_other =
!invoke_pmf_refwrap<MemFn, Class, T>;
template <typename MemObj, typename Class, typename T>
-static constexpr bool invoke_pmd_base = std::is_member_object_pointer<MemObj Class::*>::value&&
- std::is_base_of<Class, std::remove_reference_t<T>>::value;
+static constexpr bool invoke_pmd_base = std::is_member_object_pointer<MemObj Class::*>::value &&
+ std::is_base_of<Class, std::remove_reference_t<T>>::value;
template <typename MemObj, typename Class, typename T>
-static constexpr bool invoke_pmd_refwrap = std::is_member_object_pointer<MemObj Class::*>::value&&
+static constexpr bool invoke_pmd_refwrap =
+ std::is_member_object_pointer<MemObj Class::*>::value &&
is_reference_wrapper<std::remove_cv_t<std::remove_reference_t<T>>>;
template <typename MemObj, typename Class, typename T>
@@ -109,4 +112,4 @@ struct invoke_result<true, F, Args...> {
} // namespace internal
} // namespace cpp17
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
+#endif // LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h
index a5da381ae..edf7eaba1 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_UTILITY_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_UTILITY_H_
+#ifndef LIB_STDCOMPAT_INTERNAL_UTILITY_H_
+#define LIB_STDCOMPAT_INTERNAL_UTILITY_H_
#include <cstddef>
#include <utility>
@@ -134,4 +134,4 @@ struct is_nothrow_swappable<T, void_t<decltype(swap(std::declval<T&>(), std::dec
} // namespace internal
} // namespace cpp17
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_UTILITY_H_
+#endif // LIB_STDCOMPAT_INTERNAL_UTILITY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h
index 2dc3198a4..b2b172c1e 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_MEMORY_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_MEMORY_H_
+#ifndef LIB_STDCOMPAT_MEMORY_H_
+#define LIB_STDCOMPAT_MEMORY_H_
#include <memory>
@@ -63,4 +63,4 @@ constexpr typename std::pointer_traits<T>::element_type* to_address(const T& poi
} // namespace cpp20
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_MEMORY_H_
+#endif // LIB_STDCOMPAT_MEMORY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h
index bc6a421a0..d51b87e80 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_OPTIONAL_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_OPTIONAL_H_
+#ifndef LIB_STDCOMPAT_OPTIONAL_H_
+#define LIB_STDCOMPAT_OPTIONAL_H_
#include "utility.h"
#include "version.h"
@@ -472,4 +472,4 @@ constexpr bool operator>=(const T& lhs, const optional<U>& rhs) {
#endif
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_OPTIONAL_H_
+#endif // LIB_STDCOMPAT_OPTIONAL_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h
index 8f0d80ede..a40baf976 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h
@@ -2,12 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_TYPE_TRAITS_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_TYPE_TRAITS_H_
+#ifndef LIB_STDCOMPAT_TYPE_TRAITS_H_
+#define LIB_STDCOMPAT_TYPE_TRAITS_H_
#include <cstddef>
-#include <tuple>
#include <type_traits>
+#include <utility>
#include "internal/type_traits.h"
#include "version.h"
@@ -506,4 +506,4 @@ static constexpr bool is_scoped_enum_v = is_scoped_enum<T>::value;
} // namespace cpp23
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_TYPE_TRAITS_H_
+#endif // LIB_STDCOMPAT_TYPE_TRAITS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h
index fe0993d47..d056bb48e 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_UTILITY_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_UTILITY_H_
+#ifndef LIB_STDCOMPAT_UTILITY_H_
+#define LIB_STDCOMPAT_UTILITY_H_
#include <cstddef>
#include <type_traits>
@@ -115,4 +115,4 @@ constexpr T exchange(T& obj, U&& new_value) {
} // namespace cpp20
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_UTILITY_H_
+#endif // LIB_STDCOMPAT_UTILITY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h
index c022e4b30..ca436c367 100644
--- a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h
@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_VERSION_H_
-#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_VERSION_H_
+#ifndef LIB_STDCOMPAT_VERSION_H_
+#define LIB_STDCOMPAT_VERSION_H_
// This <version> polyfills is meant to provide the feature testing macros for the rest of
// the stdcompat library. It is not meant to be a full polyfill of <version>.
@@ -76,4 +76,4 @@
#endif // __has_include(<version>) && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
-#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_VERSION_H_
+#endif // LIB_STDCOMPAT_VERSION_H_
diff --git a/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/internal/storage.h b/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/internal/storage.h
new file mode 100644
index 000000000..42bb0a0dd
--- /dev/null
+++ b/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/internal/storage.h
@@ -0,0 +1,66 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_LAZY_INIT_INTERNAL_STORAGE_H_
+#define LIB_LAZY_INIT_INTERNAL_STORAGE_H_
+
+#include <lib/lazy_init/options.h>
+
+#include <type_traits>
+#include <utility>
+
+#include "assert.h"
+
+namespace lazy_init {
+namespace internal {
+
+// Empty type that is trivially constructible/destructible.
+struct Empty {};
+
+// Lazy-initialized storage type for trivially destructible value types.
+template <typename T, bool = std::is_trivially_destructible_v<T>>
+union LazyInitStorage {
+ constexpr LazyInitStorage() : empty{} {}
+
+ // Trivial destructor required so that the overall union is also trivially
+ // destructible.
+ ~LazyInitStorage() = default;
+
+ constexpr T& operator*() { return value; }
+ constexpr T* operator->() { return &value; }
+ constexpr T* GetStorageAddress() { return &value; }
+
+ constexpr const T& operator*() const { return value; }
+ constexpr const T* operator->() const { return &value; }
+ constexpr const T* GetStorageAddress() const { return &value; }
+
+ Empty empty;
+ T value;
+};
+
+// Lazy-initialized storage type for non-trivially destructible value types.
+template <typename T>
+union LazyInitStorage<T, false> {
+ constexpr LazyInitStorage() : empty{} {}
+
+ // Non-trivial destructor required when at least one variant is non-
+ // trivially destructible, making the overall union also non-trivially
+ // destructible.
+ ~LazyInitStorage() {}
+
+ constexpr T& operator*() { return value; }
+ constexpr T* operator->() { return &value; }
+ constexpr T* GetStorageAddress() { return &value; }
+
+ constexpr const T& operator*() const { return value; }
+ constexpr const T* operator->() const { return &value; }
+ constexpr const T* GetStorageAddress() const { return &value; }
+
+ Empty empty;
+ T value;
+};
+
+} // namespace internal
+} // namespace lazy_init
+#endif // LIB_LAZY_INIT_INTERNAL_STORAGE_H_
diff --git a/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/lazy_init.h b/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/lazy_init.h
new file mode 100644
index 000000000..6d0d9530a
--- /dev/null
+++ b/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/lazy_init.h
@@ -0,0 +1,287 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_LAZY_INIT_LAZY_INIT_H_
+#define LIB_LAZY_INIT_LAZY_INIT_H_
+
+#include <atomic>
+#include <cstddef>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#include "internal/storage.h"
+#include "options.h"
+
+namespace lazy_init {
+
+// Wrapper type for global variables that removes automatic constructor and
+// destructor generation and provides explicit control over initialization.
+// This avoids initialization order hazards between globals, as well as code
+// that runs before and after global constructors are invoked.
+//
+// This is the base type that specifies the default parameters that control
+// consistency checks and global destructor generation options.
+//
+// See lib/lazy_init/options.h for a description of CheckType and Destructor
+// option values.
+//
+// Note, T must be construtible by LazyInit, however, its constructor need not
+// be public if Access is declared as a friend. For example:
+//
+// class Foo {
+// friend lazy_init::Access;
+//
+// Foo(int arg);
+// };
+//
+template <typename T, CheckType = CheckType::Default, Destructor = Destructor::Default>
+class LazyInit;
+
+// Utility type to provide access to non-public constructors. Types with private
+// or protected constructors that need lazy initialization must friend Access.
+class Access {
+ template <typename, CheckType, Destructor>
+ friend class LazyInit;
+
+ // Constructs the given storage using the constructor matching T(Args...).
+ template <typename T, typename... Args>
+ static void Initialize(T* value, Args&&... args) {
+ new (value) T(std::forward<Args>(args)...);
+ }
+};
+
+// Specialization that does not provide consistency checks.
+template <typename T>
+class LazyInit<T, CheckType::None, Destructor::Disabled> {
+ public:
+ constexpr LazyInit() = default;
+ ~LazyInit() = default;
+
+ LazyInit(const LazyInit&) = delete;
+ LazyInit& operator=(const LazyInit&) = delete;
+ LazyInit(LazyInit&&) = delete;
+ LazyInit& operator=(LazyInit&&) = delete;
+
+ // Explicitly constructs the wrapped global. It is up to the caller to
+ // ensure that initialization is not already performed.
+ // Returns a reference to the newly constructed global.
+ template <typename... Args>
+ T& Initialize(Args&&... args) {
+ static_assert(alignof(LazyInit) >= alignof(T));
+ Access::Initialize(&storage_.value, std::forward<Args>(args)...);
+ return *storage_;
+ }
+
+ // Returns a reference to the wrapped global. It is up to the caller to
+ // ensure that initialization is already performed and that the effects of
+ // initialization are visible.
+ T& Get() { return *storage_; }
+ const T& Get() const { return *storage_; }
+
+ // Accesses the wrapped global by pointer. It is up to the caller to ensure
+ // that initialization is already performed and that the effects of
+ // initialization are visible.
+ T* operator->() { return &Get(); }
+ const T* operator->() const { return &Get(); }
+
+ // Returns a pointer to the wrapped global. All specializations return a
+ // pointer without performing consistency checks. This should be used
+ // cautiously, preferably only in constant expressions that take the address
+ // of the wrapped global.
+ constexpr T* GetAddressUnchecked() { return storage_.GetStorageAddress(); }
+ constexpr const T* GetAddressUnchecked() const { return storage_.GetStorageAddress(); }
+
+ private:
+ template <typename, CheckType, Destructor>
+ friend class LazyInit;
+
+ // Explicitly destroys the wrapped global. Called by the specialization of
+ // LazyInit with the destructor enabled.
+ void Destruct() { storage_->T::~T(); }
+
+ // Lazy-initialized storage for a value of type T.
+ internal::LazyInitStorage<T> storage_;
+};
+
+// Specialization that provides basic consistency checks. It is up to the caller
+// to ensure proper synchronization and barriers.
+template <typename T>
+class LazyInit<T, CheckType::Basic, Destructor::Disabled> {
+ public:
+ constexpr LazyInit() = default;
+ ~LazyInit() = default;
+
+ LazyInit(const LazyInit&) = delete;
+ LazyInit& operator=(const LazyInit&) = delete;
+ LazyInit(LazyInit&&) = delete;
+ LazyInit& operator=(LazyInit&&) = delete;
+
+ // Explicitly constructs the wrapped global. Asserts that initialization is
+ // not already performed.
+ // Returns a reference to the newly constructed global.
+ template <typename... Args>
+ T& Initialize(Args&&... args) {
+ static_assert(alignof(LazyInit) >= alignof(T));
+
+ internal::Assert(!initialized_);
+ initialized_ = true;
+ Access::Initialize(&storage_.value, std::forward<Args>(args)...);
+ return *storage_;
+ }
+
+ // Returns a reference to the wrapped global. Asserts that initialization
+ // is already performed, however, it is up to the caller to ensure that the
+ // effects of initialization are visible.
+ T& Get() {
+ internal::Assert(initialized_);
+ return *storage_;
+ }
+
+ const T& Get() const {
+ internal::Assert(initialized_);
+ return *storage_;
+ }
+
+ // Accesses the wrapped global by pointer. Asserts that initialization is
+ // already performed, however, it is up the caller to ensure that the
+ // effects of initialization are visible.
+ T* operator->() { return &Get(); }
+ const T* operator->() const { return &Get(); }
+
+ // Returns a pointer to the wrapped global. All specializations return a
+ // pointer without performing consistency checks. This should be used
+ // cautiously, preferably only in constant expressions that take the address
+ // of the wrapped global.
+ constexpr T* GetAddressUnchecked() { return storage_.GetStorageAddress(); }
+ constexpr const T* GetAddressUnchecked() const { return storage_.GetStorageAddress(); }
+
+ private:
+ template <typename, CheckType, Destructor>
+ friend class LazyInit;
+
+ // Explicitly destroys the wrapped global. Called by the specialization of
+ // LazyInit with the destructor enabled.
+ void Destruct() {
+ internal::Assert(initialized_);
+ storage_->T::~T();
+
+ // Prevent the compiler from omitting this write during destruction.
+ static_cast<volatile bool&>(initialized_) = false;
+ }
+
+ // Lazy-initialized storage for a value of type T.
+ internal::LazyInitStorage<T> storage_;
+
+ // Guard variable to check for multiple initializations and access before
+ // initialization.
+ bool initialized_{};
+};
+
+// Specialization that provides atomic consistency checks. Checks are guaranteed
+// to be consistent under races over initialization.
+template <typename T>
+class LazyInit<T, CheckType::Atomic, Destructor::Disabled> {
+ public:
+ constexpr LazyInit() = default;
+ ~LazyInit() = default;
+
+ LazyInit(const LazyInit&) = delete;
+ LazyInit& operator=(const LazyInit&) = delete;
+ LazyInit(LazyInit&&) = delete;
+ LazyInit& operator=(LazyInit&&) = delete;
+
+ // Explicitly constructs the wrapped global. Asserts that initialization is
+ // not already performed.
+ // Returns a reference to the newly constructed global.
+ template <typename... Args>
+ T& Initialize(Args&&... args) {
+ static_assert(alignof(LazyInit) >= alignof(T));
+
+ TransitionState(State::Uninitialized, State::Constructing);
+ Access::Initialize(&storage_.value, std::forward<Args>(args)...);
+ state_.store(State::Initialized, std::memory_order_release);
+
+ return *storage_;
+ }
+
+ // Returns a reference to the wrapped global. Asserts that initialization
+ // is already performed. The effects of initialization are guaranteed to be
+ // visible if the assertion passes.
+ T& Get() {
+ internal::Assert(State::Initialized == state_.load(std::memory_order_relaxed));
+ return *storage_;
+ }
+
+ const T& Get() const {
+ internal::Assert(State::Initialized == state_.load(std::memory_order_relaxed));
+ return *storage_;
+ }
+
+ // Accesses the wrapped global by pointer. Asserts that initialization is
+ // already performed. The effects of initialization are guaranteed to be
+ // visible if the assertion passes.
+ T* operator->() { return &Get(); }
+ const T* operator->() const { return &Get(); }
+
+ // Returns a pointer to the wrapped global. All specializations return a
+ // pointer without performing consistency checks. This should be used
+ // cautiously, preferably only in constant expressions that take the address
+ // of the wrapped global.
+ constexpr T* GetAddressUnchecked() { return storage_.GetStorageAddress(); }
+ constexpr const T* GetAddressUnchecked() const { return storage_.GetStorageAddress(); }
+
+ private:
+ template <typename, CheckType, Destructor>
+ friend class LazyInit;
+
+ // Enum representing the states of initialization.
+ enum class State : int {
+ Uninitialized = 0,
+ Constructing,
+ Initialized,
+ Destructing,
+ Destroyed,
+ };
+
+ // Transitions the guard from the |expected| state to the |target| state.
+ // Asserts that the expected state matches the actual state.
+ void TransitionState(State expected, State target) {
+ State actual = state_.load(std::memory_order_relaxed);
+ internal::Assert(expected == actual);
+ while (!state_.compare_exchange_weak(actual, target, std::memory_order_acquire,
+ std::memory_order_relaxed)) {
+ internal::Assert(expected == actual);
+ }
+ }
+
+ // Explicitly destroys the wrapped global. Called by the specialization of
+ // LazyInit with the destructor enabled.
+ void Destruct() {
+ TransitionState(State::Initialized, State::Destructing);
+ storage_->T::~T();
+ state_.store(State::Destroyed, std::memory_order_release);
+ }
+
+ // Lazy-initialized storage for a value of type T.
+ internal::LazyInitStorage<T> storage_;
+
+ // Guard variable to check for multiple initializations and access before
+ // initialization.
+ std::atomic<State> state_{};
+};
+
+// Specialization that includes a global destructor. This type is based on the
+// equivalent specialization without a global destructor and simply calls the
+// base type Destruct() method.
+template <typename T, CheckType Check>
+class LazyInit<T, Check, Destructor::Enabled> : public LazyInit<T, Check, Destructor::Disabled> {
+ public:
+ using LazyInit<T, Check, Destructor::Disabled>::LazyInit;
+ ~LazyInit() { this->Destruct(); }
+};
+
+} // namespace lazy_init
+
+#endif // LIB_LAZY_INIT_LAZY_INIT_H_
diff --git a/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/options.h b/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/options.h
new file mode 100644
index 000000000..c044a406c
--- /dev/null
+++ b/third_party/fuchsia/repo/zircon/system/ulib/lazy_init/include/lib/lazy_init/options.h
@@ -0,0 +1,50 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_LAZY_INIT_OPTIONS_H_
+#define LIB_LAZY_INIT_OPTIONS_H_
+
+namespace lazy_init {
+
+// Enum that specifies what kind of debug init checks to perform for a
+// lazy-initialized global variable.
+enum class CheckType {
+ // No checks are performed.
+ None,
+
+ // Initialization checks are performed. If multiple threads will access the
+ // global variable, initialization must be manually serialized with respect
+ // to the guard variable.
+ Basic,
+
+ // Initialization checks are performed using atomic operations. Checks are
+ // guaranteed to be consistent, even when races occur over initialization.
+ Atomic,
+
+ // The default check type as specified by the build. This is the check type
+ // used when not explicitly specified. It may also be specified explicitly
+ // to defer to the build configuration when setting other options.
+ // TODO(eieio): Add the build arg and conditional logic.
+ Default = None,
+};
+
+// Enum that specifies whether to enable a lazy-initialized global variable's
+// destructor. Disabling global destructors avoids destructor registration.
+// However, destructors can be conditionally enabled on builds that require
+// them, such as ASAN.
+enum class Destructor {
+ Disabled,
+ Enabled,
+
+ // The default destructor enablement as specified by the build. This is the
+ // enablement used when not explicitly specified. It may also be specified
+ // explicitly to defer to the build configuration when setting other
+ // options.
+ // TODO(eieio): Add the build arg and conditional logic.
+ Default = Disabled,
+};
+
+} // namespace lazy_init
+
+#endif // LIB_LAZY_INIT_OPTIONS_H_
diff --git a/third_party/fuzztest/BUILD.gn b/third_party/fuzztest/BUILD.gn
new file mode 100644
index 000000000..3581480c9
--- /dev/null
+++ b/third_party/fuzztest/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+
+if (dir_pw_third_party_fuzztest != "") {
+ config("fuzztest_public_config1") {
+ include_dirs = [ "$dir_pw_third_party_fuzztest" ]
+ }
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/fuzztest/CMakeLists.txt b/third_party/fuzztest/CMakeLists.txt
new file mode 100644
index 000000000..0231698d5
--- /dev/null
+++ b/third_party/fuzztest/CMakeLists.txt
@@ -0,0 +1,52 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+if(NOT dir_pw_third_party_fuzztest)
+ pw_add_error_target(pw_third_party.fuzztest
+ MESSAGE
+ "Attempted to build the pw_third_party.fuzztest without configuring it "
+ "via dir_pw_third_party_fuzztest. "
+ "See https://pigweed.dev/third_party/fuzztest."
+ )
+ pw_add_error_target(pw_third_party.fuzztest_gtest_main
+ MESSAGE
+ "Attempted to build the pw_third_party.fuzztest_gtest_main without "
+ "configuring it via dir_pw_third_party_fuzztest. "
+ "See https://pigweed.dev/third_party/fuzztest."
+ )
+ return()
+else()
+ add_subdirectory("${dir_pw_third_party_fuzztest}" third_party/fuzztest)
+endif()
+
+# See also //pw_fuzzer:fuzztest.
+add_library(pw_third_party.fuzztest INTERFACE)
+target_link_libraries(pw_third_party.fuzztest
+ INTERFACE
+ fuzztest_fuzztest
+ pw_third_party.googletest
+)
+target_include_directories(pw_third_party.fuzztest
+ INTERFACE
+ "${dir_pw_third_party_fuzztest}"
+)
+
+# See also //target/host/pw_add_test_executable.cmake.
+add_library(pw_third_party.fuzztest_gtest_main INTERFACE)
+target_link_libraries(pw_third_party.fuzztest_gtest_main
+ INTERFACE
+ fuzztest_gtest_main
+)
diff --git a/third_party/fuzztest/OWNERS b/third_party/fuzztest/OWNERS
new file mode 100644
index 000000000..419cfd37a
--- /dev/null
+++ b/third_party/fuzztest/OWNERS
@@ -0,0 +1 @@
+aarongreen@google.com
diff --git a/third_party/fuzztest/centipede/BUILD.gn b/third_party/fuzztest/centipede/BUILD.gn
new file mode 100644
index 000000000..3c5fa71fd
--- /dev/null
+++ b/third_party/fuzztest/centipede/BUILD.gn
@@ -0,0 +1,1386 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for centipede.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/error.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+
+config("centipede_config1") {
+ ldflags = [ "-lrt" ]
+}
+
+config("centipede_config2") {
+ cflags = [ "-fsanitize-coverage=0" ]
+ ldflags = [
+ "-ldl",
+ "-lpthread",
+ ]
+}
+
+# Generated from //centipede:analyze_corpora
+pw_source_set("analyze_corpora") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/analyze_corpora.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/analyze_corpora.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":binary_info",
+ ":control_flow",
+ ":corpus",
+ ":feature",
+ ":logging",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ ]
+}
+
+# Generated from //centipede:binary_info
+pw_source_set("binary_info") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/binary_info.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":call_graph",
+ ":control_flow",
+ ]
+}
+
+# Generated from //centipede:blob_file
+pw_source_set("blob_file") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/blob_file.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/blob_file.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ ":logging",
+ ":remote_file",
+ ":util",
+ "../../abseil-cpp/absl/status",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:byte_array_mutator
+pw_source_set("byte_array_mutator") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ ":knobs",
+ ]
+}
+
+# Generated from //centipede:call_graph
+pw_source_set("call_graph") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/call_graph.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/call_graph.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":control_flow",
+ ":logging",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/log:check",
+ ]
+}
+
+# Generated from //centipede:callstack
+pw_source_set("callstack") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/callstack.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [ ":int_utils" ]
+}
+
+# Generated from //centipede:centipede_callbacks
+pw_source_set("centipede_callbacks") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/centipede_callbacks.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/centipede_callbacks.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":binary_info",
+ ":byte_array_mutator",
+ ":call_graph",
+ ":command",
+ ":control_flow",
+ ":defs",
+ ":environment",
+ ":execution_request",
+ ":execution_result",
+ ":knobs",
+ ":logging",
+ ":shared_memory_blob_sequence",
+ ":util",
+ "../../abseil-cpp/absl/strings",
+ ]
+}
+
+# Generated from //centipede:centipede_default_callbacks
+pw_source_set("centipede_default_callbacks") {
+ public =
+ [ "$dir_pw_third_party_fuzztest/centipede/centipede_default_callbacks.h" ]
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/centipede_default_callbacks.cc",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":centipede_interface",
+ ":defs",
+ ":environment",
+ ":execution_result",
+ ":logging",
+ ]
+}
+
+# Generated from //centipede:centipede_interface
+pw_source_set("centipede_interface") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/centipede_interface.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/centipede_interface.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":analyze_corpora",
+ ":binary_info",
+ ":blob_file",
+ ":centipede_callbacks",
+ ":centipede_lib",
+ ":command",
+ ":coverage",
+ ":defs",
+ ":environment",
+ ":logging",
+ ":minimize_crash",
+ ":remote_file",
+ ":shard_reader",
+ ":stats",
+ ":util",
+ "../../abseil-cpp/absl/status",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/time",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:centipede_lib
+pw_source_set("centipede_lib") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/centipede.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/centipede.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":blob_file",
+ ":centipede_callbacks",
+ ":command",
+ ":control_flow",
+ ":corpus",
+ ":coverage",
+ ":defs",
+ ":environment",
+ ":execution_result",
+ ":feature",
+ ":feature_set",
+ ":logging",
+ ":remote_file",
+ ":rusage_profiler",
+ ":rusage_stats",
+ ":shard_reader",
+ ":stats",
+ ":util",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/status",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/synchronization",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:centipede_runner
+pw_source_set("centipede_runner") {
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.cc",
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.h",
+ "$dir_pw_third_party_fuzztest/centipede/callstack.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_bitset.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_byteset.h",
+ "$dir_pw_third_party_fuzztest/centipede/defs.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.h",
+ "$dir_pw_third_party_fuzztest/centipede/feature.cc",
+ "$dir_pw_third_party_fuzztest/centipede/feature.h",
+ "$dir_pw_third_party_fuzztest/centipede/foreach_nonzero.h",
+ "$dir_pw_third_party_fuzztest/centipede/int_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.cc",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.h",
+ "$dir_pw_third_party_fuzztest/centipede/pc_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/reverse_pc_table.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_cmp_trace.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_fork_server.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interceptors.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interface.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_main.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_sancov.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.cc",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ":centipede_config1",
+ ":centipede_config2",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:centipede_runner
+pw_source_set("centipede_runner") {
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.cc",
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.h",
+ "$dir_pw_third_party_fuzztest/centipede/callstack.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_bitset.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_byteset.h",
+ "$dir_pw_third_party_fuzztest/centipede/defs.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.h",
+ "$dir_pw_third_party_fuzztest/centipede/feature.cc",
+ "$dir_pw_third_party_fuzztest/centipede/feature.h",
+ "$dir_pw_third_party_fuzztest/centipede/foreach_nonzero.h",
+ "$dir_pw_third_party_fuzztest/centipede/int_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.cc",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.h",
+ "$dir_pw_third_party_fuzztest/centipede/pc_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/reverse_pc_table.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_cmp_trace.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_fork_server.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interceptors.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interface.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_main.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_sancov.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.cc",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ":centipede_config1",
+ ":centipede_config2",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:centipede_runner
+pw_source_set("centipede_runner") {
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.cc",
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.h",
+ "$dir_pw_third_party_fuzztest/centipede/callstack.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_bitset.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_byteset.h",
+ "$dir_pw_third_party_fuzztest/centipede/defs.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.h",
+ "$dir_pw_third_party_fuzztest/centipede/feature.cc",
+ "$dir_pw_third_party_fuzztest/centipede/feature.h",
+ "$dir_pw_third_party_fuzztest/centipede/foreach_nonzero.h",
+ "$dir_pw_third_party_fuzztest/centipede/int_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.cc",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.h",
+ "$dir_pw_third_party_fuzztest/centipede/pc_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/reverse_pc_table.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_cmp_trace.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_fork_server.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interceptors.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interface.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_main.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_sancov.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.cc",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ":centipede_config1",
+ ":centipede_config2",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:centipede_runner
+pw_source_set("centipede_runner") {
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.cc",
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.h",
+ "$dir_pw_third_party_fuzztest/centipede/callstack.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_bitset.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_byteset.h",
+ "$dir_pw_third_party_fuzztest/centipede/defs.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.h",
+ "$dir_pw_third_party_fuzztest/centipede/feature.cc",
+ "$dir_pw_third_party_fuzztest/centipede/feature.h",
+ "$dir_pw_third_party_fuzztest/centipede/foreach_nonzero.h",
+ "$dir_pw_third_party_fuzztest/centipede/int_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.cc",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.h",
+ "$dir_pw_third_party_fuzztest/centipede/pc_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/reverse_pc_table.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_cmp_trace.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_fork_server.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interceptors.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interface.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_main.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_sancov.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.cc",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ":centipede_config1",
+ ":centipede_config2",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:centipede_runner_no_main
+pw_source_set("centipede_runner_no_main") {
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.cc",
+ "$dir_pw_third_party_fuzztest/centipede/byte_array_mutator.h",
+ "$dir_pw_third_party_fuzztest/centipede/callstack.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_bitset.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_byteset.h",
+ "$dir_pw_third_party_fuzztest/centipede/defs.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_request.h",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.cc",
+ "$dir_pw_third_party_fuzztest/centipede/execution_result.h",
+ "$dir_pw_third_party_fuzztest/centipede/feature.cc",
+ "$dir_pw_third_party_fuzztest/centipede/feature.h",
+ "$dir_pw_third_party_fuzztest/centipede/foreach_nonzero.h",
+ "$dir_pw_third_party_fuzztest/centipede/int_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.cc",
+ "$dir_pw_third_party_fuzztest/centipede/knobs.h",
+ "$dir_pw_third_party_fuzztest/centipede/pc_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/reverse_pc_table.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_cmp_trace.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_dl_info.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_fork_server.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interceptors.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_interface.h",
+ "$dir_pw_third_party_fuzztest/centipede/runner_sancov.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.cc",
+ "$dir_pw_third_party_fuzztest/centipede/runner_utils.h",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.cc",
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ":centipede_config1",
+ ":centipede_config2",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:command
+pw_source_set("command") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/command.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/command.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ ":util",
+ "../../abseil-cpp/absl/log:check",
+ "../../abseil-cpp/absl/status",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //centipede:config_file
+pw_source_set("config_file") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/config_file.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/config_file.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":config_util",
+ ":logging",
+ ":remote_file",
+ ":util",
+ "../../abseil-cpp/absl/flags:flag",
+ "../../abseil-cpp/absl/flags:parse",
+ "../../abseil-cpp/absl/flags:reflection",
+ "../../abseil-cpp/absl/log:check",
+ "../../abseil-cpp/absl/strings",
+ ]
+}
+
+# Generated from //centipede:config_util
+pw_source_set("config_util") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/config_util.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/config_util.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/flags:reflection",
+ "../../abseil-cpp/absl/strings",
+ ]
+}
+
+# Generated from //centipede:control_flow
+pw_source_set("control_flow") {
+ public = [
+ "$dir_pw_third_party_fuzztest/centipede/control_flow.h",
+ "$dir_pw_third_party_fuzztest/centipede/symbol_table.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/control_flow.cc",
+ "$dir_pw_third_party_fuzztest/centipede/symbol_table.cc",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":command",
+ ":defs",
+ ":logging",
+ ":pc_info",
+ ":util",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/strings",
+ ]
+}
+
+# Generated from //centipede:corpus
+pw_source_set("corpus") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/corpus.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/corpus.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":binary_info",
+ ":control_flow",
+ ":coverage",
+ ":defs",
+ ":feature",
+ ":feature_set",
+ ":util",
+ "../../abseil-cpp/absl/strings",
+ ]
+}
+
+# Generated from //centipede:coverage
+pw_source_set("coverage") {
+ public = [
+ "$dir_pw_third_party_fuzztest/centipede/coverage.h",
+ "$dir_pw_third_party_fuzztest/centipede/symbol_table.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/coverage.cc",
+ "$dir_pw_third_party_fuzztest/centipede/symbol_table.cc",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":command",
+ ":control_flow",
+ ":defs",
+ ":feature",
+ ":logging",
+ ":util",
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/synchronization",
+ ]
+}
+
+# Generated from //centipede:defs
+pw_source_set("defs") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/defs.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [ "../../abseil-cpp/absl/types:span" ]
+}
+
+# Generated from //centipede:environment
+pw_source_set("environment") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/environment.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/environment.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":knobs",
+ ":logging",
+ ":remote_file",
+ ":util",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/flags:flag",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //centipede:execution_request
+pw_source_set("execution_request") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/execution_request.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/execution_request.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ ":shared_memory_blob_sequence",
+ ]
+}
+
+# Generated from //centipede:execution_result
+pw_source_set("execution_result") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/execution_result.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/execution_result.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":feature",
+ ":runner_cmp_trace",
+ ":shared_memory_blob_sequence",
+ ]
+}
+
+# Generated from //centipede:feature
+pw_source_set("feature") {
+ public = [
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_bitset.h",
+ "$dir_pw_third_party_fuzztest/centipede/concurrent_byteset.h",
+ "$dir_pw_third_party_fuzztest/centipede/feature.h",
+ "$dir_pw_third_party_fuzztest/centipede/foreach_nonzero.h",
+ ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/feature.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":int_utils",
+ "../../abseil-cpp/absl/base:core_headers",
+ ]
+}
+
+# Generated from //centipede:feature_set
+pw_source_set("feature_set") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/feature_set.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/feature_set.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":coverage",
+ ":defs",
+ ":feature",
+ ":util",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ ]
+}
+
+# Generated from //centipede:int_utils
+pw_source_set("int_utils") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/int_utils.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
+
+# Generated from //centipede:knobs
+pw_source_set("knobs") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/knobs.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/knobs.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:logging
+pw_source_set("logging") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/logging.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/logging.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/flags:flag",
+ "../../abseil-cpp/absl/log",
+ "../../abseil-cpp/absl/log:check",
+ ]
+}
+
+# Generated from //centipede:minimize_crash
+pw_source_set("minimize_crash") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/minimize_crash.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/minimize_crash.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":centipede_callbacks",
+ ":defs",
+ ":environment",
+ ":logging",
+ ":util",
+ "../../abseil-cpp/absl/synchronization",
+ ]
+}
+
+# Generated from //centipede:pc_info
+pw_source_set("pc_info") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/pc_info.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
+
+# Generated from //centipede:remote_file
+pw_source_set("remote_file") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/remote_file.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/remote_file.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ ":logging",
+ "../../abseil-cpp/absl/base:core_headers",
+ ]
+}
+
+# Generated from //centipede:reverse_pc_table
+pw_source_set("reverse_pc_table") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/reverse_pc_table.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":pc_info",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:runner_cmp_trace
+pw_source_set("runner_cmp_trace") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/runner_cmp_trace.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
+
+# Generated from //centipede:runner_fork_server
+pw_source_set("runner_fork_server") {
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/runner_fork_server.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
+
+# Generated from //centipede:rusage_profiler
+pw_source_set("rusage_profiler") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/rusage_profiler.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/rusage_profiler.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":rusage_stats",
+ "../../abseil-cpp/absl/log",
+ "../../abseil-cpp/absl/log:check",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/synchronization",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //centipede:rusage_stats
+pw_source_set("rusage_stats") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/rusage_stats.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/rusage_stats.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/log:check",
+ "../../abseil-cpp/absl/status",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //centipede:shard_reader
+pw_source_set("shard_reader") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/shard_reader.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/shard_reader.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":blob_file",
+ ":defs",
+ ":feature",
+ ":util",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ ]
+}
+
+# Generated from //centipede:shared_memory_blob_sequence
+pw_source_set("shared_memory_blob_sequence") {
+ public =
+ [ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.h" ]
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/shared_memory_blob_sequence.cc",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ":centipede_config1",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
+
+# Generated from //centipede:stats
+pw_source_set("stats") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/stats.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/stats.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":environment",
+ ":logging",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:test_util
+pw_source_set("test_util") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/test_util.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/test_util.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ ":logging",
+ ":util",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ ]
+}
+
+# Generated from //centipede:util
+pw_source_set("util") {
+ public = [ "$dir_pw_third_party_fuzztest/centipede/util.h" ]
+ sources = [
+ "$dir_pw_third_party_fuzztest/centipede/hash.cc",
+ "$dir_pw_third_party_fuzztest/centipede/util.cc",
+ ]
+ ldflags = [ "-Wl,-Bstatic -lcrypto -Wl,-Bdynamic -ldl" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":defs",
+ ":feature",
+ ":logging",
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/synchronization",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //centipede:weak_sancov_stubs
+pw_source_set("weak_sancov_stubs") {
+ sources = [ "$dir_pw_third_party_fuzztest/centipede/weak_sancov_stubs.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
diff --git a/third_party/fuzztest/codelab/BUILD.gn b/third_party/fuzztest/codelab/BUILD.gn
new file mode 100644
index 000000000..6f5fdc0b1
--- /dev/null
+++ b/third_party/fuzztest/codelab/BUILD.gn
@@ -0,0 +1,43 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for codelab.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+
+# Generated from //codelab:escaping
+pw_source_set("escaping") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_fuzztest/codelab/escaping.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/codelab/escaping.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+}
diff --git a/third_party/fuzztest/configs/BUILD.gn b/third_party/fuzztest/configs/BUILD.gn
new file mode 100644
index 000000000..3fdac81d9
--- /dev/null
+++ b/third_party/fuzztest/configs/BUILD.gn
@@ -0,0 +1,37 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+
+# Targets that include FuzzTest headers need to include this config.
+config("disabled_warnings") {
+ cflags = [
+ "-Wno-sign-compare",
+ "-Wno-sign-conversion",
+ "-Wno-shorten-64-to-32",
+ "-Wno-unused-parameter",
+ "-Wno-missing-field-initializers",
+ ]
+ cflags_cc = [ "-Wno-extra-semi" ]
+}
+
+# Include path for FuzzTest.
+#
+# This is needed as FuzzTest is built in a dedicated toolchain, and
+# `public_configs` do not propagate across toolchain boundaries by default.
+config("public_include_path") {
+ include_dirs = [ dir_pw_third_party_fuzztest ]
+}
diff --git a/third_party/fuzztest/docs.rst b/third_party/fuzztest/docs.rst
new file mode 100644
index 000000000..44e3892ad
--- /dev/null
+++ b/third_party/fuzztest/docs.rst
@@ -0,0 +1,105 @@
+.. _module-pw_third_party_fuzztest:
+
+========
+FuzzTest
+========
+The ``$dir_pw_third_party/fuzztest/`` module provides build files to allow
+optionally including upstream FuzzTest.
+
+.. _module-pw_third_party_fuzztest-using_upstream:
+
+-----------------------
+Using upstream FuzzTest
+-----------------------
+If you want to use FuzzTest, you must do the following:
+
+Submodule
+=========
+Add FuzzTest to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add https://github.com/google/fuzztest.git \
+ third_party/fuzztest
+
+.. tab-set::
+
+ .. tab-item:: GN
+
+ Set the GN following GN bauild args:
+
+ * Set ``dir_pw_third_party_fuzztest`` to the location of the FuzzTest
+ source. If you used the command above, this will be
+ ``//third_party/fuzztest``.
+
+ * Set ``dir_pw_third_party_abseil_cpp`` to the location of the
+ :ref:`module-pw_third_party_abseil_cpp` source.
+
+ * Set ``dir_pw_third_party_googletest`` to the location of the
+ :ref:`module-pw_third_party_googletest` source.
+
+ * Set ``dir_pw_third_party_re2`` to the location of the
+ :ref:`module-pw_third_party_re2` source.
+
+ This can be set in your ``args.gn`` or ``.gn`` file. For example:
+
+ .. code-block::
+
+ # Set build arguments here. See `gn help buildargs`.
+ dir_pw_third_party_abseil_cpp="//third_party/abseil-cpp"
+ dir_pw_third_party_fuzztest="//third_party/fuzztest"
+ dir_pw_third_party_googletest="//third_party/googletest"
+ dir_pw_third_party_re2="//third_party/re2"
+
+ .. tab-item:: CMake
+
+ Set the following CMake variables:
+
+ * Set ``dir_pw_third_party_fuzztest`` to the location of the
+ FuzzTest source.
+
+ * Set ``dir_pw_third_party_googletest`` to the location of the
+ :ref:`module-pw_third_party_googletest` source.
+
+ * Set ``pw_unit_test_GOOGLETEST_BACKEND`` to ``pw_third_party.fuzztest``.
+
+ .. tab-item:: Bazel
+
+ Set the following `label flags`_, either in your `target config`_ or on
+ the command line:
+
+ * ``pw_fuzzer_fuzztest_backend`` to ``@com_google_fuzztest//fuzztest``.
+
+ For example:
+
+ .. code-block:: sh
+
+ bazel test //... \
+ --@pigweed//targets:pw_fuzzer_fuzztest_backend=@com_google_fuzztest//fuzztest
+
+.. _target config: :ref:`_docs-build_system-bazel_configuration`
+.. _label flags: :ref:`_docs-build_system-bazel_flags`
+
+Updating
+========
+The GN build files are generated from the third-party Bazel build files using
+$dir_pw_build/py/pw_build/generate_3p_gn.py.
+
+The script uses data taken from ``$dir_pw_third_party/fuzztest/repo.json``.
+
+The script should be re-run whenever the submodule is updated or the JSON file
+is modified. Specify the location of the Bazel repository can be specified using
+the ``-w`` option, e.g.
+
+.. code-block:: sh
+
+ python pw_build/py/pw_build/generate_3p_gn.py \
+ -w third_party/fuzztest/src
+
+.. DO NOT EDIT BELOW THIS LINE. Generated section.
+
+Version
+=======
+The update script was last run for revision `3c77f971`_.
+
+.. _3c77f971: https://github.com/google/fuzztes/tree/3c77f97183a1270796d25db1a8956706a25af238
diff --git a/third_party/fuzztest/domain_tests/BUILD.gn b/third_party/fuzztest/domain_tests/BUILD.gn
new file mode 100644
index 000000000..5eecbd580
--- /dev/null
+++ b/third_party/fuzztest/domain_tests/BUILD.gn
@@ -0,0 +1,61 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for domain_tests.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/error.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+import("$dir_pw_third_party/googletest/googletest.gni")
+import("$dir_pw_third_party/protobuf/protobuf.gni")
+
+# Generated from //domain_tests:domain_testing
+pw_source_set("domain_testing") {
+ visibility = [ ":*" ]
+ public = [ "$dir_pw_third_party_fuzztest/domain_tests/domain_testing.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/hash",
+ "../../abseil-cpp/absl/random",
+ "../../abseil-cpp/absl/random:bit_gen_ref",
+ "../../abseil-cpp/absl/status",
+ "../../abseil-cpp/absl/strings",
+ "../../googletest",
+ "../../protobuf",
+ "../fuzztest:logging",
+ "../fuzztest:meta",
+ "../fuzztest:serialization",
+ "../fuzztest:test_protobuf_cc_proto",
+ "../fuzztest:type_support",
+ ]
+}
diff --git a/third_party/fuzztest/fuzztest.bazelrc b/third_party/fuzztest/fuzztest.bazelrc
new file mode 100644
index 000000000..e297041ca
--- /dev/null
+++ b/third_party/fuzztest/fuzztest.bazelrc
@@ -0,0 +1,47 @@
+### DO NOT EDIT. Generated file.
+#
+# To regenerate, run the following from your project's workspace:
+#
+# bazel run @com_google_fuzztest//bazel:setup_configs > fuzztest.bazelrc
+#
+# And don't forget to add the following to your project's .bazelrc:
+#
+# try-import %workspace%/fuzztest.bazelrc
+
+
+### Common options.
+#
+# Do not use directly.
+
+# Compile and link with Address Sanitizer (ASAN).
+build:fuzztest-common --linkopt=-fsanitize=address
+build:fuzztest-common --copt=-fsanitize=address
+
+# Standard define for "ifdef-ing" any fuzz test specific code.
+build:fuzztest-common --copt=-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
+
+# In fuzz tests, we want to catch assertion violations even in optimized builds.
+build:fuzztest-common --copt=-UNDEBUG
+
+# Enable libc++ assertions.
+# See https://libcxx.llvm.org/UsingLibcxx.html#enabling-the-safe-libc-mode
+build:fuzztest-common --copt=-D_LIBCPP_ENABLE_ASSERTIONS=1
+
+
+### FuzzTest build configuration.
+#
+# Use with: --config=fuzztest
+
+build:fuzztest --config=fuzztest-common
+
+# Link statically.
+build:fuzztest --dynamic_mode=off
+
+# We rely on the following flag instead of the compiler provided
+# __has_feature(address_sanitizer) to know that we have an ASAN build even in
+# the uninstrumented runtime.
+build:fuzztest --copt=-DADDRESS_SANITIZER
+
+# We apply coverage tracking instrumentation to everything but the
+# FuzzTest framework itself (including GoogleTest and GoogleMock).
+build:fuzztest --per_file_copt=+//,-//fuzztest:,-googletest/.*,-googlemock/.*@-fsanitize-coverage=inline-8bit-counters,-fsanitize-coverage=trace-cmp \ No newline at end of file
diff --git a/third_party/fuzztest/fuzztest.gni b/third_party/fuzztest/fuzztest.gni
new file mode 100644
index 000000000..ac90efdbd
--- /dev/null
+++ b/third_party/fuzztest/fuzztest.gni
@@ -0,0 +1,23 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+
+declare_args() {
+ # If compiling tests with FuzzTest, this variable is set to the path to the
+ # FuzzTest installation. When set, a pw_source_set for the FuzzTest library is
+ # created at "$dir_pw_third_party/fuzztest".
+ dir_pw_third_party_fuzztest = ""
+}
diff --git a/third_party/fuzztest/fuzztest/BUILD.gn b/third_party/fuzztest/fuzztest/BUILD.gn
new file mode 100644
index 000000000..b5a83200f
--- /dev/null
+++ b/third_party/fuzztest/fuzztest/BUILD.gn
@@ -0,0 +1,663 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+# It contains GN build targets for fuzztest.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/error.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_third_party/abseil-cpp/abseil-cpp.gni")
+import("$dir_pw_third_party/fuzztest/fuzztest.gni")
+import("$dir_pw_third_party/googletest/googletest.gni")
+import("$dir_pw_third_party/re2/re2.gni")
+
+# Generated from //fuzztest:absl_helpers
+pw_source_set("absl_helpers") {
+ public = [
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/absl_helpers.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //fuzztest:any
+pw_source_set("any") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/any.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ ":meta",
+ ]
+}
+
+# Generated from //fuzztest:compatibility_mode
+pw_source_set("compatibility_mode") {
+ public =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/internal/compatibility_mode.h" ]
+ sources =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/internal/compatibility_mode.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":fixture_driver",
+ ":logging",
+ ":runtime",
+ "../../abseil-cpp/absl/random:distributions",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //fuzztest:coverage
+pw_source_set("coverage") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/coverage.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/coverage.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ ":table_of_recent_compares",
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:domain
+pw_source_set("domain") {
+ public = [
+ "$dir_pw_third_party_fuzztest/fuzztest/domain.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/aggregate_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/arbitrary_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/bit_flag_combination_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/container_mutation_helpers.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/container_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/domain_base.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/element_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/filter_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/flat_map_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/in_grammar_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/in_range_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/in_regexp_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/map_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/one_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/optional_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/protobuf_domain_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/serialization_helpers.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/smart_pointer_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/unique_elements_container_of_impl.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/value_mutation_helpers.h",
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/variant_of_impl.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/in_grammar_impl.cc",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":absl_helpers",
+ ":any",
+ ":coverage",
+ ":logging",
+ ":meta",
+ ":regexp_dfa",
+ ":serialization",
+ ":table_of_recent_compares",
+ ":type_support",
+ "../../abseil-cpp/absl/base:core_headers",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/numeric:bits",
+ "../../abseil-cpp/absl/numeric:int128",
+ "../../abseil-cpp/absl/random",
+ "../../abseil-cpp/absl/random:bit_gen_ref",
+ "../../abseil-cpp/absl/random:distributions",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/synchronization",
+ "../../abseil-cpp/absl/time",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:fixture_driver
+pw_source_set("fixture_driver") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/fixture_driver.h" ]
+ sources =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/internal/fixture_driver.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":any",
+ ":domain",
+ ":logging",
+ ":registration",
+ ":type_support",
+ "../../abseil-cpp/absl/memory",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:fuzztest
+pw_source_set("fuzztest") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/fuzztest.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/fuzztest.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":domain",
+ ":io",
+ ":registry",
+ ":runtime",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ ]
+}
+
+# Generated from //fuzztest:fuzztest_gtest_main
+pw_source_set("fuzztest_gtest_main") {
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/fuzztest_gtest_main.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":fuzztest",
+ ":googletest_adaptor",
+ ":runtime",
+ "../../abseil-cpp/absl/flags:flag",
+ "../../abseil-cpp/absl/flags:parse",
+ "../../abseil-cpp/absl/time",
+ "../../googletest",
+ ]
+}
+
+# Generated from //fuzztest:googletest_adaptor
+pw_source_set("googletest_adaptor") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/googletest_adaptor.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":registry",
+ ":runtime",
+ "../../googletest",
+ ]
+}
+
+# Generated from //fuzztest:googletest_fixture_adapter
+pw_source_set("googletest_fixture_adapter") {
+ public =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/googletest_fixture_adapter.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":fixture_driver",
+ "../../googletest",
+ ]
+}
+
+# Generated from //fuzztest:io
+pw_source_set("io") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/io.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/io.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ "../../abseil-cpp/absl/hash",
+ "../../abseil-cpp/absl/strings:str_format",
+ ]
+}
+
+# Generated from //fuzztest:logging
+pw_source_set("logging") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/logging.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/logging.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [ "../../abseil-cpp/absl/strings" ]
+}
+
+# Generated from //fuzztest:meta
+pw_source_set("meta") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/meta.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [ "../../abseil-cpp/absl/numeric:int128" ]
+}
+
+# Generated from //fuzztest:regexp_dfa
+pw_source_set("regexp_dfa") {
+ check_includes = false
+ public =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/regexp_dfa.h" ]
+ sources =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/internal/domains/regexp_dfa.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/random:bit_gen_ref",
+ "../../abseil-cpp/absl/random:distributions",
+ "../../re2",
+ ]
+}
+
+# Generated from //fuzztest:registration
+pw_source_set("registration") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/registration.h" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":domain",
+ ":meta",
+ ":type_support",
+ "../../abseil-cpp/absl/functional:any_invocable",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:registry
+pw_source_set("registry") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/registry.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/registry.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":compatibility_mode",
+ ":fixture_driver",
+ ":registration",
+ ":runtime",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/functional:function_ref",
+ ]
+}
+
+# Generated from //fuzztest:runtime
+pw_source_set("runtime") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/runtime.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/runtime.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":coverage",
+ ":domain",
+ ":fixture_driver",
+ ":io",
+ ":logging",
+ ":meta",
+ ":registration",
+ ":seed_seq",
+ ":serialization",
+ ":type_support",
+ "../../abseil-cpp/absl/functional:any_invocable",
+ "../../abseil-cpp/absl/functional:function_ref",
+ "../../abseil-cpp/absl/random",
+ "../../abseil-cpp/absl/random:bit_gen_ref",
+ "../../abseil-cpp/absl/random:distributions",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/time",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:seed_seq
+pw_source_set("seed_seq") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/seed_seq.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/seed_seq.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ "../../abseil-cpp/absl/random",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:serialization
+pw_source_set("serialization") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/serialization.h" ]
+ sources =
+ [ "$dir_pw_third_party_fuzztest/fuzztest/internal/serialization.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":meta",
+ "../../abseil-cpp/absl/numeric:int128",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/types:span",
+ ]
+}
+
+# Generated from //fuzztest:subprocess
+pw_source_set("subprocess") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/subprocess.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/subprocess.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":logging",
+ "../../abseil-cpp/absl/container:flat_hash_map",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/time",
+ ]
+}
+
+# Generated from //fuzztest:table_of_recent_compares
+pw_source_set("table_of_recent_compares") {
+ public = [
+ "$dir_pw_third_party_fuzztest/fuzztest/internal/table_of_recent_compares.h",
+ ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":type_support",
+ "../../abseil-cpp/absl/container:flat_hash_set",
+ "../../abseil-cpp/absl/random:bit_gen_ref",
+ "../../abseil-cpp/absl/random:distributions",
+ ]
+}
+
+# Generated from //fuzztest:type_support
+pw_source_set("type_support") {
+ public = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/type_support.h" ]
+ sources = [ "$dir_pw_third_party_fuzztest/fuzztest/internal/type_support.cc" ]
+ public_configs = [ "..:fuzztest_public_config1" ]
+ configs = [
+ "../../abseil-cpp/configs:disabled_warnings",
+ "../../re2/configs:disabled_warnings",
+ "../configs:disabled_warnings",
+ ]
+ remove_configs = [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_thread",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ ]
+ public_deps = [
+ ":absl_helpers",
+ ":meta",
+ "../../abseil-cpp/absl/debugging:symbolize",
+ "../../abseil-cpp/absl/strings",
+ "../../abseil-cpp/absl/strings:str_format",
+ "../../abseil-cpp/absl/time",
+ ]
+}
diff --git a/third_party/fuzztest/repo.json b/third_party/fuzztest/repo.json
new file mode 100644
index 000000000..37581268a
--- /dev/null
+++ b/third_party/fuzztest/repo.json
@@ -0,0 +1,30 @@
+{
+ "name": "FuzzTest",
+ "repos": {
+ "com_google_absl": "abseil-cpp",
+ "com_google_googletest": "googletest",
+ "com_google_protobuf": "protobuf",
+ "com_googlesource_code_re2": "re2"
+ },
+ "aliases": {
+ "$dir_pw_third_party/googletest:gtest": "$dir_pw_third_party/googletest"
+ },
+ "add": [
+ "$dir_pw_third_party/fuzztest/configs:disabled_warnings",
+ "$dir_pw_third_party/abseil-cpp/configs:disabled_warnings",
+ "$dir_pw_third_party/re2/configs:disabled_warnings"
+ ],
+ "remove": [
+ "$dir_pw_fuzzer:instrumentation",
+ "$dir_pw_toolchain/host_clang:sanitize_address",
+ "$dir_pw_toolchain/host_clang:sanitize_memory",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined",
+ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic",
+ "$dir_pw_toolchain/host_clang:sanitize_thread"
+ ],
+ "allow_testonly": true,
+ "no_gn_check": [ "//fuzztest:regexp_dfa" ],
+ "extra_files": {
+ "fuzztest.bazelrc": "@com_google_fuzztest//bazel:setup_configs"
+ }
+}
diff --git a/third_party/googletest/BUILD.gn b/third_party/googletest/BUILD.gn
index 50f7be353..6cc73e7e2 100644
--- a/third_party/googletest/BUILD.gn
+++ b/third_party/googletest/BUILD.gn
@@ -35,6 +35,7 @@ if (dir_pw_third_party_googletest != "") {
cflags = [
"-Wno-undef",
"-Wno-conversion",
+ "-Wno-switch-enum",
]
}
diff --git a/third_party/googletest/docs.rst b/third_party/googletest/docs.rst
index e9cf3b158..86ccf57d0 100644
--- a/third_party/googletest/docs.rst
+++ b/third_party/googletest/docs.rst
@@ -4,39 +4,70 @@
GoogleTest
==========
The ``$dir_pw_third_party/googletest/`` module provides various helpers to
-optionally use full upstream GoogleTest/GoogleMock with
+optionally use full upstream `GoogleTest/GoogleMock`__ with
:ref:`module-pw_unit_test`.
+.. __: https://github.com/google/googletest
+
+.. _module-pw_third_party_googletest-using_upstream:
+
----------------------------------------
Using upstream GoogleTest and GoogleMock
----------------------------------------
If you want to use the full upstream GoogleTest/GoogleMock, you must do the
following:
-Submodule
-=========
Add GoogleTest to your workspace with the following command.
.. code-block:: sh
- git submodule add https://github.com/google/googletest third_party/googletest
-
-GN
-==
-* Set the GN var ``dir_pw_third_party_googletest`` to the location of the
- GoogleTest source. If you used the command above this will be
- ``//third_party/googletest``.
-* Set the GN var ``pw_unit_test_MAIN = dir_pigweed + "/third_party/googletest:gmock_main"``.
-* Set the GN var
- ``pw_unit_test_GOOGLETEST_BACKEND = "//third_party/googletest"``.
-
-CMake
-=====
-* Set the ``dir_pw_third_party_googletest`` to the location of the
- GoogleTest source.
-* Set the var ``pw_unit_test_MAIN`` to ``pw_third_party.googletest.gmock_main``.
-* Set the var ``pw_unit_test_GOOGLETEST_BACKEND`` to
- ``pw_third_party.googletest``.
+ git submodule add https://github.com/google/googletest third_party/googletest
+
+Configure ``pw_unit_test`` to use upstream GoogleTest/GoogleMock.
+
+.. tab-set::
+
+ .. tab-item:: GN
+
+ * Set the GN var ``dir_pw_third_party_googletest`` to the location of the
+ GoogleTest source. If you used the command above this will be
+ ``//third_party/googletest``.
+ * Set the GN var ``pw_unit_test_MAIN`` to
+ ``dir_pigweed + "/third_party/googletest:gmock_main"``.
+ * Set the GN var ``pw_unit_test_GOOGLETEST_BACKEND`` to
+ ``"//third_party/googletest"``.
+
+ Pigweed unit tests that do not work with upstream GoogleTest can be
+ disabled by setting ``enable_if`` to
+ ``pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_unit_test:light"``.
+
+ .. tab-item:: CMake
+
+ * Set the ``dir_pw_third_party_googletest`` to the location of the
+ GoogleTest source.
+ * Set the var
+ ``pw_unit_test_MAIN`` to ``pw_third_party.googletest.gmock_main``.
+ * Set the var ``pw_unit_test_GOOGLETEST_BACKEND`` to
+ ``pw_third_party.googletest``.
+
+ .. tab-item:: Bazel
+
+ Set the following :ref:`label flags <docs-build_system-bazel_flags>`,
+ either in your
+ :ref:`target config <docs-build_system-bazel_configuration>` or on
+ the command line:
+
+ * ``pw_unit_test_googletest_backend`` to
+ ``@com_google_googletest//:gtest``.
+ * ``pw_unit_test_main`` to ``@com_google_googletest//:gtest_main``.
+
+ For example:
+
+ .. code-block:: sh
+
+ bazel test //... \
+ --@pigweed//targets:pw_unit_test_googletest_backend=@com_google_googletest//:gtest \
+ --@pigweed//targets:pw_unit_test_main=@com_google_googletest//:gtest_main
.. note::
diff --git a/third_party/llvm_builtins/BUILD.gn b/third_party/llvm_builtins/BUILD.gn
new file mode 100644
index 000000000..d38f26ffe
--- /dev/null
+++ b/third_party/llvm_builtins/BUILD.gn
@@ -0,0 +1,260 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/error.gni")
+import("$dir_pw_build/target_types.gni")
+
+declare_args() {
+ # This should be set to the path of the llvm compiler-rt/builtins directory.
+ dir_pw_third_party_llvm_builtins = ""
+
+ # Points to a pw_source_set that enumerates builtins specific to
+ # the current toolchain.
+ pw_third_party_llvm_builtins_TARGET_BUILTINS = ""
+}
+
+config("no-warnings") {
+ cflags = [
+ "-Wno-double-promotion",
+ "-Wno-undef",
+ "-Wno-unused-parameter",
+ "-Wno-strict-prototypes",
+ ]
+}
+
+files = [
+ "absvdi2.c",
+ "absvsi2.c",
+ "absvti2.c",
+ "adddf3.c",
+ "addsf3.c",
+ "addtf3.c",
+ "addvdi3.c",
+ "addvsi3.c",
+ "addvti3.c",
+ "ashldi3.c",
+ "ashlti3.c",
+ "ashrdi3.c",
+ "ashrti3.c",
+ "clzti2.c",
+ "cmpdi2.c",
+ "cmpti2.c",
+ "comparedf2.c",
+ "ctzdi2.c",
+ "ctzsi2.c",
+ "ctzti2.c",
+ "divdc3.c",
+ "divdf3.c",
+ "divdi3.c",
+ "divmoddi4.c",
+ "divsc3.c",
+ "divsf3.c",
+ "divtc3.c",
+ "divtf3.c",
+ "divti3.c",
+ "extendhfsf2.c",
+ "extendsfdf2.c",
+ "ffsdi2.c",
+ "ffssi2.c",
+ "ffsti2.c",
+ "fixdfdi.c",
+ "fixdfsi.c",
+ "fixdfti.c",
+ "fixsfdi.c",
+ "fixsfsi.c",
+ "fixsfti.c",
+ "fixunsdfdi.c",
+ "fixunsdfsi.c",
+ "fixunsdfti.c",
+ "fixunssfdi.c",
+ "fixunssfsi.c",
+ "fixunssfti.c",
+ "floatdidf.c",
+ "floatdisf.c",
+ "floatsidf.c",
+ "floatsisf.c",
+ "floattidf.c",
+ "floattisf.c",
+ "floatundidf.c",
+ "floatundisf.c",
+ "floatunsidf.c",
+ "floatunsisf.c",
+ "floatuntidf.c",
+ "floatuntisf.c",
+ "int_util.c",
+ "lshrdi3.c",
+ "lshrti3.c",
+ "moddi3.c",
+ "modti3.c",
+ "muldc3.c",
+ "muldf3.c",
+ "muldi3.c",
+ "mulodi4.c",
+ "mulosi4.c",
+ "muloti4.c",
+ "mulsc3.c",
+ "mulsf3.c",
+ "multf3.c",
+ "multi3.c",
+ "mulvdi3.c",
+ "mulvsi3.c",
+ "mulvti3.c",
+ "negdf2.c",
+ "negdi2.c",
+ "negsf2.c",
+ "negti2.c",
+ "negvdi2.c",
+ "negvsi2.c",
+ "negvti2.c",
+ "os_version_check.c",
+ "paritydi2.c",
+ "paritysi2.c",
+ "parityti2.c",
+ "popcountdi2.c",
+ "popcountsi2.c",
+ "popcountti2.c",
+ "powidf2.c",
+ "powisf2.c",
+ "powitf2.c",
+ "subdf3.c",
+ "subsf3.c",
+ "subtf3.c",
+ "subvdi3.c",
+ "subvsi3.c",
+ "subvti3.c",
+ "trampoline_setup.c",
+ "truncdfhf2.c",
+ "truncdfsf2.c",
+ "truncsfhf2.c",
+ "ucmpdi2.c",
+ "ucmpti2.c",
+ "udivdi3.c",
+ "udivmoddi4.c",
+ "udivmodti4.c",
+ "udivti3.c",
+ "umoddi3.c",
+ "umodti3.c",
+]
+
+pw_source_set("arm_builtins") {
+ configs = [ ":no-warnings" ]
+ remove_configs = [ "//pw_build:extra_strict_warnings" ]
+
+ sources = []
+
+ arm_files = [
+ "arm/aeabi_cdcmp.S",
+ "arm/aeabi_cdcmpeq_check_nan.c",
+ "arm/aeabi_cfcmp.S",
+ "arm/aeabi_cfcmpeq_check_nan.c",
+ "arm/aeabi_dcmp.S",
+ "arm/aeabi_div0.c",
+ "arm/aeabi_drsub.c",
+ "arm/aeabi_fcmp.S",
+ "arm/aeabi_frsub.c",
+ "arm/aeabi_idivmod.S",
+ "arm/aeabi_ldivmod.S",
+ "arm/aeabi_memcmp.S",
+ "arm/aeabi_memcpy.S",
+ "arm/aeabi_memmove.S",
+ "arm/aeabi_memset.S",
+ "arm/aeabi_uidivmod.S",
+ "arm/aeabi_uldivmod.S",
+ "arm/bswapdi2.S",
+ "arm/bswapsi2.S",
+ "arm/clzdi2.S",
+ "arm/clzsi2.S",
+ "arm/comparesf2.S",
+ "arm/divmodsi4.S",
+ "arm/divsi3.S",
+ "arm/fp_mode.c",
+ "arm/modsi3.S",
+ "arm/switch16.S",
+ "arm/switch32.S",
+ "arm/switch8.S",
+ "arm/switchu8.S",
+ "arm/sync_fetch_and_add_4.S",
+ "arm/sync_fetch_and_add_8.S",
+ "arm/sync_fetch_and_and_4.S",
+ "arm/sync_fetch_and_and_8.S",
+ "arm/sync_fetch_and_max_4.S",
+ "arm/sync_fetch_and_max_8.S",
+ "arm/sync_fetch_and_min_4.S",
+ "arm/sync_fetch_and_min_8.S",
+ "arm/sync_fetch_and_nand_4.S",
+ "arm/sync_fetch_and_nand_8.S",
+ "arm/sync_fetch_and_or_4.S",
+ "arm/sync_fetch_and_or_8.S",
+ "arm/sync_fetch_and_sub_4.S",
+ "arm/sync_fetch_and_sub_8.S",
+ "arm/sync_fetch_and_umax_4.S",
+ "arm/sync_fetch_and_umax_8.S",
+ "arm/sync_fetch_and_umin_4.S",
+ "arm/sync_fetch_and_umin_8.S",
+ "arm/sync_fetch_and_xor_4.S",
+ "arm/sync_fetch_and_xor_8.S",
+ "arm/sync_synchronize.S",
+ "arm/udivmodsi4.S",
+ "arm/udivsi3.S",
+ "arm/umodsi3.S",
+ ]
+
+ if (dir_pw_third_party_llvm_builtins != "") {
+ foreach(file, arm_files) {
+ sources += [ "$dir_pw_third_party_llvm_builtins/$file" ]
+ }
+ } else {
+ not_needed([ "arm_files" ])
+ }
+}
+
+# This list includes the cpu's for which their is a `:${current_cpu}_builtins`
+# target.
+_default_supported_cpus = [ "arm" ]
+_current_cpu_is_known = _default_supported_cpus + [ current_cpu ] -
+ [ current_cpu ] != _default_supported_cpus
+
+pw_static_library("llvm_builtins") {
+ add_global_link_deps = false
+
+ configs = [ ":no-warnings" ]
+ remove_configs = [ "//pw_build:extra_strict_warnings" ]
+
+ sources = []
+
+ if (dir_pw_third_party_llvm_builtins != "") {
+ foreach(file, files) {
+ sources += [ "$dir_pw_third_party_llvm_builtins/$file" ]
+ }
+ } else {
+ not_needed([ "files" ])
+ }
+
+ if (pw_third_party_llvm_builtins_TARGET_BUILTINS != "") {
+ deps = [ pw_third_party_llvm_builtins_TARGET_BUILTINS ]
+ } else if (_current_cpu_is_known) {
+ deps = [ ":${current_cpu}_builtins" ]
+ } else {
+ deps = [ ":unknown_cpu" ]
+ }
+}
+
+if (pw_third_party_llvm_builtins_TARGET_BUILTINS == "" &&
+ !_current_cpu_is_known) {
+ pw_error("unknown_cpu") {
+ message = "Tried to build $dir_pw_third_party_llvm_builtins:llvm_builtins, but pw_third_party_llvm_builtins_TARGET_BUILTINS was not set and `current_cpu=\"$current_cpu\"` does not have well-known builtins set up."
+ }
+}
diff --git a/third_party/llvm_libc/BUILD.gn b/third_party/llvm_libc/BUILD.gn
new file mode 100644
index 000000000..3f49b2a6f
--- /dev/null
+++ b/third_party/llvm_libc/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("$dir_pw_docgen/docs.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/llvm_libc/docs.rst b/third_party/llvm_libc/docs.rst
new file mode 100644
index 000000000..b33b38ad9
--- /dev/null
+++ b/third_party/llvm_libc/docs.rst
@@ -0,0 +1,44 @@
+.. _module-pw_third_party_llvm_libc:
+
+=========
+LLVM libc
+=========
+The ``$dir_pw_third_party/llvm_libc/`` module provides various helpers to
+optionally use LLVM libc with :ref:`module-pw_libc`.
+
+------------------------
+Using upstream LLVM libc
+------------------------
+If you want to use LLVM libc, you must do the following:
+
+Submodule
+=========
+Add LLVM libc to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add https://llvm.googlesource.com/llvm-project/libc \
+ third_party/llvm_libc/src
+
+Note, this git repository is maintained by Google and is a slice of upstream
+LLVM including only the libc subdirectory.
+
+GN
+==
+* Set the GN var ``dir_pw_third_party_llvm_libc`` to the location of the LLVM
+ libc source. If you used the command above, this will be
+ ``//third_party/llvm_libc/src``
+
+ This can be set in your args.gn or .gn file like:
+ ``dir_pw_third_party_llvm_libc = "//third_party/llvm_libc_src"``
+
+------
+Status
+------
+Currently, pw_libc's llvm-libc integration only provides a pw_libc.a and is
+not suitable as a full replacment for libc. Not all functions used by
+projects are currently available to use from llvm-libc. Moreover, headers are
+provided from the sysroot libc. Startup files are also provided from the
+sysroot.
+
+In the future, we hope to be able to fully replace the sysroot libc.
diff --git a/third_party/llvm_libc/llvm_libc.gni b/third_party/llvm_libc/llvm_libc.gni
new file mode 100644
index 000000000..fea250900
--- /dev/null
+++ b/third_party/llvm_libc/llvm_libc.gni
@@ -0,0 +1,150 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_unit_test/test.gni")
+
+declare_args() {
+ # This variable is set to the path of llvm-libc. If set, pw_libc.a will
+ # be created using llvm-libc sources.
+ dir_pw_third_party_llvm_libc = ""
+}
+
+# Creates a source set from llvm-libc functions.
+#
+# This template creates both pw_source_set and pw_test targets pulling in the
+# necessary files to build the functions listed in the `functions` argument.
+# The target name should be the name of the standard header file the functions
+# are found in. For example, the source set that pulls in `printf` should be
+# called `stdio`. This is important because the `target_name` tells this
+# template where to look in the llvm-libc source tree for the functions.
+#
+# Args:
+# defines: A list of defines to be passed to both the implementation and test
+# sources.
+#
+# functions: This is a list of strings for the functions to include in the
+# source set. Additionally their tests will also be pulled in.
+#
+# no_test_functions: This is a list of functions from `functions` which should
+# not be tested. This is useful when the upstream tests either don't exist
+# or are not suitable for whatever reason.
+#
+# additional_srcs: This is a list of additional sources which should be added
+# to the source set. Some functions have their implementation split across
+# multiple source files, and not entirely in $function.cpp. In this case
+# `additional_srcs` should be used to pull those files in.
+#
+# non_cpu_dir: This tells the template which subdirectory to find the default
+# implementations of functions. For example, some direcotries like math/
+# have cpu specific implementations in their respective directories for
+# certain functions. `non_cpu_dir` describes where those functions can be
+# found. Note, at present upstream llvm-libc only has implementations for
+# x86 and arm64, so this template doesn't bother looking for the cpu
+# specific version, and will exclusively use the generic versions found in
+# `non_cpu_dir`.
+#
+template("pw_libc_source_set") {
+ source_set_target_name = target_name
+
+ target_dir = "$dir_pw_third_party_llvm_libc/src/$source_set_target_name"
+
+ pw_source_set(target_name) {
+ _dir = target_dir
+ _src_dir = _dir
+ if (defined(invoker.non_cpu_dir)) {
+ _src_dir += "/${invoker.non_cpu_dir}"
+ }
+
+ _additional_srcs = []
+ if (defined(invoker.additional_srcs)) {
+ _additional_srcs = invoker.additional_srcs
+ }
+
+ include_dirs = [ dir_pw_third_party_llvm_libc ]
+
+ defines = [
+ "LIBC_COPT_PUBLIC_PACKAGING=1",
+ "LIBC_COPT_USE_C_ASSERT=1",
+ "LIBC_INLINE=inline",
+ ]
+
+ if (defined(invoker.defines)) {
+ defines += invoker.defines
+ }
+
+ forward_variables_from(invoker,
+ "*",
+ [
+ "defines",
+ "functions",
+ "no_test_functions",
+ "additional_srcs",
+ "non_cpu_dir",
+ ])
+
+ public = []
+ sources = []
+
+ foreach(function, invoker.functions) {
+ public += [ "$_dir/$function.h" ]
+ sources += [ "$_src_dir/$function.cpp" ]
+ }
+
+ foreach(_src, _additional_srcs) {
+ sources += [ "$_dir/$_src" ]
+ }
+ }
+
+ pw_test("${source_set_target_name}_tests") {
+ _dir = "$dir_pw_third_party_llvm_libc/test/src/$source_set_target_name"
+
+ # This might not be used if all test functions are in no_test_functions
+ not_needed([ _dir ])
+
+ include_dirs = [ dir_pw_third_party_llvm_libc ]
+ defines = [
+ "LIBC_COPT_TEST_USE_PIGWEED",
+ "LIBC_COPT_USE_C_ASSERT=1",
+ ]
+
+ if (defined(invoker.defines)) {
+ defines += invoker.defines
+ }
+
+ forward_variables_from(invoker,
+ "*",
+ [
+ "defines",
+ "functions",
+ "no_test_functions",
+ "additional_srcs",
+ "non_cpu_dir",
+ ])
+
+ sources = []
+
+ _no_test_functions = []
+ if (defined(invoker.no_test_functions)) {
+ _no_test_functions = invoker.no_test_functions
+ }
+ foreach(function, invoker.functions - _no_test_functions) {
+ sources += [ "$_dir/${function}_test.cpp" ]
+ }
+
+ deps = [ ":$source_set_target_name" ]
+ }
+}
diff --git a/third_party/mbedtls/BUILD.bazel b/third_party/mbedtls/BUILD.bazel
index 7d007c550..55c9d0af5 100644
--- a/third_party/mbedtls/BUILD.bazel
+++ b/third_party/mbedtls/BUILD.bazel
@@ -17,23 +17,17 @@ load(
"pw_cc_library",
)
-# Ready-made configurations
-mbedtls_configs = [
- ("default", "configs/config_default.h"),
-]
+package(
+ default_visibility = ["//visibility:public"],
+)
# Config targets.
-[
- pw_cc_library(
- name = "%s_config" % config_name,
- hdrs = [
- config_header,
- "configs/config_pigweed_common.h",
- ],
- copts = ["-DMBEDTLS_CONFIG_FILE=\"%s\"" % config_header],
- includes = ["."],
- )
- for config_name, config_header in mbedtls_configs
-]
-
-# TODO(zyecheng): Add build recipe for the library.
+pw_cc_library(
+ name = "default_config",
+ hdrs = [
+ "configs/config_default.h",
+ "configs/config_pigweed_common.h",
+ ],
+ defines = ['MBEDTLS_CONFIG_FILE=\\"configs/config_default.h\\"'],
+ includes = ["."],
+)
diff --git a/third_party/mbedtls/BUILD.gn b/third_party/mbedtls/BUILD.gn
index 6d91da071..fc9d70ad9 100644
--- a/third_party/mbedtls/BUILD.gn
+++ b/third_party/mbedtls/BUILD.gn
@@ -17,8 +17,8 @@ import("$dir_pw_build/target_types.gni")
import("mbedtls.gni")
if (dir_pw_third_party_mbedtls != "") {
- # The list currently includes all source files for build.
- mbedtls_sources = [
+ # All source files for mbedtls-2.26.0 released on 2021-03-12
+ mbedtls_v2_sources = [
"library/aes.c",
"library/aesni.c",
"library/arc4.c",
@@ -107,6 +107,103 @@ if (dir_pw_third_party_mbedtls != "") {
"library/xtea.c",
]
+ # All source files for mbedtls-3.2.1 released on 2022-07-12
+ mbedtls_v3_sources = [
+ "library/aes.c",
+ "library/aesni.c",
+ "library/aria.c",
+ "library/asn1parse.c",
+ "library/asn1write.c",
+ "library/base64.c",
+ "library/bignum.c",
+ "library/camellia.c",
+ "library/ccm.c",
+ "library/chacha20.c",
+ "library/chachapoly.c",
+ "library/cipher.c",
+ "library/cipher_wrap.c",
+ "library/cmac.c",
+ "library/constant_time.c",
+ "library/ctr_drbg.c",
+ "library/debug.c",
+ "library/des.c",
+ "library/dhm.c",
+ "library/ecdh.c",
+ "library/ecdsa.c",
+ "library/ecjpake.c",
+ "library/ecp.c",
+ "library/ecp_curves.c",
+ "library/entropy.c",
+ "library/entropy_poll.c",
+ "library/error.c",
+ "library/gcm.c",
+ "library/hkdf.c",
+ "library/hmac_drbg.c",
+ "library/md.c",
+ "library/md5.c",
+ "library/memory_buffer_alloc.c",
+ "library/mps_reader.c",
+ "library/mps_trace.c",
+ "library/net_sockets.c",
+ "library/nist_kw.c",
+ "library/oid.c",
+ "library/padlock.c",
+ "library/pem.c",
+ "library/pk.c",
+ "library/pk_wrap.c",
+ "library/pkcs12.c",
+ "library/pkcs5.c",
+ "library/pkparse.c",
+ "library/pkwrite.c",
+ "library/platform.c",
+ "library/platform_util.c",
+ "library/poly1305.c",
+ "library/psa_crypto.c",
+ "library/psa_crypto_aead.c",
+ "library/psa_crypto_cipher.c",
+ "library/psa_crypto_client.c",
+ "library/psa_crypto_driver_wrappers.c",
+ "library/psa_crypto_ecp.c",
+ "library/psa_crypto_hash.c",
+ "library/psa_crypto_mac.c",
+ "library/psa_crypto_rsa.c",
+ "library/psa_crypto_se.c",
+ "library/psa_crypto_slot_management.c",
+ "library/psa_crypto_storage.c",
+ "library/psa_its_file.c",
+ "library/ripemd160.c",
+ "library/rsa.c",
+ "library/rsa_alt_helpers.c",
+ "library/sha1.c",
+ "library/sha256.c",
+ "library/sha512.c",
+ "library/ssl_cache.c",
+ "library/ssl_ciphersuites.c",
+ "library/ssl_client.c",
+ "library/ssl_cookie.c",
+ "library/ssl_debug_helpers_generated.c",
+ "library/ssl_msg.c",
+ "library/ssl_ticket.c",
+ "library/ssl_tls.c",
+ "library/ssl_tls12_client.c",
+ "library/ssl_tls12_server.c",
+ "library/ssl_tls13_client.c",
+ "library/ssl_tls13_generic.c",
+ "library/ssl_tls13_keys.c",
+ "library/ssl_tls13_server.c",
+ "library/threading.c",
+ "library/timing.c",
+ "library/version.c",
+ "library/version_features.c",
+ "library/x509.c",
+ "library/x509_create.c",
+ "library/x509_crl.c",
+ "library/x509_crt.c",
+ "library/x509_csr.c",
+ "library/x509write_crt.c",
+ "library/x509write_csr.c",
+ ]
+
config("public_config") {
include_dirs = [ "$dir_pw_third_party_mbedtls/include" ]
}
@@ -128,8 +225,32 @@ if (dir_pw_third_party_mbedtls != "") {
}
pw_source_set("mbedtls") {
+ # Skip gn check because downstream users may choose either but not
+ # both ":mbedtls" and ":mbedtls_v3" depending on if the underlying
+ # mbedtls git is 2.x or 3.x.
+ check_includes = false
sources = []
- foreach(source, mbedtls_sources) {
+ foreach(source, mbedtls_v2_sources) {
+ sources += [ "$dir_pw_third_party_mbedtls/" + source ]
+ }
+
+ public = [
+ "configs/config_default.h",
+ "configs/config_pigweed_common.h",
+ ]
+
+ public_deps = [ "$dir_pw_tls_client:time" ]
+ public_configs = [ ":public_config" ]
+ configs = [ ":internal_config" ]
+ }
+
+ pw_source_set("mbedtls_v3") {
+ # Skip gn check because downstream users may choose either but not
+ # both ":mbedtls" and ":mbedtls_v3" depending on if the underlying
+ # mbedtls git is 2.x or 3.x.
+ check_includes = false
+ sources = []
+ foreach(source, mbedtls_v3_sources) {
sources += [ "$dir_pw_third_party_mbedtls/" + source ]
}
@@ -145,4 +266,7 @@ if (dir_pw_third_party_mbedtls != "") {
} else {
group("mbedtls") {
}
+
+ group("mbedtls_v3") {
+ }
}
diff --git a/third_party/mbedtls/BUILD.mbedtls b/third_party/mbedtls/BUILD.mbedtls
new file mode 100644
index 000000000..4b90179aa
--- /dev/null
+++ b/third_party/mbedtls/BUILD.mbedtls
@@ -0,0 +1,114 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+package(
+ default_visibility = ["//visibility:public"],
+)
+
+cc_library(
+ name = "mbedtls",
+ srcs = [
+ "library/aes.c",
+ "library/aesni.c",
+ "library/aria.c",
+ "library/asn1parse.c",
+ "library/asn1write.c",
+ "library/base64.c",
+ "library/bignum.c",
+ "library/camellia.c",
+ "library/ccm.c",
+ "library/chacha20.c",
+ "library/chachapoly.c",
+ "library/cipher.c",
+ "library/cipher_wrap.c",
+ "library/cmac.c",
+ "library/constant_time.c",
+ "library/ctr_drbg.c",
+ "library/des.c",
+ "library/dhm.c",
+ "library/ecdh.c",
+ "library/ecdsa.c",
+ "library/ecjpake.c",
+ "library/ecp.c",
+ "library/ecp_curves.c",
+ "library/entropy.c",
+ "library/entropy_poll.c",
+ "library/error.c",
+ "library/gcm.c",
+ "library/hkdf.c",
+ "library/hmac_drbg.c",
+ "library/md.c",
+ "library/md5.c",
+ "library/memory_buffer_alloc.c",
+ "library/mps_reader.c",
+ "library/mps_trace.c",
+ "library/nist_kw.c",
+ "library/oid.c",
+ "library/padlock.c",
+ "library/pem.c",
+ "library/pk.c",
+ "library/pk_wrap.c",
+ "library/pkcs12.c",
+ "library/pkcs5.c",
+ "library/pkparse.c",
+ "library/pkwrite.c",
+ "library/platform.c",
+ "library/platform_util.c",
+ "library/poly1305.c",
+ "library/ripemd160.c",
+ "library/rsa.c",
+ "library/rsa_alt_helpers.c",
+ "library/sha1.c",
+ "library/sha256.c",
+ "library/sha512.c",
+ "library/ssl_debug_helpers_generated.c",
+ "library/threading.c",
+ "library/timing.c",
+ "library/version.c",
+ "library/version_features.c",
+ ],
+ includes = ["include/"],
+ textual_hdrs = [
+ "library/aesni.h",
+ "library/bignum_internal.h",
+ "library/bn_mul.h",
+ "library/cipher_wrap.h",
+ "library/common.h",
+ "library/constant_time_internal.h",
+ "library/constant_time_invasive.h",
+ "library/ecp_internal_alt.h",
+ "library/ecp_invasive.h",
+ "library/entropy_poll.h",
+ "library/md_wrap.h",
+ "library/pk_wrap.h",
+ "library/padlock.h",
+ "library/pkwrite.h",
+ "library/rsa_alt_helpers.h",
+ "library/ssl_debug_helpers.h",
+ "library/ssl_misc.h",
+ ] + glob(
+ include = ["include/**/*.h"],
+ exclude = ["include/psa/**"],
+ ),
+ deps = [
+ ":mbedtls_config",
+ ],
+)
+
+# Library containing project-specific mbedtls config header file.
+label_flag(
+ name = "mbedtls_config",
+ build_setting_default = ":empty_config",
+)
+
+cc_library(name = "empty_config")
diff --git a/third_party/mbedtls/configs/config_default.h b/third_party/mbedtls/configs/config_default.h
index 6518fbb1a..b8007e6de 100644
--- a/third_party/mbedtls/configs/config_default.h
+++ b/third_party/mbedtls/configs/config_default.h
@@ -14,7 +14,14 @@
#pragma once
+#include <mbedtls/version.h>
+
+#if MBEDTLS_VERSION_MAJOR >= 3
+#include <mbedtls/build_info.h>
+#include <mbedtls/mbedtls_config.h>
+#else
#include <mbedtls/config.h>
+#endif
// override some flags needed by pigweed
#include "configs/config_pigweed_common.h"
diff --git a/third_party/mbedtls/configs/config_pigweed_common.h b/third_party/mbedtls/configs/config_pigweed_common.h
index 94bc08d8e..557b5b19f 100644
--- a/third_party/mbedtls/configs/config_pigweed_common.h
+++ b/third_party/mbedtls/configs/config_pigweed_common.h
@@ -33,5 +33,7 @@
#define MBEDTLS_NO_PLATFORM_ENTROPY
// Error string support for debugging
#define MBEDTLS_ERROR_C
+// This feature requires MBEDTLS_PSA_CRYPTO_C.
+#undef MBEDTLS_LMS_C
#include "mbedtls/check_config.h"
diff --git a/third_party/mcuxpresso/BUILD.bazel b/third_party/mcuxpresso/BUILD.bazel
new file mode 100644
index 000000000..a230f4ca6
--- /dev/null
+++ b/third_party/mcuxpresso/BUILD.bazel
@@ -0,0 +1,29 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "default_mcuxpresso_sdk",
+ # The "default" config is not compatible with any configuration: you can't
+ # build MCUXpresso targets without choosing an SDK.
+ target_compatible_with = ["@platforms//:incompatible"],
+)
diff --git a/third_party/mcuxpresso/mcuxpresso.gni b/third_party/mcuxpresso/mcuxpresso.gni
index 27e597e1c..a89f5e314 100644
--- a/third_party/mcuxpresso/mcuxpresso.gni
+++ b/third_party/mcuxpresso/mcuxpresso.gni
@@ -17,6 +17,9 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
declare_args() {
+ # Location of MCUXpresso SDK directory
+ dir_pw_third_party_mcuxpresso = ""
+
# If compiling a project against an MCUXpresso SDK, this variable can be set
# to the name of the pw_source_set you create using `pw_mcuxpresso_sdk` to
# enable additional Pigweed support.
@@ -66,7 +69,7 @@ template("pw_mcuxpresso_sdk") {
}
_script_args = [
- "project",
+ "gn",
rebase_path(invoker.manifest),
"--prefix=$_sdk_dir",
]
diff --git a/third_party/nanopb/BUILD.gn b/third_party/nanopb/BUILD.gn
index a79221a69..56c0cc70e 100644
--- a/third_party/nanopb/BUILD.gn
+++ b/third_party/nanopb/BUILD.gn
@@ -60,8 +60,56 @@ if (dir_pw_third_party_nanopb != "") {
sources = [ "generate_nanopb_proto.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
mypy_ini = "$dir_pigweed/.mypy.ini"
+
+ # Path to protoc binary. This variable is relative to the build directory,
+ # so it must be rebased as a source-tree-absolute path.
+ _protoc_binary_path =
+ "//" +
+ rebase_path(pw_protobuf_compiler_PROTOC_BINARY, "//", root_build_dir)
+
+ if (host_os == "win") {
+ if (get_path_info(_protoc_binary_path, "extension") != "exe" &&
+ get_path_info(_protoc_binary_path, "extension") != "bat") {
+ _protoc_binary_path += ".exe"
+ }
+ }
+
+ inputs = [
+ "$dir_pw_third_party_nanopb/pb.h",
+ "$dir_pw_third_party_nanopb/pb_common.h",
+ "$dir_pw_third_party_nanopb/pb_decode.h",
+ "$dir_pw_third_party_nanopb/pb_encode.h",
+ "$dir_pw_third_party_nanopb/pb_common.c",
+ "$dir_pw_third_party_nanopb/pb_decode.c",
+ "$dir_pw_third_party_nanopb/pb_encode.c",
+ "$dir_pw_third_party_nanopb/generator/nanopb_generator.py",
+ "$dir_pw_third_party_nanopb/generator/proto/google/protobuf",
+ "$dir_pw_third_party_nanopb/generator/proto/google/protobuf/descriptor.proto",
+ "$dir_pw_third_party_nanopb/generator/proto/__init__.py",
+ "$dir_pw_third_party_nanopb/generator/proto/nanopb.proto",
+ "$dir_pw_third_party_nanopb/generator/proto/_utils.py",
+ "$dir_pw_third_party_nanopb/generator/protoc-gen-nanopb",
+ "$dir_pw_third_party_nanopb/generator/nanopb_generator.py2",
+ "$dir_pw_third_party_nanopb/generator/protoc-gen-nanopb-py2",
+ "$dir_pw_third_party_nanopb/generator/protoc",
+ "$dir_pw_third_party_nanopb/generator/protoc-gen-nanopb.bat",
+ "$dir_pw_third_party_nanopb/generator/protoc.bat",
+ _protoc_binary_path,
+ ]
action = {
- args = [ rebase_path(dir_pw_third_party_nanopb, root_build_dir) ]
+ args = [
+ "--nanopb-root=" +
+ rebase_path(dir_pw_third_party_nanopb, root_build_dir),
+ "--protoc-binary=" + pw_protobuf_compiler_PROTOC_BINARY,
+ ]
+ if (pw_third_party_nanopb_AGGRESSIVE_NANOPB_PB2_REGEN) {
+ args += [ "--aggressive-regen" ]
+ }
+
+ # Nanopb writes a file to:
+ # $dir_pw_third_party_nanopb/generator/proto/nanopb_pb2.py
+ # Since this is in the source tree, we can't track it as an output. 😒
+ # Use a stamp instead.
stamp = true
}
}
diff --git a/third_party/nanopb/CMakeLists.txt b/third_party/nanopb/CMakeLists.txt
index c8461a1d0..b3868f800 100644
--- a/third_party/nanopb/CMakeLists.txt
+++ b/third_party/nanopb/CMakeLists.txt
@@ -50,7 +50,32 @@ pw_proto_library(pw_third_party.nanopb.proto
# Generates nanopb_pb2.py, which is needed to compile protobufs with Nanopb.
add_custom_command(
COMMAND
- python3 "${CMAKE_CURRENT_LIST_DIR}/generate_nanopb_proto.py" "${dir_pw_third_party_nanopb}"
+ python3
+ "${CMAKE_CURRENT_LIST_DIR}/generate_nanopb_proto.py"
+ --nanopb-root "${dir_pw_third_party_nanopb}"
+ --protoc-binary "$ENV{PW_PIGWEED_CIPD_INSTALL_DIR}/bin/protoc"
+ DEPENDS
+ "${CMAKE_CURRENT_LIST_DIR}/generate_nanopb_proto.py"
+ "${dir_pw_third_party_nanopb}/pb.h"
+ "${dir_pw_third_party_nanopb}/pb_common.h"
+ "${dir_pw_third_party_nanopb}/pb_decode.h"
+ "${dir_pw_third_party_nanopb}/pb_encode.h"
+ "${dir_pw_third_party_nanopb}/pb_common.c"
+ "${dir_pw_third_party_nanopb}/pb_decode.c"
+ "${dir_pw_third_party_nanopb}/pb_encode.c"
+ "${dir_pw_third_party_nanopb}/generator/nanopb_generator.py"
+ "${dir_pw_third_party_nanopb}/generator/proto/google/protobuf"
+ "${dir_pw_third_party_nanopb}/generator/proto/google/protobuf/descriptor.proto"
+ "${dir_pw_third_party_nanopb}/generator/proto/__init__.py"
+ "${dir_pw_third_party_nanopb}/generator/proto/nanopb.proto"
+ "${dir_pw_third_party_nanopb}/generator/proto/_utils.py"
+ "${dir_pw_third_party_nanopb}/generator/protoc-gen-nanopb"
+ "${dir_pw_third_party_nanopb}/generator/nanopb_generator.py2"
+ "${dir_pw_third_party_nanopb}/generator/protoc-gen-nanopb-py2"
+ "${dir_pw_third_party_nanopb}/generator/protoc"
+ "${dir_pw_third_party_nanopb}/generator/protoc-gen-nanopb.bat"
+ "${dir_pw_third_party_nanopb}/generator/protoc.bat"
+ "$ENV{PW_PIGWEED_CIPD_INSTALL_DIR}/bin/protoc"
OUTPUT
"${dir_pw_third_party_nanopb}/generator/proto/nanopb_pb2.py"
)
diff --git a/third_party/nanopb/docs.rst b/third_party/nanopb/docs.rst
index 1af7211c3..360d50fb9 100644
--- a/third_party/nanopb/docs.rst
+++ b/third_party/nanopb/docs.rst
@@ -22,7 +22,7 @@ In your toolchain configuration, you can use the following:
.. code-block::
- pw_third_party_nanopb_CONFIG = "$dir_pw_third_party/nanopb:disable_error_messages"
+ pw_third_party_nanopb_CONFIG = "$dir_pw_third_party/nanopb:disable_error_messages"
This will add ``-DPB_NO_ERRMSG=1`` to the build, which disables error messages
diff --git a/third_party/nanopb/generate_nanopb_proto.py b/third_party/nanopb/generate_nanopb_proto.py
index 2934d0635..fc83df024 100644
--- a/third_party/nanopb/generate_nanopb_proto.py
+++ b/third_party/nanopb/generate_nanopb_proto.py
@@ -27,14 +27,28 @@ that nanopb_pb2.py is guaranteed to exist before they need it.
import argparse
import importlib.util
from pathlib import Path
+import os
import sys
-def generate_nanopb_proto(root: Path) -> None:
- sys.path.append(str(root / 'generator'))
+def generate_nanopb_proto(
+ nanopb_root: Path, protoc_binary: Path, aggressive_regen: bool
+) -> None:
+ generated_nanopb_pb2 = nanopb_root / 'generator' / 'proto' / 'nanopb_pb2.py'
+
+ # If protoc was updated, ensure the file is regenerated.
+ if generated_nanopb_pb2.is_file():
+ if (
+ aggressive_regen
+ or protoc_binary.stat().st_mtime
+ > generated_nanopb_pb2.stat().st_mtime
+ ):
+ os.remove(generated_nanopb_pb2)
+
+ sys.path.append(str(nanopb_root / 'generator'))
spec = importlib.util.spec_from_file_location(
- 'proto', root / 'generator' / 'proto' / '__init__.py'
+ 'proto', nanopb_root / 'generator' / 'proto' / '__init__.py'
)
assert spec is not None
proto_module = importlib.util.module_from_spec(spec)
@@ -43,7 +57,26 @@ def generate_nanopb_proto(root: Path) -> None:
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('root', type=Path, help='Nanopb root')
+ parser.add_argument(
+ '--nanopb-root',
+ type=Path,
+ help='Nanopb root',
+ )
+ parser.add_argument(
+ '--protoc-binary',
+ type=Path,
+ help='Protoc binary path',
+ )
+ parser.add_argument(
+ '--aggressive-regen',
+ action="store_true",
+ default=False,
+ help=(
+ 'If true, always regenerates nanopb_pb2.py when this script is '
+ 'run. If this is false, the file is only regenerated when the '
+ 'protoc binary is updated'
+ ),
+ )
return parser.parse_args()
diff --git a/third_party/nanopb/nanopb.gni b/third_party/nanopb/nanopb.gni
index 110debab8..69dee4818 100644
--- a/third_party/nanopb/nanopb.gni
+++ b/third_party/nanopb/nanopb.gni
@@ -25,4 +25,13 @@ declare_args() {
# module. This should point to a source set that provides defines through a
# public config (which may -include a file or add defines directly).
pw_third_party_nanopb_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+
+ # Regenerates `$dir_pw_third_party_nanopb/generator/proto/nanopb_pb2.py`
+ # whenever the `generate_nanopb_proto` action is run. If this is set to false,
+ # the file will only be regenerated if `protoc` is newer than the generated
+ # `nanopb_pb2.py`.
+ #
+ # Aggressive regeneration is NOT safe if this build will run in parallel with
+ # other build systems that try to read `nanopb_pb2.py`.
+ pw_third_party_nanopb_AGGRESSIVE_NANOPB_PB2_REGEN = true
}
diff --git a/third_party/pico_sdk/gn/BUILD.gn b/third_party/pico_sdk/gn/BUILD.gn
index 8234f01c1..eba2bc71e 100644
--- a/third_party/pico_sdk/gn/BUILD.gn
+++ b/third_party/pico_sdk/gn/BUILD.gn
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pi_pico.gni")
+
# These warnings need to be disabled when using strict warnings.
config("disable_warnings") {
cflags = [
@@ -21,3 +23,12 @@ config("disable_warnings") {
]
asmflags = cflags
}
+
+config("disable_elf2uf2_warnings") {
+ cflags = [
+ "-Wno-shadow",
+ "-Wno-reorder",
+ "-Wno-type-limits",
+ ]
+ visibility = [ "${PICO_ROOT}/src:elf2uf2" ]
+}
diff --git a/third_party/pico_sdk/src/BUILD.gn b/third_party/pico_sdk/src/BUILD.gn
index 1b3b302dd..059d3a477 100644
--- a/third_party/pico_sdk/src/BUILD.gn
+++ b/third_party/pico_sdk/src/BUILD.gn
@@ -39,6 +39,7 @@ config("elf2uf2_configs") {
pw_executable("elf2uf2") {
configs = [
":elf2uf2_configs",
+ "${PICO_ROOT}/gn:disable_elf2uf2_warnings",
"${PICO_ROOT}/gn:disable_warnings",
]
sources = [ "$PICO_SRC_DIR/tools/elf2uf2/main.cpp" ]
diff --git a/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn
index 1e7833b3d..e1d4c4d20 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn
@@ -43,5 +43,5 @@ pw_source_set("pico_multicore") {
"${PICO_ROOT}/src/rp2_common/pico_runtime",
]
public = [ "${_CWD}/include/pico/multicore.h" ]
- sources = [ "${_CWD}/pico_multicore.c" ]
+ sources = [ "${_CWD}/multicore.c" ]
}
diff --git a/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn
index 4bb51c5c3..8953dc572 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn
@@ -31,6 +31,7 @@ pw_source_set("tusb_config") {
visibility = [
":pico_stdio_usb",
"${PICO_ROOT}/src/rp2_common/tinyusb",
+ "${PICO_ROOT}/src/rp2_common/tinyusb:bsp",
]
}
diff --git a/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn b/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn
index fa1ee3360..ad5115032 100644
--- a/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn
@@ -45,15 +45,20 @@ pw_source_set("bsp") {
"${PICO_ROOT}/src/common/pico_stdlib:public_include_dirs",
"${PICO_ROOT}/src/rp2_common/pico_stdio_usb:public_include_dirs",
]
+ public_configs = [ ":tinyusb_defines" ]
include_dirs = [ "${PICO_SRC_DIR}/lib/tinyusb/hw" ]
deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_binary_info",
- "${PICO_ROOT}/src/common/pico_time",
"${PICO_ROOT}/src/rp2_common/hardware_gpio",
"${PICO_ROOT}/src/rp2_common/hardware_sync",
"${PICO_ROOT}/src/rp2_common/hardware_uart",
"${PICO_ROOT}/src/rp2_common/pico_stdio:headers",
+ "${PICO_ROOT}/src/rp2_common/pico_stdio_usb:tusb_config",
+ ]
+ public_deps = [
+ "${PICO_ROOT}/src/common/pico_sync",
+ "${PICO_ROOT}/src/common/pico_time",
]
sources = [
"${PICO_SRC_DIR}/lib/tinyusb/hw/bsp/board.h",
diff --git a/third_party/re2/BUILD.gn b/third_party/re2/BUILD.gn
new file mode 100644
index 000000000..7dbbbc4a4
--- /dev/null
+++ b/third_party/re2/BUILD.gn
@@ -0,0 +1,89 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/re2/re2.gni")
+
+if (dir_pw_third_party_re2 != "") {
+ config("re2_public_config1") {
+ include_dirs = [ "$dir_pw_third_party_re2" ]
+ }
+
+ # Generated from //:re2
+ pw_source_set("re2") {
+ public = [
+ "$dir_pw_third_party_re2/re2/filtered_re2.h",
+ "$dir_pw_third_party_re2/re2/re2.h",
+ "$dir_pw_third_party_re2/re2/set.h",
+ "$dir_pw_third_party_re2/re2/stringpiece.h",
+ ]
+ sources = [
+ "$dir_pw_third_party_re2/re2/bitmap256.cc",
+ "$dir_pw_third_party_re2/re2/bitmap256.h",
+ "$dir_pw_third_party_re2/re2/bitstate.cc",
+ "$dir_pw_third_party_re2/re2/compile.cc",
+ "$dir_pw_third_party_re2/re2/dfa.cc",
+ "$dir_pw_third_party_re2/re2/filtered_re2.cc",
+ "$dir_pw_third_party_re2/re2/mimics_pcre.cc",
+ "$dir_pw_third_party_re2/re2/nfa.cc",
+ "$dir_pw_third_party_re2/re2/onepass.cc",
+ "$dir_pw_third_party_re2/re2/parse.cc",
+ "$dir_pw_third_party_re2/re2/perl_groups.cc",
+ "$dir_pw_third_party_re2/re2/pod_array.h",
+ "$dir_pw_third_party_re2/re2/prefilter.cc",
+ "$dir_pw_third_party_re2/re2/prefilter.h",
+ "$dir_pw_third_party_re2/re2/prefilter_tree.cc",
+ "$dir_pw_third_party_re2/re2/prefilter_tree.h",
+ "$dir_pw_third_party_re2/re2/prog.cc",
+ "$dir_pw_third_party_re2/re2/prog.h",
+ "$dir_pw_third_party_re2/re2/re2.cc",
+ "$dir_pw_third_party_re2/re2/regexp.cc",
+ "$dir_pw_third_party_re2/re2/regexp.h",
+ "$dir_pw_third_party_re2/re2/set.cc",
+ "$dir_pw_third_party_re2/re2/simplify.cc",
+ "$dir_pw_third_party_re2/re2/sparse_array.h",
+ "$dir_pw_third_party_re2/re2/sparse_set.h",
+ "$dir_pw_third_party_re2/re2/stringpiece.cc",
+ "$dir_pw_third_party_re2/re2/tostring.cc",
+ "$dir_pw_third_party_re2/re2/unicode_casefold.cc",
+ "$dir_pw_third_party_re2/re2/unicode_casefold.h",
+ "$dir_pw_third_party_re2/re2/unicode_groups.cc",
+ "$dir_pw_third_party_re2/re2/unicode_groups.h",
+ "$dir_pw_third_party_re2/re2/walker-inl.h",
+ "$dir_pw_third_party_re2/util/logging.h",
+ "$dir_pw_third_party_re2/util/mix.h",
+ "$dir_pw_third_party_re2/util/mutex.h",
+ "$dir_pw_third_party_re2/util/rune.cc",
+ "$dir_pw_third_party_re2/util/strutil.cc",
+ "$dir_pw_third_party_re2/util/strutil.h",
+ "$dir_pw_third_party_re2/util/utf.h",
+ "$dir_pw_third_party_re2/util/util.h",
+ ]
+ cflags = [ "-pthread" ]
+ ldflags = [ "-pthread" ]
+ public_configs = [ ":re2_public_config1" ]
+ configs = [ "configs:internal_disabled_warnings" ]
+ remove_configs = [ "$dir_pw_fuzzer:instrumentation" ]
+ }
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/re2/OWNERS b/third_party/re2/OWNERS
new file mode 100644
index 000000000..419cfd37a
--- /dev/null
+++ b/third_party/re2/OWNERS
@@ -0,0 +1 @@
+aarongreen@google.com
diff --git a/third_party/re2/configs/BUILD.gn b/third_party/re2/configs/BUILD.gn
new file mode 100644
index 000000000..61c6bf13a
--- /dev/null
+++ b/third_party/re2/configs/BUILD.gn
@@ -0,0 +1,49 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_third_party/re2/re2.gni")
+
+# Targets that include RE2 headers need to include this config.
+config("disabled_warnings") {
+ cflags = [
+ "-Wno-dtor-name",
+ "-Wno-gnu-anonymous-struct",
+ "-Wno-nested-anon-types",
+ ]
+}
+
+# This config should only be used to build the RE2 library itself.
+config("internal_disabled_warnings") {
+ cflags = [
+ "-Wno-c99-extensions",
+ "-Wno-cast-qual",
+ "-Wno-dtor-name",
+ "-Wno-gnu-anonymous-struct",
+ "-Wno-nested-anon-types",
+ "-Wno-shadow",
+ "-Wno-switch-enum",
+ "-Wno-unused-parameter",
+ ]
+}
+
+# Include path for RE2.
+#
+# This is needed as the library is used to build FuzzTest in a dedicated
+# toolchain, and `public_configs` do not propagate across toolchain boundaries
+# by default.
+config("public_include_path") {
+ include_dirs = [ dir_pw_third_party_re2 ]
+}
diff --git a/third_party/re2/docs.rst b/third_party/re2/docs.rst
new file mode 100644
index 000000000..d3cb4cbb3
--- /dev/null
+++ b/third_party/re2/docs.rst
@@ -0,0 +1,56 @@
+.. _module-pw_third_party_re2:
+
+===
+RE2
+===
+The ``$dir_pw_third_party/re2/`` module provides build files to allow
+optionally including upstream RE2.
+
+.. _module-pw_third_party_re2-using_upstream:
+
+------------------
+Using upstream RE2
+------------------
+If you want to use RE2, you must do the following:
+
+Submodule
+=========
+Add RE2 to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add https://github.com/google/re2.git \
+ third_party/re2/src
+
+GN
+==
+* Set the GN var ``dir_pw_third_party_re2`` to the location of the
+ RE2 source.
+
+ If you used the command above, this will be
+ ``//third_party/re2/src``
+
+ This can be set in your args.gn or .gn file like:
+ ``dir_pw_third_party_re2 = "//third_party/re2/src"``
+
+Updating
+========
+The GN build files are generated from the third-party Bazel build files using
+$dir_pw_build/py/pw_build/generate_3p_gn.py.
+
+The script uses data taken from ``$dir_pw_third_party/re2/repo.json``.
+
+The script should be re-run whenever the submodule is updated or the JSON file
+is modified. Specify the location of the Bazel repository can be specified using
+the ``-w`` option, e.g.
+
+.. code-block:: sh
+
+ python pw_build/py/pw_build/generate_3p_gn.py \
+ -w third_party/re2/src
+
+Version
+=======
+The update script was last run for revision `c9cba76`_.
+
+.. _c9cba76: https://github.com/google/re2/tree/c9cba76063cf4235c1a15dd14a24a4ef8d623761
diff --git a/pw_tokenizer/py/setup.py b/third_party/re2/re2.gni
index fd5c1e6f7..faf5924ca 100644
--- a/pw_tokenizer/py/setup.py
+++ b/third_party/re2/re2.gni
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
@@ -11,12 +11,13 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""The pw_tokenizer package.
-Installing pw_tokenizer with this setup.py does not include the
-pw_tokenizer.proto package, since it contains a generated protobuf module. To
-access pw_tokenizer.proto, install pw_tokenizer from GN."""
+# DO NOT MANUALLY EDIT!
+# This file was automatically generated by pw_build/gn_writer.py
-import setuptools # type: ignore
-
-setuptools.setup() # Package definition in setup.cfg
+declare_args() {
+ # If compiling tests with RE2, this variable is set to the path to the RE2
+ # installation. When set, a pw_source_set for the RE2 library is created at
+ # "$dir_pw_third_party/re2".
+ dir_pw_third_party_re2 = ""
+}
diff --git a/third_party/re2/repo.json b/third_party/re2/repo.json
new file mode 100644
index 000000000..e33bf0839
--- /dev/null
+++ b/third_party/re2/repo.json
@@ -0,0 +1,5 @@
+{
+ "name": "RE2",
+ "add": [ "$dir_pw_third_party/re2/configs:internal_disabled_warnings" ],
+ "remove": [ "$dir_pw_fuzzer:instrumentation" ]
+}
diff --git a/third_party/rules_proto_grpc/OWNERS b/third_party/rules_proto_grpc/OWNERS
deleted file mode 100644
index 68efc833c..000000000
--- a/third_party/rules_proto_grpc/OWNERS
+++ /dev/null
@@ -1 +0,0 @@
-tpudlik@google.com
diff --git a/third_party/rules_proto_grpc/internal_proto.bzl b/third_party/rules_proto_grpc/internal_proto.bzl
deleted file mode 100644
index cf997aa7a..000000000
--- a/third_party/rules_proto_grpc/internal_proto.bzl
+++ /dev/null
@@ -1,348 +0,0 @@
-# Copyright 2022 The Pigweed Authors
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-"""Backend implementation for 'pw_protobuf_compiler/proto.bzl'"""
-
-# Apache License, Version 2.0, January 2004, http://www.apache.org/licenses/
-# Adapted from: https://github.com/rules-proto-grpc/rules_proto_grpc/
-# Files adapted:
-# - rules_proto_grpc/cpp/cpp_grpc_library.bzl
-# - rules_proto_grpc/cpp/cpp_grpc_compile.bzl
-# These two files have been adapted for use in Pigweed and combined into this
-# file.
-
-load("@rules_proto//proto:defs.bzl", "ProtoInfo")
-load(
- "@rules_proto_grpc//:defs.bzl",
- "ProtoLibraryAspectNodeInfo",
- "ProtoPluginInfo",
- "proto_compile_aspect_attrs",
- "proto_compile_aspect_impl",
- "proto_compile_attrs",
- "proto_compile_impl",
-)
-load("@rules_proto_grpc//internal:filter_files.bzl", "filter_files")
-
-def pwpb_proto_library(name, deps, **kwargs):
- """A C++ proto library generated using pw_protobuf.
-
- Attributes:
- deps: proto_library targets for which to generate this library.
- """
- _pw_proto_library(
- name = name,
- compiler = pwpb_compile,
- deps = deps,
- cc_deps = ["@pigweed//pw_protobuf"],
- has_srcs = False,
- extra_tags = [],
- **kwargs
- )
-
-def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, **kwargs):
- """A pwpb_rpc proto library target.
-
- Attributes:
- deps: proto_library targets for which to generate this library.
- pwpb_proto_library_deps: A pwpb_proto_library generated
- from the same proto_library. Required.
- """
- _pw_proto_library(
- name = name,
- compiler = pwpb_rpc_compile,
- deps = deps,
- cc_deps = [
- "@pigweed//pw_protobuf",
- "@pigweed//pw_rpc/pwpb:server_api",
- "@pigweed//pw_rpc/pwpb:client_api",
- "@pigweed//pw_rpc",
- ] + pwpb_proto_library_deps,
- has_srcs = False,
- extra_tags = [],
- **kwargs
- )
-
-def raw_rpc_proto_library(name, deps, **kwargs):
- """A raw C++ RPC proto library."""
- _pw_proto_library(
- name = name,
- compiler = raw_rpc_compile,
- deps = deps,
- cc_deps = [
- "@pigweed//pw_rpc",
- "@pigweed//pw_rpc/raw:server_api",
- "@pigweed//pw_rpc/raw:client_api",
- ],
- has_srcs = False,
- extra_tags = [],
- **kwargs
- )
-
-def nanopb_proto_library(name, deps, **kwargs):
- """A C++ proto library generated using nanopb."""
- _pw_proto_library(
- name = name,
- compiler = nanopb_compile,
- deps = deps,
- cc_deps = [
- "@com_github_nanopb_nanopb//:nanopb",
- ],
- has_srcs = True,
- # TODO(tpudlik): Find a way to get Nanopb to generate nested structs.
- # Otherwise add the manual tag to the resulting library, preventing it
- # from being built unless directly depended on. e.g. The 'Pigweed'
- # message in
- # pw_protobuf/pw_protobuf_test_protos/full_test.proto will fail to
- # compile as it has a self referring nested message. According to
- # the docs
- # https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
- # and https://github.com/nanopb/nanopb/issues/433 it seams like it
- # should be possible to configure nanopb to generate nested structs.
- extra_tags = ["manual"],
- **kwargs
- )
-
-def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, **kwargs):
- """A C++ RPC proto library using nanopb.
-
- Attributes:
- deps: proto_library targets for which to generate this library.
- nanopb_proto_library_deps: A pw_nanopb_cc_library generated
- from the same proto_library. Required.
- """
- _pw_proto_library(
- name = name,
- compiler = nanopb_rpc_compile,
- deps = deps,
- cc_deps = [
- "@com_github_nanopb_nanopb//:nanopb",
- "@pigweed//pw_rpc/nanopb:server_api",
- "@pigweed//pw_rpc/nanopb:client_api",
- "@pigweed//pw_rpc",
- ] + nanopb_proto_library_deps,
- has_srcs = True,
- # TODO(tpudlik): See nanopb_proto_library.
- extra_tags = ["manual"],
- **kwargs
- )
-
-# Create compile rule
-def _proto_compiler_aspect(plugin_group, prefix):
- return aspect(
- implementation = proto_compile_aspect_impl,
- provides = [ProtoLibraryAspectNodeInfo],
- attr_aspects = ["deps"],
- attrs = dict(
- proto_compile_aspect_attrs,
- _plugins = attr.label_list(
- doc = "List of protoc plugins to apply",
- providers = [ProtoPluginInfo],
- default = plugin_group,
- ),
- _prefix = attr.string(
- doc = "String used to disambiguate aspects when generating \
-outputs",
- default = prefix,
- ),
- ),
- toolchains = [str(Label("@rules_proto_grpc//protobuf:toolchain_type"))],
- )
-
-def _proto_compiler_rule(plugin_group, aspect):
- return rule(
- implementation = proto_compile_impl,
- attrs = dict(
- proto_compile_attrs,
- _plugins = attr.label_list(
- doc = "List of protoc plugins to apply",
- providers = [ProtoPluginInfo],
- default = plugin_group,
- ),
- protos = attr.label_list(
- providers = [ProtoInfo],
- doc = "List of proto_library targets.",
- ),
- deps = attr.label_list(
- doc = "List of proto_library targets. Prefer protos.",
- aspects = [aspect],
- ),
- ),
- toolchains = [str(Label("@rules_proto_grpc//protobuf:toolchain_type"))],
- )
-
-nanopb_compile_aspect = _proto_compiler_aspect(
- [Label("//pw_rpc:nanopb_plugin")],
- "nanopb_proto_compile_aspect",
-)
-nanopb_compile = _proto_compiler_rule(
- [Label("//pw_rpc:nanopb_plugin")],
- nanopb_compile_aspect,
-)
-
-pwpb_compile_aspect = _proto_compiler_aspect(
- [Label("@pigweed//pw_protobuf:pw_cc_plugin")],
- "pwpb_proto_compile_aspect",
-)
-pwpb_compile = _proto_compiler_rule(
- [Label("@pigweed//pw_protobuf:pw_cc_plugin")],
- pwpb_compile_aspect,
-)
-
-pwpb_rpc_compile_aspect = _proto_compiler_aspect(
- [
- Label("@pigweed//pw_rpc:pw_cc_plugin_pwpb_rpc"),
- Label("@pigweed//pw_protobuf:pw_cc_plugin"),
- ],
- "pwpb_rpc_proto_compile_aspect",
-)
-pwpb_rpc_compile = _proto_compiler_rule(
- [
- Label("@pigweed//pw_rpc:pw_cc_plugin_pwpb_rpc"),
- Label("@pigweed//pw_protobuf:pw_cc_plugin"),
- ],
- pwpb_rpc_compile_aspect,
-)
-
-raw_rpc_compile_aspect = _proto_compiler_aspect(
- [Label("@pigweed//pw_rpc:pw_cc_plugin_raw")],
- "raw_rpc_proto_compile_aspect",
-)
-raw_rpc_compile = _proto_compiler_rule(
- [Label("@pigweed//pw_rpc:pw_cc_plugin_raw")],
- raw_rpc_compile_aspect,
-)
-
-nanopb_rpc_compile_aspect = _proto_compiler_aspect(
- [
- Label("@pigweed//pw_rpc:pw_cc_plugin_nanopb_rpc"),
- Label("//pw_rpc:nanopb_plugin"),
- ],
- "nanopb_rpc_proto_compile_aspect",
-)
-nanopb_rpc_compile = _proto_compiler_rule(
- [
- Label("@pigweed//pw_rpc:pw_cc_plugin_nanopb_rpc"),
- Label("//pw_rpc:nanopb_plugin"),
- ],
- nanopb_rpc_compile_aspect,
-)
-
-def _pw_proto_library(name, compiler, deps, cc_deps, has_srcs, extra_tags, **kwargs):
- name_pb = name + ".pb"
- additional_tags = [
- tag
- for tag in extra_tags
- if tag not in kwargs.get("tags", [])
- ]
- compiler(
- name = name_pb,
- tags = additional_tags,
- # Forward deps and verbose tags to implementation
- verbose = kwargs.get("verbose", 0),
- deps = deps,
- protos = kwargs.get("protos", []),
- )
-
- # Filter files to sources and headers
- filter_files(
- name = name_pb + "_srcs",
- target = name_pb,
- extensions = ["c", "cc", "cpp", "cxx"],
- tags = additional_tags,
- )
-
- filter_files(
- name = name_pb + "_hdrs",
- target = name_pb,
- extensions = ["h"],
- tags = additional_tags,
- )
-
- # Cannot use pw_cc_library here as it will add cxxopts.
- # Note that the srcs attribute here is passed in as a DefaultInfo
- # object, which is not supported by pw_cc_library.
- native.cc_library(
- name = name,
- hdrs = [name_pb + "_hdrs"],
- includes = [name_pb],
- alwayslink = kwargs.get("alwayslink"),
- copts = kwargs.get("copts", []),
- defines = kwargs.get("defines", []),
- srcs = [name_pb + "_srcs"] if has_srcs else [],
- deps = cc_deps,
- linkopts = kwargs.get("linkopts", []),
- linkstatic = kwargs.get("linkstatic", True),
- local_defines = kwargs.get("local_defines", []),
- nocopts = kwargs.get("nocopts", ""),
- visibility = kwargs.get("visibility"),
- tags = kwargs.get("tags", []) + additional_tags,
- )
-
-def pw_proto_library(name, **kwargs): # buildifier: disable=function-docstring
- """Generate all the Pigweed proto library targets.
-
- Args:
- name: Name of this proto library.
- **kwargs: Forwarded to wrapped native.cc_library.
-
- Deprecated: This macro is deprecated and will be removed in a future
- Pigweed version. Please use one of the single-target macros above.
- """
-
- pwpb_proto_library(
- name = name + ".pwpb",
- **kwargs
- )
-
- pwpb_rpc_proto_library(
- name = name + ".pwpb_rpc",
- pwpb_proto_library_deps = [name + ".pwpb"],
- **kwargs
- )
-
- raw_rpc_proto_library(
- name = name + ".raw_rpc",
- **kwargs
- )
-
- nanopb_proto_library(
- name = name + ".nanopb",
- **kwargs
- )
-
- nanopb_rpc_proto_library(
- name = name + ".nanopb_rpc",
- nanopb_proto_library_deps = [name + ".nanopb"],
- **kwargs
- )
-
- if "manual" in kwargs.get("tags", []):
- additional_tags = []
- else:
- additional_tags = ["manual"]
-
- # Combine all plugins into a single library.
- native.cc_library(
- name = name,
- deps = [
- name + "." + plugin_name
- for plugin_name in ["pwpb", "pwpb_rpc", "raw_rpc", "nanopb", "nanopb_rpc"]
- ],
- tags = kwargs.get("tags", []) + additional_tags,
- **{
- k: v
- for k, v in kwargs.items()
- if k not in ["deps", "protos", "tags"]
- }
- )
diff --git a/third_party/stm32cube/BUILD.bazel b/third_party/stm32cube/BUILD.bazel
index 81a3ff78c..94e3537f4 100644
--- a/third_party/stm32cube/BUILD.bazel
+++ b/third_party/stm32cube/BUILD.bazel
@@ -21,10 +21,24 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+# NOTE: To depend on this target, you must set the STM32CUBE_HEADER
+# preprocessor variable, perhaps using copts associated with the target
+# platform. See the module documentation for details.
pw_cc_library(
name = "stm32cube",
hdrs = [
"public/stm32cube/init.h",
"public/stm32cube/stm32cube.h",
],
+ includes = ["public"],
+ deps = [
+ ":hal_driver",
+ ],
+)
+
+# This label_flag introduces a layer of indirection useful when building a
+# project that requires more than one STM32Cube MCU Package.
+label_flag(
+ name = "hal_driver",
+ build_setting_default = "@hal_driver",
)
diff --git a/third_party/stm32cube/BUILD.gn b/third_party/stm32cube/BUILD.gn
index da8c1373e..cdfde7393 100644
--- a/third_party/stm32cube/BUILD.gn
+++ b/third_party/stm32cube/BUILD.gn
@@ -117,7 +117,7 @@ if (dir_pw_third_party_stm32cube == "") {
inputs = [ "$dir_pw_third_party_stm32cube/hal_driver/Inc/${files.family}_hal_conf_template.h" ]
}
- config("flags") {
+ config("header_flags") {
cflags = [ "-Wno-unused-parameter" ]
cflags_c = [
"-Wno-redundant-decls",
@@ -126,6 +126,8 @@ if (dir_pw_third_party_stm32cube == "") {
"-Wno-implicit-function-declaration",
"-Wno-switch-enum",
]
+
+ # TODO: b/301262374 - Provide a better way to detect the compiler type.
if (get_path_info(pw_toolchain_SCOPE.cc, "file") == "clang") {
cflags += [ "-Wno-deprecated-volatile" ]
cflags_c += [ "-Wno-parentheses-equality" ]
@@ -146,6 +148,14 @@ if (dir_pw_third_party_stm32cube == "") {
visibility = [ ":*" ]
}
+ config("sources_flags") {
+ if (get_path_info(pw_toolchain_SCOPE.cc, "file") == "clang") {
+ cflags_c = [ "-Wno-unused-but-set-variable" ]
+ }
+
+ visibility = [ ":*" ]
+ }
+
config("public_include_paths") {
include_dirs = files.include_dirs
include_dirs += [ "public" ]
@@ -156,7 +166,7 @@ if (dir_pw_third_party_stm32cube == "") {
# this. If you just want to depend on the hal, depend on stm32cube directly.
pw_source_set("stm32cube_headers") {
public_configs = [
- ":flags",
+ ":header_flags",
":public_include_paths",
]
public = [
@@ -172,6 +182,7 @@ if (dir_pw_third_party_stm32cube == "") {
}
pw_source_set("stm32cube") {
+ configs = [ ":sources_flags" ]
public_deps = [ ":stm32cube_headers" ]
sources = files.sources
deps = [
diff --git a/third_party/stm32cube/cmsis_core.BUILD.bazel b/third_party/stm32cube/cmsis_core.BUILD.bazel
new file mode 100644
index 000000000..083434f80
--- /dev/null
+++ b/third_party/stm32cube/cmsis_core.BUILD.bazel
@@ -0,0 +1,30 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# BUILD.bazel file for cmsis_core.
+
+# buildifier: disable=module-docstring
+cc_library(
+ name = "cmsis_core",
+ hdrs = glob(
+ [
+ "Include/*.h",
+ "Include/DSP/Include/*.h",
+ ],
+ ),
+ includes = [
+ "Include",
+ "Include/DSP/Include",
+ ],
+)
diff --git a/third_party/stm32cube/cmsis_device.BUILD.bazel b/third_party/stm32cube/cmsis_device.BUILD.bazel
new file mode 100644
index 000000000..9911787cf
--- /dev/null
+++ b/third_party/stm32cube/cmsis_device.BUILD.bazel
@@ -0,0 +1,46 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# BUILD.bazel file for cmsis_device_xxx.
+
+# buildifier: disable=module-docstring
+# Should point to the corresponding cmsis_core library. Hidden behind a label
+# flag so that it can be overriden in projects that build for more than one
+# family of STM processors.
+label_flag(
+ name = "cmsis_core",
+ build_setting_default = "@cmsis_core",
+)
+
+cc_library(
+ name = "default_cmsis_init",
+ srcs = glob(["Source/Templates/system_*.c"]),
+ deps = [":cmsis_device"],
+)
+
+cc_library(
+ name = "cmsis_device",
+ hdrs = glob(
+ [
+ "Include/*.h",
+ ],
+ ),
+ includes = ["Include"],
+ defines = [
+ "__ARMCC_VERSION=0",
+ ],
+ deps = [
+ ":cmsis_core",
+ ],
+)
diff --git a/third_party/stm32cube/public/stm32cube/init.h b/third_party/stm32cube/public/stm32cube/init.h
index 2e79f617f..ec2ec5114 100644
--- a/third_party/stm32cube/public/stm32cube/init.h
+++ b/third_party/stm32cube/public/stm32cube/init.h
@@ -20,4 +20,4 @@ PW_EXTERN_C_START
void pw_stm32cube_Init(void);
-PW_EXTERN_C_END \ No newline at end of file
+PW_EXTERN_C_END
diff --git a/third_party/stm32cube/stm32_hal_driver.BUILD.bazel b/third_party/stm32cube/stm32_hal_driver.BUILD.bazel
new file mode 100644
index 000000000..8bceedfce
--- /dev/null
+++ b/third_party/stm32cube/stm32_hal_driver.BUILD.bazel
@@ -0,0 +1,117 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+# BUILD.bazel file for stm32xxx_hal_driver.
+
+# buildifier: disable=module-docstring
+# Must point to a cc_library exposing a header named stm32f4xx_hal_conf.h (or
+# similar for other families) that contains the HAL configuration
+label_flag(
+ name = "hal_config",
+ build_setting_default = ":unspecified",
+)
+
+# May point to a non-default implementation of timebase.
+label_flag(
+ name = "timebase",
+ build_setting_default = ":default_timebase",
+)
+
+# Should point to the corresponding cmsis_device library. Hidden behind a label
+# flag so that it can be overriden in projects that build for more than one
+# family of STM processors.
+label_flag(
+ name = "cmsis_device",
+ build_setting_default = "@cmsis_device",
+)
+
+label_flag(
+ name = "cmsis_init",
+ build_setting_default = "@cmsis_device//:default_cmsis_init",
+)
+
+# Special target used as a default value of the hal_config label_flag. It's not
+# compatible with any platform: To use Pigweed's STM32Cube integration, you
+# must provide a hal_config.
+cc_library(
+ name = "unspecified",
+ target_compatible_with = ["@platforms//:incompatible"],
+)
+
+cc_library(
+ name = "default_timebase",
+ srcs = glob(["Src/*_hal_timebase_tim_template.c"]),
+ deps = [":hal_driver_without_timebase"],
+)
+
+_DISABLED_WARNINGS = [
+ "-Wno-unused-parameter",
+ "-Wno-redundant-decls",
+ "-Wno-sign-compare",
+ "-Wno-undef",
+ "-Wno-implicit-function-declaration",
+ "-Wno-switch-enum",
+]
+
+cc_library(
+ name = "hal_driver",
+ deps = [
+ ":hal_driver_without_timebase",
+ ":timebase",
+ ],
+ copts = _DISABLED_WARNINGS,
+)
+
+cc_library(
+ name = "hal_driver_without_timebase",
+ srcs = glob(
+ [
+ "Src/*.c",
+ "Src/Legacy/*.c",
+ ],
+ exclude = ["Src/*_template.c"],
+ ),
+ deps = [
+ ":cmsis_device",
+ ":cmsis_init",
+ ":hal_headers",
+ ],
+ copts = _DISABLED_WARNINGS,
+)
+
+cc_library(
+ name = "hal_headers",
+ hdrs = glob(
+ [
+ "Inc/*.h",
+ "Inc/Legacy/*.h",
+ ],
+ exclude = [
+ # Excluded because implementers may want to override this template.
+ "Inc/*_hal_conf_template.h",
+ ],
+ ),
+ includes = [
+ "Inc",
+ "Inc/Legacy",
+ ],
+ deps = [
+ ":cmsis_device",
+ ":hal_config",
+ ],
+ defines = [
+ "USE_HAL_DRIVER",
+ ],
+ copts = _DISABLED_WARNINGS,
+)
diff --git a/ts/buildprotos.ts b/ts/buildprotos.ts
index 5f76e996d..198e74fa3 100644
--- a/ts/buildprotos.ts
+++ b/ts/buildprotos.ts
@@ -12,20 +12,24 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {exec, ExecException} from 'child_process';
+import { exec, ExecException } from 'child_process';
import fs from 'fs';
const run = function (executable: string, args: string[]) {
- console.log(args)
- return new Promise<void>(resolve => {
- exec(`${executable} ${args.join(" ")}`, {cwd: process.cwd()}, (error: ExecException | null, stdout: string | Buffer) => {
- if (error) {
- throw error;
- }
+ console.log(args);
+ return new Promise<void>((resolve) => {
+ exec(
+ `${executable} ${args.join(' ')}`,
+ { cwd: process.cwd() },
+ (error: ExecException | null, stdout: string | Buffer) => {
+ if (error) {
+ throw error;
+ }
- console.log(stdout);
- resolve();
- });
+ console.log(stdout);
+ resolve();
+ },
+ );
});
};
@@ -40,26 +44,29 @@ const protos = [
'pw_rpc/ts/test2.proto',
'pw_rpc/internal/packet.proto',
'pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test.proto',
- 'pw_protobuf_compiler/pw_protobuf_compiler_protos/test.proto'
+ 'pw_protobuf_compiler/pw_protobuf_compiler_protos/test.proto',
];
// Replace these import statements so they are actual paths to proto files.
const remapImports = {
- 'pw_protobuf_protos/common.proto': 'pw_protobuf/pw_protobuf_protos/common.proto',
- 'pw_tokenizer/proto/options.proto': 'pw_tokenizer/options.proto'
-}
+ 'pw_protobuf_protos/common.proto':
+ 'pw_protobuf/pw_protobuf_protos/common.proto',
+ 'pw_tokenizer/proto/options.proto': 'pw_tokenizer/options.proto',
+};
// Only modify the .proto files when running this builder and then restore any
// modified .proto files to their original states after the builder has finished
// running.
-let restoreProtoList = [];
+const restoreProtoList = [];
protos.forEach((protoPath) => {
const protoData = fs.readFileSync(protoPath, 'utf-8');
let newProtoData = protoData;
Object.keys(remapImports).forEach((remapImportFrom) => {
if (protoData.indexOf(`import "${remapImportFrom}"`) !== -1) {
- newProtoData = newProtoData
- .replaceAll(remapImportFrom, remapImports[remapImportFrom]);
+ newProtoData = newProtoData.replaceAll(
+ remapImportFrom,
+ remapImports[remapImportFrom],
+ );
}
});
if (protoData !== newProtoData) {
@@ -68,14 +75,13 @@ protos.forEach((protoPath) => {
}
});
-run('ts-node', [
- `./pw_protobuf_compiler/ts/build.ts`,
- `--out dist/protos`
-].concat(
- protos.map(proto => `-p ${proto}`)
-))
- .then(() => {
- restoreProtoList.forEach((restoreProtoData) => {
- fs.writeFileSync(restoreProtoData[0], restoreProtoData[1]);
- });
+run(
+ 'ts-node',
+ [`./pw_protobuf_compiler/ts/build.ts`, `--out dist/protos`].concat(
+ protos.map((proto) => `-p ${proto}`),
+ ),
+).then(() => {
+ restoreProtoList.forEach((restoreProtoData) => {
+ fs.writeFileSync(restoreProtoData[0], restoreProtoData[1]);
});
+});
diff --git a/ts/device/index.ts b/ts/device/index.ts
index 325007039..825ffe743 100644
--- a/ts/device/index.ts
+++ b/ts/device/index.ts
@@ -12,24 +12,24 @@
// License for the specific language governing permissions and limitations under
// the License.
-import objectPath from 'object-path';
-import {Decoder, Encoder} from 'pigweedjs/pw_hdlc';
+import { setPathOnObject } from './object_set';
+import { Decoder, Encoder } from 'pigweedjs/pw_hdlc';
import {
Client,
Channel,
ServiceClient,
UnaryMethodStub,
MethodStub,
- ServerStreamingMethodStub
+ ServerStreamingMethodStub,
} from 'pigweedjs/pw_rpc';
-import {WebSerialTransport} from '../transport/web_serial_transport';
-import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
+import { WebSerialTransport } from '../transport/web_serial_transport';
+import { ProtoCollection } from 'pigweedjs/pw_protobuf_compiler';
-function protoFieldToMethodName(string) {
- return string.split("_").map(titleCase).join("");
+function protoFieldToMethodName(fieldName: string) {
+ return fieldName.split('_').map(titleCase).join('');
}
-function titleCase(string) {
- return string.charAt(0).toUpperCase() + string.slice(1);
+function titleCase(title: string) {
+ return title.charAt(0).toUpperCase() + title.slice(1);
}
export class Device {
@@ -40,12 +40,13 @@ export class Device {
private rpcAddress: number;
private nameToMethodArgumentsMap: any;
client: Client;
- rpcs: any
+ rpcs: any;
constructor(
protoCollection: ProtoCollection,
transport: WebSerialTransport = new WebSerialTransport(),
- rpcAddress: number = 82) {
+ rpcAddress = 82,
+ ) {
this.transport = transport;
this.rpcAddress = rpcAddress;
this.protoCollection = protoCollection;
@@ -56,9 +57,9 @@ export class Device {
new Channel(1, (bytes) => {
const hdlcBytes = this.encoder.uiFrame(this.rpcAddress, bytes);
this.transport.sendChunk(hdlcBytes);
- })];
- this.client =
- Client.fromProtoSet(channels, this.protoCollection);
+ }),
+ ];
+ this.client = Client.fromProtoSet(channels, this.protoCollection);
this.setupRpcs();
}
@@ -75,34 +76,39 @@ export class Device {
});
}
- getMethodArguments(fullPath) {
+ getMethodArguments(fullPath: string) {
return this.nameToMethodArgumentsMap[fullPath];
}
private setupRpcs() {
- let rpcMap = {};
- let channel = this.client.channel();
- let servicesKeys = Array.from(channel.services.keys());
+ const rpcMap = {};
+ const channel = this.client.channel()!;
+ const servicesKeys = Array.from(channel.services.keys());
servicesKeys.forEach((serviceKey) => {
- objectPath.set(rpcMap, serviceKey,
- this.mapServiceMethods(channel.services.get(serviceKey))
+ setPathOnObject(
+ rpcMap,
+ serviceKey,
+ this.mapServiceMethods(channel.services.get(serviceKey)!),
);
});
this.rpcs = rpcMap;
}
private mapServiceMethods(service: ServiceClient) {
- let methodMap = {};
- let methodKeys = Array.from(service.methodsByName.keys());
+ const methodMap: { [index: string]: any } = {};
+ const methodKeys = Array.from(service.methodsByName.keys());
methodKeys
- .filter((method: any) =>
- service.methodsByName.get(method) instanceof UnaryMethodStub
- || service.methodsByName.get(method) instanceof ServerStreamingMethodStub)
- .forEach(key => {
- let fn = this.createMethodWrapper(
- service.methodsByName.get(key),
+ .filter(
+ (method: any) =>
+ service.methodsByName.get(method) instanceof UnaryMethodStub ||
+ service.methodsByName.get(method) instanceof
+ ServerStreamingMethodStub,
+ )
+ .forEach((key) => {
+ const fn = this.createMethodWrapper(
+ service.methodsByName.get(key)!,
key,
- `${service.name}.${key}`
+ `${service.name}.${key}`,
);
methodMap[key] = fn;
});
@@ -112,47 +118,51 @@ export class Device {
private createMethodWrapper(
realMethod: MethodStub,
methodName: string,
- fullMethodPath: string) {
+ fullMethodPath: string,
+ ) {
if (realMethod instanceof UnaryMethodStub) {
return this.createUnaryMethodWrapper(
realMethod,
methodName,
- fullMethodPath);
- }
- else if (realMethod instanceof ServerStreamingMethodStub) {
+ fullMethodPath,
+ );
+ } else if (realMethod instanceof ServerStreamingMethodStub) {
return this.createServerStreamingMethodWrapper(
realMethod,
methodName,
- fullMethodPath);
+ fullMethodPath,
+ );
}
+ throw new Error(`Unknown method: ${realMethod}`);
}
private createUnaryMethodWrapper(
realMethod: UnaryMethodStub,
methodName: string,
- fullMethodPath: string) {
- const requestType =
- realMethod.method.descriptor.getInputType().replace(/^\./, '');
+ fullMethodPath: string,
+ ) {
+ const requestType = realMethod.method.descriptor
+ .getInputType()
+ .replace(/^\./, '');
const requestProtoDescriptor =
- this.protoCollection.getDescriptorProto(requestType);
- const requestFields = requestProtoDescriptor.getFieldList();
+ this.protoCollection.getDescriptorProto(requestType)!;
+ const requestFields = requestProtoDescriptor.getFieldList()!;
const functionArguments = requestFields
- .map(field => field.getName())
- .concat(
- 'return this(arguments);'
- );
+ .map((field) => field.getName())
+ .concat('return this(arguments);');
// We store field names so REPL can show hints in autocomplete using these.
- this.nameToMethodArgumentsMap[fullMethodPath] = requestFields
- .map(field => field.getName());
+ this.nameToMethodArgumentsMap[fullMethodPath] = requestFields.map((field) =>
+ field.getName(),
+ );
// We create a new JS function dynamically here that takes
// proto message fields as arguments and calls the actual RPC method.
- let fn = new Function(...functionArguments).bind((args) => {
+ const fn = new Function(...functionArguments).bind((args: any[]) => {
const request = new realMethod.method.requestType();
requestFields.forEach((field, index) => {
request[`set${titleCase(field.getName())}`](args[index]);
- })
+ });
return realMethod.call(request);
});
return fn;
@@ -161,36 +171,40 @@ export class Device {
private createServerStreamingMethodWrapper(
realMethod: ServerStreamingMethodStub,
methodName: string,
- fullMethodPath: string) {
- const requestType = realMethod.method.descriptor.getInputType().replace(/^\./, '');
+ fullMethodPath: string,
+ ) {
+ const requestType = realMethod.method.descriptor
+ .getInputType()
+ .replace(/^\./, '');
const requestProtoDescriptor =
- this.protoCollection.getDescriptorProto(requestType);
+ this.protoCollection.getDescriptorProto(requestType)!;
const requestFields = requestProtoDescriptor.getFieldList();
const functionArguments = requestFields
- .map(field => field.getName())
- .concat(
- [
- 'onNext',
- 'onComplete',
- 'onError',
- 'return this(arguments);'
- ]
- );
+ .map((field) => field.getName())
+ .concat(['onNext', 'onComplete', 'onError', 'return this(arguments);']);
// We store field names so REPL can show hints in autocomplete using these.
- this.nameToMethodArgumentsMap[fullMethodPath] = requestFields
- .map(field => field.getName());
+ this.nameToMethodArgumentsMap[fullMethodPath] = requestFields.map((field) =>
+ field.getName(),
+ );
// We create a new JS function dynamically here that takes
// proto message fields as arguments and calls the actual RPC method.
- let fn = new Function(...functionArguments).bind((args) => {
+ const fn = new Function(...functionArguments).bind((args: any[]) => {
const request = new realMethod.method.requestType();
requestFields.forEach((field, index) => {
request[`set${protoFieldToMethodName(field.getName())}`](args[index]);
- })
+ });
const callbacks = Array.from(args).slice(requestFields.length);
- // @ts-ignore
- return realMethod.invoke(request, callbacks[0], callbacks[1], callbacks[2]);
+ return realMethod.invoke(
+ request,
+ // @ts-ignore
+ callbacks[0],
+ // @ts-ignore
+ callbacks[1],
+ // @ts-ignore
+ callbacks[2],
+ );
});
return fn;
}
diff --git a/ts/device/index_test.ts b/ts/device/index_test.ts
index 3a383bfc3..6316c5dcf 100644
--- a/ts/device/index_test.ts
+++ b/ts/device/index_test.ts
@@ -13,18 +13,19 @@
// the License.
/* eslint-env browser */
-import {SerialMock} from '../transport/serial_mock';
-import {Device} from "./"
-import {ProtoCollection} from 'pigweedjs/protos/collection';
-import {WebSerialTransport} from '../transport/web_serial_transport';
-import {Serial} from 'pigweedjs/types/serial';
-import {Message} from 'google-protobuf';
-import {RpcPacket, PacketType} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
-import {Method, ServerStreamingMethodStub} from 'pigweedjs/pw_rpc';
-import {Status} from 'pigweedjs/pw_status';
+import { SerialMock } from '../transport/serial_mock';
+import { Device } from './';
+import { ProtoCollection } from 'pigweedjs/protos/collection';
+import { WebSerialTransport } from '../transport/web_serial_transport';
+import { Serial } from 'pigweedjs/types/serial';
+import { Message } from 'google-protobuf';
import {
- Response,
-} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
+ RpcPacket,
+ PacketType,
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import { Method, ServerStreamingMethodStub } from 'pigweedjs/pw_rpc';
+import { Status } from 'pigweedjs/pw_status';
+import { Response } from 'pigweedjs/protos/pw_rpc/ts/test_pb';
describe('WebSerialTransport', () => {
let device: Device;
@@ -40,7 +41,7 @@ describe('WebSerialTransport', () => {
channelId: number,
method: Method,
status: Status,
- response?: Message
+ response?: Message,
) {
const packet = new RpcPacket();
packet.setType(PacketType.RESPONSE);
@@ -49,7 +50,7 @@ describe('WebSerialTransport', () => {
packet.setMethodId(method.id);
packet.setStatus(status);
if (response === undefined) {
- packet.setPayload(new Uint8Array());
+ packet.setPayload(new Uint8Array(0));
} else {
packet.setPayload(response.serializeBinary());
}
@@ -60,7 +61,7 @@ describe('WebSerialTransport', () => {
channelId: number,
method: Method,
response: Message,
- status: Status = Status.OK
+ status: Status = Status.OK,
) {
const packet = new RpcPacket();
packet.setType(PacketType.SERVER_STREAM);
@@ -74,7 +75,10 @@ describe('WebSerialTransport', () => {
beforeEach(() => {
serialMock = new SerialMock();
- device = new Device(new ProtoCollection(), new WebSerialTransport(serialMock as Serial));
+ device = new Device(
+ new ProtoCollection(),
+ new WebSerialTransport(serialMock as Serial),
+ );
});
it('has rpcs defined', () => {
@@ -83,21 +87,29 @@ describe('WebSerialTransport', () => {
});
it('has method arguments data', () => {
- expect(device.getMethodArguments("pw.rpc.EchoService.Echo")).toStrictEqual(["msg"]);
- expect(device.getMethodArguments("pw.test2.Alpha.Unary")).toStrictEqual(['magic_number']);
+ expect(device.getMethodArguments('pw.rpc.EchoService.Echo')).toStrictEqual([
+ 'msg',
+ ]);
+ expect(device.getMethodArguments('pw.test2.Alpha.Unary')).toStrictEqual([
+ 'magic_number',
+ ]);
});
it('unary rpc sends request to serial', async () => {
+ // prettier-ignore
const helloResponse = new Uint8Array([
126, 165, 3, 42, 7, 10, 5, 104,
101, 108, 108, 111, 8, 1, 16, 1,
29, 82, 208, 251, 20, 37, 233, 14,
- 71, 139, 109, 127, 108, 165, 126]);
+ 71, 139, 109, 127, 108, 165, 126,
+ ]);
await device.connect();
serialMock.dataFromDevice(helloResponse);
- const [status, response] = await device.rpcs.pw.rpc.EchoService.Echo("hello");
- expect(response.getMsg()).toBe("hello");
+ const [status, response] = await device.rpcs.pw.rpc.EchoService.Echo(
+ 'hello',
+ );
+ expect(response.getMsg()).toBe('hello');
expect(status).toBe(0);
});
@@ -108,16 +120,27 @@ describe('WebSerialTransport', () => {
const serverStreaming = device.client
.channel()
?.methodStub(
- 'pw.rpc.test1.TheTestService.SomeServerStreaming'
- )! as ServerStreamingMethodStub;
+ 'pw.rpc.test1.TheTestService.SomeServerStreaming',
+ ) as ServerStreamingMethodStub;
const onNext = jest.fn();
const onCompleted = jest.fn();
const onError = jest.fn();
- device.rpcs.pw.rpc.test1.TheTestService.SomeServerStreaming(4, onNext, onCompleted, onError);
- device.client.processPacket(generateStreamingPacket(1, serverStreaming.method, response1));
- device.client.processPacket(generateStreamingPacket(1, serverStreaming.method, response2));
- device.client.processPacket(generateResponsePacket(1, serverStreaming.method, Status.ABORTED));
+ device.rpcs.pw.rpc.test1.TheTestService.SomeServerStreaming(
+ 4,
+ onNext,
+ onCompleted,
+ onError,
+ );
+ device.client.processPacket(
+ generateStreamingPacket(1, serverStreaming.method, response1),
+ );
+ device.client.processPacket(
+ generateStreamingPacket(1, serverStreaming.method, response2),
+ );
+ device.client.processPacket(
+ generateResponsePacket(1, serverStreaming.method, Status.ABORTED),
+ );
expect(onNext).toBeCalledWith(response1);
expect(onNext).toBeCalledWith(response2);
diff --git a/ts/device/object_set.ts b/ts/device/object_set.ts
new file mode 100644
index 000000000..7bc5f1d91
--- /dev/null
+++ b/ts/device/object_set.ts
@@ -0,0 +1,85 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+type ObjectType = {
+ [index: number | string]: any;
+};
+
+function hasOwnProperty(obj: ObjectType, prop: number | string) {
+ if (obj == null) {
+ return false;
+ }
+ //to handle objects with null prototypes (too edge case?)
+ return Object.prototype.hasOwnProperty.call(obj, prop);
+}
+function hasShallowProperty(obj: ObjectType, prop: number | string) {
+ return (
+ (typeof prop === 'number' && Array.isArray(obj)) ||
+ hasOwnProperty(obj, prop)
+ );
+}
+
+function getShallowProperty(obj: ObjectType, prop: number | string) {
+ if (hasShallowProperty(obj, prop)) {
+ return obj[prop];
+ }
+}
+function getKey(key: string) {
+ const intKey = parseInt(key);
+ if (intKey.toString() === key) {
+ return intKey;
+ }
+ return key;
+}
+
+export function setPathOnObject(
+ obj: ObjectType,
+ path: number | string | Array<number | string>,
+ value: any,
+ doNotReplace: boolean = false,
+) {
+ if (typeof path === 'number') {
+ path = [path];
+ }
+ if (!path || path.length === 0) {
+ return obj;
+ }
+ if (typeof path === 'string') {
+ return setPathOnObject(
+ obj,
+ path.split('.').map(getKey),
+ value,
+ doNotReplace,
+ );
+ }
+ const currentPath = path[0];
+ const currentValue = getShallowProperty(obj, currentPath);
+ if (path.length === 1) {
+ if (currentValue === void 0 || !doNotReplace) {
+ obj[currentPath] = value;
+ }
+ return currentValue;
+ }
+
+ if (currentValue === void 0) {
+ //check if we assume an array
+ if (typeof path[1] === 'number') {
+ obj[currentPath] = [];
+ } else {
+ obj[currentPath] = {};
+ }
+ }
+
+ return setPathOnObject(obj[currentPath], path.slice(1), value, doNotReplace);
+}
diff --git a/ts/index.ts b/ts/index.ts
index 9b0b02dc7..8a1decd26 100644
--- a/ts/index.ts
+++ b/ts/index.ts
@@ -12,11 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-export * as pw_hdlc from "../pw_hdlc/ts";
-export * as pw_rpc from "../pw_rpc/ts";
-export * as pw_status from "../pw_status/ts";
-export * as pw_tokenizer from "../pw_tokenizer/ts";
-export * as pw_protobuf_compiler from "../pw_protobuf_compiler/ts";
-export * as pw_transfer from "../pw_transfer/ts";
-export * as WebSerial from "./transport/web_serial_transport";
-export {Device} from "./device";
+export * as pw_hdlc from '../pw_hdlc/ts';
+export * as pw_rpc from '../pw_rpc/ts';
+export * as pw_status from '../pw_status/ts';
+export * as pw_tokenizer from '../pw_tokenizer/ts';
+export * as pw_protobuf_compiler from '../pw_protobuf_compiler/ts';
+export * as pw_transfer from '../pw_transfer/ts';
+export * as WebSerial from './transport/web_serial_transport';
+export { Device } from './device';
diff --git a/ts/index_test.ts b/ts/index_test.ts
index 9019842b2..66e024bc1 100644
--- a/ts/index_test.ts
+++ b/ts/index_test.ts
@@ -22,14 +22,18 @@ import {
pw_rpc,
pw_tokenizer,
pw_transfer,
- WebSerial
-} from "../dist/index.umd";
+ WebSerial,
+} from '../dist/index.umd';
-import {ProtoCollection} from "../dist/protos/collection.umd";
-import * as fs from "fs";
+import { ProtoCollection } from '../dist/protos/collection.umd';
+import {
+ createLogViewer,
+ MockLogSource,
+ PigweedRPCLogSource,
+} from '../dist/logging.umd';
+import * as fs from 'fs';
describe('Pigweed Bundle', () => {
-
it('proto collection has file list', () => {
const protoCollection = new ProtoCollection();
const fd = protoCollection.fileDescriptorSet.getFileList();
@@ -67,10 +71,26 @@ describe('Pigweed Bundle', () => {
expect(WebSerial.WebSerialTransport).toBeDefined();
});
+ it('has ProgressStats defined', () => {
+ expect(pw_transfer.ProgressStats).toBeDefined();
+ });
+
+ it('has log viewer exports defined', () => {
+ expect(createLogViewer).toBeDefined();
+ expect(typeof createLogViewer).toBe('function');
+
+ expect(MockLogSource).toBeDefined();
+ expect(typeof MockLogSource).toBe('function');
+ expect(MockLogSource.name).toBe('MockLogSource');
+
+ expect(PigweedRPCLogSource).toBeDefined();
+ expect(typeof PigweedRPCLogSource).toBe('function');
+ expect(PigweedRPCLogSource.name).toBe('PigweedRPCLogSource');
+ });
+
it('is not referring to any outside Pigweed modules', () => {
const requireString = "require('pigweedjs";
- const file = fs.readFileSync(require.resolve("../dist/index.umd"));
- expect(file.indexOf(requireString)).toBe(-1)
+ const file = fs.readFileSync(require.resolve('../dist/index.umd'));
+ expect(file.indexOf(requireString)).toBe(-1);
});
-
});
diff --git a/ts/logging.ts b/ts/logging.ts
new file mode 100644
index 000000000..c56a63783
--- /dev/null
+++ b/ts/logging.ts
@@ -0,0 +1,26 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import '../pw_web/log-viewer/src/index.css';
+
+export { LogSource } from '../pw_web/log-viewer/src/log-source';
+export { MockLogSource } from '../pw_web/log-viewer/src/custom/mock-log-source';
+export { LogViewer } from '../pw_web/log-viewer/src/components/log-viewer';
+export { createLogViewer } from '../pw_web/log-viewer/src/createLogViewer';
+export {
+ LogEntry,
+ FieldData,
+ Severity,
+} from '../pw_web/log-viewer/src/shared/interfaces';
+export { PigweedRPCLogSource } from './logging_source_rpc';
diff --git a/ts/logging_source_rpc.ts b/ts/logging_source_rpc.ts
new file mode 100644
index 000000000..4cf55ff7d
--- /dev/null
+++ b/ts/logging_source_rpc.ts
@@ -0,0 +1,83 @@
+// Copyright 2023 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import { Detokenizer } from '../pw_tokenizer/ts';
+import { LogSource } from '../pw_web/log-viewer/src/log-source';
+import { Device } from './device';
+import { LogEntry } from './logging';
+
+export class PigweedRPCLogSource extends LogSource {
+ private detokenizer: Detokenizer | undefined;
+ private logs: LogEntry[] = [];
+ private call: any;
+ constructor(device: Device, tokenDB: string | undefined) {
+ super();
+ if (tokenDB && tokenDB.length > 0) {
+ this.detokenizer = new Detokenizer(tokenDB);
+ }
+ this.call = device.rpcs.pw.log.Logs.Listen((msg: any) => {
+ msg
+ .getEntriesList()
+ .forEach((entry: any) => this.processFrame(entry.getMessage()));
+ });
+ }
+
+ destroy() {
+ this.call.cancel();
+ }
+
+ private processFrame(frame: Uint8Array) {
+ let entry: LogEntry;
+ if (this.detokenizer) {
+ const detokenized = this.detokenizer.detokenizeUint8Array(frame);
+ entry = this.parseLogMsg(detokenized);
+ } else {
+ const decoded = new TextDecoder().decode(frame);
+ entry = this.parseLogMsg(decoded);
+ }
+ this.logs = [...this.logs, entry];
+ this.emitEvent('logEntry', entry);
+ }
+
+ private parseLogMsg(msg: string): LogEntry {
+ const pairs = msg
+ .split('■')
+ .slice(1)
+ .map((pair) => pair.split('♦'));
+
+ // Not a valid message, print as-is.
+ const timestamp = new Date();
+ if (pairs.length === 0) {
+ return {
+ fields: [
+ { key: 'timestamp', value: timestamp.toISOString() },
+ { key: 'message', value: msg },
+ ],
+ timestamp: timestamp,
+ };
+ }
+
+ const map: any = {};
+ pairs.forEach((pair) => (map[pair[0]] = pair[1]));
+ return {
+ fields: [
+ { key: 'timestamp', value: timestamp },
+ { key: 'message', value: map.msg },
+ { key: 'module', value: map.module },
+ { key: 'file', value: map.file },
+ ],
+ timestamp: timestamp,
+ };
+ }
+}
diff --git a/ts/transport/device_transport.ts b/ts/transport/device_transport.ts
index ee2df8720..dbe3410b4 100644
--- a/ts/transport/device_transport.ts
+++ b/ts/transport/device_transport.ts
@@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {BehaviorSubject, Observable} from 'rxjs';
+import { BehaviorSubject, Observable } from 'rxjs';
export default interface DeviceTransport {
chunks: Observable<Uint8Array>;
diff --git a/ts/transport/serial_mock.ts b/ts/transport/serial_mock.ts
index 8fb48c4da..8c64867c0 100644
--- a/ts/transport/serial_mock.ts
+++ b/ts/transport/serial_mock.ts
@@ -13,8 +13,14 @@
// the License.
/* eslint-env browser */
-import {Subject} from 'rxjs';
-import type {SerialConnectionEvent, SerialPort, Serial, SerialPortRequestOptions, SerialOptions} from "pigweedjs/types/serial"
+import { Subject } from 'rxjs';
+import type {
+ SerialConnectionEvent,
+ SerialPort,
+ Serial,
+ SerialPortRequestOptions,
+ SerialOptions,
+} from 'pigweedjs/types/serial';
/**
* AsyncQueue is a queue that allows values to be dequeued
* before they are enqueued, returning a promise that resolves
@@ -46,7 +52,7 @@ class AsyncQueue<T> {
if (val !== undefined) {
return val;
} else {
- const queuePromise = new Promise<T>(resolve => {
+ const queuePromise = new Promise<T>((resolve) => {
this.requestQueue.push(resolve);
});
return queuePromise;
@@ -71,14 +77,14 @@ class SerialPortMock implements SerialPort {
* @param {Uint8Array} data
*/
dataFromDevice(data: Uint8Array) {
- this.deviceData.enqueue({data});
+ this.deviceData.enqueue({ data });
}
/**
* Simulate the device closing the connection with the browser.
*/
closeFromDevice() {
- this.deviceData.enqueue({done: true});
+ this.deviceData.enqueue({ done: true });
}
/**
@@ -86,7 +92,7 @@ class SerialPortMock implements SerialPort {
* @param {Error} error
*/
errorFromDevice(error: Error) {
- this.deviceData.enqueue({error});
+ this.deviceData.enqueue({ error });
}
/**
@@ -98,8 +104,8 @@ class SerialPortMock implements SerialPort {
* The ReadableStream of bytes from the device.
*/
readable = new ReadableStream<Uint8Array>({
- pull: async controller => {
- const {data, done, error} = await this.deviceData.dequeue();
+ pull: async (controller) => {
+ const { data, done, error } = await this.deviceData.dequeue();
if (done) {
controller.close();
return;
@@ -117,7 +123,7 @@ class SerialPortMock implements SerialPort {
* The WritableStream of bytes to the device.
*/
writable = new WritableStream<Uint8Array>({
- write: chunk => {
+ write: (chunk) => {
this.dataToDevice.next(chunk);
},
});
@@ -125,12 +131,16 @@ class SerialPortMock implements SerialPort {
/**
* A spy for opening the serial port.
*/
- open = jest.fn(async (options?: SerialOptions) => { });
+ open = jest.fn(async (options?: SerialOptions) => {
+ // Do nothing.
+ });
/**
* A spy for closing the serial port.
*/
- close = jest.fn(() => { });
+ close = jest.fn(() => {
+ // Do nothing.
+ });
}
export class SerialMock implements Serial {
@@ -171,13 +181,13 @@ export class SerialMock implements Serial {
addEventListener(
type: 'connect' | 'disconnect',
listener: (this: this, ev: SerialConnectionEvent) => any,
- useCapture?: boolean
+ useCapture?: boolean,
): void;
addEventListener(
type: string,
listener: EventListener | EventListenerObject | null,
- options?: boolean | AddEventListenerOptions
+ options?: boolean | AddEventListenerOptions,
): void;
addEventListener(type: any, listener: any, options?: any) {
@@ -187,13 +197,13 @@ export class SerialMock implements Serial {
removeEventListener(
type: 'connect' | 'disconnect',
callback: (this: this, ev: SerialConnectionEvent) => any,
- useCapture?: boolean
+ useCapture?: boolean,
): void;
removeEventListener(
type: string,
callback: EventListener | EventListenerObject | null,
- options?: boolean | EventListenerOptions
+ options?: boolean | EventListenerOptions,
): void;
removeEventListener(type: any, callback: any, options?: any) {
diff --git a/ts/transport/web_serial_transport.ts b/ts/transport/web_serial_transport.ts
index 17edc1d3a..f4f918072 100644
--- a/ts/transport/web_serial_transport.ts
+++ b/ts/transport/web_serial_transport.ts
@@ -13,11 +13,17 @@
// the License.
/* eslint-env browser */
-import {BehaviorSubject, Observable, Subject, Subscription} from 'rxjs';
+import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import DeviceTransport from './device_transport';
-import type {SerialPort, Serial, SerialOptions, Navigator, SerialPortFilter} from "pigweedjs/types/serial"
-
-const DEFAULT_SERIAL_OPTIONS: SerialOptions & {baudRate: number} = {
+import type {
+ SerialPort,
+ Serial,
+ SerialOptions,
+ Navigator,
+ SerialPortFilter,
+} from '../types/serial';
+
+const DEFAULT_SERIAL_OPTIONS: SerialOptions & { baudRate: number } = {
// Some versions of chrome use `baudrate` (linux)
baudrate: 115200,
// Some versions use `baudRate` (chromebook)
@@ -37,11 +43,11 @@ interface PortConnection extends PortReadConnection {
}
export class DeviceLostError extends Error {
- message = 'The device has been lost';
+ override message = 'The device has been lost';
}
export class DeviceLockedError extends Error {
- message =
+ override message =
"The device's port is locked. Try unplugging it" +
' and plugging it back in.';
}
@@ -57,12 +63,14 @@ export class WebSerialTransport implements DeviceTransport {
private portConnections: Map<SerialPort, PortConnection> = new Map();
private activePortConnectionConnection: PortConnection | undefined;
private rxSubscriptions: Subscription[] = [];
+ private writer: WritableStreamDefaultWriter<Uint8Array> | undefined;
+ private abortController: AbortController | undefined;
constructor(
private serial: Serial = (navigator as unknown as Navigator).serial,
private filters: SerialPortFilter[] = [],
- private serialOptions = DEFAULT_SERIAL_OPTIONS
- ) { }
+ private serialOptions = DEFAULT_SERIAL_OPTIONS,
+ ) {}
/**
* Send a UInt8Array chunk of data to the connected device.
@@ -81,17 +89,25 @@ export class WebSerialTransport implements DeviceTransport {
* be called in response to user interaction.
*/
async connect(): Promise<void> {
- const port = await this.serial.requestPort({filters: this.filters});
+ const port = await this.serial.requestPort({ filters: this.filters });
await this.connectPort(port);
}
- private disconnect() {
+ async disconnect() {
for (const subscription of this.rxSubscriptions) {
subscription.unsubscribe();
}
this.rxSubscriptions = [];
this.activePortConnectionConnection = undefined;
+ this.portConnections.clear();
+ this.abortController?.abort();
+
+ try {
+ await this.writer?.close();
+ } catch (err) {
+ this.errors.next(err as Error);
+ }
this.connected.next(false);
}
@@ -100,10 +116,8 @@ export class WebSerialTransport implements DeviceTransport {
* and can be called whenever a port is available.
*/
async connectPort(port: SerialPort): Promise<void> {
- this.disconnect();
-
this.activePortConnectionConnection =
- this.portConnections.get(port) ?? (await this.conectNewPort(port));
+ this.portConnections.get(port) ?? (await this.connectNewPort(port));
this.connected.next(true);
@@ -120,8 +134,8 @@ export class WebSerialTransport implements DeviceTransport {
this.portConnections.delete(port);
// Don't complete the chunks observable because then it would not
// be able to forward any future chunks.
- }
- )
+ },
+ ),
);
this.rxSubscriptions.push(
@@ -131,22 +145,23 @@ export class WebSerialTransport implements DeviceTransport {
// The device has been lost
this.connected.next(false);
}
- })
+ }),
);
}
- private async conectNewPort(port: SerialPort): Promise<PortConnection> {
+ private async connectNewPort(port: SerialPort): Promise<PortConnection> {
await port.open(this.serialOptions);
const writer = port.writable.getWriter();
+ this.writer = writer;
async function sendChunk(chunk: Uint8Array) {
await writer.ready;
await writer.write(chunk);
}
- const {chunks, errors} = this.getChunks(port);
+ const { chunks, errors } = this.getChunks(port);
- const connection: PortConnection = {sendChunk, chunks, errors};
+ const connection: PortConnection = { sendChunk, chunks, errors };
this.portConnections.set(port, connection);
return connection;
}
@@ -154,6 +169,8 @@ export class WebSerialTransport implements DeviceTransport {
private getChunks(port: SerialPort): PortReadConnection {
const chunks = new Subject<Uint8Array>();
const errors = new Subject<Error>();
+ const abortController = new AbortController();
+ this.abortController = abortController;
async function read() {
if (!port.readable) {
@@ -164,23 +181,20 @@ export class WebSerialTransport implements DeviceTransport {
}
await port.readable.pipeTo(
new WritableStream({
- write: chunk => {
+ write: (chunk) => {
chunks.next(chunk);
},
close: () => {
chunks.complete();
errors.complete();
},
- abort: () => {
- // Reconnect to the port.
- connect();
- },
- })
+ }),
+ { signal: abortController.signal },
);
}
function connect() {
- read().catch(err => {
+ read().catch((err) => {
// Don't error the chunks observable since that stops it from
// reading any more packets, and we often want to continue
// despite an error. Instead, push errors to the 'errors'
@@ -191,6 +205,6 @@ export class WebSerialTransport implements DeviceTransport {
connect();
- return {chunks, errors};
+ return { chunks, errors };
}
}
diff --git a/ts/transport/web_serial_transport_test.ts b/ts/transport/web_serial_transport_test.ts
index 2ccbd10d6..05d102342 100644
--- a/ts/transport/web_serial_transport_test.ts
+++ b/ts/transport/web_serial_transport_test.ts
@@ -13,11 +13,11 @@
// the License.
/* eslint-env browser */
-import {last, take} from 'rxjs/operators';
+import { last, take } from 'rxjs/operators';
-import {SerialMock} from './serial_mock';
-import {WebSerialTransport, DeviceLockedError} from './web_serial_transport';
-import type {Serial} from "pigweedjs/types/serial"
+import { SerialMock } from './serial_mock';
+import { WebSerialTransport, DeviceLockedError } from './web_serial_transport';
+import type { Serial } from 'pigweedjs/types/serial';
describe('WebSerialTransport', () => {
let serialMock: SerialMock;
@@ -69,7 +69,7 @@ describe('WebSerialTransport', () => {
const dataToDevice = serialMock.dataToDevice.pipe(take(1)).toPromise();
let writtenData: Uint8Array | undefined = undefined;
- dataToDevice.then(data => {
+ dataToDevice.then((data) => {
writtenData = data;
});
@@ -104,7 +104,7 @@ describe('WebSerialTransport', () => {
await transport.connect();
const reportedErrorPromise = transport.errors.pipe(take(1)).toPromise();
- serialMock.serialPort.errorFromDevice(new Error());
+ serialMock.serialPort.errorFromDevice(new DeviceLockedError());
expect(await reportedErrorPromise).toEqual(new DeviceLockedError());
});
diff --git a/ts/types/serial.d.ts b/ts/types/serial.d.ts
index 0e4142c6e..aa99b0e55 100644
--- a/ts/types/serial.d.ts
+++ b/ts/types/serial.d.ts
@@ -14,9 +14,8 @@
* the License.
*/
-
/** @see https://wicg.github.io/serial/#paritytype-enum */
-type ParityType = 'none'|'even'|'odd';
+type ParityType = 'none' | 'even' | 'odd';
/** @see https://wicg.github.io/serial/#serialoptions-dictionary */
interface SerialOptions {
@@ -69,26 +68,30 @@ declare class SerialConnectionEvent extends Event {
/** @see https://wicg.github.io/serial/#serial-interface */
declare class Serial extends EventTarget {
- onconnect(): ((this: this, ev: SerialConnectionEvent) => any)|null;
- ondisconnect(): ((this: this, ev: SerialConnectionEvent) => any)|null;
+ onconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null;
+ ondisconnect(): ((this: this, ev: SerialConnectionEvent) => any) | null;
getPorts(): Promise<SerialPort[]>;
requestPort(options?: SerialPortRequestOptions): Promise<SerialPort>;
addEventListener(
- type: 'connect'|'disconnect',
- listener: (this: this, ev: SerialConnectionEvent) => any,
- useCapture?: boolean): void;
+ type: 'connect' | 'disconnect',
+ listener: (this: this, ev: SerialConnectionEvent) => any,
+ useCapture?: boolean,
+ ): void;
addEventListener(
- type: string,
- listener: EventListenerOrEventListenerObject|null,
- options?: boolean|AddEventListenerOptions): void;
+ type: string,
+ listener: EventListenerOrEventListenerObject | null,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
removeEventListener(
- type: 'connect'|'disconnect',
- callback: (this: this, ev: SerialConnectionEvent) => any,
- useCapture?: boolean): void;
+ type: 'connect' | 'disconnect',
+ callback: (this: this, ev: SerialConnectionEvent) => any,
+ useCapture?: boolean,
+ ): void;
removeEventListener(
- type: string,
- callback: EventListenerOrEventListenerObject|null,
- options?: EventListenerOptions|boolean): void;
+ type: string,
+ callback: EventListenerOrEventListenerObject | null,
+ options?: EventListenerOptions | boolean,
+ ): void;
}
/** @see https://wicg.github.io/serial/#extensions-to-the-navigator-interface */
@@ -103,4 +106,12 @@ interface WorkerNavigator {
readonly serial: Serial;
}
-export type {Navigator, SerialPortFilter, Serial, SerialOptions, SerialConnectionEvent, SerialPortRequestOptions, SerialPort}
+export type {
+ Navigator,
+ SerialPortFilter,
+ Serial,
+ SerialOptions,
+ SerialConnectionEvent,
+ SerialPortRequestOptions,
+ SerialPort,
+};
diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt
new file mode 100644
index 000000000..4097e6e21
--- /dev/null
+++ b/zephyr/CMakeLists.txt
@@ -0,0 +1,59 @@
+# Copyright 2023 The Pigweed Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not
+# use this file except in compliance with the License. You may obtain a copy of
+# the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations under
+# the License.
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Add library linkage for Zephyr
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_BASE64 pw_base64)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_BYTES pw_bytes)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_CHECKSUM pw_checksum)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_CONTAINERS pw_containers)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_DIGITAL_IO pw_digital_io)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_FUNCTION pw_function)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_HDLC_RPC pw_hdlc.pw_rpc pw_hdlc.encoder pw_hdlc.decoder )
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_MULTISINK pw_multisink)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_MULTISINK_UTIL pw_multisink.util)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_PREPROCESSOR pw_preprocessor)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_POLYFILL pw_polyfill)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RESULT pw_result)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_ROUTER_EGRESS pw_router.egress)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_ROUTER_EGRESS_FUNCTION pw_router.egress_function)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_ROUTER_PACKET_PARSER pw_router.packet_parser)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_ROUTER_STATIC_ROUTER pw_router.static_router)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_CLIENT pw_rpc.client)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_CLIENT_SERVER pw_rpc.client_server)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_COMMON pw_rpc.common)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_SERVER pw_rpc.server)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_NANOPB_CLIENT pw_rpc.nanopb.client_api)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_NANOPB_COMMON pw_rpc.nanopb.common)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_NANOPB_ECHO_SERVICE pw_rpc.nanopb.echo_service)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_NANOPB_METHOD pw_rpc.nanopb.method)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_RPC_NANOPB_METHOD_UNION pw_rpc.nanopb.method_union)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_SPAN pw_span)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_STATUS pw_status)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_STREAM pw_stream)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_STRING pw_string)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_SYS_IO pw_sys_io)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_TOKENIZER pw_tokenizer)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_TOKENIZER_BASE64 pw_tokenizer.base64)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_DETOKENIZER pw_tokenizer.decoder)
+pw_zephyrize_libraries_ifdef(CONFIG_PIGWEED_VARINT pw_varint)
+
+# Set Pigweed configs from Kconfig
+pw_set_config_from_zephyr(CONFIG_PIGWEED_MULTISINK_LOCK_INTERRUPT_SAFE PW_MULTISINK_CONFIG_LOCK_INTERRUPT_SAFE)
+
+# Add test libraries
+if(CONFIG_TEST)
+target_link_libraries_ifdef(CONFIG_PIGWEED_BASE64 app PRIVATE pw_base64.base64_test.lib)
+endif(CONFIG_TEST)